When it comes to help documents, everyone will think of VuePress and so on. I also tried it, but I didn’t think it was the same as me. What I wanted was a way that I could edit the documents online and publish them without compiling, and I could write (modify) the code online and run it.

VuePress is a “static website generator” that requires us to write our own documentation and hand it over to VuePress to turn it into a website. VuePress doesn’t provide a writing environment. I know there are many ways to write Markdown, but I prefer to write and browse all in one.

No, I don’t think so, so — feed yourself and get to work!

Technology stack

  • Vite: ^ 2.7.0
  • Vue: ^ 3.2.23
  • Axios: ^0.25.0 Gets the configuration and documentation in JSON format
  • Element – plus: ^ 2.0.2 UI library
  • Nf-ui-elp “: ^0.1.0 Repackaged UI library
  • @ element – plus/ICONS – vue: ^ 0.2.4 icon
  • @kangc/ V-mD-editor :”^2.3.13 MD editor
  • Viet-plugin-prismjs: ^0.0.8 code highlighting
  • Nf-state “: ^0.2.4 State management
  • Nf-web-storage “: ^0.2.3 Accessing indexedDB

Build a library project (@Naturefw/press-Edit) to achieve the document writing, browsing functions

First build a Vue3 project using Vite2:

  • Install elementPlus for page effects;
  • Install V-MD-Editor to edit and display Markdown;
  • Install @naturefw/storage operation indexedDB, implement help document storage;
  • Install @naturefw/ nF-state to implement state management;
  • Install AXIos to load JSON files for import.
  • Write a back-end API with Node to implement the function of writing JSON files.

Note: the library project requires the above plug-in to be installed. The help documentation project only needs to install @naturefw/ press-Edit.

Basic function is such, impatient can first see online demo and source code.

  • Nfpress.geitee. IO /nf-press-ed…

  • Source: gitee.com/nfpress/nf-…

  • Edit page

  • Browse the page

Two states: edit and browse

Do the two projects at the beginning, edit documents respectively and display the function of the document, but it was found that most of the internal code is the same, the maintenance time is a little trouble, so instead of in the project of edit documents to join the “browse” state, and then set the switch function, so to facilitate internal code maintenance, after mature may be divided into two separate projects.

The ability to edit status

  • Maintenance menu
  • The document maintenance
  • Documents showing
  • Import and export
  • Write/execute code online

I prefer to edit online because it’s easier, so I use El-Menu for navigation and the left menu, and then add maintenance. Use V-MD-Editor to edit and display Markdown. Then I wrote a back-end API in Node to save json files, and it was perfect.

The ability to browse status

  • navigation
  • The menu
  • Documents showing
  • Execute the code

It is to edit the status of the function on the basis of some features, remove some. Or you can actually think about it the other way around.

To realize the navigation

First, refer to VuePress to set up a JSON file for loading and saving web site information and navigation information.

/public/docs/.nfpress/project.json

{
  "projectId": "1000"."title": "The nf - press - edit!"."description": "This is a small tool for editing and presenting documents online."."navi": [{"naviId": "1010"."text": "Guide"."link": "menu"
    },
    {
      "naviId": "1020"."text": "Component"."link": "menu"
    },
    {
      "naviId": "1380"."text": "Gitee"."link": "https://gitee.com/nfpress/nf-press-edit"
    },
    {
      "naviId": "1390"."text": "Live demo"."link": "https://nfpress.gitee.io/nf-press-edit/"
    },
    {
      "naviId": "1395"."text": "I have a complaint."."link": "https://gitee.com/nfpress/nf-press-edit/issues"}}]Copy the code
  • ProjectId: projectId, which can be used to identify different help document projects.
  • Navi: Stores navigation items.
  • NaviId: Associate to menu.
  • Text: indicates the text displayed in the navigation.
  • Link: connection mode or link address. Menu: indicates that the corresponding menu should be opened. URL: Opens the connection in a new page.

Then make a component and render it with el-Menu binding data to achieve the navigation effect.

/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>
Copy the code

Multi-level navigation is supported, and the online maintenance function is not implemented.

  import { ref } from 'vue'
  import { ElMenu, ElMenuItem } from 'element-plus'
  import { state } from '@naturefw/nf-state'
   
  const props = defineProps({
    'background-color': { // Default background color
      type: String.default: '#ece5d9'
    },
    itemProps: Object
  })

  // Get the status and navigation content
  const { current, naviList } = state
  // Activate the first navigation item
  const activeIndex2 = ref(naviList[0].naviId)
  
  const handleSelect = (key, keyPath) = > {
    const navi = naviList.find((item) = > item.naviId === key)
    if (navi.link === 'menu') {
      // Open the menu
      current.naviId = key
    } else {
      // Open the connection
      window.open(navi.link, '_blank')}}Copy the code
  • @naturefw/nf-state

A lightweight state management system written by myself, which can be used as a large reactive, loads project.json through state management and then binds to render.

  • naviList

Navigation list, loaded by state manager.

  • current

Current active information, for example, current. NaviId indicates active navigation items.

To realize the menu

Similar to navigation, but with two additional features: N-level grouping and maintenance.

First, refer to VuePress to set up a JSON file to save menu information.

/public/docs/.nfpress/menu.json

[{"naviId": "1010"."menus": [{"menuId": "110100"."text": "Introduction"."description": "Description"."icon": "FolderOpened"."children": []}, {"menuId": "111100"."text": "Quick start"."description": "Description"."icon": "FolderOpened"."children": [{"menuId": 111120."text": Edit Document project."description": ""."icon": "UserFilled"."children": []}, {"menuId": 111130."text": "Show documents Project"."description": ""."icon": "UserFilled"}}]]."ver": 1.6
  },
  {
    "naviId": "1020"."menus": [{"menuId": "21000"."text": Navigation (docNavi)."description": "Description"."icon": "Star"."children": []}],"ver": 1.5}]Copy the code
  • NaviId: indicates the ID of the associated navigation item. It can be a number or other characters. It must correspond to the navigation item ID.
  • Menus: a collection of menu items corresponding to navigation items.
  • MenuId: menu item ID, associated with a document, can be numeric or English.
  • Text: indicates the name of the menu item.
  • Description: indicates the description, which is considered for future query.
  • Icon: indicates the name of the icon used in the menu.
  • Children: Submenu items, if not, can be removed.
  • Ver: indicates the version number for updating documents.

Then use el-menu binding data rendering, because to achieve n-level grouping, so make a recursive component to achieve n-level menu effect.

Achieve n level grouping menu

Make a recursive component to implement n-level grouping:

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

  <template v-for="(item, index) in subMenu">
    <! -- -- -- > branches
    <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.5 em. Height: 1.5 em. margin-right: 8px; vertical-align: middle;"
            >
            </component>
            <span>{{item.text}}</span>
          </div>
        </template>
        <! Recursive submenu -->
        <my-sub-menu2
          :subMenu="item.children"
          :dialogAddInfo="dialogAddInfo"
          :dialogModInfo="dialogModInfo"
        />
      </el-sub-menu>
    </template>
    <! -- -- -- > leaves
    <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.5 em. Height: 1.5 em. margin-right: 8px; vertical-align: middle;"
            >
            </component>
            <span >{{item.text}}</span>
          </span>
        </div>
      </template>
    </el-menu-item>
  </template>
Copy the code
  import { ElMenuItem, ElSubMenu } from 'element-plus'
  // Display submenu - recursion
  import mySubMenu2 from './menu-sub.vue'

  const props = defineProps({
    subMenu: Array.// To display the menu, can be n level
    dialogAddInfo: Object.// Add menu
    dialogModInfo: Object // Modify the menu
  })
Copy the code
  • SubMenu subMenu item to display
  • DialogAddInfo Add menu information
  • DialogModInfo modifies menu information

Realize menu maintenance function

This is relatively simple, do a form to achieve the menu to add, delete and change, space is limited skip.

Implement Markdown editing

Use V-MD-Editor to edit and display Markdown. First, the plug-in is very useful, and second, it supports VuePress themes.

/lib/md/md-edit.vue

  <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>
Copy the code
  import { watch,ref  } from 'vue'
  import { ElMessage, ElRadioGroup, ElRadioButton } from 'element-plus'
  import mdController from '.. /service/md.js'
  
  / / state
  import { state } from '@naturefw/nf-state'

  // Get the current active information
  const current = state.current
  // Load and save the document
  const { loadDocById, saveDoc } = mdController()
  
  // Visible height
  const editHeight = document.documentElement.clientHeight - 200

  // Click the Save button to save the configuration
  const mySave = (text, html) = > {
    saveDoc(current)
  }
  // Timed save
  let timeout = null
  let isSaved = true
  const timeSave = () = > {
    if (isSaved) {
      // Save it, reset the timer
      isSaved = false
    } else {
      return // There is a time, exit
    }

    timeout = setTimeout(() = > {
      // Save the document
      saveDoc(current).then(() = > {
        ElMessage({
          message: 'Auto-save document succeeded! '.type: 'success',
        })
      })
      isSaved = true
    }, 10000)}// Save the document periodically
  watch(() = > current.docInfo.md, () = > {
    timeSave()
  })

  // Load the corresponding document according to the active menu item
  watch( () = > current.menuId, async (id) => {
    const ver = current.ver
    loadDocById(id, ver).then((res) = > {
      // The document is found
      Object.assign(current.docInfo, res)
    }).catch((res) = > {
      // No documentation
      Object.assign(current.docInfo, res)
    })
  })

Copy the code
  • MdController implements the controller of adding, deleting, modifying and checking documents
  • TimeSave saves documents regularly to avoid forgetting to hit the save button

Isn’t that easy?

Realize online writing code and running functions

Since the project is based on Vue3, and is also designed to write help documentation related to Vue3, there is a practical requirement to write code online and run it.

Personally, I think this function is very practical, I know there are third-party websites that provide this function, but the Internet speed is a little slow, in addition, there is a feeling of cannon hitting mosquitoes, I just need to implement a simple code demonstration.

So I wrote a simple version of the online code and run function based on Vue’s defineAsyncComponent:

/lib/runCode/run.vue

  <div style="padding: 5px; border: 1px solid #ccc! important;">
    <async-comp></async-comp>
  </div>
Copy the code

  import {
    defineAsyncComponent,
    ref, reactive,...
    // Other common vUE built-in instructions
  } from 'vue'

  // Use eval to compile js code
  const mysetup = ` (function setup () { {{code}} }) `
  
  // Pass in the code and template to run via attributes
  const props = defineProps({
    code: {
      type: Object.default: () = > {
        return {
          js: ' '.template: ' '.style: ' '}}}})const code = props.code

  // Use defineAsyncComponent to make the code work
  const AsyncComp = defineAsyncComponent(
    () = > new Promise((resolve, reject) = > {
        resolve({
          template: code.template, // Set the template
          style: [code.style], // It's probably a style setting, but it doesn't seem to have any effect
          setup: (props, ctx) = > {
            const tmpJs = code.js // Get the js code
            let fun = null // The converted function
            try {
              if (tmpJs)
                fun = eval(mysetup.replace('{{code}}', tmpJs)) // Use eval to turn strings into functions
            } catch (error) {
              console.error('Conversion exception:', error)
            }

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

            return {
              ...re(props, ctx) // Run the function to destruct the returned object}}})})Copy the code
  • defineAsyncComponent

To load the component using defineAsyncComponent, you need to set three parts: template, setup, and style.

  • Template: it is a string and can be passed in directly
  • Setup: js code that can be dynamically compiled using eval.
  • Style: You can set the style.

This gets code written online up and running, but of course it’s limited and can only be used for simple code demonstrations.

export

All of these functions are based on indexedDB. To publish, you need to export json files.

Since you can’t write files directly in the browser, you need to use a compromise:

  • Copy and paste
  • download
  • export

Copy and paste

This is as simple as displaying JSON in a text field.

download

Use the download function provided by Chrome Browser to download files.

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

  // By creating the a tag
  var link = document.createElement("a")
  link.href = uri
  // Name the downloaded file
  link.download = fileName
  document.body.appendChild(link)
  link.click()
  document.body.removeChild(link)
Copy the code

The above introduction is the internal principle, if you just want to use it briefly, you can skip to the following introduction.

Write files to the back end

Both of these are not very convenient, so we use Node to make a simple back-end API for writing JSON files.

The code is in the API folder and can be run using the YARN API. Of course you need to set it up in package.json.

  "scripts": {
    "dev": "vite"."build": "vite build --mode project"."lib": "vite build --mode lib"."serve": "vite preview"."api": "node api/server.js"
  },
Copy the code

Implement a help document project

These are the basics of library projects, and when we do help documentation, it doesn’t have to be that complicated.

Create a Vue3 project using Vite2, then install @naturefw/ press-Edit, and use the supplied components for easy implementation.

main.js

First you need to do some Settings in main.js.

import { createApp } from 'vue'
import App from './App.vue'

// 设置 axios 的 baseUrl
const baseUrl = (document.location.host.includes('.gitee.io'))?'/doc-ui-core/' :  '/'

// Lightweight state
// Set up the indexedDB database to store various information about documents.
import { setupIndexedDB, setupStore } from '@naturefw/press-edit'
Initializes the indexedDB database
setupIndexedDB(baseUrl)
  
/ / UI library
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'

// Secondary encapsulation
import { nfElementPlus } from '@naturefw/ui-elp'
/ / Settings icon
import installIcon from './icon/index.js'

// Set the Markdown configuration function
import setMarkDown from './main-md.js'

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

const {
  VueMarkdownEditor, // Markdown editor
  VMdPreview // Markdown's browser
} = setMarkDown(vuepressTheme)

const app = createApp(App)
app.config.globalProperties.$ELEMENT = {
  locale: zhCn,
  size: 'small'
}

app.use(setupStore) // State management
  .use(nfElementPlus) // Repackaged components
  .use(installIcon) // Register the global icon
  .use(ElementPlus, { locale: zhCn, size: 'small' }) / / UI library
  .use(VueMarkdownEditor) // markDown editor
  .use(VMdPreview) / / markDown shows
  .mount('#app')

Copy the code
  • BaseUrl: Depending on the publishing platform, for example: “/doc-ui-core/”

  • SetupIndexedDB: Initializes the indexedDB database

  • SetupStore: Set the status

  • Element-plus: You can do without mounting element-plus, but the CSS needs to import it. In this case, the CDN is used to import it.

  • NfElementPlus: a repackaged component that makes it easy to add, delete, change, and search.

  • SetMarkDown: Loads the V-MD-Editor and any plug-ins needed.

  • VuepressTheme: Set the theme.

Set the Markdown

Because v-MD-editor has a lot of related Settings, so I set a separate file to manage:

/src/main-md.js


// Markdown editor
import VueMarkdownEditor from '@kangc/v-md-editor'
import '@kangc/v-md-editor/lib/style/base-editor.css'
// Is this not recognized?
// import vuepressTheme from '@kangc/v-md-editor/lib/theme/vuepress.js'
import '@kangc/v-md-editor/lib/theme/style/vuepress.css'

// Code highlights
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'

/ / process flow diagram
// 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'

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

// Highlight lines of code
import createHighlightLinesPlugin from '@kangc/v-md-editor/lib/plugins/highlight-lines/index'
import '@kangc/v-md-editor/lib/plugins/highlight-lines/highlight-lines.css'

// Copy the code
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 display
import VMdPreview from '@kangc/v-md-editor/lib/preview'
// import '@kangc/v-md-editor/lib/style/preview.css'


/** * Set the Markdown editor and browser *@param {*} vuepressTheme 
 * @returns * /
export default function setMarkDown (vuepressTheme) {

  // Set the vuePress theme
  VueMarkdownEditor.use(vuepressTheme,
    {
      Prism,
      extend(md) {
        // md is an instance of Markdown-it. You can modify the configuration here and use plugin to extend the syntax
        // md.set(option).use(plugin);}})/ / the preview
  VMdPreview.use(vuepressTheme,
    {
      Prism,
      extend(md) {
        // md is an instance of Markdown-it. You can modify the configuration here and use plugin to extend the syntax
        // md.set(option).use(plugin);}})// emoji
  VueMarkdownEditor.use(createEmojiPlugin())
  / / process flow diagram
  // VueMarkdownEditor.use(createMermaidPlugin())
  // todoList
  VueMarkdownEditor.use(createTodoListPlugin())
  // Line number
  VueMarkdownEditor.use(createLineNumbertPlugin())
  // Highlight lines of code
  VueMarkdownEditor.use(createHighlightLinesPlugin())
  // Copy the code
  VueMarkdownEditor.use(createCopyCodePlugin())
  

  // Preview the plugin
  VMdPreview.use(createEmojiPlugin())
  VMdPreview.use(createTodoListPlugin())
  VMdPreview.use(createLineNumbertPlugin())
  VMdPreview.use(createHighlightLinesPlugin())
  VMdPreview.use(createCopyCodePlugin())
  
  return {
    VueMarkdownEditor,
    VMdPreview
  }

}
Copy the code

Without further explanation, plugins can be selected as needed.

layout

Do the overall layout in the app. vue file

  <el-container>
    <el-header>
      <! Navigation -- - >
      <div style="float: left;">
        <! -- Write website logo, title, etc.
        <h1>nf-press</h1>
      </div>
      <div style="float: right; min-width: 100px; height: 60px; padding-top: 13px;">
        <! -- Write website logo, title, etc.
        <el-switch v-model="$state.current.isView" v-bind="itemProps"></el-switch>
      </div>
      <div style="float: right; min-width: 600px; height: 60px;">
        <! -- Website navigation -->
        <doc-navi ></doc-navi>
      </div>
    </el-header>
    <el-container>
      <! -- Left sidebar -->
      <el-aside width="330px">
        <! - the menu - >
        <doc-menu ></doc-menu>
      </el-aside>
      <el-main>
        <! -- Document area -->
        <component
          :is="docControl[$state.current.isView]"
        />
      </el-main>
    </el-container>
  </el-container>
Copy the code
  import { reactive, defineAsyncComponent } from 'vue'
  import { ElHeader, ElContainer ,ElAside, ElMain } from 'element-plus'
  import { docMenu, docNavi, config } from '@naturefw/press-edit' // Menu navigation
  import docView from './views/doc.vue' // Display the document

  // Load the menu control
  const docControl = {
    true: docView,
    false: defineAsyncComponent(() = > import('./views/main.vue')) // Modify the document
  }

  const itemProps = reactive({
    'inline-prompt': true.'active-text': 'look at'.'inactive-text': 'write'.'active-color': '#378FEB'.'inactive-color': '#EA9712'
  })
Copy the code
  • $state: indicates the global state. $state.current. IsView indicates whether the state is browse.
  • Doc-navi: navigation component
  • Doc-menu: menu component
  • DocControl: dictionary for loading display components or editing components based on state selection.

Although this way is a bit troublesome, but more flexible, according to the need for a variety of flexible Settings, such as adding copyright information, record information, advertising and other content.

Navigation, menus, editing, and browsing

Directly use component implementation, relatively simple not to carry, directly look at the source code.

Package distribution and version management

There are two types of packaging that need to be done: the first time or after the code is modified (code not edited online).

If only the content of the document changes, you can simply upload the JSON file without packaging it again.

There is a simple built-in version management function, which can be updated with the version number in the ver.json file.

The source code

Gitee.com/nfpress/nf-…

The online demo

Nfpress. Gitee. IO/nf – press – Ed…

demo

Gitee.com/nfpress/doc…