{"id":28710607,"url":"https://github.com/bowencool/create-vitepress-demo","last_synced_at":"2025-06-14T21:07:14.093Z","repository":{"id":43269630,"uuid":"442076778","full_name":"bowencool/create-vitepress-demo","owner":"bowencool","description":"基于 vitepress 扩展更专业的 Demo 演示能力的文档方案","archived":false,"fork":false,"pushed_at":"2024-01-26T09:02:21.000Z","size":247,"stargazers_count":63,"open_issues_count":3,"forks_count":13,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-05-29T11:25:51.104Z","etag":null,"topics":["boilerplate","demo","documentation","documentation-site","dumi","iframe","npm","static-boilerplate","vitepress","vue","vue3","vuejs","website-boilerplate","website-template"],"latest_commit_sha":null,"homepage":"https://bowencool.github.io/create-vitepress-demo/guide/contribution.html","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/bowencool.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-12-27T06:53:50.000Z","updated_at":"2025-05-05T16:44:25.000Z","dependencies_parsed_at":"2024-11-19T22:35:37.736Z","dependency_job_id":null,"html_url":"https://github.com/bowencool/create-vitepress-demo","commit_stats":null,"previous_names":[],"tags_count":19,"template":false,"template_full_name":null,"purl":"pkg:github/bowencool/create-vitepress-demo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bowencool%2Fcreate-vitepress-demo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bowencool%2Fcreate-vitepress-demo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bowencool%2Fcreate-vitepress-demo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bowencool%2Fcreate-vitepress-demo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bowencool","download_url":"https://codeload.github.com/bowencool/create-vitepress-demo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bowencool%2Fcreate-vitepress-demo/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":259884459,"owners_count":22926445,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["boilerplate","demo","documentation","documentation-site","dumi","iframe","npm","static-boilerplate","vitepress","vue","vue3","vuejs","website-boilerplate","website-template"],"created_at":"2025-06-14T21:07:12.308Z","updated_at":"2025-06-14T21:07:14.080Z","avatar_url":"https://github.com/bowencool.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# create-vitepress-demo\n\n搭建一个带有专业 demo 演示能力的 vitepress 项目，查看[相关介绍](https://blog.bowen.cool/zh/posts/add-more-professional-demo-presentation-capabilities-to-vitepress)、[示例站点](https://bowencool.github.io/create-vitepress-demo/guide/contribution.html)\n\n```bash\nnpm init vitepress-demo\n# or\nyarn create vitepress-demo\n```\n\n# 背景\n\nvitepress 凭借着 vite 的秒级启动速度、markdown-it 的强大扩展能力、天然支持 vue3 在文档圈迅速流行开来，使用 vitepress 做 vue3 组件库文档也已经非常流行。笔者也有幸实践过一次，在这里记录一下。\n\n首先 [vitepress 的 markdown 扩展能力](https://vitepress.vuejs.org/guide/markdown.html) 无疑是极香的，我觉得及其舒适的有以下几点：\n\n- [Import Code Snippets](https://vitepress.vuejs.org/guide/markdown.html#import-code-snippets)\n- [运行 vue 组件](https://vitepress.vuejs.org/guide/using-vue.html)\n\n笔者使用 vitepress 搭建业务组件库的文档，依赖 element-plus，根据 vitepress 文档，写了一个简单的 DemoContainer 组件用于包裹 Demo。\n\n## vitepress 缺点\n\n随着时间的推移和组件数量的累积，现有的开发方式逐渐暴露出来一些问题：\n\n1. 无法演示全屏组件(height:100vh)\n2. 无法演示路由组件（耦合 vue-router，如 menu-item）\n3. vitepress 有一些全局样式挺烦的，经常干扰到 Demo，比如：\n\n```css\ntable {\n  display: block;\n  border-collapse: collapse;\n  margin: 1rem 0;\n  overflow-x: auto;\n}\n```\n\n4. 引用 demo 太繁琐，而且易出错（引用一个 Demo 要 15 行代码）\n\n```markdown\n\u003cscript setup\u003e\n  // 这个 demo1 重复了多次，复制修改的时候容易漏掉\n  import Demo1 from './demo/demo1.tsx'\n\u003c/script\u003e\n\n\u003cDemoContainer title=\"基本使用\"\u003e\n\u003cClientOnly\u003e\n\u003cDemo1 /\u003e\n\u003c/ClientOnly\u003e\n  \u003cdetails\u003e\n    \u003csummary\u003e查看代码\u003c/summary\u003e\n\n\u003c!-- 这个源码引用方式是 vitepress 提供的 --\u003e\n\n\u003c\u003c\u003c packages/query-table/demo/demo1.tsx\n\n  \u003c/details\u003e\n\u003c/DemoContainer\u003e\n```\n\n前两点很容易想到用 iframe 是完美的解决方案，而且还能顺手解决第三点。\n\n总结一下缺点有两个：\n\n1. Demo 引用繁琐\n2. 缺少 iframe 模式\n\n# 前置介绍\n\n### 涉及到的框架之间的关系\n\nvitepress 本质上是一个 vite 插件，使用它开发的文档网站效果相当于 vue3 + vite 的 ssr 项目，它在内部帮你把所有逻辑都封装好了，你只需要写 markdown 就行。\n\n对 markdown 的扩展能力是基于 markdown-it 写了很多 markdown-it 插件。源码里所写的 markdown 文档最终都会转成 vue 组件，原理如下：\n\n### vitepress 运行 vue 组件原理\n\n把 markdown 编译成 html 字符串，把 html 字符串拼凑成一个 vue 字符串，交给 vue-loader，处理成一个 vue 组件挂载到页面上。\n\n# 调研\n\n- dumi 效果完美，可以说是标杆了。但是不支持 vue\n- storybook 并不是想要的 iframe 模式，也不行。\n- vitepress-for-component 是 fork 了 vitepress（因为 vitepress 目前未支持插件），提供了 demo 演示能力，但是没有 iframe 模式。\n- element-plus 也是用 vitepress , 但是也没有 iframe 模式。而且它的引用方式不清晰、不灵活。\n- 自研，舍不得 vitepress 的 markdown 扩展能力。不到走投无路不要自研。\n\n最终决定尝试通过修改配置和自定义插件解决。\n\n# 研发\n\n## demo 引入简化\n\n参考了 [element-plus](https://github.com/element-plus/element-plus) 和 [vitepress-for-component](https://github.com/dewfall123/vitepress-for-component) ，定制一个 markdown-it 插件修改 html 编译结果。\n\n### 引入方式设计\n\nelement-plus 的引入方式不够清晰，也不够灵活。采用相对路径更清晰更灵活：\n\n```markdown\n\u003cdemo src=\"./demo-example.vue\" title=\"Demo演示\" desc=\"这是一段描述\" /\u003e\n```\n\n当然 container 的方式也顺便兼容下，里面的内容可以写写 markdown：\n\n```markdown\n::: demo src=\"./demo-example.vue\" title=\"Demo 演示\"\n\n这是一段描述，可以用 `Markdown` 来写\n\n:::\n```\n\n### 插件思路\n\n遇到特定标记（如：`\u003cdemo src=xxx ... /\u003e`)，根据标记拼接字符串，将来会被插入到 vue template 里相应位置，通常情况下拼接 `\u003cDemoContainer ... \u003e\u003cDemo/\u003e\u003c/DemoContainer\u003e`，如果标记了以 iframe 模式运行 demo，则拼接一个`\u003ciframe src=xxx ... /\u003e`\n\n此过程还会包括如下步骤，感兴趣可以看[源码](https://github.com/bowencool/create-vitepress-demo)：\n\n- 插入 import statement 语句\n- 记录 demoId 和入口的对应关系\n\n这一步把引入 Demo 的过程从原来的 15 行代码之间简化到 1 行。\n\n## iframe 模式\n\n### 运行时动态创建 iframe\n\n试过在 DemoContainer 里 document.createElement('iframe')，但是没有成功：\n\n- 获取到 slot 内容的时候，组件代码已经运行了，此时放入沙箱已经晚了。\n- 获取到 demo 源代码交给 vue-compiler 编译这个**编译工作**在**运行时**做不现实。\n\n### 微前端\n\n这明显更复杂了，而且和动态 iframe 具有相同的问题。\n\n### 在 vite 配置里直接加入口\n\n第一个念头是 vite.config 里添加入口，因为 vite 就是支持多个 html 的。\n\n实际操作之后发现[根本行不通](https://github.com/vuejs/vitepress/issues/57#issuecomment-973873527)：vitepress 接管路由了，访问任何路径都会经过 vitepress router 处理。即使设置 base，也会收到 vitepress 的提醒。\n\n![image](https://user-images.githubusercontent.com/20217146/142593193-73c301b8-e1b4-4cb6-83a5-e6aba9ec3967.png)\n\n查看源码得知，devServer 拦截了所有 html 请求，根据请求路径动态生成 html：\n\n\u003cdetails\u003e\n  \u003csummary\u003e查看代码 vitepress/src/node/plugin.ts\u003c/summary\u003e\n\n```ts\nconst vitePressPlugin: Plugin = {\n  name: \"vitepress\",\n  // ...\n  configureServer(server) {\n    if (configPath) {\n      server.watcher.add(configPath);\n    }\n\n    // serve our index.html after vite history fallback\n    return () =\u003e {\n      server.middlewares.use((req, res, next) =\u003e {\n        if (req.url!.endsWith(\".html\")) {\n          res.statusCode = 200;\n          res.end(`\n\u003c!DOCTYPE html\u003e\n\u003chtml\u003e\n  \u003chead\u003e\n    \u003ctitle\u003e\u003c/title\u003e\n    \u003cmeta charset=\"utf-8\"\u003e\n    \u003cmeta name=\"viewport\" content=\"width=device-width,initial-scale=1\"\u003e\n    \u003cmeta name=\"description\" content=\"\"\u003e\n  \u003c/head\u003e\n  \u003cbody\u003e\n    \u003cdiv id=\"app\"\u003e\u003c/div\u003e\n    \u003cscript type=\"module\" src=\"/@fs/${APP_PATH}/index.js\"\u003e\u003c/script\u003e\n  \u003c/body\u003e\n\u003c/html\u003e`);\n          return;\n        }\n        next();\n      });\n    };\n  },\n};\n```\n\n\u003c/details\u003e\n\n### 定制 devServer 中间件\n\n上面 vitepress 的操作给了我灵感，我也写个 devServer 中间件，根据请求路径动态生成 html。试了一下，还真成功了，流程如下：\n\n1. 上面提到定制的 markdown-it 修改 html 输出为 `\u003ciframe src=xxx /\u003e`\n2. 浏览器会向 devServer 请求 iframe 地址\n3. devServer 中间件拿到这个请求，如果命中约定格式，比如 `/^\\/~demos\\/(?\u003cdemoId\u003e\\w+)\\.html$`，则拼接一个可以运行此 demo 的 html 字符串给浏览器。\n   1. 找到 demoId 对应的入口文件 demoEntry\n   2. 写一些运行时 script 作为入口\n      1. 通过入口地址从 vite 请求编译结果：`const module = await import('@fs/${demoEntry}')`\n      2. 约定 module.default 导出自动挂载的组件。否则视为 demoEntry 自行挂载\n\n![流程图](https://user-images.githubusercontent.com/20217146/147480565-2e3b7b64-e5a0-4cb4-ab61-a0a7700ce595.png)\n\n\u003c!--\n```sequence\nBrowser-\u003eViteDevServer: request: http://.../xxx.html\nViteDevServer-\u003eMarkdownIt: read: /xxx.md\nNote over MarkdownIt: markdown-it-demo\nMarkdownIt-\u003edemos.json: write: { Demo123: { entry: '/.../demo.vue' }, ... }\nMarkdownIt-\u003eViteDevServer: return: \\n\u003c!DOCTYPE html\u003e\\n...\u003ciframe src=\"/~demos/Demo123.html\" /\u003e...\nViteDevServer-\u003eBrowser: response: \\n\u003c!DOCTYPE html\u003e\\n...\u003ciframe src=\"/~demos/Demo123.html\" /\u003e...\nBrowser-\u003eViteDevServer: request: http://.../~demos/Demo123.html\nNote over ViteDevServer: vite-plugin-demo-iframe\\nmatched\\n /^\\/~demos\\/(\\w+)\\.html/\nViteDevServer-\u003edemos.json: read: find Demo123.html's entry\ndemos.json-\u003eViteDevServer: return: Demo123.html's entry is '/.../demo.vue'\nNote over ViteDevServer: genHtml({ entry: '/.../demo.vue' }):\\n\u003c!DOCTYPE html\u003e\\n...demo...\nViteDevServer-\u003eBrowser: response: \\n\u003c!DOCTYPE html\u003e\\n...demo...\n```\n--\u003e\n\n### 构建模式\n\n由于构建模式没有 devServer，所以上述 devServer 也不会生效。\n\nvitepress 和处理请求一样一刀切，没有留余地，无法通过 vite 添加入口。\n\n所以只能在 `vitepress build` 之后再跑一遍 `vite build -c=xxx`\n\n![流程图](https://user-images.githubusercontent.com/20217146/147480688-9bc9bbf0-d08e-47d5-a511-be401c04bfaa.png)\n\n\u003c!-- ```sequence\nvitepress build-\u003eMarkdownIt: read: /xxx.md\nNote over MarkdownIt: markdown-it-demo\nMarkdownIt-\u003edemos.json: write: { Demo123: { entry: '/.../demo.vue' }, ... }\nMarkdownIt-\u003evitepress build: return: \\n\u003c!DOCTYPE html\u003e\\n...\u003ciframe src=\"/~demos/Demo123.html\" /\u003e...\nNote over vitepress build: write dist/\nvite build-\u003edemos.json: read: all demos\nNote over vite build: add all demo entry\nNote over vite build: write dist/~demos/\n``` --\u003e\n\n\n# 总结\n\n由于这套刚刚出炉，所以有很多的优化点，发出来权当抛砖引玉了。\n\n因为 vitepress 暂时没有插件机制，所以这套方案也没什么抽象的点子，暂时作为一个样板仓库。\n\n如果你有好的点子或者优化的地方，请联系我。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbowencool%2Fcreate-vitepress-demo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbowencool%2Fcreate-vitepress-demo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbowencool%2Fcreate-vitepress-demo/lists"}