前言
VuePress是一个Vue2驱动的静态网站生成器,专注于快速搭建文档类站点与博客。
设计思路
MD to HTML
md 文件是工程师们🦍常用的文本格式文件,在使用VuePress构建的项目中约定用此文件类型写作。VuePress 编译打包工具用的Webpack,为了实现md文件的解析与模块转化,Vuepress实现了一个 Webpack Loader —— markdown-loader,它能够通过正则匹配字符串解析md文件内容,并把一个个md文件解析转换为Vue SFC,因此可以直接在md文件中使用template/script/style标签,再配合vue-loader就可以把一个个md文件转换为HTML页面
路由
VuePress 的页面路由约定由目录结构关系生成。通常我们的网站内容文件集中在一个名为 docs的文件夹中,位于 docs/hello.md 的文件所对应的路由就是 /hello,docs/zh/hello.md所对应的就是 /zh/hello
主题与插件
VuePress 维护了一个 APP 实例对象,并作为 context传递给了themeAPI与pluginAPI,主题与插件的工作原理就是通过约定的配置格式修改context状态
SSG
SSG(静态网站生成) 是 SSR(服务端渲染) 的一个变种。服务端渲染指的就是将HTML页面的生成下放到服务端,以支持更好的SEO以及更快的内容到达时间,而SSG就是在SSR的基础上每个页面只生成一次HTML(因为咱搭建的是文档类站点,默认所有IP访问此站点所呈现的内容是一摸一样的,不存在HTML内容的动态变化)
约定式配置
VuePress 专注于快速搭建文档类网站,因此在业务需求上不需要过于灵活的配置支持,故而可将部分配置抽象提取到VuePress本身实现,让用户专心于文档内容的编写,如上文所说的路由,比较典型的还有文档目录结构与主题目录结构
实现思路
cli
Vuepress 选用cac来支持命令行指令的解析与执行,详情可见GitHub,核心源码部分如下:
module.exports = function (cli, options) {
// 注册 dev cli
cli
.command(`dev [targetDir]`, 'start development server')
.option('-p, --port <port>', 'use specified port (default: 8080)')
.option('-t, --temp <temp>', 'set the directory of the temporary file')
.option('-c, --cache [cache]', 'set the directory of cache')
.option('--host <host>', 'use specified host (default: 0.0.0.0)')
.option('--no-cache', 'clean the cache before build')
.option('--no-clear-screen', 'do not clear screen when dev server is ready')
.option('--debug', 'start development server in debug mode')
.option('--silent', 'start development server in silent mode')
.option('--open', 'open browser when ready')
.action((sourceDir = '.', commandOptions) => {
const { debug, silent } = commandOptions
logger.setOptions({ logLevel: silent ? 1 : debug ? 4 : 3 })
logger.debug('global_options', options)
logger.debug('dev_options', commandOptions)
env.setOptions({ isDebug: debug, isTest: process.env.NODE_ENV === 'test' })
wrapCommand(dev)({
sourceDir: path.resolve(sourceDir),
...options,
...commandOptions
})
})
}
编译打包
在接收到cli命令如 yarn dev:docs 后,VuePress 首先会执行固定的逻辑初始化站点配置,在初始化配置结束之后根据参数dev/build执行不同的Webpack编译流程
创建实例 APP
创建APP实例,重心在于确定编译打包环境以及TargetDir
isProd编译打包环境options站点上下文配置对象sourceDir文档站点内容目录,默认为 docsvuepressDir文档站点自定义配置目录,默认为 docs/.vuepresslibDir项目根目录,默认为 docs 上一级目录
module.exports = class App {
constructor (options = {}) {
// 运行环境
this.isProd = process.env.NODE_ENV === 'production'
// 初始化参数
this.options = options
// 文档目录,常为 docs
this.sourceDir = this.options.sourceDir || path.join(__dirname, 'docs.fallback')
logger.debug('sourceDir', this.sourceDir)
if (!fs.existsSync(this.sourceDir)) {
logger.warn(`Source directory doesn't exist: ${chalk.yellow(this.sourceDir)}`)
}
// 文档自定义设置目录 常为 docs/.vuepress
this.vuepressDir = path.resolve(this.sourceDir, '.vuepress')
// VuePress 核心模块路径
this.libDir = path.join(__dirname, '../')
}
}
初始化配置
在此阶段会初始化 themeApi 以及 pluginApi,并配置好渲染模板、全局布局组件、解析匹配docs文件夹下md文件
async process () {
// 读取加载 config 文件配置
await this.resolveConfigAndInitialize()
// 依据 base 调整 head 标签的 url
this.normalizeHeadTagUrls()
// 加载主题配置
this.themeAPI = loadTheme(this)
// 初始化配置 dev 及 build 渲染模板
this.resolveTemplates()
// 加载全局基础布局组件
this.resolveGlobalLayout()
// 初始化内联插件
this.applyInternalPlugins()
// 同步设置用户自定义插件
this.applyUserPlugins()
// 同步插件配置到相应的钩子函数
this.pluginAPI.initialize()
// 初始化 markdown-it 实例
this.markdown = createMarkdown(this)
// 解析 docs 目录下 md 文件结构
await this.resolvePages()
// 调用 additionalPages 钩子函数
await this.pluginAPI.applyAsyncOption('additionalPages', this)
await Promise.all(
this.pluginAPI.getOption('additionalPages').appliedValues.map(async (options) => {
// 添加页面
await this.addPage(options)
})
)
// 调用 ready 钩子函数
await this.pluginAPI.applyAsyncOption('ready')
await Promise.all([
// 调用 clientDynamicModules 钩子函数
this.pluginAPI.applyAsyncOption('clientDynamicModules', this),
// 调用 enhanceAppFiles 钩子函数
this.pluginAPI.applyAsyncOption('enhanceAppFiles', this),
// 调用 globalUIComponents 钩子函数
this.pluginAPI.applyAsyncOption('globalUIComponents', this)
])
}
Dev
调用 this.devProcess.process() 设置好HMR并初始化Webpack配置,然后选用 WebpackDevServer启动项目,DevProcess详情请看GitHub
async dev () {
// 创建 DevProcess 实例,初始化配置
this.devProcess = new DevProcess(this)
// 初始化 webpack 配置等以准备 launch dev server
await this.devProcess.process()
const error = await new Promise(resolve => {
try {
// HMR
this.devProcess
.on('fileChanged', ({ type, target }) => {
console.log(`Reload due to ${chalk.red(type)} ${chalk.cyan(path.relative(this.sourceDir, target))}`)
this.process()
})
// 创建 dev server
.createServer()
// 启动服务
.listen(resolve)
} catch (err) {
resolve(err)
}
})
if (error) {
throw error
}
return this
}
Build
检查并初始化Webpack配置(包括 clientConfig & serverConfig,因为采用SSG),BuildProcess 详情请看GtiHub
async build () {
// 创建 BuildProcess 实例
this.buildProcess = new BuildProcess(this)
// 初始化 webpack 客服端以及服务端配置等以准备 SSR
await this.buildProcess.process()
// SSR
await this.buildProcess.render()
return this
}
App 实例属性(build)
base: string部署站点基础路径buildProcess: BuildBuild 实例cacheDirectory: string缓存文件夹cacheIdentifer:string缓存标识符ClientConputedMixinConstructor: ClientComputedMixinClientComputedMixin 类cwd: string本地项目路径devTemplate: stringdev 环境渲染模板路径globalLayout: string全局基础布局组件路径isProd: boolean运行环境标识libDir: stringVuePress核心模块路径markdown: MarkdownItMarkdownIt实例options: object命令行执行结果对象outDir:string打包文件输出目录pages: array站点所有页面数据集合pluginAPI: PluginAPIPluginAPI实例siteConfig: object站点基础配置参数sourceDir: string站点文档路径ssrTemplate: stringSSR渲染模板tempPath: string临时数据文件路径themeAPI: ThemeAPI: ThemeAPI实例themeConfig: object主题配置参数vuepressDir: string站点自定义配置路径writeTemp: function输出临时文件内容的函数
相关技术
- Vue2 流行的渐进式JavaScript框架,封装了视图与数据状态的相互制约与影响逻辑,采用 SFC(单文件组件) 以简单的声明式开发有效降低初学者学习成本
- SSR(服务端渲染) 不同于SPA的一种前端解决方案,相较而言有更快的内容到达时间,并对SEO友好,但也加大了服务端的负载
- markdown-it 一个实现解析MD为HTML的工具
- Webpack 前端不能不懂的项目编译构建打包工具