vue2+vite初體驗

磨蹭先生發表於2021-09-05

前言

自從 vite 釋出之後,社群讚譽無數,而我也一直心水 vite 的輕量快速的熱過載的特性,特別是公司的專案巨大,已經嚴重拖慢了熱過載的速度了,每次熱過載都要等上一小會,所以急需尋找一個解決方案。也發現自己很久沒更新部落格了,順手更新一篇下 ?

雖然,我們通過 webpack 配置,指定了在本地載入的路由,使得熱更新更加迅速一些,但是仍然是遠遠不夠的。所以就想著使用 vite 進行嘗試了。

const fs = require("fs");
const path = require("path");
function resolve(dir) {
  return path.join(__dirname, dir);
}
const isLocal = process.env.LOCAL === "true";
module.exports = {
  chainWebpack: (config) => {
    if (isLocal && fs.existsSync(resolve("/src/mainDev.js"))) {
      config.entry("app").clear().add("./src/mainDev.js");
    }
  },
};

ps: 我的理想的方案是:webpack 仍然作為打包工具,vite 作為開發工具。因為我仍然覺得 webpack 還是當下構建 webapp 的最佳實踐(帶有程式碼拆分,舊瀏覽器的 Legecy-build)。所以,我會盡量在 vitewebpack 環境下維護一份配置。

ps: 為了更加無縫的遷移 Vite,這裡使用了 vue-cli 外掛,即 vue-cli-plugin-vite

本次教程可能過於囉嗦,可以先到giteegithub下載體驗,也可到文末直接下載程式碼先自行體驗。。。

特別說明:專案使用的 Node 版本為 14.17.6,Node10 專案的版本為 10.15.3,皆為 Node 穩定版本

初步體驗

有了這個想法,當然就開啟官網直接開幹呀,開啟搭建第一個 Vite 專案,發現 Vite 需要 Node.js 版本 >= 12.0.0,而我公司用的是 Node10 穩定版。

哦豁 ?!!看到這裡,本以為本次遷移就到此結束了~~。

Node10 嘗試(可選)

當然,我抱著嘗著一試的心態,在 Node10 中執行 Vite,然後出現報錯了,具體如下:

Error: Cannot find module 'worker_threads'

vue2-vite-demo-icon01.jpg

所以我 google 搜尋了下 答案,發現 Node10.5 就支援了 workers,不過 Node12 是自動開啟,而 Node10 是需要手動開啟,所以這邊做了如下修改(虛擬碼):

{
  "scripts": {
    "vite": "node --experimental-worker ./bin/vite"
  }
}

然後- -,Vite 底層出現了新的報錯,因為 Vite 的使用了陣列的 flat 方法。

vue2-vite-demo-icon02.jpg

所以我們需要對 Vite 進行 Babel 的編譯,所以我們需要安裝一下 @babel/node,npm i @babel/node -D,虛擬碼:

{
  "scripts": {
    "vite": "babel-node --experimental-worker ./bin/vite"
  }
}

然後就可以愉快的執行啦

ps: 因為這裡使用的是 vue-cli-plugin-vite,他是使用 cross-spawn 執行指令碼的,所以這裡的 babel-node --experimental-worker 在 scripts 無效,需要在 ./bin/vite 檔案裡編寫,具體參考這個連結-GITEE這個連結-GITHUB

開始搭建

為了大家儘可能的少改 webpack,我的案例中也覆蓋了相對多的常用配置,比如:

  • scss 變數注入
  • 環境變數的使用
  • 使用別名 alias
  • 配置 resolve externals
  • 使用 jsx
  • require 語法
  • devServer
  • require.context 語法相容

ps: 相容這些雖然多數都是 vue-cli-plugin-vite 做的事,但是就是想著大家可以拿來即用 ?,更多相容參考vue-cli-plugin-vite

為了更好的編寫體驗,這裡提供一個基礎的 vue-clidemo,可以 download 下來一起嘗試編寫一下。

安裝 vue-cli-plugin-vite

在當前專案開啟終端,執行:

vue add vite

忽略 .vue 擴充名

這裡後你會發現專案裡多了 bin/vite 檔案,package.jsonscripts 也多少了一個 vite 的命令,執行:

npm run vite

vue2-vite-demo-icon03.jpg

Unrestricted file system access to "/src/layout",這個報錯說明找不到這個檔案,可是我們看,我們明明有layout/index.vue,但是卻報找不到,這是為什麼呢?這是因為 Vite 的 resolve.extensions 預設的 .vue 的字尾名,官方也不推薦自定義匯入型別的副檔名,因為它會影響 IDE 和型別支援。(檢視連結)

當然,我們為了相容以前的舊專案,還是需要配置的,所以我們需要更新下我們的配置,在vue.config.js中補上 resolve.extensions 的配置,程式碼如下:

module.exports = {
  // ...
  configureWebpack: {
    resolve: {
      extensions: [".mjs", ".js", ".ts", ".jsx", ".tsx", ".json", ".vue"],
    },
  },
  // ...
};

ps: 小插曲,之前測試的時候發現配了 resolve.extensions 也沒有效果,然後翻閱 Vite 文件,發現 Vite 是支援的,但是 vue-cli-plugin-vite 不支援,所以我給作者提了個 Issue,現在也支援了,感謝作者~~

ps: 以後一定要寫字尾名~~~ 相關 Issues 1782163

JSX 語法處理

新增完後,再次執行:

npm run vite

發現又報瞭如下錯誤:

vue2-vite-demo-icon04.jpg

翻譯來說就是說你在 .vue 檔案中用了無效的 js 語法(即 JSX),這裡就就需要我們在 vue 的 sfc 元件中還得加上 jsx 標識,即(src/components/HelloWorld.vue):

<script lang="jsx">
  import Test from "./Test";
  export default {
    name: "HelloWorld",
    components: {
      Test,
      TestJsx: {
        render() {
          return <div>我是vue檔案的JSX渲染的</div>;
        },
      },
    },
    props: {
      msg: String,
    },
  };
</script>

修改完後再次執行,發現又報錯了,而且這個錯誤和上面的還很類似。不過只是說我們在 .js 檔案中用了無效的 js 語法(即 JSX),如果您使用的是 JSX 請確保將檔案命名為.JSX 或.tsx 副檔名。

vue2-vite-demo-icon05.jpg

js 中不支援 jsx 的原因,尤大也在 issue 有過說明,具體參考這個連結

所以,我們只需要把 .js 檔案的字尾名修改為 .jsx 即可

修改完後,再次執行:

npm run vite

這裡會發現,瀏覽器報 require is not defined,這裡我們先把 Home.vue 檔案的 require 註釋掉先(require 的問題下面會講到),程式碼如下:

<script>
  // @ is an alias to /src
  import HelloWorld from "@comp/HelloWorld";
  // const { sum } = require('../utils/index')

  export default {
    name: "Home",
    components: {
      HelloWorld,
    },
    methods: {
      handleClick() {
        // console.log(sum(1, 32))
      },
    },
  };
</script>

出現如下報錯:

vue2-vite-demo-icon06.jpg

因為我們雖然設定了一堆使用 jsx 的配置,但是沒有在外掛上配置開啟 jsx(即不設定 vitePluginVue2Options: { jsx: true }),所以需要在 vue.config.js 編寫下 vite 的配置啦(終於開始配置 vite 了),相關 issue

module.exports = {
  pluginOptions: {
    vite: {
      /**
       * Plugin[]
       * @default []
       */
      plugins: [], // other vite plugins list, will be merge into this plugin\'s underlying vite.config.ts
      /**
       * Vite UserConfig.optimizeDeps options
       * recommended set `include` for speedup page-loaded time, e.g. include: ['vue', 'vue-router', '@scope/xxx']
       * @default {}
       */
      optimizeDeps: {},
      /**
       * type-checker, recommended disabled for large-scale old project.
       * @default false
       */
      disabledTypeChecker: true,
      /**
       * lint code by eslint
       * @default false
       */
      disabledLint: false,
      /**
       * enable css-loader url resolve compat
       * disabled it if you do not use `~@/assets/logo.png` for better performance.
       * @default true
       */
      cssLoaderCompat: true,
      vitePluginVue2Options: {
        jsx: true,
      },
    },
  },
};

再次執行,發現可以開啟頁面了

總結:在 vite 中使用 jsx 還是稍微有點麻煩的,一是使用到 jsx 語法的 js 檔案都必須改成使用 jsx 字尾名,二是在 vue 的 sfc 元件中還得加上 jsx 標識(僅僅引入一個 .jsx 檔案 不需要加上)

require 語法處理

把 require 的註釋開啟,再次執行,f12 開啟控制檯,出現如下錯誤:

vue2-vite-demo-icon07.jpg

因為 vite 不支援 require 的,那麼怎麼解決呢?這時候就需要使用 vite 外掛了。

這裡說說我是怎麼找這些外掛的吧,通常不知道怎麼辦的時候,就去 npm 搜尋一下關鍵字 vite commonjs,然後看下這些外掛的下載量,率先選擇最高的那個使用,這裡發現 @originjs/vite-plugin-commonjs 這個周下載量有 2000+。所以這裡就嘗試使用這個了,發現一試還真成了。

所以,接下來就跟著我一起安裝並且配置一下吧。

npm install @originjs/vite-plugin-commonjs -D
const { viteCommonjs } = require("@originjs/vite-plugin-commonjs");
module.exports = {
  pluginOptions: {
    vite: {
      plugins: [
        viteCommonjs({
          // lodash不需要進行轉換
          exclude: ["lodash"],
        }),
      ],
    },
  },
};

ps: 但是標籤上的 require 並不支援,所以建議全面擁抱 ES Module

ps: 路由使用 resolve => require(['../components/views/Home.vue'], resolve) 匯入的,可以通過 vscode 使用下面的正則全域性替換

搜尋:\(?resolve\)?\s*=>\s*require\(\[(.\*)\], resolve\)

替換:() => import($1)

scss 變數注入

重新執行一下,發現啥問題都沒有,看著一切正常,這時候我覺得 HelloWorld 元件缺點樣式,我想美化一樣,比如修改下字型顏色、文字大小啥的。

所以我對 HelloWorld 元件新增了樣式,進行了如下修改:

<template>
  <div class="hello">
    <h1 class="h1">{{ msg }}</h1>
    <test />
    <test-jsx />
  </div>
</template>

<script lang="jsx">
  import Test from "./Test";
  export default {
    name: "HelloWorld",
    components: {
      Test,
      TestJsx: {
        render() {
          return <div>我是vue檔案的JSX渲染的</div>;
        },
      },
    },
    props: {
      msg: String,
    },
  };
</script>

<style lang="scss" scoped>
  .h1 {
    font-size: 30px;
    color: skyblue;
  }
  .hello {
    @include bgCover("@/assets/logo.png");
  }
</style>

還沒開始寫呢,控制檯就一堆報錯:

vue2-vite-demo-icon08.jpg

猜測是使用了別名匯入 scss 後,識別到 url() 後就會輸出相對路徑,所以這邊在 vite 環境時候,使用 src/styles 匯入即可,具體 vue.config.js 修改如下:

// npm 正在執行哪個 script,npm_lifecycle_event 就返回當前正在執行的指令碼名稱。
const isVite = process.env.npm_lifecycle_event.startsWith("vite");

// 相容vite
function getAdditionalData(str) {
  if (isVite) {
    return str.replace(/@style\//, "src/styles/");
  }
  return str;
}

module.exports = {
  css: {
    requireModuleExtension: true,
    loaderOptions: {
      scss: {
        // 注意:在 sass-loader v7 中,這個選項名是 "data" 官網文件還是prependData   此專案用的7+版本
        // 注意:在 sass-loader v10 使用 additionalData,這裡為了相容vite,所以升級了sass-loader@10
        additionalData: getAdditionalData(`@import '@style/variables.scss';`),
      },
    },
  },
};

ps: 這裡也有個小知識點,我們可以通過 npm_lifecycle_event 來獲取我們執行了的指令碼名稱,通過 npm_lifecycle_script 獲取執行了什麼命令

script 指定環境

通常我們會有 beta、pre、dev 好幾個環境,在 vue-cli 開發的時候我們通過會通過 --mode env 指定我們本地的開發環境,現在我們也嘗試在 scripts 中的 vite 指定 staging 環境,發現並沒有效果:

{
  "scripts": {
    "vite": "node ./bin/vite --mode staging"
  }
}

這是為什麼呢?開啟 bin/vite 檔案一看,發現 使用 cross-spawn 執行指令碼的,所以 --mode staging 這個引數根本就沒有獲取,那麼我們怎麼可以獲取呢?

其實我們可以通過 process.argv 獲取我們執行的命令的引數,列印一下發現 argv 是個陣列,而我們需要的是最後那兩個,所以這裡需要進行如下修改(bin/vite):

#!/usr/bin/env node

const path = require("path");
const spawn = require("cross-spawn");
const configPath = require.resolve("vue-cli-plugin-vite/config/index.ts");
const cwd = path.resolve(__dirname, "../");

const params = [
  `${process.env.BUILD ? "build" : ""}`,
  process.env.VITE_DEBUG ? "--debug" : "",
  "--config",
  `${configPath}`,
  ...process.argv.slice(2),
].filter(Boolean);

console.log(`running: vite ${params.join(" ")}`);
const serveService = spawn("vite", params, {
  cwd,
  stdio: "inherit",
});

serveService.on("close", (code) => {
  process.exit(code);
});

至此,我們的 vite 命令也可以指定開發環境啦 ?

額外知識點 - keep-alive 使用動態 key 時,熱更新無效

一般的後臺管理肯定需要 keep-alive 這個元件,比如我們 layout 元件上就是用了 keep-alive,但是你會發現在你使用 keep-alive 的時候,頁面卻沒有熱更新,這個不是 vite 的問題,也不是 webpack 的問題,這是 Vue 的問題(當然也有相關 issue),而且這個 issue 已經從 18 年就開始有了,且現在仍然是 open 狀態(相關 issue)

參考評論和 issue,我們也可以編寫一個只在開發環境中使用的 keep-alive 元件了。

建立 plugins/keep-alive.js 檔案,編寫如下程式碼:

import { isArray, isRegExp } from "lodash";
function remove(arr, item) {
  if (arr.length) {
    var index = arr.indexOf(item);
    if (index > -1) {
      return arr.splice(index, 1);
    }
  }
}
function isDef(v) {
  return v !== undefined && v !== null;
}
function isAsyncPlaceholder(node) {
  return node.isComment && node.asyncFactory;
}

function getFirstComponentChild(children) {
  if (isArray(children)) {
    for (let i = 0; i < children.length; i++) {
      let c = children[i];
      if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
        return c;
      }
    }
  }
}

function getComponentName(opts) {
  return opts && (opts.Ctor.options.name || opts.tag);
}

function matches(pattern) {
  if (isArray(pattern)) {
    return pattern.indexOf(name) > -1;
  } else if (typeof pattern === "string") {
    return pattern.split(",").indexOf(name) > -1;
  } else if (isRegExp(pattern)) {
    return pattern.test(name);
  }
  /* istanbul ignore next */
  return false;
}

function pruneCache(keepAliveInstance, filter) {
  const { cache, keys, _vnode } = keepAliveInstance;
  for (const key in cache) {
    const entry = cache[key];
    if (entry) {
      const name = entry.name;
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode);
      }
    }
  }
}

function pruneCacheEntry(cache, key, keys, current) {
  const entry = cache[key];
  if (entry && (!current || entry.tag !== current.tag)) {
    entry.componentInstance.$destroy();
  }
  cache[key] = null;
  remove(keys, key);
}

export default {
  install(app) {
    //只在開發模式下生效
    if (process.env.NODE_ENV === "development") {
      /**
       * Remove an item from an array.
       */

      const patternTypes = [String, RegExp, Array];

      const KeepAlive = {
        name: "keep-alive",
        abstract: true,

        props: {
          include: patternTypes,
          exclude: patternTypes,
          max: [String, Number],
        },

        methods: {
          cacheVNode() {
            const { cache, keys, vnodeToCache, keyToCache } = this;
            if (vnodeToCache) {
              const { tag, componentInstance, componentOptions } = vnodeToCache;
              cache[keyToCache] = {
                name: getComponentName(componentOptions),
                tag,
                componentInstance,
                cid: vnodeToCache.cid,
              };
              keys.push(keyToCache);
              // prune oldest entry
              if (this.max && keys.length > parseInt(this.max)) {
                pruneCacheEntry(cache, keys[0], keys, this._vnode);
              }
              this.vnodeToCache = null;
            }
          },
        },

        created() {
          this.cache = Object.create(null);
          this.keys = [];
        },

        destroyed() {
          for (const key in this.cache) {
            pruneCacheEntry(this.cache, key, this.keys);
          }
        },

        mounted() {
          this.cacheVNode();
          this.$watch("include", (val) => {
            pruneCache(this, (name) => matches(val, name));
          });
          this.$watch("exclude", (val) => {
            pruneCache(this, (name) => !matches(val, name));
          });
        },

        updated() {
          this.cacheVNode();
        },

        render() {
          const slot = this.$slots.default;
          const vnode = getFirstComponentChild(slot);
          const componentOptions = vnode && vnode.componentOptions;
          if (componentOptions) {
            vnode.cid = componentOptions.Ctor.cid;
            // check pattern
            const name = getComponentName(componentOptions);
            const { include, exclude } = this;
            if (
              // not included
              (include && (!name || !matches(include, name))) ||
              // excluded
              (exclude && name && matches(exclude, name))
            ) {
              return vnode;
            }
            const { cache, keys } = this;
            const key =
              vnode.key == null
                ? // same constructor may get registered as different local components
                  // so cid alone is not enough (#3269)
                  componentOptions.Ctor.cid +
                  (componentOptions.tag ? `::${componentOptions.tag}` : "")
                : vnode.key;
            if (cache[key]) {
              if (vnode.cid === cache[key].cid) {
                vnode.componentInstance = cache[key].componentInstance;
                // make current key freshest
                remove(keys, key);
                keys.push(key);
              } else {
                cache[key].componentInstance.$destroy();
                cache[key] = vnode;
              }
            } else {
              // delay setting the cache until update
              this.vnodeToCache = vnode;
              this.keyToCache = key;
            }
            vnode.data.keepAlive = true;
          }
          return vnode || (slot && slot[0]);
        },
      };

      app.component("keep-alive", KeepAlive);
    }
  },
};

在 main.js 引入:

import KeepAlive from "./plugins/keep-alive";
Vue.use(KeepAlive);

這樣子,我們的 keep-alive 就具有熱更新功能啦ヾ(≧▽≦*)

未解決的問題

  • 含有 jsx 標識的 vue 檔案熱更新失效,.jsx 檔案有效,相關 issue

    • 但是有相關 pr實現了 jsx in sfc 的熱更新,但是我在 vue2 中使用並未熱更新

ps: vue-cli-plugin-vite 外掛中的 vite 是鎖定 vite@2.5.1 版本的相關 issue,而這個 issue 的 相關 pr 是 2.5.3 版本才 merge,不過我嘗試使用 vite@2.5.3 也沒有成功

ps: 看了下原始碼,github上的原始碼已經 merge 了,但是 npm 上部分包仍然沒有釋出,比如@vitejs/plugin-vue@vitejs/plugin-vue-jsx,猜測下個版本應該就能實現 jsx in sfc 的熱更新了 ?。
不過我們也可以將 pr 的原始碼複製到 node_modules 裡也可提前體驗 jsx in sfc 的熱更新?

總結

雖然- -這裡沒有用實際專案對比,也沒有實際的資料對比,但是大家可以 download 那個配置在自己專案體驗一下,遷移起來還是比較簡單的。如果有什麼問題歡迎大家留言進行交流~~

最後再強調,在 vite 中使用 jsx 語法的話,一是使用到 jsx 語法的 js 檔案都必須改成使用 jsx 字尾名,二是在 vue 的 sfc 元件中還得加上 jsx 標識(僅僅引入一個 .jsx 檔案 不需要加上)

倉庫程式碼連結如下:

最後

雖然本文羅嗦了點,但還是感謝各位觀眾老爺的能看到最後 O(∩_∩)O 希望你能有所收穫 ?