从零到一搭建一个完整React组件库
前言
开发一个组件库并供外部或者内部使用,是一个很普遍的需求,许多人通常在搭建中耗费了大量时间,本文将从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 即可 *
可以看到项目已经成功运行,默认启动在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
复制,然后稍作修改得来的,具体含义可见文档,不理解没关系,丝毫不影响我们的任何继续下一步.
可以看到页面多出了一栏:
我们参考了ant-design,将按钮放置在了通用这一栏,嘻嘻嘻,不错,我们可以稍微玩一下生成出来的文档,也可以自己定义生成其他栏目.
Storybook Css支持
默认情况下是支持导入.css
文件格式的,但通常我们可能需要less
、sass
这类的处理器,但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
即可:
构建打包
前文以及配置好了本地开发环境,但是最终我们需要构建出产物供用户下载使用,接下来我们使用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目录下我们可以看到打包出来的文件:
到这里我们已经成功打包出来文件了,但是如何检验打包出来的是否能正常使用?我们通常通过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;
可以看到页面正常显示了:
你可能看到的按钮样式不一样,是作者偷偷给他加了一点点漂亮的样式,不影响接下去的工作.
发布到npm
发布到npm实际上很简单,市面上也需要教程,咱们这里简单发布一下
npm login
然后发布
npm publish
注意: 我们只要发布dist文件夹就行,检查一下package.json的files字段是否正确哦
增加发布日志
这是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
可以看到ci正在运行,执行完毕我们打开tag看看
期间我们提交的commit都将作为changelog输出,非常友好.
部署文档
到这里,其实我们的工作已经完成,但是文档供用户访问必不可少,我们利用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
分支, 进入设置:
可以看到页面显示了:
但是按钮并未渲染出来,检查资源发现 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等其他细节需求,感谢你阅读到最后.
转载自:https://juejin.cn/post/7160905679799058469