乐趣区

关于前端:Vite-实践Vite-库模式能满足你吗或许你需要统一构建

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

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

然而在类库模式的构建上却有所欠缺,只能解决单个输出和单输出出的状况,构建场景繁多,Vite 社区上目前也没有可间接应用的工具,所以才有了开发一个 对立构建 插件的想法。

目前 vite-plugin-build 插件已能够间接应用,也录入了 Vite 官网 awesome-vite,心愿也刚好能满足一些人的须要。

什么是对立构建?

因为没有特地好的叫法,自己暂且把这叫做 对立构建 ,自己把 对立构建 演绎为如下构建:

  • Bundle 构建

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

    长处:反对 umd 格局,浏览器中可作为内部依赖,不受业务代码 bundle 影响,可利用浏览器缓存机制,进步加载性能。

    毛病:不反对 Tree Shaking 没有应用到的代码也会加载进来,因为打包到一个 bundle 文件,本地源码可读性差。

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

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

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

    长处:es 模式可反对 Tree Shaking,本地源码可读性高。

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

  • 生成 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 构建服务 来实现此性能(外表上使用者无感知)。

实现的关键点

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 插件,通过对应的钩子函数,实现构建信息的输入。

最终成果

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 选项。
退出移动版