webpack已成为现代Web开发中最重要的工具之一。它是一个用于JavaScript的模块打包工具,但是它也可以转换所有的前端资源,例如HTML和CSS,甚至是图片。它可以让你更好地控制应用程序所产生的HTTP请求数量、允许你使用其他资源的特性(例如Jade、Sass和ES6)。webpack还可以让你轻松地从npm下载包。
本文主要针对那些刚接触webpack的同学,将介绍初始设置和配置、模块、加载器、插件、代码分割和热模块替换。
在继续学习下面的内容之前需要确保你的电脑中已经安装了Node.js。
初始配置
使用npm初始化一个新项目并安装webpack:
mkdir webpack-demo
cd webpack-demo
npm init -y
npm install webpack@beta --save-dev
mkdir src
touch index.html src/app.js webpack.config.js
编写下面这些文件:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello webpack</title>
</head>
<body>
<div id="root"></div>
<script src="dist/bundle.js"></script>
</body>
</html>
// src/app.js
const root = document.querySelector('#root')
root.innerHTML = `<p>Hello webpack.</p>`
// webpack.config.js
const webpack = require('webpack')
const path = require('path')
const config = {
context: path.resolve(__dirname, 'src'),
entry: './app.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
use: [{
loader: 'babel-loader',
options: {
presets: [
['es2015', { modules: false }]
]
}
}]
}]
}
}
module.exports = config
上面的配置是一个普通的出发点,它通知webpack将入口文件src/app.js
编译输出到文件/dist/bundle.js
中、使用babel将所有的.js
文件从ES2015转换成ES5。
为了让它可以转换到ES5格式的JS文件,我们需要安装三个包:babel-core
、webpack加载器babel-loader
和预置babel-preset-es2015
。使用{ modules: false }
让Tree Shaking 从打包文件中删除未使用的导出项(exports)以减少文件大小。
npm install babel-core babel-loader babel-preset-es2015 --save-dev
最后,用以下内容替换package.json
的scripts
部分:
"scripts": {
"start": "webpack --watch",
"build": "webpack -p"
},
在命令行中运行npm start
将以监视模式启动webpack,当src
目录中的.js
文件更改时,它将重新编译bundle.js。控制台中的输出展示了打包文件的信息,注意打包文件的数量和大小十分重要。
现在当你在浏览器中加载index.html页面时会看到"Hello webpack."。
open index.html
打开dist/bundle.js
文件看看webpack都做了哪些事情,顶部是webpack的模块引导代码,底部是我们的模块。到目前为止,你可能对于这个还没有一个深刻的印象。但是现在你可以开始编写ES6模块,webpack将生成适用于所有浏览器的打包文件。
使用 Ctrl + C
停止webpack,运行npm run build
以生产模式编译我们的bundle.js
。
注意,打包文件的大小从2.61 kB减少到了585字节。再看一下dist/bundle.js
文件,你会看到大量难懂的代码,因为我们使用UglifyJS压缩了它。这样的话,我们可以使用更少的代码达到与之前一样的效果。
模块
优秀的webpack知道如何使用各种格式的JavaScript模块,最著名的两个是:
ES2015
import
语句CommonJS
require()
语句
我们可以通过安装、导入lodash来测试这些格式的模块。
npm install lodash --save
// src/app.js
import {groupBy} from 'lodash/collection'
const people = [{
manager: 'Jen',
name: 'Bob'
}, {
manager: 'Jen',
name: 'Sue'
}, {
manager: 'Bob',
name: 'Shirley'
}, {
manager: 'Bob',
name: 'Terrence'
}]
const managerGroups = groupBy(people, 'manager')
const root = document.querySelector('#root')
root.innerHTML = `<pre>${JSON.stringify(managerGroups, null, 2)}</pre>`
运行npm start
启动webpack并刷新index.html,你可以看到一个根据manager分组的数组。
让我们将这个数组转移到它自己的模块people.js
中。
// src/people.js
const people = [{
manager: 'Jen',
name: 'Bob'
}, {
manager: 'Jen',
name: 'Sue'
}, {
manager: 'Bob',
name: 'Shirley'
}, {
manager: 'Bob',
name: 'Terrence'
}]
export default people
我们可以在app.js
中使用相对路径轻松的导入它。
// src/app.js
import {groupBy} from 'lodash/collection'
import people from './people'
const managerGroups = groupBy(people, 'manager')
const root = document.querySelector('#root')
root.innerHTML = `<pre>${JSON.stringify(managerGroups, null, 2)}</pre>`
注意:像lodash/collection
这样没有相对路径的导入是导入安装在/node_modules
的模块,你自己的模块需要一个类似./people
的相对路径,你可以以此区分它们。
加载器
我们已经介绍过,你可以通过配置像babel-loader
这样的加载器来告诉webpack在遇到不同文件类型的导入时该怎么做。你可以将多个加载器组合在一起,应用到很多文件转换中。在JS中导入.sass
文件是一个非常的例子。
Sass
这种转换涉及三个单独的加载器和node-sass
库:
npm install css-loader style-loader sass-loader node-sass --save-dev
在配置文件中为.scss
文件添加新规则。
// webpack.config.js
rules: [{
test: /\.scss$/,
use: [
'style-loader',
'css-loader',
'sass-loader'
]
}, {
// ...
}]
注意: 任何时候更改webpack.config.js
中的任意一个加载规则都需要使用Ctrl + C
和npm start
重新启动构建。
加载器序列以相反的顺序进行处理:
sass-loader
将Sass转换为CSS。css-loader
将CSS解析为JavaScript并解析所有依赖。style-loader
将我们的CSS输出到文档中的<style>标签。
你可以将它们看作函数调用,将一个加载器的输出输入到下一个加载器中。
styleLoader(cssLoader(sassLoader('source')))
添加一个sass源文件:
/* src/style.scss */
$bluegrey: #2B3A42;
pre {
padding: 20px;
background: $bluegrey;
color: #dedede;
text-shadow: 0 1px 1px rgba(#000, .5);
}
你现在可以直接在JavaScript中导入Sass文件。
// src/app.js
import './style.scss'
// ...
重新加载index.html,你应该会看到一些样式。
JS中的CSS
我们刚刚在JavaScript中将一个sass文件作为模块导入了。
打开dist/bundle.js
并搜索“pre {”
。事实上,我们的sass已被编译成一串CSS,并作为一个模块保存在我们的打包文件中。当我们将这个模块导入JavaScript中时,style-loader
会将这个字符串输入到嵌入的<style>
标签中。
我知道你在想什么---为什么?
我不会在这里过多的讨论这个问题,但这里有几个值得了解的原因:
你想要包含在项目中的JavaScript组件可能依赖于其他资源才能正常运行(HTML,CSS,图片,SVG),如果这些资源都可以打包在一起,那么导入和使用将会更容易。
消除死代码:当你的代码不再导入JS组件时,CSS也将不再被导入。生成的打包文件将只会包含执行某些操作的代码。
CSS模块:CSS的全局命名空间很难保证对CSS的更改不会产生任何副作用。CSS模块通过将CSS默认设置为本地命名空间、提供可以在JavaScript中引用的唯一类名来更改这一模式。
通过打包、分割代码等巧妙的方式来减少HTTP请求的数量。
图片
加载器的最后一个例子是使用url-loader
处理图片。
在标准的HTML文档中,当浏览器遇到<img>
标签或具有background-image
属性的元素时将请求图片。你可以使用webpack将图片存储为JavaScript字符串来对小图片进行优化处理。这样你就可以预先加载它们,并且浏览器不必在以后为其单独发起请求。
npm install file-loader url-loader --save-dev
添加一个加载图片的规则:
// webpack.config.js
rules: [{
test: /\.(png|jpg)$/,
use: [{
loader: 'url-loader',
options: { limit: 10000 } // 将大小小于10kb的图片转为base64字符串
}]
}, {
// ...
}]
使用Ctrl + C
和npm start
重新启动构建。
运行下面的命令下载测试图片:
curl https://raw.githubusercontent.com/sitepoint-editors/webpack-demo/master/src/code.png --output src/code.png
现在可以在app.js的顶部导入图片:
// src/app.js
import codeURL from './code.png'
const img = document.createElement('img')
img.src = codeURL
img.style.backgroundColor = "#2B3A42"
img.style.padding = "20px"
img.width = 32
document.body.appendChild(img)
// ...
这样将引入一个图片,其中src属性包含图片本身的数据URL。
<img src="data:image/png;base64,iVBO..." style="background: #2B3A42; padding: 20px" width="32">
此外,由于css-loader
保障了使用url()引用的图片也可以通过url-loader
运行,所以可以直接在CSS中内联它。
/* src/style.scss */
pre {
background: $bluegrey url('code.png') no-repeat center center / 32px 32px;
}
编译成这样:
pre {
background: #2b3a42 url("data:image/png;base64,iVBO...") no-repeat scroll center center / 32px 32px;
}
静态资源模块
你现在应该可以明白加载器是如何建立各种资源之间的依赖关系的。
这其实也就是webpack主页上的图片所展示的那样:
虽然JavaScript是入口点,但是webpack认为其他类型资源(如HTML,CSS和SVG)都有自己的依赖关系,所以这些类型的资源应该被视为构建过程的一部分。
插件
我们已经看到一个内置的webpack插件的例子,在npm run build
脚本中调用的webpack -p
命令就是使用webpack附带的UglifyJsPlugin
插件以生产模式压缩打包文件。
加载器可以对单个文件运行转换,插件可以运行在更大的代码块上。
公共代码
commons-chunk-plugin
是webpack附带的另一个核心插件,用于创建一个单独的模块,为多个入口文件分享公共代码。到目前为止,我们一直在使用单个入口文件和单个输出打包文件。在许多实际场景中,你将受益于将其分解为多个输入和输出文件。
如果你的应用程序有两个不同的区域需要分享某个模块,例如:用于面向公共应用程序的app.js
、用于管理区域的admin.js
,你可以像这样为其创建单独的入口点:
// webpack.config.js
const webpack = require('webpack')
const path = require('path')
const extractCommons = new webpack.optimize.CommonsChunkPlugin({
name: 'commons',
filename: 'commons.js'
})
const config = {
context: path.resolve(__dirname, 'src'),
entry: {
app: './app.js',
admin: './admin.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: '[name].bundle.js'
},
module: {
// ...
},
plugins: [
extractCommons
]
}
module.exports = config
注意output.filename
的变化,现在包含了[name]
,它会被替换为块名称。因此我们可以从这个配置中得到两个输出文件、也是我们的两个入口文件:app.bundle.js
、admin.bundle.js
。
commonschunk
插件生成第三个文件commons.js
,其中包含的是我们入口文件需要的公共模块。
// src/app.js
import './style.scss'
import {groupBy} from 'lodash/collection'
import people from './people'
const managerGroups = groupBy(people, 'manager')
const root = document.querySelector('#root')
root.innerHTML = `<pre>${JSON.stringify(managerGroups, null, 2)}</pre>`
// src/admin.js
import people from './people'
const root = document.querySelector('#root')
root.innerHTML = `<p>There are ${people.length} people.</p>`
这些入口文件将输出以下文件:
app.bundle.js包括
style
和lodash/collection
模块admin.bundle.js不包含额外的模块
commons.js包括我们的
people
模块
然后我们可以在两个区域中引入共享模块:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello webpack</title>
</head>
<body>
<div id="root"></div>
<script src="dist/commons.js"></script>
<script src="dist/app.bundle.js"></script>
</body>
</html>
<!-- admin.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello webpack</title>
</head>
<body>
<div id="root"></div>
<script src="dist/commons.js"></script>
<script src="dist/admin.bundle.js"></script>
</body>
</html>
在浏览器中加载index.html
与admin.html
可以看到它们自动的创建了通用模块。
提取CSS
另一个流行的插件是extract-text-webpack-plugin
,可用于将模块提取到自己的输出文件中。
下面我们将修改.scss
规则来编译Sass,加载CSS,然后将其提取到自己的CSS打包文件中,从而将其从JavaScript打包文件中删除。
npm install extract-text-webpack-plugin@2.0.0-beta.4 --save-dev
// webpack.config.js
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const extractCSS = new ExtractTextPlugin('[name].bundle.css')
const config = {
// ...
module: {
rules: [{
test: /\.scss$/,
loader: extractCSS.extract(['css-loader','sass-loader'])
}, {
// ...
}]
},
plugins: [
extractCSS,
// ...
]
}
重新启动webpack,你应该看到一个新的包app.bundle.css
,你可以像往常一样直接引用它。
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Hello webpack</title>
<link rel="stylesheet" href="dist/app.bundle.css">
</head>
<body>
<div id="root"></div>
<script src="dist/commons.js"></script>
<script src="dist/app.bundle.js"></script>
</body>
</html>
刷新页面以确认我们的CSS已经被编译并且从app.bundle.js
移动到app.bundle.css
中。成功!
代码分割
我们已经了解了分割代码几种方法:
手动创建单独的入口文件
将共享代码自动拆分成公共块
使用
extract-text-webpack-plugin
从我们的编译包中提取出块文件
另一个分割代码的方法是使用System.import
和 require.ensure
。通过在这些函数中封装代码块,你可以在运行时创建一个按需加载的模块。这可以显著提高加载时间性能,因为在开始时不向客户端发送所有内容。System.import
使用模块名称作为参数,并返回一个Promise。require.ensure
需要一个依赖关系的列表,一个回调和一个可选的模块的名称。
如果你的应用中有一段依赖于应用其他部分不需要的依赖,那最好把它分离成单独的包。我们通过添加一个名为dashboard.js
的新模块来演示一下,这个模块需要引入d3
模块。
npm install d3 --save
// src/dashboard.js
import * as d3 from 'd3'
console.log('Loaded!', d3)
export const draw = () => {
console.log('Draw!')
}
在app.js
的底部导入dashboard.js
。
// ...
const routes = {
dashboard: () => {
System.import('./dashboard').then((dashboard) => {
dashboard.draw()
}).catch((err) => {
console.log("Chunk loading failed")
})
}
}
// demo async loading with a timeout
setTimeout(routes.dashboard, 1000)
因为我们添加了异步加载模块,所以我们需要在配置文件中使用一个output.publicPath
属性,以便让webpack知道在哪里获取它们。
// webpack.config.js
const config = {
// ...
output: {
path: path.resolve(__dirname, 'dist'),
publicPath: '/dist/',
filename: '[name].bundle.js'
},
// ...
}
重新启动构建,你会看到一个神秘的新打包文件0.bundle.js
。
webpack为了提醒你,使用[big]
来突出显示较大的包,
这个0.bundle.js
将根据需要使用JSONP请求获取,因此直接从文件系统加载文件不会再加载它。我们需要运行一个服务器,任何服务器都可以。
python -m SimpleHTTPServer 8001
打开http://localhost:8001/
加载后一秒钟,你应该看到一个指向我们动态生成的打包文件 /dist/0.bundle.js
的GET请求和打印到控制台的“Loaded!”。成功!
Webpack Dev Server
实时重新加载可以通过在文件更改时自动刷新来真正改善开发人员体验。只需安装它,并使用webpack-dev-server
启动它,你就可以进行体验了。
npm install webpack-dev-server@2.2.0-rc.0 --save-dev
修改package.json
中的start
脚本。
"start": "webpack-dev-server --inline",
运行npm start
启动服务器并且在你的浏览器中打开http://localhost:8080/
尝试更改src
目录下的任意文件,例如更改people.js
中一个名称或者style.scss
中的一个样式,你会切身感受到这一好处。
热模块替换
如果你对实时重新加载只是印象深刻,那么热模块替换(HMR)将会让你大吃一惊。现在是2017年,可能你在使用全局状态开发单页面应用程序。在开发过程中,你会对组件进行很多小的改动,然后希望在的浏览器中真实的看到这些变化。手动刷新页面或使用实时重新加载,你的全局状态将会消失,你不得不从头开始。热加载的出现从此改变了这种情况。
在开发人员理想的工作流程中,你可以对模块进行更改,并在运行时进行编译和交换,而无需刷新浏览器(丢弃本地状态)或接触其他模块。虽然有时候仍然需要手动刷新,但HMR仍然可以节省大量的时间,预计它在未来会很流行。
在package.json
中对start
脚本进行最后一次编辑。
"start": "webpack-dev-server --inline --hot",
在app.js
的顶部告诉webpack接受该模块的热加载以及它的所有依赖。
if (module.hot) {
module.hot.accept()
}
// ...
注意:因为仅在开发阶段使用,webpack-dev-server -hot
将module.hot
设置为true, 当在生产模式下构建、module.hot
设置为false时,这些将被从打包文件中分离出来。
将NamedModulesPlugin
添加到webpack.config.js
中的插件列表中以改善控制台中的日志记录性能。
plugins: [
new webpack.NamedModulesPlugin(),
// ...
]
最后,在页面中添加一个<input>元素,我们可以在输入框中添加一些文本,以证明在我们更改模块的时候不会发生全页刷新。
<body>
<input />
<div id="root"></div>
...
用npm start
重启服务器来看看热加载!
在输入框中输入“HMR规则”,然后在people.js
中更改名称,你会发现在不刷新页面的情况下发生了内容更新并且输入框丢失输入聚焦状态。
这只是一个简单的示例,但是希望你能意识到这是非常有用的。对于像React这样基于组件的开发这更是十分有用的,你有很多“笨”组件需要与其状态分离,组件可以在不丢失状态的情况下被更新并重新呈现,因此你可以不断的获得即时反馈。
热加载CSS
更改style.scss中<pre>
元素的背景颜色,你会发现它并没有被HMR更新。
pre {
background: red;
}
事实证明,当你使用style-loader
时,CSS的HMR可以直接使用而不需要做任何操作。我们通过将CSS模块提取到外部的无法替代的CSS文件中来去除这个关联。
如果我们将Sass规则恢复到初始状态,并从插件列表中删除extractCSS
,那么你也可以看到Sass的热加载。
{
test: /\.scss$/,
loader: ['style-loader', 'css-loader','sass-loader']
}
HTTP/2
使用像webpack这样的模块打包工具的好处主要是你可以通过控制资源的构建方式来帮助你提高应用性能。多年来,将文件连接起来以减少客户端上需要的请求数量一直被认为是最佳实践。但HTTP / 2现在允许在单个请求中传送多个文件,因此连接文件不再是具有极端有效性的解决方法,但是它仍然很重要。你的应用程序实际上也可以从多个拥有单独缓存的小文件中受益,客户端可以获取单个更改的模块,而不必再次请求存在大部分相同内容的整个包。
送给你的结尾语
我希望这个关于webpack2的介绍对你有所帮助、能够开始使用它来产生很好的效果。围绕webpack的配置、加载器和插件的学习可能需要一些时间,但是了解这个工具的工作原理肯定是很有好处的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。