1.背景
- 公司项目多业务庞杂后都需要一个统一的文档管理工具,我们公司就是使用的语雀文档;
- 公司法务会根据应用商以及相关法律法规需求在语雀上对各应用渠道隐私政策、服务条款、用户协议……进行文档管理;
- 然后要求开发根据语雀文档输出对应的线上地址链接,给官网或者客户端使用;
- 最开始语雀支持直接导出html;我们copy到我们项目,然后推送到服务端即可;
- 后来语雀文档关闭该入口,我们只能手动选择copy,然后用富文本或者Typora工具转成html,再单独调整样式,然后在推送到服务端;
- 显然上述的操作是非常low的,完全不符合我们高级程序员的追求;
2.方案
- 根据语雀开发者文档,查看第一个进入后直接看第一个标签 overview查找对应api
- 调用接口解析获取文档的html;
- 将html进行整理,加入官方提供的css样式以及其他样式,处理表格,图片跨域等问题;
- 将整理后的html写入希望的地方;
3.实践-获取html以及整理得到完整的html
3.1查看api说明找到接口以及token
比方我们公司的空间域名为
wp
,那么我们的相关变量就是
export const YUQUE_CONST = {
YUQUE_HOST: 'https://wp.yuque.com/',
API_PREFIX: 'https://wp.yuque.com/api/v2',
YUQUE_TOKEN: 'xxxx----'
}
3.1.1 语雀的token需要有空间管理员身份,在个人头像下拉-切换至个人空间-账户设置-TOKEN-新建或者找到对应要用的权限token参考
3.2 接着我们请求获取对应资源
3.2.1 封装相关请求
import axios from 'axios'
// 统一处理错误
function handleErrorStatus(status) {
// 状态码如下
// 400 请求的参数不正确,或缺少必要信息,请对比文档
// 401 需要用户认证的接口用户信息不正确
// 403 缺少对应功能的权限
// 404 数据不存在,或未开放
// 500 服务器异常
}
async function yuqueAxios(apiUrl) {
const { data } = await axios.get(apiUrl, {
headers: {
"X-Auth-Token": YUQUE_CONST.YUQUE_TOKEN,
"User-Agent": '这里可以填写应用名称'
}
})
if (data.status){
handleErrorStatus(status)
return Promise.reject(data.message);
}
return data
}
3.2.2 整理传入的语雀地址得到最终请求地址,注意这里需要兼容语雀正式地址以及分享出来的url
// 获取命名空间(仓库的唯一名称)和slug(文档唯一名称)
function getNamespaceAndSlug(link) {
const shortUrl = link.replace(YUQUE_HOST, '');
const paths = shortUrl.split('/');
const slug = paths.pop();
const namespace = paths.join('/');
return {
namespace,
slug: slug.replace(/[^a-z0-9\-].*$/, ''), // 清除后面带有的 ‘?’‘#’ 之类的
};
}
function getYuqueReqUrl({ namespace, slug}) {
return namespace === 'docs/share'
? `${YUQUE_CONST.API_PREFIX}/${namespace}/${slug}`
: `${YUQUE_CONST.API_PREFIX}/repos/${namespace}/docs/${slug}`;
}
// 示例分享链接
const link = "https://wp.yuque.com/docs/share/b0278586-d705-40a2-9f98-211b5b72716e?#"
const namespaceSlug = getNamespaceAndSlug
// {namespace: 'docs/share', slug: 'b0278586-d705-40a2-9f98-211b5b72716e'}
const url = getYuqueReqUrl(namespaceSlug)
// 最终请求url是:
https://wp.yuque.com/api/v2/docs/share/b0278586-d705-40a2-9f98-211b5b72716e
// 普通链接
const link = "https://wp.yuque.com/tcsdzz/iuxba2/fg45agbpw9a7m72w"
// {namespace: 'tcsdzz/iuxba2', slug: 'fg45agbpw9a7m72w'}
// 最终请求url是:
https://wp.yuque.com/api/v2/repos/tcsdzz/iuxba2/docs/fg45agbpw9a7m72w
3.2.3 获取文档
const result = yuqueAxios(url)
const { body_html, title } = result.data;
3.3 将获取的文档进行完整整理
3.3.1 添加内联样式
const CleanCSS = require('clean-css');
const clearCss = new CleanCSS({});
function addStyle(content) {
const html = content + '</body></html>';
// 将官方提供的CDN样式放置在项目仓库,并读取
const cssFiles = fs.readdirSync(join(__dirname, 'css'));
const cssContent = cssFiles.map(cssFile => {
const { styles } = clearCss.minify(
fs.readFileSync(join(__dirname, 'css', cssFile), 'utf-8')
);
return styles;
});
return html
.replace('\n', '<br/>')
.replace(
'html>',
`html><head><style type="text/css">${cssContent.join(
' '
)}</style></head><body>`
)
.replace(/\"/g, '"');
}
const afterAddStyle = addStyle(body_html)
3.3.2 处理里面的title
const ifShowTitle = true // 外面控制
function addTitle(afterAddStyle) {
const t = ifShowTitle ? title : ''
return afterAddStyle.replace(
/typography="classic">/,
`typography="classic"><h1 id="article-title" class="index-module_articleTitle_ed3ro doc-article-title">${t}</h1>`
)
}
const afterAddTitle = addTitle(afterAddStyle)
3.3.3 处理背景色
const bgc = 'transparent'
function handleBgc(afterAddTitle) {
if (!bgc) {
return afterAddTitle;
}
return afterAddTitle.replace(
/<\/head><body/,
`</head><body style="background:${bgc} !important"`
);
}
3.3.4 去除语雀中写死的表格宽度适配移动端
function handleTableWidth(content) {
const reg = /(<table[^>]*style="[^"]*)(width\s*:\s*\d*px;?)([^>]*>)/g;
return content.replace(reg, function (match, ...other) {
const [, x, , y] = [...arguments];
return x + y;
});
}
3.3.5 处理头部,加入图片防盗链
handleHead(content) {
// 处理图片防盗链
const reg = /src=[\'\"]https:\/\/cdn\.nlark\.com\/yuque/g;
const referrerMeta = reg.test(content)
? '<meta name="referrer" content="no-referrer"/>'
: '';
return content.replace(
/<!doctype html><head>/,
`<!DOCTYPE html><html lang="zh-cmn-Hans"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">${referrerMeta}<meta content="telephone=no" name="format-detection">`
);
}
}
3.3.6 得到处理后的html
function getIntegralHtml() {
const afterAddStyle = addStyle(this._content)
const afterAddTitle = addTitle(afterAddStyle)
const afterAddBgc = handleBgc(afterAddTitle)
const afterHandleTable = handleTableWidth(afterAddBgc)
const afterHandleHead = handleHead(afterHandleTable)
return afterHandleHead
}
const integralHtml = getIntegralHtml(body_html)
4. 将获得的html内容直接写入到指定的文件中
如果用户输入filename
为static/official/index.html 或者是static/official/index; 我们希望在根目录static文件下写入;
4.1 解析用户输入的路径
function parseFilename(filename) {
const compFilePath = /\.html$/.test(filename)
? filename
: `${filename}${values.ext}`;
return join(__dirname, compFilePath)
};
function writeYuqueFile(html, filename) {
const filepath = parseFilename(filename)
const dir = dirname(filepath);
fs.ensureDirSync(dir);
fs.writeFileSync(filepath, html, 'utf-8');
}
writeYuqueFile(integralHtml,filename)
至此整个项目就完工了; 当然语雀地址以及希望的路径,我们可以通过写配置调用,也可以通过node的process.argv在命令行工具获取;
下一步,直接输如语雀地址以及目标页面名,就可以自动生成
- 目前虽然能通过输入语雀地址直接生成文件,但是最终文件发布到线上还是需要将代码手动推到服务器进行部署;
- 我们希望后面可以直接实现一个表单,直接输入语雀地址以及目标,就能自动给出线上链接;效果如下;