乘风原创程序

  • 基于vite2+Vue3编写一个在线帮助文档工具
  • 2022/3/15 12:04:25
  • 提起帮助文档,想必大家都会想到 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帮助文档的资料请关注本教程网其它相关文章!