深入理解 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

源码解读

  1. 初始化启动之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。

  1. 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钩子,它的作用是清除缓存。

  1. 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);
  1. 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 导出的内容。

这为我们理解打包工具的工作原理提供了很好的入门视角。

如果你觉得这篇文章对你有帮助,请点赞 👍、收藏 👏 并关注我!👀

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/80011.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

概率相关问题

问题汇总 1. 贝叶斯定理&#xff08;贝叶斯公式和全概率公式&#xff09;2. 概率题2.1 随机发生器的概率为1/2 1. 贝叶斯定理&#xff08;贝叶斯公式和全概率公式&#xff09; 定义&#xff1a;在信息和条件有限的情况下&#xff0c;基于过去的数据&#xff0c;通过动态调整的…

【系统架构师】2025论文《WEB系统性能优化技术》

&#x1f60a;你好&#xff0c;我是小航&#xff0c;一个正在变秃、变强的文艺倾年。 &#x1f514;本文分享【系统架构师】2025论文《系统可靠性设计》&#xff0c;期待与你一同探索、学习、进步&#xff0c;一起卷起来叭&#xff01; 目录 项目介绍背景介绍系统模块技术栈性能…

ADS1220高精度ADC(TI)——应用 源码

文章目录 德州仪器ADS1220概述资料引脚&封装布线寄存器配置寄存器0&#xff08;00h&#xff09;配置寄存器1&#xff08;01h&#xff09;配置寄存器2&#xff08;02h&#xff09;配置寄存器3&#xff08;03h&#xff09; 连续转换流程驱动源码ads1220.cads1220.h 德州仪器A…

Uniapp 安卓实现讯飞语音听写(复制即用)

在移动应用开发中&#xff0c;语音交互功能能够极大提升用户体验&#xff0c;让操作更加便捷自然。讯飞语音听写技术凭借其高准确率和稳定性&#xff0c;成为众多开发者的选择。本文将详细介绍如何在 Uniapp 项目中&#xff0c;实现安卓端的讯飞语音听写功能&#xff0c;帮助你…

【golang】DNS 资源记录(RR)接口

Go 中 miekg/dns 包对 DNS 资源记录&#xff08;RR&#xff09;接口 的定义&#xff1a; type RR interface {Header() *RR_HeaderString() stringcopy() RRlen(off int, compression map[string]struct{}) intpack(...)unpack(...)parse(...)isDuplicate(r2 RR) bool }这个接…

16.2 VDMA视频转发实验之模拟源

文章目录 1 实验任务2 系统框图3 硬件设计3.1 IP核配置3.2 注意事项3.3 自定义IP核源码 4 软件设计4.1 注意事项4.2 工程源码4.2.1 main.c文件 1 实验任务 基于14.1&#xff0c;相较于16.1&#xff0c;使用自定义IP核vid_gen_motion替换Xilinx TPG IP核。 2 系统框图 基于14…

深度学习之用CelebA_Spoof数据集搭建一个活体检测-训练好的模型用MNN来推理

一、模型转换准备 首先确保已完成PyTorch到ONNX的转换&#xff1a;深度学习之用CelebA_Spoof数据集搭建活体检测系统&#xff1a;模型验证与测试。这里有将PyTorch到ONNX格式的模型转换。 二、ONNX转MNN 使用MNN转换工具进行格式转换&#xff1a;具体的编译过程可以参考MNN的…

JVM学习专题(一)类加载器与双亲委派

目录 1、JVM加载运行全过程梳理 2、JVM Hotspot底层 3、war包、jar包如何加载 4、类加载器 我们来查看一下getLauncher&#xff1a; 1.我们先查看getExtClassLoader() 2、再来看看getAppClassLoader(extcl) 5、双亲委派机制 1.职责明确&#xff0c;路径隔离​&#xff…

部署安装gitlab-ce-17.9.7-ce.0.el8.x86_64.rpm

目录 ​编辑 实验环境 所需软件 实验开始 安装部署gitlab171.配置清华源仓库&#xff08;版本高的系统无需做&#xff09;vim /etc/yum.repos.d/gitlab-ce.repo 2.提前下载包dnf localinstall gitlab-ce-17.9.7-ce.0.el8.x86_64.rpm --rocklinux 3.修改配…

使用LoRA微调Qwen2.5-VL-7B-Instruct完成电气主接线图识别

使用LoRA微调Qwen2.5-VL-7B-Instruct完成电气主接线图识别 动机 任务适配需求 Qwen2.5-VL在视觉理解方面表现优异&#xff0c;但电气主接线图识别需要特定领域的结构化输出能力&#xff08;如设备参数提取、拓扑关系解析&#xff09;。微调可增强模型对专业符号&#xff08;如…

系统集成项目管理工程师学习笔记

第九章 项目管理概论 1、项目基本要素 项目基础 项目是为创造独特的产品、服务或成果而进行的临时性工作。 项目具有临时性、独特性、渐进明细的特点。项目的“临时性”是指项目只有明确的起点和终点。“临时性”并一定意味着项目的持续时间短。 项目可宣告结束的情况&…

Secs/Gem第七讲(基于secs4net项目的ChatGpt介绍)

好的&#xff0c;那我们现在进入&#xff1a; 第七讲&#xff1a;掉电重连后&#xff0c;为什么设备不再上报事件&#xff1f;——持久化与自动恢复的系统设计 关键词&#xff1a;掉电恢复、状态重建、初始化流程、SecsMessage 缓存机制、自动重连、事件再注册 本讲目标 你将理…

室内定位:热门研究方向与未解难题深度解析

I. 引言:对普适性室内定位的持续探索 A. 室内定位在现代应用中的重要性 室内定位系统(IPS)正迅速成为众多应用领域的基石技术,其重要性源于现代社会人们约70%至90%的时间在室内度过的事实 1。这些应用横跨多个行业,包括应急响应 1、智能建筑与智慧城市 6、医疗健康(如病…

Android学习总结之Glide自定义三级缓存(实战篇)

一、为什么需要三级缓存 内存缓存&#xff08;Memory Cache&#xff09; 内存缓存旨在快速显示刚浏览过的图片&#xff0c;例如在滑动列表时来回切换的图片。在 Glide 中&#xff0c;内存缓存使用 LruCache 算法&#xff08;最近最少使用&#xff09;&#xff0c;能自动清理长…

Linux的文件查找与压缩

查找文件 find命令 # 命令&#xff1a;find 路径范围 选项1 选项1的值 \[选项2 选项2 的值…]# 作用&#xff1a;用于查找文档&#xff08;其选项有55 个之多&#xff09;# 选项&#xff1a;# -name&#xff1a;按照文档名称进行搜索&#xff08;支持模糊搜索&#xff0c;\* &…

python处理异常,JSON

异常处理 #异常处理 # 在连接MySQL数据库的过程中&#xff0c;如果不能有效地处理异常&#xff0c;则异常信息过于复杂&#xff0c;对用户不友好&#xff0c;暴露过多的敏感信息 # 所以&#xff0c;在真实的生产环境中&#xff0c; 程序必须有效地处理和控制异常&#xff0c;按…

线程的两种实现方式

线程的两种实现方式——内核支持线程&#xff08;kernal Supported Thread, KST&#xff09;&#xff0c; 用户级线程&#xff08;User Level Thread, ULT&#xff09; 1. 内核支持线程 顾名思义&#xff0c;内核支持线程即为在内核支持下的那些线程&#xff0c;它们的创建&am…

vue3基础学习(上) [简单标签] (vscode)

目录 1. Vue简介 2. 创建Vue应用 2.1 下载JS文件 2.2 引用JS文件 2.3 调用Vue方法​编辑 2.4 运行一下试试: 2.5 代码如下 3.模块化开发模式 3.1 Live Server插件 3.2 运行 4. 常用的标签 4.1 reactive 4.1.1 运行结果 4.1.2 代码: 4.2 ref 4.2.1 运行结果 4.2.2…

自定义分区器-基础

什么是分区 在 Spark 里&#xff0c;弹性分布式数据集&#xff08;RDD&#xff09;是核心的数据抽象&#xff0c;它是不可变的、可分区的、里面的元素并行计算的集合。 在 Spark 中&#xff0c;分区是指将数据集按照一定的规则划分成多个较小的子集&#xff0c;每个子集可以独立…

深入解析HTTP协议演进:从1.0到3.0的全面对比

HTTP协议作为互联网的基础协议&#xff0c;经历了多个版本的迭代演进。本文将详细解析HTTP 1.0、HTTP 1.1、HTTP/2和HTTP/3的核心特性与区别&#xff0c;帮助开发者深入理解网络协议的发展脉络。 一、HTTP 1.0&#xff1a;互联网的奠基者 核心特点&#xff1a; 短连接模式&am…