vue-cli3 vue2 保留 webpack 支援 vite 成功實踐

ESnail發表於2021-12-19

大家好!

文字是為了提升開發效率及體驗實踐誕生的。

專案背景:

  • 腳手架:vue-cli3,具體為 "@vue/cli-service": "^3.4.1"
  • 庫:vue2,具體為:"vue": "2.6.12"
  • 備註:沒有 typescript , 非 ssr

痛點:隨著時間的推移、業務的不斷迭代,依賴、功能、程式碼越來越多,專案本地啟動比較慢、開發熱更新比較慢。

改進目標:保留原來的 webpack, 支援 vite。並且儘可能的減少改動,減少維護成本。

考慮:

  • vite 生產環境用的是 rollup,rollup 更適合打包庫。
  • vite 打包提效並沒有提供太多。
  • 保留原來的 webpack 方式,儘可能的保證生產的穩定與安全。

實踐

主要是處理三方面:

  • 配置檔案需根據 vue.config.js 增加 vite.config.js
  • 入口檔案 index.html 支援 vite 的方式
  • 配置 vite 啟動命令
  • 可能需要
    • 路由懶載入,vite 需要特殊處理
    • 解決 commonjs 與 esModule 的引入與混用

增加 vite.config.js

在專案根目錄建立一個 vite.config.js

安裝所需依賴

npm i vite vite-plugin-vue2 -D

根據 vue.config.jsvite.config.js 中增加對應配置

// 若改了該檔案邏輯,請對照著改一下 vue.config.js
import path from 'path'
import fs from 'fs'
import { defineConfig } from 'vite'
import config from './config'
import { createVuePlugin } from 'vite-plugin-vue2'
import { injectHtml } from 'vite-plugin-html'

const resolve = dir => path.join(__dirname, dir)

const alias = {
  vue: 'vue/dist/vue.esm.js',
  '@': resolve('src'),
}

const publicPath = '/'
const mode = 'development'

// https://vitejs.dev/config/
export default defineConfig({
  base: publicPath,
  plugins: [
    createVuePlugin(), 
  ],
  // 這些是注入專案中的變數,若有 process 開頭的,都需要在這兒注入,vite 預設不會注入 process 相關的變數及值
  define: {
    'process.env.NODE_ENV': JSON.stringify(mode),
    'process.env.publicPath': JSON.stringify(publicPath),
  },
  resolve: {
    // 配置別名
    alias,
    // 匯入時想要省略的副檔名列表,必須加入.vue,因為 vite 預設不支援省略 .vue 字尾
    extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
  },
  server: {
    // 允許跨域
    cors: true,
    proxy: {
      // 可直接拷貝 vue.config.js 中對應的 proxy
    }
  }
})

sass 相關

配置特殊說明:若專案中有用是 sass 前處理器, 且用到變數了。

需要安裝 sass@1.32.13,不能安裝比 1.32 更高的版本,跟高版本存在這個問題Sass報錯: Using / for division is deprecated

npm i sass@1.32.13  -D
export default defineConfig({
  ...
  css: {
    // 給 sass 傳遞選項
    preprocessorOptions: {
      scss: {
        charset: false, // 最新版sass才支援
        additionalData: `@import "src/sass/common/var.scss";`,
      }
    },
  },
  ...
})

備註:如果生產環境用 vite 打包,採用 sass@1.32.13 將會遇到這個問題vite2打包出現警告,"@charset" must be the first,該如何消除呢?,但是 sass@1.32.13 不支援文件中的配置。所以這可以算是生產環境不用 vite 的一個原因。

還需全域性替換 /deep/::v-deep

入口檔案 index.html 支援 vite 的方式

由於 vite 的專案要求 index.html 檔案在根目錄,且有入口檔案配置。

所以將 index.html 從 public 目錄移到根目錄。並加入

<% if (typeof isVite === 'boolean' && isVite) { %>
  <script type="module" src="/src/main.js"></script>
<% } %>

這樣配置是為了能讓 webpack、vite 方式都都支援,共用一個檔案

而為了在 index.html 檔案注入 isVite 變數,需要安裝

npm i vite-plugin-html -D

需要在 vite.config.js 中配置

...
import { injectHtml } from 'vite-plugin-html'

export default defineConfig({
  ...
  plugins: [
    ...
    injectHtml({
      data: {
        title: 'vite-plugin-html-example',
        isVite: true
      },
    }),
  ],
  define: {
    'process.env.isVite': JSON.stringify(true)
  },
  ...
})

配置 vite 啟動命令

最後在 package.json 中增加指令碼

"scripts": {
  "vite-start": "vite"
}

路由懶載入,vite 需要特殊處理

vue 實現路由懶載入的方式是這樣的

  const Home = import(/* webpackChunkName: "[home]" */ `@/page/home.vue`)

  const routes = [
    {
      path: `/home`,
      name: 'home',
      component: Home
    },
  ]

但是 vite 不支援,解決

const modules = import.meta.glob('@/page/*.vue')

const Home = modules['@/page/home.vue']
const modules = import.meta.glob('@/page/*.vue')

// vite 會生成程式碼
const modules = {
  '@/page/home.vue': () => import('@/page/home.vue'),
  '@/page/page1.vue': () => import('@/page/page1.vue'),
  '@/page/page2.vue': () => import('@/page/page2.vue'),
  ...
}

參考:vite-Glob 匯入

所以可封裝一下:

function loadPage (view) {
  if (process.env.isVite) {
    const modules = import.meta.glob('../pages/*.vue')
    return modules[`../pages/${view}.vue`]
  }
  
  return () => import(/* webpackChunkName: "[request]" */ `@/pages/${view}`)
}

// 使用:
const routes = [
  {
    path: `/home`,
    name: 'home',
    component: loadPage('home'),
  },
  {
    path: `/404`,
    name: '404',
    component: loadPage('404'),
  },
]

但是 webpack 不支援 import.meta,需要 loader 處理。解決:在本地建一個檔案 webpack-import-meta-loader.js。

// 來源:https://github.com/KmjKoishi/webpack-import-meta-loader-fixed
// 是對 @open-wc/webpack-import-meta-loader 這個的修復
// 主要是修復 當 this.rootContext 不存在的判斷處理。構建生產環境時不存在

/* eslint-disable */
// @ts-nocheck
const path = require('path');
function toBrowserPath(filePath, _path = path) {
  return filePath.replace(new RegExp(_path.sep === '\\' ? '\\\\' : _path.sep, 'g'), '/');
};
const regex = /import\.meta/g;

/**
 * Webpack loader to rewrite `import.meta` in modules with url data to the source code file location.
 *
 * @example
 * return import.meta;
 * // becomes: return ({ url: `${window.location.protocol}//${window.location.host}/relative/path/to/file.js` });
 *
 * return import.meta.url;
 * // becomes: return ({ url: `${window.location.protocol}//${window.location.host}/relative/path/to/file.js` }).url;
 */
module.exports = function (source) {
  const path = require('path');

  const relativePath = this.context.substring(
    this.context.indexOf(this.rootContext) + (this.rootContext && this.rootContext.length >= 0 ? (this.rootContext.length + 1) : 0),
    this.resource.lastIndexOf(path.sep) + 1,
  );

  const browserPath = toBrowserPath(relativePath);

  const fileName = this.resource.substring(this.resource.lastIndexOf(path.sep) + 1);

  let found = false;
  let rewrittenSource = source.replace(regex, () => {
    found = true;
    return `({ url: getAbsoluteUrl('${browserPath}/${fileName}') })`;
  });

  if (found) {
    return `
      function getAbsoluteUrl(relativeUrl) {
        const publicPath = __webpack_public_path__;
        let url = '';
        if (!publicPath || publicPath.indexOf('://') < 0) {
          url += window.location.protocol + '//' + window.location.host;
        }
        if (publicPath) {
          url += publicPath;
        } else {
          url += '/';
        }
        return url + relativeUrl;
      }
${rewrittenSource}`;
  } else {
    return source;
  }
};

vue.config.js 修改配置:

const resolve = dir => require('path').join(__dirname, dir)

module.exports = {
  ...
  configureWebpack: {
    ...
    module: {
      rules: {
        ...
        {
          test: /index.js$/,
          use: [
            resolve('webpack-import-meta-loader'),
            'babel-loader'
          ],
          include: [resolve('src/router')]
        }
      }
    }
  }
  ...
}

解決 commonjs 與 esModule 的引入與混用

混用

webpack 方式下,若你的 src 專案原始碼中存在混用 commonjs 與 esModule。

方案一:不改原始碼,在 vite.config.js 中加配置,把 commonjs 轉換為 esModule

安裝 npm i cjs2esmodule -D

在 vite.config.js 中加配置

export default defineConfig({
  plugins: [cjs2esmVitePlugin()]
})

如果這個方案,能讓你的專案執行正常。否則,可能需要採用方案二。

方案二:把 src 程式碼中的 commonjs 語法自己手動改為 esModule

引入

如果你的專案在有一個 config.js, 被 vue.config.js 中使用。那麼你可能需要處理。

vue.config.js 必須是 commonjs 語法的檔案,才能被使用,否則會報錯。

vite.config.js 既可以 esModule 語法,也可以 commonjs 語法。預設是 esModule 語法。

若上面混用,你是採用方案二,而且 src 程式碼中也用了 config.js。那麼你只能把 config.js 改成 esModule 的方式。此時 vue.config.js 不支援了,採用的方案是根據 config.js 自動生成一個 config-cjs.js。

目的是減少後期維護成本。

// transformConfig2cjs.js

// 執行專案的時候,會根據config.js自動生成檔案:config-cjs.js, 以便讓 vue-config.js 能直接使用
const {transformAsync} = require('@babel/core');
const plugin = require('@babel/plugin-transform-modules-commonjs')
const fs = require('fs')
const resolve = dir => require('path').join(__dirname, dir)
async function transfrom () {
  const inputPath = resolve('config.js')
  const input = fs.readFileSync(inputPath, 'utf-8')
  const {code} = await transformAsync(input, {
    sourceType: 'module',
    plugins: [ plugin ]
  });
  
  fs.writeFileSync(resolve('./config-cjs.js'), code);
}
transfrom()

然後在 vue.config.js 原來引入的 config.js 的地方改為 config-cjs.

最後在 package.json 中改變下指令碼,每次都重新生成一個最新的配置。

"scripts": {
  "transformConfig2cjs": "node transformConfig2cjs.js",
  "serve": "npm run transformConfig2cjs && vue-cli-service serve",
  "build": "npm run transformConfig2cjs && vue-cli-service build",
}

總結

遇到很多坑,都是語法相關的,最終都 11 解決了!

也嘗試過一些其他方案,但是不能用,我的專案沒有生效:

支援了之後,開發是真的很高效!

希望這篇文章對有需要的有所幫助。

相關文章