前言
前面两篇我们对Vite
插件的开发有了一个大致的了解,这篇我们从零开始,一步步实现Naive UI
组件库说明文档的Vite
插件。
本文的初始化的代码可以直接克隆这里的代码,从tag 3.0
签出分支获取干净的目录结构。(实际上Naive UI
的站点更加复杂,因为还包括了主题切换、国际化、编辑器等等),这个标签的代码最大可能的排除了非重点的部分
项目初始化
项目依赖
- highlight.js
代码高亮
- vue-router
Vue的路由
开发依赖
- marked
markdown解析器
- naive-ui
组件库
- unplugin-vue-components
vite插件 自动加载naive ui组件
- eslint
- prettier
- @vicons/ionicons5
图标库 纯粹的测试用 可以不安装
- fs-extra
文件操作库
- deepmerge
对象合并库
- codesandbox
代码沙箱
- typescript
- eslint-plugin-markdown
eslint markdown的规则插件
- esbuild 借助
esbuild
的相关api处理代码
依赖安装
pnpm i highlight.js pinia vue-router -S
pnpm i marked naive-ui unplugin-vue-components eslint prettier @vicons/ionicons5 fs-extra deepmerge codesandbox typescript -D
配置eslint和tsconfig
# 初始化tsconfig.json
npx tsc --init
配置简单的eslint规则后还不够,需要针对每种文件类型的规则进行配置。详细配置如下:
// .eslintrc.cjs
module.exports = {
extends: ['plugin:markdown/recommended', 'prettier'],
parserOptions: {
project: ['./tsconfig.json']
},
overrides: [
{
files: '*.vue',
extends: [
'@vue/typescript/recommended',
'plugin:vue/vue3-recommended',
'@vue/typescript'
]
},
{
files: ['*.vue', '*.js'],
extends: ['plugin:vue/vue3-recommended'],
rules: {
'vue/multiline-html-element-content-newline': 0,
'vue/multi-word-component-names': 0,
'vue/max-attributes-per-line': [
2,
{
singleline: 20,
multiline: 1
}
],
'vue/require-default-prop': 0,
'vue/no-multiple-template-root': 0,
'vue/no-lone-template': 0,
'vue/no-v-model-argument': 0,
'vue/one-component-per-file': 0,
'import/no-cycle': 1
}
},
{
files: ['*.ts', '*.tsx'],
extends: ['standard-with-typescript', 'plugin:import/typescript'],
parserOptions: {
project: './tsconfig.json',
ecmaFeatures: {
jsx: true
}
},
rules: {
'@typescript-eslint/strict-boolean-expressions': 0,
'@typescript-eslint/prefer-nullish-coalescing': 0,
'@typescript-eslint/explicit-function-return-type': 0,
'@typescript-eslint/naming-convention': 0,
'multiline-ternary': 0,
'no-void': 0,
'import/no-cycle': 1
}
},
{
files: ['*.md'],
processor: 'markdown/markdown',
rules: {
'MD025/single-title/single-h1': 0
}
},
{
files: '*',
globals: {
__DEV__: 'readonly'
}
}
]
}
tsconfig.json配置也需要处理,初始化的tsconfig不易阅读,我们可以全部删除,贴上以下代码:
{
"compilerOptions": {
"strict": true,
"strictNullChecks": true,
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment",
"noImplicitAny": true,
"noUnusedLocals": false,
"module": "ES6",
"moduleResolution": "Node",
"declaration": true,
"forceConsistentCasingInFileNames": true,
"composite": true,
"target": "ES6",
"esModuleInterop": true,
"lib": ["ESNext", "DOM"]
},
"exclude": ["node_modules","dist"]
}
至此,eslint和tsconfig配置完成。
文件目录初始化
首先我们先将目录进行设计,主要的目录就是demo
和src
,demo
是文档站点,src
用于存放组件的代码。
|-- demo
| |-- App.vue
| |-- main.js
| |-- router
| | |-- index.js
| | |-- routes.js
| |-- layout
| | |-- BasicLayout.vue # 基础布局
| | |-- SiteFooter.vue # 底部
| | |-- SiteHeader.vue # 头部
| |-- pages
| | |-- Home.vue
| |-- utils
| | |-- codesandbox.js # 代码沙箱配置 如果组件没有发布 可以不需要配置 本文也不做重点
| | |-- composables.js # 公共方法
| | |-- ComponentDemo.vue # 演示组件
| | |-- ComponentDemos.tsx # 包裹所有演示组件的父级组件
| | |-- CopyCodeButton.tsx # 复制代码按钮
| | |-- EditInCodeSandboxButton.tsx # 代码沙盒编辑按钮
| | |-- EditOnGithubButton.tsx # 去github编辑的按钮 直接定位到github的对应demo文件
| | |-- EditOnGithubHeader.tsx # 去github编辑文档标题的按钮
| | |-- github-url.js # github地址配置
| | |-- hljs.js # 代码高亮配置
| | |-- route.js # 路由相关的函数,提供生成左侧菜单的工具函数
|-- src
| |-- index.ts # 组件库入口 也是打包的入口
| |-- components.ts # 组件库的所有组件
| |-- button
| | |-- index.ts # 组件入口
| | |-- src
| | | |-- Button.tsx # 组件源码
| | |-- demos
| | | |-- basic.demo.vue # 演示组件
| | | |-- index.demo-entry.md # 演示组件的文档
.... # 还有一些test、style等目录 本文不做关注
插件的逐步编写
其实想了很久应该怎么去写这一块,因为其中引用的东西太多了,又加上自己第一次尝试写文,所以想了想就从入口开始,遇到什么,讲什么,最终再来个概括。
入口文件内容解析
首先,我们需要对.vue
和.md
结尾的文件进行过滤,因为主要的就是对md
和xxx.demo.vue
文件进行操作,所以先定义好过滤的正则,然后对匹配到的文件进行处理:
// ./plugins/index.js
const fileRegex = /\.(md|vue)$/
const myPlugin = {
name: "vite-md-parse",
transform(code, id) {
if (fileRegex.test(id)) {
return tragetTransformedVueSrc(code)
}
},
};
接着我们创建./plugins/transfrom-vue-src.js
,这个文件是用来具体匹配进入的文件是.md
还是demo.xxx
文件。
.md
文件会被解析成一个合规的vue文件的内容,如下图蓝框部分内容,就是该文件解析的。
demo.xx
:用于展示组件的文件,一个文件最终处理为组件的形式加载到当前.md
文档内容中,即下图的红色部分,一个红色框代表一个demo文件
。
然后我们来看详细代码。很好理解,就是根据路径来区分是进行文档解析
还是demo解析
。(ps:文件和函数名尽可能的语义化,这样方便阅读)
// ./plugins/transfrom-vue-src.js
import demoLoader from './utils/demo-loader'
import docLoader from './utils/doc-loader'
const targetTransformedVueSrc = async (code, path) => {
// 解析demo.vue和demo.md
if (path.endsWith('.demo.md') || path.endsWith('.demo.vue')) {
const type = path.endsWith('.vue') ? 'vue' : 'md'
return demoLoader(code, path, type)
} else if (path.endsWith('.md')) {
// 解析 入口的md文件
return docLoader(code, path)
}
}
export default targetTransformedVueSrc
demoLoader 演示文件的解析
我们首先理清楚demoLoader
最终应该产出什么?看我刚刚截图的内容,蓝色的doc文件
中包含好多个红色的demo文件
,yes~,我们将demo文件
产出为一个vue的组件
。然后会在docLoader
中实现引入。
好了,知道产出是VueComponent
,那应该怎么去实现呢?
首先我们先看一下Naive UI
中Button
组件下的icon.demo.vue
文件的内容
<markdown>
# 图标v
在按钮上使用图标,可以使用 `render-icon` 属性或 `icon` 插槽。
</markdown>
<template>
<n-space>
<n-button secondary strong :render-icon="renderIcon">
+100 元
</n-button>
<n-button icon-placement="right" secondary strong>
<template #icon>
<n-icon>
<cash-icon />
</n-icon>
</template>
+100 元
</n-button>
</n-space>
</template>
<script lang="ts">
import { CashOutline as CashIcon } from '@vicons/ionicons5'
import { defineComponent, h } from 'vue'
export default defineComponent({
components: {
CashIcon
},
setup () {
return {
renderIcon () {
return h(CashIcon)
}
}
}
})
</script>
文件内容除了正常的vue
的语法模板外,还包括了一个<markdown></markdown>
标签,所以我们需要将文件内容处理成一个vue文件
,并且还要处理成一个个的卡片,附上codesandbox
、github
所需要的解析文本,以确保可以直接跳转到github
和codesandbox
网站去进行修改或者演示,并且可以一键复制代码、展开折叠等功能,不过这些所有的功能,都已经实现,可以直接查看这两个文件./demo/utils/ComponentDemos.tsx
和./demo/utils/ComponentDemo.vue
。
而 ./demo/utils/ComponentDemo.vue
的props
就是我们需要解析的数据,最终产物应该是这样:
<template>
<component-demo
demo-file-name="<!--DEMO_FILE_NAME-->"
relative-url="<!--URL-->"
title="<!--TITLE_SLOT-->"
ts-code="<!--TS_CODE_SLOT-->"
js-code="<!--JS_CODE_SLOT-->"
language-type="<!--LANGUAGE_TYPE_SLOT-->"
>
<template #title>
<!--TITLE_SLOT-->
</template>
<template #content>
<!--CONTENT_SLOT-->
</template>
<template #demo>
<div class="demo-card__view">
<!--DEMO_SLOT-->
</div>
</template>
</component-demo>
</template>
<!--SCRIPT_SLOT-->
<!--STYLE_SLOT-->
我们可以将它提前定义好,为了阅读方便,我们将它定义成一个文件./plugins/utils/ComponentDemoTemplate.vue
,然后在需要的时候读取。
话不多说,咱们...继续肝着~
// ./plugins/utils/demo-loader.js
import { vueToDemo, mdToDemo } from './demo-render'
import projectPath from './project-path' // 获取项目根目录
// demo文件解析
const demoLoader = (code, id, type) => {
// 获取相对路径 用于生成codesandbox、github链接等 输出为:src/button/demo/xxx.demo.vue
const relativeUrl = id.replace(projectPath + '/', '')
if (type === 'vue') {
return vueToDemo(code, {
relativeUrl,
resourcePath: id,
isVue: true
})
}
return mdToDemo(code, {
relativeUrl,
resourcePath: id,
isVue: false
})
}
export default demoLoader
我们创建一个demo-render.js
,用来处理细节:
- 我们需要先把文件生成一个数据明确的对象,满足
ComponentDemo
组件的props
- 然后生成
jsCode
和tsCode
,供codesandbox
和复制、展示代码
- 获取文件名
- 生成组件代码文本
代码文本层级处理
// ./plugins/utils/demo-render.js
import { marked } from 'marked'
import createRender from './md-render'
const mdRenderer = createRender()
// 解析demo文件的文本,生成ComponentDemo组件的props对象
function getPartsOfDemo (text) {
// 获取template、script、style、markdown等数据
const template = text.match(/<template>([\s\S]*?)<\/template>/)?.[1]
const script = text.match(/<script.*?>([\s\S]*?)<\/script>/)?.[1]
const style = text.match(/<style>([\s\S]*?)<\/style>/)?.[1]
const markdownText = text.match(/<markdown>([\s\S]*?)<\/markdown>/)?.[1]
// 获取markdown文本中的h1标题作为demo标题 将其他内容作为content进行解析
const tokens = marked.lexer(markdownText)
const contentTokens = []
let title = ''
for (const token of tokens) {
if (token.type === 'heading' && token.depth === 1) {
title = token.text
} else {
contentTokens.push(token)
}
}
const languageType = text.includes('lang="ts"') ? 'ts' : 'js'
return {
template,
script,
style,
title,
content: marked.parser(contentTokens, {
renderer: mdRenderer
}),
language: languageType
}
}
生成jsCode和tsCode
我们在上一步的结果中,插入jsCode
和tsCode
:
const mergeParts = ({ parts, isVue }) => {
const mergedParts = { ...parts }
mergedParts.tsCode = ''
mergedParts.jsCode = ''
// 因为需要手动加上template script style等标签
// 还需要将ts->js,单独把函数提取出来
handleMergeCode({ parts, mergedParts, isVue })
// encodeURIComponent 函数可把字符串作为 URI 组件进行编码。
mergedParts.tsCode = encodeURIComponent(mergedParts.tsCode.trim())
mergedParts.jsCode = encodeURIComponent(mergedParts.jsCode.trim())
return mergedParts
}
// ./plugins/utils/ts-to-js.js
const { transformSync } = require('esbuild')
const tsToJs = (content) => {
if (!content) {
return ''
}
// 注意这里,因为esbuild会把空行给去掉,所以这里先把空行替换成__blankline,然后再替换回来
const beforeTransformContent = content.replace(
/\n(\s)*\n/g,
'\n__blankline\n'
)
const { code } = transformSync(beforeTransformContent, {
loader: 'ts',
minify: false,
minifyWhitespace: false,
charset: 'utf8'
})
return code.trim().replace(/__blankline;/g, '')
}
export default tsToJs
// ./plugins/utils/handle-merge-code.js
import tsToJs from './ts-to-js'
// 将解析后的demo文件,处理成标准的vue语法,手动拼接
// 这个文件只是单纯的手动加上标签,无需额外关注,直接复制就好
function handleMergeCode ({ parts, mergedParts, isVue }) {
// 如果是vue文件并且是ts语法
if (isVue && parts.language === 'ts') {
if (parts.template) {
mergedParts.tsCode += `<template>${parts.template}</template>\n\n`
mergedParts.jsCode += `<template>${parts.template}</template>\n\n`
}
if (parts.script) {
mergedParts.tsCode += `<script lang="ts">
${parts.script}
</script>\n\n`
mergedParts.jsCode += `<script>
${tsToJs(parts.script)}
</script>\n\n`
}
if (parts.style) {
const style = `<style scoped>${parts.style}</style>`
mergedParts.tsCode += style
mergedParts.jsCode += style
}
} else {
// 如果是vue文件并且是js语法
if (parts.template) {
mergedParts.jsCode += isVue
? `<template>${parts.template}</template>`
: `<template>\n${parts.template
.split('\n')
.map((line) => (line.length ? ' ' + line : line))
.join('\n')}\n</template>\n\n`
}
if (parts.script) {
mergedParts.jsCode += `<script>
${parts.script}
</script>\n\n`
}
if (parts.style) {
const style = isVue
? `<style scoped>${parts.style}</style>`
: `<style scoped>
${parts.style}
</style>`
mergedParts.jsCode += style
}
}
}
export default handleMergeCode
获取组件名称
直接贴代码:
// 获取文件名
function getFileName (resourcePath) {
const dirs = resourcePath.split('/')
const fileNameWithExtension = dirs[dirs.length - 1]
return [fileNameWithExtension.split('.')[0], fileNameWithExtension]
}
组装成vue文件文本
此时我们创建的ComponentDemoTemplate.vue
文件就派上用场了:
import fs from 'fs'
const __HTTP__ = process.env.NODE_ENV !== 'production' ? 'http' : 'https'
const demoBlock = fs
.readFileSync(path.resolve(__dirname, 'ComponentDemoTemplate.vue'))
.toString()
// 单纯的将parts中的内容和模板占位符进行替换,不用重点关注
const genVueComponent = (parts, fileName, relativeUrl) => {
const demoFileNameReg = /<!--DEMO_FILE_NAME-->/g
const relativeUrlReg = /<!--URL-->/g
const titleReg = /<!--TITLE_SLOT-->/g
const contentReg = /<!--CONTENT_SLOT-->/
const tsCodeReg = /<!--TS_CODE_SLOT-->/
const jsCodeReg = /<!--JS_CODE_SLOT-->/
const scriptReg = /<!--SCRIPT_SLOT-->/
const styleReg = /<!--STYLE_SLOT-->/
const demoReg = /<!--DEMO_SLOT-->/
const languageTypeReg = /<!--LANGUAGE_TYPE_SLOT-->/
let src = demoBlock
src = src.replace(demoFileNameReg, fileName)
src = src.replace(relativeUrlReg, relativeUrl)
if (parts.content) {
src = src.replace(contentReg, parts.content)
}
if (parts.title) {
src = src.replace(titleReg, parts.title)
}
if (parts.tsCode) {
src = src.replace(tsCodeReg, parts.tsCode)
}
if (parts.jsCode) {
src = src.replace(jsCodeReg, parts.jsCode)
}
if (parts.script) {
const startScriptTag =
parts.language === 'ts' ? '<script lang="ts">\n' : '<script>\n'
src = src.replace(scriptReg, startScriptTag + parts.script + '\n</script>')
}
if (parts.language) {
src = src.replace(languageTypeReg, parts.language)
}
if (parts.style) {
const style = genStyle(parts.style)
if (style !== null) {
src = src.replace(styleReg, style)
}
}
if (parts.template) {
src = src.replace(demoReg, parts.template)
}
if (/__HTTP__/.test(src)) {
src = src.replace(/__HTTP__/g, __HTTP__)
}
return src.trim()
}
合体
我来组成template~,我来组成script.... 我们是.....啥也不是
好了,我们现在需要做的就是将这些函数整合起来,代码如下:
export const vueToDemo = (
code,
{ resourcePath, relativeUrl, isVue = true }
) => {
const parts = getPartsOfDemo(code) // 将文本处理成组件props结构
const mergedParts = mergeParts({ parts, isVue }) // 处理js/tsCode
const [fileName] = getFileName(resourcePath)
return genVueComponent(mergedParts, fileName, relativeUrl)
}
实际上还有一个mdToVue
的函数,原理基本一致,这里就不做过多废话了~
docLoader 文档解析
目标产物
我们需要设定好目标,才不会迷茫,不是吗?
我们先来看下Naive UI
的页面,如下图
它有以下几个部分:
content
主内容区- 大标题 并支持去
github
编辑 - 描述
- 演示组件
- API相关
- 大标题 并支持去
anchor
右侧锚点
所以我们的文档解析需要做的工作还是蛮多的,包括:生成引入组件的script命令
、生成anchor列表
、将demo组件加载到内容去,并根据配置决定一行显示几个
。
嗯?好像。。。工作也不是很多😳
然后我们计划好如何去实现:
- 解析文档获取是否有
API
、每栏组件数
、是否开启锚点
等数据 - 获取
demo
中的信息,根据展示个数来处理demo
组件 - 获取大标题信息,添加按钮,提供去
github编辑
的功能 - 生成锚点数据和标签
- 生成主体
template
- 生成最终的
script
- 合体
我们先将其他的东西拆分来讲,最后再分析主函数
处理demo组件
通过marked.lexer
,我们可以获取到 ```demo ``` 中的数据,然后得到组件名、id、标题、标签
等信息,具体代码如下(其中的resolveDemoTitle
和cameLCase
不用关注,意如其名):
// 处理每一个demo.vue文件 将其转换成组件 比如 basic.demo.vue 转换成 <BasicDemo />
const resolveDemoInfos = async (demoInfos, relativeDir, env) => {
// demoInfos 就是```demo ```中的文本,我们处理成数组
const demoStr = demoInfos
.split('\n')
.map((item) => item.trim())
.filter((id) => id.length)
const demos = []
// 处理md文件中的demo文件名 保持和真实文件名一致
for (const demo of demoStr) {
const debug = demo.includes('debug') || demo.includes('Debug')
// 生产环境就不显示debug的demo
if (env === 'production' && debug) continue
let fileName
if (demo.includes('.vue')) {
fileName = demo.slice(0, -4) + '.demo.vue'
} else {
fileName = demo + '.demo.vue'
}
const componentName = `${cameLCase(demo)}Demo`
demos.push({
id: demo.replace(/\.demo|\.vue/g, ''),
variable: componentName,
fileName,
title: await resolveDemoTitle(fileName, relativeDir),
tag: `<${componentName} />`,
debug
})
}
return demos
}
最终我们产出的是一个数组结构,来供我们主函数中使用,在主函数中,在渲染template
和script
的内容时,需要用到它来生成标签和import xxx from xxx
的命令。
生成template
的函数也很简单:
const genDemosTemplate = (demosInfos, colSpan) => {
return `<component-demos :span="${colSpan}">${demosInfos
.map(({ tag }) => tag)
.join('\n')}</component-demos>`
}
锚点数据处理
这里也区分了是组件的演示文档还是项目的说明文档,这里只用演示文档来举例子,我们在上面将组件的列表获取到了,并且拿到了id
信息,我们要做的就生成n-anchor-link
标签就可以了。
// 生成锚点的父级组件 将子组件包裹起来
function genAnchorTemplate (
children,
options = {
ignoreGap: false
}
) {
return `
<n-anchor
internal-scrollable
:bound="16"
type="block"
style="width: 192px; position: sticky; top: 32px; max-height: calc(100vh - 32px - 64px); height: auto;"
offset-target="#doc-layout"
:ignore-gap="${options.ignoreGap}"
>
${children}
</n-anchor>
`
}
// 生成文档页的锚点,快速定位到对应的位置 详情看naive的anchor文档: https://www.naiveui.com/zh-CN/os-theme/components/anchor
const genDemosAnchorTemplate = (demoInfos, hasApi, mdLayer) => {
// 如果有api,就将api的锚点放在最后 否则就放在第一个
const links = // 将demos的锚点和md文档中的h3标签的锚点合并
(
hasApi ? demoInfos.concat(genDemosApiAnchorTemplate(mdLayer)) : demoInfos
).map(
({ id, title }) => `<n-anchor-link
title="${title}"
href="#${id}"
/>`
)
// 注意 原文中有个v-if的判断displayMode,这里删除不作处理
// 将锚点放在一个n-anchor组件中
return genAnchorTemplate(links.join('\n'), {
ignoreGap: hasApi
})
}
script
代码生成
这一块的代码,一直不知道该怎么去分析。最终想了下,如果给我从零去设计,我可能是先用vue3+naive
去搭建一个中规中矩的站点,然后将其中的script
和template
中不变的地方抽取出来。实际上这一部分没什么可讲的,就是一堆定义好的变量、样式,我们只需要把引入组件的命令动态的生成,根据md
中是否开启锚点
配置,控制变量即可,代码如下:
/**
* 生成vue3的script部分
* @param {Array} demosInfos demo.vue的集合
* @param {*} components 转换后的组件
* @param {*} relativeDir 所在的相对路径
* @param {*} forceShowAnchor 是否显示锚点
*/
function genDocScript (demoInfos, components = [], url, forceShowAnchor) {
const showAnchor = !!(demoInfos.length || forceShowAnchor)
const importStmts = demoInfos
.map(({ variable, fileName }) => `import ${variable} from './${fileName}'`)
.concat(components.map(({ importStmt }) => importStmt))
.join('\n')
const componentStmts = demoInfos
.map(({ variable }) => variable)
.concat(components.map(({ ids }) => ids).flat())
.join(',\n')
const script = `<script>
${importStmts}
import { computed } from 'vue'
import { useMemo } from 'vooks'
import { useIsMobile } from '/demo/utils/composables'
export default {
components: {
${componentStmts}
},
setup () {
const isMobileRef = useIsMobile()
const showAnchorRef = useMemo(() => {
if (isMobileRef.value) return false
return ${showAnchor}
})
const useSmallPaddingRef = isMobileRef
return {
showAnchor: showAnchorRef,
wrapperStyle: computed(() => {
return !useSmallPaddingRef.value
? 'display: flex; flex-wrap: nowrap; padding: 32px 24px 56px 56px;'
: 'padding: 16px 16px 24px 16px;'
}),
contentStyle: computed(() => {
return showAnchorRef.value
? 'width: calc(100% - 228px); margin-right: 36px;'
: 'width: 100%; padding-right: 12px;';
}),
url: ${JSON.stringify(url)}
}
}
}
</script>`
return script
}
生成template
基本一致,就不做额外介绍了。大家可以在我代码仓库tag - 4.0
中查看。
文档解析,获取关键信息
文件解析,肯定还是marked.lexer
,根据Naive UI
的约定,我们对是否有<!--single-column-->
、<!--anchor:on-->
和## API
来当作参数,
const docLoader = async (code, relativeDir, env = 'development') => {
const colSpan = ~code.search('<!--single-column-->') ? 1 : 2
const forceShowAnchor = !!~code.search('<!--anchor:on-->')
const hasApi = !!~code.search('## API')
const mdLayer = marked.lexer(code)
// 获取组件的代码 比如md文件中引入了一些第三方的组件 就使用这里的代码
const componentsIndex = mdLayer.findIndex(
(item) => item.type === 'code' && item.lang === 'component'
)
let components = []
if (~componentsIndex) {
// mdLayer[componentsIndex].text : NButton: import { Button } from 'naive-ui'
components = mdLayer[componentsIndex].text
.split('\n')
.map((component) => {
const [compName, importCode] = component.split(':')
if (!compName.trim()) throw new Error('没有组件名')
if (!importCode.trim()) throw new Error('没有组件资源地址')
return {
compName: compName.split(',').map((item) => item.trim()),
importCode
}
})
.filter(({ compName, importCode }) => compName && importCode) // 过滤掉空的
}
// 处理标题 并添加在github中编辑的功能
const titleIndex = mdLayer.findIndex(
(item) => item.type === 'heading' && item.depth === 1
)
if (~titleIndex) {
const title = mdLayer[titleIndex].text
const btnTemplate =
`<edit-on-github-header :relative-url="url" text=${title}></edit-on-github-header>`
mdLayer.splice(titleIndex, 1, {
type: 'html',
pre: false,
text: btnTemplate
})
}
// 处理demo 并移除在生产中的打包构建
const demoIndex = mdLayer.findIndex(
(item) => item.type === 'code' && item.lang === 'demo'
)
let demoInfos = []
if (~demoIndex) {
demoInfos = await resolveDemoInfos(
mdLayer[demoIndex].text,
relativeDir,
env,
)
mdLayer.splice(demoIndex, 1, {
type: 'html',
pre: false,
text: genDemosTemplate(demoInfos, colSpan)
})
}
const docMainTemplate = marked.parser(mdLayer, {
renderer: mdRenderer,
gfm: true
})
// 生成文档的模板
const docTemplate = `
<template>
<div
class="doc"
:style="wrapperStyle"
>
<div :style="contentStyle">
${docMainTemplate}
</div>
<div style="width: 192px;" v-if="showAnchor">
${
demoInfos.length
? genDemosAnchorTemplate(demoInfos, hasApi, mdLayer)
: genPageAnchorTemplate(mdLayer)
}
</div>
</div>
</template>`
const docScript = genDocScript(
demoInfos,
components,
relativeDir,
forceShowAnchor,
)
return `${docTemplate}\n\n${docScript}`
}
最终,我们就完成了整体的文档解析,当然了,这个时候还有很多的问题,导致无法启动项目,比如:
- 站点项目中引入自己开发的组件问题,解决方式: dev: 配置别名引入,prod: 生成本地pack移动到
node_modules
中,注意名称保持一致。 - 组件的到处和
install
函数暴露。 等等等等的问题,不过这些都已经处理好,并上传到了仓库,大家可以在最新版本查看。
附上一个最终的效果图:
总结
看过一遍文章以后,至少已经模糊的知道了怎么去实现。简单点,其实就是在vite plugin
中,获取文件文本
和文件路径
,然后对内容进行一个约定、解析、拆分,最终组合成一个vue文本
,其实看这类的源码,最复杂的地方是在于,代码中有很多很多的非重点的内容,比如i18n
、theme
、store
等等,这些实际上并不需要跟着他们的标准来。所以我做的就是排除了很多不重要的东西,进行重点的讲解。另外,学会了,也需要进一步的加深巩固和拓展。你说,我拓展的就比原来的好吗?并不是,重要的是,我进行了进一步的改造,表示我掌握了这一块的知识。有兴趣的也可以看看这个仓库。一定一定要尝试打开自己的思维,无论思路对或错,好或坏,只要尝试,总会有更多收获。