关于前端:手写简易打包工具webpackdemo

8次阅读

共计 4011 个字符,预计需要花费 11 分钟才能阅读完成。

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

正文完
 0