Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
bowencool opened this issue Nov 27, 2021 · 0 comments
Closed

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

bowencool opened this issue Nov 27, 2021 · 0 comments

Comments

@bowencool
Copy link
Owner

bowencool commented Nov 27, 2021

背景

我司项目基本都是 @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 插件:https:/antfu/vite-plugin-optimize-persist

小结

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

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

参考链接

Repository owner locked and limited conversation to collaborators Jan 19, 2024
@bowencool bowencool converted this issue into discussion #48 Jan 19, 2024

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant