webpack是一种前端资源构建工具,一个静态资源打包器,在webpack看来,前端所有的资源文件(js/json/css/img/less/html/…)都会作为模块处理。它将根据模块的依赖关系进行静态分析,打包生成对应的静态资源。
Entry–入口(Entry)指示webpack以那个文件为入口起点开始打包,分析构建内部依赖图
Output–输出(Output)指示webpack打包后的资源bundles输出到那里去,以及如何命名。
Loader–Loader让webpack能够去处理那些非javascript文件(webpack自身只理解javascipt)
Plugins–插件(Plugins)可以用于执行范围更广的任务。
Mode–模式(Mode)指示webpack使用相应模式的配置.包括开发模式和生产模式
初始化:npm init --yes
下载安装webpack:npm i webpack -cli -g npm i webpack cli -d
创建webpack配置文件:webpack.config.js
基本配置:
entry:'./src/main.js'--入口文件
output:{
path:path.resolve(__dirname,'dist')
filename:'bundle.js'
}--出口配置
mode:'development'--开发环境配置
module:{
rules:[
{
test:'/\.css$/',
use:['style-loader','css-loader']
}
]
}--loader的配置
plugins:[new VueLoaderPlugin()]--插件的配置
//要想打包得在文件中有引用关系
npm i css-loader style-loader less-loader less --save-d//安装打包资源相应的loader
修改配置文件:
像上面一样:在rules中添加对应的对象,对象中包含正则表达式匹配资源,以及相应的loader,对于样式资源来说,要先识别css对应css-loader,在把css转化为浏览器是别的style 对应style-loader
//这个url-loader只能解析样式之中的图片,需要借助html-loader打包html中的图片资源
// limit的作用:图片大小小于8kb,就会被base64处理
// 优点: 减少请求数量(减轻服务器压力)
// 缺点:图片体积会更大(文件请求速度更慢)
在webpack.config.js中:
{
test:/\.(jpg|png|gif|jpeg)$/,
use:[{
loader:'url-loader',
options:{
limit:8192,
name: '[hash:10].[ext]',
esModule:fasle//关闭es6模式
}
}]
}
{
test:/\.html$/,
use:['html-loader']
}
在配置中引入插件htmlWebpackPlugins:
plugins:[new htmlWebpackPlugins({
template:'./src/index.html'
})]
在配置中:
{
exclude:/\.(css|less|js|html|jpg|jpeg|gif|png)$/,
use:['file-loader']
}
这个插件就是可以让我们每次打包之后不用每次删除里面的内容在打包,他可以自动在第二次打包的时候先清除在打包。
const {
CleanWebpackPlugin
} = require('clean-webpack-plugin')
在插件里面添加:new CleanWebpackPlugin()
这个插件的用处就是可以把目标文件的内容复制粘贴到打包文件夹当中,可以使我们的源码找到所引用的资源。
const copyPlugin = require('copy-webpack-plugin')
new copyPlugin({
patterns: [
{
from: './public/*.ico',
to: path.join(__dirname, './dist/favicon.ico'),
},
{
from: './public/libs',
to: path.join(__dirname, './dist/libs'),
}
]
})
如果想要跨域的话有很多方法,其中一种是通过代理服务器的方式,既可以在服务器中设置代理服务器也可以在客户端设置代理服务器,这种方式需要借助第三方插件来完成,我们可以在webpack中设置,具体设置方法为:
//设置跨域代理服务器
//这种方式需要在devServer中配置
proxy: {
"/api": {
target: 'http://localhost:3000'
},
}
webpack开发环境优化主要有两个方面,一方面是打包构建速度的优化,另一方面是代码调试的优化。
HMR–Hot Module Replacement热模块替换
什么意思?—当我们本地启动webpack-dev-server的时候,我们要对源码进行修改的时候,假如有很多文件,那么这时候,我们修改完一个文件之后,webpack-dev-server会对页面进行刷新,它会对所有的文件再一次进行打包构建,那么这时候,所有的文件都会被再一次打包构建,那么这时候,我们只想让这一个修改的文件打包优化,从而提高打包构建的速度,那么这时候就用到了HMR.
使用方法,在webpack的devServer配置中加入hot:true,即可生效
注意:js,HTML默认是不能使用HMR的,但是我们的html文件只有一个,这就导致了一个问题,html文件不能热更新了,即对html做出改变页面无变化,解决办法:只需要在entry中加入html的位置就可以了
entry: ['./src/js/index.js', './src/index.html']
那么怎么样启动js文件的热模块替换?
在打包js文件入口的js文件中,对需要启动HMR的文件做出配置即可。注意:HMR功能对js的处理,只能处理非入口js文件的其他文件。
if (module.hot) {
// 一旦 module.hot 为true,说明开启了HMR功能。 --> 让HMR功能代码生效
module.hot.accept('./js/b.js', function() {
// 方法会监听 print.js 文件的变化,一旦发生变化,其他模块不会重新打包构建。
// 会执行后面的回调函数
add();
});
}
//css文件的HMR是通过style-loader实现的
优化开发环境的代码调试是什么意思?
就是能够通过打包构建后的代码映射到源代码中的相应错误位置,并报出错误信息。
实现方式:
source-map: 一种 提供源代码到构建后代码映射 技术 (如果构建后代码出错了,通过映射可以追踪源代码错误)
[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
source-map:外部
错误代码准确信息 和 源代码的错误位置
inline-source-map:内联
只生成一个内联source-map
错误代码准确信息 和 源代码的错误位置
hidden-source-map:外部
错误代码错误原因,但是没有错误位置
不能追踪源代码错误,只能提示到构建后代码的错误位置
eval-source-map:内联
每一个文件都生成对应的source-map,都在eval
错误代码准确信息 和 源代码的错误位置
nosources-source-map:外部
错误代码准确信息, 但是没有任何源代码信息
cheap-source-map:外部
错误代码准确信息 和 源代码的错误位置
只能精确的行
cheap-module-source-map:外部
错误代码准确信息 和 源代码的错误位置
module会将loader的source map加入
内联 和 外部的区别:1. 外部生成了文件,内联没有 2. 内联构建速度更快
开发环境:速度快,调试更友好
速度快(eval>inline>cheap>...)
eval-cheap-souce-map
eval-source-map
调试更友好
souce-map
cheap-module-souce-map
cheap-souce-map
--> eval-source-map / eval-cheap-module-souce-map
生产环境:源代码要不要隐藏? 调试要不要更友好
内联会让代码体积变大,所以在生产环境不用内联
nosources-source-map 全部隐藏
hidden-source-map 只隐藏源代码,会提示构建后代码错误信息
--> source-map / cheap-module-souce-map
下载插件:npm i --save-d mini-css-extract-plugin
引入插件:const MiniCssExtractPlugin=require('mini-css-extract-plugin')
使用插件:
{
test: /\.css$/,
use: [
// 创建 style 标签,将样式放入 // 'style-loader',
// 这个loader 取代 style-loader。作用:提取 js 中的 css 成单独文件
MiniCssExtractPlugin.loader,
// 将 css 文件整合到 js 文件中
'css-loader'
]
}
plugin:[
new MiniCssExtractPlugin({
//对输出的css文件重命名
filename:'css/built.css'
})
]
下载插件:npm i --save-d postcss-loader postcss-preset-env
修改配置文件:
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [
// postcss 的插件
require('postcss-preset-env')()
]
}
}
]
}
plugins: [
new HtmlWebpackPlugin({ template: './src/index.html' }),
new MiniCssExtractPlugin({ filename: 'css/built.css' })
]
下载安装包:npm install --save-dev optimize-css-assets-webpack-plugin
修改配置文件:
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin' )
// 压缩 css
new OptimizeCssAssetsWebpackPlugin()
语法检查: eslint-loader eslint
注意:只检查自己写的源代码,第三方的库是不用检查的
设置检查规则:
package.json中eslintConfig中设置~
"eslintConfig": {
"extends": "airbnb-base"
}
airbnb --> eslint-config-airbnb-base eslint-plugin-import eslint
/*
正常来讲,一个文件只能被一个loader处理。
当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序:
先执行eslint 在执行babel
*/
{
// 在package.json中eslintConfig --> airbnb
test: /\.js$/,
exclude: /node_modules/,
// 优先执行
enforce: 'pre',
loader: 'eslint-loader',
options: {
//对语法错误主动修正
fix: true
}
}
生产模式下会自动压缩js代码--mode='production'
new htmlWebpackPlugin({
template:path.join(__dirname,'./src/index.html'),
filename:'webpack.html',
//压缩html
minify:{
// 移除空格
collapseWhitespace: true,
// 移除注释
removeComments: true
}
})
* 优化打包构建速度
* oneOf
* babel缓存
* 多进程打包
* externals
* dll
* 优化代码运行的性能
* 缓存(hash-chunkhash-contenthash)
* tree shaking
* code split
* 懒加载/预加载
* pwa
oneOf方法
module:{
rules:{
oneOf:[{},{},...要设置的loader]
}
}
缓存
多进程打包
我们的js是单线程的,那么当我们打包一个很大的文件的时候,速度肯定是有点慢的,因此,我们可以开启多线程,来打包,js代码肯定是最多的,因此主要是处理js的打包
使用thread-loader结合babel-loader来开启多线程,但是这里要注意,开启多线程大概要600ms的时间,因此当我们文件非常小的时候,不要开启多线程,在文件很大很大的时候,在开启多线程
externals
这个主要是用来处理通过CDN引进来的连接,我们做处理不希望通过cdn的连接被打包
externals: {
// 拒绝jQuery被打包进来
jquery: 'jQuery'
}
dll
tree shaking
什么是tree shaking?—就是为了去除源代码中没有使用的代码
使用前提:1.必须使用ES6模块化 2.必须是生产模式production
使代码体积减小,请求速度加快
在一些版本中可能会有一些问题因此要在在package.json中配置
“sideEffects”: false 所有代码都没有副作用(都可以进行tree shaking)
问题:可能会把css / @babel/polyfill (副作用)文件干掉
“sideEffects”: [".css", “.less”]—解决这个问题
code split—代码分割
代码分割是什么意思?
当我们打包文件时,所有的入口文件中引入的js/css/node_modules等这些模块,都会被打包到同一个js文件中,这样就会造成文件体积太大,导致我们请求资源时间过长,出现白屏现象,因此我们要想办法把不同的js模块提取出来,分割成多个打包的js文件。
我们开发时,有单页面和多页面,现在大部分都是单页面,单页面肯定是有一个入口文件,但是多页面就会有多个入口文件。
使用方法一:
修改入口文件:
// 单入口
// entry: './src/js/index.js',
entry: {
// 多入口:有一个入口,最终输出就有一个bundle
index: './src/js/index.js',
test: './src/js/test.js'
},
output: {
// [name]:取文件名
filename: 'js/[name].[contenthash:10].js',
path: resolve(__dirname, 'build')
}
使用方法二:
方法二就是将js模块中引入的node_modules中的第三方插件库提取成一个单独的js文件
在webpack配置中添加optimization
optimization: {
splitChunks: {
chunks: 'all'
}
}
有两个作用:
1. 可以将node_modules中代码单独打包一个chunk最终输出
2. 自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk
使用方式三:
方法三主要是对单页面打包js文件的处理主要是通过import('src')的动态导入语法实现的,能将通过import()语法导入的js文件打包成一个单独的文件
使用方法,就是在入口文件处,通过使用import()语法导入模块就可以了
文件资源缓存
hash: 每次wepack构建时会生成一个唯一的hash值。问题: 因为js和css同时使用一个hash值。
如果重新打包,会导致所有缓存失效。(可能我却只改动一个文件)
chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样
问题: js和css的hash值还是一样的,因为css是在js中被引入的,所以同属于一个chunk
contenthash: 根据文件的内容生成hash值。不同文件hash值一定不一样
–> 让代码上线运行缓存更好使用
chunk就是以一个js文件为打包入口引入来的css,js,img等这种形式称为chunk
懒加载/预加载
这种方式主要也是解决单页面的代码运行速度的优化。
懒加载就是按需导入,意思就是当我用到的时候才从服务器请求相应的代码,没用到的时候就不请求。
实现方式,也是通过import(‘src’)动态导入语法实现的
//这种操作是异步方法,会返回一个promise对象,因此存在弊端,就是当我这个导入的文件太大时,可能会造成请求时间过长,用户等待的现象。
import(/* webpackChunkName: 'test' */'./test')
.then(({ mul }) => {//导入成功的回调
console.log(mul(4, 5));
}).catch(()=>{//导入失败的回调
console.log('导入失败!')
})
import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test')
.then(({ mul }) => {
console.log(mul(4, 5));
});
//实现原理就是,在一开始请求的时候,如果其他资源都请求完成时,那么这个时候浏览器就可以偷偷去服务器请求预加载的资源,那么在用到的时候就可以直接从缓存读取,非常快。
raw-loader
:加载文件原始内容(utf-8)file-loader
:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)url-loader
:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值会交给 file-loader 处理,小于阈值时返回文件 base64 形式编码 (处理图片和字体)source-map-loader
:加载额外的 Source Map 文件,以方便断点调试svg-inline-loader
:将压缩后的 SVG 内容注入代码中image-loader
:加载并且压缩图片文件json-loader
加载 JSON 文件(默认包含)handlebars-loader
: 将 Handlebars 模版编译成函数并返回babel-loader
:把 ES6 转换成 ES5ts-loader
: 将 TypeScript 转换成 JavaScriptawesome-typescript-loader
:将 TypeScript 转换成 JavaScript,性能优于 ts-loadersass-loader
:将SCSS/SASS代码转换成CSScss-loader
:加载 CSS,支持模块化、压缩、文件导入等特性style-loader
:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSSpostcss-loader
:扩展 CSS 语法,使用下一代 CSS,可以配合 autoprefixer 插件自动补齐 CSS3 前缀eslint-loader
:通过 ESLint 检查 JavaScript 代码tslint-loader
:通过 TSLint检查 TypeScript 代码mocha-loader
:加载 Mocha 测试用例的代码coverjs-loader
:计算测试的覆盖率vue-loader
:加载 Vue.js 单文件组件i18n-loader
: 国际化cache-loader
: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里更多 Loader 请参考官网
define-plugin
:定义环境变量 (Webpack4 之后指定 mode 会自动配置)ignore-plugin
:忽略部分文件html-webpack-plugin
:简化 HTML 文件创建 (依赖于 html-loader)web-webpack-plugin
:可方便地为单页应用输出 HTML,比 html-webpack-plugin 好用uglifyjs-webpack-plugin
:不支持 ES6 压缩 (Webpack4 以前)terser-webpack-plugin
: 支持压缩 ES6 (Webpack4)webpack-parallel-uglify-plugin
: 多进程执行代码压缩,提升构建速度mini-css-extract-plugin
: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-webpack-plugin)serviceworker-webpack-plugin
:为网页应用增加离线缓存功能clean-webpack-plugin
: 目录清理ModuleConcatenationPlugin
: 开启 Scope Hoistingspeed-measure-webpack-plugin
: 可以看到每个 Loader 和 Plugin 执行耗时 (整个打包耗时、每个 Plugin 和 Loader 耗时)webpack-bundle-analyzer
: 可视化 Webpack 输出文件的体积 (业务组件、依赖第三方模块)更多 Plugin 请参考官网
Loader
本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。 因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。
Plugin
就是插件,基于事件流框架 Tapable
,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。
Loader
在 module.rules 中配置,作为模块的解析规则,类型为数组。每一项都是一个 Object,内部包含了 test(类型文件)、loader、options (参数)等属性。
Plugin
在 plugins 中单独配置,类型为数组,每一项是一个 Plugin 的实例,参数都通过构造函数传入。
Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
初始化参数
:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数开始编译
:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译确定入口
:根据配置中的 entry 找出所有的入口文件编译模块
:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理完成模块编译
:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系输出资源
:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会输出完成
:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统在以上过程中,Webpack
会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
简单说
webpack-dashboard
:可以更友好的展示相关打包信息。webpack-merge
:提取公共配置,减少重复配置代码speed-measure-webpack-plugin
:简称 SMP,分析出 Webpack 打包过程中 Loader 和 Plugin 的耗时,有助于找到构建过程中的性能瓶颈。size-plugin
:监控资源体积变化,尽早发现问题HotModuleReplacementPlugin
:模块热替换source map
是将编译、打包、压缩后的代码映射回源代码的过程。打包压缩后的代码不具备良好的可读性,想要调试源码就需要 soucre map。
map文件只要不打开开发者工具,浏览器是不会加载的。
线上环境一般有三种处理方案:
hidden-source-map
:借助第三方错误监控平台 Sentry 使用nosources-source-map
:只会显示具体行数以及查看源代码的错误栈。安全性比 sourcemap 高sourcemap
:通过 nginx 设置将 .map 文件只对白名单开放(公司内网)注意:避免在生产中使用 inline-
和 eval-
,因为它们会增加 bundle 体积大小,并降低整体性能。
Webpack 实际上为每个模块创造了一个可以导出和导入的环境,本质上并没有修改 代码的执行逻辑,代码执行顺序与模块加载顺序也完全一致。
在发现源码发生变化时,自动重新构建出新的输出文件。
Webpack开启监听模式,有两种方式:
缺点:每次需要手动刷新浏览器
原理:轮询判断文件的最后编辑时间是否变化,如果某个文件发生了变化,并不会立刻告诉监听者,而是先缓存起来,等 aggregateTimeout
后再执行。
module.export = {
// 默认false,也就是不开启
watch: true,
// 只有开启监听模式时,watchOptions才有意义
watchOptions: {
// 默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
// 监听到变化发生后会等300ms再去执行,默认300ms
aggregateTimeout:300,
// 判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
poll:1000
}
}
Webpack
的热更新又称热替换(Hot Module Replacement
),缩写为 HMR
。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
HMR的核心就是客户端从服务端拉去更新后的文件,准确的说是 chunk diff (chunk 需要更新的部分),实际上 WDS 与浏览器之间维护了一个 Websocket
,当本地资源发生变化时,WDS 会向浏览器推送更新,并带上构建时的 hash,让客户端与上一次资源进行对比。客户端对比出差异后会向 WDS 发起 Ajax
请求来获取更改内容(文件列表、hash),这样客户端就可以再借助这些信息继续向 WDS 发起 jsonp
请求获取该chunk的增量更新。
后续的部分(拿到增量更新之后如何处理?哪些状态该保留?哪些又需要更新?)由 HotModulePlugin
来完成,提供了相关 API 以供开发者针对自身场景进行处理,像react-hot-loader
和 vue-loader
都是借助这些 API 实现 HMR。
细节请参考Webpack HMR 原理解析
VSCode
中有一个插件 Import Cost
可以帮助我们对引入模块的大小进行实时监测,还可以使用 webpack-bundle-analyzer
生成 bundle
的模块组成图,显示所占体积。
bundlesize
工具包可以进行自动化资源体积监控。
文件指纹是打包后输出的文件名的后缀。
Hash
:和整个项目的构建相关,只要项目文件有修改,整个项目构建的 hash 值就会更改Chunkhash
:和 Webpack 打包的 chunk 有关,不同的 entry 会生出不同的 chunkhashContenthash
:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变设置 output 的 filename,用 chunkhash。
module.exports = {
entry: {
app: './scr/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path:__dirname + '/dist'
}
}
设置 MiniCssExtractPlugin 的 filename,使用 contenthash。
module.exports = {
entry: {
app: './scr/app.js',
search: './src/search.js'
},
output: {
filename: '[name][chunkhash:8].js',
path:__dirname + '/dist'
},
plugins:[
new MiniCssExtractPlugin({
filename: `[name][contenthash:8].css`
})
]
}
设置file-loader的name,使用hash。
占位符名称及含义
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename:'bundle.js',
path:path.resolve(__dirname, 'dist')
},
module:{
rules:[{
test:/\.(png|svg|jpg|gif)$/,
use:[{
loader:'file-loader',
options:{
name:'img/[name][hash:8].[ext]'
}
}]
}]
}
}
可以使用 enforce
强制执行 loader
的作用顺序,pre
代表在所有正常 loader 之前执行,post
是所有 loader 之后执行。(inline 官方不推荐使用)
高版本
的 Webpack 和 Node.js多进程/多实例构建
:HappyPack(不维护了)、thread-loader压缩代码
图片压缩
缩小打包作用域
:
提取页面公共资源
:
DLL
:
充分利用缓存提升二次构建速度
:
Tree shaking
Scope hoisting
动态Polyfill
更多优化请参考官网-构建性能
代码分割的本质其实就是在源代码直接上线
和打包成唯一脚本main.bundle.js
这两种极端方案之间的一种更适合实际场景的中间状态。
源代码直接上线:虽然过程可控,但是http请求多,性能开销大。
打包成唯一脚本:一把梭完自己爽,服务器压力小,但是页面空白期长,用户体验不好。
Loader 支持链式调用,所以开发上需要严格遵循“单一职责”,每个 Loader 只负责自己需要负责的事情。
Loader的API 可以去官网查阅
webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在特定的阶段钩入想要添加的自定义功能。Webpack 的 Tapable 事件流机制保证了插件的有序性,使得整个系统扩展性良好。
Plugin的API 可以去官网查阅
大多数JavaScript Parser遵循 estree
规范,Babel 最初基于 acorn
项目(轻量级现代 JavaScript 解析器) Babel大概分为三大部分:
想了解如何一步一步实现一个编译器的同学可以移步 Babel 官网曾经推荐的开源项目 the-super-tiny-compiler