基于 next.js + mdx 搭建组件库文档项目(一) -- 开发环境搭建

说明

之前使用过 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 配置、服务器配置等等问题上,只要专注于业务开发就可以了

优点

  1. 基本上不需要配置,模块打包、编译、优化等都不需要考虑
  2. 基于文件系统的路由,每个 pages 目录下的文件都是一个路由(有的时候可能不够灵活)
  3. 支持 SSG(静态站点生成) 和 SSR(服务器端渲染)
  4. 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 语法 来创建页面了

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值