记一次 @vue/cli 项目中启动 vite 开发

2,537 阅读4分钟

背景

我司项目基本都是 @vue/cli ,毕竟是官方出品,稳定性、维护性有保障。但是最近新一代的 no bundle 工具 vite 风头也很盛,我想着在不破坏现有体系的情况下额外提供一种尝试 vite 的方案。

本文并非全量迁移,仅多一个 vite 开发,生产还是用 @vue/cli 自带的 webpack,而且原有的 vue-cli-service serve 不受影响,技术栈为:@vue/cli-service@5、webpack@5、vite@2、vue@3

历程

vue-cli-plugin-vite

首先,在开始之前,vite 问世之初,就已经有 vite 和 vue/cli 的相关讨论了,总结就是:现阶段 vue/cli 不会支持 vite。可以考虑 vue-cli-plugin-vite。然后我就顺理成章地去试试这个插件。

刚启动就遇到了第一个错误

> node_modules/d/index.js:7:30: error: Could not read from file: /Users/xxx/61/vite-project/node_modules/es5-ext/string/index.js#/contains
    7 │   , contains        = require("es5-ext/string/#/contains");
      ╵                               ~~~~~~~~~~~~~~~~~~~~~~~~~~~

error when starting dev server:
Error: Build failed with 1 error:
node_modules/d/index.js:7:30: error: Could not read from file: /Users/xxx/61/vite-project/node_modules/es5-ext/string/index.js#/contains

顺藤摸瓜找到了依赖路径 xxx > memoizee > ex5-ext ,好在这个包是自己开发的包,其实就一个简单的异步缓存功能,功能简单我又懒得跟进这个问题,顺手就移除了这个依赖,改成手写了。后来官方也修复了这个问题

紧接着,第二个错误来了,是一个 alias 失效的问题,因为我是依赖 @vue/cli-service@next(webpack@5),所以 vue-cli-plugin-vite 没有兼容情有可原,所以打算看看源码再决定如何处理。找到相关代码在 vue-cli-plugin-vite > vite-plugin-vue-cli 里:

  config.resolve.alias = finalAlias

这一下就有些棘手了,因为它把路堵死了: alias 是直接覆盖的,我没法在外面扩展 alias 了,怎么办?等作者更新?那得啥时候去,我现在就要!这样受制于人,干脆自己启动 vite 算了,自己写 vite config ,那不是灵活地多?

自己写 vite config

首先这个启动方式是额外的尝鲜功能,想要维护性好就得尽可能从 vue.config.js 里复用配置。这个不难,直接开工。

开工之前,发现事情好像不对劲,vite 直接支持 ts 配置,整个项目也都是 ts,这个 vue.config.js 也太扎眼了吧,下意识去搜了下,现阶段并不支持 vue.config.ts,只能自己想办法。

当然这一步不是必须的,你也可以全部 js 配置。我需要 ts 配置的理由有以下几点:

  • vite.config.js 是 esm 的,vue.confgi.js 是 commonjs 的,不好复用。
  • 个人偏好 ts,如果有复杂代码可以获得更好的提示,再说整个项目都是 ts,有现成的 tsconfig 可以用
  • vue.config.js 有个 eslint 报错,虽然不会影响业务代码,但是 VS Code 一直飘着红色早就不爽了,一直没时间解决:
const Components = require('unplugin-vue-components/webpack');
// Unable to resolve path to module 'unplugin-vue-components/webpack'.eslint(import/no-unresolved)

转换所有配置为 ts 文件

编译

一开始也是看了 issue 里提供的方案:"prestart": "tsc vue.config.ts --noEmit" + git ignore vue.config.js,但是我执行下来报错太多了...是一些第三方包的类型错误,有解决办法,但是不值当的。而且生成的产物可读性也差。

然后我就尝试换一个编译器:swc。执行命令

swc vue.config.ts -o vue.config.js -C module.type=commonjs -C jsc.target=es2021 -C module.noInterop=true

但也有一些问题:swc 构建产物是 exports.default = config 而不是 exports = config。 @vue/cli 读 config 的时候并没有判断 __esModule ,直接告诉你没有 "defalut" 这个 key。找了半天也找不到在哪配置这个,放弃了。

引用

再换一种办法:直接在 js 里引用 ts.

首先尝试的是:require('@swc/register') ,直接报错:

SyntaxError: Cannot use import statement outside a module

好理解,但是要额外配置 .swcrc...搁这套娃呢?算了,换一个 register 试试。

经过测试,不需要额外配置就能正常工作的有:

  • require('sucrase/register/ts')

不能正常工作或者需要额外配置的有:

  • @babel/register
  • @swc/register
  • @swc-node/register
  • swc-register
查看 vue.config.js 代码(其他配置文件同理)
require('sucrase/register/ts');

/**
 * @type import('@vue/cli-service').ProjectOptions
 */
const config = require('./vue.config.ts').default;

module.exports = config;

兼容 @vue/cli 配置

entry & plugins

vite 的入口是 html,可以用 vite-plugin-html-template 获得和 vue/cli 一致的体验。

补齐 vue/cli 常用功能用到的 vite 插件:

  • vite-plugin-html-template
  • @vitejs/plugin-vue
  • @vitejs/plugin-vue-jsx
  • vite-plugin-eslint
  • vite-plugin-checker
    • 会出现一些第三方类型报错,可以用 patch-package 解决。
  • vite-esbuild-typescript-checker
    • 支持 watch 模式,不会提示 node_modules/ 的错误

alias

回到刚才那个 alias 失效的问题,这下就容易多了。

// vue.config.ts
import type { ProjectOptions } from '@vue/cli-service';
// ...
import packageJson from './package.json';
import { compilerOptions } from './tsconfig.json';

export const alias = {};

Object.entries(compilerOptions.paths).forEach(([key, [value]]) => {
  alias[`${key.replace(/(\/\*)?$/, (m) => (m ? '' : '$'))}`] = resolve(
    __dirname,
    compilerOptions.baseUrl || '.',
    value.replace(/\/\*$/, ''),
  );
});

export const fallback = {
  path: require.resolve('path-browserify'),
  crypto: require.resolve('crypto-browserify'),
  stream: require.resolve('stream-browserify'),
};

const config: ProjectOptions = {
  // ...
  configureWebpack: {
    resolve: {
      alias,
      fallback,
    },
    // ...
  },
};

export default config;
// vite.config.ts
import { defineConfig } from 'vite';
import vueCliConfig, { fallback, alias, devServer } from './vue.config';
// ...

Object.entries(alias).forEach(([key, value]) => {
  fallback[key.replace(/\$$/, '')] = value;
});
export default defineConfig({
  resolve: {
    alias: fallback,
  },
  // ...
});

env

vite 的环境变量是 import.meta.env.XXX 是这种形式,但是我现在并不是全量迁移,不可能去把业务代码里的那么多引用全改了,所以必须采用一种兼容方案,可以采用 @rollup/plugin-replace,也可以用 define。当然,为了和 vue/cli 保持一致,这里需要把 .env[.xxx] 文件里的也定义一下:

// vite.config.ts
import { defineConfig } from 'vite';
// import rollupReplace from '@rollup/plugin-replace';
// ...

const replacement = {
  'process.env.NODE_ENV': JSON.stringify(NODE_ENV),
  'process.env.VUE_APP_ENV': JSON.stringify(APP_ENV),
  'process.env.BUNDLER': '"vite"',
};

readFileSync(resolve(__dirname, `.env.${APP_ENV}`), 'utf-8')
  .split('\n')
  .forEach((line) => {
    line = line.trim();
    if (!line) return;
    if (line.startsWith('#')) return;
    const [key, value] = line.split('=');
    replacement[`process.env.${key}`] = JSON.stringify(value);
  });

export default defineConfig({
  plugins: [
    // rollupReplace({ values: replacement, preventAssignment: true }),
    // ...
  ],
  define: {
    'process.env': process.env,
    ...replacement,
  },
  // ...
});

babel

vite 没有 babel 插件,官方说完全 cover @rollup/plugin-babel ,但是有时候就是会依赖一些插件,如条件编译

这里我发现了一个临时方案,就是 @vitejs/plugin-vue-jsx 这个包是用 babel 的,而且接受 babelPlugins,好家伙,节省了好多时间:

// vite.config.ts
import vueJsx from '@vitejs/plugin-vue-jsx';
import babelConfig from './babel.config';
// ...

export default defineConfig({
  plugins: [
    vueJsx({
      babelPlugins: babelConfig.plugins,
    }),
    // ...
  ],
  // ...
});

注意一点:这插件仅对 tsx 文件(vue 文件里 script[lang="tsx"] 也算)生效,这个对我来说已经够了,想用的时候改个文件后缀也不算什么成本。

运行时兼容

全局变量

运行时报global is not defined,极少量,能被拿来在浏览器的代码,不会重度依赖 Nodejs 全局变量,大部分只是简单判断一下,在入口 html 里:

    <% if (NODE_ENV === 'development') { %>
    <script>
      // fix vite dev
      if (!window.global) window.global = window;
    </script>
    <% } %>

自动引入

webpack 里通过 require.context ,而 vite 里是 import.meta.globEager ,这个兼容一下,我这里选择双重判断,用条件编译可以更直接地移除代码,写原生判断为了避免条件编译失效:

function handleEachModule(module: any, filename?: string): void {
  // #if DEBUG
  console.log('%cimport "%s"', 'color: #cf222e', filename);
  // #endif
  if (module.__esModule || module.default) {
    module = module.default;
  }
  if (Array.isArray(module)) {
    menuBaseRecord.children.push(...module);
  } else if (module) {
    menuBaseRecord.children.push(module);
  }
}

// 此目录下 *.routes.ts* 都会自动引入
if (process.env.BUNDLER === 'vite') {
  // #if BUNDLER === 'vite'
  const modules = import.meta.globEager('./*.routes.ts?(x)');
  Object.entries(modules).forEach(([k, v]) => handleEachModule(v, k));
  // #endif
} else if (process.env.BUNDLER === 'webpack') {
  // #if BUNDLER === 'webpack'
  const ctx: __WebpackModuleApi.RequireContext = require.context('./', true, /\.routes\.tsx?$/);
  ctx.keys().forEach((key: string) => {
    handleEachModule(ctx(key), key);
  });
  // #endif
}

兼容 qiankun

官方暂未支持 ,这里我选择了 vite-plugin-qiankun,暂时没发现什么问题。

优化推荐

推荐一个 vite 插件:github.com/antfu/vite-…

小结

再次声明:本文并非全量迁移,仅多一个 vite 开发,生产还是用 @vue/cli 自带的 webpack。

不构成开发建议,风险自担。

参考链接