信息发布→ 登录 注册 退出

Vite + React从零开始搭建一个开源组件库

发布时间:2026-01-11

点击量:
目录
  • 前言
  • 目标
  • 搭建开发环境
    • ️生成模板
    • CSS预处理器
    • eslint
  • 组件库编译
    • ⚡️vite的打包配置
    • ⚡️自动生成ts类型文件
    • ⚡️样式懒加载与全量加载
  • 文档
    • ❤️npm 发布与发布前的测试
      • 测试

    前言

    日常使用开源的组件库时我们或多或少的都会做一些自定义的配置来符合实际的设计,当这些设计形成一定规模时,设计狮们就会形成一套规范,实施到前端这里就变成了组件库。

    本文的目标是从0开始搭建一个面向组件库的基础设施,一起来探索下吧~。

    目标

    • 开发环境
    • 组件库编译,需要生成umd和esm模块的组件代码
    • 支持按需导入与全量导入
    • 组件文档/预览
    • 代码格式化和规范检测工具

    搭建开发环境

    现在的时间点Vue或者React都可以用Vite来进行开发打包,这里有老前辈Vant的尝试我们可以放心使用~。

    ️生成模板

    yarn create vite my-components --template react-ts

    这里我们创建生成一套react-ts的应用模板,可以仅保留main.tsx用于组件库的开发调试。

    CSS预处理器

    CSS预处理器Sass与Less都可以选择,这里用了Sass:

    yarn add sass

    不需要配置直接用就可以,与它搭配的规则检查可以安装stylelint:

    yarn add stylelint stylelint-config-standard stylelint-config-prettier-scss stylelint-config-standard-scss stylelint-declaration-block-no-ignored-properties
    

    同时根目录下新建.stylelintrc:

    {
      "extends": [
        "stylelint-config-standard",
        "stylelint-config-prettier-scss",
        "stylelint-config-standard-scss"
      ],
      "plugins": [
        "stylelint-declaration-block-no-ignored-properties"
      ],
      "rules": {
        "no-descending-specificity": null,
        "no-invalid-position-at-import-rule": null,
        "declaration-empty-line-before": null,
        "keyframes-name-pattern": null,
        "custom-property-pattern": null,
        "number-max-precision": 8,
        "alpha-value-notation": "number",
        "color-function-notation": "legacy",
        "selector-class-pattern": null,
        "selector-id-pattern": null,
        "selector-not-notation": null
      }
    }

    具体的规则可以查看文档,或者直接用ant-design/vant的规范,总之制定一个用起来舒服的即可。

    eslint

    eslint与stylelint基本一个套路,这里不再重复,可以直接用开源组件库的规范。

    组件库编译

    组件库的编译和默认的应用编译有一些不同,Vite有预设的打包组件库的选项可以帮我们省去大部分自定义的时间。

    ⚡️vite的打包配置

    import { defineConfig } from 'vite'
    import react from '@vitejs/plugin-react'
    
    const path = require("path");
    
    const resolvePath = (str: string) => path.resolve(__dirname, str);
    
    export default defineConfig({
      plugins: [react()],
      build: {
        lib: {
          entry: resolvePath("packages/index.ts"),
          name: "componentsName",
          fileName: format => `componentsName.${format}.js`,
        },
        rollupOptions: {
            external: ["react", "react-dom", "antd"],
            output: {
              globals: {
                react: "react",
                antd: "antd",
                "react-dom": "react-dom",
              },
            },      
        },
      },
    })

    这里我们的入口不是上面的main.tsx,组件库的打包入口需要是一个包含了所有组件的索引文件,大概可以长这样:

    import Button from "./button/index";
    export { Button };

    默认情况下Vite的配置会打包umd和esm两种模式,只需要写一下名字即可。

    同时在打包时我们也不希望外部的库打包进去,像必然存在的reactvue,二次封装的组件库antdvant这些都需要剔除出去。

    现在直接build可以看到生成了esumd两份不同版本的文件,里面仅存在我们的代码:

    yarn build
    $ tsc && vite build
    vite v2.9.12 building for production...
    ✓ 10 modules transformed.
    dist/componentsName.es.js   2.27 KiB / gzip: 0.97 KiB
    dist/componentsName.umd.js   1.81 KiB / gzip: 0.96 KiB
    ✨  Done in 3.12s.

    ⚡️自动生成ts类型文件

    打包进行到上面已经初步可用,还不具备Ts的类型定义,用在Ts项目里会报错,这里我们可以用Ts的rollup插件来生成对应的类型:

    yarn add @rollup/plugin-typescript tslib

    Vite中的rollupOptions扩展一下plugins:

    {
      ...,
      rollupOptions: {
        ...,
        plugins: [
          typescript({
            target: "es2015", // 这里指定编译到的版本,
            rootDir: resolvePath("packages/"),
            declaration: true,
            declarationDir: resolvePath("dist"),
            exclude: resolvePath("node_modules/**"),
            allowSyntheticDefaultImports: true,
          }),
        ],
      }
    }

    重新打包会发现所有packages目录下的文件都生成了一份d.ts的类型定义。

    ⚡️样式懒加载与全量加载

    日常应用的开发时我们会在组件里导入样式,这样打包时构建工具会自动处理。

    在构建组件库时为了支持更多的环境考虑,组件内不会导入样式,样式需要单独处理。

    可以选择配置多入口或者用插件,Vite没有找到如何配置多入口,所以这里选择了用插件的方式。

    开发时由于我们的组件单独组件内不会导入具体的样式,可以在开发的入口处导入全量样式省去手工导入的麻烦:

    const req = import.meta.globEager("./*/style/index.scss");
    
    export default req;

    插件没有找到可以直接用的插件,这里自己写了一个:

    import { compile } from "sass";
    import postcss from "postcss";
    import postcssImport from "postcss-import";
    
    const autoprefixer = require("autoprefixer");
    
    const path = require("path");
    
    const resolvePath = str => path.resolve(__dirname, str);
    
    const glob = require("glob");
    
    function generateCssPlugin() {
      return {
        name: "generate-css",
        async generateBundle() {
          const files = glob.sync(resolvePath("packages/**/style/*.scss"));
    
          const allProcess = [];
          const allRawCss = [];
    
          files.forEach(file => {
            const { css } = compile(file);
            allRawCss.push(css);
            const result = postcss([autoprefixer, postcssImport]).process(css, {
              from: file,
              to: file.replace(resolvePath("packages"), "dist"),
            });
    
            allProcess.push(result);
          });
    
          const results = await Promise.all(allProcess);
    
          results.forEach(result => {
            this.emitFile({
              type: "asset",
              fileName: result.opts.from
                .replace(resolvePath("packages"), "dist")
                .replace("dist/", "")
                .replace("scss", "css"),
              source: result.css,
            });
          });
    
          // 上半部分编译单独的css,下半部分会把所有css编译为一整个。
    
          const wholeCss = await postcss([autoprefixer, postcssImport]).process(
            allRawCss.join("\n")
          );
    
          this.emitFile({
            type: "asset",
            fileName: "styles.css",
            source: wholeCss.css,
          });
        },
      };
    }
    

    generateBundle是rollup的插件运行钩子,更多信息可以在这里找到。

    再次打包可以看到生成了单个的样式与全量的样式,全量的可以走CDN导入,按需加载的可以用如vite-plugin-imp的构建工具进行按需加载。

    文档

    文档我们需要同时兼顾到预览,这里我们可以选择storybook:

    npx storybook init
    

    之后不需要配置,直接用即可。

    内置的mdx文件可以让我们同时写Markdown与jsx:

    import { Meta, Story } from "@storybook/addon-docs";
    
    import { Button } from "../packages";
    
    <Meta title="Button" component={Button} />
    
    <Canvas>
      <Story name="Button">
        <Button>这里写Jsx</Button>
      </Story>
    </Canvas>
    
    # 用法
    
    **markdown 语法**

    ❤️npm 发布与发布前的测试

    npm的发布流程比较简单,直接

    npm login
    
    npm version patch
    
    npm publish

    就可以了,对于私有的npm仓库地址我们可以在package.json中定义:

    {
      "publishConfig": {
        "registry": "https://npm.private.com"
      }
    }
    

    ,除此之外package.json中我们最好还要定义一下此组件库的基础入口信息:

    {
      "main": "./dist/componentsName.umd.js",
      "module": "./dist/componentsName.es.js",
      "typings": "./dist/index.d.ts",
    }
    

    测试

    发布前的测试不同于单元测试(本文没有折腾单元测试),我们需要将打包好的库给实际的项目去使用,模拟安装发布后的包:

    在组件库目录运行:

    npm link

    这样会基于当前目录的名字创建一个符号链接,之后在实际的项目中再次运行:

    npm link componentsName

    此时node_modules中对应的包会链接到你的组件库中,在组件库的任何修改都可以及时反馈。

    当然不仅仅用于测试,开发时也可以用这种方式。

    在线客服
    服务热线

    服务热线

    4008888355

    微信咨询
    二维码
    返回顶部
    ×二维码

    截屏,微信识别二维码

    打开微信

    微信号已复制,请打开微信添加咨询详情!