不定日拱卒:分享日常开发过程中的一些小技巧,为更多人提供类似问题的解决方案
背景
-
现代前端开发中,模块化已经成为主流
-
一般情况下,我们引入其他模块是使用 es6 的 import 方法
import moduleA from './modules/moduleA.tsx';
moduleA.doSomething();
-
在具体项目中,有时候我们的代码需要「通过变量拼接」动态引用其他的模块
-
import 语句不支持拼接路径
遇到的问题
- 我们第一时间想到的就是通过 require 来实现需求,即以下方式
const moduleName = 'moduleA';
const aModule = require(`./modules/${moduleName}.tsx`).default;
aModule.doSomething();
测试后,页面效果是符合预期的
-
然而,随着 ./modules 目录下文件的增加,我们发现打包速度越来越慢,而且包体积越来越大,这违背了我们初始的认知
-
查阅资料,发现这是由于 webpack 编译原理导致的
如果你的 require 含有表达式(expressions),webpack 会创建一个上下文(context),因为在编译时(compile time)并不清楚具体是哪一个模块被导入
webpack 解析 require 语句,提取到的信息如下
Directory: ./modules
Regular expression: /^.*\.tsx$/
webpack 会根据信息此信息去遍历目录下所有符合的文件路径,并生成如下的 map
// matchResult.js
var matched = {
'moduleA.tsx': require('./modules/moduleA.tsx'),
'moduleB.tsx': require('./modules/moduleB.tsx'),
'moduleC.tsx': require('./modules/moduleC.tsx'),
...
};
module.exports = function(key) {
return matched[key];
}
然后,上面的 require 语句就等效于
require('matchResult.js')(moduleName + '.tsx').default;
这意味着 webpack 能够支持动态 require,但会导致所有可能用到的模块都包含在 bundle 中
解决方案
-
既然无法通过语言特性解决问题,我们还是从代码本身入手,让 webpack 打包时就已经有了完整的代码
-
而我们要引用的模块会根据 moduleName 这个参数动态变化,则我们可以通过编写预处理脚本,在打包前执行,根据参数生成我们想要的最终代码,再交给 webpack 去打包
-
动态参数我们以环境变量的方式传入(此处有其他做法,不在此文讨论范围内)
-
假设 moduleName 为 moduleA,那么我们的代码就转化为
// output.tsx
import moduleA from './modules/moduleA.tsx';
export default moduleA;
// main.tsx
import aModule from './output.tsx';
moduleA.doSomething();
...
而我们的关注点就转化为如何通过变量生成 output.tsx 文件了
- 我们回顾一下 ejs、Vue template 等模板文件,发现它们是通过替换的方式来实现编译的,即将形如 {{moduleName}} 这样的字符串,通过变量替换为 moduleA,我们也可以采用这种方式,将模板文件编写如下
// output.tpl
import {{moduleName}} from './modules/{{moduleName}}.tsx';
export default {{moduleName}};
然后用 moduleName 的值替换掉它们
- 以上方案可以解决比较简单的场景,但对于以下的场景,则显得灵活性不足(变量是 moduleA,moduleB,moduleC)
// output.tsx
import moduleA from './modules/moduleA.tsx';
import moduleB from './modules/moduleB.tsx';
import moduleC from './modules/moduleC.tsx';
const modules = {
moduleA,
moduleB,
moduleC
};
export default modules;
- 面对这种更具灵活性的需求,这里提供一种方法,就是 Slot 插槽的做法,即把每一个需要动态生成的部分,加上标记位
// output.tpl
//@importSlot
//@mergeSlot
export default modules;
这样的标记位,由于是注释,也不用在生成代码后专门去清除
- 编写预处理文件
// preprocess.js
// 从环境变量中读取需要的变量
const modules = (process.env.MODULES || 'moduleA,moduleB,moduleC').split(',');
// 获取标记位末尾坐标值
const getSlotEndIndex = (origin, slot) => origin.indexOf(slot) + slot.length;
// 在指定坐标位置插入内容
const insertContent = (origin, index, content) =>
origin.substring(0, index) + content + origin.substring(index + 1, origin.length);
const generateImportContent = () =>
modules.reduce(
(result, module) => (result += `\nimport ${module} from './modules/${module}.tsx';`),
''
);
const generateMergeContent = () =>
`const modules = {\n` +
modules.reduce(
(result, module) => (result += `${module},`),
''
) +
`\n};`
// 读取 output.tpl 模板文件
const tpl = fs.readFileSync('./output.tpl').toString();
let content = tpl;
// 插入 import 语句
content = insertContent(
content,
getSlotEndIndex(content, '//@importSlot'),
generateImportContent()
);
// 插入 merge 语句
content = insertContent(
content,
getSlotEndIndex(content, '//@mergeSlot'),
generateMergeContent()
);
// 生成最终文件
fs.writeFileSync('./output.tsx', content);
- 最后,修改 package.json 编译命令(假设编译命令是 npm run build)
node preprocess.js && npm run build
有时候,我们可以适当转变一下思路,采用一些「偏方」来快速解决问题。不定日拱卒,慢慢进步。