组件库文档v1.0
2021.08.30
hzzly
上篇我们具体介绍了一些组件库的搭建,这篇我们将介绍一下组件库文档 1.0 的构建生成过程。
需求
首先来看一下组件库文档最基本的一些需求:
- 提供组件的介绍说明
- 提供组件的属性列表 Props
- 提供组件调用的示例源码 Usage
- 提供组件调用的演示 demo
方案
我们前期规范好了 markdown 说明文件以及 demo 文件,我们就可以直接通过 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, }; }
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
| 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); }
|
小结
在日常开发的过程中,我们除了组件的代码编写外,还有很多流程上、边角上的工作需要做,这些事情往往都比较琐碎又必须要做。我们多借助工具去解决我们的工作中那些零星简单的任务。