一、webpack安装
webpack官方文档:https://www.webpackjs.com/
npm官方文档:https://www.npmjs.cn/
webpack可以全局安装-g或者局部安装-D 命令:npm i webpack webpack-cli -D
webpack可以0配置打包,npx webpack,原理就是在node_modules下的bin目录中找到对应的命令执行,但是有局限,只能指定文件下的index.js文件才可以打包,在项目中我们不推荐0配置打包。
webpack不是用于单个文件打包的,主要功能是进行多文件打包。
二、webpack项目配置
配置有四大核心概念:
入口(entry)
输出(output)
loader
插件(plugins)
在根目录下创建webpack.config.js文件,webpack配置遵循CommonJS规范,使用module.exports进行暴露、属性entry路径可以设置相对路径,output属性设置的path路径必须是绝对定位,我们可以引用path插件进行设置,
output:{
path.resolve() //解析当前相对路径为绝对路径
path.join(__dirname) //拼接路径 __dirname找到当前文件的根目录进行拼接
filename:'' //打包之后输出在path文件夹下的文件
}如果不设置打包环境,默认打包环境是product(上线环境),可以使用mode进行生产环境和上线环境设置
mode:"development" //默认是product三、webpack自动编译功能
三种模式可以进行自动编译
1、webpack Watch Mode 监视文件是否有变化,使用 --watch 或者在webpack.confg.js文件中加上watch:true开启监视
2、webpack -dev-server
3.、webpack-dev-middleware
我们在项目中用到的最多的就是2方法,我们也就只学2方法
webpack -dev-server是一个插件,需要安装,在package.json中设置,webpack -dev-server打包的js文件是存在内存中的,不存在我们的硬盘中,我们看不到,在package.json中设置
'dev':"webpack-dev-server --open --hot --port 3000 --contentBase src --compress"
// open 编译自动打开默认浏览器
// hot 热加载,只编译修改的部分
// port 设置端口号
// contentBase 设置根目录
// compress 压缩,在服务端的强制压缩我们也可以在webpack.config.js文件中设置,
devServer:{
open:true,
hot:true, //开启热更新
compress:true,
port:3000,
contentBase:'/src'
}到了这里我们需要用到一个很牛逼的插件 html-webpack-plugin,安装 npm i html-webpack-plugin -D,在webpack.config.js中引用,html-webpack-plugin作用是自动在根目录帮我们根据模板生成一个index.html,它也是存在于内存中,不存在硬盘中,我们也是看不到的,另一个作用是自动帮我们引入打包生成的js文件,不需要我们在手动修改。
在webpack.config.js中设置
plugins:[
new HtmlWebpackPlugin({
filename:打包之后生成的html的名字,
template:根据某个模板生成上面打包的模板,此处写模板的路径位置
})
]打包时自动生成index.html文件
loader的使用,处理css,sass,scss es6高级语言,图片,为甚么使用loader,因为在main.js中引用css等文件,js无法解析,此时就需要一个加载器进行帮助js去解析
处理css的loader:安装npm i css-loader style-loader -D
处理less和sass的loader:安装npm i less less-loader sass-loader node-sass -D
处理图片和字体的loader:安装 npm i file-loader url-loader -D url-loader是对file-loafer的封装,可以对图片进行大小限制
在webpack.config.js中配置
module:{
rules:[
// loader的调用是从右向左管道的方式链式调用
// css-loader的作用:解析后缀名是css结尾的文件
// style-loader作用:将解析之后的文件放入html之中
{
test:/\.css$/,
use:['style-loader','css-loader']
},
{
test:/\.s(a|c)ss$/,
use:['style-loader','css-loader','sass-loader']
},
{
test:/\.(png|jpg|gif|woff|svg)$/,
use:{loader:'url-loader',options:{
limit:2*1024 //大于2kb时显示图片路径,小于2kb时转换为base64
}}
},
{
test:/\.(woff|svg|woff2|ttf|eot)$/,
use:'file-loader'
}
]
}图片打包之后会都存在dist文件中,而且名字还是hash值,特别长,此时我们可以进行设置
module:{
rules:[
{
test:/\.(png|jpg|gif|woff|svg)$/,
use:{
loader:'url-loader',
options:{
limit:2*1024, //大于2kb时显示图片路径,小于2kb时转换为base64
outputPath:'image',//打包之后生成的图片都放在image文件夹中
name:'[name]-[hash:7].[ext]' //[name]保留原图片的名称,[hash:7]随机生成的hash值保留前7位值,[ext]保留原图片的后缀值
}}
}
]
}处理js中es6高级语法babel-loader:安装 npm i babel-loader @babel/core @babel/preset-env webpack -D
npm i @babel/plugin-proposal-class-properties -D 为了处理兼容更高级的语法
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/env'],
plugins:['@babel/plugin-proposal-class-properties']
}
}
}
]
}babel中都是预要发布的新语法,想要在项目中用到什么新的语法,可以到babel中下载使用。然后在plugin中引用。
我们在打包的时候有时候需要排除一些不需要打包的js文件,我们只需要打包自己写的js代码,此时我们就需要用到exclude
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/env'],
plugins:['@babel/plugin-proposal-class-properties']
}
},
exclude:/node_modules/ //不需要打包的js文件
include:/index.js/ //包含需要打包的js文件
}
]
}处理js文件中generator语法转换,
generator的解释:
Generator函数时ES6提供的一种异步编程解决方案。Generator语法行为和普通函数完全不同,我们可以把Generator理解为一个包含了多个内部状态的状态机。
执行Generator函数回返回一个遍历器对象,也就是说Generator函数除了提供状态机,还可以生成遍历器对象。Generator可以此返回多个遍历器对象,通过这个对象可以访问到Generator函数内部的多个状态。
形式上Generator函数和普通的函数有两点不同,一是function关键字后面,函数名前面有一个星花符号“*”,二是,函数体内部使用yield定义(生产)不同的内部状态。
执行Generator函数返回的是一个遍历器对象,这个对象上有一个next方法,执行next方法会返回一个对象,这个对象上有两个属性,一个是value,是yield关键字后面的表达式的值,一个是done,布尔类型,true表示没有遇到return语句,可以继续往下执行,false表示遇到return语句。代码示例如下:
function* helloWorldGenerator () {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
console.log(hw.next()); //第一次调用,Generator函数开始执行,直到遇到yield表达式为止。next方法返回一个对象,它的value属性就是当前yield语句后面表达式的值hello,done属性为false,表示遍历还没有结束
console.log(hw.next()); //第二次调用,Generator函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield语句后面表达式的值world,done属性值为false,表示遍历还没有结束。
console.log(hw.next()); //第三次调用,Generator函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,则value属性为undefined),done属性为true,表示遍历已经执行结束。
console.log(hw.next()); //第四次调用,此时Generator函数已经执行完毕,next方法返回对戏那个的value属性为undefined,done属性为true,表示遍历结束。
console.log(hw.next()); //第五次执行和第四次执行的结果是一样的。执行结果:

1. 定义Generator函数helloWorldGenerator函数
2. 函数内部有2个yield表达式和一个return语句,return语句结束执行
3. Generator函数的调用方法和普通函数一样,也是在函数名后面加上一对圆括号。不同的是调用之后,函数不是立即执行,返回的也不是return语句的结果undefined,而是一个指向内部状态的指针对象,也就是上面说的遍历器对象(Iterator Object)
4. 调用遍历器对象的next方法,状态指针移动到下一个状态,返回{value: "hello", done: false}
5. 调用遍历器对象的next方法,状态指针移动到下一个状态,返回{value: "world", done: false}
6. 调用遍历器对象的next方法,状态指针移动到下一个状态,返回{value: "ending", done: true},done为true,说明已经遇到了return语句,后面已经没有状态可以返回了
7. 调用遍历器对象的next方法,指针不再移动,返回{value: undefined, done: true}
8. 调用遍历器对象的next方法,指针不再移动,返回{value: undefined, done: true}
注意yield表达式后面的表达式,只有当调用next方法,内部指针指向该语句时才会执行,相当于JavaScript提供了手动的“惰性求值”语法功能。
如果要在低版本的浏览器中使用generator语法,此时我们就要用babel去处理,安装 npm install -D @babel/plugin-transform-runtime 和 npm install -save @babel/runtime
module:{
rules:[
{
test:/\.js$/,
use:{
loader:'babel-loader',
options:{
presets:['@babel/env'],
plugins:[
'@babel/plugin-proposal-class-properties',
'@babel/plugin-transform-runtime'
]
}
}
}
]
}为了便于开发维护,我们一般把插件配置单独写在根目录下的 .babelrc文件下,格式是json,代码如下:
{
"presets":["@babel/env"],
"plugins":[
"@babel/plugin-proposal-class-properties",
"@babel/plugin-transform-runtime"
]
}高版本的原型方法的转义@babel/polyfill 使用polyfill进行新的api进行转换 babel是对JavaScript的新的语法转换
安装npm install @babel/polyfill,该模块需要在使用新方法的地方引用(在main.js中引用)(即打补丁)
import 'babel-polyfill'source map的使用,快速解决定位在浏览器中代码打印||报错位置,便于我们快速开发,
source map有很多模式,此时我们建议使用带cheap的eval的品质是原始代码的模式,推荐使用cheap-module-eval-source-map
在webpack.config.js文件中设置代码如下:
devtool:'cheap-module-eval-source-map'每次打包之前都需要把之前打包的dist文件删除插件:安装 npm install clean-webpack-plugin -D,使用插件,引入,直接在plugin中创建对象即可,代码如下:
plugins:[
new CleanWebpackPlugin({})
]静态资源都是不参与打包的,但是为了打包之后让静态资源进入到dist的文件夹列表中,我们需要用到插件 copy-webpack-plugin
此时我们一般把静态文件都放在assets文件夹中,插件安装:npm i copy-webpack-plugin -D,引入插件,设置插件
plugins:[
new CopyWebpackPlugin({
from:path.join(__dirname,'assets'),
to:''assets
})
]除了第三方的webpack插件,webpack还有自己的内置插件,
BannerPlugin 就是webpack的内置插件,他的作用就是给打包过的文件添加版权注释信息,因为是内置的,不需要安装,直接引用,首先要先引用webpack,const webpack =requirr('webpack')代码实现:
plugins:[
new webpack.BannerPlugin('三和大神')
]在html中引用的图片资源,build之后不会被打包,要使用我们上面学习到的loader(copy-webpack-plugin),但是前提这个图片是被放在assets文件夹下的,但是不是所有的img图片文件都是放在assets文件夹下的,有的图片资源是放在src文件下的,如果在html中直接引用,是不会被打包进dist中,此时我们就需要loader插件(html-withing-loader),安装loader:npm i -S html-withing-loader,loader安装之后,我们在html中引用图片使用正常的路径就可以,也可以进行打包
在webpack.config.js中配置,代码如下:
module:{
rules:[
{
test:/\.(htm|html)$/i,
loader:'html-withing-loader'
}
]
}多页应用打包
当有多个js文件入口的时候,我们就要有对应的多个html文件进行引用,此时我们就需要对webpack.config.js配置进行修改,
module.exports={
// 1、修改为多入口
entry:{
index:'index.js',
other:'other.js'
},
output:{
path:path.join(__dirname,'./dist'),
// 2、修改多入口对应的出口为变量
filename:'[name].js',
publicPath:'/'
},
plugin:[
// 3、如果用了html插件,需要手动设置多入口对应的html文件,将指定其对应的输出文件
new HtmlWebpackPlugin({
filename:'index.html',
template:'./index.html',
chunks:['index'] //找到对应的生成js,然后引用
}),
new HtmlWebpackPlugin({
filename:'other.html',
template:'./other.html',
chunks:['other'] //找到对应的生成js,然后引用到生成的html文件中
})
]
}第三方库的两种引用方式
expose-loader 作用将库引入到全局作用域
安装 npm i -D expose-loader
配置loader
module:{
rules:[
//require.resolve 用来获取模块的绝对路径,所以这里的loader只会作用到jQuery,并且只会在打包中使用到他时,才会进行处理
{
test:require.resolve('jquery'),
use:{
loader:'expose-loader',
options:'$'
}
}
]
}webpack-ProvidePlugin 是webpack内置插件,作用将库自动加载到每个模块
直接在webpack.config.js中配置,代码如下:
plugins:[
new webpack.ProvidePlugin({
$:'jquery',
jQuery:'jquery'
})
]Development / production 不同配置文件打包
项目开发一般需要开发环境和生产环境,用于开发环境的阶段(不压缩代码,不优化代码,增加效率),生产环境的阶段(压缩代码,优化代码,打包后直接上线使用),此时我们就需要抽取三个配置文件:
webpack.base.js webpack.prod.js webpack.dev.js
实现步骤如下:
1、将开发环境和生产环境公用的配置放在base中,不同的配置各自放入prod或dev中,(有针对的进行删除)
2、然后在dev和prof中使用webpack-merge把配置与base的配置进行合并后导出
2.1 安装webpack-merge -D,在dev和prod中引用webpack-merge,
2.2 在dev和prod中引入base文件
2.3 在dev和prod中进行配置
// webpack遵循的是nodeJs的commonJS规范
const merge = require('/webpack-merge')
const webpackBase =require('/webpack.base.js')
const webpackProd=merge(webpackBase ,{
// webpack.prod.js或者webPack.dev.js保留的配置文件
})
module.exports=webpackProd3、将package.json中的脚本参数进行修改,通过--config手动指定特定的配置文件
"scripts":{
"build":"webpack --config /webpack.prod.js",
"server":"webpack --config /webpack.dev.js
}webpack配置文件分类
我们将webpack.base.js webpack.prod.js webpack.dev.js 都放在根目录显然不是很友好,此时我们在根目录下新建一个文件夹build。将上述三个文件都移动到build中,但是移动文件之后package.json中的启动路径需要进行相应的修改,webpack.base,js中使用path拼接的绝对定位也需要进行相应的修改,否则会进行报错,打包之后dist文件会存放到build文件中等等问题,这些问题大部分都是路径不对引起的。
我们进行打包之后,想运行dist文件查看打包之后的效果,此时我们可以先安装一个插件live-server -D(web提供的一个小服务器,用于测试),然后在package.json中加入一个测试的配置项,代码如下:
"scripts":{
"start":"live-server ./dist" // 运行托管在dist文件夹下的文件
}设置环境变量进行线上和开发的url地址动态切换。插件(DefinePlugin)
在开发中我们需要请求测试环境的url,在生产环境中我们需要请求生产环境的url,此时,我们就会用到webpack的一个内置插件来为我们设置环境变量,以此来设置不同的环境变量进行请求不同的url地址,这个内置插件要在webpack.dev.js 和webpack,prod,js中配置
const webpack=require('webpack')
plugins:[
new webpack.DefinePlugin({
ENV:'true'
})
]使用devserver解决跨域问题
跨域是浏览器的同源策略导致的,只有浏览器请求才会发生同源策略,服务器和服务器之间请求不存在跨域现象
http proxy解决跨域,就是浏览器请求devserver服务器,然后devserver服务器转发请求去请求服务器解决跨域
在webpack.dev.js的devServer中加入属性proxy,代理代码如下:
devServer:{
proxy:{
'/api':{
target:'http://10.35.23.11', //请求的地址指向这个url
secure:false, //如果是https接口,需要配置为true
changeOrigin:true, //如果接口跨域,需要进行此参数配置
pathRewrite:{ //地址重写,冒号后面的可以替代冒号后面的
'^/api':''
}
}
}
}cros解决跨域,是由后端解决设置
HMR的使用技巧
需要对某个模块进行热更新的时候,我们为了增加开发体验,我们可以通过module.hot.accept方法进行文件监视,只要模块内容发生变化,就会触发回调函数,从而可以重新读取模块内容,做对应的操作,代码如下:
if(module.hot){
module.hot.accept('./hot.js(监听的文件)',function(){
console.log('改变')
let str = require('./hot.js')
console.log(str)
})
}webpack的优化
production模式打包自带优化,最主要的是三种优化方式,
tree shaking :通常用于包的移除,在js中未被引用的代码,它依赖于ES6模块系统中的import 和 export 的静态结构特性,开发时引用一个模块后,如果只使用其中一个功能,上线打包时只会把用到的功能打包进build中,其他没用到的功能都不会引用进来,可以实现最基础的优化,如果使用commonJS规范中的require语法进行引用,则不会触发tree shaking功能
scope hoisting :作用是将模块之间的关系进行结果推测,可以让webpack打包出来的代码文件更小,运行的更快,原理就是分析模块之间的关系,尽可能的将打算的模块合并到一个函数中,但前提是不能造成代码冗余,只有那些被引用了一次的模块才会被合并,源码必须使用ES6语句,否则不会被生效。
css优化
不让css存在html的style中,将css样式提取到单独的文件中,我们需要用到插件 mini-css-extract-plugin,对每个包含css的js文件都会创建一个CSS文件,支持按需加载css和sourceMap,
只能用在webpack4中,有如下优势:
异步加载
不重复编译,性能很好
容易使用
只针对CSS
使用方法:
安装
npm i -D mini-css-extract-plugin在webpack配置文件中引入插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')创建插件对象,配置抽离的css文件名,支持placeholder语法
new MiniCssExtractPlugin({ filename: '[name].css' })将原来配置的所有
style-loader替换为MiniCssExtractPlugin.loader{ test: /\.css$/, // webpack读取loader时 是从右到左的读取, 会将css文件先交给最右侧的loader来处理 // loader的执行顺序是从右到左以管道的方式链式调用 // css-loader: 解析css文件 // style-loader: 将解析出来的结果 放到html中, 使其生效 // use: ['style-loader', 'css-loader'] use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] }, // { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] }, { test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader'] }, // { test: /\.s(a|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.s(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'] },
自动添加css前缀
使用postcss,需要用到postcss-loader和autoprefixer插件
安装
npm i -D postcss-loader autoprefixer修改webpack配置文件中的loader,将
postcss-loader放置在css-loader的右边(调用链从右到左){ test: /\.css$/, // webpack读取loader时 是从右到左的读取, 会将css文件先交给最右侧的loader来处理 // loader的执行顺序是从右到左以管道的方式链式调用 // css-loader: 解析css文件 // style-loader: 将解析出来的结果 放到html中, 使其生效 // use: ['style-loader', 'css-loader'] use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader'] }, // { test: /\.less$/, use: ['style-loader', 'css-loader', 'less-loader'] }, { test: /\.less$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'less-loader'] }, // { test: /\.s(a|c)ss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }, { test: /\.s(a|c)ss$/, use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader', 'sass-loader'] },项目根目录下添加
postcss的配置文件:postcss.config.js在
postcss的配置文件中使用插件module.exports = { plugins: [require('autoprefixer')] }
开启css压缩
压缩css时,必须也要设置js压缩,否则它会把默认的js压缩设置覆盖。
需要使用optimize-css-assets-webpack-plugin插件来完成css压缩
但是由于配置css压缩时会覆盖掉webpack默认的优化配置,导致JS代码无法压缩,所以还需要手动把JS代码压缩插件导入进来:terser-webpack-plugin
安装
npm i -D optimize-css-assets-webpack-plugin terser-webpack-plugin导入插件
const TerserJSPlugin = require('terser-webpack-plugin') const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')在webpack配置文件中添加配置节点
optimization: { minimizer: [new TerserJSPlugin({}), new OptimizeCSSAssetsPlugin({})], },
tips: webpack4默认采用的JS压缩插件为:uglifyjs-webpack-plugin,在mini-css-extract-plugin上一个版本中还推荐使用该插件,但最新的v0.6中建议使用teser-webpack-plugin来完成js代码压缩,具体原因未在官网说明,我们就按照最新版的官方文档来做即可
js代码分离
Code Splitting是webpack打包时用到的重要的优化特性之一,此特性能够把代码分离到不同的 bundle 中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
有三种常用的代码分离方法:
入口起点(entry points):使用
entry配置手动地分离代码。防止重复(prevent duplication):使用
SplitChunksPlugin去重和分离 chunk。动态导入(dynamic imports):通过模块的内联函数调用来分离代码。
手动配置多入口
在webpack配置文件中配置多个入口
entry: { main: './src/main.js', other: './src/other.js' }, output: { // path.resolve() : 解析当前相对路径的绝对路径 // path: path.resolve('./dist/'), // path: path.resolve(__dirname, './dist/'), path: path.join(__dirname, '..', './dist/'), // filename: 'bundle.js', filename: '[name].bundle.js', publicPath: '/' },在main.js和other.js中都引入同一个模块,并使用其功能
main.js
import $ from 'jquery' $(function() { $('<div></div>').html('main').appendTo('body') })other.js
import $ from 'jquery' $(function() { $('<div></div>').html('other').appendTo('body') })修改package.json的脚本,添加一个使用dev配置文件进行打包的脚本(目的是不压缩代码检查打包的bundle时更方便)
"scripts": { "build": "webpack --config ./build/webpack.prod.js", "dev-build": "webpack --config ./build/webpack.dev.js" }运行
npm run dev-build,进行打包查看打包后的结果,发现other.bundle.js和main.bundle.js都同时打包了jQuery源文件
这种方法存在一些问题:
如果入口 chunks 之间包含重复的模块,那些重复模块都会被引入到各个 bundle 中。
这种方法不够灵活,并且不能将核心应用程序逻辑进行动态拆分代码。
抽取公共代码
tips: Webpack v4以上使用的插件为SplitChunksPlugin,以前使用的CommonsChunkPlugin已经被移除了,最新版的webpack只需要在配置文件中的optimization节点下添加一个splitChunks属性即可进行相关配置
修改webpack配置文件
optimization: { splitChunks: { chunks: 'all' } },运行
npm run dev-build重新打包查看
dist目录查看
vendors~main~other.bundle.js,其实就是把都用到的jQuery打包到了一个单独的js中
动态导入 (懒加载)
webpack4默认是允许import语法动态导入的,但是需要babel的插件支持,最新版babel的插件包为:@babel/plugin-syntax-dynamic-import,以前老版本不是@babel开头,已经无法使用,需要注意
动态导入最大的好处是实现了懒加载,用到哪个模块才会加载哪个模块,可以提高SPA应用程序的首屏加载速度,Vue、React、Angular框架的路由懒加载原理一样
安装babel插件
npm install -D @babel/plugin-syntax-dynamic-import修改.babelrc配置文件,添加
@babel/plugin-syntax-dynamic-import插件{ "presets": ["@babel/env"], "plugins": [ "@babel/plugin-proposal-class-properties", "@babel/plugin-transform-runtime", "@babel/plugin-syntax-dynamic-import" ] }将jQuery模块进行动态导入
function getComponent() { return import('jquery').then(({ default: $ }) => { return $('<div></div>').html('main') }) }给某个按钮添加点击事件,点击后调用getComponent函数创建元素并添加到页面
window.onload = function () { document.getElementById('btn').onclick = function () { getComponent().then(item => { item.appendTo('body') }) } }
SplitChunksPlugin配置参数
webpack4之后,使用SplitChunksPlugin插件替代了以前CommonsChunkPlugin
而SplitChunksPlugin的配置,只需要在webpack配置文件中的optimization节点下的splitChunks进行修改即可,如果没有任何修改,则会使用默认配置
默认的SplitChunksPlugin 配置适用于绝大多数用户
webpack 会基于如下默认原则自动分割代码:
公用代码块或来自 node_modules 文件夹的组件模块。
打包的代码块大小超过 30k(最小化压缩之前)。
按需加载代码块时,同时发送的请求最大数量不应该超过 5。
页面初始化时,同时发送的请求最大数量不应该超过 3。
以下是SplitChunksPlugin的默认配置:
module.exports = {
//...
optimization: {
splitChunks: {
chunks: 'async', // 只对异步加载的模块进行拆分,可选值还有all | initial
minSize: 30000, // 模块最少大于30KB才拆分
maxSize: 0, // 模块大小无上限,只要大于30KB都拆分
minChunks: 1, // 模块最少引用一次才会被拆分
maxAsyncRequests: 5, // 异步加载时同时发送的请求数量最大不能超过5,超过5的部分不拆分
maxInitialRequests: 3, // 页面初始化时同时发送的请求数量最大不能超过3,超过3的部分不拆分
automaticNameDelimiter: '~', // 默认的连接符
name: true, // 拆分的chunk名,设为true表示根据模块名和CacheGroup的key来自动生成,使用上面连接符连接
cacheGroups: { // 缓存组配置,上面配置读取完成后进行拆分,如果需要把多个模块拆分到一个文件,就需要缓存,所以命名为缓存组
vendors: { // 自定义缓存组名
test: /[\\/]node_modules[\\/]/, // 检查node_modules目录,只要模块在该目录下就使用上面配置拆分到这个组
priority: -10 // 权重-10,决定了哪个组优先匹配,例如node_modules下有个模块要拆分,同时满足vendors和default组,此时就会分到vendors组,因为-10 > -20
},
default: { // 默认缓存组名
minChunks: 2, // 最少引用两次才会被拆分
priority: -20, // 权重-20
reuseExistingChunk: true // 如果主入口中引入了两个模块,其中一个正好也引用了后一个,就会直接复用,无需引用两次
}
}
}
}
};noParse
在引入一些第三方模块时,例如jQuery、bootstrap等,我们知道其内部肯定不会依赖其他模块,因为最终我们用到的只是一个单独的js文件或css文件
所以此时如果webpack再去解析他们的内部依赖关系,其实是非常浪费时间的,我们需要阻止webpack浪费精力去解析这些明知道没有依赖的库
可以在webpack配置文件的module节点下加上noParse,并配置正则来确定不需要解析依赖关系的模块
module: {
noParse: /jquery|bootstrap/
}IgnorePlugin
在引入一些第三方模块时,例如moment,内部会做i18n国际化处理,所以会包含很多语言包,而语言包打包时会比较占用空间,如果我们项目只需要用到中文,或者少数语言,可以忽略掉所有的语言包,然后按需引入语言包
从而使得构建效率更高,打包生成的文件更小
需要忽略第三方模块内部依赖的其他模块,只需要三步:
首先要找到moment依赖的语言包是什么
使用IgnorePlugin插件忽略其依赖
需要使用某些依赖时自行手动引入
具体实现如下:
通过查看moment的源码来分析:
function loadLocale(name) { var oldLocale = null; // TODO: Find a better way to register and load all the locales in Node if (!locales[name] && (typeof module !== 'undefined') && module && module.exports) { try { oldLocale = globalLocale._abbr; var aliasedRequire = require; aliasedRequire('./locale/' + name); getSetGlobalLocale(oldLocale); } catch (e) {} } return locales[name]; }观察上方代码,同时查看moment目录下确实有locale目录,其中放着所有国家的语言包,可以分析得出:locale目录就是moment所依赖的语言包目录
使用IgnorePlugin插件来忽略掉moment模块的locale目录
在webpack配置文件中安装插件,并传入配置项
参数1:表示要忽略的资源路径
参数2:要忽略的资源上下文(所在哪个目录)
两个参数都是正则对象
new webpack.IgnorePlugin(/\.\/locale/, /moment/)使用moment时需要手动引入语言包,否则默认使用英文
import moment from 'moment' import 'moment/locale/zh-cn' moment.locale('zh-CN') console.log(moment().subtract(6, 'days').calendar())
DllPlugin
在引入一些第三方模块时,例如vue、react、angular等框架,这些框架的文件一般都是不会修改的,而每次打包都需要去解析它们,也会影响打包速度,哪怕做拆分,也只是提高了上线后用户访问速度,并不会提高构建速度,所以如果需要提高构建速度,应该使用动态链接库的方式,类似于Windows中的dll文件。
借助DllPlugin插件实现将这些框架作为一个个的动态链接库,只构建一次,以后每次构建都只生成自己的业务代码,可以大大提高构建效率!
主要思想在于,将一些不做修改的依赖文件,提前打包,这样我们开发代码发布的时候就不需要再对这部分代码进行打包,从而节省了打包时间。
涉及两个插件:
DllPlugin
使用一个单独webpack配置创建一个dll文件。并且它还创建一个manifest.json。DllReferencePlugin使用该json文件来做映射依赖性。(这个文件会告诉我们的哪些文件已经提取打包好了)
配置参数:
context (可选): manifest文件中请求的上下文,默认为该webpack文件上下文。
name: 公开的dll函数的名称,和output.library保持一致即可。
path: manifest.json生成的文件夹及名字
DllReferencePlugin
这个插件用于主webpack配置,它引用的dll需要预先构建的依赖关系。
context: manifest文件中请求的上下文。
manifest: DllPlugin插件生成的manifest.json
content(可选): 请求的映射模块id(默认为manifest.content)
name(可选): dll暴露的名称
scope(可选): 前缀用于访问dll的内容
sourceType(可选): dll是如何暴露(libraryTarget)
将Vue项目中的库抽取成Dll
准备一份将Vue打包成DLL的webpack配置文件
在build目录下新建一个文件:webpack.vue.js
配置入口:将多个要做成dll的库全放进来
配置出口:一定要设置library属性,将打包好的结果暴露在全局
配置plugin:设置打包后dll文件名和manifest文件所在地
const path = require('path') const webpack = require('webpack') module.exports = { mode: 'development', entry: { vue: [ 'vue/dist/vue.js', 'vue-router' ] }, output: { filename: '[name]_dll.js', path: path.resolve(__dirname, '../dist'), library: '[name]_dll' }, plugins: [ new webpack.DllPlugin({ name: '[name]_dll', path: path.resolve(__dirname, '../dist/manifest.json') }) ] }在webpack.base.js中进行插件的配置
使用DLLReferencePlugin指定manifest文件的位置即可
new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, '../dist/manifest.json') })安装add-asset-html-webpack-plugin
npm i add-asset-html-webpack-plugin -D配置插件自动添加script标签到HTML中
new AddAssetHtmlWebpackPlugin({ filepath: path.resolve(__dirname, '../dist/vue_dll.js') })
将React项目中的库抽取成Dll
准备一份将React打包成DLL的webpack配置文件
在build目录下新建一个文件:webpack.vue.js
配置入口:将多个要做成dll的库全放进来
配置出口:一定要设置library属性,将打包好的结果暴露在全局
配置plugin:设置打包后dll文件名和manifest文件所在地
const path = require('path') const webpack = require('webpack') module.exports = { mode: 'development', entry: { react: [ 'react', 'react-dom' ] }, output: { filename: '[name]_dll.js', path: path.resolve(__dirname, '../dist'), library: '[name]_dll' }, plugins: [ new webpack.DllPlugin({ name: '[name]_dll', path: path.resolve(__dirname, '../dist/manifest.json') }) ] }在webpack.base.js中进行插件的配置
使用DLLReferencePlugin指定manifest文件的位置即可
new webpack.DllReferencePlugin({ manifest: path.resolve(__dirname, '../dist/manifest.json') })安装add-asset-html-webpack-plugin
npm i add-asset-html-webpack-plugin -D配置插件自动添加script标签到HTML中
new AddAssetHtmlWebpackPlugin({ filepath: path.resolve(__dirname, '../dist/react_dll.js') })
Happypack

由于webpack在node环境中运行打包构建,所以是单线程的模式,在打包众多资源时效率会比较低下,早期可以通过Happypack来实现多进程打包。当然,这个问题只出现在低版本的webpack中,现在的webpack性能已经非常强劲了,所以无需使用Happypack也可以实现高性能打包
引用官网原文:
Maintenance mode notice
My interest in the project is fading away mainly because I'm not using JavaScript as much as I was in the past. Additionally, Webpack's native performance is improving and (I hope) it will soon make this plugin unnecessary.
See the FAQ entry about Webpack 4 and thread-loader.
Contributions are always welcome. Changes I make from this point will be restricted to bug-fixing. If someone wants to take over, feel free to get in touch.
Thanks to everyone who used the library, contributed to it and helped in refining it!!!
由此可以看出作者已经发现,webpack的性能已经强大到不需要使用该插件了,而且小项目使用该插件反而会导致性能损耗过大,因为开启进程是需要耗时的
使用方法:
安装插件
npm i -D happypack在webpack配置文件中引入插件
const HappyPack = require('happypack')修改loader的配置规则
{ test: /.js$/, use: { loader: 'happypack/loader' }, include: path.resolve(__dirname, '../src'), exclude: /node_modules/ }配置插件
new HappyPack({ loaders: [ 'babel-loader' ] })运行打包命令
npm run build
浏览器缓存
在做了众多代码分离的优化后,其目的是为了利用浏览器缓存,达到提高访问速度的效果,所以构建项目时做代码分割是必须的,例如将固定的第三方模块抽离,下次修改了业务代码,重新发布上线不重启服务器,用户再次访问服务器就不需要再次加载第三方模块了
但此时会遇到一个新的问题,如果再次打包上线不重启服务器,客户端会把以前的业务代码和第三方模块同时缓存,再次访问时依旧会访问缓存中的业务代码,所以会导致业务代码也无法更新
需要在output节点的filename中使用placeholder语法,根据代码内容生成文件名的hash:
output: {
// path.resolve() : 解析当前相对路径的绝对路径
// path: path.resolve('./dist/'),
// path: path.resolve(__dirname, './dist/'),
path: path.join(__dirname, '..', './dist/'),
// filename: 'bundle.js',
filename: '[name].[contenthash:8].bundle.js',
publicPath: '/'
},之后每次打包业务代码时,如果有改变,会生成新的hash作为文件名,浏览器就不会使用缓存了,而第三方模块不会重新打包生成新的名字,则会继续使用缓存
打包分析
项目构建完成后,需要通过一些工具对打包后的bundle进行分析,通过分析才能总结出一些经验,官方推荐的分析方法有两步完成:
使用
--profile --json参数,以json格式来输出打包后的结果到某个指定文件中webpack --profile --json > stats.json将stats.json文件放入工具中进行分析
官方推荐的其他四个工具:
其中webpack-bundle-analyzer是一个插件,可以以插件的方式安装到项目中
Prefetching和Preloading
在优化访问性能时,除了充分利用浏览器缓存之外,还需要涉及一个性能指标:coverage rate(覆盖率)
可以在Chrome浏览器的控制台中按:ctrl + shift + p,查找coverage,打开覆盖率面板
开始录制后刷新网页,即可看到每个js文件的覆盖率,以及总的覆盖率

想提高覆盖率,需要尽可能多的使用动态导入,也就是懒加载功能,将一切能使用懒加载的地方都是用懒加载,这样可以大大提高覆盖率
但有时候使用懒加载会影响用户体验,所以可以在懒加载时使用魔法注释:Prefetching,是指在首页资源加载完毕后,空闲时间时,将动态导入的资源加载进来,这样即可以提高首屏加载速度,也可以解决懒加载可能会影响用户体验的问题,一举两得!
function getComponent() {
return import(/* webpackPrefetch: true */ 'jquery').then(({ default: $ }) => {
return $('<div></div>').html('我是main')
})
}
第5章 webpack原理
学习目标
了解webpack打包原理
了解webpack的loader原理
了解webpack的插件原理
了解ast抽象语法树的应用
了解tapable的原理
手写一个简单的webpack
项目准备工作
新建一个项目,起一个炫酷的名字
新建
bin目录,将打包工具主程序放入其中主程序的顶部应当有:
#!/usr/bin/env node标识,指定程序执行环境为node在
package.json中配置bin脚本{ "bin": "./bin/itheima-pack.js" }通过
npm link链接到全局包中,供本地测试使用
分析webpack打包的bundle文件
其内部就是自己实现了一个__webpack_require__函数,递归导入依赖关系
(function (modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
({
"./src/index.js":
(function (module, exports, __webpack_require__) {
eval("let news = __webpack_require__(/*! ./news.js */ \"./src/news.js\")\r\nconsole.log(news.content)\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/message.js":
(function (module, exports) {
eval("module.exports = {\r\n content: '今天要下雨了!!!'\r\n}\n\n//# sourceURL=webpack:///./src/message.js?");
}),
"./src/news.js":
(function (module, exports, __webpack_require__) {
eval("let message = __webpack_require__(/*! ./message.js */ \"./src/message.js\")\r\n\r\nmodule.exports = {\r\n content: '今天有个大新闻,爆炸消息!!!内容是:' + message.content\r\n}\n\n//# sourceURL=webpack:///./src/news.js?");
})
});自定义loader
在学习给自己写的itheima-pack工具添加loader功能之前,得先学习webpack中如何自定义loader,所以学习步骤分为两大步:
掌握自定义webpack的loader
学习给itheima-pack添加loader功能并写一个loader
webpack以及我们自己写的itheima-pack都只能处理JavaScript文件,如果需要处理其他文件,或者对JavaScript代码做一些操作,则需要用到loader。
loader是webpack中四大核心概念之一,主要功能是将一段匹配规则的代码进行加工处理,生成最终的代码后输出,是webpack打包环节中非常重要的一环。
loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
之前都使用过别人写好的loader,步骤大致分为:
装包
在webpack.config.js中配置module节点下的rules即可,例如babel-loader(省略其他配置,只论loader)
(可选步骤)可能还需要其他的配置,例如babel需要配置presets和plugin
const path = require('path')
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [
{ test: /\.js$/, use: 'babel-loader' }
]
},
mode: 'development'
}实现一个简单的loader
loader到底是什么东西?能不能自己写?
答案是肯定的,loader就是一个函数,同样也可以自己来写
在项目根目录中新建一个目录存放自己写的loader:

编写myloader.js,其实loader就是对外暴露一个函数
第一个参数就是loader要处理的代码
module.exports = function(source) { console.log(source) // 只是简单打印并返回结果,不作任何处理 return source }同样在webpack.config.js中配置自己写的loader,为了方便演示,直接匹配所有的js文件使用自己的myloader进行处理
const path = require('path') module.exports = { entry: './src/index.js', output: { path: path.join(__dirname, 'dist'), filename: 'bundle.js' }, module: { rules: [ { test: /.js$/, use: './loaders/myloader.js' } ] }, mode: 'development' }如果需要实现一个简单的loader,例如将js中所有的“今天”替换成“明天”
只需要修改myloader.js的内容如下即可
module.exports = function(source) { return source.replace(/今天/g, '明天') }同时也可以配置多个loader对代码进行处理
const path = require('path') module.exports = { entry: './src/index.js', output: { path: path.join(__dirname, 'dist'), filename: 'bundle.js' }, module: { rules: [ { test: /.js$/, use: ['./loaders/myloader2.js', './loaders/myloader.js'] } ] }, mode: 'development' }myloader2.js
module.exports = function(source) { return source.replace(/爆炸/g, '小道') }
loader的分类
不同类型的loader加载时优先级不同,优先级顺序遵循:
前置 > 行内 > 普通 > 后置
pre: 前置loader
post: 后置loader
指定Rule.enforce的属性即可设置loader的种类,不设置默认为普通loader
在itheima-pack中添加loader的功能
通过配置loader和手写loader可以发现,其实webpack能支持loader,主要步骤如下:
读取webpack.config.js配置文件的module.rules配置项,进行倒序迭代(rules的每项匹配规则按倒序匹配)
根据正则匹配到对应的文件类型,同时再批量导入loader函数
倒序迭代调用所有loader函数(loader的加载顺序从右到左,也是倒叙)
最后返回处理后的代码
在实现itheima-pack的loader功能时,同样也可以在加载每个模块时,根据rules的正则来匹配是否满足条件,如果满足条件则加载对应的loader函数并迭代调用
depAnalyse()方法中获取到源码后,读取loader:
let rules = this.config.module.rules
for (let i = rules.length - 1; i >= 0; i--) {
// console.log(rules[i])
let {test, use} = rules[i]
if (test.test(modulePath)) {
for (let j = use.length - 1; j >= 0; j--) {
let loaderPath = path.join(this.root, use[j])
let loader = require(loaderPath)
source = loader(source)
}
}
}