爱客仕-前端团队博客园

webpack入门与实战

webpack是什么

官网是这么说的:

webpack is a module bundler.

即:

webpack 是一个模块捆绑器

我们告诉它入口模块,这个文件会依赖其他模块,经过webpack的处理,最终输出包含所有依赖模块的集成模块。这个集成模块不一定是单个模块,有时候为了按需加载,不是所有模块都被捆绑进主模块。输出的模块一般用于浏览器,我看到服务端代码也用webpack的- -

webpack有一个宗旨:一切皆模块
无论我们依赖的是js,css,less,json,图片,任何格式的文件都可以,统统require就搞定。
这里就要引出另外一个概念:loader
loader是什么呢,顾名思义,就是加载器,我们需要为所有类型的模块指定相应类型的loader(js除外,在webpack出现之前,我们已经把js文件称为模块已经好多年了)。比如:

  • css => css-loader
  • less => less-loader
  • json => json-loader
  • es6模块 => babel-loader
  • 二进制文件 => file-loader
  • png => url-loader

loader可以链式调用,比如less模块,被less-loader处理完后可以被css-loader处理,再继续被style-loader处理。
另外,这里可以提一下,url-loader继承了file-loader, 如果模块的大小,小于指定值,则会返回一个Data Url以减少http请求,对于小图片的处理非常有用!

很多人说,webpack是grunt,gulp之后的替代品。
然而,这并不完全正确!
webpack只是一个模块捆绑器,它只能处理从入口模块开始涉及到的模块,没有被直接或间接依赖的模块它都无法处理, 特殊的任务还是需要grunt,gulp等工具来完成的!因此,webpack和gulp搭配才是王道!

webpack有丰富的插件与loader,用来满足我们的大部分需求,只需要少量配置就可以处理原本通过grunt或gulp非常多配置或代码才能实现的功能!

webpack支持的依赖加载写法

  • CommonJs: 即require('a.js')
  • AMD: require(['a.js'], function (a) {})
  • ES6 module: import b from 'b' ,需借助babel-loader
  • require.ensure:
    1
    2
    3
    require.ensure(['c.js'], function (require) {
    var c = require('c.js')
    })

这是webpack提供的特殊语法,用于按需加载异步模块,跟AMD模式效果类似

webpack使用方式

  • 命令行: 运行后直接输出文件到磁盘,也可以使用watch模式,检测文件变化后,自动打包输出新的文件。
  • node服务中间件: 在开发阶段使用,将每次生成的模块存放在内存(重新打包速度更快),通过url的方式访问,与node服务完美结合。中间件方式主要有两个方案,一个是官方提供的webpack-dev-server,一个是开发者自行搭配express或koa,npm都有相应的中间件模块。后者相对复杂,但是具备更灵活的配置。

基本使用

阮一峰老师有一个非常好的教程仓库,非常推荐大家clone下来跑一跑,地址:https://github.com/ruanyf/webpack-demos
其实,这里有一丢丢标题党的嫌疑,说好的入门呢。好吧,那我就非常厚脸皮的贴一段官方代码吧,最少配置:

1
2
3
4
5
6
7
8
// webpack.config.js
module.exports = {
entry: './src/app.js',
output: {
path: './bin',
filename: 'app.bundle.js'
}
};

然后执行:

1
webpack

就在当前bin目录下生成app.bundle.js啦,入门了吧~

我们如何使用webpack

重头戏终于来啦,这里是实战部分。
我们使用的是Vuejs, 作者尤大提供了vue-loader用于加载vue模块,而且还提供了vue-cli这个项目用于脚手架,我们的webpack配置正是基于这个项目改造而来,顺便安利一下我们的基于yeoman的脚手架 generator-vue-workflow.

其实我最初接触webpack是因为React,当初找了很多网上的demo,都没有找到符合心中所想的webpack配置,看了好久官网也云里雾里(其实真正想配好wepback并不简单),直到遇到vue-cli这个项目,才找到我想要的,然后慢慢优化,渐渐完美~

我们的项目根目录会存在这三个webpack相关的配置文件:

  • webpack.base.conf.js:基础配置,此模块会被以下两个模块依赖
  • webpack.dev.conf.js:开发阶段的webpack配置,比如热替换,css注入style标签等
  • webpack.prod.conf.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
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
//webpack.base.conf.js
// 指定模块绝对路径的辅助模块, __dirname的值是当前模块的绝对路径
var path = require('path')
// 添加浏览器前缀的postcss插件
var autoprefixer = require('autoprefixer')
module.exports = {
// 入口模块配置
entry: {
index: path.join(__dirname, './src/index.js')
},
// 输出模块配置
output: {
// 输出到这个目录下
path: path.resolve(__dirname, 'dist'),
// 生成的文件名, [name] 即为entry配置中的key
filename: '[name].js'
},
// 寻找模块时的一些缺省设置
resolve: {
// 扩展名,按顺序下来查找, 比如代码里有一个 require('../a'),
// 那就现在相应目录下查找有没有叫做a的这个模块(对应数组里的第0个元素,末尾拼接一个'')
// 如果不存在,就继续在相应目录下查找有没有叫做a.js的这个模块(对应数组里的第1个元素,末尾拼接一个'.js')
extensions: ['', '.js'],
// 目录别名设置,有时很深目录里的一个模块,需要依赖另一边很深目录里的一个模块,写起来很繁琐,比如一种比较极端的情况是这样的:
// require('../../../../../../../a/b/c/d/e.js'),是不是眼睛都要花了?
// 如果这里配置了a: path.resolve(__dirname, 'src/a')
// 那么代码里只需要写成 require('a/b/c/d/e.js')就好了
alias: {
src: path.resolve(__dirname, 'src'),
store: path.join(__dirname, 'src/store'),
actions: path.join(__dirname, 'src/store/actions'),
views: path.join(__dirname, 'src/views'),
styles: path.join(__dirname, 'src/assets/styles'),
assets: path.join(__dirname, 'src/assets'),
components: path.join(__dirname, 'src/components'),
modules: path.join(__dirname, 'modules'),
util: path.join(__dirname, 'src/utils')
}
},
// 模块配置
module: {
// loader配置
loaders: [
// vue模块使用vue-loader加载
{
test: /\.vue$/,
loader: 'vue',
},
// js模块使用eslint-loader, babel-loader加载,注意顺序是【右往左】!
{
test: /\.js$/,
loader: 'babel!eslint',
// node_modules目录下的js模块,不使用eslint-loader, babel-loader加载
exclude: /node_modules/
},
// gif|jpg|jpeg|png|bmp|svg|woff|woff2|eot|ttf这些模块使用url-loader加载
{ test: /\.(gif|jpg|jpeg|png|bmp|svg|woff|woff2|eot|ttf)$/,
loader: 'url',
// url-loader的额外配置
query: {
// 小于8912字节的文件,返回dataurl
limit: 8912,
//生成的文件名,[name]为原始文件名,[hash:8]为根据文件内容生成8位md5值,[ext]为原始文件扩展名
name: '[name].[hash:8].[ext]'
}
}
]
},
// vue-loader配置
vue: {
// vue文件中的loader配置
loaders: {
// 使用eslint-loader, babel-loader加载vue文件中的js部分,注意顺序是【右往左】!
js: 'babel!eslint'
},
// postcss配置,把vue文件中的样式部分,做后续处理
postcss: [
// 添加浏览器前缀
autoprefixer({browsers: '> 1%'})
],
// 不使用默认的autoprefixer
autoprefixer: false
},
// 非vue文件中的纯样式部分的postcss配置
postcss: function () {
// 同上
return [
autoprefixer({browsers: '> 1%'})
];
},
// eslint-loader配置
eslint: {
// 以更友好的格式输出eslint错误信息
formatter: require('eslint-friendly-formatter')
}
}

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.dev.conf.js
var webpack = require('webpack')
// 引入基础配置
var config = require('./webpack.base.conf')
// 一个webpack插件,用于将输出的模块,自动生成script,link等标签插入html,以及设置标题等
var HtmlWebpackPlugin = require('html-webpack-plugin')
// 其他配置
var globalConfig = require('./global.config')
// 用于读取当前ip
var ip = require('ip')
// 拼接出当前服务的根路径,
// 如果只需要在PC端,自己访问本地服务的话,写成'/'就可以,
// 但是如果别人要访问我们的服务,或者是开发移动端应用,用ip就比较方便了~
var PUBLIC_PATH = ['http://', ip.address(), ':', globalConfig.serverPort, '/'].join('')
// eval-source-map is faster for development
config.devtool = '#eval-source-map'
// add hot-reload related code to entry chunks
var polyfill = 'eventsource-polyfill'
// 这里的path参数,理由同上,如果是非本机访问, 可以让hot-reload继续有效
var hotClient = 'webpack-hot-middleware/client?reload=true&path=' + PUBLIC_PATH + '__webpack_hmr'
Object.keys(config.entry).forEach(function (name, i) {
var extras = i === 0 ? [polyfill, hotClient] : [hotClient]
config.entry[name] = extras.concat(config.entry[name])
})
// necessary for the html plugin to work properly
// when serving the html from in-memory
config.output.publicPath = PUBLIC_PATH
// 把样式插入到style标签, 方便做样式的hot-reload,
// 如果提取出css文件,将无法做hot-reload
config.module.loaders.push(
{
test: /\.css$/,
loader: "style!css"
},
{
test: /\.styl/,
loader: 'style!css!postcss!stylus'
}
)
// 在基础配置的基础上继续添加插件
config.plugins = (config.plugins || []).concat([
// 设置前端级别的环境变量,从node借鉴过来,非常好用的特性,可以用来区分开发模式或生产模式
// 比如可以在开发模式下,快速生成表单数据,方便测试
new webpack.DefinePlugin({
'process.env': {
// 从node环境变量读取NODE_ENV传到webpack模块中,就不需要改env.js来切换了,不然有时候会忘记改回去就提交了
NODE_ENV: '"' + (process.env.NODE_ENV || 'development') + '"',
}
}),
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
// 跟hot-reload有关,按顺序加载模块
new webpack.optimize.OccurenceOrderPlugin(),
// hot-reload模块
new webpack.HotModuleReplacementPlugin(),
// 将输出的模块,自动生成script,link等标签插入html, 设置标题,favicon
new HtmlWebpackPlugin({
// 指定入口页面模板位置
template: 'index.template.ejs',
// 输出入口页面文件名
filename: 'index.html',
// 标题,从globalConfig读取
title: globalConfig.pageConfig.title,
// 设置 favicon路径
favicon: 'src/assets/images/favicon.png',
// 似乎没什么卵用?
cache: false,
// 是否插入script等标签
inject: true,
})
])
module.exports = config

类似的代码,就不重复说明了奥

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
// webpack.prod.conf.js
var path = require('path')
var webpack = require('webpack')
var config = require('./webpack.base.conf')
// 用于提取css文件的webpack插件
var ExtractTextPlugin = require('extract-text-webpack-plugin')
var HtmlWebpackPlugin = require('html-webpack-plugin')
var globalConfig = require('./global.config')
// 同步执行shell命令的模块
var exec = require('child_process').execSync
// 读取yml格式内容的模块
var YAML = require('yamljs')
// 这个项目会把静态资源发布到CDN, 需要预先读取CDN相关配置
var cdnConfig
try {
// 载入cdn配置项
cdnConfig = YAML.load(path.join(__dirname, 'upyun.config.yml'))
} catch (e) {
console.error(e)
throw new Error('Parsing upyun config error!')
}
// 静态资源的web绝对路径前缀
config.output.publicPath = 'http://' + cdnConfig.upyunConfig.bucket + '.b0.upaiyun.com/'
// 输出的主文件名
config.output.filename = '[name].[hash:8].js'
// 输出的按需加载的文件名
config.output.chunkFilename = '[id].[hash:8].js'
// whether to generate source map for production files.
// disabling this can speed up the build.
var SOURCE_MAP = true
config.devtool = SOURCE_MAP ? 'source-map' : false
// 提取CSS配置
config.module.loaders.push(
{
test: /\.css$/,
loader: ExtractTextPlugin.extract('css')
},
{
test: /\.styl/,
// 同样遵循从右到左的原则
loader: ExtractTextPlugin.extract('css!postcss!stylus')
}
)
config.plugins = (config.plugins || []).concat([
// http://vuejs.github.io/vue/workflow/production.html
new webpack.DefinePlugin({
'process.env': {
NODE_ENV: '"production"',
}
}),
// 压缩js
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
}),
new webpack.optimize.OccurenceOrderPlugin(),
// 提取CSS文件, [name]为相应的entry的key, [contenthash:8]为8位文件内容hash
new ExtractTextPlugin('[name].[contenthash:8].css'),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.template.ejs',
title: globalConfig.pageConfig.title,
favicon: 'src/assets/images/favicon.png',
inject: true,
// 实际就是git SHA-1散列值
// 为了确认打包出来的版本,特地加了对应git的commitId,感谢wj童鞋的合作
appVersion: exec('git rev-parse --short HEAD').toString().replace(/\n/, '')
})
])
module.exports = config

再贴出部分server的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// server.js
let config = require('./webpack.dev.conf')
let compiler = webpack(config)
// serve webpack bundle output
app.use(require('webpack-dev-middleware')(compiler, {
publicPath: config.output.publicPath,
stats: {
colors: true,
chunks: false
}
}))
// enable hot-reload and state-preserving
// compilation error display
app.use(require('webpack-hot-middleware')(compiler))

目前这套配置是用于真实项目的,也是我积累了大半年踩各种坑锤炼出来的,希望以上的注释能帮到大家理解webpack。

以上配置,只在开发阶段用到了node服务,线上是没有部署node服务的。
最近有一个项目部署了koa2服务,同时也用到了webpack,配置方式与上述有些不同,等比较稳定后再来一篇文章说说这个怎么玩~