Skip to content

Commit 6736ee5

Browse files
committed
feat: add doc
1 parent 4a6002e commit 6736ee5

File tree

1 file changed

+274
-0
lines changed

1 file changed

+274
-0
lines changed

DOC.md

+274
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
## 背景
2+
由于业务需要展示一份直观的React文档,不但要能处理Markdown文件,还需要能展示源码、运行源码、展示示例和API。
3+
## 调研
4+
通过一系列尝试,发现目前的loader并不完善,无法完全满足业务需求。
5+
6+
* **markdown-it、marked等**:能完整解析Markdown,但无法运行React代码。
7+
* **markdown-it-react-loader**:对Markdown解析不完整,基本table都不支持。
8+
* **react-markdown-loader**:对Markdown解析不完整,无法友好展示源码;仅能渲染html,无法运行jsx。
9+
* **react-for-markdown-loader**:对Markdown解析不完整,且不支持运行代码,仅表示在react中使用。
10+
11+
## loader 的编写
12+
### 1. 如何编写一个 loader
13+
14+
#### 官网描述如下:
15+
> loader 是导出为 function 的 node 模块。
16+
> 当资源应该由此 loader 转换时,调用此函数。
17+
> 在简单的情况下,当只有一个 loader 应用于资源时,调用 loader 有一个参数:作为字符串的资源文件的内容。
18+
> 在 loader 中,可以通过 this 上下文访问 [[loader API | loaders]]
19+
20+
#### 示例
21+
```
22+
// 定义 loader
23+
module.exports = function(source) {
24+
return source;
25+
};
26+
```
27+
#### 更多描述:
28+
> 一个同步 loader 可以通过 return 来返回这个值。在其他情况下,loader 可以通过 this.callback(err, values...) 函数返回任意数量的值。错误会被传到 this.callback 函数或者在同步 loader 中抛出。
29+
> 这个 loader 的 callback 应该回传一个或者两个值。第一个值的结果是 string 或 buffer 类型的 JavaScript 代码。第二个可选的值是 JavaScript 对象的 SourceMap。
30+
31+
#### 示例
32+
```
33+
// 支持 SourceMap 的 loader
34+
module.exports = function(source, map) {
35+
this.callback(null, source, map);
36+
};
37+
```
38+
#### 结论
39+
简单来说,就是我们通过参数source获取资源文件的内容,随便玩耍,处理成我们需要的东西后,抛出即可。
40+
41+
### 2. 解析Markdown
42+
这里解析Markdown使用的是[markdown-it](https://github.com/markdown-it/markdown-it),因为它“[*100% CommonMark support*](https://markdown-it.github.io/)”。
43+
#### 安装
44+
`npm install markdown-it --save`
45+
46+
#### [使用](https://markdown-it.github.io/markdown-it/)
47+
引入`markdown-it`后初始化一个实例`new MarkdownIt([presetName][, options])
48+
`。
49+
```
50+
html: false, // Enable HTML tags in source
51+
xhtmlOut: false, // Use '/' to close single tags (<br />).
52+
// This is only for full CommonMark compatibility.
53+
breaks: false, // Convert '\n' in paragraphs into <br>
54+
langPrefix: 'language-', // CSS language prefix for fenced blocks. Can be
55+
// useful for external highlighters.
56+
linkify: false, // Autoconvert URL-like text to links
57+
58+
// Enable some language-neutral replacement + quotes beautification
59+
typographer: false,
60+
61+
// Double + single quotes replacement pairs, when typographer enabled,
62+
// and smartquotes on. Could be either a String or an Array.
63+
//
64+
// For example, you can use '«»„“' for Russian, '„“‚‘' for German,
65+
// and ['«\xA0', '\xA0»', '‹\xA0', '\xA0›'] for French (including nbsp).
66+
quotes: '“”‘’',
67+
68+
// Highlighter function. Should return escaped HTML,
69+
// or '' if the source string is not changed and should be escaped externaly.
70+
// If result starts with <pre... internal wrapper is skipped.
71+
highlight: function (/*str, lang*/) { return ''; }
72+
```
73+
74+
presetName是`markdown-it`提供的一种快速配置,它支持三种模式:
75+
76+
* [commonmark](https://github.com/markdown-it/markdown-it/blob/master/lib/presets/commonmark.js):按照严格[CommonMark](http://commonmark.org/)模式解析。
77+
* [default](https://github.com/markdown-it/markdown-it/blob/master/lib/presets/default.js):允许所有规则,但是任然没有html、typographer、autolinker的支持。
78+
* [zero](https://github.com/markdown-it/markdown-it/blob/master/lib/presets/zero.js):所有规则禁用,当你仅使用`bold``italic`时可以通过`.enable()`快速启动。
79+
80+
具体options配置及含义如下:
81+
82+
* **html**`false`.是否允许源码中含有HTML标签。这个配置需要小心,以防止XSS攻击。最好的做法是通过三方插件来实现允许HTML。
83+
* **xhtmlOut**`false`.设置`true`后通过'/'来关闭空标签。仅在兼容CommonMark时需要。
84+
* **breaks**`false`.设置`true`后会将`\n`转化为`<br>`
85+
* **langPrefix**`language-`.CSS 前缀。
86+
* **linkify**`false`.设置`true`后自动转换链接。
87+
* **typographer**`false`.设置`true`后允许一些语言替换(比如单引号、双引号同时使用会变成一对单/双)和引用美化。
88+
* **quotes**`“”‘’`.设置`true`后会转化为不同语言下的引号。
89+
* **highlight**`null`.对代码块的高亮函数。
90+
91+
根据需求,首先不允许html,因此,我们不使用`commonmark`,此外,我们不单单仅使用`bold``italic`,因此,我们选择`default`配置。
92+
```
93+
// default mode
94+
let md = require('markdown-it')();
95+
```
96+
#### 插件加载
97+
我们还可能使用一些相应插件,其使用方式如下:
98+
```
99+
let md = require('markdown-it')()
100+
.use(plugin1)
101+
.use(plugin2, opts, ...)
102+
.use(plugin3);
103+
```
104+
我们使用的是[markdown-it-anchor](https://github.com/valeriangalliat/markdown-it-anchor),它是头部的锚。
105+
通过`npm install markdown-it-anchor --save`安装。
106+
我们首先需要的是`permalink`,然后是`slugify`,
107+
函数使用[`transliteration`](https://github.com/andyhu/transliteration)提供的`slugify`(`npm install transliteration --save
108+
`)。锚前面的class或者symbol等,可根据自己的需求配置。
109+
根据其API文档和我们的需求,使用配置如下:
110+
```
111+
const anchor = require('markdown-it-anchor');
112+
const slugify = require('transliteration').slugify;
113+
114+
let md = require('markdown-it').use(anchor, {
115+
slugify: slugify,
116+
permalink: true
117+
})
118+
```
119+
#### 代码高亮
120+
首先需要通过`npm install highlight --save
121+
`来安装[highlight](https://github.com/isagalaev/highlight.js)
122+
可以简单使用:
123+
```
124+
var hljs = require('highlight.js'); // https://highlightjs.org/
125+
126+
// Actual default values
127+
var md = require('markdown-it')({
128+
highlight: function (str, lang) {
129+
if (lang && hljs.getLanguage(lang)) {
130+
try {
131+
return hljs.highlight(lang, str).value;
132+
} catch (__) {}
133+
}
134+
135+
return ''; // use external default escaping
136+
}
137+
});
138+
```
139+
也可以包装起来:
140+
```
141+
var hljs = require('highlight.js'); // https://highlightjs.org/
142+
143+
// Actual default values
144+
var md = require('markdown-it')({
145+
highlight: function (str, lang) {
146+
if (lang && hljs.getLanguage(lang)) {
147+
try {
148+
return '<pre class="hljs"><code>' +
149+
hljs.highlight(lang, str, true).value +
150+
'</code></pre>';
151+
} catch (__) {}
152+
}
153+
154+
return '<pre class="hljs"><code>' + md.utils.escapeHtml(str) + '</code></pre>';
155+
}
156+
});
157+
```
158+
159+
我们处理以及含义如下:
160+
```
161+
highlight(content, languageHint){
162+
let highlightedContent;
163+
164+
highlight.configure({
165+
useBR: true,
166+
tabReplace: ' '//替换TAB为你想要的任意字符,便于排版
167+
});
168+
// 使用highlight的getLanguage获取语言
169+
if (languageHint && highlight.getLanguage(languageHint)) {
170+
try {
171+
// 高亮显示
172+
highlightedContent = highlight.highlight(languageHint, content).value;
173+
} catch (err) {
174+
}
175+
}
176+
// 当无法检测语言时,使用自动模式
177+
if (!highlightedContent) {
178+
try {
179+
highlightedContent = highlight.highlightAuto(content).value;
180+
} catch (err) {
181+
}
182+
}
183+
184+
// 把代码中的{}转
185+
highlightedContent = highlightedContent.replace(/[\{\}]/g, (match) = > `{'${match}'}`
186+
)
187+
;
188+
189+
// 加上 hljs 根据code标签上的class识别语言
190+
highlightedContent = highlightedContent.replace('<code class="', '<code class="hljs ').replace('<code>', '<code class="hljs">')
191+
192+
return highlight.fixMarkup(highlightedContent);
193+
}
194+
```
195+
**至此,解析Markdown部分基本完成。**
196+
197+
### 3. 执行代码
198+
我们需要将中的jsx语言代码块执行并渲染,需要用到`markdown-it-container`
199+
#### 安装
200+
`npm install markdown-it-container --save
201+
`
202+
#### [使用](https://github.com/markdown-it/markdown-it-container)
203+
```
204+
let md = require('markdown-it')()
205+
.use(require('markdown-it-container'), name [, options]);
206+
```
207+
参数如下:
208+
209+
* **name**- 包裹的名称
210+
* options:
211+
* **validate**- 验证函数
212+
* **render**- 渲染函数
213+
* **marker**- 分隔符(`:`)的使用
214+
215+
根据使用方法,我们代码如下:
216+
```
217+
//引用
218+
const mdContainer = require('markdown-it-container');
219+
let moduleJS = [];
220+
let flag = '';
221+
222+
md.use(mdContainer, 'demo', {
223+
validate: function (params) {
224+
return params.trim().match(/^demo\s*(.*)$/);
225+
},
226+
render: function (tokens, idx) {
227+
// container 从开头到结尾把之间的token跑一遍,其中idx定位到具体的位置
228+
229+
// 获取描述
230+
const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
231+
232+
// 有此标记代表 ::: 开始
233+
if (tokens[idx].nesting === 1) {
234+
flag = idx;
235+
236+
let jsx = '', i = 1;
237+
238+
// 从 ::: 下一个token开始
239+
let token = tokens[idx + i];
240+
241+
// 如果没有到结尾
242+
while (token.markup !== ':::') {
243+
// 只认```,其他忽略
244+
if (token.markup === '```') {
245+
if (token.info === 'js') {
246+
// 插入到import后,component前
247+
moduleJS.push(token.content);
248+
} else if (token.info === 'jsx') {
249+
// 插入render内
250+
jsx = token.content;
251+
}
252+
}
253+
i++;
254+
token = tokens[idx + i]
255+
}
256+
257+
// 描述也执行md
258+
return formatOpening(jsx, md.render(m[1]), flag);
259+
}
260+
return formatClosing(flag);
261+
}
262+
});
263+
```
264+
265+
代码具体含义可参照注释。至于`formatOpening`、`formatClosing
266+
`只是简单的使用其他HTML标签简单的包裹了一下,可忽略次函数,直接`return m[1]``flag`。
267+
268+
**至此,对代码的处理也基本完成。**
269+
可加入一些其他的美化代码后,放入`module.exports
270+
`函数中输出。
271+
272+
### 4. 组装完成
273+
此部分不做详细描述,可直接移步至[github](https://github.com/AdamantG/react-markdown-it-loader)查看。
274+

0 commit comments

Comments
 (0)