Vite3遷移Webpack5的實(shí)現(xiàn)
為什么要做遷移
現(xiàn)有問題
1、按需加載頁面的時(shí)候加載速度慢。
2、熱更新時(shí)常失效,手動(dòng)刷新加載速度也慢。
性能提升
1、舊框架啟動(dòng)約13秒,但啟動(dòng)后每個(gè)頁面切換加載都得等待5-10s,開發(fā)體驗(yàn)較差。新框架項(xiàng)目啟動(dòng)加所有頁面加載約65秒。利用webpack5
緩存的新特性,啟動(dòng)速度變快的同時(shí),帶來更好的開發(fā)體驗(yàn)。
2、舊框架在jenkins
打包需要2分52秒,新框架打包僅需1分48秒,速度提升了37%。
webpack5為什么快
webpack5
較于 webpack4
,新增了持久化緩存、改進(jìn)緩存算法等優(yōu)化,通過配置webpack 持久化緩存,來緩存生成的 webpack
模塊和 chunk
,改善下一次打包的構(gòu)建速度,可提速 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",// 對(duì)CSS文件進(jìn)行壓縮 "mini-css-extract-plugin": "2.4.5",// 將CSS文件抽取出來配置, 防止將樣式打包在 js 中文件過大和因?yàn)槲募缶W(wǎng)絡(luò)請(qǐng)求超時(shí)的情況。 "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:// 安裝到dependencies
webpack5配置
為了區(qū)分開發(fā)環(huán)境和打包環(huán)境,分了3個(gè)js文件(base、dev、prod),通過webpack-merge
這個(gè)插件,進(jìn)行合并操作。
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 中文件過大和因?yàn)槲募缶W(wǎng)絡(luò)請(qǐng)求超時(shí)的情況。 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í)不帶擴(kuò)展名,會(huì)按照配置的數(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', // 開啟多進(jìn)程打包 options: { worker: 3 } }, babelLoaderConf ], exclude: /node_modules/ }, { test: /\.css$/, use: [ // 開發(fā)環(huán)境使用style-looader(通過動(dòng)態(tài)添加 style 標(biāo)簽的方式,將樣式引入頁面),打包模式抽離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 會(huì)轉(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(), //將打包后的文件自動(dòng)引入到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: '/' }, // 日志打印只打印錯(cuò)誤和警告 stats: 'errors-warnings', devServer: { host: getNetworkIp(), port: 8094, // 端口號(hào) open: true, // 自動(dòng)打開 hot: true, // 熱更新 allowedHosts: 'all', client: { progress: false, // 將運(yùn)行進(jìn)度輸出到控制臺(tái)。 overlay: { warnings: false, errors: true } // 全屏顯示錯(cuò)誤信息 } }, plugins: [ new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('development'), __VUE_OPTIONS_API__: true, //控制臺(tái)警告處理 __VUE_PROD_DEVTOOLS__: true //控制臺(tái)警告處理 }) ], // 緩存 cache: { type: 'filesystem', buildDependencies: { config: [__filename] // 針對(duì)構(gòu)建的額外代碼依賴的數(shù)組對(duì)象。webpack 將使用這些項(xiàng)和所有依賴項(xiàng)的哈希值來使文件系統(tǒng)緩存失效。 }, cacheDirectory: resolve('temp_cache'), name: 'scf-cache', // 路徑temp_cache/scf-cache compression: 'gzip' } }) devWebpackConfig.plugins.push( // 進(jìn)度條 new ProgressBarPlugin({ format: ` :msg [:bar] ${chalk.green.bold(':percent')} (:elapsed s)`, clear: true }), // 錯(cuò)誤提示 new FriendlyErrorsWebpackPlugin({ // 成功的時(shí)候輸出 compilationSuccessInfo: { messages: [`Your application is running here: http://${devWebpackConfig.devServer.host}:${devWebpackConfig.devServer.port}`] }, // 是否每次都清空控制臺(tái) clearConsole: true }) ) module.exports = devWebpackConfig
webpack.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 中文件過大和因?yàn)槲募缶W(wǎng)絡(luò)請(qǐng)求超時(shí)的情況。 const CssMinimizerPlugin = require('css-minimizer-webpack-plugin') // 對(duì)CSS文件進(jìn)行壓縮 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會(huì)自動(dòng)加 minChunks: 1, // 只要使用一次就提取出來 chunks: 'initial', // 只提取初始化就能獲取到的模塊,不管異步的 minSize: 0, // 提取代碼體積大于0就提取出來 priority: 1 // 提取優(yōu)先級(jí)為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)境下也可以定位到具體代碼,但是相應(yīng)的打包文件也會(huì)變大(map文件體積,4m變成17m),而且會(huì)有代碼暴露的風(fēng)險(xiǎn)。 plugins: [ // 清空dist new CleanWebpackPlugin(), new CopyPlugin({ patterns: [ { from: path.resolve(__dirname, '../public'), // 復(fù)制public下文件 to: path.resolve(__dirname, '../dist'), // 復(fù)制到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è)務(wù)代碼中也可以訪問process變量區(qū)分環(huán)境 'process.env.NODE_ENV': JSON.stringify('production'), __VUE_OPTIONS_API__: true, //控制臺(tái)警告處理 __VUE_PROD_DEVTOOLS__: false //控制臺(tái)警告處理 }), ...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)絡(luò)接口列表 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 // 支持所有文件擴(kuò)展名,否則在vue文件中使用ts會(huì)報(bào)錯(cuò) } ] ], plugins: [ [ //js文件在babel轉(zhuǎn)碼后會(huì)生成很多helper函數(shù)(可能有大量重復(fù)),polyfill會(huì)在全局變量上掛載目標(biāo)瀏覽器缺失的功能,這個(gè)插件的作用是將 helper 和 polyfill 都改為從一個(gè)統(tǒng)一的地方引入,并且引入的對(duì)象和全局變量是完全隔離的 //Babel 默認(rèn)只轉(zhuǎn)換新的 JavaScript 句法(syntax),而不轉(zhuǎn)換新的 API(polify實(shí)現(xiàn)) '@babel/plugin-transform-runtime', { corejs: 3 } ] ] } }
知識(shí)點(diǎn)
環(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這個(gè)依賴幫助我們node環(huán)境下做變量標(biāo)識(shí),通過NODE_ENV
進(jìn)行聲明即可。
//webpack.dev.js new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('development'), __VUE_OPTIONS_API__: true, //控制臺(tái)警告處理 __VUE_PROD_DEVTOOLS__: true //控制臺(tái)警告處理 }) //webpack.prod.js new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify('production'), __VUE_OPTIONS_API__: true, //控制臺(tái)警告處理 __VUE_PROD_DEVTOOLS__: false //控制臺(tái)警告處理 }),
在代碼中,通過definePlugin
定義變量后,通過process.env.NODE_ENV
來獲取當(dāng)前是開發(fā)環(huán)境還是生產(chǎn)環(huán)境。
css-loader和style-loader
css-loader
的作用是將css文件轉(zhuǎn)換成webpack能夠處理的資源,而style-loader
就是幫我們直接將css-loader
解析后的內(nèi)容掛載到html頁面當(dāng)中
asset資源模塊
webpack5 新增資源模塊(asset module
),允許使用資源文件(字體,圖標(biāo)等)而無需配置額外的 loader。
{ test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, type: 'asset', parser: { dataUrlCondition: { // 文件小于 10k 會(huì)轉(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' // 注冊(cè)必須的組件 echarts.use([ LegendComponent, TitleComponent, TooltipComponent, GridComponent, DatasetComponent, TransformComponent, BarChart, LabelLayout, UniversalTransition, CanvasRenderer ])
可以看到echarts如果是全局引用的情況下,打包體積有3.67m,但按需引入后就只有1.36m了。
組件庫的按需引入
通過unplugin-vue-components/webpack
插件,不會(huì)全局引入ant-design
,會(huì)按需引入。
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會(huì)自動(dòng)加 minChunks: 1, // 只要使用一次就提取出來 chunks: 'initial', // 只提取初始化就能獲取到的模塊,不管異步的 minSize: 0, // 提取代碼體積大于0就提取出來 priority: 1 // 提取優(yōu)先級(jí)為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>
這里可以看到通過分包策略后,打出了兩個(gè)js文件,可以看到是defer
異步執(zhí)行,不阻塞html的渲染(async
也是異步的,但是并行加載,js加載好了就會(huì)執(zhí)行,如果js前后有依賴性,可能會(huì)出錯(cuò))。
多線程打包
{ test: /\.(ts|js)x?$/, use: [ { loader: 'thread-loader', // 開啟多進(jìn)程打包 options: { worker: 3 } }, babelLoaderConf ], exclude: /node_modules/ },
由于運(yùn)行在 Node.js 之上的 webpack 是單線程模型的,我們需要 webpack 能同一時(shí)間處理多個(gè)任務(wù),發(fā)揮多核 CPU 電腦的威力。
thread-loader
就能實(shí)現(xiàn)多線程打包,它把任務(wù)分解給多個(gè)子進(jìn)程去并發(fā)的執(zhí)行,子進(jìn)程處理完后再把結(jié)果發(fā)送給主進(jìn)程,來提升打包速度。
優(yōu)化策略遠(yuǎn)不止這幾項(xiàng),還有路由懶加載
,組件懶加載
,gzip壓縮
,cdn引入第三方依賴
,DllPlugin 動(dòng)態(tài)鏈接庫
,Web Worker
,骨架屏
...通過打包后的結(jié)果進(jìn)行對(duì)應(yīng)分析即可。
踩坑記錄
Stylelint報(bào)錯(cuò)
該問題需要通過husky
配置lint-staged
處理,但由于我們項(xiàng)目前后端代碼放在一個(gè)大文件夾下內(nèi)分單獨(dú)文件夾管理,配置不了husky
,所以只能暫時(shí)將stylelint-webpack-plugin
給注釋掉,如果大佬有解決方案可以在評(píng)論區(qū)提一下感謝。
Vue動(dòng)態(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 接口返回?cái)?shù)據(jù) * @param {RouterType[]} routerData 生成數(shù)據(jù)存儲(chǔ) */ 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 }
這個(gè)component
配置包含了血淚史,因?yàn)橹耙婚_始component
配置的時(shí)候找不到父路由的時(shí)候,我給配了子路由的component
,導(dǎo)致后面component
加載重復(fù)一直切換報(bào)錯(cuò),其實(shí)配置一個(gè)null就可以。
附錄
感謝以下大佬的文章分享
# 透過分析 webpack 面試題,構(gòu)建 webpack5.x 知識(shí)體系
# 前端性能優(yōu)化——包體積壓縮82%、打包速度提升65%
# 前端性能優(yōu)化——首頁資源壓縮63%、白屏?xí)r間縮短86%
#【腳手架】從0到1搭建React18+TS4.x+Webpack5項(xiàng)目(一)項(xiàng)目初始化
到此這篇關(guān)于Vite3遷移Webpack5的實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Vite3遷移Webpack5內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue中實(shí)現(xiàn)滾動(dòng)加載與無限滾動(dòng)
本文主要介紹了Vue中實(shí)現(xiàn)滾動(dòng)加載與無限滾動(dòng),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-06-06淺談vue項(xiàng)目4rs vue-router上線后history模式遇到的坑
今天小編就為大家分享一篇淺談vue項(xiàng)目4rs vue-router上線后history模式遇到的坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-09-09vue 請(qǐng)求后臺(tái)數(shù)據(jù)的實(shí)例代碼
本篇文章主要介紹了vue 請(qǐng)求后臺(tái)數(shù)據(jù)的實(shí)例代碼,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧、2017-06-06