likes
comments
collection
share

手写简易打包工具webpack-demo

作者站长头像
站长
· 阅读数 17

webpack作为一款打包工具,在学习它之前,对它感到特别陌生,最近花了一些时间,学习了下。

学习的最大收获是手写一个简易的打包工具webpack-demo

webpack-demo分为主要分为三个部分:

  • 生成抽象语法树
  • 获取各模块依赖
  • 生成浏览器能够执行的代码

依赖准备

src目录下有三个文件:index.jsmessage.jsword.js。他们的依赖关系是:index.js是入口文件,其中index.js依赖message.jsmessage.js依赖word.js

index.js

import message from "./message.js";

console.log(message);

message.js

import { word } from "./word.js";

const message = `say ${word}`;

export default message;

word.js

var word = "uccs";

export { word };

现在要要编写一个bundle.js将这三个文件打包成浏览器能够运行的文件。

打包的相关配置项写在webpack.config.js中。配置比较简易只有entryoutput

const path = require("path");
module.exports = {
  entry: path.join(__dirname, "./src/index.js"),
  output: {
    path: path.join(__dirname, "dist"),
    filename: "main.js",
  },
};

代码分析

获取入口文件的代码

通过node提供的fs.readFileSync获取入口文件的内容

const fs = require("fs");
const content = fs.readFileSync("./src/index.html", "utf-8");

拿到入口文件的内容后,就需要获取到它的依赖./message。因为它是string类型。自然就想到用字符串截取的方式获取,但是这种方式太过麻烦,假如依赖项有很多的话,这个表达式就会特别复杂。

那有什么更好的方式可以获取到它的依赖呢?

生成抽象语法树

babel提供了一个解析代码的工具@babel/parser,这个工具有个方法parse,接收两个参数:

  • code:源代码
  • options:源代码使用ESModule,需要传入sourceType: module
function getAST(entry) {
  const source = fs.readFileSync(entry, "utf-8");
  return parser.parse(source, {
    sourceType: "module",
  });
}

这个ast是个对象,叫做抽象语法树,它可以表示当前的这段代码。

ast.program.body存放着我们的程序。通过抽象语法树可以找到声明的语句,声明语句放置就是相关的依赖关系。

通过下图可以看到第一个是import声明,第二个是表达式语句。

手写简易打包工具webpack-demo

接下来就是拿到这段代码中的所有依赖关系。

一种方式是自己写遍历,去遍历body中的type: ImportDeclaration,这种方式呢有点麻烦。

有没有更好的方式去获取呢?

获取相关依赖

babel就提供一个工具@babel/traverse,可以快速找到ImportDeclaration

traverse接收两个参数:

  • ast:抽象语法树
  • options:遍历,需要找出什么样的元素,比如ImportDeclaration,只要抽象语法树中有ImportDeclaration就会进入这个函数。
function getDependencies(ast, filename) {
  const dependencies = {};
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(filename);
      const newFile = path.join(dirname, node.source.value);
      dependencies[node.source.value] = newFile;
    },
  });
  return dependencies;
}

ImportDeclaration:会接收到一个节点node,会分析出所有的ImportDeclaration

手写简易打包工具webpack-demo

通过上图可以看到node.source.value就是依赖。将依赖保存到dependencies对象中就行了,这里面的依赖路径是相对于bundle.js或者是绝对路径,否则打包会出错。

代码转换

依赖分析完了之后,源代码是需要转换的,因为import语法在浏览器中是不能直接运行的。

babel提供了一个工具@babel/core,它是babel的核心模块,提供了一个transformFromAst方法,可以将ast转换成浏览器可以运行的代码。

它接收三个参数:

  • ast:抽象语法树
  • code:不需要,可传入null
  • options:在转换的过程中需要用的presents: ["@babel/preset-env"]
function transform(ast) {
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  });
  return code;
}

获取所有依赖

入口文件分析好之后,它的相关依赖放在dependencies中。下一步将要去依赖中的模块,一层一层的分析最终把所有模块的信息都分析出来,如何实现这个功能?

先定义一个buildModule函数,用来获取entryModuleentryModule包括filenamecodedependencies

function buildModule(filename) {
  let ast = getAST(filename);
  return {
    filename,
    code: transform(ast),
    dependencies: getDependencies(ast, filename),
  };
}

通过遍历modules获取所有的模块信息,当第一次走完for循环后,message.js的模块分析被推到modules中,这时候modules的长度变成了2,所以它会继续执行for循环去分析message.js,发现message.js的依赖有word.js,将会调用buildModule分析依赖,并推到modules中。modules的长度变成了3,在去分析word.js的依赖,发现没有依赖了,结束循环。

通过不断的循环,最终就可以把入口文件和它的依赖,以及它依赖的依赖都推到modules中。

const entryModule = this.buildModule(this.entry);
this.modules.push(entryModule);
for (let i = 0; i < this.modules.length; i++) {
  const { dependencies } = this.modules[i];
  if (dependencies) {
    for (let j in dependencies) {
      // 有依赖调用 buildmodule 再次分析,保存到 modules
      this.modules.push(this.buildModule(dependencies[j]));
    }
  }
}

modules是个的数组,在最终生成浏览器可执行代码上有点困难,所以这里做一个转换

const graphArray = {};
this.modules.forEach((module) => {
  graphArray[module.filename] = {
    code: module.code,
    dependencies: module.dependencies,
  };
});

生成浏览器可执行的代码

所有的依赖计算完之后,就需要生成浏览器能执行的代码。

这段代码是一个自执行函数,将graph传入。

graph传入时需要用JSON.stringify转换一下,因为在字符串中直接传入对象,会变成[object Object]

在打包后的代码中,有个require方法,这个方法浏览器是不支持的,所有我们需要定义这个方法。

require在导入路径时需要做一个路径转换,否在将找不到依赖,所以定义了localRequire

require内部还是一个自执行函数,接收三个参数:localRequireexportscode

const graph = JSON.stringify(graphArray);
const outputPath = path.join(this.output.path, this.output.filename);
const bundle = `
  (function(graph){
    function require(module){
      function localRequire(relativePath){
        return require(graph[module].dependencies[relativePath])
      }
      var exports = {};
      (function(require, exports, code){
        eval(code)
      })(localRequire, exports, graph[module].code)
      return exports;
    }
    require("${this.entry}")
  })(${graph})
`;
fs.writeFileSync(outputPath, bundle, "utf-8");

总结

通过手写一个简单的打包工具后,对webpack内部依赖分析、代码转换有了更深的理解,不在是一个可以使用的黑盒了。

参考资料:从基础到实战 手把手带你掌握新版 Webpack4.0