不定日拱卒-经过模板动态生成可用代码

不定日拱卒:分享平常开发过程当中的一些小技巧,为更多人提供相似问题的解决方案前端

背景

  • 现代前端开发中,模块化已经成为主流node

  • 通常状况下,咱们引入其余模块是使用 es6 的 import 方法webpack

import moduleA from './modules/moduleA.tsx';

moduleA.doSomething();
复制代码
  • 在具体项目中,有时候咱们的代码须要「经过变量拼接」动态引用其余的模块es6

  • import 语句不支持拼接路径web

遇到的问题

  • 咱们第一时间想到的就是经过 require 来实现需求,即如下方式
const moduleName = 'moduleA';
const aModule = require(`./modules/${moduleName}.tsx`).default;

aModule.doSomething();
复制代码

测试后,页面效果是符合预期的express

  • 然而,随着 ./modules 目录下文件的增长,咱们发现打包速度愈来愈慢,并且包体积愈来愈大,这违背了咱们初始的认知npm

  • 查阅资料,发现这是因为 webpack 编译原理致使的json

若是你的 require 含有表达式(expressions),webpack 会建立一个上下文(context),由于在编译时(compile time)并不清楚具体是哪个模块被导入bash

webpack 解析 require 语句,提取到的信息以下markdown

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
复制代码

有时候,咱们能够适当转变一下思路,采用一些「偏方」来快速解决问题。不定日拱卒,慢慢进步。