Vue組件庫文檔站點的搭建思路

語言: CN / TW / HK

持續創作,加速成長!這是我參與「掘金日新計劃 · 10 月更文挑戰」的第4天,點擊查看活動詳情

本文為Varlet組件庫源碼主題閲讀系列第四篇,讀完本篇,可以瞭解到如何使用ViteApi接口來啟動服務、如何動態生成多語言的頁面路由。

Varlet的文檔網站其實就是一個Vue項目,整體分成兩個單獨的頁面:文檔頁面及手機預覽頁面。

網站源代碼文件默認是放在varlet-cli目錄下,也就是腳手架的包裏:

執行腳手架提供的dev命令時會把這個目錄複製到varlet-ui/.varlet目錄下,並且動態生成兩個頁面的路由配置文件:

然後使用Vite啟動服務。

啟動命令

先來看一下varlet-cli提供的dev命令都做了些什麼。

```js // varlet-cli/src/index.ts import { Command } from 'commander' const program = new Command()

program .command('dev') .option('-f --force', 'Force dep pre-optimization regardless of whether deps have changed') .description('Run varlet development environment') .action(dev) ```

可以看到這個命令是用來運行varlet的開發環境的,還提供了一個參數,用來強制開啟Vite依賴預構建功能,處理函數是dev

ts // varlet-cli/src/commands/dev.ts export async function dev(cmd: { force?: boolean }) { process.env.NODE_ENV = 'development' // SRC_DIR:varlet-ui/src,即組件的源碼目錄 ensureDirSync(SRC_DIR) await startServer(cmd.force) }

設置了環境變量,確保組件源目錄是否存在,最後調用了startServer方法:

ts // varlet-cli/src/commands/dev.ts let server: ViteDevServer let watcher: FSWatcher async function startServer(force: boolean | undefined) { // 如果server實例已經存在了,那麼代表是重啟 const isRestart = Boolean(server) // 先關閉之前已經存在的實例 server && (await server.close()) watcher && (await watcher.close()) // 構建站點入口 await buildSiteEntry() }

構建站點項目

複製站點文件的操作就在buildSiteEntry方法裏:

ts // varlet-cli/src/compiler/compileSiteEntry.ts export async function buildSiteEntry() { getVarletConfig(true) await Promise.all([buildMobileSiteRoutes(), buildPcSiteRoutes(), buildSiteSource()]) }

主要執行了四個方法,先看getVarletConfig

```ts // varlet-cli/src/config/varlet.config.ts export function getVarletConfig(emit = false): Record { let config: any = {} // VARLET_CONFIG:varlet-ui/varlet.config.js,即varlet-ui組件庫目錄下的配置文件 if (pathExistsSync(VARLET_CONFIG)) { // require方法導入後會進行緩存,下次同樣的導入會直接使用緩存,所以當重新啟動服務時需要先刪除緩存 delete require.cache[require.resolve(VARLET_CONFIG)] config = require(VARLET_CONFIG) } // 默認配置,varlet-cli/varlet.default.config.js delete require.cache[require.resolve('../../varlet.default.config.js')] const defaultConfig = require('../../varlet.default.config.js') // 合併配置 const mergedConfig = merge(defaultConfig, config)

if (emit) { const source = JSON.stringify(mergedConfig, null, 2) // SITE_CONFIG:resolve(CWD, '.varlet/site.config.json') // outputFileSyncOnChange方法會檢查內容是否有變化,沒有變化不會重新寫入文件 outputFileSyncOnChange(SITE_CONFIG, source) } return mergedConfig } ```

這個方法主要是合併組件庫目錄varlet-ui下的配置文件和默認的配置文件,然後將合併後的配置寫入到站點的目標目錄varlet-ui/.varlet/下。

合併完配置後執行了三個build方法:

生成手機頁面路由

1.buildMobileSiteRoutes()方法:

``ts // varlet-cli/src/compiler/compileSiteEntry.ts export async function buildMobileSiteRoutes() { const examples: string[] = await findExamples() // 拼接路由 const routes = examples.map( (example) => { path: '${getExampleRoutePath(example)}', // @ts-ignore component: () => import('${example}') }` )

const source = export default [\ ${routes.join(',')} ] // SITE_MOBILE_ROUTES:resolve(CWD, '.varlet/mobile.routes.ts'),站點的手機預覽頁面路由文件 await outputFileSyncOnChange(SITE_MOBILE_ROUTES, source) } ```

這個方法主要是構建手機預覽頁面的路由文件,路由其實就是路徑到組件的映射,所以先獲取了路由組件列表,然後按格式拼接路由的內容,最後寫入文件。

findExamples()

ts // varlet-cli/src/compiler/compileSiteEntry.ts export function findExamples(): Promise<string[]> { // SRC_DIR:varlet-ui/scr目錄,即組件庫的源碼目錄 // EXAMPLE_DIR_NAME:example,即每個組件的示例目錄 // DIR_INDEX:index.vue return glob(`${SRC_DIR}/**/${EXAMPLE_DIR_NAME}/${DIR_INDEX}`) }

從組件庫源碼目錄裏獲取每個組件的示例組件,每個組件都是一個單獨的目錄,目錄下存在一個example示例文件目錄,該目錄下的index.vue即示例組件,比如按鈕組件Button的目錄及示例組件如下:

這個方法獲取到的是絕對路徑,並不能用作路由的path,所以需要進行一下處理:

ts // varlet-cli/src/compiler/compileSiteEntry.ts const EXAMPLE_COMPONENT_NAME_RE = /\/([-\w]+)\/example\/index.vue/ export function getExampleRoutePath(examplePath: string): string { return '/' + examplePath.match(EXAMPLE_COMPONENT_NAME_RE)?.[1] }

提取出example前面的一段,即組件的目錄名稱,也就是組件的名稱,最後生成的路由數據如下:

生成pc頁面路由

2.buildPcSiteRoutes()方法:

pc頁面的路由稍微會複雜一點:

ts // varlet-cli/src/compiler/compileSiteEntry.ts export async function buildPcSiteRoutes() { const [componentDocs, rootDocs, rootLocales] = await Promise.all([ findComponentDocs(), findRootDocs(), findRootLocales(), ]) }

獲取了三類文件,第一種:

ts // varlet-cli/src/compiler/compileSiteEntry.ts export function findComponentDocs(): Promise<string[]> { // SRC_DIR:varlet-ui/scr目錄,即組件庫的源碼目錄 // DOCS_DIR_NAME:docs return glob(`${SRC_DIR}/**/${DOCS_DIR_NAME}/*.md`) }

獲取組件目錄varlet-ui/src/**/docs/*.md文件,也就是獲取每個組件的文檔文件,比如Button組件:

文檔是markdown格式編寫的。

第二種:

ts // varlet-cli/src/compiler/compileSiteEntry.ts export function findRootDocs(): Promise<string[]> { // ROOT_DOCS_DIR:varlet-ui/docs return glob(`${ROOT_DOCS_DIR}/*.md`) }

獲取除組件文檔外的其他文檔,比如基本介紹、快速開始之類的。

第三種:

ts // varlet-cli/src/compiler/compileSiteEntry.ts export async function findRootLocales(): Promise<string[]> { // 默認的語言 const defaultLanguage = get(getVarletConfig(), 'defaultLanguage') // SITE:varlet-cli/site/ // LOCALE_DIR_NAME:locale const baseLocales = await glob(`${SITE}/pc/pages/**/${LOCALE_DIR_NAME}/*.ts`) }

獲取默認的語言類型,默認是zh-CN,然後獲取站點pc頁面的locale文件,繼續:

ts // varlet-cli/src/compiler/compileSiteEntry.ts const ROOT_LOCALE_RE = /\/pages\/([-\w]+)\/locale\/([-\w]+)\.ts/ export async function findRootLocales(): Promise<string[]> { // ... const filterMap = new Map() baseLocales.forEach((locale) => { // 解析出頁面path及文件的語言類型 const [, routePath, language] = locale.match(ROOT_LOCALE_RE) ?? [] // SITE_PC_DIR:varlet-ui/.varlet/site/pc filterMap.set(routePath + language, slash(`${SITE_PC_DIR}/pages/${routePath}/locale/${language}.ts`)) }) return Promise.resolve(Array.from(filterMap.values())) }

返回獲取到pc站點的locale文件路徑,也就是這些文件:

目前只有一個Index頁面,也就是站點的首頁:

回到buildPcSiteRoutes()方法,文件路徑都獲取完了,接下來肯定就是遍歷生成路由配置了:

ts // varlet-cli/src/compiler/compileSiteEntry.ts export async function buildPcSiteRoutes() { // ... // 生成站點頁面路由 const rootPagesRoutes = rootLocales.map( (rootLocale) => ` { path: '${getRootRoutePath(rootLocale)}', // @ts-ignore component: () => import('${getRootFilePath(rootLocale)}') }\ ` ) }

有多少種翻譯,同一個組件就會生成多少種路由,對於站點首頁來説,目前存在en-US.tszh-CN兩種翻譯文件,那麼會生成下面兩個路由:

繼續:

ts // varlet-cli/src/compiler/compileSiteEntry.ts export async function buildPcSiteRoutes() { // ... // 生成每個組件的文檔路由 const componentDocsRoutes = componentDocs.map( (componentDoc) => ` { path: '${getComponentDocRoutePath(componentDoc)}', // @ts-ignore component: () => import('${componentDoc}') }` ) // 生成其他文檔路由 const rootDocsRoutes = rootDocs.map( (rootDoc) => ` { path: '${getRootDocRoutePath(rootDoc)}', // @ts-ignore component: () => import('${rootDoc}') }` ) }

接下來拼接了組件文檔和其他文檔的路由,同樣也是存在幾種翻譯,就會生成幾個路由:

繼續:

ts // varlet-cli/src/compiler/compileSiteEntry.ts export async function buildPcSiteRoutes() { // ... const layoutRoutes = `{ path: '/layout', // @ts-ignore component:()=> import('${slash(SITE_PC_DIR)}/Layout.vue'), children: [ ${[...componentDocsRoutes, rootDocsRoutes].join(',')}, ] }` }

這個路由是幹嘛的呢,其實就是真正的文檔頁面了:

組件文檔路由和其他文檔路由都是它的子路由,Layout.vue組件提供了組件詳情頁面的基本骨架,包括頁面頂部欄、左邊的菜單欄,中間部分就是子路由的出口,即具體的文檔,右側通過iframe引入了手機預覽頁面。

最後導出路由配置及寫入到文件即可:

ts // varlet-cli/src/compiler/compileSiteEntry.ts export async function buildPcSiteRoutes() { // ... const source = `export default [\ ${rootPagesRoutes.join(',')}, ${layoutRoutes} ]` // SITE_PC_ROUTES:varlet-ui/.varlet/pc.routes.ts outputFileSyncOnChange(SITE_PC_ROUTES, source) }

複製站點文件

3.buildSiteSource()方法:

ts // varlet-cli/src/compiler/compileSiteEntry.ts export async function buildSiteSource() { return copy(SITE, SITE_DIR) }

這個方法很簡單,就是將站點的項目文件由varlet-cli/site目錄複製到varlet-ui/.varlet/site目錄下。

總結一下上述操作,就是將站點的源代碼文件由cli包複製到ui包,然後動態生成站點項目的路由文件。整個站點分為兩個頁面pcmobilepc頁面主要是提供文檔展示及嵌入mobile頁面,mobile頁面用來展示各個組件的demo

啟動服務

項目準備就緒,接下來就是啟動服務了,回到startServer方法:

ts // varlet-ui/src/commands/dev.ts async function startServer(force: boolean | undefined) { await buildSiteEntry() // 獲取合併後的配置 const varletConfig = getVarletConfig() // 獲取Vite的啟動配置,部分配置來自於varletConfig const devConfig = getDevConfig(varletConfig) // 將是否強制進行依賴預構建配置合併到Vite配置 const inlineConfig = merge(devConfig, force ? { server: { force: true } } : {}) }

生成Vite的啟動配置,然後就可以啟動服務了:

ts // varlet-ui/src/commands/dev.ts async function startServer(force: boolean | undefined) { // ... // 啟動Vite服務 server = await createServer(inlineConfig) await server.listen() server.printUrls() // VARLET_CONFIG:varlet-ui/varlet.config.js // 監聽用户配置文件,修改了就重新啟動服務 if (pathExistsSync(VARLET_CONFIG)) { watcher = chokidar.watch(VARLET_CONFIG) watcher.on('change', () => startServer(force)) } }

使用了ViteJavaScript API來啟動服務,並且當配置文件發送變化會重啟服務。

Vite配置

接下來詳細看一下上一步啟動服務時的Vite配置:

```ts // varlet-cli/src/config/vite.config.ts export const VITE_RESOLVE_EXTENSIONS = ['.vue', '.tsx', '.ts', '.jsx', '.js', '.less', '.css']

export function getDevConfig(varletConfig: Record): InlineConfig { // 默認語言 const defaultLanguage = get(varletConfig, 'defaultLanguage') // 端口 const host = get(varletConfig, 'host')

return { root: SITE_DIR,// 項目根目錄:varlet-ui/.varlet/site resolve: { extensions: VITE_RESOLVE_EXTENSIONS,// 導入時想要省略的擴展名列表 alias: {// 導入路徑別名 '@config': SITE_CONFIG, '@pc-routes': SITE_PC_ROUTES, '@mobile-routes': SITE_MOBILE_ROUTES, }, }, server: {// 設置要監聽的端口號和ip地址 port: get(varletConfig, 'port'), host: host === 'localhost' ? '0.0.0.0' : host, }, publicDir: SITE_PUBLIC_PATH,// 作為靜態資源服務的文件夾:varlet-ui/public // ... } } ```

設置了一些基本配置,你可能會有個小疑問,站點項目明明是個多頁面項目,但是上面似乎並沒有配置任何多頁面相關的內容,其實在Vue Cli項目中是需要修改入口配置的,但是在Vite項目中不需要,這可能就是開發環境不需要打包的一個好處吧,不過雖然開發環境不需要配置,但是最後打包的時候是需要的:

接下來還配置了一系列的插件:

```ts import vue from '@vitejs/plugin-vue' import md from '@varlet/markdown-vite-plugin' import jsx from '@vitejs/plugin-vue-jsx' import { injectHtml } from 'vite-plugin-html'

export function getDevConfig(varletConfig: Record): InlineConfig { // ... return { // ... plugins: [ // 提供 Vue 3 單文件組件支持 vue({ include: [/.vue$/, /.md$/], }), md({ style: get(varletConfig, 'highlight.style') }), // 提供 Vue 3 JSX 支持 jsx(), // 給html頁面注入數據 injectHtml({ data: { pcTitle: get(varletConfig, pc.title['${defaultLanguage}']), mobileTitle: get(varletConfig, mobile.title['${defaultLanguage}']), logo: get(varletConfig, logo), baidu: get(varletConfig, analysis.baidu, ''), }, }), ], } } ```

一共使用了四個插件,其中的md插件是Varlet自己編寫的,顧名思義,就是用來處理md文件的,具體邏輯我們下一篇再看。

打包

最後就是站點項目的打包了,使用的是varlet-cli提供的build命令:

ts // varlet-cli/src/index.ts program.command('build').description('Build varlet site for production').action(build)

處理函數為build

```ts // varlet-cli/src/commands/build.ts export async function build() { process.env.NODE_ENV = 'production'

ensureDirSync(SRC_DIR) await buildSiteEntry() const varletConfig = getVarletConfig() // 獲取Vite的打包配置 const buildConfig = getBuildConfig(varletConfig)

await buildVite(buildConfig) } ```

邏輯很簡單,先設置環境變量為生產環境,然後同樣執行了buildSiteEntry方法,最後獲取Vite的打包配置進行打包即可:

```ts // varlet-cli/src/config/vite.config.ts export function getBuildConfig(varletConfig: Record): InlineConfig { const devConfig = getDevConfig(varletConfig)

return { ...devConfig, base: './',// 公共基礎路徑 build: { outDir: SITE_OUTPUT_PATH,// varlet-ui/site brotliSize: false,// 禁用 brotli 壓縮大小報告 emptyOutDir: true,// 輸出目錄不在root目錄下,所以需要手動開啟該配置來清空輸出目錄 cssTarget: 'chrome61',// https://www.vitejs.net/config/#build-csstarget rollupOptions: { input: {// 多頁面入口配置 main: resolve(SITE_DIR, 'index.html'), mobile: resolve(SITE_DIR, 'mobile.html'), }, }, }, } } ```

在啟動服務的配置基礎上增加了幾個打包相關的配置。

到這裏文檔站點的初始化、啟動、構建辦法就介紹完了,我們下一篇再見~