本文有些内容会前后穿插,需仔细阅读 loader 和 plugin 部分。

说到前端自动化工具,我们总会想到它的这些功能:

编译

将sass、less这类css预处理语言,编译成浏览器可识别的css,如: sass less -> css

将es6,es7这类下一带JavaScript标准或者react的jsx模板,编译成浏览器可识别的 es5,如:es6,es7,coffee,ts,jsx -> es5

将各类html引擎模板,编译成浏览器可识别的普通html页面,如:jade ejs -> html

服务搭建(引用包),文件监听(组件热更新)

现在前端工作基本都采用前后端分离的模式,很多项目的开发都基于 ajax 请求数据。

因此,在项目开发时,你需要一个运行服务平台,而很多自动化工具都内置服务,倘若没有内置服务,你也可以安装相应的模块。

另外,你还可以借助相关插件插件,来实现文件监听的功能,当项目下的文件被修改,浏览器会自动刷新。

资源压缩、合并以及打包

项目开发完,发布上线前,为减少单个文件的大小,你需要对文件代码进行压缩。为减少页面请求数,你需要合并两个或多个js或css文件,即: css,js 压缩、合并

而当修改了某个文件时,为避免浏览器缓存,每次打包需要更新修改文件的后缀名(这种后缀一般通过md5)、或者对文件重命名,即: 更新文件的md5、或重命名

对于一些特殊的静态资源,比如说图片,小尺寸的图我们希望以 data url 的形式嵌入到页面HTML中,进一步减少请求数。而大图则使用普通的引入方式,即:图片优化

通常而言,我们项目有这么两个目录,srcdist (可能有其他命名),分别表示开发目录、打包目录。当开发完成,我们将 src 下的文件打包处理后,塞进 dist 目录。即:src -> dist等 文件与目录变化

和其他前端自动化工具一样,webpack 也具备了上述功能。

webpack 的理念是将所有的资源,无论是图片、还是页面、又或者 css、js,都把它们当成模块来处理。来看看它是怎么做的,首先安装它:

以下为全局安装:

> npm install -g webpack

通过 webpack -h 查看相关信息。

一、基本操作

新建项目目录,然后 npm init,生成对应的 package.json 文件。

针对单个项目,再进行本地安装,将依赖包信息写入配置文件:

> npm install --save-dev webpack

一般而言,项目都有两个基本文件夹,即 srcdist,开发目录 和 打包目录。

我们在项目根目录创建这两个文件夹,接着,在 src 文件里,新建 index.htmlmain.js 文件。

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>
// main.js
var hello = document.createElement('div');
hello.innerHTML = 'hello webpack!';
document.body.appendChild(hello);

从上面的代码,我们可以看到,页面引用了 bundle.js 这个文件,它并非我们新建的,而是后面由webpack将 main.js 打包后的 JavaScript 文件。

要将 main.js 打包成 bundle.js。只要通过以下命令:

> webpack src/main.js dist/bundle.js

注意:如果提示 bash: webpack: command not found,你可能没全局安装 webpack。如果你实在不想全局安装也行,但需要通过如下命令:

> node_modules/.bin/webpack src/main.js dist/bundle.js

编译完成后,在 dist 目录里会新生成 bundle.js 文件,直接打开 index.html 页面,你会在页面里看到 hello webpack!

因为 webpack 被称之为 模块打包器,所以,接着用它来处理模块。我们希望把 main.js 里的文字信息抽离出来,独立成一个模块。

于是,我们在 src 文件夹里新建 text.js 文件:

// text.js
module.exports = 'hello webpack!';

main.js 也更新如下:

// main.js
var text = require('./text.js');

var hello = document.createElement('div');
hello.innerHTML = text;
document.body.appendChild(hello);

再次运行 webpack src/main.js dist/bundle.js,刷新 index.html 页面,你会发现里面的内容仍然可以正常显示。

上面用到了 webpack 最基本的命令:

> webpack <entry> [<entry>] <output>

具体可详见:webpacl-cli

除了这个命令外,还有其他几个常用的命令:

命令 说明
webpack <entry> [<entry>] <output> 启动
webpack --config 默认是指定 webpack.config.js,你可指定其它的配置文件
webpack -watchwebpack -w 观察文件系统的变化,不用输入 webpack 去重新编译
webpack -p 打包并压缩文件
webpack -d 打开SourceMap调试代码
webpack -colors 开启/关闭控制台的颜色
webpack -profile 查看每一步耗时

通过上面的命令表,我们可以知道,如果要打包后的 bundle.js 是压缩过的,只需要如下命令:

> webpack -p src/main.js dist/bundle.js

二、webpack.config.js

当我们每次开发完项目,打包每次都需要在命令行里输入类似如下命令:

> webpack -p src/main.js dist/bundle.js

打包东西少还好,但一旦内容多了起来,是否会觉得繁琐,而且也有诸多不便?还好,webpack 早就为你考虑好了,默认情况下,我们可以在项目的根目录,新建一个叫 webpack.config.js 的配置文件,因为 webpack 把所有的文件都当成模块,因此,这个配置文件,你也可以把它理解为一个模块。

// webpack.config.js
var path = require('path');

module.exports = {
entry: path.resolve(__dirname, './src/main.js'),
output: {
path: path.resolve(__dirname, './dist'),
filename: 'bundle.js'
}
};

都说要用好 webpack,关键在于配置文件,webpack 会依照配置文件的内容进行打包处理。而配置文件又涉及到四个概念,即:入口文件(Entry)、出口文件(Output)、加载器(loader)、插件(plugin)。

这意味着,要操作 webpack,理解这四个概念非常重要。

当新增完配置文件,你在命令行中,只需要输入如下命令,便可完成打包:

> webpack -p

如果我们希望打包的文件名保持不变,可以使用 webpack 中提供的关键词 [name]

// webpack.config.js
var path = require('path');

module.exports = {
entry: path.resolve(__dirname, './src/main.js'),
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].js'
},
};

此时,打包后在 dist 目录便会生成 main.js 文件。

前面文章提到了,为了解决缓存问题,自动化工具都会给有修改过的文件应用 md5 后缀机制。webpack 也不例外,它还提供了两种方案,即: [hash][chunkhash]

配置文件可修改为:

// webpack.config.js
...
filename: '[name].[hash].js'
...

// webpack.config.js
...
filename: '[name].[chunkhash].js'
...

[hash] 是 webpack 每次打包编译后的版本号,通过下面的界面(假设打包main.js 和 page.js 这两个文件),可以看出,打包后的文件,都是采用相同的 md5 后缀。

这就导致了一个问题,如果有些文件没有修改,而 md5 却改变了,这会使得这些文件在浏览器的缓存失效。

因此,你需要用到 [chunkhash]

[chunkhash] 则是基于模块内容计算出的hash值,是针对单个模块。每次打包后,只有修改的文件,才更新md5后缀,下图是我修改 page.js 前后编译打包的对比结果:

可以看到,只要有文件发生变化,webpack 每次打包后版本号都不一样,另外,修改的 page.js 的md5也发生了改变。

不过,主要注意的是,因为打包后文件名发生了变化,此时页面里引用的 js 文件就不再是打包后的文件了,怎么让页面自动引用打包的文件呢?文章后面会谈到,可通过插件解决。

[hash] 与 [chunkhash] 的异同

  • [hash][chunkhash] 默认都是20位字符串,你可以手动设置位数,如8位:[hash:8]
  • [hash] 是针对webpack打包的所有文件,是整体的。[chunkhash] 是针对单个模块,是独立的
  • webpack 建议不要在开发环境使用 [chunkhash],因为会增加编译时间。我们可以将开发和生产环境的配置环境分开,在开发环境使用 [name].js 的文件名,而生产环境使用 [name].[chunkhash].js 文件名

上面的配置文件中,涉及到 entryoutput 这两部分,下面就对这两部分做简要介绍。

入口文件(Entry)

入口文件的作用是告知 webpack 从哪里开始处理,打包哪些文件。入口文件可以有单个,也可以有多个。表现为以下几种形式:

单个形式输入,单个输出:

// webpack.config.js
...
entry: path.resolve(__dirname, './src/main.js'),
...

多个数组形式输入,合并单个输出:

// webpack.config.js
...
entry: [path.resolve(__dirname, './src/main.js'), path.resolve(__dirname, './src/page.js')]
...

多个对象形式输入,对应多个输出:

// webpack.config.js
...
entry: {
main: path.resolve(__dirname, './src/main.js'),
page: path.resolve(__dirname, './src/page.js'),
jquery: path.resolve(__dirname, './src/jquery.js')
},
...

不过,我觉得对于工具库(比如 jquery),鉴于我们基本不会去修改它,可以考虑在页面里直接使用 <script src="src/jquery.js"></script> 的形式(只是页面多时,需要每个页面,手动引入),这样,也会大大提升编译速度。

以上三种形式对应的 output 写法相同,见下面说明。

出口文件(Output)

从某种程度来说,output 生成的文件数,主要取决于 entry 的结构:

  • entry 单个文件 -> output 单个文件
  • entry 数组多个文件 -> output 合并成单个文件
  • entry 对象多个文件 -> output 对应多个文件

无论输入是哪种形式,它总是包含 pathfilename 两部分:

// webpack.config.js
...
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].[chunkhash].js',
// publicPath: 'http://www.xx.com/project/res'
}
...

除了生成的文件数外,output 的另外一个重点,则是与 [hash][chunkhash] 有关,这些内容在上面已着重介绍过。

这里面还有一个 publicPath 属性,你可能会混淆它与 path 的作用。不过没关系,后面会讲到 publicPath 的用法,当编译打包时,该属性特别有用。

三、结合 package.json

到目前为止,我们只使用到了 webpack -p 命令,可以很轻松的 hold 住。但当文件越来越多,项目越来越复杂,我们需要切换各种命令,以及在命令后面加不同参数。

此时,你会觉得记住各种命令,以及相关参数,是件头疼的事。有没有办法,可以将这些命令和参数全部列出来,形成类似键值对的映射关系,配置在某个文件中。这样,我们只需要对照映射表,运行对应命令即可。

当然有,你可以借助 package.json 文件。只需要通过里面的 scripts 属性:

在根目录的 package.json 中加入 start 命令:

// package.json
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack -p"
}
...

此时,你在命令行面板运行 npm start,你会发现编译打包的结果与 webpack -p 相同。

但如果你不想用 start 作为属性名,比如,下面使用的是 build

// package.json
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack -p"
}
...

那么你得在命令行面板中,运行 npm run build 才能正常编译打包。因为对于 package.json 文件而言, npm startnpm run start 的缩写,所以,中间的 run 可以省略。

你可以添加更多的命令,如:

// package.json
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack -p",
"watch": "webpack -w",
"debug": "webpack -d",
"prod": "webpack -p --config webpack.prod.config.js"
}
...

四、基础服务器 webpack-dev-server

很多时候,我们开发的项目都是前后端分离,即需要在服务器进行开发,调用相关的接口。并且,同时希望这个服务器能够自动检测到代码的变化,然后自动刷新浏览器。那么,此时你需要 webpack-dev-server

据官网介绍,webpack-dev-server 是一个小型的 Node.js Express 服务器,它通过 Sock.js 来连接整个服务。

安装它:

> npm install --save-dev webpack-dev-server

启动服务

要启动 webpack-dev-server 服务器,需要先在 package.json 中设置启动命令:

// package.json
...
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"server": "webpack-dev-server",
"build": "webpack -p"
}
...

在文章前面,页面里引用的js文件都是加 md5,为了避免启动服务器时,页面无法找到引用的js文件而出现报错的情况,我们先将添加 hash 处理的操作去掉,并重新编译下。

需要修改 index.htmlwebpack.config.js

<!-- index.html -->
...
<script src="../dist/main.js"></script>
...
// webpack.config.js
...
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].js'
}
...

运行 npm run build,再次打包编译。此时,页面里引用的就是 main.js 了。

然后,我们再运行 npm run server,启动devSever服务器,会看到 http://localhost:8080 ... webpack: Compiled successfully. 之类的提示。打开 http://localhost:8080/src/ 便可看到 index.html 页面。

文件监听、热更新

devServer 提供了很多配置选项,通过这些选项,我们可以使用 devserver 的不同功能。常见选项如下:

配置项 说明 默认值
contentBase 设置启动服务的目录 项目根目录
port 服务器的端口号 8080
inline 文件监听,设置 false 时,应用 iframe 模式 true
historyApiFallback 页面找不到时(404),是否重定向到 index.html,设置 false 时,不重定向 true

其中最值得一提的,当属文件监听。要实现文件监听,必须在 webpack.config.js 中进行设置:

// webpack.config.js
...
module.exports = {
...
devServer: {
inline: true,
historyApiFallback: true
}
};

值得注意的是,webpack-dev-server 编译后资源(js、css等),在本地目录是无法看到的。因为为了提升编译效率,这些编译后的文件,都被暂存在内存中。你可以理解为文件每次修改后,devSever编译后的文件,都暂存在服务器对应目录下,只是这些文件对开发者不可见而已

因此,为了使得页面里正确引用到js文件,我们还得修改 index.htmlmain.js 的路径:

<!-- index.html -->
...
<script src="main.js"></script>
...

npm run server 重启服务,然后,修改 main.js 中的内容,你会发现,页面也跟着刷新。有没有瞬间感觉页面开发效率提高了很多?

但又出现了一些新问题,当我们修改了 index.html 中的内容时,发现浏览器中的页面没跟着刷新。难道它这个监听刷新只针对页面引用的资源?随后,我们又尝试着在页面里引用一个 page.css 文件,修改 page.css,结果页面还是无法自动刷新样式。

另外,还有前面提到的,当页面重新打包编译时,页面如何跟踪引用更新过md5的资源文件。

这些问题,就需要借助 插件(plugin) 来完成!

五、插件(plugin)

首先,要明白一点,插件是处理整个项目文件。

针对上一节无法监控html、css文件,以及页面内资源的正确引入问题,可以使用 html-webpack-plugin 插件。

html-webpack-plugin

安装它:

> npm install --save-dev html-webpack-plugin

html-webpack-plugin 提供了很多配置选项,通过这些选项,我们可以使用 html-webpack-plugin 的不同功能。常见选项如下:

配置项 说明
title 生成的html文件的页面标题
filename 生成的html文件名,默认是 index.html
template 要求打包的模板
inject 向template或者templateContent中注入所有静态资源,有 true'head''body'false 四个值。设置 true 或者 body 时,所有JavaScript资源都插入body底部,head 则插入head。
chunks 插入页面模板的thunk文件,它的值是一个数组,表示该模板需要引入 entry 里的哪几个文件 。不配置的话,默认将 entry 里的所有 thunk 注入到模板中。

了解了这些配置选项后,我们重新对 webpack.config.js 进行调整:

// webpack.config.js
var path = require('path');

var HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
entry: path.resolve(__dirname, './src/main.js'),
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].[chunkhash].js'
},

devServer: {
inline: true,
historyApiFallback: true
},

plugins: [
new HtmlWebpackPlugin({
template: __dirname + '/src/index.html',
//filename: __dirname + '/dist/index.html',
inject: 'body'
})
]
};

因为 html-webpack-plugin 会跟踪页面引用的资源文件,所以 output 中的 filename 更新为加 md5 的文件。正因为如此,我们便可删除 index.html 中引用的 js文件(<script src="main.js"></script>)。

此外,我们注意到,上面的代码中,还多了 plugins 这么一项。它的值是一个数组,我们可以往里面添加多个插件。

此时,再重新运行 npm run server,便可看到,无论是修改 html文件、还是 js文件,页面都能自动刷新了。

而当运行 npm run build,你会看到 dist 目录下会新生成 index.html 以及加了md5后缀的 main.js 文件。

注意:npm run server 后,即项目开发时,需要将 filename: __dirname + '/dist/index.html'publicPath(如果设置了)进行 注释。如果不注释,会导致服务运行的页面,无法找到相关资源。而打包时,则去除注释

下面介绍的插件,可以暂时跳过,先去了解 加载器(loader),之后才会用到以下插件。

extract-text-webpack-plugin

之前的 css 都是打包到 js 文件中,这样减少了请求数,当用户打开页面时,通过 <style type="text/css">...</style> 逐个插入到网页头部。

不过,当css多时,此种做法会导致js体积很大。

此时,你希望对js文件引入的css或者less文件单独外链,你可以安装 extract-text-webpack-plugin

> npm install --save-dev extract-text-webpack-plugin

首先,你在入口js文件里引入 less:

// main.js
...
import '../css/page.less';
...

然后,你需要在配置文件 webpack.config.js 里引入该插件模块,并且对其中的 moduleplugins 进行相关设置:

// webpack.config.js
...
var ExtractTextPlugin = require('extract-text-webpack-plugin');
...
module: {
loaders: [
...

// {
// test: /\.less$/,
// loader: 'style-loader!css-loader!less-loader'
// },

{
test: /\.less$/i,
loader: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'less-loader']
})
}

...
]
},
...
plugins: [
...
new ExtractTextPlugin('css/style.[chunkhash].css')
]

这里指定了css生成目录和文件名为 css/style.[chunkhash].css,再次运行 npm run servernpm run build 后,你会发现页面的样式被外链了。但不幸的是,样式里的图片不显示。因为css文件里背景图的路径为 url(res/images/big.png?57e396ba),而css文件在 res/css/ 目录,但图片在 res/images/,这显然引用不到,因为 res/css/ 压根就没 res/images/ 这么一个目录。

所以,我们需要在 loader 里 ExtractTextPlugin 部分新增一个 publicPath 属性,来覆盖原来 output 里设置的 publicPath

// webpack.config.js
...
{
test: /\.less$/i,
loader: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'less-loader'],
publicPath: '../'
})
},
...

这样打包后的css文件,里面的背景图就都变成了 url(../images/big.png?57e396ba) 这样的路径了。

clean-webpack-plugin

随着我们一次一次的修改文件,又一次次的打包,我们会发现 dist 里的文件越来越多,因为这些文件还包括了之前打包过的。但对于单个项目而言,我们每次打完包,都是将dist里的文件直接上传到服务器。我们希望每次打完包后,dist 里只有本次打包的文件。那就需要在打包前,删除 dist 里的某些文件或清空整个dist文件夹或 dist 文件夹里的文件,打包完后,dist 只剩页面和当前引用的文件及相关资源。

要在打包前清空 dist 目录,可以通过以下两种方法:

配置npm

在配置文件 package.json 的 scripts 中,增加以下两项,其中 clean (npm run clean) 表示只清空目录,而 build (npm run build) 则表示清空目录,并编译打包文件:

// package.json
...
"scripts": {
...
"clean": "rm -r dist/*",
"build": "rm -r dist/* && webpack -p"
}
...

注意,没有 dist,或者 dist下没有文件时,运行 npm run build 会报错,这种方式使用起来可能不那么灵活。

clean-webpack-plugin

如果不想配置使用 npm 这种方式,你还可以安装插件 clean-webpack-plugin:

> npm install --save-dev clean-webpack-plugin

然后,你需要在配置文件 webpack.config.js 里引入该插件模块,并且对其中的 plugins 进行相关设置:

// webpack.config.js
...
var CleanWebpackPlugin = require('clean-webpack-plugin');
...
plugins: [
...
new CleanWebpackPlugin(
[
'dist', // 移除 'dist' 文件夹
//'build/*.*', // 移除 'build' 文件夹里有后缀名的文件
//'web/*.js' // 移除 'web' 文件夹中所有的js文件
]
)
]
...

值得一提的是,CleanWebpackPlugin 除了可以移除指定的文件夹(文件)外,它还有第二个参数(可选),该参数可以指定移除的根目录,移除是否需要打印log等信息。

可以看到,npm 方式可谓是 简单粗暴,而 clean-webpack-plugin 则是功能丰富。

六、加载器(Loader)

与 plugin 不同,loader 主要是用于处理一类文件。比如说,将 css 通过 js 引入到页面中,或者将 es6、es7、jsx 转换为 es5,又或者将 sass、less 转换为 css,下面就介绍相关的 loader。

loader 执行的三种方式:命令行、单个文件require、配置文件

// 方式一
require("!css-loader!style-loader!./style.css")

// 方式一
webpack main.js bundle.js --module-bind "css=css-loader!style-loader"

// 方式三
module: {
loaders: [
{
test: /\.css$/,
loader: 'css-loader!style-loader'
}
]
}

json-loader

该loader主要处理json文件。

首先需要说明的是,webpack2.0版本,已经自带 json-loader,因此,你无需安装,也无需在 webpack.config.js 中配置,便可直接使用json文件了。

但对于1.0的版本,安装它:

> npm install --save-dev json-loader

然后,在 webpack.config.js 中进行配置:

// webpack.config.js
...
module.exports = {
...
devServer: {
inline: true,
historyApiFallback: true
},

module: {
loaders: [
{
test: /\.json$/,
loader: 'json-loader'
}
]
},
...
};
...

通过上面代码,可以看到。module.exports 中新增了 module 项,它有一个 loaders 属性,该属性值是一个数组,我们可以往里面添加更多的loader。

css-loader、style-loader

为了更接近更真实的项目开发,我们更改下项目目录:

src
index.html
res
css
images
js
dist
...
package.json
webpack.config.js

将所有的js文件都放在 res/js 这个目录下,同时更改 webpack.config.js 的路径。

// webpack.config.js
...
module.exports = {
entry: path.resolve(__dirname, './src/res/js/main.js'),
output: {
path: path.resolve(__dirname, './dist/res/'),
filename: 'js/[name].[chunkhash].js'
}
...

这里 filename 的值前面加了一个 js 目录,目的是希望打包后的js文件都生成在js文件夹。而 path 的值则作为 css、js、images打包后的父级目录。

然后,我们再在 res/css 目录下新建个css文件 page.css,在里面写点样式:

.css-box {
font-size: 20px;
-webkit-transition: 0.5s font-size;
}
.css-box:hover {
font-size: 30px;
}

接着,在 index.html 加入类名为 css-box 的div。

<!-- index.html -->
...
<div class="css-box">xxx</div>

main.js 里面引入这个css文件:

//main.js
...
require('../css/page.css');
...

安装处理css的loader:

> npm install --save-dev css-loader style-loader

css-loader 是让js(require)具备引入css文件(@import)的功能,而 style-loader 则是将计算后样式以 <style type="text/css">...</style> 的方式插入到页面head中。

同时安装多个包时,用空白隔开即可。

webpack.config.js 中,进行这两个loader设置:

// webpack.config.js
...
module: {
loaders: [
...

{
test: /\.css$/,
loader: 'style-loader!css-loader'
}
]
},
...

运行 npm run server,你会发现页面里类名为 css-box 的div,应用了相关样式。打开控制台,你会发现这些样式,以 <style type="text/css">...</style> 的形式被插入到页面的 <head>...</head> 中。它的原理是先将这些css拼接到 main.js 里的各个模块,当页面打开时,再动态插入到 head 中。

如果需要将css文件单独外链,可参见 plugin 部分的 extract-text-webpack-plugin 章节。

less-loader

处理完 css,我们接着处理 less。首先将 page.css 直接换成 page.less,内容也作如下调整:

.css-box {
font-size: 20px;
-webkit-transition: 0.5s font-size;
&:hover {
font-size: 30px;
}
}

然后,修改 main.js 里面这个css文件:

//main.js
...
require('../css/page.less');
...

接着,安装相应的包:

npm install --save-dev less-loader less

注意,安装 less-loader 的同时,还要安装 less,否者会出现报错。

最后,配置 webpack.config.js 文件:

// webpack.config.js
...
module: {
loaders: [
...

{
test: /\.less$/,
loader: 'style-loader!css-loader!less-loader'
}
]
}
...

运行 npm run server,在浏览器中,你将看到页面里类名为 css-box 的div,已经应用了相关样式。

而对于sass,应该也是使用相似的加载器。

babel-loader

如果你在 main.js 里写点 es6 的东西:

// main.js
...
let amount = 5;
let sum = num => num * 5;

console.log(sum(amount));

再运行 npm run build 去打包编译,你会发现此时命令行里报错了,出现 Unexpected token: name (amount) 之类的提示。因为,没有正确的加载器,webpack 默认是不能识别 es6 的语法。

你需要 babel-loader 来将 es6 转换为 es5,它包含了几个独立的包,一并安装它:

> npm install --save-dev babel-core babel-preset-es2015 babel-loader

其中,babel-core 为 babel的核心模块,而 babel-preset-es2015 则是用于编译 es2016(es6)语法。

再在 webpack.config.js 里进行相关设置:

// webpack.config.js
...
module: {
loaders: [
...

{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015']
}
}
]
},
...

上面的代码中,exclude 表示不对node_modules这种依赖模块中的js做处理。再次运行 npm run build,页面便可以正常打包编译了。

前端目前最主流、最热门的框架当属 react,babel-loader 除了可以编译 es6,还能对 react 进行编译。只需要安装 reactreact-dom 这两个被拆分的包,以及解析 react 语法的模块:

> npm install --save-dev react react-dom babel-preset-react

然后设置配置文件 webpack.config.js,由于同属 babel-loader 加载器。因此,只需要在 presets 加入 react 即可:

// webpack.config.js
...
module: {
loaders: [
...

{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: ['es2015', 'react']
}
}
]
},
...

我们在 main.js 里加点 react 的代码,并且在 index.html 页面中加个 id 为 hello-react 的div容器:

// main.js
...
import React,{Component} from 'react';
import {render} from 'react-dom';

class HelloReact extends Component {
render(){
return (
<div>
<h1>Hello, React</h1>
</div>
)
}
};

render(<HelloReact />, document.getElementById('hello-react'));
<!-- index.html -->
...
<div id="hello-react"></div>

运行 npm run server 重启服务,刷新页面便可看到 id 为 hello-react 这个div里面的内容为 <div><h1>Hello, React</h1></div>

file-loader url-loader

除了文字,图片也是网页展示内容一个不可或缺的载体。其中,图片的表示形式又主要分为两种,即 html 中 <img src /> 标签,以及css中的 background 背景图。

先来看看背景图部分,在 res/images 中新增 small.pngbig.png 这两张图,并且分别在 index.htmlpage.less 中添加部分内容:

<!-- index.html -->
...
<div class="bg-box">
<div class="small"></div>
<div class="big"></div>
</div>
/** page.less **/
...
.bg-box {
.small {
width: 50px;
height: 50px;
background: url(../images/small.png);
}
.big {
width: 100px;
height: 100px;
background: url(../images/big.png);
}
}

npm run server 重启服务,发现编译失败了,提示内容没找到对应的加载器去处理文件类型。此时,你需要 file-loaderurl-loader。安装它:

> npm install --save-dev file-loader url-loader

这两者都是用于处理文件的,主要是用于处理图片。url-loader 可以看成是 file-loader的过滤器,小图片(一般不大于8192字节)可以使用 url-loader ,然后将图片以 data uri 的形式嵌入到页面或样式中,这样减少请求数。 而对于大一点的文件,我们则使用 file-loader

webpack.config.js 里进行设置:

// webpack.config.js
...
module: {
loaders: [
...

{
test: /\.(png|jpg|gif)$/,
loader: 'url-loader?limit=8192'
   }
]
},
...

再次运行 npm run server 重启服务,此时,再看浏览器里的页面,这两个背景图都能正常显示。打开控制台中的样式面板,你会发现小图背景被转换为 data uri 格式,而大图背景的图片名则是一个32位md5加密的字符串。

运行 npm run build,打包编译后,在 dist 目录打开 index.html 页面,此时发现大图无法正常显示。再仔细查看 dist 目录,发现大图被打包到了与 js 同一目录。所以,样式里引用不到这个图,这显然不是我们想要的结果。

默认情况下,当不对图片的输出路径以及名称进行设置时,图片会直接打包到引用 less 模块(即js文件)所在的目录,并且文件名为32位 md5 的hash值。

而我们想要的结果是,图片输出路径与 src 一致,为了方便识别,图片的名称也最好是一致,为了防止缓存,还需要在图片名后加md5。

要想以指定名称、指定图片的生成目录来打包图片,只需要对前面的 module 中的 url-loader 进行修改:

// webpack.config.js
...
module: {
loaders: [
...

{
test: /\.(png|jpg|gif)$/,
loader: 'url-loader?limit=8192&name=images/[name].[ext]?[hash:8]'
   }
]
},
...

其中,name=images/[name].[ext] 遵循 name=[path]/[name].[ext] 规则,[path] 表示打包后图片的目录,[name] 为文件名,[ext] 为扩展名,[hash:8] 为md5处理过的 hash 值,默认为 32位,这里设置了 8位。

再次运行 npm run build,打开打包后的 index.html 页面,结果发现 <div class="big"></div> 这个元素的背景还是无法显示。

控制台中显示它背景url的路径为 url(images/big.png?57e396ba),而打包后图片的目录为 dist/res/images,并且样式是内嵌在 index.html 里的,这显然是引用不到图片。要让图片显示,我们必须得将 url(images/big.png?57e396ba) 转变成 url(res/images/big.png?57e396ba)

怎么处理?还记得我们前面提到的 publicPath 吗?这时候就该它大显身手了。

output 中加入 publicPath 属性:

// webpack.config.js
...
output: {
path: path.resolve(__dirname, './dist/res/'),
filename: 'js/[name].[chunkhash].js',
publicPath: 'res/'
},
...

运行 npm run build,打开打包后的 index.html 页面,<div class="big"></div> 这个元素的背景正常显示了。

可以发现,在没设置 publicPath 属性之前,js文件被打包到 outputpath 属性所指定的目录,而图片被打包到了 url-loader 所指定的目录

但当设置了 publicPath 这个属性后,打包编译完后,js、images、css 这些静态资源文件夹,都会生成在 publicPath 指定的目录下

比如,上面的例子,publicPath 的值为 res/。其中 main.js 指定的输出路径为 'js/[name].[chunkhash].js',那么,打包后页面引用的路径就变成 res/js/main.js。而图片指定的输出路径为 images/[name].[ext]?[hash:8],那么,打包后css引用的路径就变成 res/images/big.png

而如果我们要将打包后的项目发布到线上或引用cdn资源,只需要将 publicPath 的值改成 http://www.xx.com/project/res 即可。

当然,设置 publicPath 也不是万能的。假设你不想样式内嵌到网页中,而是希望以外链文件的方式引入页面,那也没问题,可通过 插件-ExtractTextPlugin,但由于css是外链的,图片路径又会出现问题,解决方案见 插件-ExtractTextPlugin 章节。

html-loader

说完css背景图,我们再来看看 html 页面里,类似 <img src="res/images/hook.png" alt=""> 这种图片的引入方式。

index.html 中加入以下内容:

<!-- index.html -->
...
<div class="img-box">
<img src="res/images/small.png" alt="">
<img src="res/images/big.png" alt="">
</div>
...

打包后,发现 big.png 这张大图会根据 url-loader 里定义的规则,打包到了对应目录下,且是 md5、8位hash值名称的图片。

small.png 这张小图(小于8192字节)在页面里不显示。这时候,我们需要结合 html-loader 来进行打包:

> npm install --save-dev html-loader

然后,在module里进行设置:

// webpack.config.js
...
module: {
loaders: [
...

{
test: /\.html$/,
loader: 'html-loader'
}
]
}
...

再次运行 npm run build 进行打包,刷新页面,你会发现 small.png 这张图被转换为 data uri 格式,可以正常显示了。

而对于 jsx 里引用的图片,使用它们指定的语法即可,它可以是这样:

// main.js
...
<img src={require('../images/big.png')} />
...

也可以是这样:

// main.js
...
import big from '../images/big.png';

<img src={big} />
...

打包规则也相同,大图仍然是 图片名+md5,小图则是 data uri。

六、配置优化

webpack最难的地方在哪?当然是配置文件,尤其是涉及到多个环境的配置。

一般来说,对于一个项目而言,都有两套对应的环境,即 开发(dev)环境、生产(prod)环境。有时为了项目安全、方便测试,还会配置一套与生产环境相似的 集成(inter)环境。

通过前面的内容,我们知道,开发环境和生产环境的配置有很多相同的地方,但也存在一切差异。

比如,devServerHMR 这些设置只是针对开发环境,而生产环境则需要设置 outputpublicPath 属性,开发环境却无需配置。

所以,最简单的方式是建立两个配置文件 webpack.config.dev.jswebpack.config.prod.js,独立维护。

然后再更改 package.json 文件:

// package.json
...
"scripts": {
...
"server": "webpack-dev-server --config webpack.config.dev.js",
"build": "webpack -p --config webpack.config.prod.js"
},
...

通过不同的命令,设置不同的配置文件。

参考资料: