基于 vite2 + Vue3 写一个在线帮助文档工具 - 金色海洋(jyk)

vue

基于 vite2 + Vue3 写一个在线帮助文档工具

2022-03-11 18:40 

金色海洋(jyk) 

阅读(224) 

评论(0) 

编辑 

收藏 

举报

提起帮助文档,想必大家都会想到 VuePress等,我也体验了一下,但是感觉和我的思路不太一样,我希望的是那种可以直接在线编辑文档,然后无需编译就可以直接发布的方式,另外可以在线写(修改)代码并且运行的效果。

VuePress 是“静态网站生成器”,需要我们自行编写文档,然后交给VuePress变成网站,VuePress 并没有提供编写环境,我知道有很多编写 Markdown 的方式,但是我还是喜欢编写、浏览合为“一体”的方式。

似乎没有,那么 —— 自己动手丰衣足食吧,开干!

技术栈

  • vite: ^2.7.0
  • vue: ^3.2.23
  • axios: ^0.25.0 获取json格式的配置和文档
  • element-plus: ^2.0.2 UI库
  • nf-ui-elp": ^0.1.0 二次封装的UI库
  • @element-plus/icons-vue: ^0.2.4 图标
  • @kangc/v-md-editor:"^2.3.13 md 编辑器
  • vite-plugin-prismjs: ^0.0.8 代码高亮
  • nf-state": ^0.2.4 状态管理
  • nf-web-storage": ^0.2.3 访问 indexedDB

建立库项目(@naturefw/press-edit)实现文档的编写、浏览功能

首先使用 vite2 建立一个 Vue3 的项目:

  • 安装 elementPlus 实现页面效果;
  • 安装 v-md-editor 实现 Markdown 的编辑和显示;
  • 安装 @naturefw/storage 操作 indexedDB ,实现帮助文档的存储;
  • 安装 @naturefw/nf-state 实现状态管理;
  • 安装axios 用于加载 json文件,实现导入功能。
  • 用node写一个后端API,实现写入json文件的功能。

注意:库项目需要安装以上插件,帮助文档项目只需要安装 @naturefw/press-edit 即可。

基本功能就是这样,心急的可以先看在线演示和源码。

  1. 在线演示:https://nfpress.gitee.io/nf-press-edit/
  2. 源码:https://gitee.com/nfpress/nf-press-edit

两个状态:编辑和浏览

一开始做了两个项目,分别实现编辑文档和显示文档的功能,但是后来发现,内部代码大部分是相同的,维护的时候有点麻烦,所以改为在编辑文档的项目里加入“浏览”的状态,然后设置切换的功能,这样便于内部代码的维护,以后成熟了可能会分为两个单独的项目。

编辑状态的功能

  • 菜单维护
  • 文档维护
  • 文档展示
  • 导入导出
  • 在线编写/执行代码

我喜欢在线编辑的方式,这样更省心,于是我用 el-menu 实现导航和左侧的菜单,然后加上了维护功能。

使用 v-md-editor 实现 Markdown 的编辑和显示。

然后用node写了一个后端API,实现保存 json文件的功能,这样就完美了。

浏览状态的功能

  • 导航
  • 菜单
  • 文档展示
  • 执行代码

就是在编辑状态的功能的基础上,去掉一些功能。或者其实可以反过来思考。

实现导航

首先参考 VuePress 设置一个json文件,用于加载和保存网站信息、导航信息。

/public/docs/.nfpress/project.json

{

"projectId": "1000",

"title": "nf-press-edit !",

"description": "这是一个在线编辑、展示文档的小工具",

"navi": [

{

"naviId": "1010",

"text": "指南",

"link": "menu"

},

{

"naviId": "1020",

"text": "组件",

"link": "menu"

},

{

"naviId": "1380",

"text": "Gitee",

"link": "https://gitee.com/nfpress/nf-press-edit"

},

{

"naviId": "1390",

"text": "在线演示",

"link": "https://nfpress.gitee.io/nf-press-edit/"

},

{

"naviId": "1395",

"text": "我要提意见",

"link": "https://gitee.com/nfpress/nf-press-edit/issues"

}

]

}

  • projectId:项目ID,可以用于区分不同的帮助文档项目。
  • navi: 存放导航项。
  • naviId: 关联到菜单。
  • text: 导航上显示的文字。
  • link: 连接方式或链接地址。menu:表示要打开对应的菜单;URL:在新页面里打开连接。

然后做一个组件,用 el-menu 绑定数据渲染出来即可实现导航效果。

/lib/navi/navi.vue

  <el-menu

:default-active="activeIndex2"

class="el-menu-demo"

mode="horizontal"

v-bind="$attrs"

:background-color="backgroundColor"

@select="handleSelect"

>

<el-menu-item

v-for="(item, index) in naviList"

:key="index"

:index="item.naviId"

>

{{item.text}}

</el-menu-item>

</el-menu>

可以是多级的导航,暂时没有实现在线维护功能。

  import { ref } from \'vue\'

import { ElMenu, ElMenuItem } from \'element-plus\'

import { state } from \'@naturefw/nf-state\'

const props = defineProps({

\'background-color\': { // 默认背景色

type: String,

default: \'#ece5d9\'

},

itemProps: Object

})

// 获取状态和导航内容

const { current, naviList } = state

// 激活第一个导航项

const activeIndex2 = ref(naviList[0].naviId)

const handleSelect = (key, keyPath) => {

const navi = naviList.find((item) => item.naviId === key)

if (navi.link === \'menu\') {

// 打开菜单

current.naviId = key

} else {

// 打开连接

window.open(navi.link, \'_blank\')

}

}

  • @naturefw/nf-state

    自己写的一个轻量级状态管理,可以当做大号 reactive 来使用,通过状态管理加载 project.json 然后绑定渲染。

  • naviList

    导航列表,由状态管理加载。

  • current

    当前激活的各种信息,比如“current.naviId”表示激活的导航项。

实现菜单

和导航类似,只是需要增加两个功能:n级分组和维护。

首先参考 VuePress 设置一个json文件,保存菜单信息。

/public/docs/.nfpress/menu.json

[

{

"naviId": "1010",

"menus": [

{

"menuId": "110100",

"text": "介绍",

"description": "描述",

"icon": "FolderOpened",

"children": []

},

{

"menuId": "111100",

"text": "快速上手",

"description": "描述",

"icon": "FolderOpened",

"children": [

{

"menuId": 111120,

"text": "编辑文档项目",

"description": "",

"icon": "UserFilled",

"children": []

},

{

"menuId": 111130,

"text": "展示文档项目",

"description": "",

"icon": "UserFilled"

}

]

}

],

"ver": 1.6

},

{

"naviId": "1020",

"menus": [

{

"menuId": "21000",

"text": "导航(docNavi)",

"description": "描述",

"icon": "Star",

"children": []

}

],

"ver": 1.5

}

]

  • naviId: 关联导航项ID,可以是数字,也可以是其他字符。需要和导航项ID对应。
  • menus: 导航项对应的菜单项集合。
  • menuId: 菜单项ID,关联一个文档,可以是数字或者英文。
  • text: 菜单项名称。
  • description: 描述,考虑以后用于查询。
  • icon: 菜单使用的图标名称。
  • children: 子菜单项目,没有的话可以去掉。
  • ver: 版本号,便于更新文档。

然后用 el-menu 绑定数据渲染,因为要实现n级分组,所以做一个递归组件实现n级菜单的效果。

实现n级分组菜单

做一个递归组件实现n级分组的功能:

/lib/menu/menu-sub-edit.vue

  <template v-for="(item, index) in subMenu">

<!--树枝-->

<template v-if="item.children && item.children.length > 0">

<el-sub-menu

:key="item.menuId + \'_\' + index"

:index="item.menuId"

style="vertical-align: middle;"

>

<template #title>

<div style="display:inline;width: 100%;">

<component

:is="$icon[item.icon]"

style="width: 1.5em; height: 1.5em; margin-right: 8px;vertical-align: middle;"

>

</component>

<span>{{item.text}}</span>

</div>

</template>

<!--递归子菜单-->

<my-sub-menu2

:subMenu="item.children"

:dialogAddInfo="dialogAddInfo"

:dialogModInfo="dialogModInfo"

/>

</el-sub-menu>

</template>

<!--树叶-->

<el-menu-item v-else

:index="item.menuId"

:key="item.menuId + \'son_\' + index"

>

<template #title>

<div style="display:inline;width: 100%;">

<span style="float: left;">

<component

:is="$icon[item.icon]"

style="width: 1.5em; height: 1.5em; margin-right: 8px;vertical-align: middle;"

>

</component>

<span >{{item.text}}</span>

</span>

</div>

</template>

</el-menu-item>

</template>

  import { ElMenuItem, ElSubMenu } from \'element-plus\'

// 展示子菜单 - 递归

import mySubMenu2 from \'./menu-sub.vue\'

const props = defineProps({

subMenu: Array, // 要显示的菜单,可以n级

dialogAddInfo: Object, // 添加菜单

dialogModInfo: Object // 修改菜单

})

  • subMenu 要显示的子菜单项
  • dialogAddInfo 添加菜单的信息
  • dialogModInfo 修改菜单的信息

实现菜单的维护功能

这个就比较简单了,做个表单实现菜单的增删改即可,篇幅有限跳过。

实现 Markdown 的编辑

使用 v-md-editor 实现 Markdown 的编辑和展示,首先该插件非常好用,其次支持VuePress的主题。

建立 /lib/md/md-edit.vue 实现编辑 Markdown 的功能:

  <v-md-editor

:toolbar="toolbar"

left-toolbar="undo redo clear | tip emoji code | h bold italic strikethrough quote | ul ol table hr | link image | save | customToolbar"

:include-level="[1, 2, 3, 4]"

v-model="current.docInfo.md"

:height="editHeight + \'px\'"

@save="mySave"

>

</v-md-editor>

  import { watch,ref  } from \'vue\'

import { ElMessage, ElRadioGroup, ElRadioButton } from \'element-plus\'

import mdController from \'../service/md.js\'

// 状态

import { state } from \'@naturefw/nf-state\'

// 获取当前激活的信息

const current = state.current

// 文档的加载和保存

const { loadDocById, saveDoc } = mdController()

// 可见的高度

const editHeight = document.documentElement.clientHeight - 200

// 单击 保存 按钮,实现保存功能

const mySave = (text, html) => {

saveDoc(current)

}

// 定时保存

let timeout = null

let isSaved = true

const timeSave = () => {

if (isSaved) {

// 保存过了,重新计时

isSaved = false

} else {

return // 有计时,退出

}

timeout = setTimeout(() => {

// 保存文档

saveDoc(current).then(() => {

ElMessage({

message: \'自动保存文档成功!\',

type: \'success\',

})

})

isSaved = true

}, 10000)

}

// 定时保存文档

watch(() => current.docInfo.md, () => {

timeSave()

})

// 根据激活的菜单项,加载对应的文档

watch( () => current.menuId, async (id) => {

const ver = current.ver

loadDocById(id, ver).then((res) => {

// 找到了文档

Object.assign(current.docInfo, res)

}).catch((res) => {

// 没有文档

Object.assign(current.docInfo, res)

})

})

  • mdController 实现文档的增删改查的controller
  • timeSave 定时保存文档,避免忘记点保存按钮

是不是挺简单的。

实现在线编写代码并且运行的功能

因为是基于Vue3建立的项目,而且也是为了写vue3相关的帮助文档,那么就有一个很实用的要求:在线写代码并且可以运行。

个人感觉这个功能还是很实用的,我知道有第三方网站提供了这种功能,但是网速有点慢,另外有一种大炮打蚊子的感觉,我只需要实现简单的代码演示。

于是我基于 vue 的 defineAsyncComponent 写了一个简单版的在线编写代码且运行的功能:

/lib/runCode/run.vue

  <div style="padding: 5px; border: 1px solid #ccc!important;">

<async-comp></async-comp>

</div>

import {

defineAsyncComponent,

ref, reactive,...

// 其他常用的vue内置指令

} from \'vue\'

// 使用 eval编译js代码

const mysetup = `

(function setup () {

{{code}}

})

`

// 通过属性传入需要运行的代码和模板

const props = defineProps({

code: {

type: Object,

default: () => {

return {

js: \'\',

template: \'\',

style: \'\'

}

}

}

})

const code = props.code

// 使用 defineAsyncComponent 让代码运行起来

const AsyncComp = defineAsyncComponent(

() => new Promise((resolve, reject) => {

resolve({

template: code.template, // 设置模板

style: [code.style], // 大概是样式设置,但是好像没啥效果

setup: (props, ctx) => {

const tmpJs = code.js // 获取js代码

let fun = null // 转换后的函数

try {

if (tmpJs)

fun = eval(mysetup.replace(\'{{code}}\', tmpJs)) // 用 eval 把 字符串 变成 函数

} catch (error) {

console.error(\'转换出现异常:\', error)

}

const re = typeof fun === \'function\' ? fun : () => {}

return {

...re(props, ctx) // 运行函数,解构返回对象

}

}

})

})

)

  • defineAsyncComponent

    实用 defineAsyncComponent 加载组件,需要设置三个部分:模板、setup和style。

  • template: 字符串形式,可以直接传入

  • setup: js代码,可以用eval的方式进行动态编译。

  • style: 可以设置样式。

这样即可让在线编写的代码运行起来,当然功能有限,只能用于一些简单的代码演示。

导出

以上这些功能都是基于 indexedDB 进行的,想要发布的话,需要先导出为json文件。

因为浏览器里不能直接写文件,所以需要使用折中的方式:

  • 复制粘贴
  • 下载
  • 导出

复制粘贴

这个简单,用文本域显示json即可。

下载

使用 chrome 浏览器提供的下载功能下载文件。

  const uri = \'data:text/json;charset=utf-8,\ufeff\' + encodeURIComponent(show.navi)

//通过创建a标签实现

var link = document.createElement("a")

link.href = uri

//对下载的文件命名

link.download = fileName

document.body.appendChild(link)

link.click()

document.body.removeChild(link)

以上介绍的是内部原理,如果只是想简单使用的话,可以跳过,直接看下面的介绍。

用后端写文件

以上两种都不太方便,于是用node做了个简单的后端API,用于实现写入json文件的功能。

代码放在了 api文件夹里,可以使用 yarn api运行。当然需要在 package.json 里做一下设置。

  "scripts": {

"dev": "vite",

"build": "vite build --mode project",

"lib": "vite build --mode lib",

"serve": "vite preview",

"api": "node api/server.js"

},

实现一个帮助文档的项目

上面介绍的是库项目的基本原理,我们要做帮助文档的时候,并不需要那么复杂。

使用 vite2 建立一个vue3的项目,然后安装 @naturefw/press-edit,使用提供的组件即可方便的实现。

main.js

首先需要在 main.js 里面做一些设置。

import { createApp } from \'vue\'

import App from \'./App.vue\'

// 设置 axios 的 baseUrl

const baseUrl = (document.location.host.includes(\'.gitee.io\')) ?

\'/doc-ui-core/\' : \'/\'

// 轻量级状态

// 设置 indexedDB 数据库,存放文档的各种信息。

import { setupIndexedDB, setupStore } from \'@naturefw/press-edit\'

// 初始化 indexedDB 数据库

setupIndexedDB(baseUrl)

// UI库

import ElementPlus from \'element-plus\'

// import \'element-plus/lib/theme-chalk/index.css\'

// import \'dayjs/locale/zh-cn\'

import zhCn from \'element-plus/es/locale/lang/zh-cn\'

// 二次封装

import { nfElementPlus } from \'@naturefw/ui-elp\'

// 设置icon

import installIcon from \'./icon/index.js\'

// 设置 Markdown 的配置函数

import setMarkDown from \'./main-md.js\'

// 主题

import vuepressTheme from \'@kangc/v-md-editor/lib/theme/vuepress.js\'

const {

VueMarkdownEditor, // Markdown 的编辑器

VMdPreview // Markdown 的浏览器

} = setMarkDown(vuepressTheme)

const app = createApp(App)

app.config.globalProperties.$ELEMENT = {

locale: zhCn,

size: \'small\'

}

app.use(setupStore) // 状态管理

.use(nfElementPlus) // 二次封装的组件

.use(installIcon) // 注册全局图标

.use(ElementPlus, { locale: zhCn, size: \'small\' }) // UI库

.use(VueMarkdownEditor) // markDown编辑器

.use(VMdPreview) // markDown 显示

.mount(\'#app\')

  • baseUrl: 根据发布平台的情况进行设置,比如这里需要设置为:“/doc-ui-core/”

  • setupIndexedDB: 初始化 indexedDB 数据库

  • setupStore: 设置状态

  • element-plus:element-plus 可以不挂载,但是css需要 import 进来,这里采用CDN的方式引入。

  • nfElementPlus: 二次封装的组件,便于实现增删改查。

  • setMarkDown: 加载 v-md-editor ,以及需要的插件。

  • vuepressTheme: 设置主题。

设置 Markdown

因为 v-md-editor 相关设置比较多,所以设置了一个单独文件进行管理:

/src/main-md.js

// Markdown 编辑器

import VueMarkdownEditor from \'@kangc/v-md-editor\'

import \'@kangc/v-md-editor/lib/style/base-editor.css\'

// 在这里引入,不被识别?

// import vuepressTheme from \'@kangc/v-md-editor/lib/theme/vuepress.js\'

import \'@kangc/v-md-editor/lib/theme/style/vuepress.css\'

// 代码高亮

import Prism from \'prismjs\'

// emoji

import createEmojiPlugin from \'@kangc/v-md-editor/lib/plugins/emoji/index\'

import \'@kangc/v-md-editor/lib/plugins/emoji/emoji.css\'

// 流程图

// import createMermaidPlugin from \'@kangc/v-md-editor/lib/plugins/mermaid/cdn\'

// import \'@kangc/v-md-editor/lib/plugins/mermaid/mermaid.css\'

// todoList

import createTodoListPlugin from \'@kangc/v-md-editor/lib/plugins/todo-list/index\'

import \'@kangc/v-md-editor/lib/plugins/todo-list/todo-list.css\'

// 代码行号

import createLineNumbertPlugin from \'@kangc/v-md-editor/lib/plugins/line-number/index\';

// 高亮代码行

import createHighlightLinesPlugin from \'@kangc/v-md-editor/lib/plugins/highlight-lines/index\'

import \'@kangc/v-md-editor/lib/plugins/highlight-lines/highlight-lines.css\'

// 复制代码

import createCopyCodePlugin from \'@kangc/v-md-editor/lib/plugins/copy-code/index\'

import \'@kangc/v-md-editor/lib/plugins/copy-code/copy-code.css\'

// markdown 显示器

import VMdPreview from \'@kangc/v-md-editor/lib/preview\'

// import \'@kangc/v-md-editor/lib/style/preview.css\'

/**

* 设置 Markdown 编辑器 和浏览器

* @param {*} vuepressTheme

* @returns

*/

export default function setMarkDown (vuepressTheme) {

// 设置 vuePress 主题

VueMarkdownEditor.use(vuepressTheme,

{

Prism,

extend(md) {

// md为 markdown-it 实例,可以在此处进行修改配置,并使用 plugin 进行语法扩展

// md.set(option).use(plugin);

},

}

)

// 预览

VMdPreview.use(vuepressTheme,

{

Prism,

extend(md) {

// md为 markdown-it 实例,可以在此处进行修改配置,并使用 plugin 进行语法扩展

// md.set(option).use(plugin);

},

}

)

// emoji

VueMarkdownEditor.use(createEmojiPlugin())

// 流程图

// VueMarkdownEditor.use(createMermaidPlugin())

// todoList

VueMarkdownEditor.use(createTodoListPlugin())

// 代码行号

VueMarkdownEditor.use(createLineNumbertPlugin())

// 高亮代码行

VueMarkdownEditor.use(createHighlightLinesPlugin())

// 复制代码

VueMarkdownEditor.use(createCopyCodePlugin())

// 预览的插件

VMdPreview.use(createEmojiPlugin())

VMdPreview.use(createTodoListPlugin())

VMdPreview.use(createLineNumbertPlugin())

VMdPreview.use(createHighlightLinesPlugin())

VMdPreview.use(createCopyCodePlugin())

return {

VueMarkdownEditor,

VMdPreview

}

}

不多介绍了,可以根据需要选择插件。

布局

在App.vue文件里面进行整体布局

  <el-container>

<el-header>

<!--导航-->

<div style="float: left;">

<!--写网站logo、标题等-->

<h1>nf-press</h1>

</div>

<div style="float: right;min-width: 100px;height: 60px;padding-top: 13px;">

<!--写网站logo、标题等-->

<el-switch v-model="$state.current.isView" v-bind="itemProps"></el-switch>

</div>

<div style="float: right;min-width: 600px;height: 60px;">

<!--网站导航-->

<doc-navi ></doc-navi>

</div>

</el-header>

<el-container>

<!--左侧边栏-->

<el-aside width="330px">

<!--菜单-->

<doc-menu ></doc-menu>

</el-aside>

<el-main>

<!--文档区域-->

<component

:is="docControl[$state.current.isView]"

/>

</el-main>

</el-container>

</el-container>

  import { reactive, defineAsyncComponent } from \'vue\'

import { ElHeader, ElContainer ,ElAside, ElMain } from \'element-plus\'

import { docMenu, docNavi, config } from \'@naturefw/press-edit\' // 菜单 导航

import docView from \'./views/doc.vue\' // 显示文档

// 加载菜单子控件

const docControl = {

true: docView,

false: defineAsyncComponent(() => import(\'./views/main.vue\')) // 修改文档

}

const itemProps = reactive({

\'inline-prompt\': true,

\'active-text\': \'看\',

\'inactive-text\': \'写\',

\'active-color\': \'#378FEB\',

\'inactive-color\': \'#EA9712\'

})

  • $state:全局状态,$state.current.isView 设置是否是浏览状态。
  • doc-navi:导航组件
  • doc-menu:菜单组件
  • docControl:根据状态选择加载显示组件或者编辑组件的字典。

这种方式虽然有点麻烦,但是比较灵活,可以根据需要进行各种灵活设置,比如添加版权信息、备案信息、广告等内容。

导航、菜单、编辑和浏览

直接使用组件实现,比较简单不搬运了,直接看源码即可。

打包发布与版本管理

需要打包的情况分为两种:第一次打包、修改代码(非在线编辑的代码)后打包。

如果只是文档内容有变化的话,只需要直接上传json文件即可,不需要再次打包。

内置了一个简单的版本管理功能,可以通过 ver.json文件里的版本号实现更新功能。

源码

https://gitee.com/nfpress/nf-press-edit

在线演示

https://nfpress.gitee.io/nf-press-edit/

demo

https://gitee.com/nfpress/doc-ui-elp

以上是 基于 vite2 + Vue3 写一个在线帮助文档工具 - 金色海洋(jyk) 的全部内容, 来源链接: utcz.com/z/380955.html

回到顶部