自实现 vuepress 效果图如下:
1. 全局安装 vuepress
npm install -g vuepress
2. 运行编写好的 docs 文件,编译后的浏览器显示文档网页
vuepress dev docs
3. 将编写好的 docs
md文档文件 build 打包成静态 html
网页
vuepress build docs
接下来教你如何自实现 uabpress ,并实现以上功能!
1.实现npm全局安装 uabpress 库并且 命令行 可执行
1. 首先我们来实现自己编写一个库,发布到npm后 可通过npm i 自己库名
安装
//创建一个项目名文件夹,在文件夹内初始化这个项目
npm init -y // 项目初始化
npm adduser //添加你的 npm 账号 目的:指定发布库的账号
npm publish //将当前库发布到 npm 内 (开发完成这个库后通过这个命名发布)
发布后过一会儿即可去 npm
内查看, 这是我发布后的库: uabpress
如果 npm
你的账号的 packages
内以生成该库即可通过npm install -g 你的库名
安装了
2. 全局安装库后,直接通过库名在cmd中运行你的库
这里我们需要用到一个命令行插件 commander, 下面是commander的使用方法
commander 的功能很简单配置如下:
const { program } = require('commander') //生成一个获取命令行输入内容的全局对象
program.version('1.0.0') //设置你的库当前版本
//下面就是通过 获取命令行输入内容 做一系列操作 运行对应执行文件等
这是 uabpress
内获取命令行输入内容进行的操作: github.com/uabjs/uabpress/blob/main/bin/index.js
那为什么 uabpress dev docs
会首先运行 bin/index.js
文件呢? 见 package.json
{
"name": "uabpress",
"version": "1.0.0",
"description": "基于 Vue3.0 SSR 的一个快速高效的 Markdown 网站制作工具",
"main": "index.js",
"bin": { //npm全局安装库会将package.json内的bin内容自动设置成电脑全局变量
"uabpress": "./bin/index.js" //这一行的目的就是向你的电脑全局一个uabpress的环境变量
},
....
这样下来我们全局安装uabpress:npm i -g uabpress
后, 在命令行中就可以通过uabpress -V
命令来查看版本号,或者uabpress dev docs
运行对应文档文件
2.实现vuepress dev docs
功能
可见当执行vuepress dev docs
后就自动打开了一个浏览器窗口,运行出了通过docs文件夹编译成的文档网页,这里是项目中最核心的环节,详情如下:
1. 开启一个 koa
服务默认开启端口为3000端口
对应项目文件 45行
app.start(options.port, () => {
console.log('编译的docs目录: ' + path.resolve(options.sourceDir))
})
2. 静态文件加载 如:image、css、js
对应项目文件 15行
app.use(async (ctx, next) => {
if (ctx.url.startsWith('/assets')) {
try {
const buffer = fs.readFileSync(path.resolve(__dirname, './' + ctx.url))
ctx.type = path.extname(ctx.url).slice(1)
ctx.body = buffer
} catch(e) {
ctx.body = ''
}
} else {
await next()
}
})
3. 通过docs文件夹路径 获取该文件夹所有文件路径
对应项目文件 12行
const glob = require('glob')
//sourceDir 是docs文件夹路径
function getFolder(sourceDir) {
return glob.sync(path.join(sourceDir, '/**/*.md'), { absolute: false })
.filter(v => v.indexOf('node_modules') === -1 )
.map(v => path.relative(sourceDir, v))
}
function createMiddleware (options) {
return async (ctx, next) => {
ctx.menu = getFolder(options.sourceDir) //获取文件名菜单 返回路径数组
await next()
}
}
4. 通过路径判断是否 新增、更新、删除 该fileNode(文件节点)
通过ctx.menu
路径判断是否 新增、更新、删除 该fileNode节点,从而生成一棵树在nodes
数组上的文件结构树
对应项目文件 27行
async patch(filePaths) {
const treeFlags = this.treekey()
const newFiles = {}
filePaths.forEach(filePath => {
newFiles[this.formatFilePath(filePath)] = null
})
Object.keys(this.nodes)
.filter(filePaths => filePaths.indexOf(treeFlags) === -1)
.forEach(async (filePath) => {
if (filePath in newFiles) {
await this.updateFile(filePath)
delete newFiles[filePath]
} else {
await this.removeFile(filePath)
}
})
//剩下的 newFiles 则是新增的文件
Object.keys(newFiles).forEach(async (filePath) => {
await this.addFile(filePath)
})
}
5. 以docs文件路径 编译文档结构树的具体实现如下
对应项目文件 3行
const Provider = require('./Provider')
module.exports = function () {
const provider = new Provider()
Array.from([
require('./middleware/title'), // 解析标题
require('./middleware/prefix'), // 标题层级
require('./middleware/breadcrumb'), // 计算面包屑
require('./middleware/autoNumber'), // 自动生成序号
require('./middleware/marked'), // markdown转html
require('./middleware/themes') // 添加样式
]).forEach(middleware => {
provider.useMiddleware(middleware)
})
return provider
}
/**
* root: TreeNode { path:'', children:[], parent: [TreeNode] },
* nodes 内有两种形式: 标题集, 文件集
* nodes: {
* '标题集/?': {
* path: 'level1B/levelB',
children: [ [FileNode], ... ],
parent: TreeNode { path: 'level1B', children: [Array], parent: [TreeNode] }
* }
* '标题集/文件集.md': FileNode {
resolvePath: [Function],
path: '文件集.md',
isFileNode: true,
parent: [TreeNode],
body: '11111111111',
lastModified: 1606828666908,
title: '文件集.md',
prefix: '',
breadcrumb: [Object],
catalogs: [],
html: '<p>11111111111</p>\n'+ ...<h1~3>,
themes: [Array],
getTheme: [Function]
},
...
},
middlewares: [],
resolvePath: fn
*/
6. 获取浏览器url 查找文档结构树中对应页面 ssr渲染
koa
获取浏览器请求时的url
,通过 url
我们可以找到nodes
节点树上对应该文件的fileNode
文件节点, 获取该文件节点里面编译好的html内容,在通过vue3
, ssr
生成网页html,传入ctx.body内返回给前端
对应项目文件 14行
const Vue = require('vue') //这是vue3
const compilerSsr = require('@vue/compiler-ssr') //将vue文件template模板编译成 render 方法
const compilerSfc = require('@vue/compiler-sfc') // 解析vue文件的内容
const serverRenderer = require('@vue/server-renderer') //将createApp的实例转换成html字符串
const createRender = sfcPath => {
sfcPath = sfcPath ? sfcPath : path.resolve(__dirname, '../template/App.vue')
const { descriptor } = compilerSfc.parse(fs.readFileSync(sfcPath, 'utf-8'))
const render = compilerSsr.compile(descriptor.template.content).code
return async (data) => {
const app = Vue.createApp({
ssrRender: new Function('require', render)(require),
data: () => data
})
return serverRenderer.renderToString(app) //将编译好的html字符串传入ctx.body相应出去
}
}
const renderMarkdown = async ({ reqFile, template, provider, options }) => {
const skin = options.theme || '默认皮肤'
// 获取 menu 菜单数据及其页面数据
const data = {
menu: provider.toArray(fileNode => ({
path: fileNode.path,
name: fileNode.title,
prefix: fileNode.prefix || ''
})),
skinPath: '',
breadcrumb: null,
catalogs: [],
markdown: ''
}
//解析该路径页面的 data 数据
await provider.getItem(reqFile, fileNode => {
if (!fileNode) {
data.markdown = `<h3>${reqFile} 不存在<h3>`
} else {
// console.log("fileNode----", fileNode);
data.breadcrumb = fileNode.breadcrumb.html
data.catalogs = fileNode.catalogs
data.skinPath = fileNode.getTheme(skin).path
data.markdown = [
fileNode.html
].join('')
}
})
return await createRender(template)(data)
}
7. 通过watch
监听文件变化利用socket.io
实现热加载
对应项目文件 13行
const watch = require('watch')
const io = require('socket.io')
//options.sourceDir就是docs文件夹路径,目的:监听该路径内的文件修改时间如果改变则触发页面刷新
watch.watchTree(options.sourceDir, changePath => {
progress.init() //刷新的进度条 初始化
progress.step() //刷新的进度条 加载动画
socket.emit('reload', changePath) //docs 改变重新加载
})
app.use(async (ctx, next) => {
await next() //当所有中间件都执行完成后给最后生成的body添加socket监听
if (ctx.type === 'text/html') {
ctx.body = `
<!DOCTYPE html>
<html>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io()
socket.on("reload", function(changePath) {
window.location.reload()
})
</script>
${ctx.body}
</html>
`
}
})
最后一步就是通过 open 打开浏览器以及对应地址
对应项目文件 36行
const open = require("open")
open(`http://localhost:${port}`)
到此全部工序就完成了!
3.实现vuepress build docs
打包功能
命令执行在:
对应项目文件 43行
// 2. 解析到指令为 build 则执行下面操作
program
.command('build')
.description('编译页面文件(生成html)')
.option('-t, --theme [theme]', 'Markdown样式,可选 default、techo')
.option('-o, --output [output]', '输出目录')
.action(async (options) => {
console.log('')
// 打包生成静态html页面
await build({
theme: options.theme || 'default', //打包后的html样式
root: path.resolve(options.args.length > 0 ? options.args[0] : '.'), //打包路径
output: path.resolve(options.output || 'dist') //默认输出到dist文件夹
})
process.exit()
})
// 1. 将命令传递给 program 解析
program.parse(process.argv) // process.argv是命令行输入的命令
核心打包实现:
对应项目文件 66行