webpack 到底是如何工作的,原理是什么?了解了这些原理,我们应该如何扩展,以解决工作中的实际问题?
吐血整理了一波 webpack 经典面试题 + 手写基本原理.
一、webpack运行流程
webpack的运行过程可以简单概述如下:
初始化配置参数 -> 绑定事件钩子回调 -> 确定Entry逐一遍历 -> 使用loader编译文件 -> 输出文件.
具体流程:
-
首先,webpack会读取你在命令行传入的配置以及项目里的 webpack.config.js 文件,初始化本次构建的配置参数,并且执行配置文件中的插件实例化语句,生成Compiler传入plugin的apply方法,为webpack事件流挂上自定义钩子.
-
接下来到了entryOption阶段,webpack开始读取配置的Entries,递归遍历所有的入口文件
-
Webpack接下来就开始了compilation过程。会依次进入其中每一个入口文件(entry),先使用用户配置好的loader对文件内容进行编译(buildModule),我们可以从传入事件回调的compilation上拿到module的resource(资源路径)、loaders(经过的loaders)等信息;之后,再将编译好的文件内容使用acorn解析生成AST静态语法树(normalModuleLoader),分析文件的依赖关系逐个拉取依赖模块并重复上述过程,最后将所有模块中的require语法替换成__webpack_require__来模拟模块化操作。
-
emit阶段,所有文件的编译及转化都已经完成,包含了最终输出的资源,我们可以在传入事件回调的compilation.assets 上拿到所需数据,其中包括即将输出的资源、代码块Chunk等等信息。
二、webpack打包原理
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 getModuleInfo = (file) => {
const body = fs.readFileSync(file, 'utf-8')
// console.log(body)
// 1 将我们提供的代码解析成完整的ECMAScript代码的AST
const ast = parser.parse(body,{
sourceType: 'module'
})
// console.log(ast.program.body)
// 2 遍历AST,将用到的依赖收集起来
const deps = {}
traverse(ast, {
ImportDeclaration({node}) {
const dirname = path.dirname(file)
// value指的就是import后面的 './add.js'
const abspath = './' + path.join(dirname, node.source.value)
deps[node.source.value] = abspath
}
})
console.log(deps)
// 3 把获得的ES6的AST转化成ES5
const {code} = babel.transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
// console.log(code)
const moduleInfo = {file, deps, code}
return moduleInfo;
}
// 4,获取依赖, 类似一个层序遍历
const parseModules = (file) => {
const entry = getModuleInfo(file)
const temp = [entry]
const depsGraph = {} // map
for(let i = 0; i < temp.length; i++) {
const deps = temp[i].deps
if(deps) {
for(const key in deps) {
if(deps.hasOwnProperty(key)) {
temp.push(getModuleInfo(deps[key]))
}
}
}
}
temp.forEach(moduleInfo => {
depsGraph[moduleInfo.file] = {
deps: moduleInfo.deps,
code: moduleInfo.code
}
})
// console.log(depsGraph)
return depsGraph;
}
// 5, 把index.js的内容和它的依赖模块整合起来。然后把代码写到一个新建的bundle.js文件
// 浏览器不识别执行require和exports, 需要定义
const bundle = (file) => {
const depsGraph = JSON.stringify(parseModules(file))
// eval(code)。也就是执行主模块的code这段代码
return `(function(graph){
function require(file) {
function absRequire(relPath){
return require(graph[file].deps[relPath])
}
var exports = {};
(function(require, exports, code){
eval(code)
})(absRequire, exports, graph[file].code)
return exports;
}
require('${file}')
})(${depsGraph})`
}
const content = bundle('./src/index.js')
console.log(content)
fs.mkdirSync('./dist')
fs.writeFileSync('./dist/bundle.js', content)
三、webpack 将代码编译成了什么?
webpack 打包结果就是一个 IIFE,一般称它为 webpackBootstrap,这个 IIFE 接收一个对象 modules 作为参数,modules 对象的 key 是依赖路径,value 是经过简单处理后的脚本(它不完全等同于我们编写的业务脚本,而是被 webpack 进行包裹后的内容)。
打包结果中,定义了一个重要的模块加载函数 webpack_require。
我们首先使用 webpack_require 加载函数去加载入口模块 ./src/index.js。 加载函数 webpack_require 使用了闭包变量 installedModules,它的作用是将已加载过的模块结果保存在内存中。
四、按需加载打包结果,webpack 又会产出什么样的代码呢?
重新构建后会输出两个文件,分别是执行入口文件 main.js 和异步加载文件 0.js;
main.js:
多了一个 webpack_require.e:它初始化了一个 promise 数组,使用 Promise.all() 进行异步插入 script 脚本;
多了一个 webpackJsonp: 它会挂在到全局对象 window 上,进行模块安装。
五、compiler 和 compilation
compiler 和 compilation 这两个对象是 webpack 核心原理中最重要的概念。它们是理解 webpack 工作原理、loader 和插件工作的基础。
compiler 对象:它的实例包含了完整的 webpack 配置,全局只有一个 compiler 实例,因此它就像 webpack 的骨架或神经中枢。当插件被实例化的时候,会收到一个 compiler 对象,通过这个对象可以访问 webpack 的内部环境。
compilation 对象:当 webpack 以开发模式运行时,每当检测到文件变化,一个新的 compilation 对象将被创建。这个对象包含了当前的模块资源、编译生成资源、变化的文件等信息。也就是说,所有构建过程中产生的构建数据都存储在该对象上,它也掌控着构建过程中的每一个环节。该对象也提供了很多事件回调供插件做扩展。
六、plugin
plugin 的实现可以是一个带有 apply方法 的 类class,使用时传入相关配置来创建一个实例,然后放到配置的 plugins 字段中,而 plugin 实例中最重要的方法是 apply,该方法在 webpack compiler 安装插件时会被调用一次,apply 接收 webpack compiler 对象实例的引用,你可以在 compiler 对象实例上注册各种事件钩子函数,来影响 webpack 的所有构建流程,以便完成更多其他的构建任务。
事件钩子可以理解为当 webpack 运行中执行到某个钩子的状态时,便会触发你注册的事件,即发布订阅模式。
Compiler对象包含了所有的webpack可配置的内容。开发插件时,我们可以从 compiler 对象中拿到所有和 webpack 主环境相关的内容。
compilation 对象包含了当前的模块资源、编译生成资源、文件的变化等。当webpack在开发模式下运行时,每当检测到一个文件发生改变的时候,那么一次新的 Compilation将会被创建。从而生成一组新的编译资源。
Compiler对象 与 Compilation 对象 的区别是:Compiler代表了是整个webpack从启动到关闭的生命周期。Compilation 对象只代表了一次新的编译。
class FileListPlugin {
constructor(options) {
// 读取 plugin 实例化时传入的配置
}
apply(compiler) {
// 在 compiler 的 emit hook 中注册一个方法,当 webpack 执行到该阶段时会调用这个方法
// 在我们的emit钩子事件发生时,表示的含义是:源文件的转换和组装已经完成了,在这里事件钩子里面我们可以读取到最终将输出的资源、代码块、模块及对应的依赖文件。并且我们还可以输出资源文件的内容。
compiler.hooks.emit.tap('FileListPlugin', (compilation) => {
// 给生成的 markdown 文件创建一个简单标题
var filelist = 'In this build:\n\n'
// 遍历所有编译后的资源,每一个文件添加一行说明
for (var filename in compilation.assets) {
filelist += ('- '+ filename +'\n')
}
// 将列表作为一个新的文件资源插入到 webpack 构建结果中
compilation.assets['filelist.md'] = {
source: function() {
return filelist
},
size: function() {
return filelist.length
},
}
})
}
}
module.exports = FileListPlugin
使用
// 假设我们上述那个例子的代码是 ./plugins/FileListPlugin 这个文件
const FileListPlugin = require('./plugins/FileListPlugin.js')
module.exports = {
// ... 其他配置
plugins: [
new FileListPlugin(), // 实例化这个插件,有的时候需要传入对应的配置
],
}
暂时总结这么多,后续会继续更新,可以点个关注或者收藏本文。