我們知道,在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)