本文為Varlet元件庫源碼主題閱讀系列第四篇,讀完本篇,可以了解到如何使用的
Vite
接口來啟動服務、如何動态生成多語言的頁面路由。
Api
Varlet
的文檔網站其實就是一個
Vue
項目,整體分成兩個單獨的頁面:文檔頁面及手機預覽頁面。
網站源代碼檔案預設是放在
varlet-cli
目錄下,也就是腳手架的包裡:
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICN4ETMfdHLkVGepZ2XtxSZ6l2clJ3LcBnYldHL0FWby9mZvwVPrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdsAjMfd3bkFGazxCMx8VesATMfhHLlN3XnxCMz8FdsYkRGZkRG9lcvx2bjxSa2EWNhJTW1AlUxEFeVRUUfRHelRHL2EzXlpXazxyayFWbyVGdhd3LcV2Zh1Wa9M3clN2byBXLzN3btg3PwJWZ35yM4IzMzQTN2IGNiNDO4kDNzYzX3AzMwADM1EzLcBTMyIDMy8CXn9Gbi9CXzV2Zh1WavwVbvNmLvR3YxUjLyM3Lc9CX6MHc0RHaiojIsJye.webp)
執行腳手架提供的
dev
指令時會把這個目錄複制到
varlet-ui/.varlet
目錄下,并且動态生成兩個頁面的路由配置檔案:
然後使用
Vite
啟動服務。
啟動指令
先來看一下
varlet-cli
提供的
dev
指令都做了些什麼。
// 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
:
// 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
方法:
// 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
方法裡:
// varlet-cli/src/compiler/compileSiteEntry.ts
export async function buildSiteEntry() {
getVarletConfig(true)
await Promise.all([buildMobileSiteRoutes(), buildPcSiteRoutes(), buildSiteSource()])
}
主要執行了四個方法,先看
getVarletConfig
:
// varlet-cli/src/config/varlet.config.ts
export function getVarletConfig(emit = false): Record<string, any> {
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
這個方法主要是合并元件庫目錄
varlet-ui
下的配置檔案和預設的配置檔案,然後将合并後的配置寫入到站點的目标目錄
varlet-ui/.varlet/
下。
合并完配置後執行了三個
build
方法:
生成手機頁面路由
1.
buildMobileSiteRoutes()
方法:
// 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()
:
// 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
,是以需要進行一下處理:
// 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
頁面的路由稍微會複雜一點:
// varlet-cli/src/compiler/compileSiteEntry.ts
export async function buildPcSiteRoutes() {
const [componentDocs, rootDocs, rootLocales] = await Promise.all([
findComponentDocs(),
findRootDocs(),
findRootLocales(),
])
}
擷取了三類檔案,第一種:
// 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
格式編寫的。
第二種:
// varlet-cli/src/compiler/compileSiteEntry.ts
export function findRootDocs(): Promise<string[]> {
// ROOT_DOCS_DIR:varlet-ui/docs
return glob(`${ROOT_DOCS_DIR}/*.md`)
}
擷取除元件文檔外的其他文檔,比如基本介紹、快速開始之類的。
第三種:
// 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
檔案,繼續:
// 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()
方法,檔案路徑都擷取完了,接下來肯定就是周遊生成路由配置了:
// 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.ts
、
zh-CN
兩種翻譯檔案,那麼會生成下面兩個路由:
繼續:
// 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}')
}`
接下來拼接了元件文檔和其他文檔的路由,同樣也是存在幾種翻譯,就會生成幾個路由:
繼續:
// 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
引入了手機預覽頁面。
最後導出路由配置及寫入到檔案即可:
// 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()
方法:
// varlet-cli/src/compiler/compileSiteEntry.ts
export async function buildSiteSource() {
return copy(SITE, SITE_DIR)
}
這個方法很簡單,就是将站點的項目檔案由
varlet-cli/site
目錄複制到
varlet-ui/.varlet/site
目錄下。
總結一下上述操作,就是将站點的源代碼檔案由
cli
包複制到
ui
包,然後動态生成站點項目的路由檔案。整個站點分為兩個頁面
pc
、
mobile
,
pc
頁面主要是提供文檔展示及嵌入
mobile
頁面,
mobile
頁面用來展示各個元件的
demo
。
啟動服務
項目準備就緒,接下來就是啟動服務了,回到
startServer
方法:
// 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
的啟動配置,然後就可以啟動服務了:
// 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))
}
}
使用了
Vite
的JavaScript API來啟動服務,并且當配置檔案發送變化會重新開機服務。
Vite配置
接下來詳細看一下上一步啟動服務時的
Vite
配置:
// varlet-cli/src/config/vite.config.ts
export const VITE_RESOLVE_EXTENSIONS = ['.vue', '.tsx', '.ts', '.jsx', '.js', '.less', '.css']
export function getDevConfig(varletConfig: Record<string, any>): 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
項目中不需要,這可能就是開發環境不需要打包的一個好處吧,不過雖然開發環境不需要配置,但是最後打包的時候是需要的:
接下來還配置了一系列的插件:
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<string, any>): 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
指令:
// varlet-cli/src/index.ts
program.command('build').description('Build varlet site for production').action(build)
處理函數為
build
:
// 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)
}
// varlet-cli/src/config/vite.config.ts
export function getBuildConfig(varletConfig: Record<string, any>): 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'),
},
},
},
}
}