老早的时候就听说了 Webpack 这个工具, 当时大概的印象就是类似 Gulp 这样的东西, 并且看起来好像挺复杂的. 直到学习 React 的时候才开始接触 Webpack, 才知道 Webpack 更多的是做模块化的工作. 不过当时也是乱配置一通能用就行=.=.

现在 Vue 标配也是用 Webpack 了. Webpack 其实并没有想象中的那么复杂, 其实最核心的还是 loader 那一块. 这次就主要聊一聊 Webpack. 我用的是 Webpack 最新版本 2.1.0-beta.27.

what-is-webpack

Loader

Loader 是 Webpack 的核心, 它会自动查找项目中的我们指定的文件类型, 然后使用我们指定的 Loader 进行处理. 例如:

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
module: {
rules: [{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
css: ExtractTextPlugin.extract({
loader: ['css-loader?minimize', 'postcss-loader'],
fallbackLoader: 'vue-style-loader'
})
}
}
}, {
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
}, {
test: /\.css$/,
loader: ExtractTextPlugin.extract({
loader: ['css-loader?minimize', 'postcss-loader']
})
}, {
test: /\.(eot|woff|woff2|ttf)([\?]?.*)$/,
loader: 'file-loader'
}, {
test: /\.(png|jpg|gif|svg|ico)$/,
loader: 'url-loader?limit=8192',
}]
},

对于 Vue 文件, 我们要让 vue-loader 来处理, 这里可以先忽略 ExtractTextPlugin 部分, 它作用是提取 CSS 这个在后面会提. 对于 .js 文件, 我们使用 babel-loader 来处理, 我们可以在项目配置一个 .babelrc 文件来指定我们使用的 presets 和 plugins.

Webpack 我觉得一个不太好的地方就是写法很多, 而且那么多种写法大体是一样的, 但是在一些场景下它们可能又会有区别, 就不能统一一下吗? 例如, 如果我们使用了 Sass, 那常用的两种写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 写法1
{
test: /\.scss$/,
loader: 'css-loader!sass-loader'
}
// 写法2
{
test: /\.scss$/,
loader: [
'css-loader',
'sass-loader'
]
}

我们可以使用 ! 来连接多个 loader, 它们会自右向左执行.

另外, -loader 可以省略不写, 但在 Webpack2 中推荐写上. 如果不加 -loader 的话在一些场景下它会出错.

devServer

这个是 webpack 另一个强大的地方了. Webpack-dev-server 是一个小型的 node.js Express 服务器, 通过 websocket 可以实现浏览器的模块热替换. 即前端代码变动的时候无需刷新整个页面, 而只是把变化的部分替换掉. 关于这个热替换, 其实也有好几种配置方法, 这里我只说我用的情况.

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
devServer: {
hot: true, // 热替换
historyApiFallback: true, // HTML5 Mode
port: 7000, // 端口
proxy: { // 代理
'/api/*': {
target: 'http://127.0.0.1:3000'
},
'/auth/*': {
target: 'http://127.0.0.1:3000'
},
'/img/*': {
target: 'http://127.0.0.1:3000'
},
'/css/*': {
target: 'http://127.0.0.1:3000'
},
'/fonts/*': {
target: 'http://127.0.0.1:3000'
},
'/js/*': {
target: 'http://127.0.0.1:3000'
},
'/favicon/*': {
target: 'http://127.0.0.1:3000'
}
},
}
}

这里的 proxy 也是 webpack-dev-server 一个强大的地方之一, 我们可以配置一些代理来避免跨域问题和端口不一致的问题.

接着运行即可

1
webpack-dev-server --hot --open --inline --progress

减少打包体积

Webpack 在开发环境打包的体积非常大, 因为其包含了 source-map 等. 我们在生产环境并不需要它, 可以如下配置:

1
2
3
{
devtool: isProduction() ? false : '#eval-source-map'
}

除了这点, 有时候我们还想生产环境使用 CDN, 开发环境使用本地的资源. CDN 可以通过 externals 配置

1
2
3
4
5
6
7
8
9
{
externals = {
'vue': 'Vue',
'underscore': '_',
'vue-resource': 'VueResource',
'vue-router': 'VueRouter',
'vuex': 'Vuex'
}
}

同时不要忘记了在 index.html 中把各文件的 CDN 链接导入. 区分两个环境我们可以建立两个配置文件, 或者简单的通过条件语句判断. 这样处理后生产环境和开发环境的 index.html 就有了比较大的区别, 我处理方式是建立了两个 index.html 一个用于开发环境一个用于生产环境, 再者他们刚好也位于不同的位置, 开发环境从根目录加载 index.html , 而生产环境则有后端根据 UA 指向 public 下的 index.html

再有的优化就是进行 JavaScript 代码的压缩混淆, 当然这个也只推荐在生产环境中使用:

1
2
3
4
5
6
7
8
9
plugins.push(
// 生产环境压缩 JavaScript 代码
new webpack.optimize.UglifyJsPlugin({
test: /(\.vue|\.js)$/,
compress: {
warnings: false
},
})
)

导入这个插件即可

另外还有 CSS 的压缩:

1
2
3
4
5
6
{
test: /\.css$/,
loader: ExtractTextPlugin.extract({
loader: ['css-loader?minimize', 'postcss-loader']
})
}

只要在 css-loader 后面加上 ?minimize 就好了.

提取 CSS

就上面那段代码, 用到了 ExtractTextPlugin 这个插件, 它就是用来分离 CSS 代码的, 我们需要安装这个插件, 然后在 Webpack 中导入. 使用方法就上面这样, 但还要做一个配置:

1
2
3
4
5
6
plugins.push(
new ExtractTextPlugin({
filename: isProduction() ? 'style.[contenthash:4].css' : 'style.css',
allChunks: true,
})
)

导入这个插件, 并配置文件名. 之后他就会在我们的 output 处输出这个 CSS 文件.

我这里不仅仅是要处理 CSS 文件, 还要处理 .vue 中的 CSS 样式.

1
2
3
4
5
6
7
8
9
10
11
12
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
css: ExtractTextPlugin.extract({
loader: ['css-loader?minimize', 'postcss-loader'],
fallbackLoader: 'vue-style-loader'
})
}
}
}

注意 Webpack2 最新版本 API 想比 Webpack1 有较大变化. Webpack2 不支持在配置文件中插入其他东西, 如果你想对这个 loader 进行进一步配置, 需要在 options 中配置. 这里对 vue-loader 进行了进一步配置, 加入了 postcss-loadercss-loader .

postcss-loader 也是一个比较坑的地方, 在 Webpack2 最新版本已经不支持使用 postcss.config.js 文件的配置, 你需要自己在 Webpack 中配置这个插件.

1
2
3
4
5
6
7
8
9
plugins.push(
new webpack.LoaderOptionsPlugin({
options: {
postcss: [
require('postcss-nested'),
require('postcss-cssnext')
]
}})
)

插入这个插件后后面才可以正常使用 postcss-loader

我们同时处理了 .css.vue 中的 css 并最终生成了一个 CSS 文件. 这里在生产环境会生成 style.[contenthash:4].css , contenthash 是根据文件内容生成的, 在文件名加入其哈希值后, 我们就可以大胆的最样式表进行长期缓存, 因为样式表内容一变化文件名也变了.

文件名嵌入哈希值

除了 CSS 处理外, 我们还要对 JavaScript 进行处理, 这个是在 output 中配置的:

1
2
3
4
5
6
7
{
output = {
path: path.resolve(__dirname, './public/static/'),
publicPath: '/static/',
filename: 'build.[chunkhash:4].js'
}
}

注意这里用的是 chunkhash , 我们在 CSS 中用的则是 contenthash

最后我们就会生成如下的文件名:

1
2
style.dd51.css
build.84e5.js

但这样做是不够的, 我们不能每次都自己手动修改 index.html , 我们要让 index.html 中的文件哈希值也自动变化.

这个可以通过自定义插件来做, 我是直接参考了别人写的, 并没有深入去了解(仅供参考, 下面有说更好的方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
plugins.push(
function () {
this.plugin('done', function (statsData) {
var stats = statsData.toJson()
if (!stats.errors.length) {
var html = fs.readFileSync('./public/index.html', 'utf8')
var htmlOutput = html.replace(
/static\/(.+?)">/g,
function (word) {
let filename = word.split('/')[1].split('.')[0]
for (let i = 0; i < stats.assetsByChunkName.main.length; i++) {
if (stats.assetsByChunkName.main[i].indexOf(filename) !== -1) {
return 'static/' + stats.assetsByChunkName.main[i] + '">'
}
}
})
fs.writeFileSync(
'./public/index.html',
htmlOutput)
}
})
}
)

这里的正则和路径都是根据自己项目的情况做出来的.

大致的意思就是监听插件的 done 事件, 然后传入 statsData 到这个插件的回调函数里, 如果没有出错, 那么获取得到 webpack 生成的文件名即上面说的文件名如 style.dd51.css, 即 stats.assetsByChunkName.main 这个数组. 这个数组保存着 webpack 生成的文件名, 接着我们获取 index.html 并用正则获取所有的 scriptstyle , 我这里的处理措施是得到文件名如 style , 然后在 stats.assetsByChunkName 中查找包含这个串的输出文件名, 将这个文件名替换原来的即可.

// 2017.1.5 更新

其实还有更简便的方法, 使用 HtmlWebpackPlugin 插件, 然后进行下面的配置:

1
2
3
4
5
new HtmlWebpackPlugin({
template: 'public/index.html',
filename: '../index.html',
inject: 'head'
})

Webpack 在运行过程提取出的 chunk, 自动输出到 public/index.htmlhead 中. 然后存储到 output 设置的 publicPath 中, 因为 index.html 通常存放在资源外面, 所以这里文件名进行了相对路劲的处理.

到这一步不得不感慨 Webpack 的强大, 上面我说的有点乱, 可以在这里(最新的配置文件已经发生了改变)查看我的详细 webpack 配置. 这里没有细说每个配置的每个选项, 这些选项有些我自己也还搞不太明白, 最近还要再好好看下里面一些选项的细微区别.

总结下, 上面的 Webpack 帮我们做了这些事情:

  • 模块化

    一切皆模块, 只要有 loader. 我们可以在我们的 JS 文件中导入 CSS, 图片等资源. Webpack 会自动帮我们做处理. 只要你想的话, 你还可以用 CSS in JS. 如果你单独分离 CSS, 那么最终生成的就是一个 JavaScript 文件.

  • 使用 Babel 和 PostCSS

    在 Webpack 中使用 babel-loader 处理 .js.vue 文件, 我们就可以任性的写 ES6 和 ES7 了. 给 .cssvue-loader 加入 post-loader 后我们就可以任性的使用 cssnext 等特性了. 原本我是用 sass-loader 的, 但是我主要用的嵌套功能其实 postcss-loader 也可以处理, 并且我挺喜欢 postcss-loader 的丰富插件这个特性. 从此抛开 CSS 预处理器.

  • 压缩合并 JS 和 CSS

    不需要使用 Gulp 了. Webpack 对 JS 和 CSS 的压缩合并处理不能再简单了.

  • 代理服务器

    反向代理了我们的 API, 避免了端口修改和跨域的问题.

  • 文件哈希名

    给 CSS 和 JS 嵌入了哈希值, 并且自动替换 index.html 中的路径文件.

恩, 不愧是前端模块化和自动化利器.