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

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

一种形式是本人写遍历,去遍历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

通过上图能够看到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