深入理解 Webpack 核心机制与编译流程
🤖 作者简介:水煮白菜王,一位前端劝退师 👻
👀 文章专栏: 前端专栏 ,记录一下平时在博客写作中,总结出的一些开发技巧和知识归纳总结✍。
感谢支持💕💕💕
目录
- Webpack核心机制
- Tapable:Webpack 插件系统的“心脏”
- Tapabel提供的钩子及示例
- 源码解读
- 编译构建
- compile
- make
- 1. Module
- 2. loader-runner
- 3. acorn
- 4. Chunk生成算法
- eal
- emit
- 总结
- Webpack打包机制
- 简单版打包模型步骤
- 单个文件的依赖模块Map
- 单个文件的依赖模块Map
- 输出立即执行函数
- webpack打包流程概括
- 实现一个丐版Webpack
- 开始
- 接下来我们再来逐行解析 bundle 函数
- 如果你觉得这篇文章对你有帮助,请点赞 👍、收藏 👏 并关注我!👀
Webpack核心机制
Webpack 本质上是一个高度可配置且可扩展的模块捆绑器,它采用了一种基于事件流的编程范例。Webpack 的运作依赖于一系列插件来完成各种任务,从简单的文件转换到复杂的构建优化。
Webpack 主要使用 Compiler 和 Compilation 两个类来控制整个生命周期。它们都继承自 Tapable 并利用它注册构建过程中的各个阶段所需触发的事件。
Tapable:Webpack 插件系统的“心脏”
Tapable 是一个类似于 Node.js 的 EventEmitter 的库,主要用于管理钩子函数的发布与订阅。在 Webpack 的插件系统中,Tapable 扮演着核心调度者的角色。
Tapabel提供的钩子及示例
Tapable 提供了多种类型的钩子(Hook)以便挂载,适用于不同的执行场景(同步 / 异步、串行 / 并发、是否支持熔断等):
const {SyncHook, // 同步钩子:依次执行所有订阅者SyncBailHook, // 同步熔断钩子:一旦某个订阅者返回非 undefined 值则停止执行SyncWaterfallHook, // 同步流水钩子:前一个订阅者的返回值作为参数传给下一个SyncLoopHook, // 同步循环钩子:重复执行订阅者直到返回 undefinedAsyncParallelHook, // 异步并发钩子:并行执行所有订阅者(不关心顺序)AsyncParallelBailHook, // 异步并发熔断钩子:任意一个订阅者返回非 undefined 则立即结束AsyncSeriesHook, // 异步串行钩子:按顺序依次执行每个订阅者AsyncSeriesBailHook, // 异步串行熔断钩子:同 SyncBailHook,但为异步模式AsyncSeriesWaterfallHook // 异步串行流水钩子:同 SyncWaterfallHook,但为异步模式
} = require("tapable");
Tabpack 提供了同步&异步绑定钩子的方法对比如下:
类型 | 绑定方法 | 执行方法 |
---|---|---|
同步 (Sync) | .tap(name, fn) | .call(args...) |
异步 (Async) | .apAsync(name, fn) /.tapPromise(name, fn) | .callAsync(args..., cb) / .promise(args...) |
Tabpack 同步简单示例:
const { SyncHook } = require("tapable");// 创建一个带有三个参数的同步钩子
const demohook = new SyncHook(["arg1", "arg2", "arg3"]);// 注册监听函数 绑定事件到webpack事件流
demohook.tap("hook1", (arg1, arg2, arg3) => {console.log("接收到参数:", arg1, arg2, arg3);
});// 触发钩子 执行绑定的事件
demohook.call(1, 2, 3);
// 输出: 接收到参数:1 2 3
源码解读
- 初始化启动之Webpack的入口文件
● 追本溯源,第一步我们要找到Webpack的入口文件。
● 当通过命令行启动Webpack后,npm会让命令行工具进入node_modules.bin 目录。
● 然后查找是否存在 webpack.sh 或者 webpack.cmd 文件,如果存在,就执行它们,不存在就会抛出错误。
● 实际的入口文件是:node_modules/webpack/bin/webpack.js
,让我们来看一下里面的核心函数。
// node_modules/webpack/bin/webpack.js
// 正常执行返回
process.exitCode = 0;
// 运行某个命令
const runCommand = (command, args) => {...}
// 判断某个包是否安装
const isInstalled = packageName => {...}
// webpack可用的CLI:webpacl-cli和webpack-command
const CLIs = {...}
// 判断是否两个CLI是否安装了
const installedClis = CLIs.filter(cli=>cli.installed);
// 根据安装数量进行处理
if (installedClis.length === 0) {...} else if (installedClis.length === 1) {...} else {...}
启动后,Webpack最终会找到 webpack-cli /webpack-command的 npm 包,并且 执行 CLI。
- webpack-cli
搞清楚了Webpack启动的入口文件后,接下来让我们把目光转移到webpack-cli,看看它做了哪些动作。
● 引入 yargs,对命令行进行定制分析命令行参数,对各个参数进行转换,组成编译配置项引用webpack,根据配置项进行编译和构建
● webpack-cli 会处理不需要经过编译的命令。
// node_modules/webpack-cli/bin/cli.js
const {NON_COMPILATION_ARGS} = require("./utils/constants");
const NON_COMPILATION_CMD = process.argv.find(arg => {if (arg === "serve") {global.process.argv = global.process.argv.filter(a => a !== "serve");process.argv = global.process.argv;}return NON_COMPILATION_ARGS.find(a => a === arg);
});
if (NON_COMPILATION_CMD) {return require("./utils/prompt-command")(NON_COMPILATION_CMD,...process.argv);
}
webpack-cli提供的不需要编译的命令如下
// node_modules/webpack-cli/bin/untils/constants.js
const NON_COMPILATION_ARGS = ["init", // 创建一份webpack配置文件"migrate", // 进行webpack版本迁移"add", // 往webpack配置文件中增加属性"remove", // 往webpack配置文件中删除属性"serve", // 运行webpack-serve"generate-loader", // 生成webpack loader代码"generate-plugin", // 生成webpack plugin代码"info" // 返回与本地环境相关的一些信息
];
webpack-cli 使用命令行工具包yargs
// node_modules/webpack-cli/bin/config/config-yargs.js
const {CONFIG_GROUP,BASIC_GROUP,MODULE_GROUP,OUTPUT_GROUP,ADVANCED_GROUP,RESOLVE_GROUP,OPTIMIZE_GROUP,DISPLAY_GROUP
} = GROUPS;
● webpack-cli对配置文件和命令行参数进行转换最终生成配置选项参数 options,最终会根据配置参数实例化webpack对象,然后执行构建流程。
● 除此之外,让我们回到node_modules/webpack/lib/webpack.js里来看一下Webpack还做了哪些准备工作。
// node_modules/webpack/lib/webpack.js
const webpack = (options, callback) => {...options = new WebpackOptionsDefaulter().process(options);compiler = new Compiler(options.context);new NodeEnvironmentPlugin().apply(compiler);...compiler.options = new WebpackOptionsApply().process(options, compiler);...webpack.WebpackOptionsDefaulter = WebpackOptionsDefaulter;webpack.WebpackOptionsApply = WebpackOptionsApply;...webpack.NodeEnvironmentPlugin = NodeEnvironmentPlugin;
}
WebpackOptionsDefaulter的功能是设置一些默认的Options(代码比较多,可自行查看node_modules/webpack/lib/WebpackOptionsDefaulter.js)
// node_modules/webpack/lib/node/NodeEnvironmentPlugin.js
class NodeEnvironmentPlugin {apply(compiler) {... compiler.hooks.beforeRun.tap("NodeEnvironmentPlugin", compiler => {if (compiler.inputFileSystem === inputFileSystem) inputFileSystem.purge();});}
}
从上面的代码我们可以知道,NodeEnvironmentPlugin插件监听了beforeRun钩子,它的作用是清除缓存。
- WebpackOptionsApply
WebpackOptionsApply会将所有的配置options参数转换成webpack内部插件。
使用默认插件列表:
● output.library -> LibraryTemplatePlugin
● externals -> ExternalsPlugin
● devtool -> EvalDevtoolModulePlugin, SourceMapDevToolPlugin
● AMDPlugin, CommonJsPlugin
● RemoveEmptyChunksPlugin
// node_modules/webpack/lib/WebpackOptionsApply.js
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
- EntryOptionPlugin
下来让我们进入EntryOptionPlugin插件,看看它做了哪些动作。
// node_modules/webpack/lib/EntryOptionPlugin.js
module.exports = class EntryOptionPlugin {apply(compiler) {compiler.hooks.entryOption.tap("EntryOptionPlugin", (context, entry) => {if (typeof entry === "string" || Array.isArray(entry)) {itemToPlugin(context, entry, "main").apply(compiler);} else if (typeof entry === "object") {for (const name of Object.keys(entry)) {itemToPlugin(context, entry[name], name).apply(compiler);}} else if (typeof entry === "function") {new DynamicEntryPlugin(context, entry).apply(compiler);}return true;});}
};
● 如果是数组,则转换成多个entry来处理,如果是对象则转换成一个个entry来处理。
● compiler实例化是在node_modules/webpack/lib/webpack.js里完成的。通过EntryOptionPlugin插件进行参数校验。通过WebpackOptionsDefaulter将传入的参数和默认参数进行合并成为新的options,创建compiler,以及相关plugin,最后通过
● WebpackOptionsApply将所有的配置options参数转换成Webpack内部插件。
● 再次来到我们的node_modules/webpack/lib/webpack.js中
if (options.watch === true || (Array.isArray(options) && options.some(o => o.watch))) {const watchOptions = Array.isArray(options)? options.map(o => o.watchOptions || {}): options.watchOptions || {};return compiler.watch(watchOptions, callback);
}
compiler.run(callback);
实例compiler后会根据options的watch判断是否启动了watch,如果启动watch了就调用compiler.watch来监控构建文件,否则启动compiler.run来构建文件。
编译构建
compile
首先会实例化NormalModuleFactory和ContextModuleFactory。然后进入到run方法。
// node_modules/webpack/lib/Compiler.js
run(callback) { ...// beforeRun 如上文NodeEnvironmentPlugin插件清除缓存this.hooks.beforeRun.callAsync(this, err => {if (err) return finalCallback(err);// 执行run Hook开始编译this.hooks.run.callAsync(this, err => {if (err) return finalCallback(err);this.readRecords(err => {if (err) return finalCallback(err);// 执行compilethis.compile(onCompiled);});});});
}
在执行this.hooks.compile之前会执行this.hooks.beforeCompile,来对编译之前需要处理的插件进行执行。紧接着this.hooks.compile执行后会实例化Compilation对象
// node_modules/webpack/lib/compiler.js
compile(callback) {const params = this.newCompilationParams();this.hooks.beforeCompile.callAsync(params, err => {if (err) return callback(err);// 进入compile阶段this.hooks.compile.call(params);const compilation = this.newCompilation(params);// 进入make阶段this.hooks.make.callAsync(compilation, err => {if (err) return callback(err);compilation.finish(err => {if (err) return callback(err);// 进入seal阶段compilation.seal(err => {if (err) return callback(err);this.hooks.afterCompile.callAsync(compilation, err => {if (err) return callback(err);return callback(null, compilation);})})})})})
}
make
● 一个新的Compilation创建完毕,将从Entry开始读取文件,根据文件类型和配置的Loader对文件进行编译,编译完成后再找出该文件依赖的文件,递归的编译和解析。
● 我们来看一下make钩子被监听的地方。
● 如代码中注释所示,addEntry是make构建阶段真正开始的标志
// node_modules/webpack/lib/SingleEntryPlugin.js
compiler.hooks.make.tapAsync("SingleEntryPlugin",(compilation, callback) => {const { entry, name, context } = this;cosnt dep = SingleEntryPlugin.createDependency(entry, name);// make构建阶段开始标志 compilation.addEntry(context, dep, name, callback);}
)
addEntry实际上调用了_addModuleChain方法,_addModuleChain方法将模块添加到依赖列表中去,同时进行模块构建。构建时会执行如下函数:
// node_modules/webpack/lib/Compilation.js
// addEntry -> addModuleChain
_addModuleChain(context, dependency, onModule, callback) {...this.buildModule(module, false, null, null, err => {...})...}
如果模块构建完成,会触发finishModules。
// node_modules/webpack/lib/Compilation.js
finish(callback) {const modules = this.modules;this.hooks.finishModules.callAsync(modules, err => {if (err) return callback(err);for (let index = 0; index < modules.length; index++) {const module = modules[index]; this.reportDependencyErrorsAndWarnings(module, [module]);}callback();})
}
1. Module
● Module包括NormalModule(普通模块)、ContextModule(./src/a ./src/b)、ExternalModule(module.exports=jQuery)、DelegatedModule(manifest)以及MultiModule(entry:[‘a’, ‘b’])。
● 本文以NormalModule(普通模块)为例子,看一下构建(Compilation)的过程。
使用 loader-runner 运行 loadersLoader转换完后,使用 acorn 解析生成AST使用 ParserPlugins 添加依赖
2. loader-runner
// node_modules/webpack/lib/NormalModule.js
const { getContext, runLoaders } = require("loader-runner");
doBuild(){...runLoaders(...)...}
...
try {const result = this.parser.parse()
}
doBuild会去加载资源,doBuild中会传入资源路径和插件资源去调用loader-runner插件的runLoaders方法去加载和执行loader
3. acorn
// node_modules/webpack/lib/Parser.jsconst acorn = require("acorn");
使用acorn解析转换后的内容,输出对应的抽象语法树(AST)。
// node_modules/webpack/lib/Compilation.js
this.hooks.buildModule.call(module);
...
if (error) {this.hooks.failedModule.call(module, error);return callback(error);
}
this.hooks.succeedModule.call(module);
return callback();
● 成功就触发succeedModule,失败就触发failedModule。
● 最终将上述阶段生成的产物存放到Compilation.js的this.modules = [];上。
完成后就到了seal阶段。
这里补充介绍一下Chunk生成的算法
4. Chunk生成算法
● webpack首先会将entry中对应的module都生成一个新的chunk。
● 遍历module的依赖列表,将依赖的module也加入到chunk中。
● 如果一个依赖module是动态引入的模块,会根据这个module创建一个新的chunk,继续遍历依赖。
● 重复上面的过程,直至得到所有的chunk。
eal
● 所有模块及其依赖的模块都通过Loader转换完成,根据依赖关系开始生成Chunk。
● seal阶段也做了大量的的优化工作,进行了hash的创建以及对内容进行生成(createModuleAssets)。
// node_modules/webpack/lib/Compilation.jsthis.createHash();
this.modifyHash();
this.createModuleAssets();
// node_modules/webpack/lib/Compilation.js
createModuleAssets(){for (let i = 0; i < this.modules.length; i++) {const module = this.modules[i];if (module.buildInfo.assets) {for (const assetName of Object.keys(module.buildInfo.assets)) {const fileName = this.getPath(assetName);this.assets[fileName] = module.buildInfo.assets[assetName];this.hooks.moduleAsset.call(module, fileName);}}}
}
seal阶段经历了很多的优化,比如tree shaking就是在这个阶段执行。最终生成的代码会存放在Compilation的assets属性上
emit
将输出的内容输出到磁盘,创建目录生成文件,文件生成阶段结束。
// node_modules/webpack/lib/compiler.js
this.hooks.emit.callAsync(compilation, err => {if (err) return callback(err);outputPath = compilation.getPath(this.outputPath);this.outputFileSystem.mkdirp(outputPath, emitFiles);
})
总结
Webpack在启动阶段对配置参数和命令行参数以及默认参数进行了合并,并进行了插件的初始化工作。完成初始化的工作后调用Compiler的run开启Webpack编译构建过程,构建主要流程包括compile、make、build、seal、emit等阶段。
Webpack打包机制
webpack是一个打包模块化 JavaScript 的工具,在 webpack里一切文件皆模块,通过 Loader 转换文件,通过 Plugin 注入钩子,最后输出由多个模块组合成的文件。webpack专注于构建模块化项目。
简单版打包模型步骤
从简单的入手看,当 webpack 的配置只有一个出口时,不考虑分包的情况,其实我们只得到了一个bundle.js的文件,这个文件里包含了我们所有用到的js模块,可以直接被加载执行。那么,我可以分析一下它的打包思路,大概有以下4步:
- 利用
babel
完成代码转换及解析,并生成单个文件的依赖模块Map
- 从入口开始递归分析,并生成整个项目的依赖图谱
- 将各个引用模块打包为一个立即执行函数
- 将最终的
bundle
文件写入bundle.js
中
单个文件的依赖模块Map
我们会可以使用这几个包:
@babel/parser
:负责将代码解析为抽象语法树@babel/traverse
:遍历抽象语法树的工具,我们可以在语法树中解析特定的节点,然后做一些操作,如ImportDeclaration
获取通过import
引入的模块,FunctionDeclaration
获取函数@babel/core
:代码转换,如ES6的代码转为ES5的模式
由这几个模块的作用,其实已经可以推断出应该怎样获取单个文件的依赖模块了,转为
Ast->遍历Ast->调用ImportDeclaration
。代码如下:
// exportDependencies.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')const exportDependencies = (filename)=>{const content = fs.readFileSync(filename,'utf-8')// 转为Astconst ast = parser.parse(content, {sourceType : 'module'//babel官方规定必须加这个参数,不然无法识别ES Module})const dependencies = {}//遍历AST抽象语法树traverse(ast, {//调用ImportDeclaration获取通过import引入的模块ImportDeclaration({node}){const dirname = path.dirname(filename)const newFile = './' + path.join(dirname, node.source.value)//保存所依赖的模块dependencies[node.source.value] = newFile}})//通过@babel/core和@babel/preset-env进行代码的转换const {code} = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"]})return{filename,//该文件名dependencies,//该文件所依赖的模块集合(键值对存储)code//转换后的代码}
}
module.exports = exportDependencies
试跑工作:
//info.js
const a = 1
export a
// index.js
import info from'./info.js'
console.log(info)//testExport.js
const exportDependencies = require('./exportDependencies')
console.log(exportDependencies('./src/index.js'))
单个文件的依赖模块Map
有了获取单个文件依赖的基础,我们就可以在这基础上,进一步得出整个项目的模块依赖图谱了。首先,从入口开始计算,得到entryMap
,然后遍历entryMap.dependencies
,取出其value(即依赖的模块的路径),然后再获取这个依赖模块的依赖图谱,以此类推递归下去即可,代码如下:
const exportDependencies = require('./exportDependencies')//entry为入口文件路径
const exportGraph = (entry)=>{const entryModule = exportDependencies(entry)const graphArray = [entryModule]for(let i = 0; i < graphArray.length; i++){const item = graphArray[i];//拿到文件所依赖的模块集合,dependencies的值参考exportDependenciesconst { dependencies } = item;for(let j in dependencies){graphArray.push(exportDependencies(dependencies[j]))//关键代码,目的是将入口模块及其所有相关的模块放入数组}}//接下来生成图谱const graph = {}graphArray.forEach(item => {graph[item.filename] = {dependencies: item.dependencies,code: item.code}})//可以看出,graph其实是 文件路径名:文件内容 的集合return graph
}
module.exports = exportGraph
输出立即执行函数
首先,我们的代码被加载到页面中的时候,是需要立即执行的。所以输出的bundle.js
实质上要是一个立即执行函数。我们主要注意以下几点:
- 我们写模块的时候,用的是
import/export.
经转换后,变成了require/exports
- 我们要让
require/exports
能正常运行,那么我们得定义这两个东西,并加到bundle.js里 - 在依赖图谱里,代码都成了字符串。要执行,可以使用
eval
因此,我们要做这些工作:
- 定义一个
require
函数,require
函数的本质是执行一个模块的代码,然后将相应变量挂载到exports
对象上 - 获取整个项目的依赖图谱,从入口开始,调用
require
方法。完整代码如下:
const exportGraph = require('./exportGraph')
// 写入文件,可以用fs.writeFileSync等方法,写入到output.path中
const exportBundle = require('./exportBundle')const exportCode = (entry)=>{//要先把对象转换为字符串,不然在下面的模板字符串中会默认调取对象的toString方法,参数变成[Object object]const graph = JSON.stringify(exportGraph(entry))exportBundle(`(function(graph) {//require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上function require(module) {//localRequire的本质是拿到依赖包的exports变量function localRequire(relativePath) {return require(graph[module].dependencies[relativePath]);}var exports = {};(function(require, exports, code) {eval(code);})(localRequire, exports, graph[module].code);return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁}require('${entry}')})(${graph})`)
}
module.exports = exportCode
至此,简单打包完成,跑出结果。bundle.js的文件内容为:
(function(graph) {//require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上function require(module) {//localRequire的本质是拿到依赖包的exports变量function localRequire(relativePath) {returnrequire(graph[module].dependencies[relativePath]);}var exports = {};(function(require, exports, code) {eval(code);})(localRequire, exports, graph[module].code);return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁}require('./src/index.js')
})({"./src/index.js":{"dependencies":{"./info.js":"./src/info.js"},"code":"\"use strict\";\n\nvar _info = _interopRequireDefault(require(\"./info.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_info[\"default\"]);"},"./src/info.js":{"dependencies":{"./name.js":"./src/name.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports[\"default\"] = void 0;\n\nvar _name = require(\"./name.js\");\n\nvar info = \"\".concat(_name.name, \" is beautiful\");\nvar _default = info;\nexports[\"default\"] = _default;"},"./src/name.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.name = void 0;\nvar name = 'winty';\nexports.name = name;"}})
webpack打包流程概括
webpack的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参数:Webpack启动时,依据命令行参数和配置文件设置编译所需的各项基本参数,确保准备好开始构建。
- 开始编译: 用上一步得到的参数初始Compiler对象,加载所有配置的插件,通 过执行对象的run方法开始执行编译
- 确定入口: 根据配置中的 Entry 找出所有入口文件
- 编译模块: 从入口文件出发,调用所有配置的 Loader 对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
- 完成模块编译: 在经过第4步使用 Loader 翻译完所有模块后, 得到了每个模块被编译后的最终内容及它们之间的依赖关系
- 输出资源 :根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再将每个 Chunk 转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会
- 输出完成: 在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统中。
在以上过程中, Webpack 会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,井且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。其实以上7个步骤,可以简单归纳为初始化、编译、输出,三个过程,而这个过程其实就是前面说的基本模型的扩展。
实现一个丐版Webpack
该工具可以实现以下两个功能
● 将 ES6 转换为 ES5
● 支持在 JS 文件中 import CSS 文件
通过这个工具的实现,可以更好地理解打包工具背后的运行原理。
开始
由于需要将 ES6 转换为 ES5,我们首先需要安装一些 Babel 相关的依赖包:
yarn add babylon babel-traverse babel-core babel-preset-env
接下来我们将这些工具引入文件中
const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const { transformFromAst } = require('babel-core')
第一步:首先,实现如何使用 Babel 解析并转换代码
function readCode(filePath) {// 读取文件内容const content = fs.readFileSync(filePath, 'utf-8')// 生成 ASTconst ast = babylon.parse(content, {sourceType: 'module'})// 寻找当前文件的依赖关系const dependencies = []traverse(ast, {ImportDeclaration: ({ node }) => {dependencies.push(node.source.value)}})// 通过 AST 将代码转为 ES5const { code } = transformFromAst(ast, null, {presets: ['env']})return {filePath,dependencies,code}
}
● 首先我们传入一个文件路径参数,通过 fs 模块读取其内容。
● 接下来我们通过 babylon 解析代码生成抽象语法树(AST),用于分析是否存在其他导入文件。
● 通过 babel-traverse 遍历 AST,提取出所有依赖路径。
● 通过 dependencies 来存储文件中的依赖,最终调用 transformFromAst 将 AST 转换为 ES5 代码。
● 最后函数返回了一个对象,对象中包含了当前文件路径、当前文件依赖和当前文件转换后的代码
接下来我们需要构建一个函数来处理整个依赖图谱,这个函数的功能有以下几点
● 调用 readCode 函数,传入入口文件
● 分析入口文件的依赖
● 识别 JS 和 CSS 文件
function getDependencies(entry) {// 读取入口文件const entryObject = readCode(entry)const dependencies = [entryObject]// 遍历所有文件依赖关系for (const asset of dependencies) {// 获得文件目录const dirname = path.dirname(asset.filePath)// 遍历当前文件依赖关系asset.dependencies.forEach(relativePath => {// 获得绝对路径const absolutePath = path.join(dirname, relativePath)// CSS 文件逻辑就是将代码插入到 `style` 标签中if (/\.css$/.test(absolutePath)) {const content = fs.readFileSync(absolutePath, 'utf-8')const code = `const style = document.createElement('style')style.innerText = ${JSON.stringify(content).replace(/\\r\\n/g, '')}document.head.appendChild(style)`dependencies.push({filePath: absolutePath,relativePath,dependencies: [],code})} else {// JS 代码需要继续查找是否有依赖关系const child = readCode(absolutePath)child.relativePath = relativePathdependencies.push(child)}})}return dependencies
}
● 首先我们读取入口文件,然后创建一个数组,该数组的目的是存储代码中涉及到的所有文件
● 接下来我们遍历这个数组,一开始这个数组中只有入口文件,在遍历的过程中,如果入口文件有依赖其他的文件,那么就会被 push 到这个数组中
● 在遍历的过程中,我们先获得该文件对应的目录,然后遍历当前文件的依赖关系
● 在遍历当前文件依赖关系的过程中,首先生成依赖文件的绝对路径,然后判断当前文件是 CSS 文件还是 JS 文件
● 如果是 CSS 文件的话,我们就不能用 Babel 去编译了,只需要读取 CSS 文件中的代码,然后创建一个 <style>
标签,将代码插入进标签并且放入 head 中即可
● 如果是 JS 文件的话,我们还需要分析 JS 文件是否还有别的依赖关系
● 最后将读取文件后的对象 push 进数组中,此时已经获取一个包含所有依赖项的对象数组。
● 现在我们已经获取到了所有的依赖文件,接下来就是实现打包的功能了
第三步:打包依赖,模拟 CommonJS 运行环境
现在我们已经收集了完整的依赖图,下一步是将这些模块打包成一个可以在浏览器中运行的单文件。
function bundle(dependencies, entry) {let modules = ''// 构建函数参数,生成的结构为// { './entry.js': function(module, exports, require) { 代码 } }dependencies.forEach(dep => {const filePath = dep.relativePath || entrymodules += `'${filePath}': (function (module, exports, require) { ${dep.code} }),`})// 构建 require 函数,目的是为了获取模块暴露出来的内容const result = `(function(modules) {function require(id) {const module = { exports : {} }modules[id](module, module.exports, require)return module.exports}require('${entry}')})({${modules}})`// 当生成的内容写入到文件中fs.writeFileSync('./bundle.js', result)
}
这段代码需要结合着 Babel 转换后的代码来看,这样大家就能理解为什么需要这样写了,
代码结构与 Babel 编译后的 CommonJS 代码相对应,目的是在浏览器端模拟模块化运行环境。
示例:Babel 转换后的代码如下
// entry.js
var _a = require('./a.js')
var _a2 = _interopRequireDefault(_a)
function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj }
}
console.log(_a2.default)
// a.js
Object.defineProperty(exports, '__esModule', {value: true
})
var a = 1
exports.default = a
Babel 将我们 ES6的模块化代码转换为了 CommonJS的代码,但是浏览器是不支持 CommonJS 的,所以如果这段代码需要在浏览器环境下运行的话,我们需要手动实现 CommonJS 相关的类似机制,这就是 bundle 函数做的大部分事情。bundle 函数正是为此而设计。
接下来我们再来逐行解析 bundle 函数
● 首先遍历所有依赖文件,构建出一个函数参数对象
● 对象的属性就是当前文件的相对路径,属性值是一个函数,函数体是当前文件下的代码,函数接受三个参数 module、exports、 require
○ module 参数对应 CommonJS 中的 module
○ exports 参数对应 CommonJS 中的 module.export
○ require 参数对应我们自己创建的 require 函数
● 接下来就是构造一个使用参数的函数了,函数做的事情很简单,就是内部创建一个 require函数,然后调用 require(entry),也就是 require(‘./entry.js’),这样就会从函数参数中找到 ./entry.js 对应的函数并执行,最后将导出的内容通过 module.export 的方式让外部获取到
● 最后再将打包出来的内容写入到单独的文件中
如果你对于上面的实现还有疑惑的话,可以阅读下打包后的部分简化代码
;(function(modules) {function require(id) {// 构造一个 CommonJS 导出代码const module = { exports: {} }// 去参数中获取文件对应的函数并执行modules[id](module, module.exports, require)return module.exports}require('./entry.js')
})({'./entry.js': function(module, exports, require) {// 这里继续通过构造的 require 去找到 a.js 文件对应的函数var _a = require('./a.js')console.log(_a2.default)},'./a.js': function(module, exports, require) {var a = 1// 将 require 函数中的变量 module 变成了这样的结构// module.exports = 1// 这样就能在外部取到导出的内容了exports.default = a}// 省略
})
尽管这个“丐版 Webpack”仅用了不到百行代码实现,但它涵盖了现代打包工具的核心思想:
● 找出入口文件所有的依赖关系。
● 将不同类型的资源统一处理
● 然后通过构建 CommonJS 代码来获取 exports 导出的内容。
这为我们理解打包工具的工作原理提供了很好的入门视角。
如果你觉得这篇文章对你有帮助,请点赞 👍、收藏 👏 并关注我!👀
相关文章:
深入理解 Webpack 核心机制与编译流程
🤖 作者简介:水煮白菜王,一位前端劝退师 👻 👀 文章专栏: 前端专栏 ,记录一下平时在博客写作中,总结出的一些开发技巧和知识归纳总结✍。 感谢支持💕💕&#…...
okhttp3.Interceptor简介-笔记
1. Interceptor 简介 okhttp3.Interceptor 是 OkHttp 提供的一个核心接口,用于拦截 HTTP 请求和响应,允许开发者在请求发送前和响应接收后插入自定义逻辑。它在构建灵活、可扩展的网络请求逻辑中扮演着重要角色。常见的用途包括: 添加请求头…...
交易流水表的分库分表设计
交易流水表的分库分表设计需要结合业务特点、数据增长趋势和查询模式,以下是常见的分库分表策略及实施建议: 一、分库分表核心目标 解决性能瓶颈:应对高并发写入和查询压力。数据均衡分布:避免单库/单表数据倾斜。简化运维&#…...
《AI大模型应知应会100篇》第59篇:Flowise:无代码搭建大模型应用
第59篇:Flowise:无代码搭建大模型应用 摘要:本文将详细探讨 Flowise 无代码平台的核心特性、使用方法和最佳实践,提供从安装到部署的全流程指南,帮助开发者和非技术用户快速构建复杂的大模型应用。文章结合实战案例与配…...
开发环境(Development Environment)
在软件开发与部署过程中,通常会划分 开发环境(Development)、测试环境(Testing)、生产环境(Production) 这三个核心环境,以确保代码在不同阶段的质量和稳定性。以下是它们的详细介绍…...
MySQL的sql_mode详解:从优雅草分发平台故障谈数据库模式配置-优雅草卓伊凡
MySQL的sql_mode详解:从优雅草分发平台故障谈数据库模式配置-优雅草卓伊凡 引言:优雅草分发平台的故障与解决 近日,优雅草分发平台(youyacaocn)在运行过程中遭遇了一次数据库访问故障。在排查过程中,技术…...
PyCharm 快捷键指南
PyCharm 快捷键指南 常用编辑快捷键 代码完成:Ctrl Space 提供基本的代码完成选项(类、方法、属性)导入类:Ctrl Alt Space 快速导入所需类语句完成:Ctrl Shift Enter 自动结束代码(如添加分号&#…...
【数据结构】map_set前传:二叉搜索树(C++)
目录 二叉搜索树K模型的模拟实现 二叉搜索树的结构: Insert()插入: InOrder()中序遍历: Find()查找: Erase()删除: 参考代码: 二叉搜索树K/V模型的模拟实现: K/V模型的简单应用举例&…...
ZYNQ处理器在发热后功耗增加的原因分析及解决方案
Zynq处理器(结合ARM Cortex-A系列CPU和FPGA可编程逻辑)在发热后功耗增大的现象,通常由以下原因导致。以下是系统性分析及解决方案: 1. 根本原因分析 现象物理机制漏电流(Leakage Current)增加温度升高导致…...
Vue学习百日计划-Deepseek版
阶段1:基础夯实(Day 1-30) 目标:掌握HTML/CSS/JavaScript基础,理解Vue核心概念和基础语法。 每日学习内容(2小时): HTML/CSS(Day 1-10) 学习HTML标签语义化…...
DeepSeek-R1-Distill-Qwen-1.5B代表什么含义?
DeepSeek‑R1‑Distill‑Qwen‑1.5B 完整释义与合规须知 一句话先行 这是 DeepSeek‑AI 把自家 R1 大模型 的知识,通过蒸馏压缩进一套 Qwen‑1.5B 架构 的轻量学生网络,并以宽松开源许可证发布的模型权重。 1 | 名字逐段拆解 片段意义备注DeepSee…...
内网服务器之间传输单个大文件最佳解决方案
内网服务器之间传输单个大文件,采用python的http.server模块,结合wget下载文件是最快的传输方案。 笔者在ubuntu与debian之间传输单个单文件进行文件,尝试了scp、sftp、rsync等方案,但传输速度都只有1-3MB/秒;采用pyt…...
Linux常用命令详解(上):目录与文件操作及拷贝移动命令
Linux系统以其强大的命令行工具著称,无论是日常文件管理还是自动化运维,都离不开基础命令的灵活运用。本文将通过功能说明、语法格式、常用选项和实例演示,系统讲解Linux中目录操作、文件操作及拷贝移动的核心命令。 一、目录操作命令 1. c…...
可灵 AI:开启 AI 视频创作新时代
在当今数字化浪潮中,人工智能(AI)技术正以前所未有的速度渗透到各个领域,尤其是在内容创作领域,AI 的应用正引发一场革命性的变革。可灵 AI 作为快手团队精心打造的一款前沿 AI 视频生成工具,宛如一颗璀璨的…...
动态域名解析(DDNS)实战指南,原理、配置与远程访问,附无公网ip方案
本文从实际场景出发,详解如何通过动态域名(DDNS)解决动态IP访问难题,覆盖家庭、企业及IoT场景,并提供动态域名解析、内网端口映射外网远程访问等方案。 一、动态域名(DDNS)是什么?它…...
基于STM32、HAL库的BMP388 气压传感器 驱动程序设计
一、简介: BMP388是Bosch Sensortec推出的一款高精度、低功耗的数字气压传感器,具有以下特点: 压力测量范围:300hPa至1250hPa 相对精度:0.08hPa(相当于0.5米) 温度测量范围:-40C至+85C 工作电压:1.65V至3.6V 低功耗:2μA @1Hz采样率 支持I2C和SPI接口(最高10MHz) …...
window 显示驱动开发-指定 DMA 缓冲区的段
显示微型端口驱动程序可以指定可从中分配 DMA 缓冲区的光圈段。 DMA 缓冲区也可以分配为连续锁定的系统内存。 当应用程序需要 DMA 缓冲区时,视频内存管理器会分配和销毁这些缓冲区。 因此,视频内存管理器需要一组可以分配 DMA 缓冲区的段。 请注意&…...
AnaTraf:深度解析网络性能分析(NPM)
目录 一、为什么网络性能分析比你想象的重要? 二、网络性能分析的核心构成 1. 数据采集层 2. 数据分析层 3. 可视化与告警层 三、网络性能分析中关注的关键指标 四、NPM部署策略:选对位置,才能看清全局 1. 边缘部署 2. 核心网络部署…...
安装Python和配置开发环境
用ChatGPT做软件测试 “工欲善其事,必先利其器。” 学习编程,不只是下载安装一个解释器,更是打开一个技术世界的大门。配置开发环境不仅关乎效率,更关乎思维方式、习惯培养与未来技术路线的选择。 一、为什么安装Python不仅仅是“…...
n8n 修改或者智能体用文档知识库创建pdf
以下是对 Nextcloud、OnlyOffice、Seafile、Etherpad、BookStack 和 Confluence 等本地部署文档协作工具的综合评测、对比分析和使用推荐,帮助您根据不同需求选择合适的解决方案。 🧰 工具功能对比 工具名称核心功能本地部署支持适用场景优势与劣势Next…...
Python | Dashboard制作 【待续】
运行环境:jupyter notebook (python 3.12.7)...
Linux 详解inode
目录 一、inode是什么? inode包含的主要信息(inode是一个结构体): 硬链接计数(有多少个文件名指向这个inode) inode的特点: inode编号 二、block区 定义与作用 特点…...
Milvus 2.4 使用详解:从零构建向量数据库并实现搜索功能(Python 实战)
文章目录 🌟 引言🧰 环境准备依赖安装 📁 整体代码结构概览🛠️ 核心函数详解1️⃣ 初始化 Milvus 客户端2️⃣ 创建集合 Schema3️⃣ 准备索引参数4️⃣ 删除已存在的集合(可选)5️⃣ 创建集合并建立索引6…...
NY115NY121美光科技芯片NY122NY130
NY115NY121美光科技芯片NY122NY130 美光科技:存储芯片领域的领航者 在全球半导体产业竞争日益激烈的背景下,美光科技(Micron)作为存储技术领域的领先企业,不仅展现了其强大的科技研发力量,更在战略布局上…...
【类拷贝文件的运用】
常用示例 当我们面临将文本文件分成最大大小块的时,我们可能会尝试编写如下代码: public class TestSplit {private static final long maxFileSizeBytes 10 * 1024 * 1024; // 默认10MBpublic void split(Path inputFile, Path outputDir) throws IOException {…...
python标准库--heapq - 堆队列算法(优先队列)在算法比赛的应用
目录 一、基本操作 1.构造堆 2.访问堆顶元素(返回堆顶元素) 3.删除堆顶元素(返回堆顶元素) 4.插入新元素,时间复杂度为 O (log n) 5. 插入并删除元素(高效操作) 6. 高级操作- 合并多个有…...
5.12第四次作业
实验要求:完成上图内容,要求五台路由器的环回地址均可以相互访问 AR1 AR2 AR3 AR4 AR5 AS 200 ospf配置 AR2 AR3 AR4 BGP配置 AR1(AS100) AR2(AS200) AR4 AR5(AS300) 结果...
一文读懂如何使用MCP创建服务器
如果你对MCP(模型上下文协议)一窍不通,在阅读本篇文章之前(在获得对MCP深度认识之前),你可以理解为学习MCP就是在学习一个python工具库mcp,类似于其它python工具库一样,如numpy、sys…...
telnetlib源码深入解析
telnetlib 是 Python 标准库中实现 Telnet 客户端协议的模块,其核心是 Telnet 类。以下从 协议实现、核心代码逻辑 和 关键设计思想 三个维度深入解析其源码。 一、Telnet 协议基础 Telnet 协议基于 明文传输,通过 IAC(Interpret As Command…...
PID与模糊PID系统设计——基于模糊PID的水下航行器运动控制研究Simulink仿真(包含设计报告)
1.模型简介 本仿真模型基于MATLAB/Simulink(版本MATLAB 2016Rb)软件。建议采用matlab2016 Rb及以上版本打开。(若需要其他版本可联系代为转换) 针对水下航行器控制系统参数变化和海洋环境干扰等影响,研究水下航行器运…...
GPU SIMT架构的极限压榨:PTX汇编指令级并行优化实践
点击 “AladdinEdu,同学们用得起的【H卡】算力平台”,H卡级别算力,按量计费,灵活弹性,顶级配置,学生专属优惠。 一、SIMT架构的调度哲学与寄存器平衡艺术 1.1 Warp Scheduler的调度策略解构 在NVIDIA GPU…...
spark的处理过程-转换算子和行动算子
(一)RDD的处理过程 【老师讲授,画图】 Spark使用Scala语言实现了RDD的API,程序开发者可以通过调用API对RDD进行操作处理。RDD的处理过程如图所示; RDD经过一系列的“转换”操作,每一次转换都会产生不同的RDD…...
设计杂谈-工厂模式
“工厂”模式在各种框架中非常常见,包括 MyBatis,它是一种创建对象的设计模式。使用工厂模式有很多好处,尤其是在复杂的框架中,它可以带来更好的灵活性、可维护性和可配置性。 让我们以 MyBatis 为例,来理解工厂模式及…...
职坐标IT培训:互联网行业核心技能精讲
在互联网行业高速迭代的今天,掌握全链路核心技能已成为职业发展的关键突破口。职坐标IT培训聚焦行业需求,系统拆解从需求分析到系统部署的完整能力模型,助力从业者构建多维竞争力。无论是产品岗的用户调研与原型设计,还是技术岗的…...
IBM BAW(原BPM升级版)使用教程第十二讲
续前篇! 一、用户界面:Process Portal和Workplace Process Portal 和 Workplace 都是 IBM Business Automation Workflow (BAW) 中提供的 Web 界面,供用户查看和处理流程任务、监控流程状态等,但它们之间有着不同的历史背景和功…...
2025 年福建省职业院校技能大赛网络建设与运维赛项Linux赛题解析
准备环境:系统安装及网络配置 [!TIP] 接下来将完全按照国赛评分标准进行,过程中需要掌握基础的Linux命令以及理解Linux系统,建议大家在做题前将Linux基础命令熟练运用 网络建设与运维赛项详细教程请联系主页一、X86架构计算机操作系统安装…...
Netty在Java网络编程中的应用:实现高性能的异步通信
Netty在Java网络编程中的应用:实现高性能的异步通信 在当今的分布式系统中,高效、稳定的网络通信是保障系统运行的关键。Java作为一门广泛使用的编程语言,提供了多种网络编程方式,但传统的Socket编程在面对高并发场景时往往显得力…...
[高阶数据结构]二叉树经典面试题
二叉树经典面试题:: 目录 二叉树经典面试题:: 1.根据二叉树创建字符串 2.二叉树的层序遍历 3.二叉树的层序遍历II 4.二叉树的最近公共祖先 5.二叉树与双向链表 6.从前序与中序序列构造二叉树 7.从中序与后序序列构造二叉…...
第一章 应急响应-webshell查杀
远程连接一下 我们先查找一下网站的目录,到网站页面,可以看到有很多php文件,这样我们可以大致确定黑客上传的应该是php木马 通过ls -a 查看一下隐藏文件 现在我们查看一下各个php文件的内容 可以看到shell.php是一句话木马,但没…...
残差网络(ResNet)
残差网络(Residual Network, ResNet)介绍 残差网络(ResNet)是由微软研究院的何恺明(Kai Ming He)等人于2015年提出的深度卷积神经网络架构,其核心思想是通过残差连接(Skip Connectio…...
全视通智慧病房无感巡视解决方案:科技赋能,重塑护理巡视新篇
护理巡视是保障患者安全与护理质量的关键环节。现有特级、一、二、三级护理虽有明确巡视要求,但从护士手工填写记录表,均存在诸多弊端。或因需人工操作易遗忘、无法准确界定巡视人员,或因设备携带不便、需额外充电、布网复杂等,导…...
【数据结构入门训练DAY-32】LETTERS
本文介绍了一个关于使用深度优先搜索(DFS)解决字母矩阵问题的训练内容。题目要求在一个RS的大写字母矩阵中,从左上角开始移动,可以上下左右四个方向移动,但不能重复经过相同的字母,目标是找出最多能经过的不…...
Linux笔记---信号(上)
1. 信号的概念 Linux下的信号机制是一种进程间通信(IPC)的方式,用于在不同进程之间传递信息。 信号是一种异步的信息传递方式,这意味着发送信号的进程只发送由信号作为载体的命令,而并不关心接收信号的进程如何处置这…...
FanControl(电脑风扇转速控制软件) v224 中文版
FanControl是一款用于控制计算机风扇速度的软件。它能够监测计算机的内部温度,并根据温度的变化来自动调整风扇的速度,以保持计算机的散热效果和稳定运行。 软件功能 温度监测:实时监测计算机的内部温度,显示在界面上。 风扇速度控…...
推理加速新范式:火山引擎高性能分布式 KVCache (EIC)核心技术解读
资料来源:火山引擎-开发者社区 分布式 KVCache 的兴起 背景 在大模型领域,随着模型参数规模的扩大和上下文长度增加,算力消耗显著增长。在 LLM 推理过程中,如何减少算力消耗并提升推理吞吐已经成为关键性优化方向。以多轮对话场…...
2025年5月12日第一轮
1.百词斩 2.阅读 3.翻译 4.单词 radical 激进的 Some people in the US have asserted that forgiving student loan debt is one way to stimulate the economy and give assistance to those in need. 1.数学 Hainan was the second island on the Taiwan,a province whi…...
Spark目前支持的部署模式。
一、本地模式(Local Mode) 特点: 在单台机器上运行,无需集群。主要用于开发、测试和调试。所有组件(Driver、Executor)在同一个 JVM 中运行。 启动命令: bash spark-submit --master local[*]…...
如何理解“数组也是对象“——Java中的数组
在Java中,数组确实是一种特殊的对象,这一点经常让初学者感到困惑。本文将深入探讨数组的对象本质,并通过代码示例展示数组作为对象的特性。 数组是对象的证据 1. 数组继承自Object类 所有Java数组都隐式继承自java.lang.Object类ÿ…...
第二章、物理层
目录 2.1、物理层的基本概念 2.2、数据通信的基础知识 2.2.1、数据通信系统的模型 2.2.2、有关信道的几个基本概念 调制的方法 常用的编码方式 基本的带通调制 2.2.3、信道的极限容量 信道能够通过的频率范围 2.3、物理层下面的传输媒介 2.3.1、导引型传输媒体 &…...
UART16550 IP core笔记二
XIN时钟 表示use external clk for baud rate选型,IP核会出现Xin时钟引脚 XIN输入被外部驱动,也就是外部时钟源,那么外部时钟必须要满足特定的要求,就是XIN 的range范围是xin<S_AXI_CLK/2,如果不满足这个条件,那么A…...