vlambda博客
学习文章列表

Vue卡片市场方案(二)

    之前我们采用Vue3-sfc-loader组件实现了一种。但是前述方案有几个待优化点:

  1. 渲染时,如果卡片过多,需要加载很多的vue文件,对于资源消耗比较大

  2. Vue文件(代码)直接明文暴露给前端,可能会有风险

  3. 渲染过程可能会比较耗时
    针对上述几个问题,我们考虑基于vue3-sfc-loader组件,进行改造。实现在卡片上传时进行预编译(减轻渲染时的压力),生成json,存储于服务器上;前端渲染时服务器直接下发json字符串,前端直接加载预编译好的代码。下面将从代码编译流程以及优化方案两个方面着手介绍。

一、编译Vue3-sfc-loader组件

要对代码进行改造,首先要能够正确编译该开源组件,下面将介绍整个编译流程。

1.1 编译环境

    对于编译环境,可以在ubuntu上进行也可以在windows上,node版本要高于12。本文中使用的是Ubuntu 20.04.3, node版本使用的是16.x

对于在ubuntu上安装升级node版本,可以参考使用如下命令


# 默认下载的nodejs版本可能比较低
sudo apt install nodejs
# 升级nodejs
curl -sL https://deb.nodesource.com/setup_16.x | bash -
apt-get install -y nodejs

1.2 编译流程:

# 1. clone 代码
git clone https://github.com/FranckFreiburger/vue3-sfc-loader.git

#
2.如果没有yarn,要先安装yarn,如果有了则略过
npm install --global yarn
# 如果npm比较慢,可以使用cnpm替换
npm install -g cnpm --registry=https://registry.npmmirror.com

#
3. 执行yarn install,安装依赖
yarn install

#
4.执行编译
npm run build
# 如果执行过程中内存溢出,可以通过max_old_space_size指定使用的内存大小
npm run build --max_old_space_size=8192

#
5.如果没有什么问题,dist/目录下应该就已经有了编译完的产物了

1.3 测试代码

    以Vue2项目的测试为例:项目有个test目录,里边有Vue2.html文件,同时在项目根目录下package.json中的scripts内,有个testVue2标签,直接运行该标签即可(或者可以使用npm run testVue2)。
    另外为了能够测试自己编译生成的依赖,还需要修改Vue2.html文件里的<script>标签内容

// 使vue2-sf-loader从cdn加载换成自己编译好的产物
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue2-sfc-loader.js"></script>

//可以使用express启动一个服务器,指定dist/目录为根目录,提供vue2-sfc-loader的下载,防止出现跨域等问题
// 参考如下命令:
// 1. cd dist/
// 2. npm install express
// 3. node -e "require('express')().use(require('express').static(__dirname, {index:'index.html'})).listen(3333)"

<script src="http://localhost:3333/vue2-sfc-loader.js"></script>

1.4 注意事项:

  1. 第3步中如果出现node-sass相关的错误,可以试试使用 yarn add node-sass单独下载依赖

  2. 第4步中执行编译的的命令参考package.json中scripts内容,之所以使用npm run build命令,是因为可以指定编译过程中可用的内存大小

  3. 如果出现如下错误:BrowserslistError: Unknown browser query dead,可以在package.json的browserlist字段里去掉not dead

二、卡片市场方案

    方案主要实现的点在于,卡片在注册上传时对其进行预编译,生成json串。这样做有几点好处:

  1. 可以缩减卡片加载的时间,减轻前端压力;

  2. 可以起到一定的混淆作用;

  3. json串也利于后端存储

2.1 解析SFC转成json

    经过了解webpack的原理我们知道:webpack在编译组件时,先使用@vue/component-compiler-utils,将Vue文件parse成SFCDescriptor,进而对descriptor里的template、script、style等多个部分进行处理,最终生成js文件。vue3-sfc-loader原理与之类似,以createVue2SFCModule.ts为例。

// 1. 将vue文件parse成SFCDescriptor
const descriptor = sfc_parse({
source,
filename: strFilename,
needMap: genSourcemap,
compiler: vueTemplateCompiler as VueTemplateCompiler}
);

// 2. parse descriptor的script部分生成编译后的代码
if ( descriptor.script ) {

...
// transformedScriptSource即为解析后的script代码
const [ depsList, transformedScriptSource ] = await withCache(compiledCache, [ componentHash, src, additionalBabelParserPlugins, Object.keys(additionalBabelPlugins) ], async ({ preventCache }) => {
...

return await transformJSCode(src, true, strFilename, [ ...additionalBabelParserPlugins, 'jsx' ], { ...additionalBabelPlugins, jsx, babelSugarInjectH }, log);
});
}

// 3. parse descriptor.template部分
if ( descriptor.template !== null ) {

// templateTransformedSource即为解析后的template代码
const [ templateDepsList, templateTransformedSource ] = await withCache(compiledCache, [ componentHash, compileTemplateOptions.source ], async ({ preventCache }) => {

....
....

// 将template代码加载成component
return await transformJSCode(template.code, true, filename, additionalBabelParserPlugins, additionalBabelPlugins, log);
});
}

// 4. parse style部分
for ( const descStyle of descriptor.styles ) {

...
const style = await withCache(compiledCache, [ componentHash, src, descStyle.lang ], async ({ preventCache }) => {

.....
// compiledStyle为解析后的style部分
const compiledStyle = await sfc_compileStyleAsync(compileStyleOptions);

return compiledStyle.code;
});
// 加载样式
addStyle(style, descStyle.scoped ? scopeId : undefined);
}

    以上就是对于单文件组件的解析过程,其实我们只需要将各个部分编译后的代码(即transformedScriptSourcetemplateTransformedSourcecompiledStyle)直接拿出来,并拼成一个json传给后端存储起来即可。

2.2 解析json转成vue component

    通过对createVue2SFCModule.ts里代码的分析,可以看到,样式style部分,通过addStyle方法渲染,而这部分处理逻辑是前端代码传入的。比如在demo中给的示例:

const options = {
moduleCache: {
vue: Vue
},

async getFile(url) {
...
},

// 样式的处理逻辑
addStyle(textContent) {
const style = Object.assign(document.createElement('style'), { textContent });
const ref = document.head.getElementsByTagName('style')[0] || null;
document.head.insertBefore(style, ref);
},
}

    因此,最主要的部分还是script和template的加载。可以看到代码里专门给出了一个方法加载这两部分,即createCJSModule,它的源码如下:

Function('exports', 'require', 'module', '__filename', '__dirname', 'import__', source).call(module.exports, module.exports, require, module, refPath, pathResolve({ refPath, relPath: '.' }), importFunction);
// 通过注释可以看到,该方法实际上是调用了Module.prototype._compile方法中的ReflectApply方法,对js module进行加载

    所以,从json转换成component的逻辑也就有了:从json中解析出templatescript等各部分代码,使用createCJSModule方法进行js模块的加载,然后使用Object.assign赋给返回对象,对于style部分处理逻辑类似。

三、总结

    该方法本质上是对vue3-sfc-loader代码进行了拆解,将原先都需要在渲染时进行的操作,分成了代码编译和加载两个步骤,这样可以在组件上传时进行代码编译,在渲染时进行加载。经过测试上述方案可以在一定程度上优化文章开头提到的那三点问题。
当然谁知道后边会不会有更好的方案呢?