初始化
// webpack.js
const webpack = (options, callback) => {
let compiler
// 补全默认配置
options = new WebpackOptionsDefaulter().process(options)
// 创建 compiler 对象
compiler = new Compiler(options.context)
compiler.options = options
// 应用用户通过 webpack.config.js 配置或命令行参数传递的插件
if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler)
}
}
// 根据配置,应用 webpack 内置插件
compiler.options = new WebpackOptionsApply().process(options, compiler)
// compiler 启动
// 这个方法调用了 compile 方法,而 compile 触发了 make 这个事件,控制权转移到 compilation
compiler.run(callback)
return compiler
}
- npx webpack//npx为npm5.x自带
- webpack 会解析 webpack.config.js 文件,以及命令行参数,将其中的配置和参数合成一个 options 对象
- 创建了 compiler 对象,并将完整的配置参数 options 保存到 compiler 对象中 — 这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin
- 执行plugins的apply
- 用户配置的 plugin 先于内置的 plugin 被应用。
- 最后调用了 compiler 的 run 方法
关于 WebpackOptionsApply.process
// WebpackOptionsApply.js
class WebpackOptionsApply extends OptionsApply {
process(options, compiler) {
new EntryOptionPlugin().apply(compiler)
compiler.hooks.entryOption.call(options.context, options.entry)
}
}
- WebpackOptionsApply (.process)应用了 EntryOptionPlugin 插件并立即触发了 compiler 的 entryOption 事件钩子 (entryOption:钩子事件在new EntryOptionPlugin().apply(compiler)执行后绑定)
// EntryOptionPlugin.js
const itemToPlugin = (context, item, name) => {
if (Array.isArray(item)) {
return new MultiEntryPlugin(context, item, name)
}
return new SingleEntryPlugin(context, item, name)
}
module.exports = class EntryOptionPlugin {
apply(compiler) {
compiler.hooks.entryOption.tap('EntryOptionPlugin', (context, entry) => {
if (typeof entry === 'string' || Array.isArray(entry)) {
// 如果没有指定入口的名字,那么默认为 main
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)
}
// 注意这里返回了 true,
return true
})
}
}
- 而 EntryOptionPlugin 内部则注册了对 entryOption 事件钩子的监听
- entryOption 是个 SyncBailHook, 意味着只要有一个插件返回了 true, 注册在这个钩子上的后续插件代码,将不会被调用。我们在编写小程序插件时,用到了这个特性
- SyncBailHook同步熔断保险钩子,即return一个非undefined的值,则不再继续执行后面的监听函数
// SingleEntryPlugin.js
class SingleEntryPlugin {
constructor(context, entry, name) {
this.context = context
this.entry = entry
this.name = name
}
apply(compiler) {
//触发时机: compiler 在 run 方法中调用了 compile 方法,在该方法中创建了 compilation 对象
compiler.hooks.compilation.tap('SingleEntryPlugin', (compilation, { normalModuleFactory }) => {
// 设置 dependency 和 module 工厂之间的映射关系
compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory)
})
// 触发时机:compiler 创建 compilation 对象后,触发 make 事件
compiler.hooks.make.tapAsync('SingleEntryPlugin', (compilation, callback) => {
const { entry, name, context } = this
// 根据入口文件和名称创建 Dependency 对象
const dep = SingleEntryPlugin.createDependency(entry, name)
// 随着这个方法被调用,将会开启编译流程
compilation.addEntry(context, dep, name, callback)
})
}
static createDependency(entry, name) {
const dep = new SingleEntryDependency(entry)
dep.loc = { name }
return dep
}
}
- compiler.hooks.compilation.tap
- 触发时机: compiler 在 run 方法中调用了 compile 方法,在该方法中创建了 compilation 对象时触发(参考Compiler.js)
- compiler.hooks.make.tapAsync
- compiler 创建 compilation 对象后,触发 make 事件
// Compiler.js
class Compiler extends Tapable {
run(callback) {
const onCompiled = (err, compilation) => {
// ...
}
// 调用 compile 方法
this.compile(onCompiled)
}
compile(callback) {
const params = this.newCompilationParams()
this.hooks.compile.call(params)
// 创建 compilation 对象
const compilation = this.newCompilation(params)
// 触发 make 事件钩子,控制权转移到 compilation,开始编译流程
this.hooks.make.callAsync(compilation, err => {
// ...
})
}
newCompilation(params) {
const compilation = this.createCompilation()
// compilation 对象创建后,触发 compilation 事件钩子
// 如果想要监听 compilation 中的事件,这是个好时机
this.hooks.compilation.call(compilation, params)
return compilation
}
}
- webpack->
- compiler 对象代表了完整的 webpack 环境配置
- 而 compilatoin 对象则负责整个打包过程 , 它存储着打包过程的中间产物
- compiler 对象触发 make 事件后,控制权就会转移到 compilation,compilation 通过调用 addEntry 方法,开始了编译与构建主流程
自定义一个插件
class MinaWebpackPlugin {
constructor() {
this.entries = []
}
// apply 是每一个插件的入口
apply(compiler) {
const { context, entry } = compiler.options
// 找到所有的入口文件,存放在 entries 里面
// 这里订阅了 compiler 的 entryOption 事件,当事件发生时,就会执行回调里的代码
compiler.hooks.entryOption.tap('MinaWebpackPlugin', () => {
xxxxx 执行操作
// 返回 true 告诉 webpack 内置插件就不要处理入口文件了,因为这里已经处理了
return true
})
}
module 构建阶段–loader的使用
class Compilation extends Tapable {
// 如果有留意 SingleEntryPlugin 的源码,应该知道这里的 entry 不是字符串,而是 Dependency 对象
// 根据entry(依赖dependency)获取对应模块工厂(moduleFactory),
//通过模块工厂(moduleFactory.create())创建对一个模块
addEntry(context, entry, name, callback) {
this._addModuleChain(context, entry, onModule, callbak)
}
_addModuleChain(context, dependency, onModule, callback) {
const Dep = dependency.constructor
// 获取模块对应的工厂,这个映射关系在 SingleEntryPlugin 中有设置
const moduleFactory = this.dependencyFactories.get(Dep)
// 通过工厂创建模块
moduleFactory.create(/*
在这个方法的回调中,
调用 this.buildModule 来构建模块。
构建完成后,调用 this.processModuleDependencies 来处理模块的依赖
这是一个循环和递归过程,通过依赖获得它对应的模块工厂来构建子模块,直到把所有的子模块都构建完成
*/)
}
buildModule(module, optional, origin, dependencies, thisCallback) {
// 构建模块
module.build(/*
而构建模块作为最耗时的一步,又可细化为三步:
1. 调用各 loader 处理模块之间的依赖
2. 解析经 loader 处理后的源文件生成抽象语法树 AST
3. 遍历 AST,获取 module 的依赖,结果会存放在 module 的 dependencies 属性中
*/)
}
}
- 在 SingleEntryPlugin.apply中定义 compiler.hooks.compilation.tap 的钩子(run时调用)设置了dependency 和 module 工厂之间的映射关系
- compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory)
- 在 SingleEntryPlugin.apply中定义 compiler.hooks.make.tapAsync的钩子(run时调用)设置了 compilation.addEntry(context, dep, name, callback) 随着这个方法被调用,将会开启编译流程 (addEntry步骤)
- 根据entry(依赖dependency)获取对应模块工厂(const moduleFactory = this.dependencyFactories.get(Dep) ),//映射关系如上 dependencyFactories
- 通过模块工厂(moduleFactory.create())创建对一个模块
- 在这个方法( create )的回调中 调用 this.buildModule 来构建模块。( 步骤:this.buildModule )
- 调用 module.build() // 构建模块 (步骤:module.build())而构建模块作为最耗时的一步,又可细化为三步 ()
- 调用各 loader 处理模块之间的依赖
- 解析经 loader 处理后的源文件生成抽象语法树 AST
- 遍历 AST,获取 module 的依赖,结果会存放在 module 的 dependencies 属性中
- 调用 module.build() // 构建模块 (步骤:module.build())而构建模块作为最耗时的一步,又可细化为三步 ()
- 构建完成后,调用 this.processModuleDependencies 来处理模块的依赖
- 总的来说:以上这是一个循环和递归过程,通过依赖获得它对应的模块工厂来构建子模块,直到把所有的子模块都构建完成
- 在这个方法( create )的回调中 调用 this.buildModule 来构建模块。( 步骤:this.buildModule )
chunk 生成阶段 — 在所有的模块构建完成后开始生成 chunks
webpack 调用 compilation.seal
方法
class Compiler extends Tapable {
compile(callback) {
const params = this.newCompilationParams()
this.hooks.compile.call(params)
// 创建 compilation 对象
const compilation = this.newCompilation(params)
// 触发 make 事件钩子,控制权转移到 compilation,开始编译流程
this.hooks.make.callAsync(compilation, err => {
// 编译和构建流程结束后,回到这里,
compilation.seal(err => {})
})
}
}
- 每一个入口起点、公共依赖、动态导入、runtime 抽离 都会生成一个 chunk。
// https://github.com/webpack/webpack/blob/master/lib/Compilation.js#L1188
class Compilation extends Tapable {
seal(callback) {
// _preparedEntrypoints 在 addEntry 方法中被填充,
//它存放着 entry 名称和对应的 entry module
// 将 entry 中对应的 module 都生成一个新的 chunk
for (const preparedEntrypoint of this._preparedEntrypoints) {
const module = preparedEntrypoint.module
const name = preparedEntrypoint.name
// 创建 chunk
const chunk = this.addChunk(name)
// Entrypoint 继承于 ChunkGroup
const entrypoint = new Entrypoint(name)
entrypoint.setRuntimeChunk(chunk)
entrypoint.addOrigin(null, name, preparedEntrypoint.request)
this.namedChunkGroups.set(name, entrypoint)
this.entrypoints.set(name, entrypoint)
this.chunkGroups.push(entrypoint)
// 建立 chunk 和 chunkGroup 之间的关系
GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk)
// 建立 chunk 和 module 之间的关系
GraphHelpers.connectChunkAndModule(chunk, module)
// 表明这个 chunk 是通过 entry 生成的
chunk.entryModule = module
chunk.name = name
this.assignDepth(module)
}
// 遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中
this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice())
// 优化
// SplitChunksPlugin 会监听 optimizeChunksAdvanced 事件,
//抽取公共模块,形成新的 chunk
// RuntimeChunkPlugin 会监听 optimizeChunksAdvanced 事件,
// 抽离 runtime chunk
while (
this.hooks.optimizeChunksBasic.call(this.chunks, this.chunkGroups) ||
this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups) ||
this.hooks.optimizeChunksAdvanced.call(this.chunks, this.chunkGroups)
) {
/* empty */
}
this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups)
// 哈希
this.createHash()
// 通过这个钩子,可以在生成 assets 之前修改 chunks,我们后面会用到
this.hooks.beforeChunkAssets.call()
// 通过 chunks 生成 assets
this.createChunkAssets()
}
}
assets 渲染阶段
通过 chunks 生成 assets
class Compilation extends Tapable {
// https://github.com/webpack/webpack/blob/master/lib/Compilation.js#L2373
createChunkAssets() {
// 每一个 chunk 会被渲染成一个 asset
for (let i = 0; i < this.chunks.length; i++) {
// 如果 chunk 包含 webpack runtime 代码,
// 就用 mainTemplate 来渲染,否则用 chunkTemplate 来渲染
// 关于什么是 webpack runtime,后续我们在优化小程序 webpack 插件时会讲到
const template = chunk.hasRuntime() ?
this.mainTemplate : this.chunkTemplate
}
}
}
// https://github.com/webpack/webpack/blob/master/lib/MainTemplate.js
class MainTemplate extends Tapable {
constructor(outputOptions) {
super()
this.hooks = {
bootstrap: new SyncWaterfallHook(['source', 'chunk', 'hash', 'moduleTemplate', 'dependencyTemplates']),
render: new SyncWaterfallHook(['source', 'chunk', 'hash', 'moduleTemplate', 'dependencyTemplates']),
renderWithEntry: new SyncWaterfallHook(['source', 'chunk', 'hash']),
}
// 自身监听了 render 事件
this.hooks.render.tap('MainTemplate', (bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
const source = new ConcatSource()
// 拼接 runtime 源码
source.add(new PrefixSource('/******/', bootstrapSource))
// 拼接 module 源码,mainTemplate 把渲染模块代码的职责委托给了
moduleTemplate,自身只负责生成 runtime 代码
source.add(this.hooks.modules.call(new RawSource(''), chunk, hash, moduleTemplate, dependencyTemplates))
return source
})
}
// 这是 Template 的入口方法
render(hash, chunk, moduleTemplate, dependencyTemplates) {
// 生成 runtime 代码
const buf = this.renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates)
// 触发 render 事件,请注意 MainTemplate 自身在构造函数中监听了这一事件,
完成了对 runtime 代码和 module 代码的拼接
let source = this.hooks.render.call(
// 传入 runtime 代码
new OriginalSource(Template.prefix(buf, ' \t') + '\n', 'webpack/bootstrap'),
chunk,
hash,
moduleTemplate,
dependencyTemplates,
)
// 对于每一个入口 module, 即通过 compilation.addEntry 添加的模块
if (chunk.hasEntryModule()) {
// 触发 renderWithEntry 事件,让我们有机会修改生成后的代码
source = this.hooks.renderWithEntry.call(source, chunk, hash)
}
return new ConcatSource(source, ';')
}
renderBootstrap(hash, chunk, moduleTemplate, dependencyTemplates) {
const buf = []
// 通过 bootstrap 这个钩子,用户可以添加自己的 runtime 代码
buf.push(this.hooks.bootstrap.call('', chunk, hash, moduleTemplate, dependencyTemplates))
return buf
}
}
- 所谓渲染就是生成代码的过程 , 渲染就是拼接和替换字符串的过程
- 最终渲染好的代码会存放在 compilation 的 assets 属性中。
输出文件阶段
// https://github.com/webpack/webpack/blob/master/lib/Compiler.js
class Compiler extends Tapable {
run(callback) {
const onCompiled = (err, compilation) => {
// 输出文件
this.emitAssets(compilation, err => {})
}
// 调用 compile 方法
this.compile(onCompiled)
}
emitAssets(compilation, callback) {
const emitFiles = err => {}
// 在输入文件之前,触发 emit 事件,这是最后可以修改 assets 的机会了
this.hooks.emit.callAsync(compilation, err => {
outputPath = compilation.getPath(this.outputPath)
this.outputFileSystem.mkdirp(outputPath, emitFiles)
})
}
compile(onCompiled) {
const params = this.newCompilationParams()
this.hooks.compile.call(params)
// 创建 compilation 对象
const compilation = this.newCompilation(params)
// 触发 make 事件钩子,控制权转移到 compilation,开始编译 module
this.hooks.make.callAsync(compilation, err => {
// 模块编译和构建完成后,开始生成 chunks 和 assets
compilation.seal(err => {
// chunks 和 assets 生成后,调用 emitAssets
return onCompiled(null, compilation)
})
})
}
}
总结流程: http://note.youdao.com/s/I7zTZzEx
1.npx webpack
2.options = new WebpackOptionsDefaulter().process(options) // 补全默认配置
3.compiler = new Compiler(options.context) // 创建 compiler 对象
4.compiler.options = options
// 应用用户通过 webpack.config.js 配置或命令行参数传递的插件
5.if (options.plugins && Array.isArray(options.plugins)) {
for (const plugin of options.plugins) {
plugin.apply(compiler)
}
}
6. WebpackOptionsApply.process()
6.1 entryOptionsPlugin.apply() //
6.1.1 apply:compiler.hooks.entryOption.tap//绑定entryOption事件
6.1.1.1 compiler.hooks.entryOption.tap :执行如下两种方法
DynamicEntryPlugin.apply()
itemToPlugin.apply(){
if (Array.isArray(item)) {
return new MultiEntryPlugin(context, item, name)
}else{
return new SingleEntryPlugin(context, item, name)
}
}
SingleEntryPlugin.apply(){
绑定了:compiler.hooks.compilation.tap()
绑定了:compiler.hooks.make.tapAsync()
} //参考如上SingleEntryPlugin.js
6.2 compiler.hooks.entryOption.call()//触发entryOption事件
7.compiler.run()
7.1 const onCompiled = (err, compilation) => {
// 输出文件
this.emitAssets(compilation, err => {})
}
}
7.1.1 emitAssets(compilation, callback) {
const emitFiles = err => {}
// 在输入文件之前,触发 emit 事件,这是最后可以修改 assets 的机会了
this.hooks.emit.callAsync(compilation, err => {
outputPath = compilation.getPath(this.outputPath)
this.outputFileSystem.mkdirp(outputPath, emitFiles)
})
7.2 this.compile(onCompiled)// 调用 compile方法
7.2.1 const params = this.newCompilationParams()
7.2.2 this.hooks.compile.call(params)
7.2.3 // 创建 compilation 对象 //此时触发如上定义compilation钩子事件
const compilation = this.newCompilation(params)
钩子总结
钩子执行 | 对应构建结果 | |
compilation钩子 | // 设置 dependency 和 module 工厂之间的映射关系 compilation.dependencyFactories.set(SingleEntryDependency, normalModuleFactory) | |
make钩子 | // 根据入口文件和名称创建 Dependency 对象 const dep = SingleEntryPlugin.createDependency(entry, name) // 随着这个方法被调用,将会开启编译流程 compilation.addEntry(context, dep, name, callback) { this._addModuleChain(context, entry, onModule, callbak)//compliation对象方法 } //最终生成module | |
compilation.seal | //make后回调 — // 模块编译和构建完成后,开始生成 chunks 和 assets | |
this.createChunkAssets() | // 通过 chunks 生成 assets | |
MainTemplate | // 如果 chunk 包含 webpack runtime 代码,就用 mainTemplate 来渲染,否则用 chunkTemplate 来渲染 //代码拼接,生成assets代码 最终渲染好的代码会存放在 compilation 的 assets 属性中。 | |
调用 Compiler 的 emitAssets 方法 | //seal回调后–// chunks 和 assets 生成后,调用 emitAssets //输出文件 this.emitAssets(compilation, err => {}) |
分离 Runtime
我们不希望每个入口文件都生成 runtime 代码,而是希望将其抽离到一个单独的文件中,以减少 app 的体积。我们通过配置 runtimeChunk 来达到这一目的。
watch 模式
我们每修改一次代码,便执行一次 npx webpack
,这有些麻烦,能不能让 webpack 检测文件的变化,自动刷新呢?答案是有的。
webpack 可以以 run 或 watchRun 的方式运行
// https://github.com/webpack/webpack/blob/master/lib/webpack.js#L62
const webpack = (options, callback) => {
if (options.watch === true || (Array.isArray(options) && options.some(o => o.watch))) {
const watchOptions = Array.isArray(options) ? options.map(o => o.watchOptions || {}) : options.watchOptions || {}
// 如果执行了 watch 就不会执行 run
return compiler.watch(watchOptions, callback)
}
compiler.run(callback)
return compiler
}
webpack配置优化
提取公共代码
splitChunks: {
chunks: 'all',
name: 'common',
minChunks: 2,
minSize: 0,
},
可以看到 dist 目录下生成了一个 common.js 文件
多环境配置
配置方式:–使用 webpack.EnvironmentPlugin
const webpack = require('webpack');
const debuggable = process.env.BUILD_TYPE !== 'release'
module.exports = {
plugins: [
new webpack.EnvironmentPlugin({
NODE_ENV: JSON.stringify(process.env.NODE_ENV) || 'development',
BUILD_TYPE: JSON.stringify(process.env.BUILD_TYPE) || 'debug',
}),
],
mode: debuggable ? 'none' : 'production',
}
默认情况下,webpack 会帮我们把 process.env.NODE_ENV
的值设置成 mode 的值
使用 NODE_ENV 来区分环境类型是约定俗成的(node中)–有如下区别:
Development
- More logs come out
- No views are cached
- More verbose error messages are generated
- Front end stuff like javascript, css, etc. aren’t minized and cached
Production
Below are common, regardless of frameworks:
- Middleware and other dependencies switch to use efficient code path
- Only packages in dependencies are installed. Dependencies in devDependencies and peerDependencies are ignored.
代码中可以通过以下方式读取这些环境变量
console.log(`环境:${process.env.NODE_ENV} 构建类型:${process.env.BUILD_TYPE}`)
关于process.env的变量注入–npm script
关于less的使用
{
test: /\.(less)$/,
include: /src/,
use: [
{
loader: 'file-loader',
options: {
useRelativePath: true,
name: '[path][name].wxss',
context: resolve('src'),
},
},
{
loader: 'less-loader',
options: {
lessOptions:{
javascriptEnabled: false,
includePaths: [resolve('src', 'styles'), resolve('src')],
}
},
},
]
},
]