已有环境
目前是有一套 webpack 的 vue2 环境,打生产包需要 2 ~ 3 分钟左右,开发启动需要 1 分钟左右。
迁移后,打生产包仅需 1 分钟,开发启动 10 秒左右。
注意,由于 vite v5 版本需要 node18 或者 node20+,所以一般只能升级到 v4,等稳定后再升 v5:从 v4 迁移
vite v4 版本只需要 node14 就可以了,我自己目前的环境是 node12,可以直接升级,不会有很多坑。
准备迁移
安装依赖
安装下面的依赖:
npm install @vitejs/plugin-vue2 vite vite-plugin-html -Dbash
修改配置文件
在项目根目录创建 vite-config
目录,然后依次创建下面的文件:
// config.ts
import { UserConfig } from 'vite'
import vue from '@vitejs/plugin-vue2'
import { createHtmlPlugin } from 'vite-plugin-html'
import { resolve } from 'path'
export const env = {
// 页面 context path
base: process.env.NODE_ENV === 'production' ? '/app' : ''
}
const config: UserConfig = {
plugins: [
vue(),
createHtmlPlugin({
entry: 'src/main.js',
template: 'index.html',
inject: {
data: {
base: env.base
}
}
})
],
publicDir: 'static',
resolve: {
alias: {
'@': resolve(__dirname, '../src'),
'~@': resolve(__dirname, '../src')
},
extensions: ['.mjs', '.js', '.ts', '.vue']
}
}
export default configtypescript
// dev.config.js
import { defineConfig } from 'vite'
import baseConfig, { env } from './config'
const PROXY_TARGET = 'https://abc.com'
export default defineConfig({
...baseConfig,
base: env.base,
define: {
'process.env': {
NODE_ENV: 'development',
BASE_API: '/app-api'
}
},
server: {
port: 1002,
proxy: {
'/app-api': {
target: PROXY_TARGET,
changeOrigin: true,
secure: false,
headers: {
host: new URL(PROXY_TARGET).host,
Referer: `${PROXY_TARGET}/app-api/`,
Origin: PROXY_TARGET
}
}
}
},
})typescript
// prod.config.ts
import { defineConfig } from 'vite'
import baseConfig, { env } from './config'
export default defineConfig({
...baseConfig,
base: env.base,
define: {
'process.env': {
NODE_ENV: 'production',
BASE_API: '/app-api'
}
},
esbuild: {
drop: ['debugger']
},
build: {
outDir: "app",
assetsDir: 'static',
cssCodeSplit: true,
emptyOutDir: true,
}
})typescript
不用多说,既然都来搞 vite 升级,肯定都能一眼看懂。
之后修改 pakcage.json 的启动配置:
{
// snip
"scripts": {
"dev": "vite --config vite-config/dev.config.ts",
"start": "npm run dev",
"build": "vite --config vite-config/prod.config.ts build",
"lint": "eslint --fix --ext .js --ext .vue src/"
},
// snip
}json
配置入口文件
主要是修改 vue 创建的方式:
new Vue({
router,
store,
i18n,
render: h => h(App)
}).$mount('#app')javascript
然后修改我们的入口 html 文件:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>App</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="ie=edge,chrome=1">
<link rel="stylesheet" type="text/css" href="/css/reset.css">
</head>
<body>
<div id="app"></div>
<script>
// 可以注入属性.
window.base = '<%- base %>'
</script>
</body>
</html>html
至此理论上就可以直接启动了,启动后就可以根据编译的报错一个一个改了,后面就讲一下我碰到的坑。
迁移碰到的坑
静态资源获取
首先 vite 专门有个静态资源目录,如果你用的是我上面的配置,那么静态资源目录就在项目根目录下的 static
目录中。
假设有这样一个文件 static/img/hello.png
,如果想要使用,直接使用根路径引用即可:
<img src="/img/hello.png">html
而不是:
<img src="/static/img/hello.png">
<img src="<context path>/static/img/hello.png">
<img src="<context path>/img/hello.png">html
上面几种写法均为错误写法!
对于第一种和第二种,不需要加上静态资源目录的名称,对于第三种,不需要加上 <context path>
,在打生产包时,如果你在配置文件中配置了 base
属性,vite 会自动给你加上!
此外下面的写法会让 vite 的自动添加路径失效:
<div style="background-image: url('/img/hello.png')"></div>plaintext
这里直接将文件路径写在了 style 中,如果在生产模式下配置了 base,就会导致生产模式无法读取到图片,对于这种写法必须要以 css
的形式写,不能用内联样式!
'require' is not defined
这里 webpack 有两种用途:
- 导入 nodejs 模块
- 使用
require.context
动态导入模块或文件
对于前者,我建议赶快把 nodejs 模块全都换掉,别想着适配了,一般这种情况经常会出现在一些加密算法的库里,直接找到一个适合前端的库替换就行了。
而对于后者,就比较麻烦了…
我的项目里是这样的一个操作:
const requireAll = requireContext => requireContext.keys().map(requireContext)
const req = require.context('./svg', false, /\.svg$/)
requireAll(req)javascript
上面的代码,会将 svg
目录下的所有文件注册为一个组件,然后放在页面上,然后使用 svg 的 use 来引用:
<svg>
<use :xlink:href="iconName"></use>
</svg>html
这里建议直接改造,将图片直接封装成组件使用:
import { defineAsyncComponent } from 'vue'
const components = {}
{
const files = import.meta.glob('./svg/*.svg', {
query: 'component'
})
for (const filesKey in files) {
// remove prefix './svg/' and suffix '.svg'
const key = filesKey.substring(6, filesKey.length - 4)
components[key] = defineAsyncComponent(files[filesKey])
}
}
const getIcon = (name) => {
const entity = components[name]
if (!entity) {
console.warn('Icon not found: ' + name)
}
return entity
}
export default getIconjavascript
这里很容易理解,就是 getIcon
方法只需要传入文件的名称就会返回一个异步组件,然后在外部直接使用就可以了。
然后另外一个问题就来了,defineAsyncComponent
是 vue3 的 API (如果报错了,请把你的 vue2 升级到 2 的最后一个版本),那么我使用也得使用 vue3 的写法 (至少我是没找到怎么用 vue2 的写法来渲染这个组件的…):
<template>
<div
:class="svgClass"
aria-hidden="true"
>
<Icon
width="100%"
height="100%"
/>
</div>
</template>
<script setup>
import getIcon from './index'
const props = defineProps({
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: undefined
}
})
const Icon = getIcon(props.iconClass)
const svgClass = props.className ? 'svg-icon ' + props.className : 'svg-icon'
</script>html
vue3 写法 eslint 报错
如果直接用 vue3 setup 写法,eslint 可能会报错,但是仍然能够通过编译并使用,这里也需要一起升级一下 eslint。
安装/更新依赖:
npm i eslint@^8 eslint-plugin-vue@^9 vue-eslint-parser@^9 -Dbash
修改 eslint 配置:
module.exports = {
root: true,
parser: 'vue-eslint-parser',
parserOptions: {
sourceType: 'module'
},
env: {
browser: true,
node: true,
es6: true
},
extends: ['eslint:recommended', 'plugin:vue/recommended'],
rules: {
// snip
}
}javascript
主要是注意 parser 那个配置,别的根据自己的需求修改。
以 base64 导入文件
虽然是个很奇葩的需求,但是还是写一下。这里要求以 base64 格式导入 src
(非静态资源目录)下的某个文件,默认情况下 vite 肯定是做不到的,这里要求我们自己定义插件:
// base64Loader.ts
import type { Plugin } from 'rollup'
import * as fs from 'fs'
const base64Loader: Plugin = {
name: 'base64-loader',
transform(_: any, id: string) {
const [path, query] = id.split('?')
if (query !== 'base64') return null
const data = fs.readFileSync(path)
const base64 = data.toString('base64')
return `export default '${base64}';`
}
}
export default base64Loadertypescript
这段代码的作用是,如果在导入一个模块时加上了 ?base
,则会以 base 格式进行导入。
然后在 vite 配置文件中配置:
// vite.config.ts
import base64Loader from './base64Loader'
const config: UserConfig = {
plugins: [
// snip
base64Loader
]
// snip
}typescript