我们知道,在node端是使用npm将包下载到本地,通过读写文件进行引用,但是在前端只能通过script加载网络文件,此时commonjs天生不适用前端。
但随着node的普及,大家更愿意使用npm下载依赖到本地,开发完成后再打包产生可给script直接用的js文件,也就是开发阶段在本地使用node的文件读写功能实现依赖获取,文件打包等功能,在生产阶段直接用打包出来的文件。
此处,我尝试重写了比较古老的前端打包工具browserify
注意:此处只做项目文件打包,所以不包含如下内容:
- 引入npm包
- 打包成多个文件
下面我们开始重写
流程:
-
browserify index.js
命令行选择打包目标文件 - index.js 中require的内容会被打包在目标文件中
思路:
1.假定index.js require module1.js
2.获取index.js代码
3.正则按顺序解析require路径,并替换require为_require,路径为绝对路径(用作缓存key)
4.引入方式思考:
1.纯代码替换,则每次引入都是源文件的深拷贝,不符合共享依赖
2.将引入内容放到一个引用Cache中缓存起来,存包含代码的函数
此时_require各包,_require是从Cache取,此时可以保证多模块引用的是同一个对象
5.顺序执行即可
设计
- 由于我们使用js进行编码,而不是生成一个可执行browserify命令,所以使用browserify.js完成功能
- 入口文件可通过命令行输入,所以打包方式为
node ./browserify.js index.js
- 假定入口文件为index.js,在其中require module1.js来进行测试
实现
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
//主函数,传入文件路径,返回最终打包完成的代码块
const browserify = (path) => {
// 获取绝对路径
const wholePath = resolve(path);
// 为每个require的模块拼接代码,为其提供module实例,并返回module.exports
codeSplicing(wholePath);
// 阻止代码,使其能解析代码cache对象,并依照引入顺序来执行代码块
return getCode(wholePath);
};
// 执行命令行传入打包源文件 node ./browserify.js index.js,此时path即index.js
const [path] = process.argv.splice(2);
// 写目标文件;
fs.writeFileSync("./chunk.js", browserify(path));
其中,codeSplicing方法主要是为每个js模块代码块提供module.exports代码拼接
//文件绝对路径为key,拼接的代码块为value
const moduleFuncCache = {};
const codeSplicing = (path) => {
/** 调整代码块,使其执行到require时执行我们新定义的_require
* 获取其引用文件中的require,同样转化为_require执行,如index引入module1,module1引入module2,此时应该递归将三个文件都执行一遍
* 以绝对路径为key,提供了module环境的代码块为value,收集所有文件代码,放到moduleFuncCache对象中,注意:编码是为了处理中文
*/
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碰到内层引用比如module2的text中的中文会失败,所以需要转码
* 可以通过eval或者new Function将代码块在目标文件中转回js 函数
*/
moduleFuncCache[encodeURIComponent(path)] = encodeURIComponent(`
const module = {exports:{}};
let {exports} = module;
${text}
return module.exports
`);
};
getCode方法则是提供执行流程代码
const getCode = (entry) => {
// eval方式转函数
return `
// 自执行函数,避免全局污染
(function(){
const moduleCache = {}
const _require = function(path){
// 第一次引用该模块则执行,后续从缓存中取
if(!moduleCache[path]) moduleCache[path] = formatModuleFuncCache[path]()
return moduleCache[path]
}
//从json转化回对象
const moduleFuncCache = JSON.parse('${JSON.stringify(
moduleFuncCache
)}')
//转码代码块,并将类型转化成函数
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), {})
//执行入口文件代码
formatModuleFuncCache['${encodeURIComponent(entry)}']()
})()
`;
};
测试
1.执行命令行node ./browserify.js index.js
生成chunk文件
2.执行命令行node ./chunk.js
输出 { name: 'jane', age: 30 }
总结
- 递归为每个模块提供运行环境
- 提供执行代码,由于require换成了从代码块对象moduleFuncCache中取函数执行,所以能取到module.exports导出内容
- 每个module.exports导出内容必须是单例,所以使用缓存
其他
1.本此重写没有实现
- 引入npm包 (这个实现比较容易,实现node commonjs的调用链即可)
- 打包成多个文件 (这个需要提供配置确定打包策略)
2.此方式使用了eval,经查询有安全风险,可以参考基于本文的重写的browserify改进
3.代码地址:https://github.com/a793816354/myBrowserify/tree/init