信息发布→ 登录 注册 退出

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

发布时间:2026-01-11

点击量:
目录
  • 技术栈
  • 建立库项目(@naturefw/press-edit)实现文档的编写、浏览功能
    • 编辑状态的功能
    • 浏览状态的功能
  • 实现导航
    • 实现菜单
      • 实现n级分组菜单
      • 实现菜单的维护功能
    • 实现 Markdown 的编辑
      • 实现在线编写代码并且运行的功能
        • 导出
          • 复制粘贴
          • 下载
          • 用后端写文件
        • 实现一个帮助文档的项目
          • main.js
          • 设置 Markdown
          • 布局
          • 导航、菜单、编辑和浏览
        • 打包发布与版本管理

          提起帮助文档,想必大家都会想到 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 即可。

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

          在线演示

          源码

          编辑页面

          浏览页面

          两个状态:编辑和浏览

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

          编辑状态的功能

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

          我喜欢在线编辑的方式,这样更省心,于是我用 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文件里的版本号实现更新功能。

          源码

          在线演示

          demo

          以上就是基于vite2+Vue3编写一个在线帮助文档工具的详细内容,更多关于vite2 Vue3帮助文档的资料请关注其它相关文章!

          在线客服
          服务热线

          服务热线

          4008888355

          微信咨询
          二维码
          返回顶部
          ×二维码

          截屏,微信识别二维码

          打开微信

          微信号已复制,请打开微信添加咨询详情!