Jeffrey Wang
文章84
标签144
分类12
使用 Vue3 + vite + elementUI 开发一个 Utools Markdown 编辑器插件

使用 Vue3 + vite + elementUI 开发一个 Utools Markdown 编辑器插件

目的

博主个人已经用了很久的 Utools 了,会员也续到了 2024 年,它跟随博主从 Deepin 到 Manjaro,再到 Windows 再到 MacOS,在很多场景下都非常方便,比如选中文本中键翻译、复制 JSON 后自动格式化、正则表达式匹配小工具、计算稿纸等。已推荐给很多同事、朋友使用,无不夸其高效便捷。 而在使用的同时,也想为社区出一份力,实现一些插件为自己为他人提供更多的便利,Utools 为实现多端兼容,主要使用 JS 开发插件,界面 UI 与组件交互与传统的 web 开发别无二致,只不过可以利用更多的系统能力去做传统 web 开发不能做到的事情。

开发文档整理

Utools 开发者文档:http://u.tools/docs/developer/welcome.html ElementUI开发文档:https://element-plus.org/#/zh-CN/component/installation Vue3 开发文档:https://v3.vuejs.org/guide/introduction.html Vite 开发文档:https://vitejs.dev/config/ 在开发者文档中,快速上手仅提供了原生 JS + utools 能力的调用结合,这篇文档的目标是将 Vue3 与 Utools 开发结合,并将国内比较流行的 ElementUI 框架集成在一起,最终会基于这些技术创造出一个简易的 Markdown 编辑器,Utools 上搜索 『清爽 Markdown 编辑器』 即可体验。 具体代码见: Github:https://github.com/wangerzi/utools-vue3-markdown-editor Gitee:https://gitee.com/wangerzi/utools-vue3-markdown-editor 界面效果如下: 还有一些优秀的开源插件可参考: https://github.com/xiaou66/utools-pictureBed https://github.com/xkloveme/utools-calendar https://github.com/in3102/upassword

基础工具的集成

这一小节的目的主要是将业务框架和主要依赖安装好,为实现业务做准备。 空白项目只有一个 README.md.gitignore 本阶段的配置和代码执行已放入 https://github.com/wangerzi/utools-vue3-markdown-editor/tree/element-template ,如果想基于同样的技术栈做研发,可以直接把代码下载下来改。

初始化项目

首先,这是一个 vue3 + vite 的项目,根据官方的 快速上手指引,在项目根目录下执行如下指令,注意对比执行结果和 node 版本

其中 mv utools-vue3-markdown-editor/* ./ 是因为项目初始化在子文件夹中,不在主目录初始化是因为会删除该目录所有文件,而空白项目中已经有 .git/README.md/.gitignore 了,初始化到本目录中,会导致这些数据被清理掉,规避风险所以创建在了子目录中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
$ node -v
v12.16.1

$ npm -v
6.13.4

$ npm init vite utools-vue3-markdown-editor -- --template vue
npx: 6 安装成功,用时 3.61 秒
√ Select a framework: » vue
√ Select a variant: » vue

Scaffolding project in D:\phpStudy\WWW\github\utools-vue3-markdown-editor\utools-vue3-markdown-editor...

Done. Now run:

cd utools-vue3-markdown-editor
npm install
npm run dev

$ mv utools-vue3-markdown-editor/* ./

$ rm -rf utools-vue3-markdown-editor\

$ npm install

> esbuild@0.12.18 postinstall D:\phpStudy\WWW\github\utools-vue3-markdown-editor\node_modules\esbuild
> node install.js

npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN utools-vue3-markdown-editor@0.0.0 No repository field.
npm WARN utools-vue3-markdown-editor@0.0.0 No license field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.3.2 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.3.2: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

added 57 packages from 78 contributors in 10.725s

3 packages are looking for funding
run `npm fund` for details


$ npm run dev

> utools-vue3-markdown-editor@0.0.0 dev D:\phpStudy\WWW\github\utools-vue3-markdown-editor
> vite

Pre-bundling dependencies:
vue
(this will be run only when your dependencies or config have changed)

vite v2.4.4 dev server running at:

> Local: http://localhost:3000/
> Network: use `--host` to expose

ready in 1375ms.

此时访问 http://localhost:3000/ 将会看到如下界面,表示项目初始化完成

框架引入

下一步引入 element 框架,主要参考 官方安装文档 执行如下指令

1
2
3
4
5
6
7
8
9
10
11
12
$ npm install element-plus --save
npm WARN utools-vue3-markdown-editor@0.0.0 No repository field.
npm WARN utools-vue3-markdown-editor@0.0.0 No license field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.3.2 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.3.2: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ element-plus@1.0.2-beta.70
added 9 packages from 6 contributors in 11.379s

4 packages are looking for funding
run `npm fund` for details

按需引用和 SASS

由于 vite 、 webpack 等打包工具会用 tree-shaking 剔除未使用的代码,做按需引用可最大程度的利用此功能,减少打包体积。ElementUI 官方也提供了 element 按需引用的使用说明。 这一步的目的是安装 vite 的 style 引入插件,并安装 sass 和 sass-loader 以兼容 sass 的加载,执行如下指令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ npm install vite-plugin-style-import -D
npm WARN utools-vue3-markdown-editor@0.0.0 No repository field.
npm WARN utools-vue3-markdown-editor@0.0.0 No license field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.3.2 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.3.2: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ vite-plugin-style-import@1.1.1
added 22 packages from 10 contributors in 3.215s

5 packages are looking for funding
run `npm fund` for details

$ npm install sass sass-loader
npm WARN sass-loader@12.1.0 requires a peer of fibers@>= 3.1.0 but none is installed. You must install peer dependencies yourself.
npm WARN sass-loader@12.1.0 requires a peer of node-sass@^4.0.0 ^5.0.0 ^6.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN sass-loader@12.1.0 requires a peer of webpack@^5.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN utools-vue3-markdown-editor@0.0.0 No repository field.
npm WARN utools-vue3-markdown-editor@0.0.0 No license field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.3.2 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.3.2: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ sass@1.37.5
+ sass-loader@12.1.0
added 17 packages from 20 contributors in 3.052s

6 packages are looking for funding
run `npm fund` for details

编辑 vite.config.js,调整为如下格式,这一步的目的有两个

  • 指定明确的开发端口,这在 utools 的开发配置中也将有所体现
  • 按需加载 elementUI 的 .scss 文件处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import styleImport from 'vite-plugin-style-import'

// https://vitejs.dev/config/
export default defineConfig({
base: './', // !!!!!!! 非常重要,否则打包后无法访问 !!!!!!!
server: {
port: 3000,
},
plugins: [
vue(),
styleImport({
libs: [{
libraryName: 'element-plus',
esModule: true,
ensureStyleFile: true,
resolveStyle: (name) => {
name = name.slice(3)
return `element-plus/packages/theme-chalk/src/${name}.scss`
},
resolveComponent: (name) => {
return `element-plus/lib/${name}`
},
}]
})
]
})

随后,修改 src/main.js,在其中添入按需引入的 ElementUI 插件和 base.scss

1
2
3
4
5
6
7
8
9
10
11
12
import { createApp } from 'vue'
import App from './App.vue'

import { ElButton, ElSelect } from 'element-plus'
import 'element-plus/packages/theme-chalk/src/base.scss'

const app = createApp(App);

app.component(ElButton.name, ElButton)
app.component(ElSelect.name, ElSelect)

app.mount('#app')

引入验证

src/components/HelloWorld.vue 中小改一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<template>
<el-button>Hello World</el-button>
<h1>{{ msg }}</h1>

<p>
<a href="https://vitejs.dev/guide/features.html" target="_blank">
Vite Documentation
</a>

<a href="https://v3.vuejs.org/" target="_blank">Vue 3 Documentation</a>
</p>

<button type="button" @click="state.count++">
count is: {{ state.count }}
</button>
<p>
Edit
<code>components/HelloWorld.vue</code> to test hot module replacement.
</p>
</template>

<script setup>
import { defineProps, reactive } from 'vue'

defineProps({
msg: String
})

const state = reactive({ count: 0 })
</script>

<style scoped>
a {
color: #42b983;
}
</style>

执行 npm run dev 并访问 http://localhost:3000/,看到 ElementUI 风格的按钮展现在页面上即表示成功

utools 开发配置

上面的步骤操作完毕后,开发框架和基础目录都已经建好了,但 utools 与传统 web 开发有区别的地方在于,它可以利用客户端的能力,并能在 utools 中快速调用,所以我们需要定义两个文件 plugin.jsonpreload.js 用来指定插件的配置,封装插件可使用的客户端能力。 官方配置文档:http://u.tools/docs/developer/welcome.html 由于我们调试环境是运行在 localhost:3000 上的,utools 也考虑到了这种调试需求,根据文档可以做出配置 而这次要实现插件可以由两个入口进入,可定义两个 feature

  • 关键字 『markdown 编辑器』进入主页面
  • 复制后缀名为 .md 的文件后,唤醒 utools,我们将会自动读取对应文件并做相关编辑

考虑到 打包后的 plugin.jsonpreload.js logo.png 均需要出现在 dist/ 目录下 ,所以我将这三个文件都放到了 public/ 中,这样打包后这三个文件将会出现在 dist/plugin.jsondist/preload.js dist/logo.png,符合打包要求,utools 需要加载的目标文件就是 dist/index.html,所以 plugin.json 中的 main 配置写 index.html 即可。 logo 去 https://www.iconfont.cn/ 随便找了个跟文本编辑相关的拿来用了 public/plugin.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
"main": "index.html",
"logo": "logo.png",
"platform": ["win32", "darwin", "linux"],
"preload": "preload.js",
"development": {
"main": "http://127.0.0.1:3000"
},
"features": [
{
"code": "main",
"explain": "一个方便的 markdown 编辑工具",
"cmds":["markdown 编辑器"]
},
{
"code": "copy",
"explain": "复制文件预览及编辑",
"cmds": [
{
"type": "files",
"label": "markdown 文件预览",
"fileType": "file",
"match": "/\\.md$/i",
"minLength": 1,
"maxLength": 1
}
]
}
]
}

preload 先简单写一个 console,具体需要用到 node / electron 相关能力了,再回来补全。 public/preload.js

1
console.log("preload js loaded")

运行 npm run dev 尝试下打包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ npm run build

> utools-vue3-markdown-editor@0.0.0 build D:\phpStudy\WWW\github\utools-vue3-markdown-editor
> vite build

vite v2.4.4 building for production...
✓ 358 modules transformed.
dist/assets/logo.03d6d6da.png 6.69kb
dist/assets/element-icons.9c88a535.woff 24.24kb
dist/assets/element-icons.de5eb258.ttf 49.19kb
dist/index.html 0.48kb
dist/assets/index.5be5297f.js 1.02kb / brotli: 0.52kb
dist/assets/index.66070cd5.css 52.22kb / brotli: 7.52kb
dist/assets/vendor.27ac3d2d.js 211.72kb / brotli: 63.94kb

调试和打包插件

首先去 utools 插件中搜索『开发者工具』,打开后点击新建项目,补齐相关信息 上一步 npm build 后,会产生一个 dist/ 目录,将其下的 dist/plugin.json 拖到开发者工具中,点击运行 由于是调试模式,并且我们在 plugin.json 中制定了 development.mainlocalhost:3000,所以在调试期间需要 npm run dev 把 devserver 跑起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ npm run dev

> utools-vue3-markdown-editor@0.0.0 dev D:\phpStudy\WWW\github\utools-vue3-markdown-editor
> vite


vite v2.4.4 dev server running at:

> Local: http://localhost:3000/
> Network: use `--host` to expose

ready in 1238ms.

[@vue/compiler-sfc] <script setup> is still an experimental proposal.
Follow its status at https://github.com/vuejs/rfcs/pull/227.

[@vue/compiler-sfc] When using experimental features,
it is recommended to pin your vue dependencies to exact versions to avoid breakage.

[@vue/compiler-sfc] `defineProps` is a compiler macro and no longer needs to be imported.

然后 utools 中输入关键字 『markdown』即可看到处于 dev 状态下的插件 进入插件后,点击右上角按钮或者 ctrl+sfhit+i 可进入开发者模式,开发者模式中可以看到 preload.js 正常运行,输出了 『preload js loaded』

注意:指定的 plugin.json 为 dev/plugin.json,所以 public/plugin.jsonpublic/preload.js 有任何修改,需要手动复制或者 npm run build 重新打包,然后 utools 开发者工具点击按钮刷新 plugin.json 即可。

功能实现

功能整体比较简单,左侧为编辑区,由 textarea 实现,右侧为预览区,实时渲染左侧编辑区域的 markdown 语法的结果,下方为两个控制按钮,分别是保存和另存。

依赖库的安装调用

1
2
3
4
5
6
7
8
9
10
11
12
$ npm i marked keyboardjs github-markdown-css highlight.js
npm WARN sass-loader@12.1.0 requires a peer of webpack@^5.0.0 but none is installed. You must install peer dependencies yourself.
npm WARN utools-vue3-markdown-editor@0.0.0 No repository field.
npm WARN utools-vue3-markdown-editor@0.0.0 No license field.
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@2.3.2 (node_modules\fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@2.3.2: wanted {"os":"darwin","arch":"any"} (current: {"os":"win32","arch":"x64"})

+ marked@2.1.3
added 1 package from 1 contributor in 33.831s

6 packages are looking for funding
run `npm fund` for details

布局实现

由于此应用单页即可完成,所以简单改造下 src/App.vuesrc/components/HelloWorld.vue 即可,其中 HelloWorld.vue 在项目中被重命名为了 Editor.vue,目录结构如下图所示:

Editor.vue

模板部分用了 Element 的 el-row 和 el-col,规划好基础布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div class="container">
<el-divider content-position="center">{{props.path?('当前:'+props.path):'临时文件'}}</el-divider>
<el-row :gutter="30">
<el-col :span="12">
<el-input type="textarea" placeholder="markdown..." resize="none" :rows="19" :autofocus="true" v-model="state.content"></el-input>
</el-col>
<el-col :span="12">
<div class="rendered markdown-body" v-html="renderedContent"></div>
</el-col>
</el-row>

<el-row justify="center" :gutter="30">
<el-col :span="6">
<el-button class="save-button" @click="handleSave">{{saveText}}</el-button>
</el-col>
<el-col :span="6">
<el-button class="save-button" @click="handleSaveAs">{{saveAsText}}</el-button>
</el-col>
</el-row>
</div>
</template>

样式部分,手动做了些阴影和高度、换行限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<style scoped>
.el-row {
margin-bottom: 20px;
}
.container {
width: 90%;
margin: 20px auto;
}
.rendered {
/*height: calc(100% - 20px);*/

height: calc(407px - 20px);

word-break: break-all;

box-shadow: 0 2px 4px rgba(0,0,0,0.12),0 0 6px rgba(0,0,0,0.04);

border: 2px solid #eee;
padding: 10px 20px;

overflow-y: auto;
}

.save-button {
margin: 0 auto;
display: block;
}
</style>

处理逻辑里边,使用了 vue3 的 setup api,定义了 state.pathstate.content 两个关键的响应式变量,调用了 markd, highlight, keyboardjs 等项目实现功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
<script setup>
import { defineProps, defineEmits, reactive, watch, computed } from 'vue'
import marked from 'marked'
import "github-markdown-css/github-markdown.css"

import hljs from 'highlight.js'
import "highlight.js/scss/default.scss"

import keyboard from "keyboardjs"

marked.setOptions({
renderer: new marked.Renderer(),
highlight: function(code, lang) {
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
return hljs.highlight(code, { language }).value;
},
pedantic: false,
gfm: true,
breaks: false,
sanitize: false,
smartLists: true,
smartypants: false,
xhtml: false
});

const props = defineProps({
content: String,
path: String,
})

const state = reactive({ content: props.content })

watch(() => props.content, () => {
state.content = props.content
})

watch(() => props.path, () => {
state.path = props.path
})

const renderedContent = computed(() => {
return marked(state.content)
})


// save and save as
const emits = defineEmits(['save'])

const saveText = "保存( " + (utools.isMacOs() ? "⌘" : 'Ctrl') + " + S )"
const saveAsText = "另存为( " + (utools.isMacOs() ? "⌘" : 'Ctrl') + " + Shift + S )"

function handleSave() {
if (props.path === "") {
handleSaveAs()
} else {
emits('save', props.path, state.content);
}
}

function handleSaveAs() {
const savePath = utools.showSaveDialog({
title: '保存位置',
defaultPath: "临时文件.md",
buttonLabel: '保存'
})
if (savePath) {
emits('save', savePath, state.content);
}
}

// keyboard
keyboard.bind("mod > s", () => {
handleSave()
});
keyboard.bind("mod + shift > s", () => {
handleSaveAs()
});

</script>

调用 utools 能力做保存

preload.js

上一节提到了 preload.js 可以实现一些 web 无法实现的客户端功能,比如读取、保存客户端文件,官方规定,需要上架插件市场的插件,均需要明文 preload.js 以便审核,这里的需要用到的核心能力就是读取和保存用户文件了,这里针对读取和保存也做了收口,减少由于业务层 BUG 穿透过去影响系统正常运行的可能性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const fs = require('fs');

console.log("preload js loaded")

window.readMarkdownFile = function (path) {
if (path.match(/\.md$/i)) {
return fs.readFileSync(path, {
encoding: "utf-8"
});
} else {
return "";
}
}

window.writeMarkdownFile = function (path, content) {
if (fs.existsSync(path)) {
if (path.match(/\.md$/i)) {
fs.writeFileSync(path, content)
return true;
} else {
return false;
}
} else {
fs.writeFileSync(path, content)
return true;
}
}

App.vue

在业务层中即可调用经过 window 变量暴露出来的 readMarkdownFilewriteMarkdownFile 方法,使用 utools 的钩子函数(onPluginEnter)即可识别入口为复制了 markdown 文件还是直接打开。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<template>
<Editor :content="state.content" :path="state.path" @save="handleSave" />
</template>

<script setup>
import {reactive} from 'vue';
import { ElMessage } from 'element-plus'
import Editor from './components/Editor.vue'

const state = reactive({
content: "",
path: "",
})

function handleSave(path, content) {
if (path && content !== state.content) {
writeMarkdownFile(path, content)
ElMessage.success({
message: '保存成功',
type: 'success'
});

if (state.path === '') {
state.path = path
state.content = readMarkdownFile(state.path)
}
}
}

utools.onPluginEnter(({code, type, payload}) => {
console.log('用户进入插件', code, type, payload)

if (type === 'files') {
state.path = payload[0].path;
state.content = readMarkdownFile(state.path)
} else {
state.path = ""
state.content = ""
}
})
</script>

<style>
</style>

打包 upx 或发布到插件中心

调试无误后,点击打包为 upx 即可自行安装测试或分发 也可以在『插件发布中』点击发布插件,填写相关信息,审核后即可在插件市场看到发布的插件。

常见问题和总结

不论是使用 webpack 还是 vite ,打包时一定要注意打包路径(base: “./“),因为 utools 需要根据相对路径索引打包资源,否则调试模式下是好的,只要发布为 upx 就出问题。 开发 utools 插件上手也很快的,本博客对应的插件在基础框架引入后,开发、调试、编写文档的时间不到六小时。 最后,祝 utools 越做越好,完善 utools 插件生态的人越来越多,星星之火可以燎原。

本文作者:Jeffrey Wang
本文链接:https://blog.wj2015.com/2021/08/12/%E4%BD%BF%E7%94%A8-vue3-vite-elementui-%E5%BC%80%E5%8F%91%E4%B8%80%E4%B8%AA-utools-markdown-%E7%BC%96%E8%BE%91%E5%99%A8%E6%8F%92%E4%BB%B6/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×