|
| 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