likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 15

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

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

vue-cli和vite的差异

依赖引入方式

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

index.html的处理

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

<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来使用。

解决方案

required依赖引入方式

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

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

vite.config.js

import { viteCommonjs } from '@originjs/vite-plugin-commonjs'

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

index.html的处理

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

pnpm add vite-plugin-html -D
<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>
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。具体代码如下:

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 来实现。

envPrifix: 'VUE_APP_'

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

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配置

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的配置语法类似,可以直接抄过来。