【Vite 实践】Vite 库模式能满足你吗?或许你需要统一构建

2年前 (2022) 程序员胖胖胖虎阿
208 0 0

2022 年本人投入了 Vite 的怀抱,开始参与到 Vite 社区中,陆续开发了一些插件。

Vite 秉承了开箱即用,简化配置的思路,确实显著提升了前端开发体验。

但是在类库模式的构建上却有所欠缺,只能处理单个输入和单输入出的情况,构建场景单一,Vite 社区上目前也没有可直接使用的工具,所以才有了开发一个统一构建插件的想法。

目前 vite-plugin-build 插件已可以直接使用,也录入了 Vite 官方 awesome-vite,希望也刚好能满足一些人的需要。

【Vite 实践】Vite 库模式能满足你吗?或许你需要统一构建

什么是统一构建?

因为没有特别好的叫法,本人暂且把这叫做统一构建,本人把统一构建归纳为如下构建:

  • Bundle 构建

    Vite(也是 Rollup)的库打包模式,单输入文件,单输出 bundle 文件,如果没有设置外部依赖(external)所有涉及的依赖包都会打包到一个 bundle 文件中。

    优点:支持 umd 格式,浏览器中可作为外部依赖,不受业务代码 bundle 影响,可利用浏览器缓存机制,提高加载性能。

    缺点:不支持 Tree Shaking 没有使用到的代码也会加载进来,由于打包到一个 bundle 文件,本地源码可读性差。

    【Vite 实践】Vite 库模式能满足你吗?或许你需要统一构建

  • 文件夹构建(文件到文件转换器,file-to-file transformer)

    文件夹所有的符合格式的文件(['ts', 'tsx', 'js', 'jsx', 'vue', 'svelte'])会转换为对应同名的 .js 文件,只支持 commonjses 格式。

    转换的时候,所有的 import 依赖包不会打包进来,根据需要转换的格式转换为 commonjsrequire 或者 esimport 语法。

    优点:es 模式可支持 Tree Shaking,本地源码可读性高。

    缺点:代码在 Webpack、Vite 这些构建工具中会和业务代码一起打包到 bundle 文件中,很难利用跨站点缓存优势。

    【Vite 实践】Vite 库模式能满足你吗?或许你需要统一构建

  • 生成 TypeScript 声明文件

    支持原生 TypeScript、Vue TypeScript 和 Svelte TypeScript 声明文件的生成(如果有其他类型的框架也可以在此拓展)。

vite-plugin-build 通过 Vite 配合使用 @vitejs/plugin-react@vitejs/plugin-vue@sveltejs/vite-plugin-svelte 可支持上面的三种构建方式。

为什么要开发一个 Vite 统一构建插件?

理由一,Vite 构建场景单一,不支持如下场景:

  • 多输入多输出(多输入多 bundle)
  • 转换文件夹(文件转文件的转换方式,不打包成一个 bundle 文件)
  • 生成 TypeScript 声明文件

理由二,Vite 社区缺乏可直接代替的工具。

Vite Github 上官方插件库使用的是 unbuild,是一款统一构建的工具,虽然挺方便的,但是 unbuild 更倾向于处理纯 JavaScript 或者 TypeScript 的代码,对于 React、Vue、Svelte 等浏览器 UI 类型相关的打包缺乏相关的转换处理。

理由三,Vite 体系一条龙服务。

Vite 统一构建插件,可在一些场景下让 Vite、Vitest 形成以个闭环体系,无需用到其他的构建和单元测试工具,一个 Vite 配置文件闯天下。

萌生想法

Vite 还没有兴起的之前,公司组内业务组件和个人的一些 Github 项目一开始是使用 Rollup 进行构建,Rollup 并不能开箱即用,还需要各种插件配置,相对较繁琐。

后来使用 Vite 的库模式来代替原生的 Rollup 可以减少不少的插件配置,不过整个文件夹的所有文件单独转换为 commonjs 和 es 的格式,还是需要通过 Vite 提供的 build API 实现,如下是通过指定文件夹下的所有目标文件,然后遍历运行 build 方式实现(还需要多一个 Vite 配置文件来配置):

const fs = require('fs');
const path = require('path');
const spawn = require('cross-spawn');
const srcDir = path.resolve(__dirname, '../src');

// 所有 src 文件夹包括子文件夹的 js、ts、jsx、tsx 文件路径数组
const srcFilePaths = getTargetDirFilePaths(srcDir);
srcFilePaths.forEach((file) => {
  const fileRelativePath = path.relative(srcDir, file);
  spawn(
    'npm',
    ['run', 'vite', '--', 'build', '--mode', fileRelativePath, '--outDir', 'es', '--config', './vite.file.config.ts'],
    {
      stdio: 'inherit',
    },
  );
});

同时还需要配置 npm run tsc 生成声明文件,pacakge.json scripts 字段比较繁琐:

{
  "scripts": {
    "tsc:es": "tsc --declarationDir es",
    "tsc:lib": "tsc --declarationDir lib",
    "tsc": "npm run tsc:lib && npm run tsc:es",
    "vite": "vite",
    "build:lib": "vite build",
    "build:file": "node ./scripts/buildFiles.js",
    "build": "npm run tsc && npm run build:file && npm run build:lib"
  }
}

新的项目,就直接复制修改一下,虽然也能达到构建的目的,但是就是不够方便,本人懒所以还是想有没有更简单点的方式?如下方使用一个 script 就可以解决?

{
  "scripts": {
    "build": "vite build"
  }
}

实现思路

首先完全通过 Vite 配置文件是无法实现统一构建的功能,即通过正常运行一次 Vite 构建服务无法实现统一构建的功能。

还是得多次运行 Vite 构建服务来实现此功能(表面上使用者无感知)。

【Vite 实践】Vite 库模式能满足你吗?或许你需要统一构建

实现的关键点

Bundle 构建

Vite 库模式就是 bundle 构建模式,不过只能设置一个入口文件,一个 bundle 输出文件。

实际场景可能需要多个入口文件,多个 bundle 输出文件,这个功能不复杂,通过遍历多个 vite build 构建即可实现,代码大致如下:

import { build } from 'vite';

const buildPs = lastBuildOptions.map((buildOption) => {
  return build({
    ...viteConfig, // 透传 vite.config.ts 或者 vite.config.js 的用户配置,插件需要过滤自身(vite-plugin-build)
    mode: 'production',
    configFile: false,
    logLevel: 'error',
    build: buildOption,
  });
});
await Promise.all(buildPs);

文件夹构建

实现思路也不复杂,如下:

  1. 获取文件夹中所有符合的文件路径
  2. 遍历所有文件路径,运行 vite build 构建
  3. 由于 Vue 和 Svelte 的 import 是需要带后缀名的,需要额外移除文件内容中的 .vue.svelte 后缀名。

简单代码实现大致如下:

import { build } from 'vite';
import fg from 'fast-glob';

const {
  inputFolder = 'src',
  extensions = ['ts', 'tsx', 'js', 'jsx', 'vue', 'svelte'],
  ignoreInputs,
  ...restOptions,
} = options;
// 获取默认为项目根目录下 src 文件夹包括子文件夹的所有 js、ts、jsx、tsx、vue、sevele 文件路径,除开 .test.*、.spec.* 和 .d.ts 三种后缀名的文件
// 返回格式为 ['src/**/*.ts', 'src/**/*.tsx']
const srcFilePaths = fg.sync([`${inputFolder}/**/*.{${extensions.join(',')}}`], {
  ignore: ignoreInputs || [`**/*.spec.*`, '**/*.test.*', '**/*.d.ts', '**/__tests__/**'],
});
const buildPromiseAll = srcFilePaths.map((fileRelativePath) => build({
  ...viteConfig, // 透传 vite.config.ts 或者 vite.config.js 的用户配置,插件需要过滤自身(vite-plugin-build)
  mode: 'production',
  configFile: false,
  logLevel: 'error',
  build: {
        lib: {
      entry: fileRelativePath,
            ...
    },
    ...restOptions
  },
}));
await Promise.all(buildPromiseAll);
await removeSuffix(); // 移除生成文件内容中的 `.vue` 和 `.svelte` 后缀名

TypeScript 声明文件生成

前端 UI 框架的多种多样,像 Vue、Svlete 这类有自身自定义的语法,TypeScript 的语法需要特殊支持,生成声明文件自然也需要特殊处理。

原生 TypeScript

原生的 TypeScript 官方直接提供 tsc 工具可直接使用,通过直接运行 tsc bin 文件,并传递对应的配置即可实现。

简单的代码实现如下:

import spawn from 'cross-spawn';

const { rootDir, outputDir } = options;
const tscPath = path.resolve(require.resolve('typescript').split('node_modules')[0], 'node_modules/.bin/tsc');
spawn.sync(
  tscPath,
  ['--rootDir', rootDir, '--declaration', '--emitDeclarationOnly', '--declarationDir', outputDir],
  {
    stdio: 'ignore',
  },
);

Vue TypeScript

Vue 3 出来后,对 TypeScript 的支持就比较完善了,Vue 社区的 vue-tsc 可以用来替代 tsc,用法保持和 tsc 一致。

有一点不一样的是 vue-tsc 生成的声明文件是 .vue.d.ts 后缀名的,所以需要重命名为 .d.ts 后缀名。

简单的代码实现如下:

import spawn from 'cross-spawn';

const { rootDir, outputDir } = options;
const vueTscPath = path.resolve(
  require.resolve('vue-tsc/out/proxy').split('node_modules')[0],
  'node_modules/.bin/vue-tsc',
);
spawn.sync(
  vueTscPath,
  ['--rootDir', rootDir, '--declaration', '--emitDeclarationOnly', '--declarationDir', outputDir],
  {
    stdio: 'ignore',
  },
);

if (isVue) {
  renameVueTdsFileName(); // 重命名 .vue.d.ts 为 .d.ts
}

Svelte TypeScript

Svelte 社区没有像 Vue 那么强大,没有类似 vue-tsc 这样的工具,最终找到 svelte-type-generator 可以实现,参考 svelte-type-generator 的代码实现了,生成声明文件的功能(暂时不支持 tsc cli 的功能)。

const { compile } = require('svelte-tsc');
const { rootDir, outputDir } = options;

compile({
  rootDir,
  declaration: true,
  emitDeclarationOnly: true,
  declarationDir: outputDir,
});

if (isVue) {
  renameSvelteTdsFileName(); // 重命名 .svelte.d.ts 为 .svelte.d.ts
}

打印构建信息

由于是触发运行多个 vite build 所以如果直接输出默认的构建信息,那么会显得混乱,无法像运行一个 vite build 输出构建信息。

所以得拦截和隐藏原有的构建信息,自定义输出新的构建信息。

  1. 拦截和隐藏原有的构建信息,通过重写 console.logconsole.warn 可以达到目的,代码如下:

    export const restoreConsole = { ...console };
    
    export class InterceptConsole {
      public log: typeof console.log;
      public warn: typeof console.warn;
      public clear: typeof console.clear;
    
      constructor() {
        const { log, warn } = console;
        this.log = log;
        this.warn = warn;
      }
    
      silent() {
        console.log = () => {};
        console.warn = () => {};
      }
    
      restore() {
        console.log = this.log;
        console.warn = this.warn;
      }
    }
  2. 自定义输出新的构建信息

    功能参考 Vite 的内置 reporter 插件,通过对应的钩子函数,实现构建信息的输出。

最终效果

【Vite 实践】Vite 库模式能满足你吗?或许你需要统一构建

Github 例子

vanilla

vanilla-ts

react

react-ts

vue

vue-ts

svelte

svelte-ts

所有例子,运行如下命令即可

$ npm install
$ npm run build

Codesandbox 在线例子

  • vanilla-ts
  • react-ts
  • vue-ts
  • svelte-ts

后续

如果感兴趣的也可以加入一起建设,目前 svelte-tsc 实现和 vue-tsc 一样的用法,是有点挑战性的。

  1. 生成声明文件支持配置 tsconfig 配置文件路径
  2. svelte-tsc 支持 bin 文件,功能参照 vue-tsc ,支持所有 tsc 的 cli 选项。

相关文章

暂无评论

暂无评论...