Next.js搭建Monorepo組件庫(kù)文檔實(shí)現(xiàn)詳解
前言
- 使用 pnpm 搭建一個(gè) Monorepo 組件庫(kù)
- 使用 Next.js 開發(fā)一個(gè)組件庫(kù)文檔
- changesets 來管理包的 version 和生成 changelog
- 使用 vercel 部署在線文檔
代碼倉(cāng)庫(kù):github.com/maqi1520/ne…
組件化開發(fā)是前端的基石,正因?yàn)榻M件化,前端得以百花齊放,百家爭(zhēng)鳴。我們每天在項(xiàng)目中都寫著各種各樣的組件,如果在面試的時(shí)候,跟面試官說,你每天的工作是開發(fā)組件,那么顯然這沒有什么優(yōu)勢(shì),如果你說,你開發(fā)了一個(gè)組件庫(kù),并且有一個(gè)在線文檔可以直接預(yù)覽,這可能會(huì)是你的一個(gè)加分項(xiàng)。今天我們就來聊聊組件庫(kù)的開發(fā),主要是組件庫(kù)的搭建和文檔建設(shè),至于組件數(shù)量,那是時(shí)間問題,以及你是否有時(shí)間維護(hù)好這個(gè)組件庫(kù)的問題。
基礎(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ú)迭代開發(fā),單獨(dú)發(fā)布,卻有一些共同的邏輯,比如沒有登錄的時(shí)候都需要調(diào)用一個(gè)”登錄彈窗“,或者說在項(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 來搭建組件庫(kù)文檔?
組件文檔有個(gè)特別重要的功能就是“寫 markdown 文檔,可以看到代碼以及運(yùn)行效果”,這方面有很多優(yōu)秀的開源庫(kù),比如 Ant design 使用的是 bisheng, react use 使用的是 storybook, 還有一些優(yōu)秀的庫(kù),比如:dumi,Docz 等。 本地跑過 Ant design 的同學(xué)都知道, Ant design 的啟動(dòng)速度非常慢,因?yàn)榈讓邮褂玫?webpack,要啟動(dòng)開發(fā)服務(wù)器,必須將所有組件都進(jìn)行編譯,這會(huì)對(duì)開發(fā)者造成一些困擾,因?yàn)槿绻菢I(yè)務(wù)組件的話,開發(fā)者只關(guān)注單個(gè)組件,而不是全部組件。而使用 Next.jz 就有 2 個(gè)非常大的優(yōu)勢(shì):
- 使用 swc 編譯,Next.js 中實(shí)現(xiàn)了快 3 倍的快速刷新和快 5 倍的構(gòu)建速度;
- 按需編譯,在開發(fā)環(huán)境下,只有訪問的頁(yè)面才會(huì)進(jì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ù)的文檔開發(fā)
npx create-next-app@latest --ts
要想啟動(dòng) pnpm 的 workspace 功能,需要工程根目錄下存在 pnpm-workspace.yaml
配置文件,并且在 pnpm-workspace.yaml
中指定工作空間的目錄。比如這里我們所有的子包都是放在 packages 目錄下
packages: - 'packages/*'
接下來,我們?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
開頭
{ "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
組件開發(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
接下來要讓文檔支持 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 文件
接下來,我們要實(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問 http://localhost:3000/docs/login
查看效果
在頁(yè)面上會(huì)提示,無法找到@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)擊可以收起和展開,這該怎么實(shí)現(xiàn)呢?
我們可以利用 mdx 的自定義組件來實(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 來判斷,返回不同的 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,一起來看下實(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)大起來,我們先安裝一些 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 在編譯過程中會(huì)涉及 3 種 ast 抽象語(yǔ)法樹 , 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 文件中,一起來看下效果吧!
發(fā)布工作流
workspace 中的包版本管理是一個(gè)復(fù)雜的任務(wù),pnpm 目前也并未提供內(nèi)置的解決方案。pnpm 推薦了兩個(gè)開源的版本控制工具:changesets 和 rush,這里我采用了 changesets 來實(shí)現(xiàn)依賴包的管理。
配置
要在 pnpm 工作空間上配置 changesets,請(qǐng)將 changesets 作為開發(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ā)布過程,在工程根目錄下的 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問速度相對(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é)
本文,我們從零開始,使用 Next.js 和 pnpm 搭建了一個(gè)組件庫(kù)文檔,主要使用 Next.js 動(dòng)態(tài)導(dǎo)入功能解決了開發(fā)服務(wù)緩慢的問題,使用 Next.js 的 SSG 模式來生成靜態(tài)文檔。最后我們使用 changesets 來管理包的 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-09React中路由參數(shù)如何改變頁(yè)面不刷新數(shù)據(jù)的情況
這篇文章主要介紹了React中路由參數(shù)如何改變頁(yè)面不刷新數(shù)據(jù)的情況,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08基于react hooks,zarm組件庫(kù)配置開發(fā)h5表單頁(yè)面的實(shí)例代碼
這篇文章主要介紹了基于react hooks,zarm組件庫(kù)配置開發(fā)h5表單頁(yè)面,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-04-04