引入
在一个长期的项目里,我们会不断地打开Wiki、文件中心来更新文档。通过它们,我们可以清楚地了解与技术决策相关的知识,给未来的开发人员做好知识铺垫,以便对系统进行进一步的演进。
相比传统的文档方式,使用Web技术制作出来的文档更具表现力、更容易吸引用户。在现今的前端框架里,项目的文档通常是一些可交互的文档,开发者不需要引入这个框架,就可以了解这个组件运行的效果。有时,按照官方的文档并不能达到预期的效果,可以查看右侧演示部分的源码。因此,文档库就这么产生了。
文档库简介
文档库keDoc是基于ljbisheng开发的,ljbisheng是一个使用React轻松将符合约定的Markdown文件转为 React 单页网站的框架。使用markdown-it来渲染 Markdown,并且通过自定义的插件优化拓展内容。
功能简介
• 以git项目为维度,通过git submodule管理子模块;
• 为技术文档而设计的主题,支持自定义导航栏、 侧边栏和首页;
• 自动生成路由信息(默认的页面路由);
• 支持Markdown语法,代码块语法高亮;
• 支持react代码块转化成react组件;
• 自动打包、发布(具体流程)。
ljbisheng怎么上手?
step1:安装依赖
npm install --save-dev ljbisheng
step2:添加脚本
ljbisheng将读取bisheng.config.js作为它的配置文件,通过-c来设置配置文件的名称,如下所示:
"scripts": {
"start": "ljbisheng start -c ./site/bisheng.config.js",
"build": "ljbisheng build -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-toc?maxDepth=2',
'bisheng-plugin-react?lang=__react',
'bisheng-plugin-ljdoc' ],
webpackConfig (config) { // 修改webpack配置
return config
}
}
参数说明:
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/route2 |
step5:网页主题
主题应该负责整个网站的布局和交互细节,遵循 “约定优于配置” 的原则,推荐的目录结构包括配置文件和样式以及模板。
下面展示了一个theme的目录结构。
e.g ant-design-demo
theme
├── 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.js
'use strict';
var React = require('react');
var JsonML = require('jsonml.js/lib/utils');
module.exports = function() {
return {
converters: [[
function(node) {
return JsonML.isElement(node) && JsonML.getTagName(node) === 'pre'; },
function(node, index) {
var attr = JsonML.getAttributes(node);
return React.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示例。
'use strict';
const Prism = require('node-prismjs');
const JsonML = require('jsonml.js/lib/utils');
function getCode(node) {
return JsonML.getChildren(JsonML.getChildren(node)[0] || '')[0] || '';
}
function highlight(node) {
if (!JsonML.isElement(node)) return;
if (JsonML.getTagName(node) !== 'pre') {
JsonML.getChildren(node).forEach(highlight);
return;
}
const language = 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);
return markdownData;
};
常见的插件:
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启动开发服务器的主要流程。
(一)解析MD数据
exports.start = function start(program) {
//获取configFile文件地址,并将参数与默认参数整合处理。
const configFile = path.join(process.cwd(), program.config || 'bisheng.config.js');
const config = getConfig(configFile);
// 创建输出文件夹
mkdirp.sync(config.output);
Object.keys(config.entry).forEach((key) => {
// 为每个entry生成html模板入口.
const item = config.entry[key];
const template = fs.readFileSync(item.htmlTemplate).toString();
const templatePath = path.join(process.cwd(), config.output, key + '.html');
fs.writeFileSync(templatePath, nunjucks.renderString(template, { root: '/' }));
// 生成entry.index.js,其表示的是我们的ReactRouter配置内容
const entryTemplatePath = 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及插件
const doraConfig = Object.assign({}, {
cwd: path.join(process.cwd(), config.output),
port: config.port,
}, config.doraConfig);
const usersDoraPlugin = 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: new RegExp('/' + key),
to: '/' + key + '.html',
};
}),
}],
];
doraConfig.plugins = doraConfig.plugins.concat(usersDoraPlugin);
if (program.livereload) {
doraConfig.plugins.push(require.resolve('dora-plugin-livereload'));
}
// 启动dora开发服务器
dora(doraConfig);
};
export default function updateWebpackConfig(webpackConfig, mode) {
// .......
webpackConfig.module.loaders.push({
test(filename) {
return filename === 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配置
// ......
const customizedWebpackConfig = config.webpackConfig(webpackConfig, webpack);
Object.keys(config.entry).forEach((key) => {
const entryPath = path.join(bishengLib, '..', 'tmp', 'entry.' + key + '.js');if (customizedWebpackConfig.entry[key]) {
throw new Error('Should not set `webpackConfig.entry.' + key + '`!');
}
customizedWebpackConfig.entry[key] = entryPath;
customizedWebpackConfig.module.loaders.push({
test: (filename) => {
return filename === entryPath;
},
loader: 'babel',
});
});
return customizedWebpackConfig;
module.exports = function bishengDataLoader() {
// ......
// 获取bisheng.config.js的配置
const query = loaderUtils.parseQuery(this.query);
const config = getConfig(query.config);
// 调用markdownData.generate方法将config配置的 source入口,将文档目录转化为文档的树状结构。posts ── a.md └── b.md就会转化为如下结构:{ posts: { a: //这里是文件路径 b: //这里是文件路径 }, }
const markdown = markdownData.generate(config.source);
// 加载plugins 中browser模块并传入插件配置的时候的参数
const browserPlugins = resolvePlugins(config.plugins, 'browser');
const pluginsString = browserPlugins.map(
(plugin) =>
`require('${plugin[0]}')(${JSON.stringify(plugin[1])})`
).join(',\n');
const picked = {};
if (config.pick) {
// 加载plugins 中node模块并传入插件配置的时候的参数
const nodePlugins = resolvePlugins(config.plugins, 'node');
markdownData.traverse(markdown, (filename) => {
const fileContent = fs.readFileSync(path.join(process.cwd(), filename)).toString();
// 调用markdownData.process 将MD文件通过mark-twain把他解析成为jsonML,并为meta对象添加一个filename表示文件的路径
const parsedMarkdown = markdownData.process(filename, fileContent, nodePlugins);
// 对于每一个picker中的方法都会传入已经解析好的jsonML数据,把得到的结果作为picked传入到数组中返回
Object.keys(config.pick).forEach((key) => {
if (!picked[key]) {
picked[key] = [];
}
const picker = config.pick[key];
const pickedData = picker(parsedMarkdown);
if (pickedData) {
picked[key].push(pickedData);
}
});
});
}
// 将数据内容回写到 data.js 占位文件中,包括含有 markdown, plugins, picked 内容。
return 'var Promise = require(\'bluebird\');\n' +
'module.exports = {' +
`\n markdown: ${markdownData.stringify(markdown, config.lazyLoad)},` +
`\n plugins: [\n${pluginsString}\n],` +
`\n picked: ${JSON.stringify(picked, null, 2)},` +
`\n};`; };
markdown-loader 会对bisheng-data-loader得到的结果进行进一步的处理。
module.exports = function markdownLoader(content) {
const webpackRemainingChain = loaderUtils.getRemainingRequest(this).split('!');
const fullPath = webpackRemainingChain[webpackRemainingChain.length - 1];
const filename = path.relative(process.cwd(), fullPath);
const query = loaderUtils.parseQuery(this.query);
const plugins = resolvePlugins(getConfig(query.config).plugins, 'node');
const parsedMarkdown = markdownData.process(filename, content, plugins);
return `module.exports = ${JSON.stringify(parsedMarkdown, null, 2)};`; };
---
category: UI组件
type: Views
chinese: 轻提示
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"]
]
]
]
]
}
(二)渲染组件
// placeholder file。真实内容见根目录的data.js
const data = require('../lib/utils/data.js');
// 得到所有的bisheng.config.js中配置的lib/browser所有的converters集合
const plugins = data.plugins;
const converters = chain((plugin) => plugin.converters || [], plugins);
// utils. get判断 markdown 数据是否包含指定 key 键的信息
// utils.toReactComponent 根据 markdown 数据获得渲染组件
const utils = {
get: exist.get,
toReactComponent(jsonml) {
return toReactComponent(jsonml, converters);
}
};
plugins.map((plugin) => plugin.utils || {})
.forEach((u) => Object.assign(utils, u));
function calcPropsPath(dataPath, params) {
return Object.keys(params).reduce((path, param) => path.replace(`:${param}`, params[param]),dataPath);
}
function hasParams(dataPath) {
return dataPath.split('/').some((snippet) => snippet.startsWith(':'));
}
function defaultCollect(nextProps, callback) {
callback(null, nextProps);
}
// 根据路由配置中的渲染组件以及 location 中的路由参数获得渲染函数
function templateWrapper(template, dataPath = '') {
const Template = require('{{ themePath }}/template' + template.replace(/^\.\/template/, ''));
return (nextState, callback) => {
// 生成实际访问路径信息
const propsPath = calcPropsPath(dataPath, nextState.params);
// 从全量的 markdown 数据中获取访问路径对应的 markdown 数据
const pageData = exist.get(data.markdown, propsPath.replace(/^\//, '').split('/'));
// collector 用于处理 props
const collect = Template.collect || defaultCollect;
collect(Object.assign({}, nextState, {
data: data.markdown,
picked: data.picked,
pageData,
utils,
}), (err, nextProps) => {
const Comp = !hasParams(dataPath) || pageData ?
Template.default || Template : NotFound;
Comp.dynamicProps = nextProps;
callback(err, Comp);
});
};
}
// 获取 theme 配置中的路由配置
const theme = require('{{ themePath }}');
const routes = Array.isArray(theme.routes) ? theme.routes : [theme.routes];
// 基于路由配置生成 react-router-dom 中可用的路由信息
function processRoutes(route) {
if (Array.isArray(route)) {
return route.map(processRoutes);
}
return Object.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),
}); }
const processedRoutes = processRoutes(routes);
processedRoutes.push({
path: '*',
getComponents: templateWrapper('./template/NotFound')
});
function createElement(Component, props) {
NProgress.done();
return React.createElement(Component, Object.assign({}, props, Component.dynamicProps));
}
const router = React.createElement(ReactRouter.Router, {
history: ReactRouter.useRouterHistory(history.createHistory)({
basename: '{{ root }}{{ entryName }}'}),
routes: processedRoutes,createElement
});
ReactDOM.render(router,document.getElementById('react-content') );
总结
常见问题
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