概述
webpack是一个模块打包工具,支持CommonJs、AMD、ES6等模块化方式,并将各种静态资源都视为模块。webpack会递归地解析所有文件,构建成一个依赖关系图,最终打包成一个文件,由浏览器加载。
模块
webpack支持以下模块:
- ES6 import语句
- CommonJS require语句
- AMD define、require语句
- css/sass/less @import语句
- 样式(url(…))或 HTML 文件(
<img src=...>
)中的图片链接(image url)
核心概念
- entry: 一个可执行模块或库的入口文件。
- chunk :多个文件组成的一个代码块,例如把一个可执行模块和它所有依赖的模块组合和一个 chunk 。这体现了webpack的打包机制。
- loader :文件转换器,例如把es6转换为es5,scss转换为css。
- plugin :插件,用于扩展webpack的功能,在webpack构建生命周期的节点上加入扩展hook为webpack加入功能。
配置
webpack.config.js作为配置文件。由于这个文件是在nodejs中运行的,因此不支持ES6的import语法。
const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
index: [path.resolve(__dirname, 'src/index.js')],
vendors: ['react', 'react-dom', 'react-redux', 'react-router-dom'],
polyfills: ['babel-polyfill'],
libs: ['moment']
},
output: {
path: path.resolve(__dirname, DevConfig.dist),
publicPath: DevConfig.publicPath,
filename: '[name].[hash].js'
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
//react-hot-loader用于react热加载,即只有更改的部分会刷新,其它部分保持不变
//要想使用热加载,除了此处之外,还需在dev-server中开启hot选项,并启用HotModuleReplacementPlugin
loader: 'react-hot-loader!babel-loader?presets[]=es2015&presets[]=react',
exclude: path.resolve(__dirname, 'node_modules')
},
{
enforce: "pre",
test: /\.js|jsx$/,
exclude: /node_modules/,
loader: "eslint-loader",
},
{
test: /\.scss$/,
loader: 'style-loader!css-loader!sass-loader'
}
{
test: /\.(png|jpg|gif|woff|woff2)$/,
loader: 'url-loader?limit=8192'
}
]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
names: ['vendors', 'polyfills', 'libs', 'manifest']
}),
new HtmlWebpackPlugin({
title: 'test',
template: path.resolve(__dirname, 'src/index.html'),
filename: 'index.html',
chunks: ['index', 'vendors', 'polyfills', 'libs', 'manifest'],
inject: 'body'
}),
new InlineManifestWebpackPlugin({
name: 'webpackManifest'
}),
new webpack.HotModuleReplacementPlugin()
],
devServer: {
port: 8100,
historyApiFallback: true
}
}
下面来一项一项解释。
entry
entry 参数定义了打包的入口文件。
- 单个入口文件
传入字符串形式的文件路径:
module.exports = {
entry:'./src/main.js'
}
- 多个入口文件
数组形式:将文件路径数组传给entry,数组中所有文件最终会打包生成一个文件。
module.exports = {
entry:["./src/main.js","./src/index.js]
}
对象语法:是定义入口的最可扩展的方式,最后可以从不同的入口文件出发构建成不同的文件。
{
entry:{
page1: "./page1",
page2: ["./entry1", "./entry2"]
},
output:{
path:'dist',
publicPath:'/output',
filename:'[name].bundle.js'
}
}
上面这段代码最终会生成page1.bundle.js和page2.bundle.js,并存放在dist文件夹下。
output
配置打包输出相关。
注意,即使可以存在多个入口起点,但只指定一个输出配置。
output接受一个对象作为配置参数。
{
output:{
path:'dist',
filename: 'bundle.js',
}
}
- path:打包文件存放的绝对路径
- filename:打包后的文件名
因此现在打包文件将为dist/buldle.js。
正如之前在entry的配置中看到的,如果配置entry使用了对象语法,传入了多个入口文件,那么在output中可以使用占位符来确保每个打包文件有唯一的名称:
{
entry: {
app: './src/app.js',
search: './src/search.js'
},
output: {
filename: '[name].[hash].js',
path: __dirname + '/dist'
}
}
其中name占位符对应的是entry中相应的入口名称,此处为app与search;
而hash占位符则可以保证每次打包生成的文件有一个hash值,来防止缓存
最后两个打包文件分别为 dist/app.xxx.js 和 dist/search.xxx.js。
module
在webpack中JavaScript,CSS,LESS,TypeScript,JSX,CoffeeScript,图片等静态文件都可以是模块,都可以通过import导入。
不同模块的加载是通过相应的loader来进行编译的。
{
module:{
rules:[
//loader之间用!相连,表示使用多个loader。
//多个loader从右向左匹配,最后一个loader一定返回的是js
{
test:/\.scss$/,
loader:'style-loader!css-loader!less-loader'
},
{
test:/\.css$/,
loader:'style-loader!css-loader'
},
//使用babel-loader处理js和jsx,其中presets[]=es2015&presets[]=react表示要使用babel-preset-es2015与babel-preset-react,分别用于转换es2015和react jsx
//exclude用于排除node_modules目录下的文件, npm安装的包不需要编译
//react-hot-loader用于react组件的热替换,也就是修改代码后可以实时看到变化,而无需刷新浏览器。Webpack开发服务器也需要开启HMR参数hot
{
test: /\.(js|jsx)$/,
loader: 'react-hot-loader!babel-loader?presets[]=es2015&presets[]=react',
exclude: path.resolve(__dirname, 'node_modules')
},
//enforce属性用于描述loader是前置、normal还是后置的
//这里eslint-loader为前置,也就是在其他匹配js/jsx的loader前加载
{
enforce: "pre",
test: /\.js|jsx$/,
exclude: /node_modules/,
loader: "eslint-loader",
},
{
test:/\.(png|jpg)$/,
loader:'url-loader?limit=8192'
//url-loader接受一个limit参数, 单位为字节(byte)
//当文件体积小于limit时, url-loader把文件转为Data URI的格式内联到引用的地方
//当文件大于limit时, url-loader会调用file-loader, 把文件储存到输出目录, 并把引用的文件路径改写成输出后的路径
}
]
}
}
resolve
用于文件路径的解析。
alias
创建 import 或 require 的别名,来确保模块引入变得更简单
{
resolve:{
alias: {
Utilities: path.resolve(__dirname, 'src/utilities/'),
Templates: path.resolve(__dirname, 'src/templates/')
}
}
}
在这里为两个路径配置了别名,普通情况下我们可能这样导入模块:
import Utility from '../../utilities/utility';
现在可以这样使用别名:
import Utility from 'Utilities/utility';
也可以直接给路径下的文件配置别名:
{
resolve:{
alias: {
AppStore : 'js/stores/AppStores.js',
ActionType : 'js/actions/ActionType.js',
AppAction : 'js/actions/AppAction.js'
}
}
}
这样后续直接 require(‘AppStore’) 即可。
extensions
自动扩展文件后缀名,意味着我们require模块可以省略不写后缀名
{
resolve:{
extensions: ['', '.js', '.json', '.scss'],
}
}
这样我们加载js、json和scss文件时无需写后缀名了,只要 require(‘common’)就可以加载common.js文件了。
modules
告诉webpack解析模块时应该搜索的目录。
当引用模块时出现import 'vue'
这样不是相对路径、也不是绝对路径的写法时,会逐级向上地去各个node_modules 目录下找。但通常项目目录里只有一个 node_modules,且是在项目根目录,为了减少搜索范围,可以直接写明 node_modules 的全路径;
resolve:{
modules:["node_modules"]
}
plugin
plugin和loader的区别是,loader是在加载不同类型的模块时用于编译对应文件类型的工具,
而plugin是用于扩展webpack的功能,在webpack构建生命周期的节点上做相应的工作。
plugins: [
//plugins list
]
一些常用的插件如下:
CommonsChunkPlugin
很常用的插件。
当我们使用的第三方文件的体积很大,或者希望提升初始加载时间时,我们希望把这些依赖的公共部分代码与业务代码分离开来,使得用户在我们更新应用时不必再次下载第三方文件。
CommonsChunkPlugin 用于提取这些依赖到共享的 bundle 中,来避免重复打包。
可以像这样添加:
module.exports = {
entry:{
main:__dirname + '/app/main.js',
vendor:['moment']
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
names:['vendor','manifest']
});
]
}
我们看到向插件的构造函数传入了两个参数vendor和manifest,以及我们在entry也加入了新的入口vendor。
moment是常用的时间处理的第三方库,也就是我们这个工程所用的公共代码。在entry处引入vendor:’moment’,最后打包时将打包出main.x.js和vendor.x.js两个文件,main.x.js文件将保存我们的业务代码,vendor.x.js将保存moment的代码。
我们在插件的构造函数中传入了vendor值,表示entry中的vendor是公共部分,打包时会被单独提取,这样我们将公共代码和业务代码进行了初步分离。
在新添加的CommonmChunkPlugin插件中,我们添加了manifest值。如果不添加这个值,在打包时会发现main.x.js和vendor.x.js都会有更新,而添加manifest值可以保证公共部分的vendor不会每次被更新。
下面是另一个配置提取公共代码的例子:
module.exports = {
entry: {
index: [path.resolve(__dirname, 'src/index.js')],
vendors: ['react', 'react-dom', 'react-redux', 'react-router-dom'],
polyfills: ['babel-polyfill'],
libs: ['moment']
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
names: ['vendors', 'polyfills', 'libs', 'manifest']
})
]
}
此处的vendors,polyfills和libs都是公共部分的代码,使用CommonsChunkPlugin进行提取。
HotModuleReplacementPlugin
模块热替换功能会在应用程序运行过程中替换、添加或删除模块,而无需重新加载整个页面。
CleanWebpackPlugin
用于在打包的时候删除旧文件,不会出现同一份文件存在多份的情况。
var CleanWebpackPlugin = require('clean-webpack-plugin');
plugins:[
new CleanWebpackPlugin(
['public/main.*.js','public/manifest.*.js'],//要删除的文件目录匹配
{
root:__dirname,
verbose:true,
dry:false
}
)
]
UglifyJsPlugin
有时我们会需要压缩图片、css、js等资源。这个插件就是用于压缩js。
var UglifyJsPlugin = require('uglifyjs-webpack-plugin');
new UglifyJsPlugin({
beautify:true,
exclude:['/node_modules/'],
compress:{
warnings:false
},
output:{
comments:false
}
})
HtmlWebpackPlugin
可以生成入口html文件,其中的script标签引入了webpack打包完成的js。
var HtmlWebpackPlugin = require('html-webpack-plugin');
new HtmlWebpackPlugin({
title: '赛程 - CBA 数据库',
template: path.resolve(__dirname, 'src/index.html'),
filename: 'index.html',
chunks: ['index', 'vendors', 'polyfills', 'libs', 'manifest'],
inject: 'body'
})
chunk属性:webpack会按照这个属性定义的数组,将数组中所有片段完成打包,并用script标签将打包的js插入到生成的页面中,没有在数组中的片段,则不插入页面
template:模版名称,有时我们不想直接使用插件自动生成的html文件,我们可以指定一个模版,让插件根据模版来生成html
- inject:{true | ‘head’ | ‘body’ | false} ,注入所有的资源到特定的位置。
如果设置为 true 或者 body,所有的 javascript 资源将被放置到 body 元素的底部,’head’ 将放置到 head 元素中。
devServer
webpack-dev-server是一个小型的Node.js Express服务器,默认情况下它将在 localhost:8080 启动一个 express 静态资源 web 服务器。
# 安装
$ npm install webpack-dev-server -g
# 运行
$ webpack-dev-server --devtool eval --progress --colors --hot --content-base build
上面指令的意思是:
- webpack-dev-server - 在 localhost:8080 建立一个 Web 服务器
- –devtool eval - 为你的代码创建源地址。当有任何报错的时候可以让你更加精确地定位到文件和行号
- –progress - 显示合并代码进度
- –colors - 命令行中显示颜色
- –content-base build - 指向设置的输出目录
- –hot:开启热更新功能, 参数会帮我们往配置里添加HotModuleReplacementPlugin插件
- -d:开发环境模式,会在我们的配置文件中插入调试相关的选项, 比如打开debug, 打开sourceMap, 代码中插入源文件路径注释.
- -p:生产环境模式, 这个模式下webpack会将代码做压缩等优化
可以把命令行写在package.json的scripts中:
{
"scripts": {
"dev": "webpack-dev-server -d --hot --env.dev",
"build": "webpack -p"
}
}
npm run时会自动寻找./node_modules/.bin/目录下的命令,因此直接执行npm run dev
和npm run build
即可
externals
externals中配置的模块不参与打包,在运行时再从外部获取这些模块,但是依旧可以在代码中通过CommonJS、AMD、ES6或者window/global全局的方式访问。
一般用于配置由CDN引入的外部模块。
如引入jQuery:
//html中通过script引入
<script src="https://code.jquery.com/jquery-3.1.0.js"></script>
//webpack.config.js
externals:{
jquery:'jQuery'
}
这里配置的键值对,key 是 require 的包名,value 是全局的变量。
下面的代码还是可以正常运行:
import $ from 'jquery';
$('.my-element').animate(...);
构建流程
从启动webpack构建到输出结果经历了一系列过程:
- 解析webpack配置参数,合并从shell和webpack.config.js文件里配置的参数,生成最后的配置结果
- 注册所有配置的插件,好让插件监听webpack构建生命周期的事件节点,以在正确的节点进行工作。
- 从entry入口文件开始解析文件,找出每个文件依赖的文件,递归下去
- 在解析文件的过程中根据文件类型使用对应的loader来对文件进行转换
- 解析文件结束后,根据entry配置生成打包文件
- 输出打包文件
几个注意点:
在解析文件的过程中,如果一个文件被依赖了多次,也只会被打包一份。
webpack打包的原理为,在入口文件中,对每个require资源文件进行配置一个id, 也
就是说,对于同一个资源,就算是require多次的话,它的id也是一样的,所以无论在多少个文件中
require,它都只会打包一分多入口文件,从每个入口文件开始分别打包,互相之间并不影响。因此若有两个入口文件都引用了同一个文件,这个文件会被分别打包到对应的bundle中。
而针对多入口文件重复打包的问题,就可以使用CommonsChunkPlugin来优化,它原理就是把多个入口共同的依赖单独打包,这样就只打包一次。
优化
打包体积优化
- 通过externals排除从CDN引入的第三方包。
- 针对生产环境,引入UglifyJsPlugin 来压缩代码。
打包速度优化
减小搜索范围,提高查找速度:
- 配置 resolve.modules:可以直接从根目录的node_modules中寻找模块
- 在loaders的配置中设置include和exclude:
对于include,更精确指定要处理的目录,这可以减少不必要的遍历,从而减少性能损失;
同样,对于已经明确知道的不需要处理的目录,则应该予以排除,从而进一步提升性能
UglifyJS 优化
Webpack 默认提供的 UglifyJS 插件,由于采用单线程压缩,速度颇慢。
可以使用webpack-parallel-uglify-plugin来替代,该插件用于帮助具有多个入口点的项目加快其构建速度。 UglifyJS插件在每个输出文件上依次运行。 这个插件则可以并行运行UglifyJS,更加充分而合理的使用 CPU 资源,这可以大大减少的构建时间。babel-loader 优化
babel-loader非常慢,因此可以配置其cacheDirectory选项,充分利用缓存。
之后的 Webpack 构建将尝试从缓存中读取,以避免在每次运行时运行潜在昂贵的 Babel 重新编译过程。
rules: [
{
test: /\.js$/,
loader: 'babel-loader?cacheDirectory=true',
exclude: /node_modules/,
include: [resolve('src'), resolve('test')]
},
... ...
]