使用 Next-js 搭建 Monorepo 組件庫文檔

閱讀本文你將:

代碼倉庫:

​https://github.com/maqi1520/nextjs-components-docs

前言

組件化開發是前端的基石,正因爲組件化,前端得以百花齊放,百家爭鳴。我們每天在項目中都寫着各種各樣的組件,如果在面試的時候,跟面試官說,你每天的工作是開發組件,那麼顯然這沒有什麼優勢,如果你說,你開發了一個組件庫,並且有一個在線文檔可以直接預覽,這可能會是你的一個加分項。今天我們就來聊聊組件庫的開發,主要是組件庫的搭建和文檔建設,至於組件數量,那是時間問題,以及你是否有時間維護好這個組件庫的問題。

基礎組件和業務組件

首先組件庫分爲基礎組件和業務組件,所謂基礎組件就是 UI 組件,類似 Ant design,它是單包架構,所有的組件都是在一個包中,一旦其中一個組件有改動,就需要發整包。另外一種是業務組件,組件中包含了一些業務邏輯,它在企業內部是很有必要的。比如飛書文檔,包含在線文檔,在線 PPT、視頻會議等,這些都是獨立的產品,單獨迭代開發,單獨發佈,卻有一些共同的邏輯,比如沒有登錄的時候都需要調用一個” 登錄彈窗 “,或者說在項目協同的時候,都需要邀請人員加入,那麼需要一個 “人員選擇組件”, 這就是業務組件。業務組件不同於基礎組件,單獨安裝,依賴發包,而並不是全量發包。那麼這些業務組件也需要一個文檔,因此我們使用 Monorepo(單倉庫管理),這樣方便管理和維護。

爲什麼選用 Next.js 來搭建組件庫文檔?

組件文檔有個特別重要的功能就是 “寫 markdown 文檔,可以看到代碼以及運行效果”,這方面有很多優秀的開源庫,比如 Ant design 使用的是 bisheng[1], react use 使用的是 storybook[2], 還有一些優秀的庫,比如:dumi[3],Docz[4] 等。 本地跑過 Ant design 的同學都知道, Ant design 的啓動速度非常慢,因爲底層使用的 webpack,要啓動開發服務器,必須將所有組件都進行編譯,這會對開發者造成一些困擾,因爲如果是業務組件的話,開發者只關注單個組件,而不是全部組件。而使用 Next.jz 就有 2 個非常大的優勢:

那麼接下來的問題就是:要在 Next.js 中實現 “寫 Markdown Example 可預覽” 的功能,若要自己實現這個功能,確實是一件麻煩的事情。我們換一個思維,組件展示,也就是在 markdown 中運行 react 組件,這不就是 mdx[5] 的功能嗎? 而在 Next.js 中可以很方便地集成 MDX。

效果演示  

實現效果

目前這是一個簡易版,只爲展示 Next.js 搭建文檔

項目初始化

首先我們創建一個 next typescript 作爲我們項目的主目錄,用於組件庫的文檔開發

npx create-next-app@latest --ts

要想啓動 pnpm 的 workspace 功能,需要工程根目錄下存在 pnpm-workspace.yaml 配置文件,並且在 pnpm-workspace.yaml 中指定工作空間的目錄。比如這裏我們所有的子包都是放在 packages 目錄下

packages:

  - 'packages/*'

接下來,我們在 packages 文件夾下創建三個子項目,分別是:user-select、login 和 utils, 對應用戶選擇,登錄 和工具類。

├── packages

│   ├── user-select

│   ├── login

│   ├── utils

user-select 和 login 依賴 utils,我們可以將一些公用方法放到 utils 中。

給每個 package 下面創建 package.json 文件,包名稱通常是”@命名空間 + 包名 @“的方式,比如 @vite/xx 或 @babel/xx,在本例中,這裏我們都以@mastack開頭

{

  "name": "@mastack/login",

  "version": "1.0.0",

  "description": "",

  "main": "dist/index.js",

  "types": "dist/index.d.ts",

  "scripts": {

    "build": "tsc"

  },

  "keywords": [],

  "author": "",

  "license": "ISC"

}

給每個 package 安裝 typescript

pnpm add typescript -r  -D

給每個 package 創建 tsconfig.json 文件

{

  "include": ["src/**/*"],

  "compilerOptions": {

    "jsx": "react",

    "outDir": "dist",

    "target": "ES2020",

    "module": "esnext",

    "strict": true,

    "esModuleInterop": true,

    "skipLibCheck": true,

    "moduleResolution": "node",

    "declaration": true,

    "forceConsistentCasingInFileNames": true

  }

}

執行下面代碼,往 login 組件中安裝 utils;

pnpm i @mastack/utils --filter @mastack/login

安裝完成後,設置依賴版本的時候推薦用 workspace:*,就可以保持依賴的版本是工作空間裏最新版本,不需要每次手動更新依賴版本。

pnpm 提供了 -w, --workspace-root 參數,可以將依賴包安裝到工程的根目錄下,作爲所有 package 的公共依賴,這麼我們安裝 antd

pnpm install antd -w

組件開發

我們在 login 組件下,新建一個組件 src/index.tsx

import React, { useState } from "react";

import { Button, Modal } from "antd";



interface Props {

  className: string;

}



export default function Login({ className }: Props) {

  const [open, setopen] = useState(false);

  return (

    <>

      <Button onClick={() => setopen(true)} className={className}>

        登錄

      </Button>

      <Modal

        title="登錄"

        open={open}

        onCancel={() => setopen(false)}

        onOk={() => setopen(false)}

      >

        <p>登錄彈窗</p>

      </Modal>

    </>

  );

}

先寫一個最簡單版本,組件代碼並不是最重要的,後續可以再優化。

在 package.json 中添加構建命令

"scripts": {

    "build": "tsc"

  }

然後在組件目錄下執行 yarn build 。此時組件以及可以打包成功!

Next.js 支持 MDX

接下來要讓文檔支持 MDX,在根目錄下執行以下命令,安裝 mdx 和 loader 相關包

pnpm add @next/mdx @mdx-js/loader @mdx-js/react -w

修改 next.config.js 爲以下代碼

const withMDX = require('@next/mdx')({

  extension: /\.mdx?$/,

})



module.exports = withMDX({

  pageExtensions: ['js', 'jsx', 'ts', 'tsx', 'md', 'mdx'],

  reactStrictMode: true,

  swcMinify: true,

})

這樣就可以在 Next 中支持 MDX 了。

我們在 src/pages 目錄下,新建一個 docs/index.mdx

markdown 信息

先寫一個簡單的 markdown 文件測試下

markdown 渲染

這樣 Next.js 就支持 mdx 文檔了。

Next 動態加載 md 文件

接下來,我們要實現動態加載 packages 中的文件 md 文件。新建一個 pages/docs/[...slug].tsx 文件。

export async function getStaticPaths(context: GetStaticPathsContext) {

  return {

    paths: [

      { params: { slug: ["login"] } },

      { params: { slug: ["user-selecter"] } },

    ],

    fallback: false, // SSG 模式

  };

}



export async function getStaticProps({

  params,

}: GetStaticPropsContext<{ slug: string[] }>) {

  const slug = params?.slug.join("/");



  return {

    props: {

      slug,

    }, // 傳遞給組件的props

  };

}

我們使用的是 SSG 模式。上面代碼中 getStaticPaths 我先寫了 2 條數據,因爲我們目前只有 2 個組件,它會在構建的時候會生成靜態頁面。 getStaticProps函數可以獲取 URL 上的參數,我們將 slug 參數傳遞給組件,然後在 Page 函數中,我們使用 next/dynamic 動態加載 packages 中的 mdx 文件

import React from "react";

import {

  GetStaticPathsContext,

  InferGetServerSidePropsType,

  GetStaticPropsContext,

} from "next";

import dynamic from "next/dynamic";



type Props = InferGetServerSidePropsType<typeof getStaticProps>;



export default function Page({ slug }: Props) {

  const Content = dynamic(() => import(`../packages/${slug}/docs/index.mdx`), {

    ssr: false,

  });



  return (

    <div>

      <Content />

    </div>

  );

}

此時我們訪問 http://localhost:3000/docs/login 查看效果

Next.js 編譯報錯

在頁面上會提示,無法找到@mastack/login 這個包,我們需要在項目的根目錄下的 tsconfig.json 中加入別名

{

  "compilerOptions": {

    "paths": {

      "@/*": ["./src/*"],

      "@mastack/login": ["packages/login/src"],

      "@mastack/user-select": ["packages/user-select/src"]

    },

  }

}

保存後,頁面會自動刷新,我們就可以在頁面上看到如下效果。

Next.js  動態加載 mdx

至此文檔與 packages 目錄下的 mdx 已經打通。修改 packages/login/docs/index.mdx 中的文檔,頁面會自動熱更新。

自定義 mdx 組件

上面代碼已經實現了在 md 文檔中顯示組件和代碼,但我們想要的是類似於 ant design 那樣的效果,默認代碼不展示,點擊可以收起和展開,這該怎麼實現呢?

ant design 代碼塊

我們可以利用 mdx 的自定義組件來實現這個效果。

寫 mdx 的時候,在組件 <Login/>和代碼外層嵌套一個自定義組件DemoBlock

markdown 信息

然後實現一個自定義一個 DemoBlock 組件,提供給 MDXProvider,這樣所有的 mdx 文檔中,不需要 import 就可以使用組件。

import dynamic from "next/dynamic";

import { MDXProvider } from "@mdx-js/react";



const DemoBlock = ({ children }: any) => {

  console.log(children);

  return null

};



const components = {

  DemoBlock,

};



export default function Page({ slug }: Props) {

  const Content = dynamic(() => import(`packages/${slug}/docs/index.mdx`), {

    ssr: false,

  });



  return (

    <div>

      <MDXProvider components={components}>

        <Content />

      </MDXProvider>

    </div>

  );

}

我們先寫一個空組件,看下 children 的值。刷新頁面, 此時 DemoBlock中的組件和代碼不會顯示,我們看一下打印出的 children 節點信息;

DemoBlock children 節點數據

chilren 爲 react 中的 vNode,現在我們就可以根據 type 來判斷,返回不同的 jsx,這樣就可以實現DemoBlock組件了,代碼如下:

import React, { useState } from "react";



const DemoBlock = ({ children }: any) => {

  const [visible, setVisible] = useState(false);



  return (

    <div class>

      {children.map((child: any) => {

        if (child.type === "pre") {

          return (

            <div key={child.key}>

              <div

                class

                onClick={() => setVisible(!visible)}

              >

                {!visible ? "顯示代碼" : "收起代碼"}

              </div>

              {visible && child}

            </div>

          );

        }

        return child;

      })}

    </div>

  );

};

再給組件添加一些樣式,給按鈕添加一個 svg icon,一起來看下實現效果:

組件文檔 demo 效果

是不是有跟 antd 的 demo block 有些相似了呢? 若要顯示更多字段和描述,我們可以修改組件代碼,實現完全自定義。

優化文檔界面

至此我們的文檔,還是有些簡陋,我們得優化下文檔界面,讓我們的界面顯示更美觀。

  1. 安裝並且初始化 tailwindcss
pnpm install -Dw tailwindcss postcss autoprefixer @tailwindcss/typography

pnpx tailwindcss init -p

修改 globals.css 爲 tailwindcss 默認指令

@tailwind base;

@tailwind components;

@tailwind utilities;

修改 tailwind.config.js 配置文件,讓我們的應用支持文章默認樣式,並且在 md 和 mdx 文件中也可以寫 tailwindcss

const defaultTheme = require("tailwindcss/defaultTheme");

const colors = require("tailwindcss/colors");



/** @type {import("tailwindcss").Config } */

module.exports = {

  content: [

    "./pages/**/*.{js,ts,jsx,tsx,md,mdx}",

    "./components/**/*.{js,ts,jsx,tsx}",

    "./packages/**/*.{md,mdx}",

  ],

  darkMode: "class",

  plugins: [require("@tailwindcss/typography")],

};

在 MDX Content 組件 外層可以加一個 prose class,這樣我們的文檔就有了默認好看文章樣式了。

現在 md 文檔功能還很薄弱,我們需要讓它強大起來,我們先安裝一些 markdown 常用的包

pnpm install remark-gfm remark-footnotes remark-math rehype-katex rehype-slug rehype-autolink-headings rehype-prism-plus -w

修改 next.config.jsnext.config.mjs,並輸入以下代碼

// Remark packages

import remarkGfm from "remark-gfm";

import remarkFootnotes from "remark-footnotes";

import remarkMath from "remark-math";

// Rehype packages

import rehypeSlug from "rehype-slug";

import rehypeAutolinkHeadings from "rehype-autolink-headings";

import rehypePrismPlus from "rehype-prism-plus";



import nextMDX from "@next/mdx";



const withMDX = nextMDX({

  extension: /\.mdx?$/,

  options: {

    remarkPlugins: [

      remarkMath,

      remarkGfm,

      [remarkFootnotes, { inlineNotes: true }],

    ],

    rehypePlugins: [

      rehypeSlug,

      rehypeAutolinkHeadings,

      [rehypePrismPlus, { ignoreMissing: true }],

    ],

  },

});



export default withMDX({

  pageExtensions: ["js", "jsx", "ts", "tsx", "md", "mdx"],

  reactStrictMode: true,

  swcMinify: true,

});

我們在這裏可以配置 remarkPlugins 和 rehypePlugins;

markdown 在編譯過程中會涉及 3 種 ast 抽象語法樹 , remark 負責轉換爲 mdast,它可以操作 markdown 文件,比如讓 markdown 支持更多格式(比如:公式、腳註、任務列表等),需要使用 remark 插件; rehype 負責轉換爲 hast ,它可以轉換 html,比如給 標題加 id,給代碼高亮,   這一步是在操作 HTML 後完成的。因此我們也可以自己寫插件,具體寫什麼插件,就要看插件在哪個階段運行。

最後我們到 github prism-themes 中複製一份代碼高亮的樣式到我們的 css 文件中,一起來看下效果吧!

組件文檔代碼高亮

發佈工作流

workspace 中的包版本管理是一個複雜的任務,pnpm 目前也並未提供內置的解決方案。pnpm 推薦了兩個開源的版本控制工具:changesets 和 rush,這裏我採用了 changesets[6] 來實現依賴包的管理。

配置

要在 pnpm 工作空間上配置 changesets,請將 changesets 作爲開發依賴項安裝在工作空間的根目錄中:

pnpm add -Dw @changesets/cli

然後 changesets 的初始化命令:

pnpm changeset init

添加新的 changesets

要生成新的 changesets,請在倉庫的根目錄中執行pnpm changeset.changeset 目錄中生成的 markdown 文件需要被提交到到倉庫。

發佈變更

爲了方便所有包的發佈過程,在工程根目錄下的 pacakge.json 的 scripts 中增加如下幾條腳本:

"compile": "pnpm --filter=@mastack/* run build",

"pub": "pnpm compile && pnpm --recursive --registry https://registry.npmjs.org/ publish --access public"

編譯階段,生成構建產物

  1. 運行pnpm changeset version。 這將提高先前使用 pnpm changeset (以及它們的任何依賴項)的版本,並更新變更日誌文件。

  2. 運行 pnpm install。 這將更新鎖文件並重新構建包。

  3. 提交更改。

  4. 運行 pnpm pub。 此命令將發佈所有包含被更新版本且尚未出現在包註冊源中的包。

部署

部署可以選擇 gitbub pages 或者 vercel 部署,他們都是免費的,Github pages 只支持靜態網站,vercel 支持動態網站,它會將 nextjs page 中,單獨部署成函數的形式。我這裏選擇使用 vercel,因爲它的訪問速度相對比 gitbub pages 要快很多。只需要使用 github 賬號登錄 https://vercel.com/ 導入項目,便會自動部署,而且會自動分配一個 https://xxx.vercel.app/ 二級域名。

也可以使用命令行工具,在項目跟目錄下執行,根據提示,選擇默認即可

npx vercel

vercel 部署

預覽地址:https://nextjs-components-docs.vercel.app/

小結

本文,我們從零開始,使用 Next.js 和 pnpm 搭建了一個組件庫文檔,主要使用 Next.js 動態導入功能解決了開發服務緩慢的問題,使用 Next.js 的 SSG 模式來生成靜態文檔。最後我們使用 changesets 來管理包的 version 和生成 changelog。

好了,以上就是本文的全部內容,你學會了嗎?接下來我將繼續分享 Next.js 相關的實戰文章,歡迎各位關注我的《 Next.js 全棧開發實戰》 專欄,感謝您的閱讀。

[1]bisheng: https://github.com/benjycui/bisheng

[2]storybook: https://github.com/storybookjs/storybook

[3]dumi: https://github.com/umijs/dumi

[4]docz: https://github.com/doczjs/docz

[5]mdx: https://github.com/mdx-js/mdx

[6]changesets: https://github.com/changesets/changesets

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/9K2a0fjeQCHcuILfH4tFWw