基本概念
1.chunk: 被打包出来的文件,装载了module
2.module: 具有一定功能的模块
3.loader: 是处理各个类型资源的转化器
4.plugin: 可以触及整个打包流程,去实现一些特殊的功能
打包结果
一个模块
我们先来看下一个打包后的文件内容 :
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if(installedModules[moduleId])
return installedModules[moduleId].exports;
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
exports: {},
id: moduleId,
loaded: false
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.loaded = true;
// Return the exports of the module
return module.exports;
}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// __webpack_public_path__
__webpack_require__.p = "";
// Load entry module and return exports
return __webpack_require__(0);
})
([
/* 0 */
function(module, exports) {
alert(1);
}
]);
这是一个立即执行函数,接受一个 `modules ` 参数,是一个数组,存放所有的模块,这些模块按照一定顺序排列,其索引作为其标示。
然后函数开始执行,`installedModules` 用来存放已经加载过的模块,用来作为缓存,然后定义了一个函数 `__webpack_require__` 下面就调用了这个函数,并传入参数0。
我们看下这个函数
先从缓存拿模块,没有的话,定义一个module,结构为
{
exports: {},
id: moduleId,// id 就是modules数组的索引值
loaded: false
}
然后执行 modules[moduleId] 这个模块,也就是上面的第一个函数,把`module`, `module.exports`, `__webpack_require__`,这3个参数传入,这样我们的模块就能通过`exports`导出内容,使用`require` 去加载模块。这样我们就执行了模块,拿到结果。不管是es6还是jsx,最终都会被相应的loader转化成接受这3个参数的一个函数。
多个模块
现在只有一个模块,如果我在入口文件引入了另一个文件呢?那么变化的只有`modules`参数,看下:
[function (module, exports, __webpack_require__) {
var module1=__webpack_require__(1);
alert(module1.value);
}, function (module, exports) {
var value = 1;
exports.value = value;
}]
在第一个模块执行的时候,会去require第2个模块。
公共提取
下面我们来看下公共提取工的作机制,如果使用 CommonsChunkPlugin 插件来提取公共模块,那么打包后的文件会被拆分,那么是这个机制又是怎么样的呢?
比如我们的2个入口文件 index.js 和 index1.js, 都用到来一个模块,这个模块被打到了common.js 文件,看下这个文件内容
(function (modules) { // webpackBootstrap
// install a JSONP callback for chunk loading
var parentJsonpFunction = window["webpackJsonp"];
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, callbacks = [];
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (installedChunks[chunkId])
callbacks.push.apply(callbacks, installedChunks[chunkId]);
installedChunks[chunkId] = 0;
}
for (moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId];
}
if (parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
while (callbacks.length)
callbacks.shift().call(null, __webpack_require__);
if (moreModules[0]) {
installedModules[0] = 0;
return __webpack_require__(0);
}
};
// The module cache
var installedModules = {};
// object to store loaded and loading chunks
// "0" means "already loaded"
// Array means "loading", array contains callbacks
var installedChunks = {
2: 0
};
// The require function
function __webpack_require__(moduleId) {
// 已经介绍过 ,这里先省略
}
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId, callback) {
// 按需加载需要,这里先省略
};
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// __webpack_public_path__
__webpack_require__.p = "";
})([, function (module, exports) {
var value = 1;
exports.value = value;
}]);
然后是index.js
webpackJsonp([0],[function(module, exports, __webpack_require__) {
var module1=__webpack_require__(1);
alert(module1.value);
}]);
index1.js
webpackJsonp([1],[function(module, exports, __webpack_require__) {
var module1=__webpack_require__(1);
alert(module1.value);
}]);
当我们的代码执行的时候,先加载common.js文件,我们先自己看下modules参数,这个数组其实有2个值,第一个值是空的,我们的公用模块是第2个,第一个值为2个入口文件的初始模块留坑。
这里主要定义了`webpackJsonp` 函数,并没有实际运行什么模块代码。
当加载index.js文件的时候,开始调用`webpackJsonp`,他接受2个参数 `chunkIds`和`moreModules`,
首先处理`chunkIds` 相关,这个在下一节按需加载分析,这里最后调用了`__webpack_require__(0)`,去加载入口模块,然后在这个模块调用了`__webpack_require__(1)`,这里的处理逻辑就清晰了,2个入口文件根据其chunkId的分别是chunk0和chunk1,他们的入口模块id都是0,公用的模块的id是1,这样就能保证2个chunk文件中能通过模块id找到被独立出来的common模块了。
按需加载
我们可以通过 `require.ensure` 来实现按需加载,这个函数就是上文省略的
__webpack_require__.e = function requireEnsure(chunkId, callback) {
// "0" is the signal for "already loaded"
if (installedChunks[chunkId] === 0)
return callback.call(null, __webpack_require__);
// an array means "currently loading".
if (installedChunks[chunkId] !== undefined) {
installedChunks[chunkId].push(callback);
} else {
// start chunk loading
installedChunks[chunkId] = [callback];
var head = document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.charset = 'utf-8';
script.async = true;
script.src = __webpack_require__.p + "" + chunkId + "." + ({ "0": "index"}[chunkId] || chunkId) + ".js";
head.appendChild(script);
}
};
我们来回忆上上文中有一个installedChunks 数组来保存chunks的加载情况,0标示已经加载,数组标示正在加载。
这里的代码切割逻辑和上文的公共提取类似,会把要被异步加载的模块单独成文件,内容像这样:
webpackJsonp([1],[
/* 0 */,
/* 1 */
function(module, exports) {
var value = 1;
exports.value = value;
}
]);
我们的主文件中的modules则变成了这样:
[function(module, exports, __webpack_require__) {
__webpack_require__.e(1, function(require) {
var module1 = __webpack_require__(1);
alert(module1.value)
});
}])];
ps:这个模块需要用到的module1现在还没有,在另一个未被加载的文件里面。
文件开始执行,调用`__webpack_require__(0)`,然后是` __webpack_require__.e`,这个函数先检测这个chunk是否被加载,没有的话,先把模块,也就是上面的回调函数放到installedChunks,注意其chunkid是1(ps:chunkid和moduleid不要搞混),然后会通过script标签的形式去加载这chunk文件,然后`webpackJsonp`被调用,然后看按需加载的`webpackJsonp`函数,和上面的有所区别:
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
var moduleId, chunkId, i = 0, callbacks = [];
for(;i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if(installedChunks[chunkId])
callbacks.push.apply(callbacks, installedChunks[chunkId]);
installedChunks[chunkId] = 0;
}
for(moduleId in moreModules) {
modules[moduleId] = moreModules[moduleId];
}
if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
while(callbacks.length)
callbacks.shift().call(null, __webpack_require__);
}
这个函数内部先是拿到了上面注册的chunk1的模块的callback,然后注册这个chunk的module,最后执行callbeck,因为module1被加载了,所以代码终于可以跑起来了。
打包流程
上面我们看了生成的结果文件,那么现在我们来看根据一个配置文件,来把我们的资源文件进行打包构建的具体流程。
&amp;amp;amp;lt;img src="https://pic1.zhimg.com/v2-18bc1cb51e185a78950a7cf311692370_b.jpg" data-rawwidth="4436" data-rawheight="4244" class="origin_image zh-lightbox-thumb" width="4436" data-original="https://pic1.zhimg.com/v2-18bc1cb51e185a78950a7cf311692370_r.jpg"&amp;amp;amp;gt;
tapable
webpack的整体流程是基于插件架构的,而提供整个插件机制的是 tapable,他通过订阅和发布事件来注册和执行各个插件,在 这里,我们可以看到webpack内部使用了大量的插件。下文的Compiler就是tapable的实例。22
Compiler
Compiler 是 webpack 的主要引擎,webpack 通过实例化 compiler,然后调用它的 run 方法来使用它。
既然他是主流程,那么我们来看下他的主要事件流,就能大致了解整个打包流程.(来自webpack2)
- entry-option 参数处理
- after-plugins 设置插件的初始配置后
- after-resolvers 设置解析器后
- environment 环境配置
- after-environment 环境配置完成
- before-run 编译之前
- run 开始编译
- watch-run 监视后开始编译之前
- normal-module-factory 创建 NormalModuleFactory 后
- context-module-factory 创建 ContextModuleFactory 后
- before-compile 编译参数创建完成
- compile 创建新编译之前
- this-compilation 发射 compilation 事件之前
- compilation 编译创建完成
- make 从entry开始递归分析依赖,构建模块
- after-compile 编译之后
- should-emit 此时可以返回 true/false ,来判断是否生成文件
- emit 生成文件之前
- after-emit 生成文件结束
- done 整体流程结束
Compilation
Compilation实例继承于compiler,这个对象主要负责编译流程。在编译阶段,模块被加载,封闭,优化,分块,哈希和重建等。以下是这个对象的主要方法
- templatesPlugin
- addModule
- getModule
- findModule
- buildModule
- processModuleDependencies
- addModuleDependencies
- _addModuleChain
- addEntry
- prefetch
- rebuildModule
- seal
- sortModules
- addChunk
- processDependenciesBlockForChunk
- removeChunkFromDependencies
- applyModuleIds
- applyChunkIds
- sortItems
- summarizeDependencies
- createHash
- modifyHash
- createModuleAssets
- createChunkAssets
- getPath
- getStats
- createChildCompiler
流程大概是调用 addEntry 找到入口文件,然后调用_addModuleChain ,获得相应 moduleFactory ,开始构建模块,构建模块流程比较复杂,其中的步骤有
* 调用各 loader 处理模块
* 使用 acorn 解析js文件,得到 AST 然后拿到依赖模块,调用addDependency方法添加
* 不断重复以上流程,来递归到构建模块
流程中处理的的核心对象就是module
以下是一次构建流程中一个module对象的key值:
['dependencies','blocks','variables','context','reasons','debugId','lastId','id','index','index2','chunks','warnings','dependenciesWarnings','errors','dependenciesErrors','request','userRequest','rawRequest','parser','resource','loaders','fileDependencies','contextDependencies','error','_source','meta','assets','built','_cachedSource','issuer','optional','building','buildTimestamp','cacheable' ]
其中几个关键的
- dependencies 模块依赖
- context 模块文件的上下文
- chunks 此模块所在的chunks
- request 加载此模块的完整请求,包括loader及其参数
- resource 该模块对应的资源文件
- loaders 解析该模块的loader
- fileDependencies 文件依赖,包括自己
- issuer 模块调用方
- _source 模块内容
输出文件
等到构建结束以后,就要输出文件了。webpack会对构建结果整理分析,在将结果拆分,合并。
主要流程是
- 对chunk进行排序
- 遍历chunk,为每个chunk整理其module,为module添加chunk
- 进行各种优化
- 调用`createChunkAssets` 方法,这里会根据每个 chunk 的类型找到相应到文件 template 进行 render,最后在根据用户的配置来输出最终的文件,也就是上面打包结果章节的内的文件。
loader
loader就是一个函数,接受字符串内容,返回转化后的内容,一个loader只做一件事,多个loader串行执行调用来完成资源转化。比如一个最简单的loader,raw-loader内容如下:
module.exports = function(content) {
this.cacheable && this.cacheable();
this.value = content;
return "module.exports = " + JSON.stringify(content);
}
plugin
webpack给予插件触及webpac各个流程的能力,他痛殴一个apply方法来安装到webpack中,写法如:
function MyPlugin(options) {
// Configure your plugin with options...
}
MyPlugin.prototype.apply = function(compiler) {
compiler.plugin("compile", function(params) {
console.log("The compiler is starting to compile...");
});
compiler.plugin("compilation", function(compilation) {
console.log("The compiler is starting a new compilation...");
compilation.plugin("optimize", function() {
console.log("The compilation is starting to optimize files...");
});
});
compiler.plugin("emit", function(compilation, callback) {
console.log("The compilation is going to emit files...");
callback();
});
};
module.exports = MyPlugin;