react组件库开发实录·二 添加less,使用mdx编写项目文档

First Post:

Last Update:

上期地址:react组件库开发实录·一 项目搭建

项目添加less

首先,你需要先安装lessless-loader

1
npm install less less-loader --save-dev

然后将该loader添加到webpack的配置中去,例如:
webpack.config.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
module: {
rules: [
{
test: /\.less$/i,
use: [
// compiles Less to CSS
'style-loader',
'css-loader',
'less-loader',
],
},
],
},
};

然后把之前的/src/doc/index.css改成/src/doc/index.less

使用mdx编写组件文档

安装@mdx-js/loader

1
npm install --save-dev @mdx-js/loader

配置webpack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
module: {
// ...
rules: [
// ...
{
test: /\.mdx$/,
use: [
{
loader: 'babel-loader',
options: {
presets: ["@babel/env", "@babel/preset-react"]
}
},
{ loader: '@mdx-js/loader' }
]
}
]
}
}

新建声明文件touch src/types/mdx.d.ts:

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
declare module '@mdx-js/react' {
import * as React from 'react'
type ComponentType =
| 'a'
| 'blockquote'
| 'code'
| 'delete'
| 'em'
| 'h1'
| 'h2'
| 'h3'
| 'h4'
| 'h5'
| 'h6'
| 'hr'
| 'img'
| 'inlineCode'
| 'li'
| 'ol'
| 'p'
| 'pre'
| 'strong'
| 'sup'
| 'table'
| 'td'
| 'thematicBreak'
| 'tr'
| 'ul'
export type Components = {
[key in ComponentType]?: React.ComponentType<{children: React.ReactNode}>
}
export interface MDXProviderProps {
children: React.ReactNode
components: Components
}
export class MDXProvider extends React.Component<MDXProviderProps> {}
// mdx: any;
const mdx: any;
}

declare module '*.mdx' {
let MDXComponent: (props: any) => JSX.Element
export default MDXComponent
}

配置tsconfig:

1
2
3
4
5
6
7
8
{
// ...
"include": [
"**/*.ts",
"**/*.tsx",
"**/*.mdx",
],
}

使用:
src/doc/index.tsx:

1
2
3
4
5
6
7
8
9
10
import React from 'react';
import { render } from "react-dom";
import { HashRouter } from 'react-router-dom';
import 'antd/dist/antd.css';
import App from './app';
import { AppRoute } from '@components';
import routes from './routes';

const rootElement = document.getElementById("root");
render(<HashRouter><App><AppRoute routeList={routes} /></App></HashRouter>, rootElement);

src/doc/app.tsx:

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
import React from 'react';
import { Layout, Menu } from 'antd';
import './index.less';
import { Link } from 'react-router-dom';
import { MDXProvider } from '@mdx-js/react';
import CodeBlock from '@doc/components/codeBlock';

const { Header, Content, Footer, Sider } = Layout;
const { SubMenu } = Menu;

const components = {
pre: props => <div {...props} />,
code: (props) => <CodeBlock {...props} />
}

export default (props) => {

return (
<Layout style={{ minHeight: '100vh' }}>
<Sider collapsible>
<div className="logo">
React-ERP
</div>
<Menu theme="dark" defaultSelectedKeys={['components']} mode="inline">
<SubMenu key="components" title="Components">
<Menu.Item key="components-table">
<Link to="/components/table">Table</Link>
</Menu.Item>
</SubMenu>
</Menu>
</Sider>
<Layout className="site-layout">
<Header className="site-layout-background" style={{ padding: 0 }} />
<Content style={{ margin: '16px' }}>
<div className="site-layout-background" style={{ padding: 24, minHeight: 360 }}>
<MDXProvider components={components}>
{props.children}
</MDXProvider>
</div>
</Content>
<Footer style={{ textAlign: 'center' }}>react-erp ©2021 Created by fssc</Footer>
</Layout>
</Layout>
);
}

src/doc/routes.tsx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React from "react";
import { RouteData } from "@components";
import Table from '@doc/pages/components/table/index.mdx';

const routes: RouteData[] = [
{ path: '/', loader: import('@doc/app') },
{
path: 'components',
children: [
{ path: 'table', element: <Table /> }
],
},
];

export default routes;

src/doc/components/codeBlock/index.tsx:

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
import React, { useState } from 'react';
import Highlight, { defaultProps } from 'prism-react-renderer';
import { LiveProvider, LiveEditor, LiveError, LivePreview } from 'react-live';
import { mdx } from '@mdx-js/react';
import { Button, Collapse, message } from 'antd';
import { CopyToClipboard } from 'react-copy-to-clipboard';

const { Panel } = Collapse;

export default ({ children, className, live, render }) => {
const [collapseShow, setCollapseShow] = useState<boolean>(true);
const language = className.replace(/language-/, '')

if (live) {
return (
<div style={{ marginTop: '40px', backgroundColor: 'black' }}>
<LiveProvider
code={children.trim()}
transformCode={code => '/** @jsx mdx */' + code}
scope={{ mdx }}
>
<LivePreview />
<LiveEditor />
<LiveError />
</LiveProvider>
</div>
)
}

if (render) {
return (
<div style={{ marginTop: '40px' }}>
<LiveProvider code={children}>
<LivePreview />
</LiveProvider>
</div>
)
}

const handleCopy = (str) => {
if (str) {
message.success('复制成功!');
} else {
message.warning('复制失败,请手动复制!');
}
};

const genExtra = () => (
<CopyToClipboard
text={children} // 需要复制的文本
onCopy={handleCopy}
>
<Button type='link'>复制代码</Button>
</CopyToClipboard>

);

return (
<Collapse bordered={false} defaultActiveKey={['1']} className='doc-custom-collapse'>
<Panel header='代码部分' key='1' extra={genExtra()} collapsible='header'>
<Highlight {...defaultProps} code={children.trim()} language={language}>
{({ className, style, tokens, getLineProps, getTokenProps }) => (
<pre className={className} style={{ ...style, padding: '20px' }}>
{tokens.map((line, i) => (
<div key={i} {...getLineProps({ line, key: i })}>
{line.map((token, key) => (
<span key={key} {...getTokenProps({ token, key })} />
))}
</div>
))}
</pre>
)}
</Highlight>
</Panel>
</Collapse>
)
}

src/doc/pages/components/table/baseDemo/tsx:

1
2
3
4
5
6
import { Table } from '@components';
import React from 'react';

export default () => {
return <Table />
};

src/doc/pages/components/table/index/mdx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
## Table

### 基础使用

import BaseTable from "./baseDemo";

<BaseTable />

```js
import { Table } from "@components";
import React from "react";

export default () => {
return <Table />;
};

使用ESLint

安装:

1
npm install eslint --save-dev

配置touch .eslintrc.js

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
85
86
87
88
89
90
91
92
93
94
95
96
97
/* 
* "off"或者0,不启用这个规则
* "warn"或者1,出现问题会有警告
* "error"或者2,出现问题会报错
* */
module.exports = {
"env": {
"browser": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/eslint-recommended",
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
settings: {
//自动发现React的版本,从而进行规范react代码
react: {
pragma: 'React',
version: 'detect'
}
},
"plugins": [
"react",
"@typescript-eslint"
],
"rules": {
"no-unused-vars": 0,//不能有声明后未被使用的变量或参数
"prefer-const": 0,//必须使用const
"quotes": [2, "single"], //单引号
"no-irregular-whitespace": 2, //不规则的空白不允许
"no-trailing-spaces": 1, //一行结束后面有空格就发出警告
'jsx-quotes': [0, "prefer-single"], //强制在 JSX 属性中一致地使用单引号
"react/no-deprecated": 1, //不使用弃用的方法
"no-unreachable": 1, //不能有无法执行的代码
"react/no-array-index-key": 2, //防止在数组中遍历中使用数组key做索引
"space-before-function-paren": 0, // 函数定义时括号前面要不要有空格
"eol-last": 0, // 文件以单一的换行符结束
"no-extra-semi": 1, // 可以多余的冒号
"semi": 0, // 语句可以不需要分号结尾
"eqeqeq": 0, // 必须使用全等
"one-var": 0, // 连续声明
'react/prop-types': 0,
"react/display-name": 0,
"react/no-unescaped-entities": 0,
"no-self-assign": 0,//不能与自己浅拷贝
"no-empty": 0,//不能留空
"max-len": ["error", { code: 300 }], //代码过长是否警报
"no-nested-ternary": 2, // 禁止嵌套三元表达式
'no-compare-neg-zero': 2, //禁止与 -0 进行比较
'no-cond-assign': 2,//无条件assign
'no-control-regex': 2,//可控正则
'no-dupe-args': 2,//无重复参数
'no-duplicate-case': 2,
'no-extra-semi': 2,//不能有额外分号
'no-extra-parens': 0,//无外部括号
'no-unexpected-multiline': 2,//没有意外的多行
'no-else-return': 2,//else里面不能有return
'no-magic-numbers': 0,//没有魔术数字
'no-multi-spaces': 2,//不能有多余的空格
'no-redeclare': 2,//不重新声明
'no-self-compare': 2,//没有自我比较
'no-unmodified-loop-condition': 2,//循环总没有为定义的条件
'camelcase': 0,//驼峰变量
'comma-style': 0,//逗号样式
'key-spacing': 2, //强制在对象字面量的属性中键和值之间使用一致的间距
'linebreak-style': 2, //强制使用一致的换行风格
'no-compare-neg-zero': 2, //禁止与 -0 进行比较
'no-bitwise': 2,//没有位运算
'no-lonely-if': 0,//不能有单独的if
'no-multi-assign': 2,//无多重assign
'no-multiple-empty-lines': 2,//没有多的空行
'semi': 2,//分号
'semi-spacing': 2,//分号空格
'semi-style': 0,//style内分号
'sort-keys': 0,//排序数组key值
'no-confusing-arrow': 2,//不能有不规范的箭头函数
"no-lone-blocks": 0,//没有行内块
"no-undef": 0,//可以使用undefined
"array-bracket-newline": 0,//数组是否可以换行
"prettier/prettier": ["error", { singleQuote: true, jsxSingleQuote: true, printWidth: 160, },]//单引号;最大宽度
}
};

配置vscode:
.vscode/settings.json:

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
{
"files.eol": "\n",
"editor.tabSize": 2,

"eslint.format.enable": true,
//autoFix默认开启,只需输入字符串数组即可
"eslint.validate": [
"javascript",
"typescript",
"react",
"html"
],

"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true, // 所有插件的所有可自动修复的ESLint错误都将在保存时修复
},
"eslint.codeAction.showDocumentation": {
"enable": true // 在快速修复菜单中显示打开的eslint规则文档网页。
},
"[typescriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
},
"[html]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"editor.suggestSelection": "first",
}