提起幫助文件,想必大家都會想到 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檔案裡的版本號實現更新功能。
原始碼
https://gitee.com/nfpress/nf-press-edit
線上演示
https://nfpress.gitee.io/nf-press-edit/