webpack2的vuejs老專案遷移到vite2.0的記錄

wojianishanghaojiugoujia發表於2021-04-17

最近 vite2 非常火,它基於瀏覽器原生ES模組載入的現代化構建工具,主要由兩部分組成:

  • 一個開發伺服器,它利用 原生 ES 模組 提供了 豐富的內建功能,如速度快到驚人的 模組熱更新(HMR)。
  • 一套構建指令,它使用 Rollup 打包你的程式碼,預配置輸出高度優化的靜態資源用於生產。

為什麼選用vite?

隨著我們開始構建越來越多的雄心勃勃的應用程式,我們處理的 JavaScript 數量也呈指數級增長。大型專案包含數千個模組的情況並不少見。我們開始遇到基於 JavaScript 的工具的效能瓶頸:通常需要很長時間(有時甚至是幾分鐘!)才能啟動開發伺服器,即使使用 HMR,檔案編輯也需要幾秒鐘才能在瀏覽器中反映出來。緩慢的反饋會極大地影響開發人員的生產力和幸福感。
Vite 旨在利用生態系統中的新進展解決上述問題:瀏覽器支援原生模組,越來越多 JavaScript 工具使用編譯型語言編寫。

公司內部OA系統,基於vue2+webpack2開發,目前頁面有上百個,每次啟動開發或者編譯都需要至少5分鐘,非常慢。由於 vite 官方沒有原生支援vue2.0,需要依賴於第三方外掛,而且對於編譯的穩定性和風險沒辦法保證,因此本次引入vite優先保證本地開發伺服器的執行,儘量避免修改程式碼,編譯還是由原來的webpack來執行。

首先安裝 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 協議》,轉載必須註明作者和本文連結

相關文章