likes
comments
collection
share

从零到一搭建一个完整React组件库

作者站长头像
站长
· 阅读数 19

前言

开发一个组件库并供外部或者内部使用,是一个很普遍的需求,许多人通常在搭建中耗费了大量时间,本文将从0到1搭建一个包括 文档自动化部署单元测试开发环境、十分全面的组件库开发模板,如果你想快速的拉取一个模板,本模板已经发布为脚手架模板,你可以通过如下参数快速拉取:

npm create dino --template=react-component-library

本文github地址: react-component-library-template

初始化

我们创建一个项目并进入该文件进行初始化

mkdir react-component-library && cd react-component-library

我们创建了一个名为 react-component-library 的文件夹并进入其中,接下来我们进行初始化

npm init

安装所需依赖

组件库最好都是通过typescript开发,以便友好的提示输入输出,我们安装相关依赖

npm install react @types/react react-dom typescript -D

安装完毕后可见如下字段,我们的React版本为最新版18.x

"devDependencies": {
	"@types/react": "^18.0.24",
	"react": "^18.2.0",
	"typescript": "^4.8.4",
	"react-dom": "^18.2.0"
}

创建基础开发目录

.

├── src
│ ├── components
| │ ├── Button
| | │ ├── Button.tsx // 组件的核心逻辑
| | │ └── index.ts   // 统一导出组件 
| │ └── index.ts     // 导出所有组件
│ └── index.ts       // 外部引用的入口
├── package.json
└── package-lock.json

我们创建出如上的基础开发目录结构,接下来便开始着手写代码.

src/components/Button/Button.tsx


import React from "react";

export interface ButtonProps {
  label: string;
}

export const Button = (props: ButtonProps) => {
  return <button>{props.label}</button>;
};

为了保持项目初期简化,只创建了一个基础按钮组件,并默认导出

修改 src/components/Button/index.ts

export * from "./Button";

然后,我们从组件目录导出该按钮:

修改 src/components/index.ts

export * from "./Button";

最后,我们从基本src目录导出所有组件:

src/index.ts

export * from './components';

启动项目

到现在为止,我们还未正式启动项目,但请别着急,我们将使用storybook来开发以及调试我们的应用,最终也将其发布为在线文档可供用户访问。

为什么使用storybook而不是其他 ?

storybook友好且维护积极并且开箱即用,新手最忌讳的是使用一些过新或者维护不及时的工具,且不应该把时间浪费在选型以及踩坑上面,因此我们选择了storybook

npx storybook init --builder vite

安装过程可以有点缓慢,需要耐心等待一下,如出现以下错误,请查看该解决办法也可忽略 :

gyp: No Xcode or CLT version detected!

安装完毕后,我们需要再安装vite:

npm install vite -D

接着运行项目:

npm run storybook 

*注: 如果运行出错,我们需要重新 npm install 即可 *

从零到一搭建一个完整React组件库

可以看到项目已经成功运行,默认启动在6006端口,但是我们发现左侧的Button组件并不是我们最初编写的,而是他自动生成了一个,接下来我们着手修改:

新增 src/components/Button/Button.stories.tsx 并输入:

import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { Button } from './Button';

// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export

export default {
	title: '通用/Button',
	component: Button,
	// More on argTypes: https://storybook.js.org/docs/react/api/argtypes
	argTypes: {
		backgroundColor: { control: 'color' },
	},
} as ComponentMeta<typeof Button>;

// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template: ComponentStory<typeof Button> = (args) => <Button {...args} />;

  

export const Primary = Template.bind({});

// More on args: https://storybook.js.org/docs/react/writing-stories/args

Primary.args = {
	label: 'Button',
};

export const Secondary = Template.bind({});

Secondary.args = {
	label: 'Button 2',
};

突然多了这么多代码!不用担心,其实是我们从stories/Button.stories.tsx 复制,然后稍作修改得来的,具体含义可见文档,不理解没关系,丝毫不影响我们的任何继续下一步.

可以看到页面多出了一栏:

从零到一搭建一个完整React组件库

我们参考了ant-design,将按钮放置在了通用这一栏,嘻嘻嘻,不错,我们可以稍微玩一下生成出来的文档,也可以自己定义生成其他栏目.

Storybook Css支持

默认情况下是支持导入.css文件格式的,但通常我们可能需要lesssass这类的处理器,但Storybook默认并不支持,我们需要安装一些依赖,sass 与 less 是同类型的,因此本文只演示其中一个。

新增 src/components/Button/Button.scss :

button {
	color: #1ea7fd;
}

src/components/Button/Button.tsx 导入使用:

import React from "react";

// 新增
import './Button.scss'

// ....

这种情况下我们无法正常使用,因为storybook无法解析,需要配置对应的loader,安装依赖:

npm install sass -D

可以看到的是已经生效了 字体颜色变为了#1ea7fd, 对于less的配置也是相同,依赖于vite的文档,其实我们如果想支持less,只需要npm install less -D 即可:

从零到一搭建一个完整React组件库

构建打包

前文以及配置好了本地开发环境,但是最终我们需要构建出产物供用户下载使用,接下来我们使用rollup将将组件进行打包发布.

typescript 配置

在打包之前,通常我们非常依赖于本地的typescript配置,当然你没有该配置也是可以运行项目,但将缺少许多特性.

执行命令:

npx tsc --init

该命令生成了一个tsconfig.json文件,我们将增加如下配置:

{
  "compilerOptions": {
    // 默认
    "target": "ES5", // 修改
    "esModuleInterop": true, 
    "forceConsistentCasingInFileNames": true,
    "strict": true, 
    "skipLibCheck": true,
    
    // 新增
    "jsx": "react", 
    "module": "ESNext",  
    "declaration": true,
    "declarationDir": "types",
    "sourceMap": true,
    "outDir": "dist",
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "emitDeclarationOnly": true,
  }
}

当前使用的是ts版本是4.8.4,为了防止后期官方修改初始化的配置,请你也检查是否一一对应,标记为新增的是我们当前版本默认未开启的,我们添加到tsconfig.json中。

  • "jsx": "react" -- 将JSX转换为React代码。
  • "module": "ESNext" -- 指定生成的模块代码。
  • "declaration": true -- 为我们的库类型输出一个.d.ts文件
  • "declarationDir": "types" -- 将.d.ts文件放在哪里,我们放在types里
  • "sourceMap": true -- 将JS代码映射回其TS文件源进行调试
  • "outDir": "dist" -- 将生成项目的目录
  • "moduleResolution": "node" -- 遵循node.js规则查找模块
  • "allowSyntheticDefaultImports": true -- 当模块没有默认导出时,允许“从y导入x”。
  • "emitDeclarationOnly": true -- 只生成声明.d.ts文件,而不会生成js文件

rollup 配置

我们将依靠以下插件来初始配置我们的库:

npm install rollup @rollup/plugin-node-resolve @rollup/plugin-typescript @rollup/plugin-commonjs rollup-plugin-dts rollup-plugin-postcss postcss --save-dev
  • @rollup/plugin-node-resolve
  • @rollup/plugin-typescript
  • @rollup/plugin-commonjs
  • rollup-plugin-dts 用于生成.d.ts
  • rollup-plugin-dts
  • postcss

创建 rollup.config.mjs 位于根目录,.mjs代表该文件是esmodule规范,我们想正确的使用import,内容如下:


import { readFileSync } from 'node:fs';

import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
import postcss from "rollup-plugin-postcss";

const pkg = JSON.parse(readFileSync('./package.json'));

export default [
  {
    input: "./src/index.ts",
    output: [

      {
        file: pkg.main,
        format: "cjs",
        sourcemap: true,
      },
      {
        file:  pkg.module,
        format: "esm",
        sourcemap: true,
      },
    ],
    plugins: [
      resolve(),
      commonjs(),
      typescript({ tsconfig: "./tsconfig.json"}),
      postcss(),
    ],
  },
  {
    input: "./dist/esm/index.d.ts",
    output: [{ file: "./dist/index.d.ts", format: "esm" }],
    plugins: [dts()],
    external: [/\.(css|less|scss)$/],
  },
];

接下来需要修改 package.json

{
  "name": "@yucccc/template-react-component-library",
  "version": "0.0.1",
  "description": "template for react component",
  "main": "./dist/cjs/index.js",
  "module": "./dist/esm/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js"
    }
  },
  "files": [ "dist" ],
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook",
    "build": "rollup -c"
  },
  "author": "yucccc",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.19.6",
    "@rollup/plugin-commonjs": "^23.0.2",
    "@rollup/plugin-node-resolve": "^15.0.1",
    "@rollup/plugin-typescript": "^9.0.2",
    "@storybook/addon-actions": "^6.5.13",
    "@storybook/addon-essentials": "^6.5.13",
    "@storybook/addon-interactions": "^6.5.13",
    "@storybook/addon-links": "^6.5.13",
    "@storybook/builder-vite": "^0.2.5",
    "@storybook/react": "^6.5.13",
    "@storybook/testing-library": "0.0.13",
    "@types/react": "^18.0.24",
    "babel-loader": "^8.2.5",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "rollup": "^3.2.3",
    "rollup-plugin-dts": "^5.0.0",
    "rollup-plugin-postcss": "^4.0.2",
    "sass": "^1.55.0",
    "typescript": "^4.8.4",
    "vite": "^3.2.2",
    "postcss": "^8.4.18"
  },
  "dependencies": {
  }
}


我们新增了

  • "main": "./dist/cjs/index.js"
  • "module": "./dist/esm/index.js"
  • "types": "./dist/index.d.ts"
  • "files": [ "dist" ]
  • "build": "rollup -c"

执行命令:

npm run build

打包成功:


./src/index.ts → ./dist/cjs/index.js, ./dist/esm/index.js...
created ./dist/cjs/index.js, ./dist/esm/index.js in 3.1s

./dist/esm/index.d.ts → ./dist/index.d.ts...
created ./dist/index.d.ts in 24ms

在dist目录下我们可以看到打包出来的文件:

从零到一搭建一个完整React组件库

到这里我们已经成功打包出来文件了,但是如何检验打包出来的是否能正常使用?我们通常通过npm link 来在本地测试是否正常:

npm link

测试打包库

通常直接发布到npm上,然后再下载下来并不是明智的选择,我们上一步已经npm link链接到本地,下面我们随意新建一个react项目进行测试。

npx create-react-app test-react-app --template typescript

进入 test-react-app内进行link

 npm link @yucccc/template-react-component-library

📢 注意: @yucccc/template-react-component-library 为你的package.json name字段,如果你定义的是其他,需要进行替换

/test-react-app/src/App.tsx

import React from 'react';
import logo from './logo.svg';
import './App.css';
//  +++++++ 新增 +++++++
import { Button } from '@yucccc/template-react-component-library'
function App() {
  return (
    <div className="App">
      <header className="App-header">
	    //  +++++++ 新增 +++++++
        <Button label='简单的按钮'></Button>
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.tsx</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

可以看到页面正常显示了:

从零到一搭建一个完整React组件库

你可能看到的按钮样式不一样,是作者偷偷给他加了一点点漂亮的样式,不影响接下去的工作.

发布到npm

发布到npm实际上很简单,市面上也需要教程,咱们这里简单发布一下

npm login

然后发布

npm publish

注意: 我们只要发布dist文件夹就行,检查一下package.json的files字段是否正确哦

增加发布日志

从零到一搭建一个完整React组件库

这是ant-design的版本日志,我们也想拥有怎么办?其实不难,我们新建如下目录结构,利用cicd来帮我们完成.

.

├── .github
│ ├── workflows
| │ ├── release.yml


我们使用changelogithub一键搞定

release.yml

name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v3
        with:
          node-version: 16.x

      - run: npx changelogithub
        env:
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

意思为:当我们发布一个以v开头的tag的时候,帮我自动生成changelog.

提交到github

需要先将提交到github,注意提交前需要创建.gitignore并写上对应的忽略文件.

假设你已经提交到github完毕,接下来我们创建一个tag,我们可以使用任意工具来创建一个tag, 这里我为了演示,将使用git命令行:

git tag -a v0.0.8 -m "release v0.0.8"

创建了一个命名为v0.0.8的tag 提交信息为 "release v0.0.8"

推送到远程master分支

git push origin master --tags

从零到一搭建一个完整React组件库

可以看到ci正在运行,执行完毕我们打开tag看看

从零到一搭建一个完整React组件库

期间我们提交的commit都将作为changelog输出,非常友好.

从零到一搭建一个完整React组件库

部署文档

到这里,其实我们的工作已经完成,但是文档供用户访问必不可少,我们利用GitPage来部署我们的文档,storybook部署文档

测试打包后文档是否正常访问

npm run build-storybook

默认会打包到storybook-static文件夹下

npx http-server storybook-static

访问 http://127.0.0.1:8080 查看是否正常,当前默认开启的端口是8080,后续可能因为http-server更新而改动,需要根据实际提示进行访问。

配置GitPage

增加一个ci文件

.github/workflows/storybook.yml


name: Storybook
on:
  push:
    branches:
      - main # if any push happens on branch `main`, run this workflow. You could also add `paths` to detect changes in specific folder

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2.3.1

      - name: Install and Build
        run: |
          npm ci
          npm run build-storybook

      - name: Deploy
        uses: JamesIves/github-pages-deploy-action@3.6.2
        with:
          branch: gh-pages
          folder: storybook-static # npm run build-storybook输出的文件夹

提交代码 可以看到正在运行ci 此时已经两个任务, 执行完毕后

我们可以看到多出了一个gh-pages分支, 进入设置:

从零到一搭建一个完整React组件库

可以看到页面显示了:

从零到一搭建一个完整React组件库

但是按钮并未渲染出来,检查资源发现 iframe.js 404了 ,果然,我本地没问题,怎么上线就出了问题呢?原因我们大致已经猜到了,资源路径错误,我们修改下打包资源路径访问问题

.storybook/main.js


module.exports = {
  "stories": [
    "../src/**/*.stories.mdx",
    "../src/**/*.stories.@(js|jsx|ts|tsx)"
  ],
  "addons": [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions"
  ],
  "framework": "@storybook/react",
  "core": {
    "builder": "@storybook/builder-vite"
  },
  //  +++++ 新增 ++++ 
  async viteFinal(config) {
    config.base = '/react-component-library-template/'
    return config
  },
  "features": {
    "storyStoreV7": true
  }
}

📢注意: base的路径为你github仓库的路径,本文中github仓库项目名为 react-component-library-template

再次访问,已经正常显示.

最后

本文已经完成自动发布,changelog,文档访问,发布下载等,已经能满足日常需求,后续将更新jest等其他细节需求,感谢你阅读到最后.