什么是 webpack?

webpack 是近期最火的一款模块加载器兼打包工具,它能把各种资源,例如 JS(含 JSX)、coffee、样式(含 less/sass)、图片等都作为模块来使用和处理。

webpack 的优势

其优势主要可以归类为如下几个:

  1. webpack 是以 commonJS 的形式来书写脚本滴,但对 AMD/CMD 的支持也很全面,方便旧项目进行代码迁移。

  2. 能被模块化的不仅仅是 JS 了。

  3. 开发便捷,能替代部分 grunt/gulp 的工作,比如打包、压缩混淆、图片转 base64 等。

  4. 扩展性强,插件机制完善,特别是支持 React 热插拔(见 react-hot-loader )的功能让人眼前一亮。

我们谈谈第一点。以 AMD/CMD 模式来说,鉴于模块是异步加载的,所以我们常规需要使用 define 函数来帮我们搞回调:

1
2
3
4
5
6
7
8
9
define(['package/lib'], function (lib) {
function foo() {
lib.log('hello world!');
}

return {
foo: foo,
};
});

另外为了可以兼容 commonJS 的写法,我们也可以将 define 这么写:

1
2
3
4
5
6
7
8
9
10
11
12
define(function (require, exports, module) {
var someModule = require('someModule');
var anotherModule = require('anotherModule');

someModule.doTehAwesome();
anotherModule.doMoarAwesome();

exports.asplode = function () {
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
};
});

然而对 webpack 来说,我们可以直接在上面书写 commonJS 形式的语法,无须任何 define (毕竟最终模块都打包在一起,webpack 也会最终自动加上自己的加载器):

1
2
3
4
5
6
7
8
9
10
var someModule = require('someModule');
var anotherModule = require('anotherModule');

someModule.doTehAwesome();
anotherModule.doMoarAwesome();

exports.asplode = function () {
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
};

这样撸码自然更简单,跟回调神马的说 byebye~

不过即使你保留了之前 define 的写法也是可以滴,毕竟 webpack 的兼容性相当出色,方便你旧项目的模块直接迁移过来。

安装和配置

一. 安装

我们常规直接使用 npm 的形式来安装:

1
$ npm install webpack -g

当然如果常规项目还是把依赖写入 package.json 包去更人性化:

1
2
$ npm init
$ npm install webpack --save-dev

二. 配置

每个项目下都必须配置有一个 webpack.config.js ,它的作用如同常规的 gulpfile.js/Gruntfile.js ,就是一个配置项,告诉 webpack 它需要做什么。
需要安装的插件有:style-loader`css-loaderjsx-loadersass-loaderless-loaderbabel-loaderbabel-preset-es2015babel-preset-react `
我们看看下方的示例:

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
var webpack = require('webpack');
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js');

module.exports = {
//插件项
plugins: [commonsPlugin],
//页面入口文件配置
entry: {
index: './src/js/page/index.js',
vendor: ['vue', 'vue-router', 'vue-resource', 'vuex'], // 将这些库单独打包 webpack2是vendors
},
//入口文件输出配置
output: {
path: 'dist/js/page',
filename: '[name].js',
},
module: {
//加载器配置
loaders: [
{ test: /\.css$/, loader: 'style-loader!css-loader' },
{ test: /\.js[x]?$/, loader: 'jsx-loader?harmony' },
{ test: /\.scss$/, loader: 'style!css!sass?sourceMap' },
{ test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192' },
{
test: /\.jsx?$/,
loader: 'babel',
query: { presets: ['react', 'es2015'] },
},
],
},
//其它解决方案配置
resolve: {
root: 'E:/github/flux-example/src', //绝对路径
extensions: ['', '.js', '.json', '.scss'],
alias: {
AppStore: 'js/stores/AppStores.js',
ActionType: 'js/actions/ActionType.js',
AppAction: 'js/actions/AppAction.js',
},
},
};

⑴ plugins 是插件项,这里我们使用了一个 CommonsChunkPlugin 的插件,它用于提取多个入口文件的公共脚本部分,然后生成一个 common.js 来方便多页面之间进行复用。

⑵ entry 是页面入口文件配置,output 是对应输出项配置(即入口文件最终要生成什么名字的文件、存放到哪里),其语法大致为:

1
2
3
4
5
6
7
8
9
10
11
{
entry: {
page1: "./page1",
//支持数组形式,将加载数组中的所有模块,但以最后一个模块作为输出
page2: ["./entry1", "./entry2"]
},
output: {
path: "dist/js/page",
filename: "[name].bundle.js"
}
}

该段代码最终会生成一个 page1.bundle.js 和 page2.bundle.js,并存放到 ./dist/js/page 文件夹下。

⑶ module.loaders 是最关键的一块配置。它告知 webpack 每一种文件都需要使用什么加载器来处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module: {
//加载器配置
loaders: [
//.css 文件使用 style-loader 和 css-loader 来处理
{ test: /\.css$/, loader: 'style-loader!css-loader' },
//.jsx 文件使用 jsx-loader 来编译处理
{ test: /\.js[x]?$/, loader: 'jsx-loader?harmony' },
//.scss 文件使用 style-loader、css-loader 和 sass-loader 来编译处理
{ test: /\.scss$/, loader: 'style!css!sass?sourceMap'},
//.less 文件使用 style-loader、css-loader 和 less-loader 来编译处理
{ test: /\.less$/, loader: 'style!css!less?sourceMap'},
//图片文件使用 url-loader 来处理,小于8kb的直接转为base64
{ test: /\.(png|jpg)$/, loader: 'url-loader?limit=8192'}
]
}

如上,”-loader”其实是可以省略不写的,多个 loader 之间用“!”连接起来。

注意所有的加载器都需要通过 npm 来加载,并建议查阅它们对应的 readme 来看看如何使用。

拿最后一个url-loader来说,它会将样式中引用到的图片转为模块来处理,使用该加载器需要先进行安装:

1
npm install url-loader -save-dev

配置信息的参数“?limit=8192”表示将所有小于 8kb 的图片都转为 base64 形式
(其实应该说超过 8kb 的才使用 url-loader 来映射到文件,否则转为 data url 形式)

你可以点这里查阅全部的 loader 列表。
常用的还有:
autoprefixer-loader、json-loader

⑷ 最后是 resolve 配置,这块很好理解,直接写注释了:

1
2
3
4
5
6
7
8
9
10
11
12
resolve: {
//查找module的话从这里开始查找
root: 'E:/github/flux-example/src', //绝对路径
//自动扩展文件后缀名,意味着我们require模块可以省略不写后缀名
extensions: ['', '.js', '.json', '.scss'],
//模块别名定义,方便后续直接引用别名,无须多写长长的地址
alias: {
AppStore : 'js/stores/AppStores.js',//后续直接 require('AppStore') 即可
ActionType : 'js/actions/ActionType.js',
AppAction : 'js/actions/AppAction.js'
}
}

关于 webpack.config.js 更详尽的配置可以参考这里

运行 webpack

webpack 的执行也很简单,直接执行

1
$ webpack --display-error-details

即可,后面的参数“–display-error-details”是推荐加上的,方便出错时能查阅更详尽的信息(比如 webpack 寻找模块的过程),从而更好定位到问题。

其他主要的参数有:

1
2
3
4
5
6
7
$ webpack --config XXX.js   //使用另一份配置文件(比如webpack.config2.js)来打包

$ webpack -w //监听变动并自动打包

$ webpack -p //压缩混淆脚本,这个非常非常重要!

$ webpack -d //生成map映射文件,告知哪些模块被最终打包到哪里了

其中的-p是很重要的参数,曾经一个未压缩的 700kb 的文件,压缩后直接降到 180kb,
主要是样式这块一句就独占一行脚本,导致未压缩脚本变得很大。

模块引入

上面唠嗑了那么多配置和执行方法,下面开始说说寻常页面和脚本怎么使用呗。

一. HTML

直接在页面引入 webpack 最终生成的页面脚本即可,不用再写什么 data-main 或 seajs.use 了:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title>demo</title>
</head>
<body>
<script src="dist/js/page/common.js"></script>
<script src="dist/js/page/index.js"></script>
</body>
</html>

可以看到我们连样式都不用引入,毕竟脚本执行时会动态生成style并标签打到 head 里。

二. JS

各脚本模块可以直接使用 commonJS 来书写,并可以直接引入未经编译的模块,比如 JSX、sass、coffee 等(只要你在 webpack.config.js 里配置好了对应的加载器)。

我们再看看编译前的页面入口文件(index.jsx):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require('../../css/reset.scss'); //加载初始化样式
require('../../css/allComponent.scss'); //加载组件样式
var React = require('react');
var AppWrap = require('../component/AppWrap'); //加载组件
var createRedux = require('redux').createRedux;
var Provider = require('redux/react').Provider;
var stores = require('AppStore');

var redux = createRedux(stores);

var App = React.createClass({
render: function() {
return (
<Provider redux={redux}>
{function() { return <AppWrap >; }}
</Provider>
);
}
});

React.render(
<App>, document.body
);

一切就是这么简单么么哒~ 后续各种有的没的,webpack 都会帮你进行处理。

其他

至此我们已经基本上手了 webpack 的使用,下面是补充一些有用的技巧。

一. shimming

在 AMD/CMD 中,我们需要对不符合规范的模块(比如一些直接返回全局变量的插件)进行 shim 处理,
这时候我们需要使用exports-loader 来帮忙:

1
{ test: require.resolve("./src/js/tool/swipe.js"),  loader: "exports?swipe"}

之后在脚本中需要引用该模块的时候,这么简单地来使用就可以了:

1
2
require('./tool/swipe.js');
swipe();

二. 自定义公共模块提取

在文章开始我们使用了 CommonsChunkPlugin 插件来提取多个页面之间的公共模块,并将该模块打包为 common.js 。

但有时候我们希望能更加个性化一些,我们可以这样配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
module.exports = {
entry: {
p1: './page1',
p2: './page2',
p3: './page3',
ap1: './admin/page1',
ap2: './admin/page2',
},
output: {
filename: '[name].js',
},
plugins: [
new CommonsChunkPlugin('admin-commons.js', ['ap1', 'ap2']),
new CommonsChunkPlugin('commons.js', ['p1', 'p2', 'admin-commons.js']),
],
};
// <script>s required:
// page1.html: commons.js, p1.js
// page2.html: commons.js, p2.js
// page3.html: p3.js
// admin-page1.html: commons.js, admin-commons.js, ap1.js
// admin-page2.html: commons.js, admin-commons.js, ap2.js

三. 独立打包样式文件

有时候可能希望项目的样式能不要被打包到脚本中,而是独立出来作为.css,然后在页面中以link标签引入。
这时候我们需要extract-text-webpack-plugin来帮忙:

1
2
3
4
5
6
7
8
var webpack = require('webpack');
var commonsPlugin = new webpack.optimize.CommonsChunkPlugin('common.js');
var ExtractTextPlugin = require("extract-text-webpack-plugin");

module.exports = {
plugins: [commonsPlugin, new ExtractTextPlugin("[name].css")],
entry: {
//...省略其它配置

最终 webpack 执行后会乖乖地把样式文件提取出来:

四. 使用 CDN/远程文件

有时候我们希望某些模块走 CDN 并以script的形式挂载到页面上来加载,但又希望能在 webpack 的模块中使用上。

这时候我们可以在配置文件里使用 externals 属性来帮忙:

1
2
3
4
5
6
7
{
externals: {
// require("jquery") 是引用自外部模块的
// 对应全局变量 jQuery
"jquery": "jQuery"
}
}

需要留意的是,得确保 CDN 文件必须在 webpack 打包文件引入之前先引入。

我们倒也可以使用 script.js

1
2
3
4
5
6
7
var $script = require('scriptjs');
$script(
'//ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js',
function () {
$('body').html('It works!');
}
);

五. 与 grunt/gulp 配合

以 gulp 为示例,我们可以这样混搭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
gulp.task('webpack', function (callback) {
// run webpack
webpack(
{
// configuration
},
function (err, stats) {
if (err) throw new gutil.PluginError('webpack', err);
gutil.log(
'[webpack]',
stats.toString({
// output options
})
);
callback();
}
);
});

当然我们只需要把配置写到 webpack({ … }) 中去即可,无须再写 webpack.config.js 了。

更多参照信息请参阅:grunt 配置/gulp 配置

六. React 相关

⑴ 推荐使用 npm install react 的形式来安装并引用 React 模块,而不是直接使用编译后的 react.js,这样最终编译出来的 React 部分的脚本会减少 10-20 kb 左右的大小。

react-hot-loader是一款非常好用的 React 热插拔的加载插件,通过它可以实现修改-运行同步的效果,配合webpack-dev-server使用更佳!

1
webpack-dev-server --hot

类似于监听的热插拔功能

七、其他插件

https://github.com/ruanyf/webpack-demos#demo07-uglifyjs-plugin-source
建议:配合 gulp 来使用

polyfill 和 runtime 的区别

ransform-runtime 只会对 es6 的语法进行转换(例如代码块{},class 等等)
babel-polyfill,对新 api 进行转换

总结

webpack 就是向 react 这种存在依赖关系的工具才用这种个打包工具(需要 require 这种)
只是解决依赖库的打包,其他的还是用 gulp 来完成的
不过可以都通过这个工具打包成 js 文件,最后用 gulp 来打包成一个 js 就行了

优化体积

参考文章

可视化定位 webpack 大的原因

webpack-bundle-analyze
类似工具
webpack-chartwebpack-analyse

提升构建速度

DllPlugin 和 DllReferencePlugin 提供了以大幅度提高构建时间性能的方式拆分软件包的方法。其中原理是,将特定的第三方 NPM 包模块提前构建 👌,然后通过页面引入。这不仅能够使得 vendor 文件可以大幅度减小,同时,也极大的提高了构件速度。鉴于篇幅,具体用法可参见:webpack.dll.conf.js

外部引入模块(CDN)

webpack 可以处理使之不参与打包,而依旧可以在代码中通过 CMD、AMD 或者 window/global 全局的方式访问

1
2
3
4
// webpack 中予以指定 externals: { // 'vue': 'Vue', // 'lodash': '_',
'babel-polyfill': 'window' } //
<script src="//cdn.bootcss.com/autotrack/2.4.1/autotrack.js"></script>
<script src="//cdn.bootcss.com/babel-polyfill/7.0.0-alpha.15/polyfill.min.js"></script>

让每个第三包“引有所值”

1.不引入没必要的库–jquery 2.避免类库引用而不用,可以通过 ESlint 来排查规范 3.使用模块化引入
lodash 提供了模块化的引入方式;可按需引入

6
1
2
3
4
5
import { debounce } from 'lodash'
import { throttle } from 'lodash'
// 改成如下写法
import debounce from 'lodash/debounce'
import throttle from 'lodash/throttle'

优化:
lodash-webpack-plugin 和 babel-plugin-lodash (组合使用),可将全路径引用的 lodash, 自动转变为模块化按使用引入(如下例示);

6
1
2
3
4
// 引入组件,自动转换
import _ from 'lodash'
_.debounce()
_.throttle()

还是不够快捷,每个用到的文件,都写一遍 import,实在多有不便。
更可取的是,将项目所需的方法,统一引入,按需添加,组建出本地 lodash 类库,然后 export 给框架层(比如 Vue.prototype),以便全局使用

6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// helper 文件夹下 lodash,统一引入你需要的方法
import _ from 'lodash'
export default {
cloneDeep: _.cloneDeep,
debounce: _.debounce,
throttle: _.throttle,
size: _.size,
pick: _.pick,
isEmpty: _.isEmpty
}
// 注入到全局
import _ from '@helper/lodash.js'
Vue.prototype.$_ = _ // 重点语句
// vue 组件内运用
this.$_.debounce()

4.引入更合适的包
moment 太大可以改用date-fns
现代 JavaScript 日期实用程序库,如 lodash 一样,可支持模块化

按需异步加载模块

6
1
import Foo from './Foo.vue'

改为如下写法:

6
1
const Foo = () => import('./Foo.vue')

生产环境,压缩混淆并移除 console

使用 UglifyJsPlugin 插件来压缩代码,加入如下配置,即可移除掉代码中的 console

6
1
2
3
4
5
6
7
8
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false,
drop_console: true,
pure_funcs: ['console.log']
},
sourceMap: false
})

Webpack3 新功能: Scope Hoisting

又译作“作用域提升”。
只需在配置文件中添加一个新的插件,就可以让 Webpack 打包出来的代码文件更小、运行的更快:

6
1
2
3
4
5
module.exports = {
plugins: [
new webpack.optimize.ModuleConcatenationPlugin()
]
}

据悉这个 Scope Hoisting 与 Tree Shaking,最初都是由 Rollup 实现的.

模块化原理

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
// 1、模块缓存对象
var installedModules = {};
// 2、webpack实现的require
function __webpack_require__(moduleId) {
// 3、判断是否已缓存模块
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 4、缓存模块
var module = (installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {},
});
// 5、调用模块函数
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
// 6、标记模块为已加载
module.l = true;
// 7、返回module.exports
return module.exports;
}
// 8、require第一个模块
return __webpack_require__((__webpack_require__.s = 0));
/************************************************************************/
[
/* 0 */
function (module, exports, __webpack_require__) {
'use strict';

var bar = __webpack_require__(1);
bar();
},
/* 1 */
function (module, exports, __webpack_require__) {
'use strict';

exports.bar = function () {
return 1;
};
},
];

参考文章 1
参考文章 2

Webpack 优化

文件模块化显示

harmony
1
2
3
4
build: {
// Set to `true` or `false` to always turn it on or off
bundleAnalyzerReport: true; // 改为true会自动打开 http://127.0.0.1:8888/
}

重复块优化

1. 优化配置

  • 升级webpack 3,优化 js 的编译能力(Scope Hoisting)
1
2
// 主要配置
plugins: [new webpack.optimize.ModuleConcatenationPlugin()];
  • 合理规划 entry 入口配置(平衡 vendor.js, app.js 文件的大小)
1
2
3
4
5
6
7
8
9
10
11
12
// main.js中第三方公共库提出,作为公共vendor.js, 配合package.json固定第三方库版本,最大化利用浏览器缓存加载js
entry: {
vendor:['vue', 'vue-router', 'vue-resource'],
app: './src/main.js'
}
// ...
plugins:[
new webpack.optimize.CommonsChunkPlugin({
name: ['manifest','vendor'].reverse(),
minChunks:Infinity
})
]

2. 减小代码量

  • 提取 chunk 中使用的公共库(能为 chunk 代码节约近 1/3 的代码量)
1
2
3
4
5
6
7
8
9
new webpack.optimize.CommonsChunkPlugin({
name: 'app',
async: 'vendor-async',
children: true,
minChunks: (module, count) => {
// 被 3 个及以上 chunk 使用的共用模块提取出来
return count >= 3;
},
});
  • 减少图片 base64 的使用,降低  限制,限制 2k(vue 官方配置是 10k,会大大增加 js 文件体积,移动端对 base64 的解析成本高)
1
2
3
4
5
6
7
8
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 2048,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
}
  • 生产模式(pro)下第三方库使用已压缩版本
1
2
3
4
5
6
7
8
9
10
11
12
// 开发模式
resolve: {
alias: {
'vue': 'vue/dist/vue.esm.js'
}
}
// 生产模式
resolve: {
alias: {
'vue': 'vue/dist/vue.min.js'
}
}
  • 不要再引入 babel-polyfill(随着 es6,es7api 越来越多体积越来越大,目前 120 多 k,两月前 80k)
1
2
3
4
entry: {
vendor:['babel-polyfill', 'vue', 'vue-router', 'vue-resource'],
app: './src/main.js'
}
  • 极致压缩 js,css 代码
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
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin');

plugins: [
new webpack.optimize.UglifyJsPlugin({
// 最紧凑的输出
beautify: false,
// 删除所有的注释
comments: false,
compress: {
// 在UglifyJs删除没有用到的代码时不输出警告
warnings: false,
// 删除所有的 `console` 语句
drop_console: true,
// 内嵌定义了但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
},
sourceMap: true,
}),
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true,
discardComments: { removeAll: true },
},
}),
];
  • 第三方库的依赖过滤,如下:
1
2
// 此插件默认全部引入语言库,但我们只用到了中文,最多英文,所以进行了过滤,大大减少了总体代码量
plugins: [new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /zh|en/)];

2. 减少请求数:

  • manifest.js 文件  内联(app.css 可以自行选择,当小于 10k 是最好内联),webpack 推荐配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
// 引入内联插件
var HtmlWebpackInlineSourcePlugin = require('html-webpack-inline-source-plugin');

plugins: [
// ...
new HtmlWebpackPlugin({
// ... 其他不相关配置省略
inlineSource: /(app\.(.+)?\.css|manifest\.(.+)?\.js)$/,
// ...
}),
new HtmlWebpackInlineSourcePlugin(),
];

开发插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//  MyPlugin.js

function MyPlugin(options) {
// Configure your plugin with options...
}

MyPlugin.prototype.apply = function (compiler) {
// ...
compiler.plugin('compilation', function (compilation) {
console.log('The compiler is starting a new compilation...');

compilation.plugin(
'html-webpack-plugin-before-html-processing',
function (htmlPluginData, callback) {
htmlPluginData.html += 'The magic footer';
callback(null, htmlPluginData);
}
);
});
};

module.exports = MyPlugin;

Vue CLI3 相关配置

深入浅析 vue-cli@3.0 使用及配置说明
关于 Preload, 你应该知道些什么?

← Prev Next →