使用 Next-js 搭建 Monorepo 組件庫文檔
閱讀本文你將:
-
使用 pnpm 搭建一個 Monorepo 組件庫
-
使用 Next.js 開發一個組件庫文檔
-
changesets 來管理包的 version 和生成 changelog
-
使用 vercel 部署在線文檔
代碼倉庫:
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 個非常大的優勢:
-
使用 swc 編譯,Next.js 中實現了快 3 倍的快速刷新和快 5 倍的構建速度;
-
按需編譯,在開發環境下,只有訪問的頁面纔會進行編譯
那麼接下來的問題就是:要在 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 文件測試下
這樣 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
查看效果
在頁面上會提示,無法找到@mastack/login
這個包,我們需要在項目的根目錄下的 tsconfig.json
中加入別名
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@mastack/login": ["packages/login/src"],
"@mastack/user-select": ["packages/user-select/src"]
},
}
}
保存後,頁面會自動刷新,我們就可以在頁面上看到如下效果。
至此文檔與 packages 目錄下的 mdx 已經打通。修改 packages/login/docs/index.mdx
中的文檔,頁面會自動熱更新。
自定義 mdx 組件
上面代碼已經實現了在 md 文檔中顯示組件和代碼,但我們想要的是類似於 ant design 那樣的效果,默認代碼不展示,點擊可以收起和展開,這該怎麼實現呢?
我們可以利用 mdx 的自定義組件來實現這個效果。
寫 mdx 的時候,在組件 <Login/>
和代碼外層嵌套一個自定義組件DemoBlock
然後實現一個自定義一個 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
節點信息;
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,一起來看下實現效果:
是不是有跟 antd 的 demo block 有些相似了呢? 若要顯示更多字段和描述,我們可以修改組件代碼,實現完全自定義。
優化文檔界面
至此我們的文檔,還是有些簡陋,我們得優化下文檔界面,讓我們的界面顯示更美觀。
- 安裝並且初始化 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
-
remark-gfm
讓 md 支持 GitHub Flavored Markdown (自動超鏈接鏈接文字、腳註、刪除線、表格、任務列表) -
remark-math
rehype-katex 支持數學公式 -
rehype-slug
rehype-autolink-headings 自動給標題加唯一 id -
rehype-prism-plus
支持代碼高亮
修改 next.config.js
爲 next.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"
編譯階段,生成構建產物
-
運行
pnpm changeset version
。 這將提高先前使用pnpm changeset
(以及它們的任何依賴項)的版本,並更新變更日誌文件。 -
運行
pnpm install
。 這將更新鎖文件並重新構建包。 -
提交更改。
-
運行
pnpm pub
。 此命令將發佈所有包含被更新版本且尚未出現在包註冊源中的包。
部署
部署可以選擇 gitbub pages 或者 vercel 部署,他們都是免費的,Github pages 只支持靜態網站,vercel 支持動態網站,它會將 nextjs page 中,單獨部署成函數的形式。我這裏選擇使用 vercel,因爲它的訪問速度相對比 gitbub pages 要快很多。只需要使用 github 賬號登錄 https://vercel.com/ 導入項目,便會自動部署,而且會自動分配一個 https://xxx.vercel.app/ 二級域名。
也可以使用命令行工具,在項目跟目錄下執行,根據提示,選擇默認即可
npx 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