

手写简易打包工具webpack-demo
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.

手写简易打包工具webpack-demo
webpack
作为一款打包工具,在学习它之前,对它感到特别陌生,最近花了一些时间,学习了下。
学习的最大收获是手写一个简易的打包工具webpack-demo
。
webpack-demo
分为主要分为三个部分:
- 生成抽象语法树
- 获取各模块依赖
- 生成浏览器能够执行的代码
src
目录下有三个文件:index.js
、message.js
、word.js
。他们的依赖关系是:index.js
是入口文件,其中index.js
依赖message.js
,message.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
中。配置比较简易只有entry
和output
。
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
函数,用来获取entryModule
。entryModule
包括filename
、code
、dependencies
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
内部还是一个自执行函数,接收三个参数:localRequire
、exports
、code
。
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
内部依赖分析、代码转换有了更深的理解,不在是一个可以使用的黑盒了。
Recommend
-
58
这个小项目是我读过一点Spring的源码后,模仿Spring的IOC写的一个简易的IOC,当然Spring的在天上,我写的在马里亚纳海沟,哈哈 感兴趣的小伙伴可以去我的github拉取代码看着玩 地址: https://github.com/zhuchangwu/CIOC
-
19
手写简易模块打包器(手写系列五)饥人谷若愚饥人谷前端,培养有灵魂的前端工程师...
-
7
我们在之前用了两篇文章来介绍了Webpack的配置和优化,那么为什么这篇文章还要来对比Parcel和Rollup呢?配置过Webpack的童鞋可能会发现了,虽然它具有很高的可配置性和扩展性,以及丰富的插件系统,但是这些无一不给我们的上手带来限制,有很高的上手门槛。...
-
5
一份关于计算机Demo的简易历史蓬岸 Dr.Quest电脑博物馆 www.DNBWG.com 站长D...
-
7
什么是webpack打包工具以及其优点用法 webpack中文文档:
-
8
编译产物分析 (() => { // 模块依赖 var __webpack_modules__ = ({ "./src/index.js": ((module, __unused_webpack_exports, __webpack_require__) => { // 执行模块代码其中 同时执行__webpack_require__ 引用代码...
-
6
| 隋堤倦客 404 - arao'blog ...
-
4
1、maven依赖本开发教程适用于sonarqube5.x、6.x。 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://m...
-
5
与许多大规模公司一样,随着代码库不断变大,爱彼迎也在打包工具方面经历了阵痛。即使代码库增至四倍,爱彼迎在2018年将Jav...
-
21
字节开源基于 Rust 打包工具 Rspack,比 Webpack 快了 10 倍
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK