React--》使用vite构建器打造高效的React组件库

发布于:2025-06-15 ⋅ 阅读:(18) ⋅ 点赞:(0)

vite作为一款现代化的构建工具以其极速的构建速度和出色的开发体验,成为了众多开发者的首选工具,接下来我们就开始通过vite构建工具来构建结构清晰的react组件库

目录

初始化项目

monorepo环境搭建

review环境搭建

部署项目打包

代码格式配置

组件文档搭建

初始化项目

        根据上文 地址 我们简单的了解到了组件库的设计与架构规划,接下来我们就开始正式的组件库开发了,这里我们以目前热门的vite构建器作为基座来实现组件库的构建开发打包流程,这里直接通过如下命令来初始化vite项目,然后选择react+ts模板进行操作即可:

npm init vite@latest

monorepo环境搭建

        要知道随着项目规模的不断扩大,多个子项目之间的依赖关系越来越复杂,如何高效地管理多个模块避免重复安装、解决版本冲突成为了开发者的一大难题,所以这里我们下载好vite模板之后,接下来通过monorepo架构进行项目分区控制,如下对monorepo进行简单的介绍:

monorepo(单一代码库):是一种将多个项目(通常是多个子应用、库或服务)存储在同一个代码仓库中的架构模式,在monorepo中所有相关项目共享一个版本控制系统(如git)并且这些项目通常会一起发布或更新,monorepo架构与传统架构之前的区别如下所示:

传统架构

1)独立项目结构,所有项目都是分开仓库(如github)进行存放,且技术栈都互相独立

2)规范化、自动化相关处理与依赖及版本分开管理,项目间存在割裂

3)跨项目协作和依赖管理往往需要手动协调或使用第三方工具,部署自动化脚本很难统一

monorepo架构

1)混合项目结构,所有相关的工程形成子包进行管理,技术栈高度统一

2)规范化、自动化、流程化项目间共享,可以减少重复代码并提高开发效率

3)提供集中式的构建和测试,确保所有子项目在统一的基础上工作,减少了版本不一致和依赖问题

架构方案:monorepo的架构方案有很多且侧重于不同的需求和技术栈,以下是几种常见的方案:

1)bazel:适用于大型代码库和monorepo架构,支持跨语言构建具有高性能和增量构建能力

2)lerna:专门为monorepo中的多包管理而设计,帮助在一个仓库中管理多个包并且能够处理版本控制和依赖管理

3)nx:支持monorepo开发工具主要用于前端开发基于angular,也支持react、vue和其他前端框架

4)pnpm workspaces:高效的js包管理工具,适用于需要高效管理多个JS包的项目特别是对磁盘空间有要求的场景

这些方案各有特点,选择适合的monorepo架构方案通常取决于团队的技术栈、项目规模和构建需求,像我们常见的脚手架开发一般都采用pnpm workspaces方案来处理,接下来就选择该方法来实现我们的组件库开发:

pnpm workspaces: pnpm内置对单一存储库(也称为多包存储库、多项目存储库或整体存储库)的支持,可以创建一个工作区来将多个项目联合到一个存储库中,具体的可以参考:官网 进行查看,详细的步骤就是当我们初始化一个项目之后就可以来设置我们的工作空间用来表明空间中的所有文件都是一个独立的项目,创建工作空间有两种方式:

1)在package.json中设置workspaces:指定了一个数组,数组中的每个路径都会被识别为一个工作区

这种方式是传统的配置方式,在此例中packages/* 表示packages目录下的所有子目录都会作为工作区被管理,可以使用通配符来匹配多个包:

{
  "workspaces": [
    "packages/*"
  ]
}

2)创建pnpm-workspace.yaml文件设置工作区:在根目录创建该文件

这种方式是专门为pnpm引入的配置文件用来进一步控制和管理工作区,在pnpm-workspace.yaml文件中可以像上面一样使用通配符定义需要包含的工作区路径,同时也可以使用 ! 来排除某些目录或文件,在下面这个例子中,packages/* 和 apis/* 是包含的工作区路径;!**/test/** 用来排除所有路径下的 test 目录:

packages:
  - 'packages/*'
  - 'apis/*'
  - '!**/test/**'

当然如果项目结构比较简单只需要列出包含的路径,可以使用package.json的workspaces字段,如果需要更细致的控制比如排除某些目录或路径,pnpm-workspace.yaml文件会更加适合,两者的效果是相似的但pnpm-workspace.yaml提供了更多的配置选项,接下来我们实际组件库开发就采用该方法进行设置,如下所示可以看到我们对项目进行分区处理:

接下来我们在packages文件夹下新建核心文件,作为主要的组件库代码则存放在components文件里面,然后我们在该文件夹下新建package.json文件作为子目录进行管理,这里我们选择一个npm平台不存在的包名作为我们UI组件库的包名:

后期如果想添加新功能,比如说在components文件下新建公共样式文件,也是创建package.json文件即可,如果packages文件夹下新建的这些包文件想互相调用第三方依赖,我们可以将这些依赖全部放在最外层目录下的node_packages文件夹下即可,如下所示:

review环境搭建

当我们编写完组件UI代码之后肯定是需要通过代码来进行实际调用来查看效果的,这里我们把vite模板下的src目录内容删掉然后新建一个review文件夹,在review文件夹下我们再创建一个vite模板用于重新检阅我们编写的组件UI代码,如下所示当我执行pnpm install进行安装的时候,如果全局状态下已经有的包就会直接被引用过来,无需二次安装依赖了,非常的方便:

然后我们给review文件夹设置为子目录的工作区,如果我们想在根目录下运行子目录配置的package.json命令,我们可以通过如下-C来执行review项目文件下的dev命令运行项目,如下:

最终我们的项目就可以在根目录下成功被运行了,如下所示:

部署项目打包

接下来我们开始配置一下vite进行组件库打包的内容,这里我们可以来到 官方文档 了解并学习一下vite构建库模式的方法:

结合我们自己部署组件库的代码结构进行打包,因为我们开发组件库是需要类型提示的,所以我们打包代码文件的时候也是需要带上.d.ts文件的,终端执行如下命令进行安装,因为我们想在根目录安装该依赖,可以通过加上 -w 或 --workspace-root 标志来明确表示:

# 处理类型声明文件
pnpm install vite-plugin-dts -D -w
# 打包构建前快速清除构建产物
pnpm i rollup-plugin-clear -D -w

因为vite底层也是采用了rollup和esbuild进行打包构建运行的,所以当我们配置打包代码的时候也可以使用rollup插件进行特定需求处理的,这里我们安装上面清除构建产物的插件,来使用到我们的项目当中,这里在vite.config.ts配置如下库模式打包命令。

这里我们采用打包后的具体产物的项目结构为antd的项目结构:

import path, { resolve } from 'path'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import dts from 'vite-plugin-dts'
import clear from 'rollup-plugin-clear'

const nodeEnv = process.env.NODE_ENV === 'production' ? '"production"' : '"development"';

// https://vite.dev/config/
export default defineConfig({
  plugins: [react(), 
    dts({
      tsconfigPath: 'tsconfig.json', // 指向tsconfig.json文件
      entryRoot: './packages/components', // 指向组件库源代码目录
      outDir: './packages/build/es',  // 指向组件库声明文件输出目录
      exclude: ['node_modules', 'docs', 'review', 'vite.config.ts'], // 排除的文件
    }),
    dts({
      tsconfigPath: 'tsconfig.json',
      entryRoot: 'packages/components',
      outDir: 'packages/build/lib',
      exclude: ['node_modules', 'docs', 'review', 'vite.config.ts'],
    }),
  ],
  resolve: {
    alias: {
      '@ease-reactify': path.resolve(__dirname, './packages'),
    }
  },
  define: { 'process.env.NODE_ENV': nodeEnv },
  build: {
    emptyOutDir: true, // 构建前清空输出目录
    minify: true, // 压缩
    cssCodeSplit: true, // 拆分样式
    copyPublicDir: false, // 关闭vite自带拷贝public目录到build的功能
    lib: {
      entry: resolve(__dirname, 'packages/index.ts'), // 指向组件库核心代码入口文件
      name: 'ease-reactify-ui',
      fileName: (format) => `ease-reactify.${format}.js`,
      cssFileName: 'style', // 样式文件名
    },
    rollupOptions: {
      external: ['react', 'react-dom', 'react/jsx-runtime'],
      output: [
        {
          // es 产物配置
          format: 'es', // 打包格式
          entryFileNames: '[name].js', // 打包后的文件名
          exports: 'named', // 打包后的导出方式
          preserveModules: true, // 保留模块结构
          preserveModulesRoot: './packages/components', // 保留模块结构的根目录
          dir: 'packages/build/es', // 打包后的目录
          assetFileNames: (assetInfo) => { // 打包后的资源文件名
            if (assetInfo.names && assetInfo.names.some(name => name.endsWith('.css'))) {
              return path.join(path.dirname(assetInfo.names[0]), 'style.css')
            }
            return '[name].[ext]'
          },
        },
        {
          // cjs 产物配置
          format: 'cjs',
          entryFileNames: '[name].js',
          exports: 'named',
          preserveModules: true,
          preserveModulesRoot: 'packages/components',
          dir: 'packages/build/lib',
          assetFileNames: (assetInfo) => {
            if (assetInfo.names && assetInfo.names.some(name => name.endsWith('.css'))) {
              return path.join(path.dirname(assetInfo.names[0]), 'style.css')
            }
            return '[name].[ext]'
          },
        },
        {
          format: 'umd',
          entryFileNames: 'index.js',
          exports: 'named',
          name: 'ease-reactify-ui',
          dir: 'packages/build/dist',
          sourcemap: true,
          globals: {
            'react': 'React',
            'react-dom': 'ReactDOM',
            'react/jsx-runtime': 'jsxRuntime',
          }
        }
      ],
      plugins: [
        clear({
          targets: ['./packages/build'], // 清除目录
        }),
      ]
    }
  }
})

然后结合tsconfig.json文件进行如下的打包命令设置:

{
  "compilerOptions": {
    // 基础配置
    "module": "esnext", // 指定生成哪个模块系统代码
    "target": "es6", // 指定ECMAScript目标版本
    "jsx": "react", // 允许在.ts文件中使用JSX
    "moduleResolution": "Bundler", // 指定模块解析策略
    "resolvePackageJsonExports": true, // 支持从package.json的exports字段导入模块
    "resolveJsonModule": true, // 解析JSON模块
    "esModuleInterop": true, // 允许从默认导出导入模块
    "allowSyntheticDefaultImports": true, // 允许从没有默认导出的模块中默认导入
    "removeComments": true, // 删除注释
    "noEmit": true, // 
    "composite": false, // 不允许构建引用项目

    // 类型校验
    "skipLibCheck": true, // 跳过所有声明文件的类型检查
    "strict": true, // 启用所有严格类型检查选项
    "strictNullChecks": true, // 启用严格的null检查
    "noImplicitAny": true, // 禁止隐式any类型
    "noUnusedLocals": true, // 报告未使用的局部变量为错误
    "noUnusedParameters": true, // 报告未使用的参数为错误
    "stripInternal": true, // 移除所有内部属性
    "forceConsistentCasingInFileNames": true, // 强制区分文件引用的大小写

    // 编译配置
    "lib": ["esnext", "dom"], // 指定编译过程中要包含的库文件列表
    "baseUrl": ".", // 设置解析非相对模块的基地址,默认是当前目录
    "paths": {
      "ease-reactify": ["packages/components/index.ts"],
      "ease-reactify/es/*": ["packages/components/*"]
    }
  },
  // "include": ["packages/components/*.tsx", "packages/components/*.ts"],
  "exclude": [ // 排除编译目录
    "node_modules",
    "**/__tests__", // **代表任意目录,排除所有测试目录
    "review/**", // 排除review目录
    "vite.config.ts",
  ]
}

后期也可以结合具体情况再修改,最终呈现的效果如下所示:

代码格式配置

在我们进行代码编写的时候,如果想注意一些格式化内容如:代码属性是否换行、代码是否有用、代码是否有提示或代码格式化操作,如果需要统一代码格式的话还是需要配置eslint进行操作:

其实当我们拉取vite构建器创建react项目模板的时候,项目文件默认就已经生成了一个eslint的config文件在根目录中,默认配置如下所示:

import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'

export default tseslint.config(
  { ignores: ['dist'] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ['**/*.{ts,tsx}'],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh,
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      'react-refresh/only-export-components': [
        'warn',
        { allowConstantExport: true }
      ],
    },
  },
)

        早期的esLint 配置文件通常使用.eslintrc加上扩展名的形式如.eslintrc.json、.eslintrc.js或.eslintrc.yaml。其中.eslintrc.cjs是使用CommonJS模块格式的JavaScript文件,在eslint v8及以后引入了eslint.config.js作为一种新的配置文件格式,这种配置文件使用es模块(import和export),对于使用现代JavaScript开发尤其是使用es模块的项目,eslint.config.js提供了更自然的配置方式,符合现代 JavaScript 的开发习惯,这里我们可以访问:官网 来查询eslint的一些相关配置:

其实我们下载vite的react模板之后,基本上eslint已经帮助我们配置好了,不生效的原因是我们还需要安装一个eslint插件进行配合使用:

安装完插件之后,我们可以结合自身的一些编码习惯进行一些配置,如下:

像我们声明的一些未使用的变量就会直接被eslint检测到并提示报错了:

然后我们鼠标悬浮上去点击快速修复,就会给出一些修复建议让我们选择:

当然如果想控制一下我们代码的一些空格缩进或者编码格式的话,根目录新建.editorconfig文件对编辑器的一些代码规范进行设置,然后安装如下插件执行我们编辑器的配置文件:

如下所示可以看到我们配置的文件已经生效了:

组件文档搭建 

组件库文档搭建有好多方式,可以参考: 地址,因为这里我们采用使用vite进行组件库的打包,所以为了统一技术栈使用vitepress搭建在线文档,这里我们终端执行如下命令来执行我们的vitepress:

pnpm add -D vitepress
npx vitepress init

接下来根据选项配置我们的组件库文档,点击确定之后就会在我们的根目录下生成docs文件夹,并且在根目录下的package.json文件中也配置了相关命令:

然后我们终端执行 pnpm run docs:dev 命令允许项目,可以看到我们的组件库文档已经被成功运行:

然后接下来我们就可以仿照antd文档的格式来写我们自己的组件库文档:

在vitepress中我们配置一些路由选项的话可以在.vitepress文件下的config文件进行配置,如下:

然后我们就可以在我们的组件库文档的页面中进行查看我们设置的内容了:

后面文章会结合具体的组件进行详细演示!