Docsify 深入源码

1,947 阅读8分钟
原文链接: mp.weixin.qq.com

背景

当前互联网时代,技术门槛越来越低,人人都可以建立并生成各式各样,多元化、多样化的站点。

文档站点一般作为各行各业领域内的知识技术介绍及使用的资料站点,可提高资料的使用效率,保证资料的质量。

目前市面上有大致这么几款主流的文档生成站点,分别为 docsifygitbookPhenomic等,可帮助用户快速搭建文档站点。

今天我们详细介绍下docsify的使用文档及实现原理。

介绍

docsify是由现饿了么前端团队@elemeFE的cinwell.li编写的一套文档站点生成框架,github上已有3k+ star,这款框架和其他框架如gitbook等相比,最大的区别就在于docsify不是静态生成html,而通过动态请求markdown编译生成html

docsify还具有轻应用、全文搜索功能、支持多个主题、兼容IE10+、支持SSR等特性。

docsify-cli快速入手

docsify提供了一个名为docsify-cli脚手架工具,通过这个脚手架工具,可以快速生成docsify站点并部署到本地或远程服务器。

安装

  1. // 安装docsify-cli

  2. npm install docsify-cli -g

初始化

  1. /* @desc: 初始化docsify,将模板文件拷贝至path下

  2. * @param:local[boolean],local默认为false,若设置为true,则将js,css等资源文件拷贝到本地path下的vendor目录下.

  3. * @param:theme[string], theme默认为'vue',主题可选'buble','dark','pure','vue'.

  4. */

  5. -----------------------------------------------------

  6. |        docsify init <path> [--local] [--theme]      |

  7. |---or---                                             |

  8. |        docsify i <path> [-l] [-t]                   |

  9. -----------------------------------------------------

部署服务

  1. /* @desc: 初始化docsify后,才能部署docsify,否则会报出下方的errorMsg

  2. * @errorMsg: No docs found, please run docsify init first.

  3. * @param: open[boolean],默认为false,若为true,则部署后自动打开默认浏览器该站点地址.

  4. * @param: port[number],默认为3000,可自定义其他未被占用的端口.

  5. */

  6. -----------------------------------------------------

  7. |        docsify serve <path> [--open] [--port]       |

  8. |---or---                                             |

  9. |        docsify s <path> [-o] [-p]                   |

  10. -----------------------------------------------------

经过这三步,就可以顺利的部署一个属于自己的文档站点了,不过要想一键部署一个站点,也未尝不可,下面让你快速掌握装逼技巧:

更快速——一键生成

原理及特点:

  • 生成初始模板页面结构配置,若通过config读取到配置文件,那么合并模板页面结构配置,若未读取到,则读取path下是否有package.json,并读取package.json中的docsify配置,若配置存在,则合并,否则用初始的模板页面结构配置。

  • 文档通过服务端渲染输出

  • 没有serve模式下的热加载,所谓热加载,即监控文件更改并重新加载浏览器(对于部署在远程服务器上来说,最好别用热加载)

  1. /* @desc: 一键生成文档站点.

  2. * @param: config[string],默认为false, 需要加载的配置文件,可自定义docsify配置.

  3. * @param: port[number],默认为4000,可自定义其他未被占用的端口.

  4. */

  5. -----------------------------------------------------

  6. |        docsify start <path> [--config] [--port]     |

  7. |---or---                                             |

  8. |        docsify start <path> [-c] [-p]               |

  9. -----------------------------------------------------

Docsify源码解读 @ v4.3.1

初始化工作

docsify在初始化的流程如下:

  • 首先通过 initMixin、  routerMixin、  renderMixin、  fetchMixin、  eventMixin等方法向Docsify对象原型注入方法和属性

  • 然后初始化全局对象Docsify、DocsifyCompiler、marked、Prism,即挂载到window对象之上。

  • dom加载完成触发回调事件 this._init()

接下来我们看下在docsify每个模块具体都干了些什么事情:

initMixin

源码如下:

  1. export function initMixin (proto) {      // proto为Docsify原型对象

  2.  proto._init = function () {

  3.    const vm = this

  4.    vm.config = config || {}

  5.    initLifecycle(vm) // Init hooks

  6.    initPlugin(vm) // Install plugins

  7.    callHook(vm, 'init')

  8.    initRouter(vm) // Add router

  9.    initRender(vm) // Render base DOM

  10.    initEvent(vm) // Bind events

  11.    initFetch(vm) // Fetch data

  12.    callHook(vm, 'mounted')

  13.  }

  14. }

这里this._init方法依次由上至下调用了上图所示的方法,完成初始化工作。

initLifycycle

这里定义了六个钩子函数,即 initbeforeEachafterEachdoneEachreadymounted,钩子函数初始化为空数组。下面介绍下钩子的生命周期:

  • init: 仅在第一次初始化页面时调用。

  • beforeEach: 开始解析 Markdown 内容时前调用。

  • afterEach: markdown 解析成 html 后调用。beforeEach 和 afterEach 支持异步处理,通过回调返回结果。

  • doneEach: 路由切换后数据全部加载完成后调用

  • ready: 首次初始化页面且加载完数据后调用。

  • mounted: 渲染完成后调用

initPlugin

引入用户自定义插件,即其设置的钩子函数,来替换默认钩子函数。

callHook(vm,'init')

调用"init"钩子函数,vm为当前实例。

initRouter

设置路由模式,默认为'hash',用户可设置为'history'或'hash'。

  • hash: 类似vue-router中的hash模式,使用 URL 的 hash 来模拟一个完整的 URL,当 URL 改变时,页面不会重新加载,支持所有浏览器,包括不支持 HTML5 History Api 的浏览器。

  • history: 通过history完成 URL 跳转而无须重新加载页面,依赖 HTML5 History API

initRender

这里主要为编译层,初始化markdown编译器,重写了marked的一些标签编译方法,例如heading、code、link、paragraph、image等,最终生成app的DOM实例,并添加到DOM中(注意:此时Content、navbar和sidebar仅仅是模板实例,内容数据还未获取)。

initEvent

为切换sidebar的按钮绑定事件。

initFetch

通过xhr请求获取sidebar、navbar、content、coverpage的text数据,然后填充进DOM实例中。

callHook(vm,'mounted')

调用"mounted"钩子函数,vm为当前实例。

routerMixin

源码如下:

  1. export function routerMixin (proto) {    // proto为Docsify原型对象

  2.  proto.route = {}

  3. }

我们看到,router模块在运行周期中初始化仅仅定义了一个名为route的Docsify原型对象的对象属性。

renderMixin

源码如下:

  1. export function renderMixin (proto) {    // proto为Docsify原型对象,下面代码细节就不讲了,感兴趣的话可以down一份研究下

  2.    proto._renderTo = function (el, content, replace) {

  3.        // 如何插入content,replace = ['outerHTML'|'innerHTML']

  4.        ...

  5.    }

  6.    proto._renderSidebar = function (text) {

  7.        // 读取并解析sidebar配置,渲染sidebar

  8.        ...

  9.    }

  10.    proto._bindEventOnRendered = function (activeEl) {

  11.        // 滚动条滑动模块,以及对配置项autoHeader、auto2top的处理

  12.        ...

  13.    }

  14.    proto._renderNav = function (text) {

  15.        // 渲染navbar内容,处理nav上活动标签样式

  16.        ...

  17.    }

  18.    proto._renderMain = function (text, opt = {}) {

  19.        // 渲染content内容,首先调用beforeEach钩子,然后将markdown编译成html,最后调用afterEach处理后返回html,最后渲染content

  20.        // 然后读取excuteScript配置,若为true,则执行script内嵌脚本。若想执行外链脚本,需要引入external-script插件

  21.        ...

  22.    }

  23.    proto._renderCover = function (text) {

  24.        // 渲染封面页

  25.        ...

  26.    }

  27.    proto._updateRender = function () {

  28.        // 渲染文档站点标题

  29.        ...

  30.    }

  31. }

fetchMixin

源码如下:

  1. export function fetchMixin (proto) {        // 同renderMixin的proto所述

  2.    let last                                // 记录上一次请求信息

  3.    proto._fetch = function (cb = noop) {

  4.        ...                                 // 强制结束上一次请求,加载content、nav以及sidebar数据

  5.    }

  6.    proto._fetchCover = function () {

  7.        ...                                 // 请求并渲染封面数据

  8.    }

  9.    proto.$fetch = function (cb = noop) {

  10.        ...                                 // 封装了一个方法用作路由切换的数据请求

  11.    }

  12. }

eventMixin

源码如下:

  1. export function eventMixin (proto) {        // 同renderMixin的proto所述

  2.  proto.$resetEvents = function () {        // 滚动到当前路由中id='query.id'的dom块,并使sidebar的活动标签实时响应,获取nav活动标签

  3.    scrollIntoView(this.route.query.id)

  4.    sidebar.getAndActive(this.router, 'nav')

  5.  }

  6. }

initGlobalAPI

源码如下:

  1. export default function () {

  2.  window.Docsify = { util, dom, get, slugify }

  3.  window.DocsifyCompiler = Compiler

  4.  window.marked = marked

  5.  window.Prism = prism

  6. }

这里导出一个方法,里面主要暴露了四个全局对象,方便提供使用。

  • window.Docsify 对象包含了 工具类方法-> util 、 dom操作方法->dom、  请求 markdown内容方法-> get 、 缓存处理方法->slugify

  • window.DocsifyCompiler 为构造函数生成的对象,是对开源项目marked做了一些扩展性,自定义了一些如前文所述的标签编译方法。

  • window.marked 即如上所述的marked对象

  • window.Prism 即开源项目prism,一个轻量级,强大,优雅的语法高亮库。

最后

Docsify好用之处就在于其提供了丰富的可扩展性,用户可以做更多想做的事情。除此之外,Docsify还提供了一些实用的功能(也可查阅Docsify官方文档):

  • 可将文档站点部署到Github Pages或远程服务器,若要配置到远程服务器的话且在serve模式下,可在serve手动关掉livereload,因为它会单独开启一个端口服务来监控文件更改。

  • vue开发者的福利——可在markdown写vue代码,来演示vue的Demo

  • PWA离线功能,虽然还没试过此功能,但感觉会瞬间让站点高大上起来。

  • 服务端渲染(SSR),主要依赖Docsify的 docsify -server -renderer模块,官方示例Node直出-Docsify。

若本文内容有不足或需要改正的地方,欢迎在评论区拍砖!