webpack2的vuejs老项目迁移到vite2.0的记录

最近 vite2 非常火,它基于浏览器原生ES模块加载的现代化构建工具,主要由两部分组成:

  • 一个开发服务器,它利用 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。
  • 一套构建指令,它使用 Rollup 打包你的代码,预配置输出高度优化的静态资源用于生产。

为什么选用vite?

随着我们开始构建越来越多的雄心勃勃的应用程序,我们处理的 JavaScript 数量也呈指数级增长。大型项目包含数千个模块的情况并不少见。我们开始遇到基于 JavaScript 的工具的性能瓶颈:通常需要很长时间(有时甚至是几分钟!)才能启动开发服务器,即使使用 HMR,文件编辑也需要几秒钟才能在浏览器中反映出来。缓慢的反馈会极大地影响开发人员的生产力和幸福感。
Vite 旨在利用生态系统中的新进展解决上述问题:浏览器支持原生模块,越来越多 JavaScript 工具使用编译型语言编写。

项目背景

公司内部OA系统,基于vue2+webpack2开发,目前页面有上百个,每次启动开发或者编译都需要至少5分钟,非常慢。由于 vite 官方没有原生支持vue2.0,需要依赖于第三方插件,而且对于编译的稳定性和风险没办法保证,因此本次引入vite优先保证本地开发服务器的运行,尽量避免修改代码,编译还是由原来的webpack来执行。

引入vite

首先安装 vitevite-plugin-vue2

$ npm i vite vite-plugin-vue2 sass --save-dev

新建配置文件 vite.config.js

import {defineConfig} from 'vite'
import {createVuePlugin} from 'vite-plugin-vue2'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    createVuePlugin({
      jsx: true,
    }),
  ],
  resolve: {
    extensions: ['.vue', '.js', '.json'],
    alias: [
      {find: "@", replacement: path.resolve(__dirname, './src')},
    ],
  },
  server: {
    proxy: {}, // 原本项目的后端接口代理
    base: '/index-vite.html', // 保留原本的 index.html,新建一个 index-vite.html
    open: '/index-vite.html'
  },
})

index-vite.html 并增加入口

<!DOCTYPE html>
<html>
    <head>
        <!-- 页面字符集 -->
        <meta charset="utf-8" />
        <!-- 360标签,页面需默认用极速核-->
        <meta name="renderer" content="webkit" />
        <!-- 禁止移动端点击输入框自动放大-->
        <!-- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" /> -->
        <!-- 使用Chrome内核来做渲染-->
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
        <!-- 网页标题-->
        <title>document</title>
    </head>
    <body>
    <div id="app"></div>
    <!-- 入口 -->
    <script type="module" src="./src/index.js"></script>
    </body>
</html>

为了尽量不对源代码进行修改,保证能正常编译项目,因此对代码的修改使用 vite-plugin transform 钩子来实现,能够在引入代码之前对代码进行修改。

参照项目中的旧代码,我们的插件主要需要提供以下几种对代码的转换:

  • module.exports -> export default
  • 懒加载require转成 import('...')
  • 代码中使用 require(...),需替换成临时变量 script_inject_var__0,然后在脚本作用域顶部引入变量:import script_inject_var__0 from '...'
  • template中引入的require资源,例如 <img :src="require(...)" -> <img :src="tpl_inject_var__1"、在 script 块中 import tpl_inject_var__1 from '...',然后在组件计算属性/data属性中引入 computed: {tpl_inject_var__1}
  • require.context 转换成 import.meta.globEager

首先创建 ./vite/plugins/replace.js

import path from 'path'
import url from 'url'

const dirname = path.dirname(url.fileURLToPath(import.meta.url))
const srcDir = path.resolve(dirname, '../../src')
const enterDir = path.resolve(dirname, '../../enter')

function getPath(filepath) {
  let sps = filepath.split('?', 2)
  return sps[0]
}

function getExtension(filepath) {
  return path.extname(getPath(filepath))
}

function replaceCode(code, filename, ext) {
  let n = 0
  let script, template, style
  if (ext === '.vue') {
    try {
      template = code.match(/<template(?:.*?)>(?<template>.+)<\/template>/is).groups.template
    } catch (ignore) {
    }
    try {
      script = code.match(/<script(?:.*?)>(?<script>.+)<\/script>/is).groups.script
    } catch (ignore) {
    }
    try {
      style = code.match(/<style(?:.*?)>(?<style>.+)<\/style>/is).groups.style
    } catch (ignore) {
    }
  } else if (ext === '.js') {
    script = code
  } else {
    return {n, code}
  }

  let tplInjectVar = []

  if (template) {
    template = template.replace(/require\((.+?)\)/igm, function (_, match) {
      n++
      tplInjectVar.push(match)
      return `tpl_inject_var__${tplInjectVar.length - 1}`
    })
  }

  if (style) {
    style = style.replace(/\/deep\//ig, function (match) {
      n++
      return '::v-deep'
    })
  }

  let scriptInjectVar = []
  if (script) {
    // 替换 resolve => require(['@    resolve => require(['@/guide/guidePage.vue'], resolve)
    //  () => import('../src/guide/guidePage.vue'),
    script = script.replace(/resolve\s*?=>\s*?require\(\[['"`]@(\/.*?)(["'`])],\s*?resolve\s*?\)/img, function (matched, m1, m2) {
      n++
      return `() => import(${m2}../src${m1}${m2})`
    })

    // 替换 resolve => require([
    script = script.replace(/resolve\s*?=>\s*?require\(\[(['"`]@\/.*?["'`])],\s*?resolve\s*?\)/img, function (matched, m1) {
      n++
      return `() => import(${m1})`
    })

    script = script.replace(/require\((.+?)\)(.default)?/img, function (_, match) {
      n++
      scriptInjectVar.push(match)
      return `script_inject_var__${scriptInjectVar.length - 1}`
    })

    scriptInjectVar.forEach((vvar, i) => {
      n++
      script = `import script_inject_var__${i} from ${vvar}\n${script}`
    })

    // 替换 module.exports =
    script = script.replace(/module\.exports\s*?=/, function () {
      n++
      return 'export default '
    })
  }

  if (ext === '.js') {
    code = script
    return {n, code}
  }

  if (ext === '.vue') {
    tplInjectVar.forEach((vvar, i) => {
      n++
      script = `import tpl_inject_var__${i} from ${vvar}\n${script}`
    })

    if (tplInjectVar.length) { // 添加计算属性
      let computedN = 0
      script = script.replace(/(computed\s*?:\s*?){/is, function (match) {
        n++
        computedN++
        let vvars = ''
        tplInjectVar.forEach((vvar, i) => {
          vvars += `tpl_inject_var__${i},`
        })
        return `${match}${vvars}`
      })
      if (!computedN) { // 没有计算属性,就加一个
        script = script.replace(/\s*?export\s+?default\s+?{/is, function (match) {
          n++
          computedN++
          let vvars = ''
          tplInjectVar.forEach((vvar, i) => {
            vvars += `tpl_inject_var__${i},`
          })
          return `${match}\ncomputed:{${vvars}},`
        })
      }
      if (!computedN) {
        console.warn('没有注入 computed!!')
      }
    }

    let ret = code
    if (template) {
      ret = ret.replace(/(<template(?:.*?)>)(.+)(<\/template>)/is, function (_, m1, m2, m3) {
        return `${m1}${template}${m3}`
      })
    }
    if (script) {
      ret = ret.replace(/(<script(?:.*?)>)(.+)(<\/script>)/is, function (_, m1, m2, m3) {
        return `${m1}${script}${m3}`
      })
    }
    if (style) {
      ret = ret.replace(/(<style(?:.*?)>)(.+)(<\/style>)/is, function (_, m1, m2, m3) {
        return `${m1}${style}${m3}`
      })
    }

    return {n, code: ret}
  }
}

export default function replacement() {
  return {
    enforce: 'pre',
    transform(code, rawId) {
     // 由于就这一个地方,因此就直接用路径判断,直接返回源码,就不做正则匹配替换了
      if (rawId.endsWith('/views/liveMonitor/icons/index.js')) {
        return {
          code: `
import Vue from 'vue'
import SvgIcon from '../components/SvgIcon'// svg component

// register globally
Vue.component('svg-icon', SvgIcon)

let all = import.meta.globEager('./svg/*.svg')
`,
          map: null,
        }
      }
      let filepath = path.resolve(getPath(rawId))
      let ext = getExtension(getPath(rawId))

      if ((filepath.startsWith(srcDir) || filepath.startsWith(enterDir)) && ['.vue', '.js'].includes(ext)) {
        let ret = replaceCode(code, filepath, ext)
        if (ret.n === 0) {
          return null
        }
        return {
          code: ret.code,
          map: null,
        }
      }
      return null
    }
  }
}

vite.config.js 中引入插件

import replacement from './vite/plugins/replacement.js'
...
export default defineConfig({
  plugins: [
    replacement(),
    createVuePlugin({
      jsx: true,
    }),
  ],
  ...

在 package.json scripts 中新增两项

    "vite-dev": "vite"

至此可以启动 vite 了。

npm run vite-dev

总结

以上就是为项目引入 vite2,并且对原本代码0修改的过程记录。这里仅做抛砖引玉,读者可以自定义插件来对代码进行替换,对其他不兼容的语法打补丁。

本作品采用《CC 协议》,转载必须注明作者和本文链接
wojianishanghaojiugoujia
讨论数量: 2

我按步骤操作后,elementUI的图表样式没有显示

2年前 评论
wojianishanghaojiugoujia (楼主) 2年前

github.com/originjs/webpack-to-vit...

这是我对旧项目进行转换时,搜索错误信息发现的一个 github 项目,它列出了一些转换项和错误修复方法,甚至可以一键转换旧项目到 vite 项目,非常棒,推荐一下!

2年前 评论
wojianishanghaojiugoujia (楼主) 2年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!