Next.js搭建Monorepo組件庫(kù)文檔實(shí)現(xiàn)詳解
前言
- 使用 pnpm 搭建一個(gè) Monorepo 組件庫(kù)
- 使用 Next.js 開(kāi)發(fā)一個(gè)組件庫(kù)文檔
- changesets 來(lái)管理包的 version 和生成 changelog
- 使用 vercel 部署在線文檔
代碼倉(cāng)庫(kù):github.com/maqi1520/ne…
組件化開(kāi)發(fā)是前端的基石,正因?yàn)榻M件化,前端得以百花齊放,百家爭(zhēng)鳴。我們每天在項(xiàng)目中都寫著各種各樣的組件,如果在面試的時(shí)候,跟面試官說(shuō),你每天的工作是開(kāi)發(fā)組件,那么顯然這沒(méi)有什么優(yōu)勢(shì),如果你說(shuō),你開(kāi)發(fā)了一個(gè)組件庫(kù),并且有一個(gè)在線文檔可以直接預(yù)覽,這可能會(huì)是你的一個(gè)加分項(xiàng)。今天我們就來(lái)聊聊組件庫(kù)的開(kāi)發(fā),主要是組件庫(kù)的搭建和文檔建設(shè),至于組件數(shù)量,那是時(shí)間問(wèn)題,以及你是否有時(shí)間維護(hù)好這個(gè)組件庫(kù)的問(wèn)題。
基礎(chǔ)組件和業(yè)務(wù)組件
首先組件庫(kù)分為基礎(chǔ)組件和業(yè)務(wù)組件,所謂基礎(chǔ)組件就是 UI 組件,類似 Ant design,它是單包架構(gòu),所有的組件都是在一個(gè)包中,一旦其中一個(gè)組件有改動(dòng),就需要發(fā)整包。另外一種是業(yè)務(wù)組件,組件中包含了一些業(yè)務(wù)邏輯,它在企業(yè)內(nèi)部是很有必要的。比如飛書文檔,包含在線文檔,在線 PPT、視頻會(huì)議等,這些都是獨(dú)立的產(chǎn)品,單獨(dú)迭代開(kāi)發(fā),單獨(dú)發(fā)布,卻有一些共同的邏輯,比如沒(méi)有登錄的時(shí)候都需要調(diào)用一個(gè)”登錄彈窗“,或者說(shuō)在項(xiàng)目協(xié)同的時(shí)候,都需要邀請(qǐng)人員加入,那么需要一個(gè)“人員選擇組件”, 這就是業(yè)務(wù)組件。業(yè)務(wù)組件不同于基礎(chǔ)組件,單獨(dú)安裝,依賴發(fā)包,而并不是全量發(fā)包。那么這些業(yè)務(wù)組件也需要一個(gè)文檔,因此我們使用 Monorepo(單倉(cāng)庫(kù)管理),這樣方便管理和維護(hù)。
為什么選用 Next.js 來(lái)搭建組件庫(kù)文檔?
組件文檔有個(gè)特別重要的功能就是“寫 markdown 文檔,可以看到代碼以及運(yùn)行效果”,這方面有很多優(yōu)秀的開(kāi)源庫(kù),比如 Ant design 使用的是 bisheng, react use 使用的是 storybook, 還有一些優(yōu)秀的庫(kù),比如:dumi,Docz 等。 本地跑過(guò) Ant design 的同學(xué)都知道, Ant design 的啟動(dòng)速度非常慢,因?yàn)榈讓邮褂玫?webpack,要啟動(dòng)開(kāi)發(fā)服務(wù)器,必須將所有組件都進(jìn)行編譯,這會(huì)對(duì)開(kāi)發(fā)者造成一些困擾,因?yàn)槿绻菢I(yè)務(wù)組件的話,開(kāi)發(fā)者只關(guān)注單個(gè)組件,而不是全部組件。而使用 Next.jz 就有 2 個(gè)非常大的優(yōu)勢(shì):
- 使用 swc 編譯,Next.js 中實(shí)現(xiàn)了快 3 倍的快速刷新和快 5 倍的構(gòu)建速度;
- 按需編譯,在開(kāi)發(fā)環(huán)境下,只有訪問(wèn)的頁(yè)面才會(huì)進(jìn)行編譯
那么接下來(lái)的問(wèn)題就是:要在 Next.js 中實(shí)現(xiàn) “寫 Markdown Example 可預(yù)覽”的功能,若要自己實(shí)現(xiàn)這個(gè)功能,確實(shí)是一件麻煩的事情。我們換一個(gè)思維,組件展示,也就是在 markdown 中運(yùn)行 react 組件,這不就是 mdx 的功能嗎? 而在 Next.js 中可以很方便地集成 MDX。
效果演示

目前這是一個(gè)簡(jiǎn)易版,只為展示 Next.js 搭建文檔
項(xiàng)目初始化
首先我們創(chuàng)建一個(gè) next typescript 作為我們項(xiàng)目的主目錄,用于組件庫(kù)的文檔開(kāi)發(fā)
npx create-next-app@latest --ts
要想啟動(dòng) pnpm 的 workspace 功能,需要工程根目錄下存在 pnpm-workspace.yaml 配置文件,并且在 pnpm-workspace.yaml 中指定工作空間的目錄。比如這里我們所有的子包都是放在 packages 目錄下
packages: - 'packages/*'
接下來(lái),我們?cè)?packages 文件夾下創(chuàng)建三個(gè)子項(xiàng)目,分別是:user-select、login 和 utils, 對(duì)應(yīng)用戶選擇,登錄 和工具類。
├── packages │ ├── user-select │ ├── login │ ├── utils
user-select 和 login 依賴 utils,我們可以將一些公用方法放到 utils 中。
給每個(gè) package 下面創(chuàng)建 package.json 文件,包名稱通常是”@命名空間+包名@“的方式,比如@vite/xx 或@babel/xx,在本例中,這里我們都以@mastack開(kāi)頭
{
"name": "@mastack/login",
"version": "1.0.0",
"description": "",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc"
},
"keywords": [],
"author": "",
"license": "ISC"
}
給每個(gè) package 安裝 typescript
pnpm add typescript -r -D
給每個(gè) package 創(chuàng)建 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
}
}
執(zhí)行下面代碼,往 login 組件中安裝 utils;
pnpm i @mastack/utils --filter @mastack/login
安裝完成后,設(shè)置依賴版本的時(shí)候推薦用 workspace:*,就可以保持依賴的版本是工作空間里最新版本,不需要每次手動(dòng)更新依賴版本。
pnpm 提供了 -w, --workspace-root 參數(shù),可以將依賴包安裝到工程的根目錄下,作為所有 package 的公共依賴,這么我們安裝 antd
pnpm install antd -w
組件開(kāi)發(fā)
我們?cè)?login 組件下,新建一個(gè)組件 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>
</>
);
}
先寫一個(gè)最簡(jiǎn)單版本,組件代碼并不是最重要的,后續(xù)可以再優(yōu)化。
在package.json 中添加構(gòu)建命令
"scripts": {
"build": "tsc"
}
然后在組件目錄下執(zhí)行 yarn build 。此時(shí)組件以及可以打包成功!
Next.js 支持 MDX
接下來(lái)要讓文檔支持 MDX,在根目錄下執(zhí)行以下命令,安裝 mdx 和 loader 相關(guān)包
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 了。
我們?cè)?src/pages 目錄下,新建一個(gè) docs/index.mdx

先寫一個(gè)簡(jiǎn)單的 markdown 文件測(cè)試下

這樣 Next.js 就支持 mdx 文檔了。
Next 動(dòng)態(tài)加載 md 文件
接下來(lái),我們要實(shí)現(xiàn)動(dòng)態(tài)加載 packages 中的文件 md 文件。新建一個(gè) 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 條數(shù)據(jù),因?yàn)槲覀兡壳爸挥?2 個(gè)組件,它會(huì)在構(gòu)建的時(shí)候會(huì)生成靜態(tài)頁(yè)面。 getStaticProps函數(shù)可以獲取 URL 上的參數(shù),我們將 slug 參數(shù)傳遞給組件,然后在 Page 函數(shù)中,我們使用 next/dynamic 動(dòng)態(tài)加載 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>
);
}
此時(shí)我們?cè)L問(wèn) http://localhost:3000/docs/login 查看效果

在頁(yè)面上會(huì)提示,無(wú)法找到@mastack/login 這個(gè)包,我們需要在項(xiàng)目的根目錄下的 tsconfig.json 中加入別名
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"],
"@mastack/login": ["packages/login/src"],
"@mastack/user-select": ["packages/user-select/src"]
},
}
}
保存后,頁(yè)面會(huì)自動(dòng)刷新,我們就可以在頁(yè)面上看到如下效果。

至此文檔與 packages 目錄下的 mdx 已經(jīng)打通。修改 packages/login/docs/index.mdx 中的文檔,頁(yè)面會(huì)自動(dòng)熱更新。
自定義 mdx 組件
上面代碼已經(jīng)實(shí)現(xiàn)了在 md 文檔中顯示組件和代碼,但我們想要的是類似于 ant design 那樣的效果,默認(rèn)代碼不展示,點(diǎn)擊可以收起和展開(kāi),這該怎么實(shí)現(xiàn)呢?

我們可以利用 mdx 的自定義組件來(lái)實(shí)現(xiàn)這個(gè)效果。
寫 mdx 的時(shí)候,在組件 <Login/>和代碼外層嵌套一個(gè)自定義組件DemoBlock

然后實(shí)現(xiàn)一個(gè)自定義一個(gè) 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>
);
}
我們先寫一個(gè)空組件,看下 children 的值。刷新頁(yè)面, 此時(shí) DemoBlock中的組件和代碼不會(huì)顯示,我們看一下打印出的 children 節(jié)點(diǎn)信息;

chilren 為 react 中的 vNode,現(xiàn)在我們就可以根據(jù) type 來(lái)判斷,返回不同的 jsx,這樣就可以實(shí)現(xiàn)DemoBlock組件了,代碼如下:
import React, { useState } from "react";
const DemoBlock = ({ children }: any) => {
const [visible, setVisible] = useState(false);
return (
<div className="demo-block">
{children.map((child: any) => {
if (child.type === "pre") {
return (
<div key={child.key}>
<div
className="demo-block-button"
onClick={() => setVisible(!visible)}
>
{!visible ? "顯示代碼" : "收起代碼"}
</div>
{visible && child}
</div>
);
}
return child;
})}
</div>
);
};
再給組件添加一些樣式,給按鈕添加一個(gè) svg icon,一起來(lái)看下實(shí)現(xiàn)效果:

是不是有跟 antd 的 demo block 有些相似了呢? 若要顯示更多字段和描述,我們可以修改組件代碼,實(shí)現(xiàn)完全自定義。
優(yōu)化文檔界面
至此我們的文檔,還是有些簡(jiǎn)陋,我們得優(yōu)化下文檔界面,讓我們的界面顯示更美觀。
- 安裝并且初始化 tailwindcss
pnpm install -Dw tailwindcss postcss autoprefixer @tailwindcss/typography pnpx tailwindcss init -p
修改 globals.css 為 tailwindcss 默認(rèn)指令
@tailwind base; @tailwind components; @tailwind utilities;
修改 tailwind.config.js 配置文件,讓我們的應(yīng)用支持文章默認(rèn)樣式,并且在 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 組件 外層可以加一個(gè) prose class,這樣我們的文檔就有了默認(rèn)好看文章樣式了。
現(xiàn)在 md 文檔功能還很薄弱,我們需要讓它強(qiáng)大起來(lái),我們先安裝一些 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 (自動(dòng)超鏈接鏈接文字、腳注、刪除線、表格、任務(wù)列表)
remark-math rehype-katex 支持?jǐn)?shù)學(xué)公式
rehype-slug rehype-autolink-headings 自動(dòng)給標(biāo)題加唯一 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,
});
我們?cè)谶@里可以配置 remarkPlugins 和 rehypePlugins;
markdown 在編譯過(guò)程中會(huì)涉及 3 種 ast 抽象語(yǔ)法樹(shù) , remark 負(fù)責(zé)轉(zhuǎn)換為 mdast,它可以操作 markdown 文件,比如讓 markdown 支持更多格式(比如:公式、腳注、任務(wù)列表等),需要使用 remark 插件; rehype 負(fù)責(zé)轉(zhuǎn)換為 hast ,它可以轉(zhuǎn)換 html,比如給 標(biāo)題加 id,給代碼高亮, 這一步是在操作 HTML 后完成的。因此我們也可以自己寫插件,具體寫什么插件,就要看插件在哪個(gè)階段運(yùn)行。
最后我們到 github prism-themes 中復(fù)制一份代碼高亮的樣式到我們的 css 文件中,一起來(lái)看下效果吧!

發(fā)布工作流
workspace 中的包版本管理是一個(gè)復(fù)雜的任務(wù),pnpm 目前也并未提供內(nèi)置的解決方案。pnpm 推薦了兩個(gè)開(kāi)源的版本控制工具:changesets 和 rush,這里我采用了 changesets 來(lái)實(shí)現(xiàn)依賴包的管理。
配置
要在 pnpm 工作空間上配置 changesets,請(qǐng)將 changesets 作為開(kāi)發(fā)依賴項(xiàng)安裝在工作空間的根目錄中:
pnpm add -Dw @changesets/cli
然后 changesets 的初始化命令:
pnpm changeset init
添加新的 changesets
要生成新的 changesets,請(qǐng)?jiān)趥}(cāng)庫(kù)的根目錄中執(zhí)行pnpm changeset。 .changeset 目錄中生成的 markdown 文件需要被提交到到倉(cāng)庫(kù)。
發(fā)布變更
為了方便所有包的發(fā)布過(guò)程,在工程根目錄下的 pacakge.json 的 scripts 中增加如下幾條腳本:
"compile": "pnpm --filter=@mastack/* run build", "pub": "pnpm compile && pnpm --recursive --registry https://registry.npmjs.org/ publish --access public"
編譯階段,生成構(gòu)建產(chǎn)物
- 運(yùn)行
pnpm changeset version。 這將提高先前使用pnpm changeset(以及它們的任何依賴項(xiàng))的版本,并更新變更日志文件。 - 運(yùn)行
pnpm install。 這將更新鎖文件并重新構(gòu)建包。 - 提交更改。
- 運(yùn)行
pnpm pub。 此命令將發(fā)布所有包含被更新版本且尚未出現(xiàn)在包注冊(cè)源中的包。
部署
部署可以選擇 gitbub pages 或者 vercel 部署,他們都是免費(fèi)的,Github pages 只支持靜態(tài)網(wǎng)站,vercel 支持動(dòng)態(tài)的網(wǎng)站,它會(huì)將 nextjs page 中,單獨(dú)部署成函數(shù)的形式。我這里選擇使用 vercel,因?yàn)樗脑L問(wèn)速度相對(duì)比 gitbub pages 要快很多。只需要使用 github 賬號(hào)登錄 vercel.com/ 導(dǎo)入項(xiàng)目,便會(huì)自動(dòng)部署,而且會(huì)自動(dòng)分配一個(gè) xxx.vercel.app/ 二級(jí)域名。
也可以使用命令行工具,在項(xiàng)目跟目錄下執(zhí)行,根據(jù)提示,選擇默認(rèn)即可
npx vercel

預(yù)覽地址:nextjs-components-docs.vercel.app/
小結(jié)
本文,我們從零開(kāi)始,使用 Next.js 和 pnpm 搭建了一個(gè)組件庫(kù)文檔,主要使用 Next.js 動(dòng)態(tài)導(dǎo)入功能解決了開(kāi)發(fā)服務(wù)緩慢的問(wèn)題,使用 Next.js 的 SSG 模式來(lái)生成靜態(tài)文檔。最后我們使用 changesets 來(lái)管理包的 version 和生成 changelog。
以上就是Next.js搭建Monorepo組件庫(kù)文檔的詳細(xì)內(nèi)容,更多關(guān)于Next.js搭建Monorepo的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
react使用websocket實(shí)時(shí)通信方式
這篇文章主要介紹了react使用websocket實(shí)時(shí)通信方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-09-09
React中路由參數(shù)如何改變頁(yè)面不刷新數(shù)據(jù)的情況
這篇文章主要介紹了React中路由參數(shù)如何改變頁(yè)面不刷新數(shù)據(jù)的情況,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08
基于react hooks,zarm組件庫(kù)配置開(kāi)發(fā)h5表單頁(yè)面的實(shí)例代碼
這篇文章主要介紹了基于react hooks,zarm組件庫(kù)配置開(kāi)發(fā)h5表單頁(yè)面,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04
React 性能優(yōu)化之非必要的渲染問(wèn)題解決
本文主要介紹了React 性能優(yōu)化之非必要的渲染問(wèn)題解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-07-07

