Vite3迁移Webpack5的实现
作者:鱿鱼须不懂Vue
为什么要做迁移
现有问题
1、按需加载页面的时候加载速度慢。
2、热更新时常失效,手动刷新加载速度也慢。
性能提升
1、旧框架启动约13秒,但启动后每个页面切换加载都得等待5-10s,开发体验较差。新框架项目启动加所有页面加载约65秒。利用webpack5
缓存的新特性,启动速度变快的同时,带来更好的开发体验。
2、旧框架在jenkins
打包需要2分52秒,新框架打包仅需1分48秒,速度提升了37%。
webpack5为什么快
webpack5
较于 webpack4
,新增了持久化缓存、改进缓存算法等优化,通过配置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 中文件过大和因为文件大网络请求超时的情况。 "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"// 安装到dependencies
webpack5配置
为了区分开发环境和打包环境,分了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 中文件过大和因为文件大网络请求超时的情况。 const { AntDesignVueResolver } = require('unplugin-vue-components/resolvers') const isDev = process.env.NODE_ENV === 'development' // 是否是开发模式 module.exports = { entry: { app: resolve('src/main.ts') }, resolve: { //引入模块时不带扩展名,会按照配置的数组从左到右的顺序去尝试解析模块 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之后已经有了解析 typescript 的能力,也就不再需要 ts-loader { test: /\.(ts|js)x?$/, use: [ { loader: 'thread-loader', // 开启多进程打包 options: { worker: 3 } }, babelLoaderConf ], exclude: /node_modules/ }, { test: /\.css$/, use: [ // 开发环境使用style-looader(通过动态添加 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 内部集成了 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 会转换为 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] // 针对构建的额外代码依赖的数组对象。webpack 将使用这些项和所有依赖项的哈希值来使文件系统缓存失效。 }, 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 = 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 中文件过大和因为文件大网络请求超时的情况。 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 // 提取优先级为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的话,生产环境下也可以定位到具体代码,但是相应的打包文件也会变大(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({ //在业务代码中也可以访问process变量区分环境 '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 { // 获得网络接口列表 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转码后会生成很多helper函数(可能有大量重复),polyfill会在全局变量上挂载目标浏览器缺失的功能,这个插件的作用是将 helper 和 polyfill 都改为从一个统一的地方引入,并且引入的对象和全局变量是完全隔离的 //Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API(polify实现) '@babel/plugin-transform-runtime', { corejs: 3 } ] ] } }
知识点
环境区分
// 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环境下需要cross-env这个依赖帮助我们node环境下做变量标识,通过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
来获取当前是开发环境还是生产环境。
css-loader和style-loader
css-loader
的作用是将css文件转换成webpack能够处理的资源,而style-loader
就是帮我们直接将css-loader
解析后的内容挂载到html页面当中
asset资源模块
webpack5 新增资源模块(asset module
),允许使用资源文件(字体,图标等)而无需配置额外的 loader。
{ test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, type: 'asset', parser: { dataUrlCondition: { // 文件小于 10k 会转换为 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]' } }
性能优化
按需引入
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 // 提取优先级为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
异步执行,不阻塞html的渲染(async
也是异步的,但是并行加载,js加载好了就会执行,如果js前后有依赖性,可能会出错)。
多线程打包
{ test: /\.(ts|js)x?$/, use: [ { loader: 'thread-loader', // 开启多进程打包 options: { worker: 3 } }, babelLoaderConf ], exclude: /node_modules/ },
由于运行在 Node.js 之上的 webpack 是单线程模型的,我们需要 webpack 能同一时间处理多个任务,发挥多核 CPU 电脑的威力。
thread-loader
就能实现多线程打包,它把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程,来提升打包速度。
优化策略远不止这几项,还有路由懒加载
,组件懒加载
,gzip压缩
,cdn引入第三方依赖
,DllPlugin 动态链接库
,Web Worker
,骨架屏
...通过打包后的结果进行对应分析即可。
踩坑记录
Stylelint报错
该问题需要通过husky
配置lint-staged
处理,但由于我们项目前后端代码放在一个大文件夹下内分单独文件夹管理,配置不了husky
,所以只能暂时将stylelint-webpack-plugin
给注释掉,如果大佬有解决方案可以在评论区提一下感谢。
Vue动态路由配置component
// 生成路由数据 const generateRoute = (list: Array<IRouteData>): RouterType[] => { const routerList: RouterType[] = [] const modules = require.context('../pages', true, /\.vue$/).keys() /** * * @param { Array<IRouteData>} routers 接口返回数据 * @param {RouterType[]} routerData 生成数据存储 */ 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 知识体系
#【脚手架】从0到1搭建React18+TS4.x+Webpack5项目(一)项目初始化
到此这篇关于Vite3迁移Webpack5的实现的文章就介绍到这了,更多相关Vite3迁移Webpack5内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!