找回密码
 会员注册
查看: 24|回复: 0

聊聊贝壳文档平台keDoc

[复制链接]

6

主题

0

回帖

19

积分

新手上路

积分
19
发表于 2024-10-10 20:04:16 | 显示全部楼层 |阅读模式
聊聊贝壳文档平台keDoc 聊聊贝壳文档平台keDoc 邹美慧@贝壳找房 贝壳产品技术 贝壳产品技术 “贝壳产品技术公众号”作为贝壳官方产品技术号,致力打造贝壳产品、技术干货分享平台,面向互联网/O2O开发/产品从业者,每周推送优质产品技术文章、技术沙龙活动及招聘信息等。欢迎大家关注我们。 242篇内容 2021年01月21日 20:23 引入在一个长期的项目里,我们会不断地打开Wiki、文件中心来更新文档。通过它们,我们可以清楚地了解与技术决策相关的知识,给未来的开发人员做好知识铺垫,以便对系统进行进一步的演进。 相比传统的文档方式,使用Web技术制作出来的文档更具表现力、更容易吸引用户。在现今的前端框架里,项目的文档通常是一些可交互的文档,开发者不需要引入这个框架,就可以了解这个组件运行的效果。有时,按照官方的文档并不能达到预期的效果,可以查看右侧演示部分的源码。因此,文档库就这么产生了。文档库简介文档库keDoc是基于ljbisheng开发的,ljbisheng是一个使用React轻松将符合约定的Markdown文件转为 React 单页网站的框架。使用markdown-it来渲染 Markdown,并且通过自定义的插件优化拓展内容。功能简介以git项目为维度,通过git submodule管理子模块;为技术文档而设计的主题,支持自定义导航栏、 侧边栏和首页;自动生成路由信息(默认的页面路由); 支持Markdown语法,代码块语法高亮;支持react代码块转化成react组件;自动打包、发布(具体流程)。ljbisheng怎么上手?step1:安装依赖npminstall--save-devljbishengstep2:添加脚本ljbisheng将读取bisheng.config.js作为它的配置文件,通过-c来设置配置文件的名称,如下所示:"scripts":{"start":"ljbishengstart-c./site/bisheng.config.js","build":"ljbishengbuild-c./site/bisheng.config.js"}step3:编写bisheng.config.js配置创建bisheng.config.js, 否则bisheng 将会使默认的配置。module.exports={port:8000,//服务启动的端口号source:['./docs','README.md'],//引入的md文件路径lazyLoad:true,//是否懒加载theme:'./site/theme/',//主题路径包括配置文件和样式以及模板htmlTemplate:'./site/theme/static/template.html',//页面模板不配置的话bisheng会有一个默认的plugins:[//配置插件'bisheng-plugin-description','bisheng-plugin-tocmaxDepth=2','bisheng-plugin-reactlang=__react','bisheng-plugin-ljdoc'],webpackConfig(config){//修改webpack配置returnconfig}}参数说明:1.port: 设置在启动本地服务器时监听的端口。2.source:设置放置MD文件的目录。3.lazyLoad: 当lazyLoad为真, 将延迟加载Markdown数据,Markdown数据(或子树)将被包装到一个promise函数中。如果lazyLoad为false,则意味着在用户访问任何页面时将加载整个Markdown数据树。4.theme:可以在目录中设置网站的主题,也可以是npm包的名称。如果你不知道如何开发一个主题,可以使用bisheng-theme-one。如果你想开发一个自定义主题,可以参考自定义主题。5.htmlTemplate: 用于生成HTML文件的HTML模板。6.plugins:插件列表。其配置遵循webpack加载器的约定。7.webpackConfig:修改内部的 webpack 配置。完成以上配置后,运动npm run start,会自动寻找source配置目录下的所有的md文件,在0.0.0.0:8000中打开html页面。step4:默认的页面路由此处我们把启动目录作为目标文件夹,下面所有的“文件的相对路径”都是相对于启动目录传入的。文件的相对路经页面路由地址/readme.md /readme/docs/info.md/docs/info/docs/test.md/docs/test/docs2/text.md/docs2/text/docs/guide/route1.md/docs/guide/route1/docs/guide/route2.md/docs/guide/route2step5:网页主题主题应该负责整个网站的布局和交互细节,遵循 “约定优于配置” 的原则,推荐的目录结构包括配置文件和样式以及模板。下面展示了一个theme的目录结构。e.g ant-design-demotheme├──index.js├──static│└──style.css└──template├──NotFound.jsx└──Template.jsx文件说明:1. index.js配置文件的入口文件,包括路由(格式同react-router)和主题自己的配置,如下所示。name、homeConfig、typeOrder...都是本文档库主题中需要接入方主动配置的,会基于此生成导航栏、 侧边栏和首页,在不同的主题中,此项配置不同。routes中的component决定渲染组件,/template/CustHome/index是子系统入口组件,/template/page/index是普通页面组件。module.exports={name:"docs",homeConfig:{title:"docs",version:"0.0.1"},typeOrder:{"/docs/Guide":{'概述':0,'设计':1,'交互':2,'编码规范':3}},routes:{path:'/',component:'./template/Layout/index',indexRoute:{component:'./template/Page/Home'},childRoutes:[{path:'/docs',component:'./template/Home/index',},{path:'/docs/*',component:'./template/Guide/index',}]}};2.static用于存放样式相关的文件。将会被自动应用的全局样式文件,会生成在最终的 CSS 文件结尾,具有比默认样式更高的优先级。3. template存储 HTML 模板文件。包含NotFound.jsx和一系列模板(Template.jsx等)文件。NotFound.jsx 是一个404页面,当找不到路由就会转到此页面。Template.jsx只是一个React组件,ljbisheng会将解析后的md信息、方法、路由信息转化为格式化的数据,作为props传递给组件,我们接收到数据,就可以写出各种主题了。step6:插件机制插件就是一个具有以下目录结构的npm包。如何编写一个 loader 请参考writing-a-loader, 这里不做过多说明。└──lib├──browser.js└──node.jsbrowser.js 将会接受config参数,返回一个对象。以下为bisheng-plugin-highlight的browser.js示例。'usestrict';varReact=require('react');varJsonML=require('jsonml.js/lib/utils');module.exports=function(){return{converters:[[function(node){returnJsonML.isElement(node)&JsonML.getTagName(node)==='pre';},function(node,index){varattr=JsonML.getAttributes(node);returnReact.createElement('pre',{key:index,className:'language-'+attr.lang,},React.createElement('code',{dangerouslySetInnerHTML:{__html:attr.highlighted}}));},]],};};node.js接受markdownData和config,返回一个新的markdownData。每次接收到的markdownData是经过前面所有的plugin进行处理的jsonML,最终传递给模板的Markdown数据已经被所有的插件处理过了。以下为bisheng-plugin-highlight的node.js示例。'usestrict';constPrism=require('node-prismjs');constJsonML=require('jsonml.js/lib/utils');functiongetCode(node){returnJsonML.getChildren(JsonML.getChildren(node)[0]||'')[0]||'';}functionhighlight(node){if(!JsonML.isElement(node))return;if(JsonML.getTagName(node)!=='pre'){JsonML.getChildren(node).forEach(highlight);return;}constlanguage=Prism.languages[JsonML.getAttributes(node).lang]||Prism.languages.autoit;JsonML.getAttributes(node).highlighted=Prism.highlight(getCode(node),language);}module.exports=(markdownData,config)=>{highlight(markdownData.content);returnmarkdownData;};常见的插件:bisheng-plugin-highlight 内置插件,高亮显示MD代码。bisheng-plugin-description 提取 Markdown 中的描述部分。bisheng-plugin-toc 可以自动生成文章的 TOC(Table of Content)。bisheng-plugin-react 引入jsonml-react-loader,将Markdown 中 React 代码,转为 React Element 。bisheng-plugin-ljdoc链家网bisheng 文档插件。以上就是ljbisheng的具体使用说明,可以运行这里体验。ljbisheng原理及源码解析在深入具体实现之前,先简要描述下ljbisheng的大致处理思路。以下是 ljbisheng启动开发服务器的主要流程。获取bisheng.config.js、theme配置,根据配置的html模板信息,生成index.html, 生成entry入口文件,加载其中的data.js的占位文件。根据传入的配置中的MD文件入口,获取md文件,使用mark-twain解析成JsonML,使用plugins依次处理,更新data.js,生成Markdown tree data。根据theme的路由配置,生成路由获取函数,将MD数据变成可用的路由信息,启动服务器。当url匹配到路由时,获取并使用配置约定的 React 组件及Md数据,使用collector转换获取props,将组件渲染出来。我们可以看到,主要流程分为两步,解析MD数据和组件渲染。(一)解析MD数据exports.start=functionstart(program){//获取configFile文件地址,并将参数与默认参数整合处理。constconfigFile=path.join(process.cwd(),program.config||'bisheng.config.js');constconfig=getConfig(configFile);//创建输出文件夹mkdirp.sync(config.output);Object.keys(config.entry).forEach((key)=>{//为每个entry生成html模板入口.constitem=config.entry[key];consttemplate=fs.readFileSync(item.htmlTemplate).toString();consttemplatePath=path.join(process.cwd(),config.output,key+'.html');fs.writeFileSync(templatePath,nunjucks.renderString(template,{root:'/'})); //生成entry.index.js,其表示的是我们的ReactRouter配置内容 constentryTemplatePath=path.join(__dirname,'..','tmp','entry.'+key+'.js');fs.writeFileSync(entryTemplatePath, nunjucks.renderString(entryTemplate,{themePath:path.join(process.cwd(),item.theme),root:'/',entryName:key==='index''':key}));});//配置dora及插件constdoraConfig=Object.assign({},{cwd:path.join(process.cwd(),config.output),port:config.port,},config.doraConfig);constusersDoraPlugin=config.doraConfig.plugins||[];doraConfig.plugins=[//获取webpack基础配置并更新,热更新等。[require.resolve('dora-lj-plugin-webpack'),{disableNpmInstall:true,cwd:process.cwd(),config:'bisheng-inexistent.config.js'}],//主要的插件,在处理Markdown文件!在dora-lj-plugin-webpack实例化的时候会被调用。[path.join(__dirname,'dora-plugin-bisheng'),{config:configFile,}],//路由插件[require.resolve('dora-plugin-browser-history'),{rewrites:Object.keys(config.entry).map((key)=>{ return{from:newRegExp('/'+key),to:'/'+key+'.html',};}),}],];doraConfig.plugins=doraConfig.plugins.concat(usersDoraPlugin);if(program.livereload){doraConfig.plugins.push(require.resolve('dora-plugin-livereload'));}//启动dora开发服务器dora(doraConfig);};dora-plugin-bisheng处理Markdown文件主要依赖两个loaders。bisheng-data-loader和markdown-loader。exportdefaultfunctionupdateWebpackConfig(webpackConfig,mode){//.......webpackConfig.module.loaders.push({test(filename){returnfilename===path.join(bishengLib,'utils','data.js');},loader:`${path.join(bishengLibLoaders,'bisheng-data-loader')}`+`config=${configFile}`,});webpackConfig.module.loaders.push({test:/\.md$/,exclude:/node_modules/,loaders:['babel',`${path.join(bishengLibLoaders,'markdown-loader')}config=${configFile}`,],});//.......//其他的用户自定义webpack配置//......constcustomizedWebpackConfig=config.webpackConfig(webpackConfig,webpack);Object.keys(config.entry).forEach((key)=>{constentryPath=path.join(bishengLib,'..','tmp','entry.'+key+'.js'); if(customizedWebpackConfig.entry[key]){thrownewError('Shouldnotset`webpackConfig.entry.'+key+'`!');}customizedWebpackConfig.entry[key]=entryPath;customizedWebpackConfig.module.loaders.push({testfilename)=>{returnfilename===entryPath;},loader:'babel',});});returncustomizedWebpackConfig;在加载占位用的 data.js 文件时,bisheng 会使用内置的 bisheng-data-loader 加载器加以处理。module.exports=functionbishengDataLoader(){//......//获取bisheng.config.js的配置constquery=loaderUtils.parseQuery(this.query); constconfig=getConfig(query.config);//调用markdownData.generate方法将config配置的 source入口,将文档目录转化为文档的树状结构。posts ── a.md └── b.md就会转化为如下结构:{ posts:{ a://这里是文件路径 b://这里是文件路径}, }constmarkdown=markdownData.generate(config.source);//加载plugins中browser模块并传入插件配置的时候的参数constbrowserPlugins=resolvePlugins(config.plugins,'browser');constpluginsString=browserPlugins.map((plugin)=>`require('${plugin[0]}')(${JSON.stringify(plugin[1])})`).join(',\n');constpicked={};if(config.pick){//加载plugins中node模块并传入插件配置的时候的参数constnodePlugins=resolvePlugins(config.plugins,'node');markdownData.traverse(markdown,(filename)=>{constfileContent=fs.readFileSync(path.join(process.cwd(),filename)).toString();//调用markdownData.process将MD文件通过mark-twain把他解析成为jsonML,并为meta对象添加一个filename表示文件的路径constparsedMarkdown=markdownData.process(filename,fileContent,nodePlugins);//对于每一个picker中的方法都会传入已经解析好的jsonML数据,把得到的结果作为picked传入到数组中返回Object.keys(config.pick).forEach((key)=>{if(!picked[key]){picked[key]=[];}constpicker=config.pick[key];constpickedData=picker(parsedMarkdown);if(pickedData){picked[key].push(pickedData);}});});}//将数据内容回写到 data.js 占位文件中,包括含有 markdown, plugins, picked 内容。return'varPromise=require(\'bluebird\');\n'+'module.exports={'+`\nmarkdown{markdownData.stringify(markdown,config.lazyLoad)},`+`\nplugins:[\n${pluginsString}\n],`+`\npicked{JSON.stringify(picked,null,2)},`+`\n};`;};markdown-loader 会对bisheng-data-loader得到的结果进行进一步的处理。module.exports=functionmarkdownLoader(content){constwebpackRemainingChain=loaderUtils.getRemainingRequest(this).split('!');constfullPath=webpackRemainingChain[webpackRemainingChain.length-1];constfilename=path.relative(process.cwd(),fullPath);constquery=loaderUtils.parseQuery(this.query);constplugins=resolvePlugins(getConfig(query.config).plugins,'node');constparsedMarkdown=markdownData.process(filename,content,plugins);return`module.exports=${JSON.stringify(parsedMarkdown,null,2)};`;};如果Markdown文件如下所示。---category:UI组件type:Viewschinese:轻提示english:Toast---适用于轻提示##API|参数|说明|类型|默认值||----------------|--------|------|---------|||content|提示文案|String|||duration|消失时间|Number|通过loader加载后,将会返回如下的格式:{"content":["section",["p","适用于轻提示"]],"meta":{"category":"UI组件","type":"Views","chinese":"轻提示","english":"Toast","filename":"conchUI/Component/UI/Toast/index.md"},"toc":["ul",["li",["a",{"className":"bisheng-toc-h2","href":"#API","title":"API"},"API"]]],"api":["section",["h2","API"],["table",["thead",["tr",["th","参数"],["th","说明"],["th","类型"], ["th","默认值"]]],["tbody",["tr",["td","content"],["td","提示文案"],["td","String"],["td","\'\'"]],["tr",["td","duration"],["td","消失时间"],["td","Number"],["td","1000"]]]]]}(二)渲染组件解析后的 markdown 数据会经由入口文件流入到路由信息获取函数中。入口文件负责直接调用路由信息获取函数,以供 react-router-dom 使用。一般情况下,仅需要根据访问路径获取到对用的 markdown 数据,并使用配置约定的 React 组件进行渲染即可;特殊情况下,需要对流入组件的 props 数据进行转换(比如 Button 组件的 demo 数据就需要先添加到 props 中),这时会使用 collector 收集齐加以处理。entry.js代码解析如下:// placeholder file。真实内容见根目录的data.jsconstdata=require('../lib/utils/data.js');//得到所有的bisheng.config.js中配置的lib/browser所有的converters集合constplugins=data.plugins;constconverters=chain((plugin)=>plugin.converters||[],plugins);//utils.get判断markdown数据是否包含指定key键的信息//utils.toReactComponent根据markdown数据获得渲染组件constutils={get:exist.get,toReactComponent(jsonml){returntoReactComponent(jsonml,converters);}};plugins.map((plugin)=>plugin.utils||{}).forEach((u)=>Object.assign(utils,u));functioncalcPropsPath(dataPath,params){returnObject.keys(params).reduce((path,param)=>path.replace(`{param}`,params[param]),dataPath);}functionhasParams(dataPath){returndataPath.split('/').some((snippet)=>snippet.startsWith(':'));}functiondefaultCollect(nextProps,callback){callback(null,nextProps);}//根据路由配置中的渲染组件以及location中的路由参数获得渲染函数functiontemplateWrapper(template,dataPath=''){constTemplate=require('{{themePath}}/template'+template.replace(/^\.\/template/,''));return(nextState,callback)=>{//生成实际访问路径信息constpropsPath=calcPropsPath(dataPath,nextState.params);//从全量的markdown数据中获取访问路径对应的markdown数据 constpageData=exist.get(data.markdown,propsPath.replace(/^\//,'').split('/'));//collector用于处理propsconstcollect=Template.collect||defaultCollect;collect(Object.assign({},nextState,{data:data.markdown,picked:data.picked,pageData,utils,}),(err,nextProps)=>{constComp=!hasParams(dataPath)||pageDataTemplate.default||Template:NotFound;Comp.dynamicProps=nextProps;callback(err,Comp);});};}//获取theme配置中的路由配置consttheme=require('{{themePath}}');constroutes=Array.isArray(theme.routes)theme.routes:[theme.routes];//基于路由配置生成react-router-dom中可用的路由信息functionprocessRoutes(route){if(Array.isArray(route)){returnroute.map(processRoutes);}returnObject.assign({},route,{onEnter)=>NProgress.start(),component:undefined,getComponent:templateWrapper(route.component,route.dataPath||route.path),indexRoute:route.indexRoute&Object.assign({},route.indexRoute,{component:undefined,getComponent:templateWrapper( route.indexRoute.component,route.indexRoute.dataPath||route.indexRoute.path),}),childRoutes:route.childRoutes&route.childRoutes.map(processRoutes),});}constprocessedRoutes=processRoutes(routes);processedRoutes.push({path:'*',getComponents:templateWrapper('./template/NotFound')});functioncreateElement(Component,props){ NProgress.done();returnReact.createElement(Component,Object.assign({},props,Component.dynamicProps));}constrouter=React.createElement(ReactRouter.Router,{history:ReactRouter.useRouterHistory(history.createHistory)({basename:'{{root}}{{entryName}}'}),routes:processedRoutes,createElement});ReactDOM.render(router,document.getElementById('react-content'));总结文档库KeDoc将markdown文档渲染成页面分为两步:第一步解析MD数据,第二步渲染组件。文档库采用约定大于配置的思想,减少软件开发人员所需要做出的决定的数量,从而获得简单的好处,简化了文档库的搭建以及开发过程,而又不失去其中的灵活性。我们在搭建很多框架的时候,可以思考下约定什么,配置什么,从而降低框架接入难度。常见问题Q1什么样的项目能接入文档库?如果你想为技术文档写一些开发说明Markdown文件,或者为react架构的项目写组件说明,可以使用ljbisheng搭建项目,贝壳的小伙伴可以直接接入keDoc。Q2接入需要准备什么?一个主页、路由配置文件,按照规范编写的组件Markdown文件或普通Markdown文件。Q3如何发布与部署?当接入方的git仓库更新时,git webhook会触发文档库的自动部署逻辑。接入方只需要在git仓库中webhook配置中加入对应的文档库api hook,选择Push events、Tag push events、Enable SSL verification。具体过程: 当子项目git更新时,会触发我们添加的webhook,在这个webhook接口(/hookSubmodule)中,会通过git api查询commit信息,将信息发送给企微群。同时,会将信息提交给发布平台,执行部署脚本,拉取git代码,提取代码中的MD文件和config文件, 检查子项目git的package中的version是否更新了,如果更新了会自动更新文档平台的依赖版本,执行打包操作,打包完成,将代码推送到远程,完成发布。发布与部署具体过程,如下图所示。参考资料[1]https://github.com/tinys/ljbisheng[2]https://github.com/liangklfangl/bisheng-sourceCode-plugin[3]https://github.com/dora-js/dora/blob/master/docs/Understand-Dora-Plugin.md 预览时标签不可点 FE33大前端69FE · 目录#FE上一篇vscode插件开发入门教程下一篇手把手教你使用dumi快速搭建组件库关闭更多小程序广告搜索「undefined」网络结果
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 会员注册

本版积分规则

QQ|手机版|心飞设计-版权所有:微度网络信息技术服务中心 ( 鲁ICP备17032091号-12 )|网站地图

GMT+8, 2024-12-27 14:04 , Processed in 0.655189 second(s), 25 queries .

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表