用 vite 2 平滑升级 vue 2 + webpack 项目实战
qiyuwang 2024-10-06 12:21 42 浏览 0 评论
目录
- Vite vs. Webpack
- 完整迁移实战
Vite vs. Webpack
指标对比
经过实际运行,在同一项目中、采用几乎相同的设置,结果如下:
指标 \ 工具 | Vite | Vite(legecy) | Vue-cli + Webpack |
npm run debug 至页面可用 (ms) | 2405 | 4351 | 21418 |
npm run build 时间 (ms) | 19727 | 82277 | 61000 |
打包后的 JS 文件数量 | 22 | 45 | 46 |
平均 JS 文件体积 (kb) | 175 | 174 | 88 |
总 JS 文件体积 (kb) | 3864 | 7832 | 4080 |
开发环节区别
webpack:
- 先转译打包,然后启动 dev server
- 热更新时,把改动过模块的相关依赖模块全部编译一次
vite:
- 对于不会变动的第三方依赖,采用编译速度更快的go编写的esbuild预构建
- 对于 js/jsx/css 等源码,转译为原生 ES Module(ESM)
- 利用了现代浏览器支持 ESM,会自动向依赖的 Module 发出请求的特性
- 直接启动 dev server (不需要打包),对请求的模块按需实时编译
- 热更新时,仅让浏览器重新请求改动过的模块
目前由 webpack 或 vite 做的这些架设本地服务、静态资源打包、动态更新的工作,起码追溯到十多年前陆续都有各种解决方案了
构建环节
- 考虑到加载和缓存等,在生产环境中发布未打包的 ESM 仍然效率低下
- vite 利用成熟的 Rollup,完成 tree-shaking、懒加载和 chunk 分割等
源码浅析
运行 vite 命令后:
-> start() // packages/vite/bin/vite.js
-> 利用 cac 工具构建可以处理 dev/build/preview 等命令的 cli 实例
-> cli.parse() // packages/vite/src/node/cli.ts
复制代码
1. vite (dev 模式)
-> createServer() // packages/vite/src/node/server/index.ts
- resolveHttpServer() // 基于 http 原生模块创建服务
- createWebSocketServer() // 用 WebSocket 发送类似下面这样的热更新消息
- chokidar.watch(path.resolve(root), ...) // 监听源码变化
-> handleHMRUpdate() // 处理热更新 packages/vite/src/node/server/hmr.ts
- updateModules()
``````
ws.send({
type: 'update',
updates
})
[浏览器中 ws://localhost:8080/my-report/]
{
"type": "update",
"updates": [
{
"type": "js-update",
"timestamp": 1646797458716,
"path": "/src/app.vue",
"acceptedPath": "/src/app.vue?vue&type=template&lang.js"
}
]
}
``````
复制代码
浏览器中响应 hmr 的部分:
-> handleMessage() // packages/vite/src/client/client.ts
``````
if (update.type === 'js-update') {
queueUpdate(fetchUpdate(update))
} else {
``````
-> fetchUpdate()
``````
// 利用了浏览器的动态引入 https://github.com/tc39/proposal-dynamic-import
// 可见请求如 http://.../src/app.vue?import&t=1646797458716&vue&type=template&lang.js
const newMod = await import(
/* @vite-ignore */
base +
path.slice(1) +
`?import&t=${timestamp}${query ? `&${query}` : ''}`
)
``````
复制代码
2. vite build
-> build() // packages/vite/src/node/cli.ts
-> doBuild() // packages/vite/src/node/build.ts
- resolveConfig() // 处理 vite.config.js 和 cli 参数等配置
- prepareOutDir() // 清空打包目录等
- rollup.rollup()['write']() // 用 rollup 完成实际打包和写入工作
复制代码
迁移实践
业务背景和迁移原则
迁移背景:
- 现有项目的 webpack 开发调试和打包速度已经较慢
- 查看后台统计数据,项目的浏览器覆盖情况可以支持抛掉历史包袱
- 项目具有代表性,已经包含了 TS/JSX/FC 等写法的组件和模块
- 需要渐进迈向 vue3 技术栈
升级原则:
- 对原有开发打包流程无痛、交付产出物结构基本不变
- 保证线上产品安全,设置观察期并 兼容 webpack 流程 而非直接替换
- 覆盖后台访问记录中的主流浏览器并周知测试产品等研发环节
主要涉及文件:
- /index.html -- 新的入口,原有 src/index.html 暂时保留
- /vite.config.js -- vite 工具的配置文件
vite版本:
- vite v2.8.2
node 版本:
- node v14.19.0
- 实践表明 v14 可以兼顾新的 vite 和既有 webpack 两套流程
- 如果涉及 jenkins 等部署环节,可能需要关心相关 node 软件包的升级
package.json
依赖
"devDependencies": {
"vite": "^2.8.2",
"vite-plugin-vue2": "^1.9.3",
"vite-plugin-html": "^3.0.4",
"vite-plugin-time-reporter": "^1.0.0",
"sass": "^1.49.7",
"rollup-plugin-copy": "^3.4.0",
"@vue/compiler-sfc": "^3.2.31",
},
复制代码
npm scripts
"debug": "vite --mode dev",
"build": "vite build --mode production",
"preview": "vite preview --port 8082",
复制代码
之前的 webpack 命令加前缀(如:"webpack:build"),继续可用
node-sass
升级版本,同时满足了 webpack/vite 的打包要求
- "node-sass": "^4.9.2",
+ "node-sass": "^6.0.0",
- "sass-loader": "^7.0.3",
+ "sass-loader": "^10.0.0"
复制代码
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="shortcut icon" href="/src/assets/imgs/report.ico" />
<link rel="stylesheet" href="<%- htmlWebpackPlugin.options.navCss %>" />
<title><%- htmlWebpackPlugin.options.title %></title>
<script
type="text/javascript"
src="<%- htmlWebpackPlugin.options.navJs %>"
></script>
</head>
<body>
<div id="nav"></div>
<div id="app"></div>
<script type="module" src="/src/index.js"></script>
</body>
</html>
复制代码
- 位于根目录,vite 默认的入口
- 加入 type="module" 的入口文件 script 元素
- <%= => 语法变为 <%- ->
基础配置
复用并完善了之前的打包和开发配置文件:
// build/config.js
module.exports = {
title: '报表',
// 打包文件夹名称
base: 'my-report',
// 调试配置
debug: {
pubDir: 'dist',
assetsDir: 'assets',
host: 'localhost',
port: 8080,
navCss: '/src/assets/common2.0/scss/nav-common.css',
navJs: '/src/assets/common2.0/js/nav-common.js',
proxy: {
target: 'http://api.foo.com'
}
},
// 生产配置
prod: {
navJs: '/public/v3/js/nav-common.js',
navCss: '/public/v3/css/nav-common.css',
}
};
复制代码
vite.config.js 基本结构
import {createVuePlugin} from 'vite-plugin-vue2';
export default ({mode}) => {
const isProduction = mode === 'production';
return defineConfig({
base: `/${config.base}/`,
logLevel: 'info',
// 插件,兼容 rollup
plugins: [
// vue2 和 jsx
createVuePlugin({
jsx: true,
jsxOptions: {
compositionAPI: true
}
}),
// 打包统计
timeReporter()
],
// devServer 设置
server: {},
// 依赖解析规则等
resolve: {
alias: {}
},
// 打包目录、素材目录、rollup原生选项等
build: {}
});
};
复制代码
resolve 的迁移
之前 webpack 中的配置:
resolve: {
extensions: ['.ts', '.tsx', '.vue', '.js', '.jsx', '.json', '.css', '.scss'],
alias: {
'@': path.resolve(__dirname, '../src'),
assets: path.resolve(__dirname, '../src/assets'),
vue$: path.resolve(__dirname, '../node_modules', 'vue/dist/vue.esm.js')
},
symlinks: false
},
复制代码
vite 中的写法:
resolve: {
extensions: ['.ts', '.tsx', '.vue', '.js', '.jsx', '.json', '.css', '.scss'],
alias: [
{
find: '@',
replacement: path.resolve(__dirname, 'src')
},
{
find: 'assets',
replacement: path.resolve(__dirname, 'src', 'assets')
},
{
find: 'vue#39;,
replacement: path.resolve(__dirname, 'node_modules', 'vue/dist/vue.esm.js')
},
{
find: '~@foo/src/styles/common/publicVar',
replacement: 'node_modules/@foo/src/styles/common/_publicVar.scss'
},
{
find: '~@foo/src/styles/mixins/all',
replacement: 'node_modules/@foo/src/styles/mixins/_all.scss'
}
]
},
复制代码
以上最后两项配置属于之前引用的错误路径,vite 无法跳过,并将引起打包失败;需要修正引用或在此特殊处理
build 的迁移
之前 webpack 中的配置:
context: path.resolve(__dirname, '../'),
mode: isProduction ? 'production' : 'development',
entry: {
index: './src/index.js'
},
output: {
path: path.resolve(__dirname, '../dist', config.base),
publicPath,
filename: isProduction ? 'assets/js/[name].[contenthash:8].js' : 'assets/js/[name].[hash:8].js',
chunkFilename: isProduction
? 'assets/js/[name].[contenthash:8].chunk.js'
: 'assets/js/[name].[hash:8].chunk.js'
},
performance: {
maxEntrypointSize: 2000000,
maxAssetSize: 1000000
}
复制代码
vite 中的写法:
build: {
outDir: `${pubDir}/${config.base}`,
assetsDir,
rollupOptions: {
},
chunkSizeWarningLimit: 1000000,
cssCodeSplit: true
}
复制代码
直接拷贝的素材
- 业务中有一部分动态路径的素材图引用 <img :src="path">,path 可能为 assets/imgs/noData.png 这样的相对路径
- webpack 中用 'copy-webpack-plugin' 插件拷贝图片到发布目录下,调试过程中是可以访问到的
- vite 用拷贝插件 'rollup-plugin-copy' 同样可以拷贝成功,但调试进程中访问不了 dist 目录
import copy from 'rollup-plugin-copy';
...
// 打包时才拷贝
plugins: [
isProduction
? copy({
targets: [
{
src: path.resolve(__dirname, 'src/assets/imgs'),
dest: `${pubDir}/${config.base}/${assetsDir}`
}
],
hook: 'writeBundle'
})
: void 0,
],
// 调试过程中特殊转写
server: {
proxy: {
'/my-report/assets/imgs/': {
target: `http://${host}:${port}/`,
rewrite: path => path.replace('assets', 'src/assets')
}
},
}
复制代码
特殊的外部引用
- vite 需要用 'vite-plugin-html' 插件来达成和兼容与 'html-webpack-plugin' 一样的 html 注入效果
- 形如 '/public/v3/css/nav-common.css' 这样的特殊引用,不符合 vite 内部的保留策略,会被删除原 <link> 标签并转换成 js import,这将造成页面无法正常访问
- 结合自定义插件实现打包过程中的 hack 和打包结束后的恢复
import {createHtmlPlugin} from 'vite-plugin-html';
...
const indexReplaceHolder = '//fakePrefix';
...
plugins: [
createHtmlPlugin({
template: 'index.html',
minify: true,
inject: {
data: {
htmlWebpackPlugin: {
options: {
title: config.title,
navCss: isProduction ? indexReplaceHolder + config.prod.navCss : config.debug.navCss,
navJs: isProduction ? indexReplaceHolder + config.prod.navJs : config.debug.navJs
}
}
}
}
}),
(function() {
let viteConfig;
return {
name: 'vite-plugin-fix-index',
configResolved(resolvedConfig) {
viteConfig = resolvedConfig;
},
transformIndexHtml(code) {
if (viteConfig.command === 'build' && isProduction) {
const re = new RegExp(indexReplaceHolder, 'g');
code = code.replace(re, '');
}
return code;
}
};
})(),
],
复制代码
传统浏览器兼容
- vite 用 @vitejs/plugin-legacy 插件为打包后的文件提供传统浏览器兼容性支持
- legacy 对 build 速度影响较大,酌情采用
plugins: [
legacy({
targets: ['> 1%', 'last 2 versions', 'not ie <= 10']
}),
]
复制代码
legecy后全局 css 失效
- vue 2 中,build.cssCodeSplit: false 加上 legecy 将导致全局样式丢失等问题(gitee.com/ZhongBangKeJi)
环境变量
- process.env 的写法在 vite 中改为了 import.meta,并且使用上有差异
// src/utils/env.js
export const getEnvMode = () => {
try {
// eslint-disable-next-line
if (typeof process !== 'undefined' && process.env) {
// eslint-disable-next-line
return process.env.NODE_ENV;
}
// eslint-disable-next-line
if (import.meta && import.meta.env) {
return import.meta.env.MODE;
}
} catch (e) {
console.log(e);
}
};
复制代码
// package.json
"devDependencies": {
"@open-wc/webpack-import-meta-loader": "^0.4.7",
}
复制代码
// webpack -> module -> rules
{
test: /\.jsx?$/,
-loader: 'babel-loader',
+loaders: ['babel-loader', {loader: require.resolve('@open-wc/webpack-import-meta-loader')}],
include: [path.resolve(__dirname, '../src')]
}
复制代码
// jest.config.js -> collectCoverageFrom
[
'!<rootDir>/src/utils/env.js'
]
复制代码
// __tests__/setup.js
jest.mock('../src/utils/env.js', () => {
return {
getEnvMode: () => 'production'
};
});
复制代码
require.ensure
- 暂时没有很好的兼容写法,应尽量避免
new Set()
- 如果使用了 Map/Set 等 ES6 的类型且没有使用 polyfill,应该注意其行为
- 比如 Set 的值可能在 webpack/babel 的转写中会自动变为数组,而新的流程中需要手动用 Array.from() 处理
总结
- webpack 工作流基本可以被 vite 完整复刻,适应线上平滑升级
- 基于浏览器访问记录评估,大部分项目可以享受 vite 极速打包福利
- 对于需要兼容 IE 11 等特殊情况的,需要充分测试后,考虑用 legecy 模式迁移
- 需要注意生产环境rollup打包与开发环境的代码会不一致,最好用 preview 验证
最后
如果你觉得此文对你有一丁点帮助,点个赞。或者可以加入我的开发交流群:1025263163相互学习,我们会有专业的技术答疑解惑
如果你觉得这篇文章对你有点用的话,麻烦请给我们的开源项目点点star:http://github.crmeb.net/u/defu不胜感激 !
PHP学习手册:https://doc.crmeb.com
技术交流论坛:https://q.crmeb.com
相关推荐
- 在Word中分栏设置页码一页两个页码的技巧!
-
施老师:在正常情况下,Word文档中一页只会出现一个页码。但在某种情况下,比如说:用了分栏后,我们希望一页中出现两个页码,那应该如何实现呢?今天,就由宁双学好网施老师来为大家讲一下,利用域来实现一页两...
- 如何在关键时刻向上自荐(如何在关键时刻做出正确选择)
-
抓住机会,挺身而出有种时刻叫“关键时刻”,关键时刻,作为一个认为自己有能力的、训练有素的人,应该考虑挺身而出,甚至应该不考虑就挺身而出。...
- WPS Word:跨页的文档表格,快速调整为一页。#Excel
-
如何快速将跨页的文档表格调整为一页?需要根据两种情况分别处理。如果表格所有行的行高相同,调整为一页的方法有两种。第一种方法是将光标移动到表格内,然后将鼠标移动到表格右下角的方框处,按住鼠标左键向上拖动...
- word文档插入下一页分节符(word下一页分页符)
-
在word文档中,对文档页面进行分页是特别常见的操作,其中的下一页分节符也是用得比较多的,但是一些人不太清楚在哪里设置,也不知道它具体能实现的功能是什么。接下来看看如何在word文档中插入下一页分节符...
- word文档如何设置某一页纸张的方向
-
word文档页面方向有横向和纵向,纵向是默认的纸张方向,有时我们需要将页面设置为横向,或只设置其中某一页方向,应该怎么操作呢?一起来看看下面的详细介绍第一步:...
- word怎么单独设置一页为横向(word2019怎样设置单独一页为横向)
-
word里面其中一页可以改为横向的吗?经过实际操作发现是完全可以的。...
- Word如何设置分栏,如何一页内容同时显示一栏和两栏
-
我们使用Word文档,有时需要用到两栏的排版,甚至一页内容同时包含一栏和两栏的排版,这种格式怎么设置呢?具体步骤如下:首先是两栏排版的设置,直接点击Word文件上方工具栏【布局】,选择【分栏】下面的【...
- Word怎么分页?这三个方法可以帮到你
-
我们不仅可以利用Word编辑文档,还可以编辑文集呢。但是有时候会出现两个部分的文章长短不一,我们需要对文档进行分页处理。这样可以方便我们对文档进行其他操作。那么Word怎么分页呢?大家可以采用下面这...
- Word内容稍超一页,如何优化至单页打印?
-
如何将两页纸的内容,缩到一页打印呢?有时候一页纸多一点内容,我们完全可以缩一下,放到一页来打印。...
- [word] word 表格如何跨行显示表头、标题
-
word表格如何跨行显示表头、标题在Word中的表格如果过长的话,会跨行显示在另一页,如果想要在其它页面上也显示表头,更直观的查看数据。难道要一个个复制表头吗?当然不是,教你简单的方法操作设置Wo...
- Word表格跨页如何续上表?(word如何让表格跨页不断掉)
-
长文档的表格跨页时,你会发现页末空白太多了,这时要怎么调整?选中整张表格,右击【表格属性】,点击【行】选项,之后勾选【允许跨页断行】,点击确定即可解决空白问题。...
- Word怎么连续自动生成页码,操作步骤来了!
-
Word怎么连续自动生成页码,操作步骤来了!...
- word文档怎么把两页合并成一页内容?教你4种方法
-
word怎么把两页合并成一页?word怎么把两页合并成一页?用四种方法演示一下。·方法一:把这一个文档合并成一页,按ctrl加a全选文档,然后右键点击段落,弹出的界面行距改成固定值,磅值可以改小一点,...
- 如何将Word中的一页的纸张方向设置为横向?这里提供详细步骤
-
默认情况下,MicrosoftWord将页面定向为纵向视图。虽然这在大多数情况下都很好,但你可能拥有在横向视图中看起来更好的页面或页面组。以下是实现这一目标的两种方法。无论使用哪种方法,请注意,如果...
- Word横竖混排你会玩吗?(word横排竖排混合)
-
我们在用Word排版的时候,一般都是竖版格式,但偶尔会需要到一些特殊的版式要求,比如文档中插入的一个表格,横向的内容比较多,这时就需要用到横版,否则表格显示不全。这种横竖版混排的要求,在Word20...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- navicat无法连接mysql服务器 (65)
- 下横线怎么打 (71)
- flash插件怎么安装 (60)
- lol体验服怎么进 (66)
- ae插件怎么安装 (62)
- yum卸载 (75)
- .key文件 (63)
- cad一打开就致命错误是怎么回事 (61)
- rpm文件怎么安装 (66)
- linux取消挂载 (81)
- ie代理配置错误 (61)
- ajax error (67)
- centos7 重启网络 (67)
- centos6下载 (58)
- mysql 外网访问权限 (69)
- centos查看内核版本 (61)
- ps错误16 (66)
- nodejs读取json文件 (64)
- centos7 1810 (59)
- 加载com加载项时运行错误 (67)
- php打乱数组顺序 (68)
- cad安装失败怎么解决 (58)
- 因文件头错误而不能打开怎么解决 (68)
- js判断字符串为空 (62)
- centos查看端口 (64)