webpack2源码分析打包流程(三)

前言

  在写浅谈webpack2–学会配置(一)的时候,就想把plugins单独拿出来说。但是webpack使用了tapable来实现插件对事件订阅执行,使得插件和webpack打包流程密不可分,所以这篇文章就不得不提webpack的整个打包流程。

webpack打包流程

流程图

  了解tapable和webpack核心概念之后,我们来看一张webpack流程图,我们将根据这张流程图进行源码分析。

(这张图是来自网络)

配置和shell解析

  这是webpack打包流程的第一步。这一步分为2种不一样的情况:

  • 通过命令执行输入webpack中执行,程序会执行bin/webpack.js脚本文件。
  • 通过nodejs中require('webpack')执行,程序会去执行lib/webpack.js文件。
    (如果不懂为什么的同学,可以去先去了解package.json的配置具体含义。)

bin/webpack.js源代码解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
//命令行工具
var yargs = require("yargs");
...
//命令行配置 主要是项目的配置
require("./config-yargs")(yargs);
...
//命令行配置 主要是展示类型的配置
yargs.options({"json": {
...
//将options和shell进行合并,下面会细说
var options = require("./convert-argv")(yargs, argv);
...
var webpack = require("../lib/webpack.js");
...
compiler = webpack(options);//生成compiler
...
compiler.watch(watchOptions, compilerCallback);//watch模式
...
compiler.run(compilerCallback);//执行编译
...

yargs

  yargs和commander,optimist一样都是命令行工具。执行webpack --config webpack.config.js,
通过var argv = yargs.argv;var config = argv.config就能获取命令行输入。

convert-argv.js

  请求配置文件,将配置信息加入options对象中。再根据判断命令行配置信息,对options对象添加Loader,plugins,output,entry等配置属性。
convert-argv.js源码解析:

1
2
3
4
5
6
7
8
9
10
11
12
//loader配置
function bindLoaders(arg, collection) {
ifArgPair(arg, function (name, binding) {
if (name === null) {
name = binding;
binding += "-loader";
}
options.module[collection].push({
...
bindLoaders("module-bind", "loaders");
bindLoaders("module-bind-pre", "preLoaders");
bindLoaders("module-bind-post", "postLoaders");

  注意:源码已经改变。比如:loader使用的preLoaders,postLoaders明显已经不是新版的webpack配置了。新webpack使用rule.enforce等于postpre代替。

模块创建与构建

compilation 对象

  当options配置信息设置完成后,执行了compiler.run(),开始编译和构建。run首先触发before-runrun事件,提供一个增强型的文件系统,然后执行compile方法。compile方法首先获取compilation构造函数的入参对象(包含两种类型的模块工厂和一个依赖数组)。然后执行before-compilecompile事件,再生成compilation对象。然后触发make事件来调用SingleEntryPlugin或者MultiEntryPlugin两个插件中的compilation.addEntry方法。
compilation.addEntry调用源码解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// /lib/Compiler.js
var compilation = self.newCompilation(params);
self.applyPluginsParallel("make", compilation, function (err) {
...
// /lib/SingleEntryPlugin.js
compiler.plugin("make", (compilation, callback) => {
const dep = SingleEntryPlugin.createDependency(this.entry, this.name);
compilation.addEntry(this.context, dep, this.name, callback);
});
...
// /lib/EntryOptionPlugin.js
// 根据入口文件配置,加载不同的插件,我象征性的列举了一个。
apply(compiler) {
compiler.plugin("entry-option", (context, entry) => {
...
return new SingleEntryPlugin(context, item, name);
}
...

  在前面一章讲解compiler的时候说过,执行WebpackOptionsApply对象的process方法,根据配置加载插件。就是这个时候EntryOptionPlugin插件被加载到程序中,该插件根据入口文件配置类型加载不同的插件,我这里只写了SingleEntryPlugin。当触发make事件时,调用compilation.addEntry方法。

模块创建和构建主流程

  compilation.addEntry最主要调用了私有方法_addModuleChain。_addModuleChain根据不同的入口依赖类型获取不同的模块工厂函数进行模块创建,再执行buildModule函数对模块进行构建。

_addModuleChain源码解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
_addModuleChain(context, dependency, onModule, callback) {
...
//根据依赖模块类型,获取模块工厂函数
const moduleFactory = this.dependencyFactories.get(dependency.constructor);
if(!moduleFactory) {
throw new Error(`No dependency factory available for this dependency type: ${dependency.constructor.name}`);
}
//创建模块,并将创建好的模块传入回调函数
moduleFactory.create({
contextInfo: {
issuer: "",
compiler: this.compiler.name
},
context: context,
dependencies: [dependency]
}, (err, module) => {
...
//记录创建输出耗时
if(this.profile) {
if(!module.profile) {
module.profile = {};
}
afterFactory = Date.now();
module.profile.factory = afterFactory - start;
}
//将module存入compilation的modules,_modules属性
//返回Boolean值 或 来至cache的module
//result表示是否重新build
const result = this.addModule(module);
if(!result) {
module = this.getModule(module);
//将module存入entry和compilation的entries属性,后面会使用到
onModule(module);
if(this.profile) {
const afterBuilding = Date.now();
module.profile.building = afterBuilding - afterFactory;
}
return callback(null, module);
}
//cache中返回的module,之前已经执行过build
if(result instanceof Module) {
if(this.profile) {
result.profile = module.profile;
}
module = result;
onModule(module);
//模块依赖处理
moduleReady.call(this);
return;
}
onModule(module);
//模块构建
this.buildModule(module, false, null, null, (err) => {
if(err) {
return errorAndCallback(err);
}
if(this.profile) {
const afterBuilding = Date.now();
module.profile.building = afterBuilding - afterFactory;
}
//模块依赖处理
moduleReady.call(this);
});
function moduleReady() {
this.processModuleDependencies(module, err => {
if(err) {
return callback(err);
}
return callback(null, module);
});
}
});
}

模块详细构建过程分析

  buildModule方法会调用每个模块都具有的build方法,进行模块的构建。然而每种类型的模块构建方式都不一样。例如,因为MultiModule类型模块实质是由多个依赖模块构成的混合模块,所以MultiModule的build方法只是去标明该模块已经被构建,而它的依赖模块会在后面的依赖处理中被构建。NormalModule类型模块的构建,先会被不同loader处理,然后将处理完成的源文件用acorn解析生成AST(抽象语法树)。最后被依赖解析插件遍历AST,将依赖加入dependencies数组
MultiModule类型构建源码解析:

1
2
3
4
5
6
build(options, compilation, resolver, fs, callback) {
//表明已构建
this.built = true;
return callback();
}

NormalModule类型构建过程源码解析:

  • 模块loader处理流程

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //options 配置信息
    //compilation compilation
    //resolver 根据options.resolve配置,生成的解析工具
    //fs inputFileSystem
    doBuild(options, compilation, resolver, fs, callback) {
    this.cacheable = false;
    //loaderContext对象包含各种编译信息,resolve,resolveSync等
    const loaderContext = this.createLoaderContext(resolver, options, compilation, fs);
    //执行
    //this.loaders 由NormalModuleFactory中触发resolver事件后返回的函数,根据信息判断,返回的不同loaders
    runLoaders({
    resource: this.resource,
    loaders: this.loaders,
    context: loaderContext,
    readResource: fs.readFile.bind(fs)
    }, (err, result) => {
    ... //将loader处理后的资源赋值给模块属性
    });
    }
  • loader处理后,acorn解析生成AST

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    build(options, compilation, resolver, fs, callback) {
    ... //添加module信息
    //执行doBuild,对模块进行loader处理。
    return this.doBuild(options, compilation, resolver, fs, (err) => {
    ...
    //noParse为ture,将不会生成AST和依赖查找,也就不会构建依赖了
    const noParseRule = options.module && options.module.noParse;
    if (this.shouldPreventParsing(noParseRule, this.request)) {
    return callback();
    }
    // parse中,用acorn解析生成AST
    try {
    this.parser.parse(this._source.source(), {
    current: this,
    module: this,
    compilation: compilation,
    options: options
    });
    }
    ...
    return callback();
    });
    }
  • 遍历AST,收集依赖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //this.parser.parse函数
    parse(source, initialState)
    {
    ...
    try {
    comments.length = 0;
    // 关于需要解析的类型,解析插件配置
    POSSIBLE_AST_OPTIONS[i].onComment = comments;
    // 用acorn解析生成AST
    ast = acorn.parse(source, POSSIBLE_AST_OPTIONS[i]);
    } catch (e) {
    }
    ...
    //触发AMDPlugin,DefinePlugin等解析器插件的"expression "+outerExpr事件
    //然后根据AST抽象语法树,对依赖进行添加
    if (this.applyPluginsBailResult("program", ast, comments) === undefined) {
    this.prewalkStatements(ast.body);
    this.walkStatements(ast.body);
    }
    }
  • loader处理前后资源变化:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //这里使用了babel-loader,设置 "presets": ["es2015"]
    //前
    import React from 'react';
    //后
    'use strict';
    var _react = require('react');
    var _react2 = _interopRequireDefault(_react);
    function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

模块依赖构建过程分析

  当前模块构建完成后,对其依赖处理。processModuleDependencies将模块依赖数组传入addModuleDependencies函数。该函数对依赖进行遍历,异步构建每一个模块。
addModuleDependencies源码解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
//对依赖数组进行遍历
for(let i = 0; i < dependencies.length; i++) {
const factory = _this.dependencyFactories.get(dependencies[i][0].constructor);
if(!factory) {
return callback(new Error(`No module factory available for dependency type: ${dependencies[i][0].constructor.name}`));
}
factories[i] = [factory, dependencies[i]];
}
//异步的构建一个个依赖模块
asyncLib.forEach(factories, function iteratorFactory(item, callback) {
const dependencies = item[1];
... //下面的过程和addModuleChain类似
//不断重复这个步骤

  整个模块的创建和构建完成后,触发finish-modules事件。

模块封装与输出

模块封装

  • seal方法
      模块和依赖构建完成后,调用compilation的seal方法触发seal事件。遍历preparedChunk,生成chunk。处理模块依赖,当模块属性blocks为true时(也就是异步加载依赖模块时),生成新的chunk。然后调用各种优化事件对chunk和模块等各种优化处理,生成hash。最后通过调用createModuleAssetscreateChunkAssets,进行模块封装,生成source。赋值给this.assets[file]生成最终asset。
    seal源码解析:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    seal(callback)
    {
    const self = this;
    self.applyPlugins0("seal");
    self.nextFreeModuleIndex = 0;
    self.nextFreeModuleIndex2 = 0;
    // 遍历preparedChunks,preparedChunks中包含了入口文件构建后的模块
    self.preparedChunks.forEach(preparedChunk => {
    const module = preparedChunk.module;
    //创建chunk, compilation中添加chunk
    const chunk = self.addChunk(preparedChunk.name, module);
    //模块封装时,会根据entrypoint选择不同的模板对象进行封装
    const entrypoint = self.entrypoints[chunk.name] = new Entrypoint(chunk.name);
    //entrypoint添加chunk,chunk添加entrypoint
    entrypoint.unshiftChunk(chunk);
    //chunk装载模块
    chunk.addModule(module);
    module.addChunk(chunk);
    chunk.entryModule = module;
    self.assignIndex(module);
    self.assignDepth(module);
    //block模块添加chunk,但是没有entrypoint
    //当module.blocks为true,也就是异步加载的依赖时,会新生成并添加chunk
    self.processDependenciesBlockForChunk(module, chunk);
    });
    //优化编译,树的异步优化,模块的优化,chunk的优化等等
    ...
    //hash生成
    self.createHash();
    ...
    //modules中assets赋值给compilation.assets数组
    //module.assets: 某些loader执行this.emitFile后,生成 module.assets ,所以开发在自定义loader时,可以使用this.emitFile来最终输出文件
    //file-loader中就要使用this.emitFile
    self.createModuleAssets();
    //根据chunk.hasRuntime()调用不同模板,生成不同的source
    // chunk.hasRuntime()源码:if(this.entrypoints.length === 0) return false;return this.entrypoints[0].chunks[0] === this;
    self.createChunkAssets();
    ...
    //生成最终assets
    this.assets[file] = source;
    chunk.files.push(file);
    ...
  • createChunkAssets调用Template生成source(结果代码)
      createChunkAssets中遍历chunk,根据判断是否为入口文件,还是异步加载文件,调用mainTemplate和chunkTemplate两种不同的模板,生成source。
    createChunkAssets源码解析:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    for (let i = 0; i < this.chunks.length; i++) {
    ...
    //缓存
    if (this.cache && this.cache[cacheName] && this.cache[cacheName].hash === usedHash) {
    source = this.cache[cacheName].source;
    } else {
    //生成项目入口文件
    if (chunk.hasRuntime()) {
    source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);
    //生成异步加载文件
    } else {
    source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates);
    }
    ...
  • Template详细生成source
      mainTemplate和chunkTemplate对模块进行封装,其实除了这两种Template还有ModuleTemplate和HotUpdateChunkTemplate。ModuleTemplate会对依赖模块进行逐一的render,HotUpdateChunkTemplate热替换模块。
    mainTemplate和chunkTemplate会遍历chunk中装载的模块,调用ModuleTemplate最后生成source
    mainTemplate源码解析:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //模块封装
    render(hash, chunk, moduleTemplate, dependencyTemplates)
    {
    const buf = [];
    buf.push(this.applyPluginsWaterfall("bootstrap", "", chunk, hash, moduleTemplate, dependencyTemplates));
    buf.push(this.applyPluginsWaterfall("local-vars", "", chunk, hash));
    buf.push("");
    buf.push("// The require function");
    buf.push(`function ${this.requireFn}(moduleId) {`);
    ...
    //调用ModuleTemplate类型模板的render方法,依次遍历chunk中每一个装载的模块。
    let source = this.applyPluginsWaterfall("render", new OriginalSource(this.prefix(buf, " \t") + "\n", `webpack/bootstrap ${hash}`), chunk, hash, moduleTemplate, dependencyTemplates);
    ...
    return new ConcatSource(source, ";");
    }

chunkTemplate源码解析:

1
2
3
4
5
6
7
8
9
10
11
12
13
render(chunk, moduleTemplate, dependencyTemplates)
{
const moduleSources = this.renderChunkModules(chunk, moduleTemplate, dependencyTemplates);
const core = this.applyPluginsWaterfall("modules", moduleSources, chunk, moduleTemplate, dependencyTemplates);
//调用ModuleTemplate类型模板的render方法,依次遍历chunk中每一个装载的模块。
let source = this.applyPluginsWaterfall("render", core, chunk, moduleTemplate, dependencyTemplates);
if (chunk.hasEntryModule()) {
source = this.applyPluginsWaterfall("render-with-entry", source, chunk);
}
chunk.rendered = true;
return new ConcatSource(source, ";");
}

遍历,调用ModuleTemplate源码:

1
2
3
4
5
6
var allModules = chunk.modules.map(function(module) {
return {
id: module.id,
source: moduleTemplate.render(module, dependencyTemplates, chunk)
};
});

模块输出

从模块创建到asset生成可以说是webpack编译的整个过程,最后一步模块输出其实已经不涉及到编译。而是执行compiler.emitAssets方法,对模块进行输出。在输出之前,会触发emit事件,这也是开发者最后修改输出文件的机会。然后遍历compilation.assets异步的输出文件到outputPath的输出地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Compiler.prototype.emitAssets = function(compilation, callback) {
var outputPath;
//emit事件
this.applyPluginsAsync("emit", compilation, function(err) {
if(err) return callback(err);
//输出路径
outputPath = compilation.getPath(this.outputPath);
this.outputFileSystem.mkdirp(outputPath, emitFiles.bind(this));
}.bind(this));
function emitFiles(err) {
if(err) return callback(err);
//遍历异步输出
require("async").forEach(Object.keys(compilation.assets), function(file, callback) {
....

主要事件

  • entry-option 该事件入参为content作用域和entry入口文件信息。webpack中使用该事件获取不同的插件,在最终生成不同类型的模块。
  • run 入参compiler。
  • compile 开始编译,compilation创建前触发,入参params。
  • make 根据入口信息开始创建并构建模块和他依赖的模块时触发,入参compilation。
  • build-module 模块开始构建时触发,入参为module。
  • “expression “+outerExpr 入参expr webpack中根据AST抽象语法树,对依赖进行添加。
  • seal 模块构建完成后,开始封装模块(创建chunk,为chunk装载module,根据依赖创建chunk,优化,生成hash)时触发。
  • emit 将根据chunk封装后的assert输出成文件前触发。
注:本文章中的源代码来至我本地的2.7.0版本。所以也许和你看见的不一致。O__O “…