6

手写简易打包工具webpack-demo

 2 years ago
source link: https://segmentfault.com/a/1190000040768609
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

手写简易打包工具webpack-demo

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声明,第二个是表达式语句。

1.png

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

一种方式是自己写遍历,去遍历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

2.png

通过上图可以看到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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK