说明
之前使用过 Docz 来作为组件库文档搭建工具,它基于 gatsby , 提供了高度的定制化能力,但是截止 2021-06-22, Docz 停留在 v2.3.1(2020-04-05) 已经一年多,其所依赖的 gatsby 相关包,已经有了多个版本的更新,每次 安装 docz 都会在控制台收到大量的警告,虽然项目依然可以运行,但是为长久打算,决定放弃 Docz ,尝试基于 MDX 来搭建一个组件库
相关文章
预期计划
- 使用 MDX 来作为文档的主要载体,这样组件库的文档还是通过 markdown 来写,同时在 markdown 中可以使用我们编写的 react 组件
- 借助 react-docgen-typescript 来帮助将组件的 Props 类型输出到文档中
- 借助 react-live 来实现源码展示、源码实时编译、源码映射组件展示
- 使用 Next.js 中文 帮助搭建工程化开发环境,借助 Next.js 可以轻松实现 SSG(静态站点生成) SSR(服务器端渲染),与我们常用的 SPA (单页面应用)相比,会有更高的 SEO 优化空间
- 封装与 docz 类似的 API(
<Playground> <Props>
),来实现组件的演示与Props展示 - Typescript 作为主要的开发语言
1. 搭建 MDX + Next 开发环境
1. 使用 MDX 作为载体的优势
- MDX 将 markdown 和 JSX 语法混合在一起并完美地 融入基于 JSX 的项目当中。
- 一切皆组件: 导入(import) JSX 组件并 在 MDX 文档中直接渲染它们。
- 基于 Markdown: Markdown 的简洁和优雅依然得到了保存, 只须在需要时才混入 JSX
- 超级超级快: MDX 没有运行时,所有的编译都发生在 构建阶段。
- 容易定制:Markdown 语法中的控件,都可以定制化样式
2. Next.js 介绍
Next.js 是一个 React 服务器端渲染应用框架,它提供了一整套的解决方案,使得开发者不需要花费更多的经历在 webpack 配置、router 配置、服务器配置等等问题上,只要专注于业务开发就可以了
优点
- 基本上不需要配置,模块打包、编译、优化等都不需要考虑
- 基于文件系统的路由,每个 pages 目录下的文件都是一个路由(有的时候可能不够灵活)
- 支持 SSG(静态站点生成) 和 SSR(服务器端渲染)
- API 简洁、使用方便、文档健全、支持页面快速刷新
3. MDX + Next.js 开发环境搭建
借助 create-next-app 创建一个 next 项目
npx create-next-app --typescript
可以观察到项目中有如下几个重要文件
├── next-env.d.ts # next 的 TS 类型文件
├── next.config.js # next 的配置文件
├── package.json
├── pages # 页面入口,也可以手动创建 src/pages 具有同样的路由功能
├── public # 静态资源存放
└── tsconfig.json
package.json 中会增加如下几个 script 命令
{
"dev": "next dev",
"build": "next build",
// "build": "next build && next export -o ../../docs", // 输出 SSG
"start": "next start",
}
next.config.js 作为 next 的配置文件,用来配置项目打包情况,为了使项目支持引入 mdx 文件,我们需要追加一个插件
yarn add -D @next/mdx @mdx-js/loader
const withMDX = require("@next/mdx")({
extension: /\.mdx?$/,
options: {
remarkPlugins: [], // mdx 支持的插件
rehypePlugins: [] // mdx 支持的插件
}
});
module.exports = withMDX({
pageExtensions: ["mdx", "tsx", 'ts', 'js', 'md'],
})
借助
@mdx-js/react
提供的 MDXProvider 来定义 markdown 语法的对应的 react 组件样式,可以在 这里 查看有那些 markdown 的语法是可以重新定义样式的
pages/_app.tsx
是在 pages 目录下比较特殊的一个文件,它是一个顶层组件,会在所有 page 中渲染,因此,一些全局样式,或者全局配置要在这个文件中定义
// pages/_app.tsx
import './globals.less';
import type { AppProps } from 'next/app';
import { MDXProvider } from '@mdx-js/react';
import { Wrapper, Paragraph, headings, UL, OL, Code } from '~controls';
// 定义 mdx 中语法的映射组件
const components = {
...headings,
p: Paragraph,
ol: OL,
ul: UL,
pre: Code,
wrapper: Wrapper,
};
// 顶级组件,所有 page 之间共享,用于全局样式,全局 state
const MyApp = ({ Component, pageProps }: AppProps) => {
return (
<MDXProvider components={components}>
<main id="document-scroll-container">
<Component {...pageProps} />
</main>
</MDXProvider>
);
};
export default MyApp;
通过以上配置,就可以在普通页面中使用 mdx 的文件了
import ButtonMdx from '../../docs/core/Button/Code.mdx';
export default function Home() {
return <ButtonMdx />;
}
4. 怎么使用 less 定义样式
在 next.js 项目中内置了对 sass、css 的支持,且只有在 pages/_app.tsx 中才能使用 global css 其他页面中仅能使用 css module, 查看内置对 CSS 的支持 ,获得更多解释
很遗憾,我们的文档项目是给组件库使用的,而组件库已经使用了 less 作为样式的预编译语言,且为了方便组件库的使用者可以通过样式覆盖来自定义,组件库输出的时 global css
为了在 next.js 中使用 global + less ,我们需要对 next的 webpack 配置做些定制化,在 next.config.js 配置中有一个选项
webpack: (webpackConfig: T, nextConfig: K) => T
,我们借助这个配置来将默认的 webpack 配置修改掉
/** ----- next.config.js ----- */
module.exports = withMDX({
webpack(config, nextConfig) {
// add less compile
config = injectLessLoader(config, nextConfig);
return config
},
});
/** ----- injectLessLoader.js ----- */
const get = require('lodash/get');
const cloneDeep = require('lodash/cloneDeep');
const addLessToRegExp = (rx) =>
new RegExp(rx.source.replace("|sass", "|sass|less"), rx.flags);
const injectLessLoader = (config, nextConfig) => {
let sassModuleRule;
// 1. 找到处理样式的 rule
const cssRule = config.module.rules.find((rule) => get(rule, 'oneOf[0].options.__next_css_remove'));
cssRule.oneOf.forEach((rule) => {
if (get(rule, 'use.loader') === "error-loader") return;
// 2. 增加 file-loader 处理 less 中资源的能力
if ((get(rule, 'use.loader') || []).includes("file-loader")) {
rule.issuer = Array.isArray(rule.issuer)
? rule.issuer.map(rx => addLessToRegExp(rx))
: addLessToRegExp(rule.issuer);
}
// 3. 筛选出 sass 的处理 rule
if (get(rule, 'test.source') === "\\.module\\.(scss|sass)$") {
sassModuleRule = rule;
}
});
// 4. less 的处理是在拷贝的 sass 的处理基础上修修改改得到的
let lessRule = cloneDeep(sassModuleRule);
lessRule.test = /\.less$/;
// 删除 issuer 对文件目录的限制
delete lessRule.issuer;
// 替换 sass-loader 为 less-loader
lessRule.use.splice(-1, 1, {
loader: "less-loader",
options: {
lessOptions: {
javascriptEnabled: true,
globalVars: {
ASSET_PATH: get(nextConfig, 'config.env.ASSET_PATH') || '/'
}
}
},
});
// 重要:将 sideEffects 设置为 true 否则非 css-module 不会被打包
lessRule.sideEffects = true;
// 更改 css-loader 的配置
const cssLoaderInLessModuleIndex = lessRule.use.findIndex((item) =>
`${item.loader}`.includes('css-loader'),
);
const cssLoaderInLessModule = lessRule.use.find((item) =>
`${item.loader}`.includes('css-loader'),
);
// clone
const cssLoaderClone = cloneDeep(cssLoaderInLessModule);
// 去除 getLocalIdent,改为使用 localIdentName
if (get(cssLoaderClone, 'options.modules.getLocalIdent')) {
delete cssLoaderClone.options.modules.getLocalIdent;
}
// 更新 css-loader 主要更新 modules 配置
cssLoaderClone.options = {
...cssLoaderClone.options,
sourceMap: Boolean(nextConfig.dev),
modules: {
localIdentName: '[local]--[hash:4]',
...cssLoaderClone.options.modules,
mode: 'local',
auto: true, // 自动识别是不是 css-module
}
}
// 5. 将定制后 less 处理插入到 webpack 配置中
lessRule.use.splice(cssLoaderInLessModuleIndex, 1, cssLoaderClone);
cssRule.oneOf.splice(cssRule.oneOf.indexOf(sassModuleRule), 0, lessRule);
return config;
}
module.exports = injectLessLoader;
通过如上的配置,我们就可以安全的在 项目中使用 less 了,默认导入的 less 都是 global 的,如果想要适用 css-module 请给文件命名为 xxx.module.less
5. 定义 webpack alias
由于整个组件库项目使用的时 monorepo 结构,因此组件的实现代码在另外一个 子包中,要想在 文档项目 中方便的使用另外一个 package 中的组件,需要我们做如下的配置
/** ----- next.config.js ----- */
module.exports = withMDX({
webpack(config, nextConfig) {
// add less compile
config = injectLessLoader(config, nextConfig);
// add alias
config = injectAlias(config);
return config
},
});
/** ----- injectAlias.js ----- */
const path = require('path');
const injectAlias = (config) => {
config.resolve.alias = {
...config.resolve.alias,
'~controls': path.resolve(__dirname, './src/controls'),
'~docs': path.resolve(__dirname, './docs'),
"@mjz-test/mjz-ui": path.resolve(__dirname, "../../../mjz-ui/src"), // 配置去获取源文件, 这样可以通过解析拿到组件的 TS 类型
"@mjz-test/icons": path.resolve(__dirname, "../../../icons/src"),
}
return config;
}
module.exports = injectAlias;
/** ----- tsconfig.json ----- */
{
"compilerOptions": {
//...
"paths": { // 配置路径,使得 TS 可以识别
"~controls": [
"./src/controls/index.tsx"
],
"~docs/*": [
"./docs/*"
],
"@mjz-test/mjz-ui": [
"../mjz-ui/src/index.tsx"
],
"@mjz-test/icons": [
"../icons/src/index.tsx"
]
}
},
"include": [
"next-env.d.ts",
"src",
"docs",
"typings",
"../mjz-ui/src", // 加入解析
"../icons/src"
],
}
经过上阶段的配置虽然可以在项目中使用 mdx 语法 来创建页面了