Vite3遷移Webpack5的實現(xiàn)
為什么要做遷移
現(xiàn)有問題
1、按需加載頁面的時候加載速度慢。
2、熱更新時常失效,手動刷新加載速度也慢。
性能提升
1、舊框架啟動約13秒,但啟動后每個頁面切換加載都得等待5-10s,開發(fā)體驗較差。新框架項目啟動加所有頁面加載約65秒。利用webpack5緩存的新特性,啟動速度變快的同時,帶來更好的開發(fā)體驗。
2、舊框架在jenkins打包需要2分52秒,新框架打包僅需1分48秒,速度提升了37%。
webpack5為什么快
webpack5 較于 webpack4,新增了持久化緩存、改進緩存算法等優(yōu)化,通過配置webpack 持久化緩存,來緩存生成的 webpack 模塊和 chunk,改善下一次打包的構建速度,可提速 90% 左右
安裝依賴
"vue": "^3.2.37",
"webpack": "5.64.4",
"webpack-bundle-analyzer": "4.5.0",
"webpack-cli": "4.10.0",
"webpack-dev-server": "4.6.0",
"webpack-merge": "5.8.0"
"babel-loader": "8.2.3",
"@babel/plugin-transform-runtime": "7.16.4",
"clean-webpack-plugin": "4.0.0",
"css-loader": "6.5.1",
"css-minimizer-webpack-plugin": "3.2.0",// 對CSS文件進行壓縮
"mini-css-extract-plugin": "2.4.5",// 將CSS文件抽取出來配置, 防止將樣式打包在 js 中文件過大和因為文件大網(wǎng)絡請求超時的情況。
"postcss-loader": "6.2.1",
"postcss-preset-env": "7.0.1",
"vue-style-loader": "4.1.3",
"style-loader": "^3.3.2",
"less-loader": "^11.1.0",
"friendly-errors-webpack-plugin": "1.7.0",
"html-webpack-plugin": "5.5.0",
"progress-bar-webpack-plugin": "2.1.0",
"vue-loader": "^17.0.1",
"eslint-webpack-plugin": "^4.0.0",
"stylelint-webpack-plugin": "^4.1.0",
"copy-webpack-plugin": "^11.0.0",
"cross-env": "^7.0.3",
"@babel/runtime-corejs3": "7.16.3"http:// 安裝到dependencieswebpack5配置
為了區(qū)分開發(fā)環(huán)境和打包環(huán)境,分了3個js文件(base、dev、prod),通過webpack-merge這個插件,進行合并操作。
webpack.base.conf.js
const { resolve, babelLoaderConf } = require('./utils.js')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const { VueLoaderPlugin } = require('vue-loader/dist/index')
// const StylelintPlugin = require('stylelint-webpack-plugin')
const ESLintPlugin = require('eslint-webpack-plugin')
const { YouibotPlusResolver } = require('youibot-plus')
const MiniCssExtractPlugin = require('mini-css-extract-plugin') // 將CSS文件抽取出來配置, 防止將樣式打包在 js 中文件過大和因為文件大網(wǎng)絡請求超時的情況。
const { AntDesignVueResolver } = require('unplugin-vue-components/resolvers')
const isDev = process.env.NODE_ENV === 'development' // 是否是開發(fā)模式
module.exports = {
entry: {
app: resolve('src/main.ts')
},
resolve: {
//引入模塊時不帶擴展名,會按照配置的數(shù)組從左到右的順序去嘗試解析模塊
extensions: ['.ts', '.tsx', '.js', '.vue', '.json'],
alias: {
'@': resolve('src')
}
},
module: {
noParse: /^(vue|vue-router|youibot-plus|echarts)$/, // 不解析庫
rules: [
{
test: /\.vue$/,
use: [
{
loader: 'vue-loader'
}
],
include: /(src)/
},
//babel7之后已經(jīng)有了解析 typescript 的能力,也就不再需要 ts-loader
{
test: /\.(ts|js)x?$/,
use: [
{
loader: 'thread-loader', // 開啟多進程打包
options: {
worker: 3
}
},
babelLoaderConf
],
exclude: /node_modules/
},
{
test: /\.css$/,
use: [
// 開發(fā)環(huán)境使用style-looader(通過動態(tài)添加 style 標簽的方式,將樣式引入頁面),打包模式抽離css
isDev ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: false
}
}
]
},
{
test: /\.less$/,
use: [
isDev ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: false
}
},
{
loader: 'postcss-loader',
options: {
postcssOptions: {
// postcss-preset-env 內(nèi)部集成了 autoprefixer 添加css第三方前綴
plugins: ['postcss-preset-env']
}
}
},
{
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true
},
additionalData: '@import "@/styles/variables.less";'
}
}
]
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
type: 'asset',
parser: {
dataUrlCondition: {
// 文件小于 10k 會轉(zhuǎn)換為 base64,大于則拷貝文件
maxSize: 10 * 1024
}
},
generator: {
filename: 'images/[base]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
type: 'asset',
generator: {
filename: 'files/[base]'
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
type: 'asset',
generator: {
filename: 'media/[base]'
}
}
]
},
plugins: [
new VueLoaderPlugin(),
//將打包后的文件自動引入到index.html里面
new HtmlWebpackPlugin({
template: resolve('public/index.html'),
favicon: resolve('public/logo.ico')
}),
require('unplugin-vue-components/webpack')({
resolvers: [
AntDesignVueResolver({
importStyle: false
}),
YouibotPlusResolver()
],
dts: true,
dirs: ['src/components', 'src/pages'] // 按需加載的文件夾
}),
require('unplugin-auto-import/webpack')({
imports: ['vue', 'vue-router', 'pinia'],
resolvers: [AntDesignVueResolver()],
eslintrc: {
enabled: true,
filepath: '../.eslintrc-auto-import.json',
globalsPropValue: true
},
dts: 'src/types/auto-imports.d.ts'
}),
// new StylelintPlugin(),
new ESLintPlugin()
]
}webpack.dev.js
const { merge } = require('webpack-merge')
const webpack = require('webpack')
const { getNetworkIp, resolve } = require('./utils.js')
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin')
const common = require('./webpack.base.conf')
const chalk = require('chalk')
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
const devWebpackConfig = merge(common, {
mode: 'development',
devtool: 'eval-cheap-module-source-map',
output: {
path: resolve('dist'),
filename: 'js/[name].[hash].js',
chunkFilename: 'js/[name].[hash].js',
publicPath: '/'
},
// 日志打印只打印錯誤和警告
stats: 'errors-warnings',
devServer: {
host: getNetworkIp(),
port: 8094, // 端口號
open: true, // 自動打開
hot: true, // 熱更新
allowedHosts: 'all',
client: {
progress: false, // 將運行進度輸出到控制臺。
overlay: { warnings: false, errors: true } // 全屏顯示錯誤信息
}
},
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development'),
__VUE_OPTIONS_API__: true, //控制臺警告處理
__VUE_PROD_DEVTOOLS__: true //控制臺警告處理
})
],
// 緩存
cache: {
type: 'filesystem',
buildDependencies: {
config: [__filename] // 針對構建的額外代碼依賴的數(shù)組對象。webpack 將使用這些項和所有依賴項的哈希值來使文件系統(tǒng)緩存失效。
},
cacheDirectory: resolve('temp_cache'),
name: 'scf-cache', // 路徑temp_cache/scf-cache
compression: 'gzip'
}
})
devWebpackConfig.plugins.push(
// 進度條
new ProgressBarPlugin({
format: ` :msg [:bar] ${chalk.green.bold(':percent')} (:elapsed s)`,
clear: true
}),
// 錯誤提示
new FriendlyErrorsWebpackPlugin({
// 成功的時候輸出
compilationSuccessInfo: {
messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${devWebpackConfig.devServer.port}`]
},
// 是否每次都清空控制臺
clearConsole: true
})
)
module.exports = devWebpackConfigwebpack.prod.js
const path = require('path')
const { merge } = require('webpack-merge')
const webpack = require('webpack')
const { resolve } = require('./utils.js')
const MiniCssExtractPlugin = require('mini-css-extract-plugin') // 將CSS文件抽取出來配置, 防止將樣式打包在 js 中文件過大和因為文件大網(wǎng)絡請求超時的情況。
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') // 對CSS文件進行壓縮
const TerserPlugin = require('terser-webpack-plugin')
const common = require('./webpack.base.conf')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const { CleanWebpackPlugin } = require('clean-webpack-plugin')
const CopyPlugin = require('copy-webpack-plugin')
module.exports = () => {
const analyzerPlugins = process.env.NODE_ENV === 'analyzer' ? [new BundleAnalyzerPlugin()] : []
return merge(common, {
mode: 'production',
optimization: {
// chunk拆分,提升首屏加載速度
splitChunks: {
cacheGroups: {
vendors: {
// 提取node_modules代碼
test: /node_modules/, // 只匹配node_modules里面的模塊
name: 'vendors', // 提取文件命名為vendors,js后綴和chunkhash會自動加
minChunks: 1, // 只要使用一次就提取出來
chunks: 'initial', // 只提取初始化就能獲取到的模塊,不管異步的
minSize: 0, // 提取代碼體積大于0就提取出來
priority: 1 // 提取優(yōu)先級為1
},
commons: {
// 提取頁面公共代碼
name: 'commons', // 提取文件命名為commons
minChunks: 2, // 只要使用兩次就提取出來
chunks: 'initial', // 只提取初始化就能獲取到的模塊,不管異步的
minSize: 0 // 提取代碼體積大于0就提取出來
}
}
},
// 壓縮
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true, // 開啟多線程壓縮
terserOptions: {
compress: {
pure_funcs: ['console.log'] // 刪除console.log
}
}
}),
new CssMinimizerPlugin()
],
// tree shaking
usedExports: true
},
performance: {
hints: false
},
// devtool: 'source-map', //如果配置source-map的話,生產(chǎn)環(huán)境下也可以定位到具體代碼,但是相應的打包文件也會變大(map文件體積,4m變成17m),而且會有代碼暴露的風險。
plugins: [
// 清空dist
new CleanWebpackPlugin(),
new CopyPlugin({
patterns: [
{
from: path.resolve(__dirname, '../public'), // 復制public下文件
to: path.resolve(__dirname, '../dist'), // 復制到dist目錄中
filter: source => !source.includes('index.html') // 忽略index.html
}
]
}),
// css抽離
new MiniCssExtractPlugin({
filename: 'css/[name].[contenthash].css',
chunkFilename: 'css/[name].[contenthash].css'
}),
// css壓縮
new CssMinimizerPlugin(),
new webpack.DefinePlugin({
//在業(yè)務代碼中也可以訪問process變量區(qū)分環(huán)境
'process.env.NODE_ENV': JSON.stringify('production'),
__VUE_OPTIONS_API__: true, //控制臺警告處理
__VUE_PROD_DEVTOOLS__: false //控制臺警告處理
}),
...analyzerPlugins
],
output: {
path: resolve('dist'),
filename: 'js/[name].[hash].js',
chunkFilename: 'js/[name].[hash].js'
}
})
}utils.js
const path = require('path')
const os = require('os')
exports.getNetworkIp = function () {
let needHost = '' // 打開的host
try {
// 獲得網(wǎng)絡接口列表
let network = os.networkInterfaces()
for (let dev in network) {
let iface = network[dev]
for (let i = 0; i < iface.length; i++) {
let alias = iface[i]
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
needHost = alias.address
}
}
}
} catch (e) {
needHost = 'localhost'
}
return needHost
}
exports.resolve = function (dir) {
return path.join(__dirname, '..', dir)
}
// babel-loader配置
exports.babelLoaderConf = {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
targets: {
browsers: ['ie>=8', 'chrome>=62'],
node: '8.9.0'
},
debug: false,
useBuiltIns: 'usage',
corejs: '3.0'
}
],
[
'@babel/preset-typescript',
{
allExtensions: true // 支持所有文件擴展名,否則在vue文件中使用ts會報錯
}
]
],
plugins: [
[
//js文件在babel轉(zhuǎn)碼后會生成很多helper函數(shù)(可能有大量重復),polyfill會在全局變量上掛載目標瀏覽器缺失的功能,這個插件的作用是將 helper 和 polyfill 都改為從一個統(tǒng)一的地方引入,并且引入的對象和全局變量是完全隔離的
//Babel 默認只轉(zhuǎn)換新的 JavaScript 句法(syntax),而不轉(zhuǎn)換新的 API(polify實現(xiàn))
'@babel/plugin-transform-runtime',
{
corejs: 3
}
]
]
}
}知識點
環(huán)境區(qū)分
// package.json 命令行 "build:dev": "cross-env NODE_ENV=development webpack serve --config build/webpack.dev.js", "build:prod": "cross-env NODE_ENV=production webpack --config build/webpack.prod.js", "build:analyzer": "cross-env NODE_ENV=analyzer webpack serve --config build/webpack.prod.js",
在window環(huán)境下需要cross-env這個依賴幫助我們node環(huán)境下做變量標識,通過NODE_ENV進行聲明即可。
//webpack.dev.js
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development'),
__VUE_OPTIONS_API__: true, //控制臺警告處理
__VUE_PROD_DEVTOOLS__: true //控制臺警告處理
})
//webpack.prod.js
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
__VUE_OPTIONS_API__: true, //控制臺警告處理
__VUE_PROD_DEVTOOLS__: false //控制臺警告處理
}),在代碼中,通過definePlugin定義變量后,通過process.env.NODE_ENV來獲取當前是開發(fā)環(huán)境還是生產(chǎn)環(huán)境。
css-loader和style-loader
css-loader的作用是將css文件轉(zhuǎn)換成webpack能夠處理的資源,而style-loader就是幫我們直接將css-loader解析后的內(nèi)容掛載到html頁面當中
asset資源模塊
webpack5 新增資源模塊(asset module),允許使用資源文件(字體,圖標等)而無需配置額外的 loader。
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
type: 'asset',
parser: {
dataUrlCondition: {
// 文件小于 10k 會轉(zhuǎn)換為 base64,大于則拷貝文件
maxSize: 10 * 1024
}
},
generator: {
filename: 'images/[base]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
type: 'asset',
generator: {
filename: 'files/[base]'
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
type: 'asset',
generator: {
filename: 'media/[base]'
}
}性能優(yōu)化
按需引入
echarts打包后占用體積過大
import * as echarts from 'echarts'//全局引入echarts
//按需引入echarts
import * as echarts from 'echarts/core'
import { BarChart } from 'echarts/charts'
import { LegendComponent, TitleComponent, TooltipComponent, GridComponent, DatasetComponent, TransformComponent } from 'echarts/components'
import { LabelLayout, UniversalTransition } from 'echarts/features'
import { CanvasRenderer } from 'echarts/renderers'
import { setTimeSecond, setTimeStr } from '@/utils/index'
import useStore from '@/stores'
// 注冊必須的組件
echarts.use([
LegendComponent,
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
BarChart,
LabelLayout,
UniversalTransition,
CanvasRenderer
])

可以看到echarts如果是全局引用的情況下,打包體積有3.67m,但按需引入后就只有1.36m了。
組件庫的按需引入
通過unplugin-vue-components/webpack插件,不會全局引入ant-design,會按需引入。
require('unplugin-vue-components/webpack')({
resolvers: [
AntDesignVueResolver({
importStyle: false
}),
YouibotPlusResolver()
],
dts: true,
dirs: ['src/components', 'src/pages'] // 按需加載的文件夾
}),分包策略
// chunk拆分,提升首屏加載速度
splitChunks: {
cacheGroups: {
vendors: {
// 提取node_modules代碼
test: /node_modules/, // 只匹配node_modules里面的模塊
name: 'vendors', // 提取文件命名為vendors,js后綴和chunkhash會自動加
minChunks: 1, // 只要使用一次就提取出來
chunks: 'initial', // 只提取初始化就能獲取到的模塊,不管異步的
minSize: 0, // 提取代碼體積大于0就提取出來
priority: 1 // 提取優(yōu)先級為1
},
commons: {
// 提取頁面公共代碼
name: 'commons', // 提取文件命名為commons
minChunks: 2, // 只要使用兩次就提取出來
chunks: 'initial', // 只提取初始化就能獲取到的模塊,不管異步的
minSize: 0 // 提取代碼體積大于0就提取出來
}
}
},
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/logo.ico" rel="external nofollow" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<script src="/config.js"></script>
<title>YOUIFLEET</title>
<link rel="icon" href="logo.ico" rel="external nofollow" />
<script defer="defer" src="js/vendors.cb5671c1aeb89321634e.js"></script>
<script defer="defer" src="js/app.cb5671c1aeb89321634e.js"></script>
<link href="css/vendors.acd8e0885f2241c62cf1.css" rel="external nofollow" rel="stylesheet" />
<link href="css/app.63706e02f684f71c27bd.css" rel="external nofollow" rel="stylesheet" />
</head>
<body>
<div id="app"></div>
<link rel="stylesheet/less" href="/color.less" rel="external nofollow" />
<script src="/less-2.7.2.min.js"></script>
</body>
</html>這里可以看到通過分包策略后,打出了兩個js文件,可以看到是defer異步執(zhí)行,不阻塞html的渲染(async也是異步的,但是并行加載,js加載好了就會執(zhí)行,如果js前后有依賴性,可能會出錯)。
多線程打包
{
test: /\.(ts|js)x?$/,
use: [
{
loader: 'thread-loader', // 開啟多進程打包
options: {
worker: 3
}
},
babelLoaderConf
],
exclude: /node_modules/
},由于運行在 Node.js 之上的 webpack 是單線程模型的,我們需要 webpack 能同一時間處理多個任務,發(fā)揮多核 CPU 電腦的威力。
thread-loader 就能實現(xiàn)多線程打包,它把任務分解給多個子進程去并發(fā)的執(zhí)行,子進程處理完后再把結果發(fā)送給主進程,來提升打包速度。
優(yōu)化策略遠不止這幾項,還有路由懶加載,組件懶加載,gzip壓縮,cdn引入第三方依賴,DllPlugin 動態(tài)鏈接庫,Web Worker,骨架屏...通過打包后的結果進行對應分析即可。
踩坑記錄
Stylelint報錯

該問題需要通過husky配置lint-staged處理,但由于我們項目前后端代碼放在一個大文件夾下內(nèi)分單獨文件夾管理,配置不了husky,所以只能暫時將stylelint-webpack-plugin給注釋掉,如果大佬有解決方案可以在評論區(qū)提一下感謝。
Vue動態(tài)路由配置component
// 生成路由數(shù)據(jù)
const generateRoute = (list: Array<IRouteData>): RouterType[] => {
const routerList: RouterType[] = []
const modules = require.context('../pages', true, /\.vue$/).keys()
/**
*
* @param { Array<IRouteData>} routers 接口返回數(shù)據(jù)
* @param {RouterType[]} routerData 生成數(shù)據(jù)存儲
*/
function generateRouter(routers: Array<IRouteData>, routerData: RouterType[] = []): void {
routers.forEach(routerItem => {
const { url, name, icon, children } = routerItem
//判斷是否存在子路由
const isRouteChildren = children && children.length && children[0].type === 0
const redirect = isRouteChildren ? children[0].url : undefined
const component =
modules.indexOf(`.${url}/index.vue`) !== -1 ? () => import(/* webpackChunkName: "[request]" */ `@/pages${url}/index.vue`) : null
const routerItemData: RouterType = {
path: url,
redirect,
name,
component,
meta: {
title: name,
icon: icon,
attribution: name
},
children: []
}
if (isRouteChildren) {
generateRouter(children, routerItemData.children)
}
routerData.push(routerItemData)
})
}
generateRouter(list, routerList)
return routerList
}這個component配置包含了血淚史,因為之前一開始component配置的時候找不到父路由的時候,我給配了子路由的component,導致后面component加載重復一直切換報錯,其實配置一個null就可以。
附錄
感謝以下大佬的文章分享
# 透過分析 webpack 面試題,構建 webpack5.x 知識體系
# 前端性能優(yōu)化——包體積壓縮82%、打包速度提升65%
# 前端性能優(yōu)化——首頁資源壓縮63%、白屏時間縮短86%
#【腳手架】從0到1搭建React18+TS4.x+Webpack5項目(一)項目初始化
到此這篇關于Vite3遷移Webpack5的實現(xiàn)的文章就介紹到這了,更多相關Vite3遷移Webpack5內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
淺談vue項目4rs vue-router上線后history模式遇到的坑
今天小編就為大家分享一篇淺談vue項目4rs vue-router上線后history模式遇到的坑,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-09-09

