实现构建工具之打包

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

你可能感兴趣的:(实现构建工具之打包)