- 发布于
Webpack打包原理
- Authors
- Name
- 田中原
Webpack简介
Webpack是一个打包模块化Javascript的工具,在Webpack里一切文件皆模块,通过Loader转换文件,通过Plugin注入钩子,最后输出由多个模块组合成的文件。Webpack专注于构建模块化项目。 ——《深入浅出Webpack》
其官网的首页图很形象地展示了Webpack的定义 如下图所示
分析打包后的文件 (webpack版本: 4.41.4)
配置文件: webpack.config.js
const path = require('path')
module.exports = {
mode: 'development',
devtool: 'none',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].js',
},
}
入口文件: ./src/index.js
let title = require('./title')
console.log(title)
被入口文件导入的文件: ./src/title.js
module.exports = '标题'
打包后的文件: main.js
;(function (modules) {
// 模块缓存
var installedModules = {}
function __webpack_require__(moduleId) {
// 检查模块是否在缓存中
if (installedModules[moduleId]) {
return installedModules[moduleId].exports
}
// 创建一个新的模块,并放到缓存中
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
})
// 执行模块函数
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
// 模块已加载成功
module.l = true
// 返回此模块的导出对象
return module.exports
}
// 把modules对象放在__webpack_require__.m属性上
__webpack_require__.m = modules
// 把模块的缓存对象放在__webpack_require__.c属性上
__webpack_require__.c = installedModules
// 导出定义getter函数
__webpack_require__.d = function (exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter })
}
}
// 声明es6模块
__webpack_require__.r = function (exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
}
Object.defineProperty(exports, '__esModule', { value: true })
}
// 包装es6模块
__webpack_require__.t = function (value, mode) {
if (mode & 1) value = __webpack_require__(value)
if (mode & 8) return value
if (mode & 4 && typeof value === 'object' && value && value.__esModule) return value
var ns = Object.create(null)
__webpack_require__.r(ns)
Object.defineProperty(ns, 'default', { enumerable: true, value: value })
if (mode & 2 && typeof value != 'string')
for (var key in value)
__webpack_require__.d(
ns,
key,
function (key) {
return value[key]
}.bind(null, key)
)
return ns
}
// 获取默认导出的函数,为了兼容非harmony模块
__webpack_require__.n = function (module) {
var getter =
module && module.__esModule
? function getDefault() {
return module['default']
}
: function getModuleExports() {
return module
}
__webpack_require__.d(getter, 'a', getter)
return getter
}
// Object.prototye.hasOwnProperty
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property)
}
// 把公开访问路径放在__webpack_require__.p属性上
__webpack_require__.p = ''
// 加载入口模块并且返回exports (__webpack_require__.s: 指定入口模块ID)
return __webpack_require__((__webpack_require__.s = './src/index.js'))
})({
'./src/index.js': function (module, exports, __webpack_require__) {
let title = __webpack_require__('./src/title.js')
console.log(title)
},
'./src/title.js': function (module, exports) {
module.exports = '标题'
},
})
打包后文件中常用的几个方法
__webpack_require__.r (声明es6模块)
__webpack_require__.r = function (exports) {
/**
* Symbol.toStringTag 是一个内置 symbol,它通常作为对象的属性键使用
* 对应的属性值应该为字符串类型,这个字符串用来表示该对象的自定义类型标签
* 通常只有内置的 Object.prototype.toString() 方法会去读取这个标签并把它包含在自己的返回值里
*/
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
-------------------------------------------------------------------------------------------
实际可以理解成如下代码:
__webpack_require__.r = function (exports) {
exports[Symbol.toStringTag] = 'Module';
exports.__esModule = true;
}
let exports = {};
__webpack_require__.r(exports);
console.log(Object.prototype.toString.call(exports));
// 控制台打印结果:"[object Module]"
__webpack_require__.o (Object.prototye.hasOwnProperty)
// 实际就是对Object.prototype.hasOwnProperty.call做了一个包装
__webpack_require__.o = function (object, property) {
return Object.prototype.hasOwnProperty.call(object, property)
}
__webpack_require__.d (导出定义getter函数)
__webpack_require__.d = function (exports, name, getter) {
// 检查形参name是否是形参exports的私有属性
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter })
}
}
__webpack_require__.n (获取默认导出的函数,为了兼容非harmony模块)
__webpack_require__.n = function (module) {
var getter =
module && module.__esModule
? function getDefault() {
// ES6 modules
return module['default']
}
: function getModuleExports() {
// common.js
return module
}
__webpack_require__.d(getter, 'a', getter)
return getter
}
__webpack_require__.t (包装es6模块)
// 懒加载的时候用的
// require.t 一般来说核心用法是用来把一个任意模块都变成一个es模块
// import('./xxx').then(result => console.log(result))..不管懒加载的是一个common.js还是es6模块,都会变成es6模块的格式
// create a fake namespace object 创建一个模拟的命名空间对象,不管什么模块都转成es6模块
// mode & 1: value is a module id, require it value是一个模块ID,需要用require加载
// mode & 2: merge all properties of value into the ns // 将值的所有属性合并到ns中
// mode & 4: return value when already ns object 如果已经是ns对象了,则直接返回
// mode & 8 | 1: behave like require 等同于require方法
__webpack_require__.t = function (value, mode) {
// 1转为二进制 0b0001
if (mode & 1) value = __webpack_require__(value)
// 8转为二进制 0b1000
if (mode & 8) return value
// 4转为二进制 0b0100 value已经是es模块了,可以直接返回
if (mode & 4 && typeof value === 'object' && value && value.__esModule) return value
var ns = Object.create(null)
__webpack_require__.r(ns) // ns.__esModule=true
Object.defineProperty(ns, 'default', { enumerable: true, value: value })
if (mode & 2 && typeof value != 'string')
// 把值拷贝到ns上
for (var key in value)
__webpack_require__.d(
ns,
key,
function (key) {
return value[key]
}.bind(null, key)
)
return ns
}
harmony
顾名思义,harmony中文是和谐的意思。webpack_require.n方法就是为了兼容非harmony的模块。
common.js加载 common.js
入口文件: ./src/index.js
let title = require('./title')
console.log(title.name)
console.log(title.age)
被入口文件导入的文件: ./src/title.js
exports.name = 'title_name'
exports.age = 'title_age'
打包后的文件: main.js (自执行函数实参部分)
{
'./src/index.js': function (module, exports, __webpack_require__) {
let title = __webpack_require__('./src/title.js');
console.log(title.name);
console.log(title.age);
},
'./src/title.js': function (module, exports) {
exports.name = 'title_name';
exports.age = 'title_age';
}
}
common.js加载 ES6 modules
入口文件: ./src/index.js
let title = require('./title')
console.log(title)
被入口文件导入的文件: ./src/title.js
export default 'title_name' // 默认导出
export const age = 'title_age' // 单个导出
打包后的文件: main.js (自执行函数实参部分)
{
'./src/index.js': function (module, exports, __webpack_require__) {
let title = __webpack_require__('./src/title.js');
console.log(title);
},
'./src/title.js': function (module, __webpack_exports__, __webpack_require__) {
'use strict';
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, 'age', function () {
return age;
});
__webpack_exports__['default'] = 'title_name'; // 默认导出
const age = 'title_age'; // 单个导出
}
}
ES6 modules 加载 ES6 modules
入口文件: ./src/index.js
import name, { age } from './title'
console.log(name)
console.log(age)
被入口文件导入的文件: ./src/title.js
export default 'title_name' // 默认导出
export const age = 'title_age' // 单个导出
打包后的文件: main.js (自执行函数实参部分)
{
'./src/index.js': function (module, __webpack_exports__, __webpack_require__) {
'use strict';
__webpack_require__.r(__webpack_exports__);
var _title__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
'./src/title.js'
);
console.log(_title__WEBPACK_IMPORTED_MODULE_0__['default']);
console.log(_title__WEBPACK_IMPORTED_MODULE_0__['age']);
},
'./src/title.js': function (module, __webpack_exports__, __webpack_require__) {
'use strict';
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, 'age', function () {
return age;
});
__webpack_exports__['default'] = 'title_name'; // 默认导出
const age = 'title_age'; // 单个导出
}
}
ES6 modules 加载 common.js
入口文件: ./src/index.js
import name, { age } from './title'
console.log(name)
console.log(age)
被入口文件导入的文件: ./src/title.js
exports.name = 'title_name'
exports.age = 'title_age'
打包后的文件: main.js (自执行函数实参部分)
{
'./src/index.js': function (module, __webpack_exports__, __webpack_require__) {
'use strict';
__webpack_require__.r(__webpack_exports__);
var _title__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
'./src/title.js'
);
var _title__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(
_title__WEBPACK_IMPORTED_MODULE_0__
);
console.log(_title__WEBPACK_IMPORTED_MODULE_0___default.a);
console.log(_title__WEBPACK_IMPORTED_MODULE_0__['age']);
},
'./src/title.js': function (module, exports) {
exports.name = 'title_name';
exports.age = 'title_age';
}
}
针对harmony的简单总结
- 如果模块是用common.js写的,则不需要做任何转换
- 如果模块里有export或者import或者都有
- webpack_require.r(webpack_exports); 先表明是ES6模块
- 如果是ES6 modules 加载 common.js,并且有默认导入
- 需要通过webpack_require.n(_titleWEBPACK_IMPORTED_MODULE_0);得到默认导入,_titleWEBPACKIMPORTED_MODULE_0default.a就是默认导入了
异步加载
入口文件: ./src/index.js
let importBtn = document.getElementById('import')
importBtn.addEventListener('click', () => {
import(/* webpackChunkName: 'title' */ './title').then((result) => {
console.log(result)
})
})
被入口文件导入的文件: ./src/title.js
exports.name = 'title_name'
exports.age = 'title_age'
打包后的文件: main.js (自执行函数实参部分)
{
'./src/index.js': function (module, exports, __webpack_require__) {
let importBtn = document.getElementById('import');
importBtn.addEventListener('click', () => {
// 下面代码就是 import('./title')
__webpack_require__
.e('title')
.then(
__webpack_require__.t.bind(null, /*! ./title */ './src/title.js', 7)
)
.then((result) => {
// result就是这个title的导出对象
console.log(result);
});
});
},
}
打包后的文件(chunk):title.js
(window['webpackJsonp'] = window['webpackJsonp'] || []).push([
['title'],
{
'./src/title.js': function (module, exports) {
exports.name = 'title_name';
exports.age = 'title_age';
},
},
]);
针对懒加载的总结
- 首先会执行webpack_require.e('title'), 并传入chunkId
- webpack_require.e('title')方法会通过JSONP的方式,向页面的head添加一个script标签,并且script标签的src为chunkId + '.js'
- 此刻会执行打包后的title.js中的代码,也就是会执行webpackJsonpCallback这个方法
- webpackJsonpCallback方法会把title这个代码块设置为已加载成功状态,并依次让resolves执行,把promise变成成功状态
- 进入webpack_require.t.bind('./src/title.js', 7)方法
- 先执行webpack_require方法
- 创建ns对象,并为它赋值 ns.__esModule = true; 和 ns.default = value;
- 把vaule上的属性拷贝一份到ns上,返回ns。也就是最终结果
编译流程
基本概念
- Entry 入口,webpack执行构建的第一步将从Entry开始,可抽象成输入
- Module 模块,在webpack里一切皆模块,一个模块对应着一个文件。webpack会从配置的Entry开始递归找出所有依赖的模块。
- Chunk 代码块,一个Chunk由多个模块组合而成,用于代码合并与分割。
- Loader 模块转换器,用于把模块原内容按照需求转换成新内容。
- Plugin 扩展插件,在webpack构建流程中特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。
理解事件流机制 Tapable
- webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable。
- Tapable 事件流机制保证了插件的有序性,将各个插件串联起来, Webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webapck机制中,去改变webapck的运作,使得整个系统扩展性良好。
理解Compiler
Compiler 对象包含了当前运行Webpack的配置,包括entry、output、loaders等配置,这个对象在启动Webpack时被实例化,而且是全局唯一的。Plugin可以通过该对象获取到Webpack的配置信息进行处理。
理解Compilation
Compilation对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 Compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息,简单来讲就是把本次打包编译的内容存到内存里。Compilation 对象也提供了插件需要自定义功能的回调,以供插件做自定义处理时选择使用拓展。 简单来说,Compilation的职责就是构建模块和Chunk,并利用插件优化构建过程。
针对编译流程的简单总结
Webpack的运行是个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参数:从配置文件和shell语句中读取与合并参数,得出最终参数。
- 开始编译:用得到的参数初始化compiler对象,加载所有配置的插件,执行对象的Run方法开始编译。
- 确定入口:根据配置中的entry找出所有入口文件。
- 编译模块:从入口文件出发,调用所有配置的loader对模块进行翻译,再找出该模块所依赖的模块,再- 递归本步骤。直到所有入口依赖文件都经过了本步骤的处理。(此处为深度优先遍历)
- 完成模板编译:使用loader翻译完所有模块后,得到每个模块被翻译后的最终内容,以及它们之间的依赖关系。
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。