如何配置和启动、性能分析、压缩编译时间、编译体积、运行得更快
项目初始化
// 安装
npm init -y
npm i webpack webpack-cli html-webpack-plugin webpack-dev-server cross-env -D
配置环境
模式
- 开发环境:构建结果用于本地开发调试,不进行代码压缩,打印debug信息,包含sourcemap文件
- 需要生成sourcemap文件
- 需要打印debug信息
- 需要live reload或者hot reload功能
- 生产环境:直接用于线上的,代码都是压缩后,运行时不打印debug信息,静态文件不包括sourcemap;默认会启用各种性能优化的功能,包括构建结构优化以及webpack运行性能优化(tree-shaking)
- 需要分离css为单独文件,以便共享一个css文件
- 需要压缩html\css\js、图片
- mode默认值为:production
webpack 4.x版本引入mode概念,将process.env.NODE_ENV设为development
- production:在生产环境开启压缩和优化,启用 FlagDependencyUsagePlugin, FlagIncludedChunksPlugin, ModuleConcatenationPlugin, NoEmitOnErrorsPlugin, OccurrenceOrderPlugin, SideEffectsFlagPlugin 和 UglifyJsPlugin
- development:启用NamedChunksPlugin和NamedModulesPlugin
区分环境
- –mode:用来设置模块内的 process.env.NODE_ENV(node环境下的取不到)
webpack的mode默认为production
webpack serve的mode默认为development
// package.json
"script":{
"buuld": "webpack --mode=development" // 配置覆盖webpack.config,js里的默认值
}
// src/index.js
console.log(process.env.NODE_ENV) //
- –env:用来设置webpack配置文件的函数参数(DefinePlugin可以让任何地方都取到变量)
- define-plugin 用来配置在编译时用的全局常量
// package.json
"script":{
"buuld": "webpack --env=development"
}
// webpack.config.js
module.exports=(env={})=>{
return ({
mode: env.development? 'development' : 'production',
entry:{},
output:{},
module:{},
plugins:[
// 将环境变量加载到js文件中,定义在编译使用的全局变量,在浏览器运行阶段只是值
new webpack.DefinePlugin({
// "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV), // cross-env 配置
"ENVIORNMENT": "\"development"\"
})
]
})
}
- cross-env:用来设置node环境变量,可以在任何地方拿到process.env.NODE_NEV
// package.json
"script":{
"buuld": "cross-env NODE_NEV=development webpack"
}
读取.env文件
- dotenv:按需加载不同的环境变量文件
// .env
{
"NODE_ENV": "production"
}
// webpack.config.js
// require('dotenv').config()
const fs = require('fs')
let env = JSON.parse(fs.readFileSync(path.resolve(__dirname, '.env'), 'utf-8'))
for(let key in env) proccess.env[key] = env[key]
数据分析
1. 日志美化
friendly-errors-webpack-plugin可以识别某些类别的webpack错误,并清理、聚合和优先级,以提供更好的开发体验
npm i friendly-errors-webpack-plugin node-notifier -D // 通知器
2. 速度分析
// 安装
npm install --save-dev speed-measure-webpack-plugin
// 使用
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const webpackConfig = smp.wrap({
plugins: [
new MyPlugin(),
new MyOtherPlugin()
]
});
3. 文件体积监控
webpack-bundle-analyzer插件可以生成代码分析报告,直观分析打包的文件包含哪些、大小占比如何,模块包含关系,依赖项,文件是都重复,压缩后大小如何
需要配合webpack和webpack-cli一起使用
// 安装
npm i webpack-bundle-analyzer -D
// 使用
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
// 先把它存下来
analyzerMode: 'disabled', // 不启动展示打包报告的http服务器
generateStatsFile: true // 不打开网站,但是在dist生成stats.json文件
})
]
}
// package.json
"scripts": {
"dev": "webpack --progress",
"analyzer": "webpack-bundle-analyzer --port 8888 ./dist/stats.json" // 什么时候想看才打开来看
}
// 执行
npm run dev
npm run analyzer
优化编译时间
- 减少要处理的文件
- 缩小查找范围
缩小查找范围
- extensions:指定需要引入的文件扩展名,查找时会依次尝试对扩展名进行匹配
- alias:配置别名,可加快webpack查找模块的速度;如果直接引入bootstrap,可以不用从node_modules文件夹中按模块的查找规则查找
- mainFields:默认情况下package.json中按main字段的文件名查找入口文件
const bootstrap = path.resolve(__dirname, 'node_modules/bootstrap/dist/css/bootstrap.css')
resolve:{
extensions: [".js", ".jsx", ".json"], // 指定文件扩展名
alias: { bootstrap }, // 查找别名
// 第三方模块安装在全局或外面可以先找外面,再找node_modules
modules: ['c:/node_modules', 'node_modules'], // 指定查找目录
// target为web或webworker时,默认是:
// mainFields: ['browser', 'module', 'main']
// target为其它时,默认是
mainFields: ['module', 'main'] // 如果找不到,会找index.js
}
// index.js 中引入
require('bootstrap')
- oneOf:每个文件对rules中所有规则都会遍历一遍,如果使用oneOf就可以解决该问题,只要能匹配一个即可退出;在oneOf中不能两个配置处理同一种类型文件
module:{
rules:{
// 只能匹配数组中的某一个,找到一个后就不再继续查找剩下的loader,不然会先查找.css再找.less
oneOf: [
{
test: /\.css$/,
use:['style-loader', 'css-loader']
},
{
test: /\.less$/,
use:['style-loader', 'css-loader', 'less-loader']
}
]
}
},
- external:想引用一个库,但不想让webpack打包,并且又不影响以CMD、AMD或者window/global全局等方式进行使用
// 手动在src/index.html中引入链接
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script>
console.log(jQuery)
</script>
// src/index.js
let $ = require('jquery') // 相当于window.jQuery
console.log($)
externals:{
jquery: 'jQuery'
}
使用CDN方式引入js的另一种方式:也可以通过配置html-webpack-externals-plugin来自动帮你cdn脚本,即在index.html中动态添加script标签
// 安装
npm i html-webpack-externals-plugin -D
// 使用
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
module.exports = {
// 其它省略...
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [{
module: 'vue',
entry: 'https://lib.baomitu.com/vue/2.6.12/vue.min.js',
global: 'Vue'
}]
})
],
// 其它省略...
}
- resolveLoader:引用自定义loader
区别:reslove只对普通loader有用
// loaders/logger-loader.js
// loader是一个函数,接收一个内容,返回处理后的内容
function loader(source){
return source
}
module.exports = loader
// webpack.config.js
const loadersPath = path.resolve(__dirname, 'loaders')
{
resolveLoader: {
modules: [loadersPath] // 表示去这里找自定义loader
},
modules:{
rules:[
{
test: /\.css$/,
// 引入logger-loader
use:['logger-loader', 'style-loader', 'style-loader', 'css-loader']
},
]
}
}
noParse
可以配置哪些模块文件的内容不需要进行解析
比如:不需要解析依赖(即无依赖)的第三方大类库等,可以提供整体构建速度
使用noParse进行忽略的模块文件中不能使用import、require等语法
module:{
// 不需要找这个依赖项
noParse: /title.js/
}
IgnorePlugin
ignore-plugin:忽略某些待定的模块,让webpack不把这些指定的模块打包进去
requestRegExp:匹配资源请求路径的正则表达式
contextRegExp:匹配资源上下文目录的正则表达式
moment:会将所有本地化内容和核心功能一起打包,可以使用IgnorePlugin在打包时忽略本地化内容
npm i moment -S
// src/index.js
import moment form 'moment'
console.log(moment)
// webpack.config.js
const webpack = require('webpack')
plugins:[
webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/, // 忽略资源,忽略moment的语言文件,不打语言包
contextRegExp: /moment$/ // 忽略上下文目录
})
]
thread-loader 多进程(happypack已被废掉)
把thread-loader 放在其它loader之前,放置在这个loader之后的loader会在一个单独的线程里运行
include:表示哪些目录中的js文件需要进行babel-loader
exclude:表示哪些目录中的js不需要进行babel-loader(exclude优先级大于include,尽量避免exclude)
// 安装
npm i thread-loader babel-loader @babel/core @babel/preset-env -D
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
exclude: /node_modules/, // 不解析node_modules
use:[
// 少使用:开启线程和线程通信都需要时间
{loader: 'thread-loader', options:{workers: 3}}, // 开启新的进程3个线程
'babel-loader'
]
}
利用缓存
- babel-loader
babel在转义js文件中消耗性能较高,将babel-loader执行的结果缓存起来,当重新打包构建时会尝试读取缓存,从而提高打包构建速度、降低消耗
默认存放位置:node_modules/.cache/babel-loader
{
test: /\.js$/,
include: path.resolve(__dirname, 'src'),
exclude: /node_modules/, // 不解析node_modules
use:[
{
loader: 'babel-loader',
options: {
cacheDirectory: true // 开启babel缓存,下次编译不用再处理它
}
}
]
}
- cache-loader
比如css-loader不能像babel-loader配置缓存,可以用cache-loader,将
默认存放位置:node_modules/.cache/cache-loader
// 安装
npm i cache-loader -D
// 使用
{
test: /\.css$/,
use: [
'cache-loader', // 开启缓存
'logger-loader',
'style-loader',
'css-loader'
]
}
- hard-source-webpack-plugin
为模块提供了中间缓存,存放路径是:node_modules/.cache/hard-source
配置hard-source-webpack-plugin后,首次构建时间不会又太大变化,第二次构建时间可以减少80%左右
注意:webpack5中已经内置了模块缓存,不需要再使用此插件
// 安装
npm i hard-source-webpack-plugin -D
// webpack.config.js
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
plugins: [
new HardSourceWebpackPlugin()
]
webpack dll配置非常复杂,后面被hard-source-webpack-plugin替换掉
优化编译体积
压缩js、css、html和图片
- terser-webpack-plugin:优化和压缩js
- optimize-css-assets-webpack-plugin:优化和压缩css
- image-webpack-loader:压缩和优化图片
// 安装
npm i terser-webpack optimize-css-assets-webpack-plugin image-webpack-loader -D
// webpack.config.js
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const TerserPlugin = require("terser-webpack-plugin")
optimization:{
minimize: true, // 开启最小优化
minimizer: [ // 优化器
new TerserPlugin() // 压缩JS
]
},
module:{
rules:[
{
test: /\.(jpg|png|gif|bmp)$/,
use: [
'file-loader',
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
},
optipng: {
enabled: false,
},
pngquant: {
quality: [0.65, 0.90],
speed: 4
},
gifsicle: {
interlaced: false,
},
webp: {
quality: 75
}
}
},
],
}
]
},
plugins: [
new HtmlWbapackPlugin({
template: './src/index.html',
minify:{ // 压缩html
collapseWhitespace: true,
removeComments: true
}
}),
new OptimizeCssAssetsPlugin({ // 压缩css
assetNameRegExp: /\.optimize\.css$/g,
cssProcessor: require('cssnano'),
cssProcessorPluginOptions: {
preset: ['default', { discardComments: { removeAll: true } }],
},
canPrint: true
})
]
清除无用css
purgecss-webpack-plugin单独提取css并清除用不到的css
// 安装
npm i purgecss-webpack-plugin mini-css-extract-plugin -D
// webpack.config.js
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const PATHS = {
src: path.join(__dirname, 'src')
}
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader"
]
},
{
test: /\.less$/,
use: [
MiniCssExtractPlugin.loader,
"css-loader",
"less-loader"
]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
}),
// **匹配任意字符,包括路径分割符,*匹配任意字符,不暴扣路径分割符
new PurgeCSSPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
}),
]
}
tree shaking
开启:
- production下默认开启
- 在package.json中配置
- sideEffect:false 所有的代码都没有副作用情况下(bundle.js中就会清除未引用代码了)
- 可能会把css和@babel/polyfill文件干掉,可以设置sideEffect:[’*.css’,"./src/extends.js"]防止被清掉
// package.json
{
"sideEffects": false // 默认true,表示当前项目中的模块是否有副作用
}
// webpack.config.js
module.exports = {
optimization: {
sideEffects: true // 默认false,表示是否移除无副作用的模块
usedExports: true,
}
}
Scope Hosting 作用域提升
可以让webpack打包出来的代码文件更小,运行更快(webpack3中推出)
原理:将所有的模块按照引用顺序放在一个函数作用域里,然后适当地重命名一些变量以防止命名冲突
在production下默认开启,开发环境下要用webpack.optimizeModuleConcatenationPlugin插件
运行速度优化
code-split代码分割
- 入口点分割(配置提取公共代码、重复模块)
- 懒加载( 按需加载/code-split)
即用户需要什么功能就只加载这个功能对应的代码
采用的原则:- 对网站功能进行划分,每一类一个chunk
- 对首次打开页面需要的功能直接加载,尽快展示给用户,某些依赖大量代码的功能点可以按需加载
- 被分割出去的代码需要一个按需加载的时机
// 安装 识别import()动态导入
npm i babel-plugin-syntax-dynamic-import -D
// 按需加载/懒加载,即import的动态加载模块/组件,分类:
// 静态代码分割:在代码中明确声明需要异步加载的代码
import('./hello').then(result=>{...})
// 动态代码分割:在代码调用时根据当前的状态,动态地异步加载对应的代码块
const getTheme = (themeName) => import(`./src/themes/${themeName}`)
- prefetch预获取:浏览器空闲的时候才加载
- reload预加载:资源肯定会用到,先加载了
// webpackPreload:true
import(/*webpackChunkName:'hello', webpackPrefetch: true, */'./hello').then(result=>{...})
- 提取公共代码 splitChunks
从webpack v4开始,CommonsChunkPlugin删除了,而改为optimization.splitChunks
module、chunk、bundle
- module:就是js模块化webpack支持commonJS、es6等模块化规范,简单来说就是通过impoer语句引入的代码
- chunk:是webpack根据功能拆分出来的,包含三种情况
- 项目入口entry
- 通过import动态加载的代码
- 通过splitChunks拆分出来的代码
- bundle:是webpack打包之后的各个文件,一般就是和chunk一对一的关系,bundle就是对chunk进行编译压缩打包处理之后产出的
js兼容性
babel
babel是一个js编译器,将ECMAScript 2015+ 版本的代码转换为向后兼容的js语法,做语法转换
编译过程:解析、转换和打印输出
@babel/core:把js代码分析成ast
// 安装
npm @babel/core @babel/cli -D
// src/index.js
const sum = (a, b)=> a+b
console.log(sum)
//package.json
"scripts":{
"build": "babel src --out-dir dist --watch"
}
babel插件
babel虽然是一个编译器,但是什么动作都不做(输出什么返回什么 const babel = code=>code),将代码解析后再输出同样的代码,如果想要做实际的工作,需要为其添加插件
// 安装
npm i @babel/plugin-transform-arrow-functoins -D
// .babelrc
{
"plugins": ["@babel/plugin-transform-arow-functions"]
}
babel预设
@babel/preset-env
- 可以让你使用最新的js语法,而不需要去管理语法转换器(并且可选的支持目标浏览器环境的polyfills);
- 会根据你配置的目标环境,生成插件列表来编译,可以使用.browserslistrc文件来指定目标环境
// .babelrc
{
// "plugins": ["@babel/plugin-transform-arow-functions"], 这个可以不要了
"presets": ["@babel/preset-env"] // 是一堆插件的babel plugin的集合
}
// @babel/preset-env 返回plugin集合
module.exports = function(){
return {plugins: ["pluginA", "pluginB", "pluginC"]}
}
// .browserslistrc
> 0.25%
not dead
// 或者最新的版本
last 2 Chrome versions
polyfill
@babel/preset-env只转换新的js语法,而不转换新的api,比如Iterator、Generator、Set、Map、Proxy、Reflect、Symbol、Promise等全局对象。以及一些在全局对象上的方法(比如Object.assign、Array.from)都不会转码,所以必须使用babel-polyfill来转换
npm i @babel/polyfill core-js@3 -S
npm i webpack webpack-cli babel-loader -D
- useBuiltIns: false 此时不对polyfill做操作。如果引入@babel/polyfill,则无视配置的浏览器兼容,引入所有的polyfill
- useBuiltIns: entry 根据配置的浏览器,引入浏览器不兼容的polyfill(需要在入口文件添加import “@babel/polyfill”)
- 需要指定core-js的版本,如果"corejs":3,则 import “@babel/polyfill” 需要改成 import “core-js/stable” ; import “regenerator-runtime/runtime”
- core-js@2中已经不再添加新特性,新特性会添加到core-js@3,如Array.prototype.flat()
- useBuiltIns: usage 会根据配置的浏览器兼容,以及你代码中用到的API来进行polyfill,实现了按需添加
// src/index.js
import "@babel/polyfill"
console.log(Arrat.isArray([]))
let p = new Promise()
// webpack.config.js
{
test: /\.js?$/,
use: {
loader: 'babel-loader',
options: {
presets: [
["@babel/preset-env",
{
// 此时不对polyfill做操作,要自己在文件引入import "@babel/polyfill"
// useBuiltIns: false ,
useBuiltIns: 'usage',
corejs: 3,
targets: 'last 2 Chorme versions'
}
]
]
}
}
}
// src/index.js
require("@core-js/stable")
require("regenerator-runtime/runtime")
console.log(Arrat.isArray([]))
let p = new Promise()
// webpack.config.js
{
useBuiltIns: 'entry',
corejs: 3,
targets: 'last 2 Chorme versions'
}
babel-runtime
babel提供useBuiltIns: usage会污染全局变量,为了解决这个问题,提供了单独的包babel-runtime用来提供编译模块的工具函数
babel-runtime像一种按需加载的实现,比如你哪里需要使用promise,只要在这个文件头部 import Promise from ''babel-runtime/core-js/promise"
// 安装
npm i babel-runtime -D
// src/index.js
import Promise from "babel-runtime/core-js/promise" // 手动引入不方便
const p = new Promise((resolve)=>{
resolve('ok')
})
console.log(p)
// webpack.config.js
{
test: /\.js?$/,
use: {
loader: 'babel-loader',
options: {
presets: [
["@babel/preset-env"]
]
}
}
}
babel-plugin-transform-runtime
为了不手动引入babel-runtime,可以使用babel-plugin-transform-runtime
启动插件babel-plugin-transform-runtime后,babel就会使用babel-runtime下的工具函数
// 安装
npm i @babel/plugin-transform-runtime @babel/runtime-corejs2 @babel/runtime-corejs3 -D
// webpack.config.js
{
test: /\.js?$/,
use: {
loader: 'babel-loader',
options: {
presets: [
["@babel/preset-env", {
targets: "> 0.25%, not dead"
}]
],
plugins: [
"@babel/plugin-transform-runtime",
{
corejs: 3, // 如果用到promise,会自动引入对应的polyfill 3
helpers: true, // 移除内两的babel helpers 并自动引入babel-runtime
regenerator: true // 是否开启gen函数转换成使用regenerator runtime来避免污染全局作用域
}
]
}
}
}
执行顺序
- 先执行插件, 再执行presets预设
- 插件顺序从前往后排列
- presets顺序从后往前执行
// plugins/plugin1.js
const visitor={
FunctionDeclaration(){
console.log("plugin1")
}
}
module.exports = function(babel){
return {
visitor
}
}
// webpack.config.js
const path = require('path')
const plugin1 = path.resolve(__dirname, 'plugins', 'plugins1.js')
const plugin2 = path.resolve(__dirname, 'plugins', 'plugins2.js')
const plugin3 = path.resolve(__dirname, 'plugins', 'plugins3.js')
const plugin4 = path.resolve(__dirname, 'plugins', 'plugins4.js')
const plugin5 = path.resolve(__dirname, 'plugins', 'plugins5.js')
const plugin6 = path.resolve(__dirname, 'plugins', 'plugins6.js')
function preset1(){
return {plugins:[plugin5, plugin6]}
}
function preset2(){
return {plugins:[plugin3, plugin4]}
}
module.exports = {
mode: "development",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js"
},
module: {
rules: [
test: /\.js$/,
use: {
loader: "babel-loader",
options: {
plugins: [plugin1, plugin2],
presets: [preset1, preset2]
}
}
]
}
}
contenthash
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: "css/[name].[contenthash].css"
})
]
}