上一篇介绍了Gulp,公司的React项目用的是webpack打包,抽空将webpack的知识点整理进本篇。
先简单介绍一下webpack吧。前端工程模块化,组件化就不多讲了,关键是要解析出这些模块间的依赖关系,并将它们打包合并压缩,插入到html中执行。Webpack就是模块化管理工具,可以实现模块按需加载,预处理,打包等功能。
先从官网上盗下图,webpack就是一个模块打包工具,理顺各模块间的依赖关系后,将它们按照指定的规则打包成静态资源。
从图中可以看出,webpack可以处理不同类型的模块。除js外,还能处理less,css,jade,coffeeScript等。原理是通过Loader来适配各种非js资源,将它们(除图片资源外)全都转换成js模块。
各种不同的模块可能遵循不同的标准,如CommonJS或AMD等,webpack的解析器几乎可以处理所有通用标准的模块。
执行时,webpack采用异步IO和多级缓存策略,总之,你只要知道打包速度很快就是了。
- 安装与执行
- 配置文件
- devServer
- devTool
- Loaders
- Plugins
安装与执行
安装很简单:
npm install webpack -g //安装到全局
npm install webpack --save-dev //安装到本地项目中
体验一下webpack是如何解析依赖关系并打包的,本地目录里新建两个子目录app(开发用目录)和release(发布用目录)。
app目录里新建一个Hello.js:
module.exports = function() {
var showTxt = document.createElement('div');
showTxt.textContent = "Hello webpack!";
return showTxt;
};
app目录里新建一个main.js,依赖Hello.js:
var showTxt = require('./Hello.js');
document.getElementById('root').appendChild(showTxt());
release目录里新建一个index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Webpack</title>
</head>
<body>
<div id='root'>
</div>
<script src="bundle.js"></script>
</body>
</html>
该html格式非常简单,但body里加载的bundle.js哪里来的?这就是接下来webpack需要做的。将app下开发者写的main.js和Hello.js打包生成bundle.js,并放进release目录。执行:
webpack ./app/main.js ./release/bundle.js
会发现release目录下多了个bundle.js。观察上述打包过程,webpack先解析main.js,发现它依赖Hello.js,于是将这两个文件打包进指定的release目录,生成bundle.js。用浏览器打开index.html会看到页面正确显示了Hello webpack!。
稍有点经验的开发者都知道起名index.html就不是让你本地双击浏览器打开页面的,而是应该让web服务器读取该页面。webpack提供了web服务器,先安装:
npm install --save-dev webpack-dev-server
安装完后,执行:
webpack-dev-server
现在访问http://localhost:8080
就能看到我们第一个webpack页面了。
配置文件
实际项目中,通常不会在终端敲上述命令,而是将各种需求写入webpack的配置文件中,然后一键执行。webpack的配置文件名叫webpack.config.js。看后缀就知道这个配置文件是个js文件,是个node.js模块,依赖于你前面安装的webpack模块,exports出json格式的配置信息对象。例如:
module.exports = {
entry: __dirname + "/app/main.js", // __dirname是Node的全局变量,值为当前目录
output: {
path: __dirname + "/release",
filename: "bundle.js"
}
}
这个配置文件非常简单,即使不看文档,光阅读代码也能知道都干了些什么。现在终端执行webpack就能实现一键打包啦。
当然通常会在node环境中开发,因此可以将终端命令放入package.json里:
{
...
"scripts": {
"start": "NODE_ENV=development webpack",
"dev": "NODE_ENV=development webpack-dev-server --progress",
"build": "NODE_ENV=production webpack -p"
},
...
}
-p是Webpack的命令行参数,用于打包成压缩后的production形式。但--progress用于显示Node的构建进度,包括NODE_ENV,这些不属于Webpack知识范畴。
配置文件当然不止上述entry和output这么简单(当然说实话,即使是output也不简单,除了常用的path和filename,官网还提供了丰富的配置项。入门简单,但想要玩的6并不容易)。下面介绍几个重要的配置项。
devServer
安装webpack-dev-server后,如果webpack.config.js里不配置devServer,那web服务器用的都是默认参数。你可以在webpack.config.js自定义服务器,例如:
module.exports = {
...
devServer: {
contentBase: "./release",
colors: true,
historyApiFallback: true, //针对HTML5 History API
inline: true
}
}
官网提供了很多参数,包括如果觉得localhost的127.0.0.1这个IP不爽,可以配置host设为当前PC的IP。包括如果觉得默认8080端口不爽,可以配置port,设成一个随机数Math.floor(Math.random() * 65536);
。包括给web服务器配置https指定证书等。
devTool
打包后的代码难以调试,这在开发阶段是无法接受的,因此需要source map。你可以将Source map打包进文件,也可以生成外联.map文件,当然通常推荐生成外联.map文件比较好。Webpack通过devtool来配置source map,具体选项和特性见下图:
打包速度从上到下越来越慢,而且通常调试时我们并不关心列信息,因此cheap模式可以提高效率。推荐开发阶段选择eval-source-map或cheap-module-eval-source-map,生产阶段选择cheap-module-source-map。例如:
module.exports = {
...
devtool: 'eval-source-map', // Source Maps
}
具体这些参数有什么差异,可以参考这里,整理的挺详细。
Loaders
前面已经简单提过,webpack支持将各种资源文件打包成js文件,依靠的就是Loaders加载器。参照官网的连接,通常项目中会用到多个Loader,它们都被配置在modules下面。Loaders有几个子项:
test:(必须)加载器可以处理的文件后缀名的正则表达式
loader / loaders:(必须)加载器名字。前者用字符串形式指定多个loader。后者用数组形式指定多个loader。(webpack 2.x版本中改名为rules)
include / exclude:(可选)除了正则匹配到的文件,还可以手动添加或排除某些文件
query:(可选)额外的设置选项。(webpack 2.x版本中改名为option)
以babel-loader为例,用React开发或PC端用ES6语法兼容老版本浏览器时,需要用Babel转码。我们先将Babel下载下来:
npm install --save-dev babel-core babel-loader babel-preset-es2015 babel-preset-react
其中babel-core是Babel的核心包,babel-preset-es2015用于解码ES6语法,babel-preset-react用于解码React的JSX语法(Babel的入门教程可以参照这里)
在webpack.config.js里配置Babel加载器:
module.exports = {
...
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel',
query: {
presets: ['es2015','react']
}
}
]
}
}
当然Babel的配置项很多,通常推荐将它们单独放在.babelrc这个配置文件中。这样Loaders可以简化成:
module.exports = {
...
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel'
}
]
}
}
Webpack启动babel加载器时,会自动读取.babelrc中的babel配置项。现在你可以把main.js和Hello.js的代码换成React+ES6,重启webpack-dev-server,React项目顺利启动_
再看一个webpack如何打包CSS的例子。Webpack提供了css-loader和style-loader,两者配合将css嵌入到js文件中。前者让你能用require(ES6里是import)来加载CSS模块,后者将计算好的样式加入页面中。先下载这两个加载器:
npm install --save-dev style-loader css-loader
在webpack.config.js里配置CSS加载器:
module.exports = {
...
module: {
loaders: [
...
{
test: /\.css$/,
loader: 'style!css' // !惊叹号让同一文件能够使用不同类型的loader
}
]
}
}
现在你创建好css文件后,就可以在main.js里require(ES6是import)该文件了,例如:
import './hello.css';
再看一个webpack如何打包img的例子。先下载url-loader加载器:
npm install --save-dev url-loader
在webpack.config.js里配置图片加载器:
module.exports = {
...
module: {
loaders: [
...
{
test: /\.(png|jpg)$/,
loader: 'url-loader?limit=8192'
}
]
}
}
加载器的参数可以直接追加在 loader后面,如上例中指定url-loader的limit参数为8K。(webpack 2.x版本中加载器的参数不必这样用问号拼接在loader后面了,而是新增了use参数)
准备两张图片,一张小于8K,一张大于8K,在Hello.js里加载这两张图片,例如:
![](./small.png)
![](./big.png)
url-loader会判断原始图片大小是否小于limit参数指定的8K,小于的话直接Base64转码后塞入页面,大于的话才作为单独的图片让浏览器发起请求去下载图片。
更多的加载器如何配置(例如sass,less,stylus,postcss-loader,json-loader,file-loader,raw-loader,i18n,jshint,coffeeScript等),可以见官网,Webpack提供了丰富的加载器能让你自由选择喜欢的开发工具。
Plugins
插件Plugins和加载器Loaders是不同东西。Loaders用于在打包过程中将不同类型的文件解析成js源代码,而Plugins是用于拓展Webpack功能的。介绍几个常用的plugin:
- webpack.HotModuleReplacementPlugin
- webpack.EnvironmentPlugin
- webpack.DefinePlugin
- webpack.optimize.CommonsChunkPlugin
- html-webpack-plugin
- open-browser-webpack-plugin
webpack.HotModuleReplacementPlugin热加载插件感觉是最实用的插件了。公司老项目习惯于改完代码,执行打包,通常要等2-3秒,然后刷新页面才能看到效果。但React项目由于使用了Webpack装了Hot Module Replacement(HMR)热加载插件,你的对代码的任何修改,都能自动刷新到页面上。总之一句话:用了之后再也回不去了。
配置HMR很简单,在webpack.config.js里配置plguins加载器,再在devServer里加上参数hot:
var webpack = require('webpack');
module.exports = {
...
devServer: {
hot: true,
...
},
...
plugins: [
new webpack.HotModuleReplacementPlugin()
]
}
webpack.EnvironmentPlugin插件可以让我们在client端获取到process.env的环境变量,例如:
var webpack = require('webpack');
module.exports = {
...
plugins: [
new webpack.EnvironmentPlugin([
'NODE_ENV'
])
]
}
定义了变量process.env.NODE_ENV,你可以在代码中用这个变量获取到NODE_ENV的值:
var env = process.env.NODE_ENV;
webpack.DefinePlugin插件可以为项目定义全局变量,例如:
var webpack = require('webpack');
var NODE_ENV = process.env.NODE_ENV; //从命令行获取NODE_ENV
module.exports = {
...
plugins: [
new webpack.DefinePlugin({
PRODUCTION: JSON.stringify(true),
VERSION: JSON.stringify("5fa3b9"),
BROWSER_SUPPORTS_HTML5: true,
TWO: "1+1",
"typeof window": JSON.stringify("object"),
"NODE_ENV": JSON.stringify(NODE_ENV)
})
]
}
上面最后定义的NODE_ENV,同webpack.EnvironmentPlugin例子中的功能相同。你可以在代码中直接使用这些全局变量:
if(!BROWSER_SUPPORTS_HTML5) require("html5shiv");
webpack.optimize.CommonsChunkPlugin用于将多个entry内相同的代码打包到一个共用的js里。这里相同的代码,不仅仅指源文件内写的代码,还包括打包后各个bundle.js里相同的编译后的代码。例如entry里两个文件源码有相同的依赖:
// main1.jsx
var React = require('react');
var ReactDOM = require('react-dom');
...
// main2.jsx
var React = require('react');
var ReactDOM = require('react-dom');
..
配置文件内:
module.exports = {
entry: {
bundle1: './main1.jsx',
bundle2: './main2.jsx'
},
output: {
filename: '[name].js'
},
...
plugins: [
new webpack.optimize.CommonsChunkPlugin('init.js')
]
}
表示将entry里生成的代码中相同的部分抽出,放入新文件init.js中。HTML端需要引入init.js:
<html>
<body>
...
<script src="init.js"></script>
<script src="bundle1.js"></script>
<script src="bundle2.js"></script>
</body>
</html>
html-webpack-plugin用于生成html文件,open-browser-webpack-plugin用于自动打开浏览器来加载页面。例如:
var HtmlwebpackPlugin = require('html-webpack-plugin');
var OpenBrowserPlugin = require('open-browser-webpack-plugin');
module.exports = {
...
plugins: [
new HtmlwebpackPlugin({
title: 'Webpack-demos',
filename: 'index.html'
}),
new OpenBrowserPlugin({
url: 'http://localhost:8080'
})
]
};
这样你就不用提供index.html,也不用打开浏览器访问localhost:8080了,只要webpack-dev-server启动,这些工作都交由webpack替你完成。
更多插件,例如压缩代码插件,可以到社区里去下载。
最后
Gulp和Webpack都基本可以满足前端自动化构建的任务。但侧重点不同,感觉Gulp偏重于整个过程的控制,用管道将文件连接起来。但这种处理文件的方式并不支持cmd模块化,因此需要借用browserify等工具来处理js间的依赖,最后完成打包。而Webpack正是解决了Gulp不支持处理cmd模块依赖这个痛点,能够梳理清模块间的依赖关系,最后完成打包。所以其实你可以Gulp + Webpack一起上,两者并不排它,用Gulp来控制过程(例如文件移动,压缩等),用Webpack来处理源文件间的依赖关系。