如何建设一个开源图形引擎的文档网站
凌云时刻
编者按:如果你看过 Vue.js 的纪录片,就会发现一个开源产品的成功不仅仅是优质的代码,而且还需要:清晰的文档、不土的审美、持续的迭代、定期的布道、大佬的站台……今天来聊聊“文档”这件看似简单却需要精心雕琢的小事。
图片来源于网络
众所周知,写完代码和单测只是保证了功能的实现,而面向用户最重要的东西就是文档。文档在我们生活中是最司空见惯的东西,比如你买了一台吸尘器,一定会有一本说明书告诉你怎么操作,而不是跟你说:“想知道怎么操作吗?把吸尘器拆开自己看看工作原理吧。”然而,在软件领域,让用户看源码这种荒唐的事天天在发生。懂得文档重要性的程序员才是好的推销员。
言归正传,当开始运营团队里的开源图形引擎 Oasis Engine 的时候,我才发现做出一个简洁好用的文档比想象得难很多。
Oasis Engine:https://oasisengine.cn/
选型
在开源之前,我们把文档放在语雀上。语雀作为知识管理平台确实很不错,如果维护得当,积硅步可以至千里。但它也有两方面的缺点:
1. 无法承载 API 文档、代码示例等复杂形态的需求;
2. 无法满足个性化的设计。总而言之,一个开源产品如果连个独立的网站都没有,有点说不过去。
通过了解一些知名的开源作品的网站后,我发现他们大部分都选择把网站部署在 Github Pages 上。这很符合我们团队的想法,既然引擎都开源了,文档当然也应该开源,让全世界的开发者一起来维护。经过调研,我找到了一些能够把搭建 GIthub Pages 的静态网站框架:
Jekyll:Github Pages 官方推荐的老牌框架,但是它依赖 Ruby,我觉得在 Mac OSX 下使用 Ruby 很麻烦,遂放弃。
Vuepress:我的同事 ZS 开发的基于 Vue 的框架,虽然我对 Vue 很推崇并且敬佩 ZS 的实力,但是它的数据源限于 Markdown。
Dumi:也是我厂另一位大神开发的基于 React 的框架,它为组件开发场景而生,其中的 dumi-theme-mobile 挺打动我的,但它的 Demo 预览能力和组件是耦合的,并不能满足图形引擎中非组件 Demo 的预览;另外它的默认样式有点粗糙。
Docsify:一个非常小清晰的网站生成器(它的 logo 太可爱了),之所以没有提到 Gitbook,是因为 Docsify 不仅拥有 Gitbook 的功能,而且可以直接在运行时解析 Markdown 文件,不需要编译环节,使用起来非常简单。
开源在即,为了快速上线网站,我最终选择了 Docsify。事实证明,这是个仓促的选择。当时用了三套工程方案把三种完全不同的数据都编译成 HTML 后组合在一起:Docsify 解决了把文档编译成 HTML 的问题,Typedoc 解决了把 API 编译成 HTML 的问题,Demosify 解决了把示例编译成 HTML 的问题。
除了三个工程强扭在一起之后整个网站风格不统一之外,更要命的是我们还把 API(在引擎仓库)、教程文档、示例放在三个 Github 仓库(美其名曰“洁癖”),每次更新都需要从三个仓库拷贝内容到网站仓库,维护成本非常高。开源之后第一个里程碑迭代完,团队已经被文档折腾得筋疲力尽,完善文档的积极性也降低了。
初心
2021年四月,距离 Oasis 引擎开源已经过去两个月了,热潮消退,褒贬的声音已经淡去。期间有不少人反馈我们部署在 Github Pages 上的站点打开很慢,尤其是示例页面不翻墙根本打不开,我们没有认识到网站工程的臃肿导致了访问慢,还傻傻地以为 Github 就是慢,于是在 Gitee 上又部署了一个国内镜像来缓解这个问题。
看了行业里成功的图形/游戏引擎的网站:Unity、Unreal、Cocos、LayaAir、ThreeJS、BabylonJS,他们根据自己的定位、主打产品、发展阶段、商业策略展现出不同的信息架构和风格。而我们应该做成什么样呢?
回归初心吧,少年!Oasis 引擎想成为前端友好、高性能的移动端图形引擎,那么我们的网站必须给人简洁、可靠、极速的印象。四月快结束的时候,我猛然意识到:既然我们定位是面向前端的图形引擎,为啥不朝着前端框架的模式做呢?我重新梳理一下网站的需求:
一体化:把 API 文档(TypeDoc)、教程文档(Markdown)、示例(Typescript)等不同格式的数据源放到一个站点,并且支持全局内容搜索;
示例嵌入:支持在教程文档中嵌入功能示例,并且支持跳转到 Codepen 等流行在线开发环境中编辑;
多版本:不同引擎版本的文档同时存在,支持版本切换;
国际化:支持中英语言。
梳理完毕,我发现要做的其实是个类似 Ant Design 的站点。这里有个误解,前文中提到 Dumi 的 dumi-theme-mobile,我错误地以为 Ant Design Mobile 的网站是基于 Dumi 实现的(而且 Ant Design Mobile 的作者也推荐我用 Dumi),而 Dumi 已经是调研过后的放弃的方案,又由于 Ant Design Mobile 和 Ant Design 的网站风格相似,我仍以为 Ant Design 也是用 Dumi 做的,直到发现 Ant Design Pro 的网站源码,我才知道是基于 Gatsby 实现的。
开搞
接下去的内容虽然是这篇文章的主题,但可能比较无聊,事实上我完全可以省略上述心路历程,把文章的标题改成《如何用 Gatsby 实现一个文档网站》。然而,我想强调的是当一个人面对一个陌生的领域,势必会走弯路,当回头看的时候,这些弯路都是收获。
发现 Gatsby 的时候,我十分兴奋,以至于五一假期五天时间都在捣鼓这个工具;假期结束的时候,同事们惊讶地发现我已经把网站的功能基本写完了。那么,Gatsby 到底是个什么东西呢?它和上述选型中的其他方案有什么区别呢?
我认为最本质的区别是:Gatsby 有一个叫 GraphQL 的中间数据层。不管你的输入是什么格式,只要能转成 GraphQL 格式的数据,就能在 Gatsby 中通过查询语句获取数据,最后渲染成 React 组件。比如,Oasis 引擎的官网就希望把 TypeDoc、Markdown、Typescript 格式的文件数据转成 React 组件:
相当完美的流程!这意味着数据和样式解耦,原先各种格式都要通过不同的工具编译成 HTML,现在可以通过一个工具转成 React 组件,而 React 组件的样式可以统一管控。
处理 TypeDoc 数据
TypeScript(TS) 是近几年最流行的前端开发语言,出于代码质量和可维护性的考虑,Oasis 引擎也采用了 TypeScript 编写。TypeDoc 是社区中比较优秀的生成 TS API 文档的工具,它能够读取 Typscript 的声明数据并生成 HTML 网页,但似乎很少人知道它其实有 Node module——也就是说只用它的 Node API 读取数据,渲染交给其他工具。
至此,聪明的读者想必已经知道了:找一个 TypeDoc 转 GraphQL 的工具。幸运的是,我在 Gatsby 的社区就找到一个 gatsby-source-typedoc 插件(Gatsby 的插件生态很茂盛),顺藤摸瓜,又找了该插件作者写的文章。有趣的是,文章作者是一个叫 Excalibur.js 的游戏引擎的开发者,所谓同是引擎开发者,相逢何必曾相识,这就是猿粪啊。但是,我高兴得有点早,因为文章提供的信息非常有限。这个插件仅仅是帮你读取 TypeDoc 的数据转成 GraphQL,然后你自己 JSON.parse
数据,再然后 Please do something with that data by yourself...
export const pageQuery = graphql`typedoc(typedocId: { eq: "default" }) {internal {content}}
`export default function MyPage({ data: { typedoc } }) {const typedocContent = JSON.parse(typedoc?.internal.content);// do something with that data...
}
当时的想法是,反正 TypeDoc 的默认样式也不好看,我就重写一个渲染器吧......万万没想到,这一重写就是五一三天假期????。主要原因是 TypeDoc 的数据类型挺复杂的,比如类型就有这么多(可能还没列全,终于能够理解为啥 TypeDoc 官方的渲染器每次升级都有不小的变化):
export enum Kinds {MODULE = 1,ENUM = 4,CLASS = 128,INTERFACE = 256,TYPE_ALIAS = 4194304,FUNCTION = 64,PROPERTY = 1024,CONSTRUCTOR = 512,ACCESSOR = 262144,METHOD = 2048,GET_SIGNATURE = 524288,SET_SIGNATURE = 1048576,PARAMETER = 32768,TYPE_PARAMETER = 131072,
}
这里说一下具体的步骤:
1、从 Oasis 引擎仓库获取数据源,就是入口级别的 index.ts 文件。由于 Oasis Engine 是一个 monorepo 仓库,要获取每个子仓库的 index.ts 的路径,最后写入到一个临时文件 tsfiles.js 里:
const glob = require('glob');
const fs = require('fs');glob(`${EngineRepoPath}/packages/**/src/index.ts`, {realpath: true}, function(er, files) {var re = new RegExp(/([^test]+).ts/);var tsFiles = [];for (let i = 0; i < files.length; i++) {const file = files[i];var res = re.exec(file);console.log('[Typedoc entry file]:', file);if (!res) continue;tsFiles.push(`"${file}"`);}fs.writeFile('./scripts/typedoc/tsfiles.js', `module.exports = [${tsFiles.join(',')}];`, function(err) {});
});
2、在 gatsby-config.js 中配置插件:
const DTS = require('./scripts/typedoc/tsfiles');module.exports = {plugins: [{resolve: "gatsby-source-typedoc",options: {src: DTS,typedoc: {tsconfig: `${typedocSource}/tsconfig.json`}}}]
}
3、打开 http://localhost:8000/___graphql 如果看到左侧面板中有 typedoc 说明数据读取已经成功,勾选一下 internal> content 执行查询,可以到详细的数据:
4、接下去就是使用 gatsby 创建页面,gatsby 提供了 createPages.js 入口编写创建页面的代码,以下就是插件作者在文章中省略的 do something with that data...
部分的代码:
async function createAPI(graphql, actions) {const { createPage } = actions;const apiTemplate = resolve(__dirname, '../src/templates/api.tsx');const typedocquery = await graphql(`{typedoc {internal {content}}}`,);let apis = JSON.parse(typedocquery.data.typedoc.internal.content);// do something with that data...const packages = apis.children.map((p) => {return {id: p.id,kind: p.kind,name: p.name.replace('/src', '')};});if (apis) {apis.children.forEach((node, i) => {const name = node.name.replace('/src', '');// 索引页createPage({path: `${version}/api/${name}/index`,component: apiTemplate,context: { node, type: 'package', packages }});// 详情页if (node.children) {node.children.forEach((child) => {createPage({path: `${version}/api/${name}/${child.name}`,component: apiTemplate,context: { node: child, type: 'module', packages, packageIndex: i }});})}});}
}
最终的结果,可以访问 https://oasisengine.cn/0.3/api/core/index。样式是不是比 TypeDoc 默认的好看一点?可能有人会问:Typdoc 也可以直接转成 Markdown,你为什么大费周折呢?因为一个图形引擎的复杂度相当高,API 有成千上万个,如果用 Markdown 展示是非常难看的,所以 TypeDoc 的存在是有意义的。
在 Markdown 中嵌入 Demo
这是一个很朴素的需求,就是希望能在文档中嵌入 Demo, 方便开发者理解文档中描述的功能,增强文档和示例的关联性。这也是我们做面向前端的引擎必须具备的优势,市面上大部分引擎网站都是文档和示例分离的,更别说一些 Native 引擎想在网页里渲染都很难呢。比如材质文档中讲到 PBRMaterial,总得展示一下 PBR 材质的样子吧。我们是搞图形学的,又不是搞服务端的,只是文字描述多么干涩啊。
可以负责任地告诉大家,我的五一假期剩余两天就是被这个功能消耗掉的????。接下来说一下具体的实现思路。
首先,我想让维护文档的同学轻松一点,在 Markdown 文件中嵌入一个 Demo 应该是一行代码的事情,比如:
<playground src="pbr-helmet.ts"></playground>
多么简单优雅!可是问题来了:怎么从 Markdown 中“提取”出这行代码并最终渲染成想要的样子呢?不要忘了 Markdown 本来就是 gatsby 的一项数据源,gatsby 正是通过 gatsby-transformer-remark 插件解析数据的,而数据的解析从原理上绕不过抽象语法树,看了一下 graphiQL 果然有 AST 数据:
1、第一步,在语法树中找到 <playground>
标签替换成我想要的数据。于是,我就开始了人生的第一个 gatsby 插件 gatsby-remark-oasis 的编写:
// `gatsby-remark-oasis` plugin:
// Extract <playground> from markdown AST and replace the content
const visit = require('unist-util-visit');
const fs = require('fs');
const Prism = require('prismjs');module.exports = ({ markdownAST }, { api, playground, docs }) => {visit(markdownAST, 'html', (node) => {if (node.value.includes('<playground')) {const src = /src="(.+)"/.exec(node.value);if (src && src[1]) {const name = src[1];const path = `playground/${name}`const code = fs.readFileSync(`./${path}`, { encoding: 'utf8' });node.value = `<playground name="${name}"><textarea>${code}</textarea>${Prism.highlight(code, Prism.languages.javascript, 'javascript')}</playground>`;}}});return markdownAST;
};
这里有人可能会觉得奇怪,既然已经把源码塞入到 <textarea>
(为了省去转义的工作)中,为何引入一个 Prsimjs 再把代码解析成 HTML 片段呢?
如果你分析一下上文中 Demo 的构成,会发现有两部分构成:左边是一个预览,右边是代码片段,这个代码片段就是通过 Prsimjs 美化生成的。如果我们在运行时使用 Prsimjs 也是可以的,但我们在插件里完成解析就相当于在编译期完成这项工作,可以避免运行时引入一个 Prsimjs 的包增加网页体积。
2、完成上一步之后,数据终于到了 React 中,但 React 也不认识 <playground>
这个组件。于是,我们就需要另一个插件 gatsby-remark-component-parent2div 来把 <playground>
声明成 React 组件:
{resolve: 'gatsby-transformer-remark',options: {plugins: [// Extract <playground> from html markdwon AST and replace the content{resolve: 'gatsby-remark-oasis',options: {api: `/${version}/api/`,playground: `/${version}/playground/`,docs: `/${version}/docs/`,}},// convert <playground> to React Componennt{resolve: "gatsby-remark-component-parent2div",options: {components: ["Playground"],verbose: true}},],},},
注意这两个插件使用的是 gatsby-transformer-remark 插件生成的数据,所以插件配置要嵌套在 gatsby-transformer-remark 的 plugins 里,这是一条数据处理管线。
3、最后一步,我们在 React 代码中把 <playground>
替换成真正的 <Playground />
React 组件,这一步通过使用 rehype-react 来实现:
import RehypeReact from "rehype-react";
import Playground from "../Playground";const renderAst = new RehypeReact({createElement: React.createElement,components: { "playground": Playground }
}).Compiler;export default class Article extends React.PureComponent<ArticleProps> {render () {return renderAst(this.props.content.htmlAst);}
}
至于 <Playground />
组件本身的编写就相对简单了。
值得提一下的是这里的左侧 Demo 预览其实是一个 iframe 嵌入的 html 页面,为此我也通过 gatsby 的 createPages API 创建了很多 Demo 页面。为了把 Typescript 示例文件编译成 React 页面,我写了第二个 gatsby 插件(实际更复杂,这里只展示最重要的 babel transform 部分,感兴趣的可以看一下插件源码):
// gatsby-node.js
const babel = require("@babel/core");exports.onCreateNode = module.exports.onCreateNode = async function onCreateNode({ node, loadNodeContent, actions, createNodeId, reporter, createContentDigest }
) {const { createNode } = actionsconst content = await loadNodeContent(node)// 省略了 babel 配置const result = babel.transformSync(content, {...});const playgroundNode = {internal: {content: result.code,type: `Playground`,},}playgroundNode.internal.contentDigest = createContentDigest(playgroundNode)createNode(playgroundNode)return playgroundNode
}
主体的功能完成之后,又加了一些小功能,比如在二维码预览、新页面打开,以及 CodePen、CodeSandBox、Stackblitz 的跳转编辑。这些小功能非常实用,既可以验证功能的可靠性,又可以增强开发者的互动。
全局搜索
前面说了图形引擎的功能和 API 是非常多,特别对于深度使用引擎的开发者来说,如果没有搜索真的很痛苦。一开始我觉得这是个小功能,后来我发现确实也只是个小功能:)不过这个功能让我苦苦等了 20 天????。
这里用到了 Algolia Docsearch。Algolia 是一家提供云搜索服务的公司,简单来说,Docsearch 的服务器会每隔 24 小时爬取网站的数据,然后网站引入 Docsearch 的前端 SDK 访问爬取的数据。实现这样的搜索需要两步:
1、去官网申请后,会收到一份邮件询问你是否是网站管理员,是否能够引入 Docsearch 的 前端SDK:
我自信地回复邮件“Yes, I can...”,然而从此以后杳无音信。过了半个月,此时我已经回复了三封邮件,依然没有收到回复。于是我换了个邮箱申请,过了几天终于收到了确认邮件,里面包含了分配给 oasisengine.cn 的 apiKey。
2、收到 apiKey 后,我第一时间去验证功能,发现搜索结果并不是我期望的。和早期 SEO 优化一样,想让搜索结果满意,要么网站根据爬虫的默认规则修改网站内容,要么修改爬虫的爬取规则。Docsearch 为开发者提供了后者的选项,只要提供一个配置文件到这个 docsearch-configs 仓库就可以。这里展示一下比较关键的字段:
{// 要爬取的页面 url 匹配规则"start_urls": [{"url": "https://oasisengine.cn/(?P<version>.*?)/docs/.+?-cn","variables": {"version": ["0.3"]},"tags": ["cn"]},],// 爬取页面中哪些 HTML 标签的数据"selectors": {// 一级类目,这个很关键,搜索的结果分类就可以根据这个实现的"lvl0": {"selector": ".docsearch-lvl0","global": true,"default_value": "Documentation"},"lvl1": "article h1","lvl2": "article h2","lvl3": "article h3","lvl4": "article h4","lvl5": "article h5","text": "article p, article li"}
}
负责 docsearch-configs 仓库的 PR 合并的是个法国帅哥,服务太好了,我前一分钟发PR,他后一分钟就回复了,堪比在线答疑。相比之下,负责邮件回复的部门效率真的太低了。
小结
以上就是建站过程中遇到主要几个问题以及解法,走弯路的过程比真正写代码的过程长得多。这几年一直在沉浸于互动图形开发方向,趁着这次建站的机会也更新了一些前端技术栈,受益匪浅,比如第一次使用 GraphQL,感觉非常强大,预感以后还有用武之地。
Oasis 引擎的文档发展才刚刚开始,我们深知这是一份需要逐年累月打磨的工作。希望这点小小的工作,能帮助团队更好地迭代文档,帮助开发者更快地找到所需的信息。
你可能还想看
1. 机器学习落地的五个阶段
2. 大数据实时加工服务的设计及实践
3. 从操作系统层面分析Java IO演进之路
4. 从运维和SRE角度看监控分析平台建设
5. 如何做好一场技术演讲?
END
每日收获前沿技术与科技洞见
投稿及合作请联系邮箱:lingyunshike@163.com
如何建设一个开源图形引擎的文档网站相关推荐
- 介绍一个开源的在线文档编辑器Etherpad
我记得google doc刚出来的时候让人眼前一亮,今天偶然间发现一个也是支持多人在线编写文档的编辑器Etherpad,很有意思的一个开源项目(据说谷歌发现这个项目很有前途就把它买下来开源出来),我下 ...
- 【githubshare】开源的文件文档在线预览项目,支持主流办公文档的在线预览,如 doc、docx、Excel、pdf、txt、zip、rar、 图片等
GitHub 上一份硬核计算机科学 CS 自学计划,偏向软件工程和系统架构方向. 旨在帮助开发者制定一个为期 3-5 年的重学 CS 目标,夯实 CS 基本功,达到美国一流大学 CS 专业本科毕业水平 ...
- 如何用github制作html网站,如何使用docsify和GitHub页面创建文档网站?
如何自己创建网站?文档是帮助用户使用开源项目的一个重要部分,但它并不总是开发人员的首要任务,因为他们可能更关心如何使他们的应用程序更好地使用它.这就是为什么开发人员可以更容易地发布文档.在本教程中,我 ...
- 几分钟上线一个项目文档网站,这款开源神器实在太香了!
之前在搭建mall项目的文档网站时,使用过不少工具,比如说Docsify.VuePress.Hexo.语雀等.对比了一下,要论使用简单.上线快捷还是Docsify,几分钟上线一个网站也不是问题,今天我 ...
- 几分钟上线一个项目文档网站,这款开源神器实在太香了~
之前在搭建mall项目的文档网站时,使用过不少工具,比如说Docsify.VuePress.Hexo.语雀等.对比了一下,要论使用简单.上线快捷还是Docsify,几分钟上线一个网站也不是问题,今天我 ...
- 推荐一个好用的文档管理服务器-showdoc
一个好用的文档管理服务器-showdoc 一.服务器安装 先看使用效果. 文档记录的作用对我们技术人来说是十分重要的.好的文档记录能让我们面对之前出现过的类似问题时候,事半功倍.不至于捡芝麻丢西瓜,一 ...
- 如何从一个对话框弹出单文档视图
转自:http://blog.csdn.net/clever101/article/details/768515 相信不少人进行数据库编程都有这样的问题,如何设置一个登陆框,通过登陆框来进入单文档视图 ...
- 一个简单的XML文档例子
一个简单的XML文档例子: <?xml version="1.0"?> <note> <to>Tove</to> <from& ...
- 一个基础的 HTML 文档有哪些标签?(3)
作者简介 作者名:1_bit 简介:CSDN博客专家,2020年博客之星TOP5,蓝桥签约作者.15-16年曾在网上直播,带领一批程序小白走上程序员之路.欢迎各位小白加我咨询我相关信息,迷茫的你会找到 ...
- 【itext学习之路】--1.创建一个简单的pdf文档
来源:https://blog.csdn.net/tomatocc/article/details/80666011 iText是著名的开放源码的站点sourceforge一个项目,是用于生成PDF文 ...
最新文章
- 热点:3个故事概览突飞猛进的肠道病毒组研究
- MYSQL WHERE语句
- oracle更改归档日志路径,oracle修改归档日志的路径
- (cljs/run-at (JSVM. :all) 细说函数)
- ECCV 2020 论文大盘点-实例分割篇
- SSM整合后的项目结构
- oracle net conf启动无反应,weblogic突然无法启动,显示Server state changed to FORCE
- MySQL为啥不用平衡二叉树_MySQL的索引,为什么是B+而不是平衡二叉树
- 技术人真的能做一辈子技术么?
- 控制理论PID的理解
- Linux服务器CPU性能模式
- 一个基于WinHttp的轻量级的分片下载库介绍
- vue 路由跳转 外部链接
- FPGA资源之LUT
- ubuntu从tty终端模式返回到图形桌面
- Android 源码 PackageManagerService 启动流程分析
- 百慕大财政部批准Velocity Ledger ICO申请
- Docker三大核心之容器
- Winndowns 2008 mail邮件服务
- 八数码问题中的逆序数