不定日拱卒-經過模板動態生成可用代碼

不定日拱卒:分享平常開發過程當中的一些小技巧,爲更多人提供相似問題的解決方案前端

背景

  • 現代前端開發中,模塊化已經成爲主流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
複製代碼

有時候,咱們能夠適當轉變一下思路,採用一些「偏方」來快速解決問題。不定日拱卒,慢慢進步。

相關文章
相關標籤/搜索