这篇文章是我对开发 react-notion-block 库的一个记录总结。这边文章里不会涉及到具体代码,而是关注构建 React 组件库的基本流程。
概述
react-notion-block 组件库的开发流程,包含了如下特性:
- 基于 React
- 基于 Sass
- 基于 TypeScript (支持 TypeScript types)
- 基于 Storybook 在沙盒环境中展示和使用组件
- 对组件进行完整的测试 (使用 Jest/React 测试库)
- 最终作为单个 NPM 包发布
创建组件库
首先,初始化 npm 项目 npm init 。
接着,安装 react 库,npm i -D react react-dom @types/react @types/react-dom 。
最后,将 react 和 react-dom 配置为 Peer Dependencies。(保证安装开发的组件库时, react 和 react-dom 会作为 项目的 dependencies 安装)。
...
"peerDependencies": {
"react": ">=17.0.2",
"react-dom": ">=17.0.2"
}
...
配置 TypeScript
运行 npm i -D typescript @types/node ,配置 tsconfig.json 文件,一些具体配置的意义可以参考 这里。
{
"include": [
"src/**/*"
],
"compilerOptions": {
"declaration": true,
"declarationDir": "build",
"target": "ES2019",
"strict": true,
"lib": [
"DOM",
"DOM.Iterable",
"ES2019"
],
"sourceMap": true,
"jsx": "react-jsx",
"allowSyntheticDefaultImports": true,
"moduleResolution": "node"
},
"exclude": [
"node_modules",
"build",
"src/**/*.stories.tsx",
"src/**/*.test.tsx"
]
}
其中,declaration 和 declarationDir 指明了在构建文件夹中生成组件类型文件
配置 Rollup
安装依赖
npm i -D rollup \
rollup-plugin-peer-deps-external \
rollup-plugin-terser \
@rollup/plugin-node-resolve \
rollup-plugin-typescript2 \
@rollup/plugin-commonjs \
rollup-plugin-postcss \
node-sass
-
rollup -
rollup-plugin-peer-deps-external- 打包时排除 peer dependencies,减小组件库的体积 -
rollup-plugin-terser- 缩小生成的 es 包 -
@rollup/plugin-node-resolve- 打包第三方依赖模块 -
rollup-plugin-typescript2- 将 TypeScript 文件转换为 JavaScript。设置 "useTsconfigDeclarationDir": true 输出指定目录的 .d.ts 文件 -
@rollup/plugin-commonjs- 转换为 CommonJS (CJS) 格式 -
rollup-plugin-postcss- 将 Sass 转换为 CSS,配合 node-sass 一起使用 -
node-sass
配置 rollup.config.js 文件
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "rollup-plugin-typescript2";
import postcss from "rollup-plugin-postcss";
import { terser } from "rollup-plugin-terser";
const packageJson = require("./package.json");
export default {
input: "src/index.ts",
output: [
{
file: packageJson.main,
format: "cjs",
sourcemap: true
},
{
file: packageJson.module,
format: "esm",
sourcemap: true
}
],
plugins: [
peerDepsExternal(),
resolve(),
commonjs(),
typescript({ useTsconfigDeclarationDir: true }),
postcss(),
terser()
]
};
注意这里指定了两个 output
-
CommonJS - CJS
-
ES Modules - ESM
并在 package.json 中
-
main 字段指向 CommonJS 入口点
-
module 字段指向 ES Modules 入口点
-
以及 Rollup 运行命令
...
"main": "build/index.js",
"module": "build/index.es.js",
"files": ["build"],
...
"scripts": {
...
"build": "rollup -c",
}
...
配置 Storybook
运行 npx storybook init
npm install -D @storybook/preset-scss sass
npm install -D html-webpack-plugin
npm uninstall node-sass
npm install -D \
css-loader@5 \
sass-loader@10 \
style-loader@2
npm install -D node-sass
配置 .storybook/main.js
module.exports = {
...
addons: [
...
'@storybook/preset-scss'
],
};
说明:
-
当前,storybook 与 css-loader,sass-loader,style-loader有兼容问题,所以限制这三个库的版本
-
最新的node-sass要求 sass-loader 11 以上,所以重新安装
运行 npm run storybook
配置 Jest & React Testing Library
安装依赖库
npm i -D jest \
ts-jest \
@types/jest \
identity-obj-proxy \
@testing-library/react@12 \
@testing-library/jest-dom \
jest-environment-jsdom
配置 jest.config.js 文件
module.exports = {
roots: ["./src"],
setupFilesAfterEnv: ["./jest.setup.ts"],
moduleFileExtensions: ["ts", "tsx", "js"],
testPathIgnorePatterns: ["node_modules/"],
transform: {
"^.+\.tsx?$": "ts-jest"
},
testEnvironment: "jsdom",
testMatch: ["**/*.test.(ts|tsx)"],
moduleNameMapper: {
// Mocks out all these file formats when tests are run
"\.(jpg|ico|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
"identity-obj-proxy",
"\.(css|less|scss|sass)$": "identity-obj-proxy"
}
};
配置 jest.setup.ts 文件
import "@testing-library/jest-dom";
配置 package.json 文件
...
"scripts":
{
....
"test": "jest",
"test:watch": "jest --watch",
....
}
...
运行 npm run test:watch
测试代码
最终项目的文件结构,可以参考 这里 如下:
.storybook/
main.js
preview.js
.gitignore
jest.config.js
jest.setup.ts
LICENSE
package.json
rollup.config.js
tsconfig.json
# TODO
src/
TestComponent/
TestComponent.tsx
TestComponent.types.ts
TestComponent.scss
TestComponent.stories.tsx
TestComponent.test.ts
index.ts
以下是测试的代码,可以运行 npm run storybook 和 npm run test:watch 查看效果
// TestComponent.types.ts
export interface TestComponentProps {
theme: "primary" | "secondary";
}
// TestComponent.scss
.test-component {
background-color: white;
border: 1px solid black;
padding: 16px;
width: 360px;
text-align: center;
.heading {
font-size: 64px;
}
&.test-component-secondary {
background-color: black;
color: white;
}
}
// TestComponent.tsx
import React from "react";
import { TestComponentProps } from "./TestComponent.types";
import "./TestComponent.scss";
const TestComponent: React.FC<TestComponentProps> = ({ theme }) => (
<div
data-testid="test-component"
className={`test-component test-component-${theme}`}
>
<h1 className="heading">I'm the test component</h1>
<h2>Made with love by Harvey</h2>
</div>
);
export default TestComponent;
// TestComponent.stories.tsx
import React from "react";
import TestComponent from './TestComponent';
export default {
title: "TestComponent"
};
export const Primary = () => <TestComponent theme="primary" />;
export const Secondary = () => <TestComponent theme="secondary" />;
// TestComponent.test.ts
import React from "react";
import { render } from "@testing-library/react";
import TestComponent from "./TestComponent";
import { TestComponentProps } from "./TestComponent.types";
describe("Test Component", () => {
let props: TestComponentProps;
beforeEach(() => {
props = {
theme: "primary"
};
});
const renderComponent = () => render(<TestComponent {...props} />);
it("should have primary className with default props", () => {
const { getByTestId } = renderComponent();
const testComponent = getByTestId("test-component");
expect(testComponent).toHaveClass("test-component-primary");
});
it("should have secondary className with theme set as secondary", () => {
props.theme = "secondary";
const { getByTestId } = renderComponent();
const testComponent = getByTestId("test-component");
expect(testComponent).toHaveClass("test-component-secondary");
});
});
// index.ts
import TestComponent from "./TestComponent/TestComponent";
export { TestComponent };
发布
npm run build
npm publish
关于 react-notion-block 库
react-notion-block 库 是用来渲染 Notion 数据的一个库,支持 Notion 官方 API 的数据类型。使用这个库,我搭建了自己的技术博客:
- Notion 作为 Headless CMS
- Notion API 调用数据
- react-notion-block 渲染页面组件