导航
导航
文章目录
  1. 需求
  2. 方案
  3. 规范
  4. 技术实现
    1. node 读写 markdown 和 demo
    2. 自动生成组件及路由
    3. markdown 解析
  5. 小结

组件库文档v1.0

上篇我们具体介绍了一些组件库的搭建,这篇我们将介绍一下组件库文档 1.0 的构建生成过程。

需求

首先来看一下组件库文档最基本的一些需求:

  • 提供组件的介绍说明
  • 提供组件的属性列表 Props
  • 提供组件调用的示例源码 Usage
  • 提供组件调用的演示 demo

方案

我们前期规范好了 markdown 说明文件以及 demo 文件,我们就可以直接通过 node 的读写文件再配合 react 菜单路由的方式进行生成:

  • node 读写文件
  • react 路由

规范

在上篇我们讲到对于一个组件,需要在开发组件时编写好组件的说明文档 markdown 文件以及 demo,文档规范如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
## Input

#### 输入框组件。

## Input Props:

| 参数 | 说明 | 类型 | 默认值 |
| ---------- | ------------------------ | -------------------------------------------- | ------- |
| disabled | 是否禁用 | boolean | - |
| icon | input 图标 | React.ReactElement | - |
| iconPlace | input 图标渲染位置 | 'left' \| 'right' | 'right' |
| allowClear | 是否可清除,渲染清除图标 | boolean | false |
| showNumber | 是否渲染字数计算 | boolean | false |
| maxLength | 最大输入长度 | number | 70 |
| onChange | input 输入框改变的回调 | (e: ChangeEvent\<HTMLInputElement\>) => void | - |

> Tip:Input 的 props 不止上面列举的项,可传入原生 input 的所有属性

### Usage

```js
import { Input } from "@hzzly/components";

const onChange = (e) => {
console.log("Value: ", e.target.value);
};

ReactDOM.render(<Input icon={<Icon />} onChange={onChange} />, mountNode);
```

这也是我们目前定义的组件 markdown 规范,这样书写一个 markdown 文件就能满足前三点的需求,最后一点我们单独定义了一个 demo 文件来进行组件的演示和调试。

技术实现

node 读写 markdown 和 demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 读取组件库
*/
function readComponents() {
const markdowns = {};
const demos = {};
const components = [];
const dir = path.join(__dirname, "../src");
const dirs = fs
.readdirSync(dir)
.filter(
(f) => f.indexOf(".") < 0 && f.toLowerCase().indexOf("components") > -1
);
dirs.forEach((d) => {
const absolutePath = path.join(dir, d);
if (this.isDir(absolutePath)) {
fs.readdirSync(absolutePath).forEach((f) => {
components.push(f);
const mdPath = path.join(dir, `./${d}/${f}/index.md`);
const demoPath = path.join(dir, `./${d}/${f}/demo.tsx`);
if (fs.existsSync(mdPath)) {
markdowns[f] = fs.readFileSync(mdPath).toString();
}
if (fs.existsSync(demoPath)) {
demos[f] = `../src/${d}/${f}/demo`;
}
});
}
});

return {
components,
markdowns,
demos,
};
}

/**
* 写入markdown字符串
*/
function writeMd() {
let { markdowns } = readComponents();
markdowns = {
...markdowns,
Welcome: fs
.readFileSync(path.join(__dirname, `../src/welcome.md`))
.toString(),
};
const str = JSON.stringify(markdowns, "", "\t");
fs.writeFile(
path.join(__dirname, "../src/md.ts"),
`export default ${str}`,
(err) => {
if (err) {
console.error(err);
}
console.log("写入配置成功!");
}
);
}

自动生成组件及路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
/**
* 创建组件
*/
function createComponents() {
const { components, demos } = readComponents();
components.forEach((component) => this.createComponent(component, demos));
}

function createComponent(name, demos) {
const to = path.resolve(__dirname, `../src/pages/${name}.tsx`);
fs.ensureFileSync(to); // 确保文件存在(文件目录结构没有会新建)
const template = `import React from 'react';
import { markdown } from '@/utils';
${demo[name] ? `import Demo from '${demo[name]}';` : ""}

interface Props {}

const index: React.FC<Props> = () => {
return (
<>
<div
className="markdown"
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{ __html: markdown('${name}') }}
/>
<div className="demo">
<h2 className="demo-title">演示</h2>
${demo[name] ? "<Demo />" : "<p>演示代码</p>"}
</div>
</>
)
}

export default index;
`;
fs.outputFileSync(to, template);
}

/**
* 写入路由
*/
function writeRouter() {
const { components } = readComponents();
const template = `
import React from 'react';
import { RouteConfig } from "react-router-config";

import Home from '@/pages/Home';
import Welcome from '@/pages/Welcome';

const MarkDown = React.lazy(() => import(/* webpackChunkName: "IMarkDown" */ '@/pages/MarkDown'));
${components
.map(
(component) =>
`const ${component} = React.lazy(() => import(/* webpackChunkName: "I${component}" */ '@/pages/${component}'));`
)
.join("\n")}

const routes: RouteConfig[] = [
{
path: '/',
component: Home,
children: [
{ path: "/welcome", component: Welcome, name: 'Welcome' },
{ path: "/markdown", component: MarkDown, name: 'MarkDown测试' },
${components
.map((component) => {
if (component.toLowerCase().indexOf("js") > -1) {
return `{ path: "/${component.toLowerCase()}", component: ${component}, name: '${component}' },`;
} else {
return `{ path: "/${component
.replace(/\B([A-Z])/g, "-$1")
.toLowerCase()}", component: ${component}, name: '${component}' },`;
}
})
.join("\n ")}
],
},
];

export default routes;
`;
fs.outputFileSync(path.join(__dirname, `../src/router/index.ts`), template);
}

markdown 解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// utils.ts
import marked from "marked";
import hljs from "highlight.js/lib/core";
import javascript from "highlight.js/lib/languages/javascript";
import md from "@/md";

function markdown(component: string, isComponent = true): string {
marked.setOptions({
renderer: new marked.Renderer(),
highlight: (code: string) => {
return hljs.highlightAuto(code).value;
},
pedantic: false,
gfm: true,
tables: true,
breaks: false,
sanitize: false,
smartLists: true,
smartypants: false,
xhtml: false,
});
if (md[component]) {
return marked(md[component]);
} else if (isComponent) {
return marked(`## ${component}`);
}
return marked(component);
}

小结

在日常开发的过程中,我们除了组件的代码编写外,还有很多流程上、边角上的工作需要做,这些事情往往都比较琐碎又必须要做。我们多借助工具去解决我们的工作中那些零星简单的任务。