實(shí)現(xiàn)構(gòu)建工具之打包

我們知道,在node端是使用npm將包下載到本地,通過(guò)讀寫文件進(jìn)行引用,但是在前端只能通過(guò)script加載網(wǎng)絡(luò)文件,此時(shí)commonjs天生不適用前端。

但隨著node的普及,大家更愿意使用npm下載依賴到本地,開發(fā)完成后再打包產(chǎn)生可給script直接用的js文件,也就是開發(fā)階段在本地使用node的文件讀寫功能實(shí)現(xiàn)依賴獲取,文件打包等功能,在生產(chǎn)階段直接用打包出來(lái)的文件。

此處,我嘗試重寫了比較古老的前端打包工具browserify

注意:此處只做項(xiàng)目文件打包,所以不包含如下內(nèi)容:

  • 引入npm包
  • 打包成多個(gè)文件

下面我們開始重寫

流程:

  • browserify index.js 命令行選擇打包目標(biāo)文件
  • index.js 中require的內(nèi)容會(huì)被打包在目標(biāo)文件中

思路:

1.假定index.js require module1.js
2.獲取index.js代碼
3.正則按順序解析require路徑,并替換require為_require,路徑為絕對(duì)路徑(用作緩存key)
4.引入方式思考:
    1.純代碼替換,則每次引入都是源文件的深拷貝,不符合共享依賴
    2.將引入內(nèi)容放到一個(gè)引用Cache中緩存起來(lái),存包含代碼的函數(shù)
此時(shí)_require各包,_require是從Cache取,此時(shí)可以保證多模塊引用的是同一個(gè)對(duì)象
5.順序執(zhí)行即可

設(shè)計(jì)

  • 由于我們使用js進(jìn)行編碼,而不是生成一個(gè)可執(zhí)行browserify命令,所以使用browserify.js完成功能
  • 入口文件可通過(guò)命令行輸入,所以打包方式為 node ./browserify.js index.js
  • 假定入口文件為index.js,在其中require module1.js來(lái)進(jìn)行測(cè)試

實(shí)現(xiàn)

module1.js

const obj = {
  name: "obj",
  age: 10,
};
module.exports = {
  obj,
};

index.js

const { obj } = require("./module1.js");

obj.name = "jane";
obj.age += 20;

browserify.js

//主函數(shù),傳入文件路徑,返回最終打包完成的代碼塊
const browserify = (path) => {
  // 獲取絕對(duì)路徑
  const wholePath = resolve(path);

  // 為每個(gè)require的模塊拼接代碼,為其提供module實(shí)例,并返回module.exports
  codeSplicing(wholePath);

  // 阻止代碼,使其能解析代碼cache對(duì)象,并依照引入順序來(lái)執(zhí)行代碼塊
  return getCode(wholePath);
};

// 執(zhí)行命令行傳入打包源文件 node ./browserify.js index.js,此時(shí)path即index.js
const [path] = process.argv.splice(2);
// 寫目標(biāo)文件;
fs.writeFileSync("./chunk.js", browserify(path));

其中,codeSplicing方法主要是為每個(gè)js模塊代碼塊提供module.exports代碼拼接

//文件絕對(duì)路徑為key,拼接的代碼塊為value
const moduleFuncCache = {};
const codeSplicing = (path) => {
  /** 調(diào)整代碼塊,使其執(zhí)行到require時(shí)執(zhí)行我們新定義的_require
   *  獲取其引用文件中的require,同樣轉(zhuǎn)化為_require執(zhí)行,如index引入module1,module1引入module2,此時(shí)應(yīng)該遞歸將三個(gè)文件都執(zhí)行一遍
   *  以絕對(duì)路徑為key,提供了module環(huán)境的代碼塊為value,收集所有文件代碼,放到moduleFuncCache對(duì)象中,注意:編碼是為了處理中文
   */
  const text = fs
    .readFileSync(path, "utf-8")
    .trim()
    .replace(/require/g, "_require")
    .replace(/_require\(['\"](.*)['\"]\)/g, function (matched, $1) {
      codeSplicing(resolve($1));
      return `_require("${encodeURIComponent(resolve($1))}")`;
    })
    .replace(/;$/, "");

  /**
   *  eval碰到內(nèi)層引用比如module2的text中的中文會(huì)失敗,所以需要轉(zhuǎn)碼
   *  可以通過(guò)eval或者new Function將代碼塊在目標(biāo)文件中轉(zhuǎn)回js 函數(shù)
   */

  moduleFuncCache[encodeURIComponent(path)] = encodeURIComponent(`
        const module = {exports:{}};
        let {exports} = module;
        ${text}
        return module.exports
      `);
};

getCode方法則是提供執(zhí)行流程代碼

const getCode = (entry) => {
  // eval方式轉(zhuǎn)函數(shù)
  return `
        // 自執(zhí)行函數(shù),避免全局污染
        (function(){
            const moduleCache = {}

            const _require = function(path){
                // 第一次引用該模塊則執(zhí)行,后續(xù)從緩存中取
                if(!moduleCache[path]) moduleCache[path] = formatModuleFuncCache[path]()
                return moduleCache[path]
            }
            
            //從json轉(zhuǎn)化回對(duì)象
            const moduleFuncCache = JSON.parse('${JSON.stringify(
              moduleFuncCache
            )}')

            //轉(zhuǎn)碼代碼塊,并將類型轉(zhuǎn)化成函數(shù)
            const formatModuleFuncCache = Object.entries(moduleFuncCache)
                .map(([key, value]) => {
                    return { [key]: (function(){
                                const code = decodeURIComponent(value)
                
                                eval(\` var tempFunc = function(){ \$\{code\} } \`)
                                return tempFunc
                                })()
                            };
                })
                .reduce((pre, now) => Object.assign(pre, now), {})
                
            //執(zhí)行入口文件代碼
            formatModuleFuncCache['${encodeURIComponent(entry)}']()
        })()
    `;
};

測(cè)試

1.執(zhí)行命令行node ./browserify.js index.js生成chunk文件
2.執(zhí)行命令行node ./chunk.js輸出 { name: 'jane', age: 30 }

總結(jié)

  • 遞歸為每個(gè)模塊提供運(yùn)行環(huán)境
  • 提供執(zhí)行代碼,由于require換成了從代碼塊對(duì)象moduleFuncCache中取函數(shù)執(zhí)行,所以能取到module.exports導(dǎo)出內(nèi)容
  • 每個(gè)module.exports導(dǎo)出內(nèi)容必須是單例,所以使用緩存

其他

1.本此重寫沒(méi)有實(shí)現(xiàn)

  • 引入npm包 (這個(gè)實(shí)現(xiàn)比較容易,實(shí)現(xiàn)node commonjs的調(diào)用鏈即可)
  • 打包成多個(gè)文件 (這個(gè)需要提供配置確定打包策略)

2.此方式使用了eval,經(jīng)查詢有安全風(fēng)險(xiǎn),可以參考基于本文的重寫的browserify改進(jìn)

3.代碼地址:https://github.com/a793816354/myBrowserify/tree/init

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容