0%

vue2.x老项目vue-cli升级vite

由于vue2.x的老项目越来越庞大,导致每次启动的时候要等差不多一分钟的时间,vite4不是都出来了嘛,所以就在琢磨如何将老项目升级到vite,秒启动项目。

升级vite首先要分析的是vue-cli和vite的差异有哪些地方。

vue-cli和vite的差异

依赖引入方式

vite中统一采用import的方式引入依赖和资源,vue-cli采用的webpack,故可能会有代码用require去引入依赖,这部分就需要处理一下。

index.html的处理

在vue-cli中,项目入口文件一般是放在public文件夹中,并且解析html采用的是webpack的html-webpack-plugin
插件来进行的,vite默认是不用插件处理html文件的。所以html-webpack-plugin中的一些占位符语法就不能在vite中出现,比如:

1
<link rel="icon" href="<%= BASE_URL %>favicon-alone.png">

这种代码vite不用插件就无法识别,会报错。

.vue文件引入路径

在vue-cli中,引入vue文件不需要写全.vue的后缀名,但是vite默认是需要后缀名的,虽然可以通过配置文件省略掉,但是建议还是补全后缀名,这对vscode解析vue代码起到帮助作用。

对环境变量的命名规范和使用

vue-cli和vite环境变量都可以采用.env文件去存储,但是在变量的命名方式上有很大差异。比如vue-cli是VUE_APP_前缀的环境变量,vite则是采用VITE_的变量前缀。

环境变量使用方面,vue-cli是采用process.env.xxx来使用;vite则是采用import.meta.env.xxx来使用。

解决方案

require依赖引入方式

处理required引入的方式,可以采用vite插件来完成,安装@originjs/vite-plugin-commonjs

1
pnpm add @originjs/vite-plugin-commonjs -D

vite.config.js

1
2
3
4
5
6
7
import { viteCommonjs } from '@originjs/vite-plugin-commonjs'

...
plugins: [
viteCommonjs()
]
...

index.html的处理

将原项目中public/index.html移动到根目录,安装vite-plugin-html并进行相应的配置即可。

1
pnpm add vite-plugin-html -D
1
2
3
4
5
6
7
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><%- title %></title>
<%- injectScript %>
</head>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { defineConfig, Plugin } from 'vite'
import vue from '@vitejs/plugin-vue'

import { createHtmlPlugin } from 'vite-plugin-html'

export default defineConfig({
plugins: [
vue(),
createHtmlPlugin({
minify: true,
/**
* After writing entry here, you will not need to add script tags in `index.html`, the original tags need to be deleted
* @default src/main.ts
*/
entry: 'src/main.ts',
/**
* If you want to store `index.html` in the specified folder, you can modify it, otherwise no configuration is required
* @default index.html
*/
template: 'public/index.html',

/**
* Data that needs to be injected into the index.html ejs template
*/
inject: {
data: {
title: 'index',
injectScript: `<script src="./inject.js"></script>`,
},
tags: [
{
injectTo: 'body-prepend',
tag: 'div',
attrs: {
id: 'tag',
},
},
],
},
}),
],
})

处理.vue文件引入路径

可以写个nodejs脚本去处理这种问题,不然人工一个个去改可太费时费力了。主要思路说一下,首先如何识别到vue引入路径的代码呢。其实核心思想就是利用工具去讲代码解析成AST树,然后再进行相应的替换操作。

项目下可能会出现引入vue文件的代码,只会在.js和.vue文件中,那么对这两种文件进行解析即可。

解析.js文件可以利用@babel/core这个包,解析.vue文件可以用@vue/compiler-sfc这个包。注意,@vue/compiler-sfc只能把vue文件中的script代码搞出来,最后还是得用babel去解析成AST。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import fs from 'fs-extra'
import { parseAsync } from '@babel/core'
import { parse } from '@vue/compiler-sfc'
import path from 'path'
// 这个是你的项目的完整路径下的src
const base = `/Users/xxx/xxxx/src`

async function addFix(fileurl) {
let content = fs.readFileSync(fileurl, 'utf-8')

const replaceAt = string => string.replace(/^@(.*)/, `${base}$1`)

let c, d, importNodes;
if(fileurl.endsWith('.vue')) {
c = parse(content)
const script = c.descriptor.script || c.descriptor.scriptSetup
if(!script) return
d = await parseAsync(script.content)
importNodes = d.program.body.filter(({ type }) => type === 'ImportDeclaration')
.map(v => v.source.value)
} else if(fileurl.endsWith('.js')) {
d = await parseAsync(content)
importNodes = d.program.body
.filter(({type}) => type === 'VariableDeclaration')
.map(({declarations}) => declarations[0])
.filter(v => v.init.body?.callee?.type === 'Import')
.map(v => (v.init.body.arguments[0].value))
.concat(
d.program.body
.filter(({type}) => type === 'ImportDeclaration')
.map(({source}) => source.value)
)
} else return
importNodes.map(v => ({source: v, rep: readFile(replaceAt(v), v, fileurl)}))
.forEach(v => {
if(v.rep) {
content = content.replace(v.source, v.rep)
}
})
fs.writeFileSync(fileurl, content, 'utf-8')
}

/**
*
* @param {string} p
*/
function loop(p) {
if(fs.lstatSync(p).isDirectory()) {
fs.readdirSync(p).forEach(v => {
loop(path.resolve(p, v))
})
} else {
addFix(p)
}
}
loop(base)

/**
*
* @param {string} url
* @param {string} p
*/
function readFile(url, p, fileurl, flag = false) {
if(url.endsWith('.vue')) return false
if(fs.pathExistsSync(`${url}.js`)) return false
if(fs.pathExistsSync(`${url}.vue`)) return `${p}.vue`
if(fs.pathExistsSync(url) && fs.lstatSync(url).isDirectory() && fs.pathExistsSync(`${url}/index.vue`)) return `${p}/index.vue`
if(p.startsWith('./') && !flag) {
const u = path.resolve(fileurl, '../', p)
return readFile(u, p, fileurl, true)
}
return false
}

处理环境变量

处理环境变量就比补全.vue后缀简单一些了,因为无需解析AST,直接文字硬匹配替换就好了。

此外,不一定要替换环境变量前缀,vite现在支持自定义环境变量前缀了,通过配置envPrifix
来实现。

1
envPrifix: 'VUE_APP_'

唯一需要处理的就是使用环境变量的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import fs from 'fs-extra'
import path from 'path'

// 这个是你的项目的完整路径下的src
const base = `/Users/xxx/xxxx/src`
/**
*
* @param {string} fileurl
*/
function replace(fileurl) {
const prefix = path.extname(fileurl)
if(!['.vue', '.js'].includes(prefix)) return
let content = fs.readFileSync(fileurl, 'utf-8')
const reg1 = /process\.env\.NODE_ENV (===|==) ('|")development('|")/g
const reg2 = /process\.env\.NODE_ENV !(=|==) ('|")production('|")/g
const reg3 = /process\.env\.NODE_ENV (===|==) ('|")production('|")/g
const reg4 = /process\.env\.NODE_ENV !(=|==) ('|")development('|")/g
const reg5 = /process\.env\./g

content = content.replace(reg1, 'import.meta.env.DEV')
content = content.replace(reg2, 'import.meta.env.DEV')
content = content.replace(reg3, 'import.meta.env.PROD')
content = content.replace(reg4, 'import.meta.env.PROD')
content = content.replace(reg5, 'import.meta.env.')

fs.writeFileSync(fileurl, content, 'utf-8')
}
/**
*
* @param {string} p
*/
function loop(p) {
if(fs.lstatSync(p).isDirectory()) {
fs.readdirSync(p).forEach(v => {
loop(path.resolve(p, v))
})
} else {
replace(p)
}
}
loop(path.resolve(base, 'src'))

完整更改后的vite.config.js配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue2'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
import { viteCommonjs } from '@originjs/vite-plugin-commonjs'
import babel from 'vite-plugin-babel'
import dns from 'dns'

dns.setDefaultResultOrder('verbatim')
export default defineConfig({
plugins: [
vue(),
viteCommonjs(),
babel({
filter: /^.*node_modules\/element-ui.*\.js?$/,
}),
nodePolyfills({
// Whether to polyfill `node:` protocol imports.
protocolImports: true,
}),
],
base: './',
resolve: {
extensions: ['.js', '.vue'],
alias: [{ find: /^@\/(.*)$/, replacement: '/src/$1' }],
},
})

关于proxy代理的配置,跟vue-cli的配置语法类似,可以直接抄过来。