Webpack2 学习记录 — 第三章·开发/生产配置优化

code-cleanup

  上一章我们完成了 Webpack HMR 的配置,这也是在前端工程化开发后的标配。相信这些新的东西能让你体会到一些不一样的感觉。这一章我们主要把上一章的配置进行一些优化,并且添加一些打包压缩之类的配置。我们在第一章完成了基本的配置,能够打包输出静态文件。在第二章完成了 HMR 的配置,但是我们会发现在第二章里并不会输出静态文件。所以这里引出一个概念,在前端工程化的项目里,开发生产并不一定会用同一套配置。开发时我们需要自带服务器、需要 HMR ,但是在生产环境并不需要这些,我们只需要一些静态文件,往 nginx 一丢就可以了。接下来主要就会讲这一部分!

配置优化

  项目需要同时具备 devbuild 两套配置,所以我们抽离其中一些公用的配置,减少的样板代码的重复出现。仔细思考,我们会发现其中 loaders 部分是通用的,部分 plugins 也是通用的。所以我们可以在 build 目录建立一个 base 配置,内容与项目根目录下的 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
35
36
37
38
39
// webpack.base.js
module.exports = {
entry: ['./src/main.js'],
output: {
path: '/Users/aixiaoai/nodejs/webpack-demo/dist',
publicPath: '/',
filename: 'bundle.js'
},
module: {
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.(png|jpe?g|git|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
// 如果超过大小限制,则不进行编码,输出到/img目录
limit: 10000,
name: '/Users/aixiaoai/nodejs/webpack-demo/dist/img/[name].[hash:7].[ext]'
}
}
},
{
test: /\.html$/,
use: {
loader: 'html-loader',
options: {
// 压缩 html 代码
minimize: true
}
}
}
]
}
}

上面配置存在一些问题,例如存在一些绝对路径、多入口文件配置问题,我们可以使用 NodeJSpath 模块来获取正确的路径,并且对多入口文件进行修改:

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
// webpack.base.conf.js
var path = require('path')
function resolve (dir) {
return path.join(__dirname, '../', dir)
}
module.exports = {
entry: {
app: './src/main.js'
},
output: {
path: resolve('dist'),
publicPath: '/',
filename: '[name].js'
},
module: {
rules: [
// 省略相关代码
{
test: /\.(png|jpe?g|git|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
// 如果超过大小限制,则不进行编码,输出到/img目录
limit: 10000,
name: resolve('dist/img/[name].[hash:7].[ext]')
}
}
}
// 省略相关代码
]
}
}

__dirname:NodeJs Api ,获取当前目录

path.join(__dirname, ‘../‘, dir):拼接目录

但这样也不太好,因为开发生产的构建过程我们最好通过一些环境变量来进行干预,例如一些路径我们可以通过环境不同而输出到不同路径。这里我们新建一个 config 目录,用来存放环境配置:

1
2
3
4
5
// prod.env.js 生产环境变量
module.exports = {
NODE_ENV: '"production"'
}
1
2
3
4
5
6
7
8
// dev.env.js 开发环境变量
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

webpack-merge 模块用来合并对象,这里 dev.env.js 覆盖了 prod.env.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
// index.js 环境配置文件
var path = require('path')
module.exports = {
build: {
env: require('./prod.env'),
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
productionSourceMap: true,
productionGzip: false,
productionGzipExtensions: ['js', 'css'],
bundleAnalyzerReport: process.env.npm_config_report
},
dev: {
env: require('./dev.env'),
port: 8080,
autoOpenBrowser: true,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {},
cssSourceMap: false
}
}

接下来我们来改造 webpack.base.conf.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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// webpack.base.conf.js
var path = require('path')
var config = require('../config')
function resolve (dir) {
return path.join(__dirname, '../', dir)
}
module.exports = {
entry: {
app: './src/main.js'
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.json'],
alias: {
'@': resolve('src')
}
},
module: {
rules: [
{
test: /\.js$/,
loader: 'eslint-loader',
enforce: 'pre',
include: [resolve('src'), resolve('test')],
options: {
formatter: require('eslint-friendly-formatter')
}
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test')]
},
{
test: /\.(png|jpe?g|git|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
// 如果超过大小限制,则不进行编码,输出到/img目录
limit: 10000,
name: resolve('dist/img/[name].[hash:7].[ext]')
}
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: resolve('dist/fonts/[name].[hash:7].[ext]')
}
},
{
test: /\.html$/,
use: {
loader: 'html-loader',
options: {
// 压缩 html 代码
minimize: true
}
}
}
]
}
}

这里添加了 Babel 、字体相关配置。并且将 css-loader 去除,因为 css 相关 loader 比较多。需要追加一些 sass、less 等等。封装成一个工具函数比较好

Babel:用来将 ES6 编译成 ES5 的工具

ESLint 需要添加一下配置文件

1
2
3
4
// .eslintignore
build/*.js
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
// .eslintrc.js
module.exports = {
root: true,
parser: 'babel-eslint',
parserOptions: {
sourceType: 'module'
},
env: {
browser: true,
},
extends: 'standard',
// required to lint *.vue files
plugins: [
'html'
],
// add your custom rules here
'rules': {
// allow paren-less arrow functions
'arrow-parens': 0,
// allow async-await
'generator-star-spacing': 0,
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
}
}

新建一个 utils.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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// utils.js
var path = require('path')
var config = require('../config')
// 导出 css 的相关 plugin
var ExtractTextPlugin = require('extract-text-webpack-plugin')
exports.assetsPath = function (_path) {
var assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsPublicPath
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
var cssLoader = {
loader: 'css-loader',
options: {
minimize: process.env.NODE_ENV === 'production',
sourceMap: options.sourceMap
}
}
function generateLoaders (loader, loaderOptions) {
var loaders = [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'style-loader'
})
} else {
return ['style-loader'].concat(loaders)
}
}
return {
css: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true}),
scss: generateLoaders('sass')
}
}
exports.styleLoaders = function (options) {
var output = []
var loaders = exports.cssLoaders(options)
for (var extension in loaders) {
var loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
use: loader
})
}
return output
}

继续改造 webpack.base.conf.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
35
36
// webpack.base.conf.js
var path = require('path')
var utils = require('./utils')
var config = require('../config')
// 省略相关代码
module.exports = {
// 省略相关代码
module: {
rules: [
// 省略相关代码
{
test: /\.(png|jpe?g|git|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
options: {
// 如果超过大小限制,则不进行编码,输出到/img目录
limit: 10000,
name: utils.assetsPath('/img/[name].[hash:7].[ext]')
}
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
// 省略相关代码
]
}
}

至此完成了 webpack.base.conf.js 的改造,通过封装工具函数、独立路径配置来达成高可用配置的效果。接下来完成 dev 环境的配置,我们新建一个 webpack.dev.conf.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
35
36
37
// webpack.dev.conf.js
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var HtmlWebpackPlugin = require('html-webpack-plugin')
// webpack 编译错误格式化为比较友好的格式的插件
var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
// 添加 webpack-hot-middleware 客户端
Object.keys(baseWebpackConfig.entry).forEach(function (name) {
baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name])
})
// 合并 base 配置
module.exports = merge(baseWebpackConfig, {
module: {
// 添加 css loaders
rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap })
},
devtool: '#cheap-module-eval-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': config.dev.env
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
}),
new FriendlyErrorsPlugin()
]
})

接下来修改 dev-server.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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// dev-server.js
var config = require('../config')
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV)
}
// 用户自动打开浏览器
var opn = require('opn')
var path = require('path')
var express = require('express')
var webpack = require('webpack')
// 用户设置代理请求
var proxyMiddleware = require('http-proxy-middleware')
var webpackConfig = require('./webpack.dev.conf')
// 端口号
var port = process.env.PORT || config.dev.port
// 是否自动打开浏览器
var autoOpenBrowser = !!config.dev.autoOpenBrowser
// 请求转发表
var proxyTable = config.dev.proxyTable
var app = express()
var compiler = webpack(webpackConfig)
var devMiddleware = require('webpack-dev-middleware')(compiler, {
publicPath: webpackConfig.output.publicPath,
// 格式化错误提示
quiet: true
})
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
log: () => {}
})
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})
Object.keys(proxyTable).forEach(function (context) {
var options = proxyTable[context]
if (typeof options === 'string') {
options = { target: options }
}
app.use(proxyMiddleware(options.filter || context, options))
})
app.use(require('connect-history-api-fallback')())
app.use(devMiddleware)
app.use(hotMiddleware)
var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory)
app.use(staticPath, express.static('./static'))
var uri = 'http://localhost:' + port
var _resolve
var readyPromise = new Promise(resolve => {
_resolve = resolve
})
console.log('> 启动开发服务器...')
devMiddleware.waitUntilValid(() => {
console.log('> 监听端口' + uri + '\n')
if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') {
opn(uri)
}
_resolve()
})
var server = app.listen(port)
module.exports = {
ready: readyPromise,
close: () => {
server.close()
}
}

现在完成了 dev 环境的配置,执行 yarn run dev 来测试一下吧!

1

2

很完美!不是吗!有了上面的基础,接下来编写 build 配置就稍微省些力气了,新建 webpack.prod.conf.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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
// webpack.prod.conf.js
var path = require('path')
var utils = require('./utils')
var webpack = require('webpack')
var config = require('../config')
var merge = require('webpack-merge')
var baseWebpackConfig = require('./webpack.base.conf')
var CopyWebpackPlugin = require('copy-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin')
var env = config.build.env
var webpackConfig = merge(baseWebpackConfig, {
module: {
rules: utils.styleLoaders({
sourceMap: config.build.productionSourceMap,
extract: true
})
},
devtool: config.build.productionSourceMap ? "#source-map" : false,
output: {
path: config.build.assetsRoot,
filename: utils.assetsPath('js/[name].[chunkhash].js'),
chunkFilename: utils.assetsPath('js/[id].[chunkhash].js')
},
plugins: [
new webpack.DefinePlugin({
'process.env': env
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
},
sourceMap: true
}),
new ExtractTextPlugin({
filename: utils.assetsPath('css/[name].[contenthash].css')
}),
new OptimizeCSSPlugin({
cssProcessorOptions: {
safe: true
}
}),
new HtmlWebpackPlugin({
filename: config.build.index,
template: 'index.html',
inject: true,
chunksSortMode: 'dependency'
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
minChunks: function (module, count) {
return (
module.resource &&
/\.js$/.test(module.resource) &&
module.resource.indexOf(
path.join(__dirname, '../node_modules')
) === 0
)
}
}),
new webpack.optimize.CommonsChunkPlugin({
name: 'manifest',
chunks: ['vendor']
}),
// copy custom static assets
new CopyWebpackPlugin([
{
from: path.resolve(__dirname, '../static'),
to: config.build.assetsSubDirectory,
ignore: ['.*']
}
])
]
})
if (config.build.productionGzip) {
var CompressionWebpackPlugin = require('compression-webpack-plugin')
webpackConfig.plugins.push(
new CompressionWebpackPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: new RegExp(
'\\.(' +
config.build.productionGzipExtensions.join('|') +
')$'
),
threshold: 10240,
minRatio: 0.8
})
)
}
if (config.build.bundleAnalyzerReport) {
var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
webpackConfig.plugins.push(new BundleAnalyzerPlugin())
}
module.exports = webpackConfig

创建 build.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
// build.js
process.env.NODE_ENV = 'production'
var ora = require('ora')
var rm = require('rimraf')
var path = require('path')
var chalk = require('chalk')
var webpack = require('webpack')
var config = require('../config')
var webpackConfig = require('./webpack.prod.conf')
var spinner = ora('正在构建生产环境')
spinner.start()
rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => {
if (err) throw err
webpack(webpackConfig, function (err, stats) {
spinner.stop()
if (err) throw err
process.stdout.write(stats.toString({
colors: true,
modules: false,
children: false,
chunks: false,
chunkModules: false
}) + '\n\n')
console.log(chalk.cyan(' 构建完成.\n'))
})
})

安装相关的依赖:

1
yarn add babel-core babel-eslint babel-loader babel-preset-env connect-history-api-fallback copy-webpack-plugin eslint eslint-config-standard eslint-friendly-formatter eslint-loader eslint-plugin-html eslint-plugin-import eslint-plugin-node eslint-plugin-promise eslint-plugin-standard extract-text-webpack-plugin friendly-errors-webpack-plugin http-proxy-middleware less less-loader node-sass opn optimize-css-assets-webpack-plugin ora sass-loader webpack-merge

执行 yarn run build 试一下吧!

3

大功告成!源代码