整体架构
compiler对象
compilation对象
compiler 算是核心实例,webpack的整个控制器。挂载webpack的整个配置,webpack编译等流程的执行,插件事件的触发和监听。
compilation 是一次构建
编译关键部分
Compiler
继承自Tapable
class Compiler extends Tapable {
constructor(context) {
super();
this.hooks = {
shouldEmit: new SyncBailHook(["compilation"]),
done: new AsyncSeriesHook(["stats"]),
additionalPass: new AsyncSeriesHook([]),
beforeRun: new AsyncSeriesHook(["compilation"]),
run: new AsyncSeriesHook(["compilation"]),
emit: new AsyncSeriesHook(["compilation"]),
afterEmit: new AsyncSeriesHook(["compilation"]),
thisCompilation: new SyncHook(["compilation", "params"]),
compilation: new SyncHook(["compilation", "params"]),
...
}
}
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
if (err) return callback(err);
this.hooks.compile.call(params);
// 生成一个新的compilation
const compilation = this.newCompilation(params);
// make事件
this.hooks.make.callAsync(compilation, err => {
if (err) return callback(err);
compilation.finish();
compilation.seal(err => {
if (err) return callback(err);
// 触发afterCompile事件
this.hooks.afterCompile.callAsync(compilation, err => {
if (err) return callback(err);
return callback(null, compilation);
});
});
});
});
}
}
属性
hooks // Tabable对外的钩子,所有事件都可以在这里统一看到
resolvers //
records
context // 类似process.cwd()
options
方法
constructor
watch
run
createChildCompiler
createCompilation
newCompilation
createNormalModuleFactory
createContextModuleFactory
compile
关键事件 & 编译过程
浅谈Webpack工作流程 | YoungZhang’s Blog 参见本篇文章,写的比较详细的编译过程和相应触发的事件
compile
-> make
-> build module
-> after compile
-> emit
-> after emit
this.hooks.emit.callAsync(compilation, err => {
if (err) return callback(err);
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles);
});
Compilation
class Compilation extends Tapable {
constructor(compiler) {
}
}
属性
hooks //
compiler // 初始化时传入外部的compiler
entries
chunks
modules
cache
records
assets
errors
warnings
children
方法
getStats
addModule
findModule
buildModule
addModuleDependencies
addEntry
prefetch
rebuildModule
finish
unseal
seal
关键事件
Tapable
webpack/tapable: Just a little module for plugins.
关于 tapable 你需要知道这些 - 知乎 非常简单的介绍清楚了Tapable的用户
webpack中特征,在某个事件会注册诸多的插件,插件机制在不同事件有不同的处理机制,因此有了类似EventEmitter,又比起增多了更多编排处理。
- SyncHook 类似EventEmitter
- SyncBailHook 注册的回调返回非undefiend时,就停止不再执行其他回调
SyncWaterfallHook 接受至少一个参数,上一个注册的回调返回值会作为下一个注册的回调的参数。
AsyncParallelHook 异步并发钩子,所有注册回调并发执行,当所有注册的异步回调并行执行完成后,再执行callAsync中的函数
- AsyncSeriesHook 顺序执行钩子,
- AsyncParallelBailHook
- AsyncSeriesBailHook
- AsyncSeriesWaterfallHook
const {
Tapable,
SyncHook, //
SyncBailHook,
AsyncParallelHook,
AsyncSeriesHook
} = require("tapable");
// 继承自Tabple
class Compiler extends Tapable {
constructor(context) {
super();
// hooks的初始化,定义是什么类型的hooks,hooks的参数
this.hooks = {
shouldEmit: new SyncBailHook(["compilation"]),
done: new AsyncSeriesHook(["stats"]),
additionalPass: new AsyncSeriesHook([]),
beforeRun: new AsyncSeriesHook(["compilation"]),
run: new AsyncSeriesHook(["compilation"]),
emit: new AsyncSeriesHook(["compilation"]),
compilation: new SyncHook(["compilation", "params"]),
}
}
}
// 同步的hooks调用
if (this.hooks.shouldEmit.call(compilation) === false) {
}
// 异步的hooks调用
this.hooks.done.callAsync(stats, err => {
});
插件机制
如何编写插件?
- 编写一个类,constructor中处理初始化参数,
- apply的模板方法注入compiler对象,
- compiler对象可供做监听事件钩子
- 插件钩子提供一个回调函数,拿到编译时的compilation对象,可以做各种获取和修改(编译信息,文件内容,等等)
摘自 探寻 webpack 插件机制 的一段代码
class AnalyzeWebpackPlugin {
constructor(opts = { filename: 'analyze.html' }) {
this.opts = opts
}
apply(compiler) {
const self = this
compiler.plugin("emit", function (compilation, callback) {
let stats = compilation.getStats().toJson({ chunkModules: true }) // 获取各个模块的状态
let stringifiedStats = JSON.stringify(stats)
// 服务端渲染
let html = `<!doctype html>
<meta charset="UTF-8">
<title>AnalyzeWebpackPlugin</title>
<style>${cssString}</style>
<div id="App"></div>
<script>window.stats = ${stringifiedStats};</script>
<script>${jsString}</script>
`
compilation.assets[`${self.opts.filename}`] = { // 生成文件路径
source: () => html,
size: () => html.length
}
callback()
})
}
}
探寻 webpack 插件机制
WebPack 插件机制探索 – 滴滴云博客
webpack插件机制剖析 | 大专栏
Loader
阅读 vue-loader
vuejs/component-compiler-utils: Lower level utilities for compiling Vue single file components
vue-loader/lib/index.js
const loaderUtils = require('loader-utils') // 非常重要,官方提供的
const { parse } = require('@vue/component-compiler-utils')
//
module.exports = function (source) {
const loaderContext = this;
const {
target,
request,
minimize,
sourceMap,
rootContext,
resourcePath,
resourceQuery = ''
} = loaderContext
const options = loaderUtils.getOptions(loaderContext) || {} // options.compiler
const descriptor = parse({
source,
compiler: options.compiler || loadTemplateCompiler(loaderContext),
filename,
sourceRoot,
needMap: sourceMap
})
descriptor.styles // <style></style> 可以是多个标签
descriptor.template // <template></template>标签包裹的内容
descriptor.script // <script></script> 标签
}
// 大概转义的结果是
// template
let templateImport;
const src = descriptor.template.src || resourcePath;
const query = `?vue&type=template${idQuery}${scopedQuery}${attrsQuery}${inheritQuery}`;
const request = templateRequest = stringifyRequest(src + query);
templateImport = `import { render, staticRenderFns } from ${request}`;
// script
const request = stringifyRequest(src + query)
scriptImport = (
`import script from ${request}\n` +
`export * from ${request}` // support named exports
)
let code = `
${templateImport}
${scriptImport}
${stylesCode}
/* normalize component */
import normalizer from ${stringifyRequest(`!${componentNormalizerPath}`)}
var component = normalizer(
script,
render,
staticRenderFns,
${hasFunctional ? `true` : `false`},
${/injectStyles/.test(stylesCode) ? `injectStyles` : `null`},
${hasScoped ? JSON.stringify(id) : `null`},
${isServer ? JSON.stringify(hash(request)) : `null`}
${isShadow ? `,true` : ``}
)
`.trim() + `\n`
Plugin的作用?
实践 经常loader和plugin配置
很多的插件是如何loader和plugin配合完成任务,如vue-loader中包含vue-loader和VueLoaderPlugin
MiniCssExtractPlugin | webpack
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [new MiniCssExtractPlugin()],
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
};
vue-loader
实践
编译优化
主要是本地编译速度的优化
webpack 编译优化 · Issue #5 · jpone223/blog
性能优化
为最终线上运行production的优化
- long-term-caching
- chunks 拆包 (可按被引用的次数)
- webpack dll
- tree shaking
查漏补缺 面试题
一些想法
- 不要惧怕看源码,可能并不复杂,搞清楚核心模块,以及核心模块的关系就很好阅读
如webpack先看文章,
- webpack的核心部分还是非常非常清晰的,compiler,compilation