webpack之解惑

webpack之解惑

webpack运行原理

webpack维护了一套模块化系统,它兼容所有的模块规范。结合loaderplugin输出最终的编译文件。

一个简单的webpack.config.js配置文件:

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
var path = require('path')
var webpack = require('webpack')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var CleanWebpakPlugin = require('clean-webpack-plugin')

module.exports = {
entry: {
main: path.resolve(__dirname, 'index.js')
},
output: {
path: path.resolve(__dirname, '/dist'),
filename: 'js/[name].[chunckhash:4].js'
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
},
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.tmpl.html'
}),
//公共部分拆分,利于分析代码
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest'
}),
new CleanWebpackPlugin(['dist'])
]
}

以此示例,index.js为入口文件:

1
2
3
4
5
6
7
//test.js
const str = 'test is loaded'
module.exports = str

//index.js
const test = require('./src/js/test')
console.log(test)

打包完成后的目录结构:

├── dist 
   ├── js
       ├── index-b1a8.js
       ├── manifest-eef6.js
   ├── index.html
├── node_modules
├── src
   ├── js
       ├── test.js
├── index.html          
├── package.json             
├── webpack.config.js
.

打包完成后的两个js文件的加载顺序是manifest-eef6.jsindex-b1a8.js。先看index.js中的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//index-b1a8.js
webpackJsonp([0],{
"JkW7":
(function(module, exports, __webpack_require__) {
"use strict";
var test = __webpack_require__("zFrx");
console.log(test);
}),
"zFrx":
(function(module, exports, __webpack_require__) {
"use strict";
var str = 'test is loaded';
module.exports = str;
})
},["JkW7"]);

很显然,webpackJsonp是一个全局函数,并且使用了三个参数:

  1. 数组
  2. 包含两个方法的对象,
  3. 以及[“JkW7”]数组。

在以上的第2个参数中,两个方法都包含了相同的参数moduleexports,__webpack_require,在打包的原文件中,使用了requiremodule.exports,而前者此时以__webpack_require__的形式出现。猜测它应该是模块化函数的关键。

manifest-eef6.js中的代码有151行,经过精简还有28行:

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
(function(modules) {
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
var moduleId, result;
// (1)
for (moduleId in moreModules) {
if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
modules[moduleId] = moreModules[moduleId];
}
}
// (2)
if (executeModules) {
for (i = 0; i < executeModules.length; i++) {
result = __webpack_require__(executeModules[i]);
}
}
return result;
};
var installedModules = {};

// (3)
function __webpack_require__(moduleId) {
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
var module = installedModules[moduleId] = {
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
return module.exports;
}
})([]);

整体的结构是一个IIFE,以空数组作为参数,形参为modules,而在挂载在window的全局函数webpackJsonp中,接受三个参数,分别为chunkIdsmoreModulesexecuteModules,分别对应在index-b1a8.js中的三个参数。

(1)modules通过遍历获取到moreModules内的所有方法,保存在了这个空数组中。

(2)是一个条件判断:

1
2
3
4
5
if (executeModules) {
for (i = 0; i < executeModules.length; i++) {
result = __webpack_require__(executeModules[i]);
}
}

判断第三个参数是否存在,而按照加载顺序,此时调用window下的全局方法webpackJsonp,因此执行__webpack_require__方法

(3)webpack_require函数
与(1)类似,在它的内部首先是一个赋值的逻辑。如果installedModules中存在对应的moduled,那么直接返回它的exports。而如果没有则需要赋值再返回。赋值的过程为:

1
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

在(1)中的赋值过程中modules[moduleId]已经得到了moreModules的所有方法,此时的this对象为module.exports,再把modulemodule.exports__webpack_require__传递作为参数调用。而这三个参数正是对应index中出现的三个参数,此后根据module(如test.js中的require部分)所需,递归执行__webpack_require__。在该函数中会判断是否具有缓存,如果具有便不再调用module。webpack打包的文件,就是通过函数隔离module作用域,以达到互不污染的目的。

babel的作用

webpack已经能够胜任ES6 Module的模块化的转化,但仍有ES6语法需要转译,babel可以处理ES6到ES5的转换。

babel能将ES6的模块关键字转换为CommonJS规范。这样就可以直接使用webpack运行时定义的__webpack_require__。在webpack的配置文件中需要配置才可以达到效果。方法有几种,列出其中一种:

(1).首先安装babel系列:

1
npm install --save-dev babel-loader babel-core babel-preset-latest babel-plugin-transform-runtime

(2).针对webpack配置文件

1
2
3
4
5
6
7
8
9
10
//webpack.config.js
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}
]
}

(3).创建.babelrc文件

1
2
3
4
5
6
7
8
9
//.babelrc  注意该文件形式必须符合JSON格式定义,不能出现注释!!
{
"presets": {
"babel-preset-env"
},
"plugins": {
"transform-runtime"
}
}

ES6的语法

1
2
3
4
5
6
7
//moduleA.js
export const hello = () => "hello"
export const TYPE = 'animal'
const A = "Milan"
const C = "San Siro"
export default 9
export {A,C}

转换为CommonJS语法:

1
2
3
4
5
6
7
8
9
10
//moduleA.js
var hello = exports.hello = function hello() {
return "hello";
};
var TYPE = exports.TYPE = 'animal';
var A = "Milan";
var C = "San Siro";
exports.default = 9;
exports.A = A;
exports.C = C;

babel输出都赋值给了exports,而引入时也转换为CommonJS规范,使用require引用模块。所以ES6 Module完全可以和CommonJS规范的模块互用,因为最终都转换为了CommonJS规范的语法。

webpack编译的js如何引用

通过以上配置输出的文件是无法被其他文件引用的。webpack提供了output.libraryTarget配置来定义输出文件的用途,可选项有如下:

  1. 默认项var
  2. commonjs
  3. commonjs2
  4. AMD
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//webpack.config.js
module.exports = {
//...
output: {
path: __dirname + 'dist',
filename: './[name].[chunkhash:5].js',
library: 'test' //1.默认var。不需要设置libraryTarget值

library: 'commonjs',//2.commonjs
libraryTarget: 'FOO'//输出形如 exports['FOO'] = (function(modules){})();

library: 'commonjs2'//3.commonjs2 输出形如 module.exports = {}

library:AMD //输出AMD格式
}
}

tree-shaking

webpack2开始采用了tree-shaking,即通过静态分析ES6语法,删除无用的模块。但是只对ES Module有效,所以一旦babel将ES module转换成CommonJS语法,就无法使用这一优化。在配置文件中,关闭babel的模块转换功能即可实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//webpack.config.js
//...
module:{
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
'presets': [
['babel-preset-env',{modules: false}]
]
}
}
}
]
}

修改最初的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//moduleA.js
export const hello = () => "hello"
export const TYPE = 'animal'
const A = "Milan"
const C = "San Siro"
export default 9
export {A,C}

//moduleB.js
import {hello} from './moduleA'
import a from './moduleA'
import {A} from './moduleA'
console.log(hello())
console.log(A)
console.log(a)

moduleA.js中输出了{C}变量,但是moduleB.js中并没有引用,所以优化以后moduleA.js将不再输出{C}变量