MDX -- 支持 React JSX 的 Markdown 超集

2018-11-04

前言

最近我一直在寻找书写博客的最佳方式,既要能自由书写,也要能够精准地把控布局。

为此,我分别试验了几种书写方式:

  • 手写 JSX
  • 将 JSX 封装成 DSL
  • Markdown

其中:

  • JSX、DSL 虽然能够精准把控,但是写起来太累了,自己要写很多胶水
  • Markdown 虽然书写自由,但是布局、功能都难以精确控制

正当苦恼的时候,网上的朋友指点我说,有一种 Markdown 的扩展,可以在其中混写 JSX。

这正是我需要的!经过调研,我发现了 mdx-js/mdx 这个库。

这个库的介绍如下:

JSX in Markdown for ambitious projects

为雄心勃勃的项目提供的在 Markdown 中书写 JSX 的方式

需要注意的是,mdx 库默认使用的扩展名是 .mdx。

安装

React 版本:

在安装之前,首先要检查自己项目中 React 版本,根据这个 issue 所描述的,React 的版本必须 16.3+。seddd

这个库的安装文档我有点没看懂,因此自己根据 example 摸索着手动安装。

安装以下依赖:

yarn add create-react-context
yarn add @mdx-js/loader
yarn add @mdx-js/mdx

之后配置 webpack, .mdx 后缀使用 @mdx-js/mdx 加载器(注意:我选择延续使用 .md 扩展名):

{ test: /\.md$/, exclude: /node_modules/, loader: ['babel-loader', '@mdx-js/loader']},

其中:mdx 的 loader 需要与 babel-loader 组合使用。

使用

本节介绍如何在项目中添加 mdx 组件,实现加载 Markdown(mdx 文件)。

首先我们先大概地了解下整体过程:

  • MDXProvider 组件负责将 mdx 文件转换为 React nodes,它接收两个参数,一个是 mdx 文件的内容,另一个是 html 标签到 React 标签的映射规则
  • 因此我们首先要建立映射规则
  • 之后导入 mdx 文件
  • 最后将它俩传给 MDXProvider

具体代码如下:

建立映射关系

const RList = (props) => {
    return <List bulleted>{props.children}</List>
}

const ROList = (props) => <List ordered>{props.children}</List>

const RListItem = (props) => {
    return <List.Item>{props.children}</List.Item>
}

const Quote = (props) => <Message color='orange'>{props.children}</Message>

const RTable = (props) => <Table celled>{props.children}</Table>
const components = {
    h1: H1,
    h2: H2,
    h3: H3,
    h4: H4,
    code: Code,
    ul: RList,
    ol: ROList,
    li: RListItem,
    blockquote: Quote,
    table: RTable
}

其中:

  • 我是基于 Semantic-React-UI 库进行的二次封装。
  • 重点在 componets 这个结构,markdown 的各种元素(标题、代码)最终会被转换成 h1、code。
  • 通过 componets 再将 h1、code 转换为对应的 React 节点。

导入 Markdown 文件

下面需要导入 Markdown 文件,具体导入方式有两种:

  • 同步导入:Webpack 在打包时会将 Markdown 编译到页面中,形成一个整体 bundle
  • 异步导入:文章与页面是分离的两个 bundle,页面在运行时异步加载文章 bundle。这个比较适合博客框架,一个页面的框架动态加载不同的文章。

同步的写法是在代码的开头直接 import:

import MD from 'content/blog/posts/Rxjava3.md';

异步的写法是使用 import() 方法:

import('content/blog/posts/' + post.link).then(v => {
    let Article = v.default
    ...
})

在使用时,MD 或 Article 作为 React 组件使用。

MDXProvider

下面来在 render 中写 MDXProvider:

<MDXProvider components={components}><Article/></MDXProvider>

这样就大功告成了。

我们再来回顾一下整个流程:

  • 首先我们在项目中以 mdx 语法编写文章
  • 这些文章在构建时,Webpack 会通过 @mdx-js/loader 对其进行转化,转换成一个格式为 MDXAST 的 React 语法树组件
  • 我们在代码中定义语法组件映射表
  • 在运行时导入转换后的 mdx 文件和映射表,MDXProvider 会在两者之间建立关联
  • 最后文章展示在页面上

语法

MDX 的语法是 Markdown 语法的超集,Markdown 语法自然不必多说,下面详细说超出的那一部分。

JSX 组件

JSX 组件直接使用:

<Box>
  <Heading>Here's a JSX block</Heading>
  <Text>It's pretty neat</Text>
</Box>

导入

组件在使用时需要进行导入:

import Graph from './components/graph'

## Here's a graph

<Graph />

同时,也允许导入其他数据,比如导入外部的 md 文档:

import License from './license.md'
import Contributing from './docs/contributing.md'

# Hello, world!

<License />

---

<Contributing />

测试

我尝试着引用一个 React 组件:

import DefaultBarChart from "components/d3/DefaultBarChart"

<DefaultBarChart data={[1,2,3,4]} size={[500, 200]} />

可见导入成功了,上面这段代码输入的是:

import DefaultBarChart from "components/d3/DefaultBarChart"

<DefaultBarChart data={[1,2,3,4]} size={[500, 200]} />

我发现,JSX 必须写在一行里面,如果拆分多行写会报错。

网络资源