前端時間我的一個朋友為了快速熟悉 Vue3 開發, 特意使用 electron+vue3+ts 開發了一個桌面端應用, 並在
github上開源了, 接下來我就帶大家一起了解一下這個項目, 在文章末尾我會放github的地址, 大家如果想學習vue3 + ts + electron 開發, 可以本地clone學習參考一下.
image.png
技術棧
以上是我們看到的便簽軟件使用界面, 整體技術選型如下:
-
腳手架 vue-cli
-
前端框架和語言規范 vue + typescript
-
桌面端開發框架 electron
-
electron支持插件 vue-cli-plugin-electron-builder
-
數據庫 NeDB | 一款NoSQL嵌入式數據庫
-
代碼格式規范 eslint
接下來我們來看看具體的演示效果:

具體實現過程, 內容很長, 建議先點贊收藏, 再一步步學習, 接下來會就該項目的每一個重點細節做詳細的分析.
開發思路
-
頁面:
-
列表頁
index.vue頭部、搜索、內容部分,只能有一個列表頁存在 -
設置頁
setting.vue設置內容和軟件信息,和列表頁一樣只能有一個存在 -
編輯頁
editor.vueicons功能和背景顏色功能,可以多個編輯頁同時存在
-
動效:
-
-
打開動效,有一個放大、透明度的過渡,放不了動圖這里暫時不演示了。
-
標題過渡效果
-
切換
index和setting時頭部不變,內容過渡
-
-
數據儲存:數據的創建和更新都在編輯頁
editor.vue進行,這個過程中在儲存進nedb之后才通信列表頁index.vue更新內容,考慮到性能問題,這里使用了防抖防止連續性的更新而導致卡頓(不過貌似沒有這個必要。。也算是一個小功能吧,然后可以設置這個更新速度) -
錯誤采集:采集在使用中的錯誤並彈窗提示
-
編輯顯示:
document暴露execCommand方法,該方法允許運行命令來操縱可編輯內容區域的元素。 -
在開發的時候還遇到過好多坑,這些都是在
electron環境中才有,比如-
@input觸發2次,加上v-model觸發3次。包括創建一個新的electron框架也是這樣,別人電腦上不會出現這個問題,猜測是electron緩存問題 -
vue3碰到
空屬性報錯時無限報錯,在普通瀏覽器(edge和chrome)是正常一次 -
組件無法正常渲染不報錯,只在控制台報異常
-
打包后由於
electron的緩存導致打開需要10秒左右,清除c盤軟件緩存后正常
其他的不記得了。。
這里暫時不提供vue3和electron介紹,有需要的可以先看看社區其他的有關文章或者后期再詳細專門提供。軟件命名為
i-notes。vue3中文教程 vue3js.cn/docs/zh/gui…[1] electron教程 www.electronjs.org/[2]
typescript教程 www.typescriptlang.org/[3]electron-vue里面的包環境太低了,所以是手動配置electron+vue3(雖然說是手動。。其實就兩個步驟)目錄結構
-
electron-vue-notes
-
├── public
-
│ ├── css
-
│ ├── font
-
│ └── index.html
-
├── src
-
│ ├── assets
-
│ │ └── empty-content.svg
-
│ ├── components
-
│ │ ├── message
-
│ │ ├── rightClick
-
│ │ ├── editor.vue
-
│ │ ├── header.vue
-
│ │ ├── input.vue
-
│ │ ├── messageBox.vue
-
│ │ ├── switch.vue
-
│ │ └── tick.vue
-
│ ├── config
-
│ │ ├── browser.options.ts
-
│ │ ├── classNames.options.ts
-
│ │ ├── editorIcons.options.ts
-
│ │ ├── index.ts
-
│ │ └── shortcuts.keys.ts
-
│ ├── inotedb
-
│ │ └── index.ts
-
│ ├── less
-
│ │ └── index.less
-
│ ├── router
-
│ │ └── index.ts
-
│ ├── script
-
│ │ └── deleteBuild.js
-
│ ├── store
-
│ │ ├── exeConfig.state.ts
-
│ │ └── index.ts
-
│ ├── utils
-
│ │ ├── errorLog.ts
-
│ │ └── index.ts
-
│ ├── views
-
│ │ ├── editor.vue
-
│ │ ├── index.vue
-
│ │ ├── main.vue
-
│ │ └── setting.vue
-
│ ├── App.vue
-
│ ├── background.ts
-
│ ├── main.ts
-
│ └── shims-vue.d.ts
-
├── .browserslistrc
-
├── .eslintrc.js
-
├── .prettierrc.js
-
├── babel.config.js
-
├── inoteError.log
-
├── LICENSE
-
├── package-lock.json
-
├── package.json
-
├── README.md
-
├── tsconfig.json
-
├── vue.config.js
-
└── yarn.lock
使用腳手架搭建vue3環境
沒有腳手架的可以先安裝腳手架
npm install -g @vue/cli創建vue3項目
-
vue create electron-vue-notes
-
-
# 后續
-
? Please pick a preset: (Use arrow keys)
-
Default ([Vue 2] babel, eslint)
-
Default (Vue 3 Preview) ([Vue 3] babel, eslint)
-
> Manually select features
-
# 手動選擇配置
-
-
# 后續所有配置
-
? Please pick a preset: Manually select features
-
? Check the features needed for your project: Choose Vue version, Babel, TS, Router, CSS Pre-processors, Linter
-
? Choose a version of Vue.js that you want to start the project with 3.x (Preview)
-
? Use class-style component syntax? Yes
-
? Use Babel alongside TypeScript (required for modern mode, auto-detected polyfills, transpiling JSX)? Yes
-
? Use history mode for router? (Requires proper server setup for index fallback in production) No
-
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Less
-
? Pick a linter / formatter config: Prettier
-
? Pick additional lint features: Lint on save, Lint and fix on commit
-
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
-
? Save this as a preset for future projects? (y/N) n
創建完之后的目錄是這樣的
-
electron-vue-notes
-
├── public
-
│ ├── favicon.ico
-
│ └── index.html
-
├── src
-
│ ├── assets
-
│ │ └── logo.png
-
│ ├── components
-
│ │ └── HelloWorld.vue
-
│ ├── router
-
│ │ └── index.ts
-
│ ├── views
-
│ │ ├── About.vue
-
│ │ └── Home.vue
-
│ ├── App.vue
-
│ ├── main.ts
-
│ └── shims-vue.d.ts
-
├── .browserslistrc
-
├── .eslintrc.js
-
├── babel.config.js
-
├── package.json
-
├── README.md
-
├── tsconfig.json
-
└── yarn.lock
安裝electron的依賴
-
# yarn
-
yarn add vue-cli-plugin-electron-builder electron
-
-
# npm 或 cnpm
-
npm i vue-cli-plugin-electron-builder electron
安裝完之后完善一些配置,比如
別名、eslint、prettier等等基礎配置,還有一些顏色、icons等等具體可以看下面項目的一些基礎配置
eslint
使用eslint主要是規范代碼風格,不推薦tslint是因為tslint已經不更新了,tslint也推薦使用eslint 安裝eslint
npm i eslint -g進入項目之后初始化eslint
-
eslint --init
-
-
# 后續配置
-
? How would you like to use ESLint? To check syntax and find problems
-
? What type of modules does your project use? JavaScript modules (import/export)
-
? Which framework does your project use? Vue.js
-
? Does your project use TypeScript? Yes
-
? Where does your code run? Browser, Node
-
? What format do you want your config file to be in? JavaScript
-
The config that you 've selected requires the following dependencies:
-
-
eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
-
? Would you like to install them now with npm? (Y/n) y
-
修改eslint配置,·
.eslintrc.js,規則rules可以根據自己的喜歡配置 eslint.org/docs/user-g…[4]-
module.exports = {
-
root: true,
-
env: {
-
node: true
-
},
-
extends: [
-
'plugin:vue/vue3-essential',
-
'eslint:recommended',
-
'plugin:prettier/recommended',
-
'plugin:@typescript-eslint/eslint-recommended',
-
'@vue/typescript/recommended',
-
'@vue/prettier',
-
'@vue/prettier/@typescript-eslint'
-
],
-
parserOptions: {
-
ecmaVersion: 2020
-
},
-
rules: {
-
quotes: [ 1, 'single'],
-
semi: 1,
-
'@typescript-eslint/camelcase': 0,
-
'@typescript-eslint/no-explicit-any': 0,
-
'no-irregular-whitespace': 2,
-
'no-case-declarations': 0,
-
'no-undef': 0,
-
'eol-last': 1,
-
'block-scoped-var': 2,
-
'comma-dangle': [2, 'never'],
-
'no-dupe-keys': 2,
-
'no-empty': 1,
-
'no-extra-semi': 2,
-
'no-multiple-empty-lines': [1, { max: 1, maxEOF: 1 }],
-
'no-trailing-spaces': 1,
-
'semi-spacing': [2, { before: false, after: true }],
-
'no-unreachable': 1,
-
'space-infix-ops': 1,
-
'spaced-comment': 1,
-
'no-var': 2,
-
'no-multi-spaces': 2,
-
'comma-spacing': 1
-
}
-
};
prettier
在根目錄增加
.prettierrc.js配置,根據自己的喜好進行配置,單行多少個字符、單引號、分號、逗號結尾等等-
module.exports = {
-
printWidth: 120,
-
singleQuote: true,
-
semi: true,
-
trailingComma: 'none'
-
};
tsconfig.json
如果這里沒有配置識別
@/路徑的話,在項目中使用會報錯-
"paths": {
-
"@/*": [
-
"src/*"
-
]
-
}
package.json
-
"author": "heiyehk",
-
"description": "I便箋個人開發者heiyehk獨立開發,在Windows中更方便的記錄文字。",
-
"main": "background.js",
-
"scripts": {
-
"lint": "vue-cli-service lint",
-
"electron:build": "vue-cli-service electron:build",
-
"electron:serve": "vue-cli-service electron:serve"
-
}
配置入口文件
background.ts因為需要做一些打開和關閉的動效,因此我們需要配置
electron為frame無邊框和透明transparent的屬性-
/* eslint-disable @typescript-eslint/no-empty-function */
-
'use strict';
-
-
import { app, protocol, BrowserWindow, globalShortcut } from 'electron';
-
import {
-
createProtocol
-
// installVueDevtools
-
} from 'vue-cli-plugin-electron-builder/lib';
-
-
const isDevelopment = process.env.NODE_ENV !== 'production';
-
-
let win: BrowserWindow | null;
-
protocol.registerSchemesAsPrivileged([
-
{
-
scheme: 'app',
-
privileges: {
-
secure: true,
-
standard: true
-
}
-
}
-
]);
-
-
function createWindow() {
-
win = new BrowserWindow({
-
frame: false, // 無邊框
-
hasShadow: false,
-
transparent: true, // 透明
-
width: 950,
-
height: 600,
-
webPreferences: {
-
enableRemoteModule: true,
-
nodeIntegration: true
-
}
-
});
-
-
if (process.env.WEBPACK_DEV_SERVER_URL) {
-
win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
-
if (!process.env.IS_TEST) win.webContents.openDevTools();
-
} else {
-
createProtocol( 'app');
-
win.loadURL( 'http://localhost:8080');
-
}
-
-
win.on( 'closed', () => {
-
win = null;
-
});
-
}
-
-
app.on( 'window-all-closed', () => {
-
if (process.platform !== 'darwin') {
-
app.quit();
-
}
-
});
-
-
app.on( 'activate', () => {
-
if (win === null) {
-
createWindow();
-
}
-
});
-
-
app.on( 'ready', async () => {
-
// 這里注釋掉是因為會安裝tools插件,需要屏蔽掉,有能力的話可以打開注釋
-
// if (isDevelopment && !process.env.IS_TEST) {
-
// try {
-
// await installVueDevtools();
-
// } catch (e) {
-
// console.error('Vue Devtools failed to install:', e.toString());
-
// }
-
// }
-
createWindow();
-
});
-
-
if (isDevelopment) {
-
if (process.platform === 'win32') {
-
process.on( 'message', data => {
-
if (data === 'graceful-exit') {
-
app.quit();
-
}
-
});
-
} else {
-
process.on( 'SIGTERM', () => {
-
app.quit();
-
});
-
}
-
}
啟動
yarn electron:serve
到這里配置就算是成功搭建好這個窗口了,但是還有一些其他細節需要進行配置,比如
electron打包配置,模塊化的配置等等常規配置
這里配置一些常用的開發內容和一些輪子代碼, 大家可以參考
reset.csss和common.css這兩個文件.config
這個對應項目中的config文件夾
-
config
-
├── browser.options.ts # 窗口的配置
-
├── classNames.options.ts # 樣式名的配置,背景樣式都通過這個文件渲染
-
├── editorIcons.options.ts # 編輯頁面的一些editor圖標
-
├── index.ts # 導出
-
└── shortcuts.keys.ts # 禁用的一些快捷鍵,electron是基於chromium瀏覽器,所以也存在一些瀏覽器快捷鍵比如F5
browser.options
這個文件的主要作用就是配置主窗口和編輯窗口區分開發正式的配置,寬高等等,以及要顯示的主頁面
-
/**
-
* 軟件數據和配置
-
* C:\Users\{用戶名}\AppData\Roaming
-
* 共享
-
* C:\ProgramData\Intel\ShaderCache\i-notes{xx}
-
* 快捷方式
-
* C:\Users\{用戶名}\AppData\Roaming\Microsoft\Windows\Recent
-
* 電腦自動創建緩存
-
* C:\Windows\Prefetch\I-NOTES.EXE{xx}
-
*/
-
-
/** */
-
const globalEnv = process.env.NODE_ENV;
-
-
const devWid = globalEnv === 'development' ? 950 : 0;
-
const devHei = globalEnv === 'development' ? 600 : 0;
-
-
// 底部icon: 40*40
-
const editorWindowOptions = {
-
width: devWid || 290,
-
height: devHei || 350,
-
minWidth: 250
-
};
-
-
/**
-
* BrowserWindow的配置項
-
* @param type 單獨給編輯窗口的配置
-
*/
-
const browserWindowOption = (type?: 'editor'): Electron.BrowserWindowConstructorOptions => {
-
const commonOptions = {
-
minHeight: 48,
-
frame: false,
-
hasShadow: true,
-
transparent: true,
-
webPreferences: {
-
enableRemoteModule: true,
-
nodeIntegration: true
-
}
-
};
-
if (!type) {
-
return {
-
width: devWid || 350,
-
height: devHei || 600,
-
minWidth: 320,
-
...commonOptions
-
};
-
}
-
return {
-
...editorWindowOptions,
-
...commonOptions
-
};
-
};
-
-
/**
-
* 開發環境: http://localhost:8080
-
* 正式環境: file://${__dirname}/index.html
-
*/
-
const winURL = globalEnv === 'development' ? 'http://localhost:8080' : `file://${__dirname}/index.html`;
-
-
router
增加
meta中的title屬性,顯示在軟件上方頭部
-
import { createRouter, createWebHashHistory } from 'vue-router';
-
import { RouteRecordRaw } from 'vue-router';
-
import main from '../views/main.vue';
-
-
const routes: Array<RouteRecordRaw> = [
-
{
-
path: '/',
-
name: 'main',
-
component: main,
-
children: [
-
{
-
path: '/',
-
name: 'index',
-
component: () => import('../views/index.vue'),
-
meta: {
-
title: 'I便箋'
-
}
-
},
-
{
-
path: '/editor',
-
name: 'editor',
-
component: () => import('../views/editor.vue'),
-
meta: {
-
title: ''
-
}
-
},
-
{
-
path: '/setting',
-
name: 'setting',
-
component: () => import('../views/setting.vue'),
-
meta: {
-
title: '設置'
-
}
-
}
-
]
-
}
-
];
-
-
const router = createRouter({
-
history: createWebHashHistory(process.env.BASE_URL),
-
routes
-
});
-
-
export default router;
main.vue
main.vue文件主要是作為一個整體框架,考慮到頁面切換時候的動效,分為頭部和主體部分,頭部作為一個單獨的組件處理,內容區域使用router-view渲染。html部分,這里和vue2.x有點區別的是,在vue2.x中可以直接-
// bad
-
<transition name= "fade">
-
<keep-alive>
-
<router-view />
-
</keep-alive>
-
</transition>
上面的這種寫法在vue3中會在控制台報異常,記不住寫法的可以看看控制台????????
-
<router-view v-slot= "{ Component }">
-
<transition name= "main-fade">
-
<div class= "transition" :key="routeName">
-
<keep-alive>
-
<component :is= "Component" />
-
</keep-alive>
-
</div>
-
</transition>
-
</router-view>
然后就是ts部分了,使用vue3的寫法去寫,
script標簽注意需要寫上lang="ts"代表是ts語法。router的寫法也不一樣,雖然在vue3中還能寫vue2的格式,但是不推薦使用。這里是獲取route的name屬性,來進行一個頁面過渡的效果。-
<script lang= "ts">
-
import { defineComponent, ref, onBeforeUpdate } from 'vue';
-
import { useRoute } from 'vue-router';
-
import Header from '@/components/header.vue';
-
-
export default defineComponent({
-
components: {
-
Header
-
},
-
setup() {
-
const routeName = ref(useRoute().name);
-
-
onBeforeUpdate(() => {
-
routeName.value = useRoute().name;
-
});
-
-
return {
-
routeName
-
};
-
}
-
});
-
</script>
less部分
-
<style lang= "less" scoped>
-
.main-fade-enter,
-
.main-fade-leave-to {
-
display: none;
-
opacity: 0;
-
animation: main-fade 0.4s reverse;
-
}
-
.main-fade-enter-active,
-
.main-fade-leave-active {
-
opacity: 0;
-
animation: main-fade 0.4s;
-
}
-
@keyframes main-fade {
-
from {
-
opacity: 0;
-
transform: scale( 0.96);
-
}
-
to {
-
opacity: 1;
-
transform: scale( 1);
-
}
-
}
-
</style>
以上就是
main.vue的內容,在頁面刷新或者進入的時候根據useRouter().name的切換進行放大的過渡效果,后面的內容會更簡潔一點。header.vue
onBeforeRouteUpdate
頭部組件還有一個標題過渡的效果,根據路由導航獲取當前路由的
mate.title變化進行過渡效果。vue3中路由守衛需要從vue-route導入使用。-
import { onBeforeRouteUpdate, useRoute } from 'vue-router';
-
...
-
onBeforeRouteUpdate((to, from, next) => {
-
title.value = to.meta.title;
-
currentRouteName.value = to.name;
-
next();
-
});
computed
這里是計算不同的路由下標題內邊距的不同,首頁是有個設置入口的按鈕,而設置頁面是只有兩個按鈕,
computed會返回一個你需要的新的值
-
// 獲取首頁的內邊距
-
const computedPaddingLeft = computed(() => {
-
return currentRouteName.value === 'index' ? 'padding-left: 40px;' : '';
-
});
emit子傳父和props父傳子
vue3沒有了
this,那么要使用emit怎么辦呢?在入口setup中有2個參數setup(props, content) {}
props是父組件傳給子組件的內容,props常用的emit和props都在content中。????這里需要注意的是,使用
props和emit需要先定義,才能去使用,並且會在vscode中直接調用時輔助彈窗顯示props示例
emit示例
-
export default defineComponent({
-
props: {
-
test: String
-
},
-
emits: [ 'option-click', 'on-close'],
-
// 如果只用emit的話可以使用es6解構
-
// 如:setup(props, { emit })
-
setup(props, content) {
-
console.log(props.test, content.emit( 'option-click'));
-
}
-
})
electron打開窗口
-
import { browserWindowOption } from '@/config';
-
import { createBrowserWindow, transitCloseWindow } from '@/utils';
-
...
-
const editorWinOptions = browserWindowOption('editor');
-
// 打開新窗口
-
const openNewWindow = () => {
-
createBrowserWindow(editorWinOptions, '/editor');
-
};
electron圖釘固定屏幕前面
先獲取當前屏幕實例
????這里需要注意的是,需要從
remote獲取當前窗口信息判斷當前窗口是否在最前面
isAlwaysOnTop(),然后通過setAlwaysOnTop()屬性設置當前窗口最前面。-
import { remote } from 'electron';
-
...
-
// 獲取窗口固定狀態
-
let isAlwaysOnTop = ref( false);
-
const currentWindow = remote.getCurrentWindow();
-
isAlwaysOnTop.value = currentWindow.isAlwaysOnTop();
-
-
// 固定前面
-
const drawingPin = () => {
-
if (isAlwaysOnTop.value) {
-
currentWindow.setAlwaysOnTop( false);
-
isAlwaysOnTop.value = false;
-
} else {
-
currentWindow.setAlwaysOnTop( true);
-
isAlwaysOnTop.value = true;
-
}
-
};
electron關閉窗口
這里是在
utils封裝了通過對dom的樣式名操作,達到一個退出的過渡效果,然后再關閉。-
// 過渡關閉窗口
-
export const transitCloseWindow = (): void => {
-
document.querySelector( '#app')?.classList.remove('app-show');
-
document.querySelector( '#app')?.classList.add('app-hide');
-
remote.getCurrentWindow(). close();
-
};
noteDb數據庫
安裝nedb數據庫,文檔: www.w3cschool.cn/nedbintro/n…[5]
yarn add nedb @types/nedb數據儲存在
nedb中,定義字段,並在根目錄的shims-vue.d.ts加入類型-
/**
-
* 儲存數據庫的
-
*/
-
interface DBNotes {
-
className: string; // 樣式名
-
content: string; // 內容
-
readonly createdAt: Date; // 創建時間,這個時間是nedb自動生成的
-
readonly uid: string; // uid,utils中的方法生成
-
readonly updatedAt: Date; // update,自動創建的
-
readonly _id: string; // 自動創建的
-
}
對nedb的封裝
自我感覺這里寫的有點爛。。。勿噴,持續學習中
這里的
QueryDB是shims-vue.d.ts定義好的類型這里的意思是
QueryDB<T>是一個對象,然后這個對象傳入一個泛型T,這里keyof T獲取這個對象的key(屬性)值,?:代表這個key可以是undefined,表示可以不存在。T[K]表示從這個對象中獲取這個K的值。-
type QueryDB<T> = {
-
[K in keyof T]?: T[K];
-
};
-
import Datastore from 'nedb';
-
import path from 'path';
-
import { remote } from 'electron';
-
-
/**
-
* @see https://www.npmjs.com/package/nedb
-
*/
-
class INoteDB<G = any> {
-
/**
-
* 默認儲存位置
-
* C:\Users\{Windows User Name}\AppData\Roaming\i-notes
-
*/
-
// dbPath = path.join(remote.app.getPath('userData'), 'db/inote.db');
-
// dbPath = './db/inote.db';
-
dbPath = this.path;
-
-
_db: Datastore<Datastore.DataStoreOptions> = this.backDatastore;
-
-
get path() {
-
if (process.env.NODE_ENV === 'development') {
-
return path.join(__dirname, 'db/inote.db');
-
}
-
return path.join(remote.app.getPath('userData'), 'db/inote.db');
-
}
-
-
get backDatastore() {
-
return new Datastore({
-
/**
-
* autoload
-
* default: false
-
* 當數據存儲被創建時,數據將自動從文件中加載到內存,不必去調用loadDatabase
-
* 注意所有命令操作只有在數據加載完成后才會被執行
-
*/
-
autoload: true,
-
filename: this.dbPath,
-
timestampData: true
-
});
-
}
-
-
refreshDB() {
-
this._db = this.backDatastore;
-
}
-
-
insert<T extends G>(doc: T) {
-
return new Promise((resolve: (value: T) => void) => {
-
this._db.insert(doc, (error: Error | null, document: T) => {
-
if (!error) resolve(document);
-
});
-
});
-
}
-
-
/**
-
* db.find(query)
-
* @param {Query<T>} query:object類型,查詢條件,可以使用空對象{}。
-
* 支持使用比較運算符($lt, $lte, $gt, $gte, $in, $nin, $ne)
-
* 邏輯運算符($or, $and, $not, $where)
-
* 正則表達式進行查詢。
-
*/
-
find(query: QueryDB<DBNotes>) {
-
return new Promise((resolve: (value: DBNotes[]) => void) => {
-
this._db.find(query, (error: Error | null, document: DBNotes[]) => {
-
if (!error) resolve(document as DBNotes[]);
-
});
-
});
-
}
-
-
/**
-
* db.findOne(query)
-
* @param query
-
*/
-
findOne(query: QueryDB<DBNotes>) {
-
return new Promise((resolve: (value: DBNotes) => void) => {
-
this._db.findOne(query, (error: Error | null, document) => {
-
if (!error) resolve(document as DBNotes);
-
});
-
});
-
}
-
-
/**
-
* db.remove(query, options)
-
* @param {Record<keyof DBNotes, any>} query
-
* @param {Nedb.RemoveOptions} options
-
* @return {BackPromise<number>}
-
*/
-
remove(query: QueryDB<DBNotes>, options?: Nedb.RemoveOptions) {
-
return new Promise((resolve: (value: number) => void) => {
-
if (options) {
-
this._db.remove(query, options, (error: Error | null, n: number) => {
-
if (!error) resolve(n);
-
});
-
} else {
-
this._db.remove(query, (error: Error | null, n: number) => {
-
if (!error) resolve(n);
-
});
-
}
-
});
-
}
-
-
update<T extends G>(query: T, updateQuery: T, options: Nedb.UpdateOptions = {}) {
-
return new Promise((resolve: (value: T) => void) => {
-
this._db.update(
-
query,
-
updateQuery,
-
options,
-
(error: Error | null, numberOfUpdated: number, affectedDocuments: T) => {
-
if (!error) resolve(affectedDocuments);
-
}
-
);
-
});
-
}
-
}
-
-
export default new INoteDB();
使用
ref和reactive代替vuex,並用watch監聽創建
exeConfig.state.ts用
ref和reactive引入的方式就可以達到vuex的state效果,這樣就可以完全舍棄掉vuex。比如軟件配置,創建exeConfig.state.ts在store中,這樣在外部.vue文件中進行更改也能去更新視圖。-
import { reactive, watch } from 'vue';
-
-
const exeConfigLocal = localStorage.getItem('exeConfig');
-
-
export let exeConfig = reactive({
-
syncDelay: 1000,
-
...
-
switchStatus: {
-
/**
-
* 開啟提示
-
*/
-
textTip: true
-
}
-
});
-
-
if (exeConfigLocal) {
-
exeConfig = reactive(JSON.parse(exeConfigLocal));
-
} else {
-
localStorage.setItem( 'exeConfig', JSON.stringify(exeConfig));
-
}
-
-
watch(exeConfig, e => {
-
localStorage.setItem( 'exeConfig', JSON.stringify(e));
-
});
vuex番外
vuex的使用是直接在項目中引入
useStore,但是是沒有state類型提示的,所以需要手動去推導state的內容。這里的S代表state的類型,然后傳入vuex中export declare class Store<S> { readonly state: S; }想要查看某個值的類型的時候在vscode中
ctrl+鼠標左鍵點進去就能看到,或者鼠標懸浮該值-
declare module 'vuex' {
-
type StoreStateType = typeof store.state;
-
export function useStore<S = StoreStateType>(): Store<S>;
-
}
index.vue
-
這里在防止沒有數據的時候頁面空白閃爍,使用一個圖片和列表區域去控制顯示,拿到數據之后就顯示列表,否則就只顯示圖片。
-
在這個頁面對
editor.vue進行了createNewNote創建便箋筆記、updateNoteItem_className更新類型更改顏色、updateNoteItem_content更新內容、removeEmptyNoteItem刪除、whetherToOpen是否打開(在editor中需要打開列表的操作)通信操作 -
以及對軟件失去焦點進行監聽
getCurrentWindow().on('blur'),如果失去焦點,那么在右鍵彈窗打開的情況下進行去除。 -
deleteActiveItem_{uid}刪除便箋筆記內容,這里在component封裝了一個彈窗組件messageBox,然后在彈窗的時候提示是否刪除和不在詢問的功能操作。 -
-
????如果
勾選不在詢問,那么在store=>exeConfig.state中做相應的更改 -
這里在設置中會進行詳細的介紹
-
開發一個vue3右鍵彈窗插件
vue3也發布了有段時間了,雖然還沒有完全穩定,但后面的時間出現的插件開發方式說不定也會多起來。插件開發思路
-
定義好插件類型,比如需要哪些屬性
MenuOptions -
判斷是否需要在觸發之后立即關閉還是繼續顯示
-
在插入
body時判斷是否存在,否則就刪除重新顯示
-
import { createApp, h, App, VNode, RendererElement, RendererNode } from 'vue';
-
import './index.css';
-
-
type ClassName = string | string[];
-
-
interface MenuOptions {
-
/**
-
* 文本
-
*/
-
text: string;
-
-
/**
-
* 是否在使用后就關閉
-
*/
-
once?: boolean;
-
-
/**
-
* 單獨的樣式名
-
*/
-
className?: ClassName;
-
-
/**
-
* 圖標樣式名
-
*/
-
iconName?: ClassName;
-
-
/**
-
* 函數
-
*/
-
handler(): void;
-
}
-
-
type RenderVNode = VNode<
-
RendererNode,
-
RendererElement,
-
{
-
[key: string]: any;
-
}
-
>;
-
-
class CreateRightClick {
-
rightClickEl?: App<Element>;
-
rightClickElBox?: HTMLDivElement | null;
-
-
constructor() {
-
this.removeRightClickHandler();
-
}
-
-
/**
-
* 渲染dom
-
* @param menu
-
*/
-
render(menu: MenuOptions[]): RenderVNode {
-
return h(
-
'ul',
-
{
-
class: [ 'right-click-menu-list']
-
},
-
[
-
...menu. map(item => {
-
return h(
-
'li',
-
{
-
class: item.className,
-
// vue3.x中簡化了render,直接onclick即可,onClick也可以
-
onclick: () => {
-
// 如果只是一次,那么點擊之后直接關閉
-
if (item.once) this.remove();
-
return item.handler();
-
}
-
},
-
[
-
// icon
-
h( 'i', {
-
class: item.iconName
-
}),
-
// text
-
h(
-
'span',
-
{
-
class: 'right-click-menu-text'
-
},
-
item.text
-
)
-
]
-
);
-
})
-
]
-
);
-
}
-
-
/**
-
* 給右鍵的樣式
-
* @param event 鼠標事件
-
*/
-
setRightClickElStyle(event: MouseEvent, len: number): void {
-
if (!this.rightClickElBox) return;
-
this.rightClickElBox.style.height = `${len * 36}px`;
-
const { clientX, clientY } = event;
-
const { innerWidth, innerHeight } = window;
-
const { clientWidth, clientHeight } = this.rightClickElBox;
-
let cssText = `height: ${len * 36}px;opacity: 1;transition: all 0.2s;`;
-
if (clientX + clientWidth < innerWidth) {
-
cssText += `left: ${clientX + 2}px;`;
-
} else {
-
cssText += `left: ${clientX - clientWidth}px;`;
-
}
-
if (clientY + clientHeight < innerHeight) {
-
cssText += `top: ${clientY + 2}px;`;
-
} else {
-
cssText += `top: ${clientY - clientHeight}px;`;
-
}
-
cssText += `height: ${len * 36}px`;
-
this.rightClickElBox.style.cssText = cssText;
-
}
-
-
remove(): void {
-
if (this.rightClickElBox) {
-
this.rightClickElBox.remove();
-
this.rightClickElBox = null;
-
}
-
}
-
-
removeRightClickHandler(): void {
-
document.addEventListener( 'click', e => {
-
if (this.rightClickElBox) {
-
const currentEl = e.target as Node;
-
if (!currentEl || !this.rightClickElBox.contains(currentEl)) {
-
this.remove();
-
}
-
}
-
});
-
}
-
-
/**
-
* 鼠標右鍵懸浮
-
* @param event
-
* @param menu
-
*/
-
useRightClick = (event: MouseEvent, menu: MenuOptions[] = []): void => {
-
this.remove();
-
if (!this.rightClickElBox || !this.rightClickEl) {
-
const createRender = this.render(menu);
-
this.rightClickEl = createApp({
-
setup() {
-
return () => createRender;
-
}
-
});
-
}
-
if (!this.rightClickElBox) {
-
this.rightClickElBox = document.createElement( 'div');
-
this.rightClickElBox.id = 'rightClick';
-
document.body.appendChild(this.rightClickElBox);
-
this.rightClickEl.mount( '#rightClick');
-
}
-
this.setRightClickElStyle(event, menu.length);
-
};
-
}
-
-
export default CreateRightClick;
右鍵彈窗插件配合electron打開、刪除便箋筆記
在使用的時候直接引入即可,如在
index.vue中使用創建右鍵的方式,這里需要額外的說明一下,打開窗口需要進行一個窗口通信判斷,ipcMain需要從remote中獲取-
每個便箋筆記都有一個
uid,也就是utils中生成的 -
每個在打開筆記的時候也就是編輯頁,需要判斷
該uid的窗口是否已經打開 -
窗口之間用
ipcRenderer和ipcMain去通信 -
判斷通信失敗的方法,用一個定時器來延時判斷是否
通信成功,因為沒有判斷通信失敗的方法 -
countFlag = true就說明打開窗口,countFlag = false說明沒有打開窗口
ipcRenderer和ipcMain通信????
on是一直處於通信狀態,once是通信一次之后就關閉了-
// countFlag是一個狀態來標記收到東西沒
-
// index問editor打開了沒有
-
ipcRenderer.send( '你好')
-
-
// 這時候editor收到消息了
-
remote.ipcMain.on( '你好', e => {
-
// 收到消息后顯示
-
remote.getCurrentWindow().show();
-
// 然后回index消息
-
e.sender.send( '你好我在的');
-
});
-
-
// index在等editor消息
-
ipcRenderer.on( '你好我在的', () => {
-
// 好的我收到了
-
countFlag = true;
-
});
-
-
// 如果沒收到消息,那標記一直是false,根據定時器來做相應操作
右鍵彈窗的使用
????這里的打開筆記功能會把選中的筆記
uid當作一個query參數跳轉到編輯頁-
import CreateRightClick from '@/components/rightClick';
-
...
-
const rightClick = new CreateRightClick();
-
...
-
const contextMenu = (event: MouseEvent, uid: string) => {
-
rightClick.useRightClick(event, [
-
{
-
text: '打開筆記',
-
once: true,
-
iconName: [ 'iconfont', 'icon-newopen'],
-
handler: () => {
-
let countFlag = false;
-
ipcRenderer.send( `${uid}_toOpen`);
-
ipcRenderer.on( `get_${uid}_toOpen`, () => {
-
countFlag = true;
-
});
-
setTimeout(() => {
-
if (!countFlag) openEditorWindow(uid);
-
}, 100);
-
}
-
},
-
{
-
text: '刪除筆記',
-
once: true,
-
iconName: [ 'iconfont', 'icon-delete'],
-
handler: () => {
-
deleteCurrentUid.value = uid;
-
if (exeConfig.switchStatus.deleteTip) {
-
deleteMessageShow.value = true;
-
} else {
-
// 根據彈窗組件進行判斷
-
onConfirm();
-
}
-
}
-
}
-
]);
-
};
-
...
editor.vue重點
這個
editor.vue是view/文件夾下的,以下對本頁面統稱編輯頁,更好區分editor組件和頁面 開發思路-
打開
新增編輯頁窗口時就生成uid並向數據庫nedb添加數據,並向列表頁通信ipcRenderer.send('createNewNote', res) -
需要使用富文本,能實時處理格式
document.execCommand -
頁面加載完時進行聚焦
createRange和getSelection -
對列表頁實時更新,編輯的時候防抖函數
debounce可以控制輸入更新,這個時間在設置是可控的 -
圖釘固定在header.vue已經說明 -
選項功能能選擇顏色,打開列表之后需要判斷是否已經打開列表窗口 -
在
點擊關閉的時候需要刪除數據庫本條數據,如果沒有輸入內容就刪除數據庫uid內容並向列表頁通信removeEmptyNoteItem -
在列表頁時關閉本窗口的一個通信
deleteActiveItem_{uid} -
列表頁
打開筆記時,攜帶uid,在編輯頁根據是否攜帶uid查詢該條數據庫內容
富文本編輯做成了一個單獨的組件,使
編輯頁的代碼不會太臃腫document.execCommand文檔 developer.mozilla.org/zh-CN/docs/…[6]
首先在編輯頁對路由進行判斷是否存在,如果不存在就創建,否則就查詢並把查詢到的筆記傳給
editor組件<Editor :content="editContent" :className="currentBgClassName" @on-input="changeEditContent" />-
const routeUid = useRoute().query.uid as string;
-
if (routeUid) {
-
// 查詢
-
uid.value = routeUid;
-
getCurUidItem(routeUid);
-
} else {
-
// 生成uid並把uid放到地址欄
-
const uuidString = uuid();
-
uid.value = uuidString;
-
useRouter().push({
-
query: {
-
uid: uuidString
-
}
-
});
-
// 插入數據庫並向列表頁通信
-
...
-
}
富文本聚焦和ref獲取dom節點
原理是通過
getSelection選擇光標和createRange文本范圍兩個方法,選中富文本節點。獲取-
import { defineComponent, onMounted, ref, Ref, watch } from 'vue';
-
...
-
// setup中創建一個和<div ref="editor">同名的變量,就可以直接拿到dom節點,一定要return!!!
-
let editor: Ref<HTMLDivElement | null> = ref(null);
-
-
onMounted(() => {
-
focus();
-
});
-
-
const focus = () => {
-
const range = document.createRange();
-
range.selectNodeContents(editor.value as HTMLDivElement);
-
range.collapse(false);
-
const selecton = window.getSelection() as Selection;
-
selecton.removeAllRanges();
-
selecton.addRange( range);
-
};
-
-
...
-
return {
-
editor,
-
...
-
}
editor組件的父傳子以及watch監聽
????這里需要注意的是因為在父組件傳給子組件,然后子組件進行更新一次會導致富文本無法撤回,相當於重新給富文本組件賦值渲染了一次,因此這里就只用一次
props.content-
export default defineComponent({
-
props: {
-
content: String,
-
className: String
-
},
-
emits: [ 'on-input'],
-
setup(props, { emit }) {
-
let editor: Ref<HTMLDivElement | null> = ref(null);
-
const bottomIcons = editorIcons;
-
const editorContent: Ref<string | undefined> = ref('');
-
-
// 監聽從父組件傳來的內容,因為是從數據庫查詢所以會有一定的延遲
-
watch(props, nv => {
-
if (!editorContent.value) {
-
// 只賦值一次
-
editorContent.value = nv.content;
-
}
-
});
-
}
-
});
editor組件的防抖子傳父
exeConfig.syncDelay是設置里面的一個時間,可以動態根據這個時間來調節儲存進數據庫和列表的更新,獲取富文本組件的html然后儲存到數據庫並傳到列表頁更新-
const changeEditorContent = debounce((e: InputEvent) => {
-
const editorHtml = (e.target as Element).innerHTML;
-
emit( 'on-input', editorHtml);
-
}, exeConfig.syncDelay);
富文本組件的粘貼純文本
vue自帶的粘貼事件,
@paste獲取到剪切板的內容,然后獲取文本格式的內容e.clipboardData?.getData('text/plain')並插入富文本-
const paste = (e: ClipboardEvent) => {
-
const pasteText = e.clipboardData?.getData('text/plain');
-
console.log(pasteText);
-
document.execCommand( 'insertText', false, pasteText);
-
};
(????????????額外的)
getCurrentInstance選擇dom方式官方和網上的例子是這樣:
<div ref="editor"></div>-
setup(props, { emit }) {
-
let editor = ref(null);
-
return { editor }
-
})
直接獲取
dom節點,但其實不管這個editor是什么,只要從setup中return,就會直接標記instance變量名,強行把內容替換成dom節點,甚至不用定義可以看看下面例子<div ref="test"></div>-
import { defineComponent, getCurrentInstance, onMounted } from 'vue';
-
...
-
setup(props, { emit }) {
-
onMounted(() => {
-
console.log(getCurrentInstance().refs);
-
// 得到的是test dom以及其他定義的節點
-
});
-
return {
-
test: ''
-
}
-
})
但是為了規范還是使用下面這樣
<div ref="dom"></div>-
const dom = ref(null);
-
return {
-
dom
-
};
setting.vue
這里的話需要用到
exeConfig.state.ts的配置信息,包括封裝的input、switch、tick組件
在這里說明一下,
自動縮小、靠邊隱藏和同步設置暫時還沒有開發的-
自動縮小: 編輯頁失去焦點時自動最小化,獲得焦點重新打開 -
靠邊隱藏: 把軟件拖動到屏幕邊緣時,自動隱藏到邊上,類似QQ那樣的功能 -
同步設置: 打算使用nestjs做同步服務,后面可能會出一篇有關的文章,但是功能一定會做的
directives自定義指令
根據是否開啟提示的設置寫的一個方便控制的功能,這個功能是首先獲取初始化的節點高度,放置在
dom的自定義數據上面data-xx,然后下次顯示的時候再重新獲取賦值css顯示,當然這里也是用了一個過渡效果使用方法
<div v-tip="switch"></div>-
export default defineComponent({
-
components: {
-
Tick,
-
Input,
-
Switch
-
},
-
directives: {
-
tip(el, { value }) {
-
const { height } = el.dataset;
-
// 儲存最初的高度
-
if (!height && height !== '0') {
-
el.dataset.height = el.clientHeight;
-
}
-
const clientHeight = height || el.clientHeight;
-
let cssText = 'transition: all 0.4s;';
-
if (value) {
-
cssText += `height: ${clientHeight}px;opacity: 1;`;
-
} else {
-
cssText += 'height: 0;opacity: 0;overflow: hidden;';
-
}
-
el.style.cssText = cssText;
-
}
-
}
-
})
原生點擊復制
原理是先隱藏一個
input標簽,然后點擊的之后選擇它的內容,在使用document.execCommand('copy')復制就可以-
<a @click= "copyEmail">復制</a>
-
<input class= "hide-input" ref="mailInput" type="text" value="heiyehk@foxmail.com" />
-
const mailInput: Ref<HTMLInputElement | null> = ref(null);
-
const copyEmail = () => {
-
if (copyStatus.value) return;
-
copyStatus.value = true;
-
mailInput.value?. select();
-
document.execCommand( 'copy');
-
};
-
-
return {
-
copyEmail
-
...
-
}
electron打開文件夾和打開默認瀏覽器鏈接
打開文件夾使用
shell這個方法-
import { remote } from 'electron';
-
-
remote.shell.showItemInFolder( 'D:');
打開默認瀏覽器鏈接
-
import { remote } from 'electron';
-
-
remote.shell.openExternal( 'www.github.com');
錯誤收集
收集一些使用中的錯誤,並使用
message插件進行彈窗提示,軟件寬高和屏幕寬高只是輔助信息。碰到這些錯誤之后,在軟件安裝位置輸出一個inoteError.log的錯誤日志文件,然后在設置中判斷文件是否存在,存在就打開目錄選中。-
版本號
-
時間
-
錯誤
-
electron版本
-
Windows信息
-
軟件寬高信息
-
屏幕寬高
比如這個框中的才是主要的信息
vue3 errorHandler
main.ts我們需要進行一下改造,並使用errorHandler進行全局的錯誤監控-
import { createApp } from 'vue';
-
import App from './App.vue';
-
import router from './router';
-
import outputErrorLog from '@/utils/errorLog';
-
-
const app = createApp(App);
-
-
// 錯誤收集方法
-
app.config.errorHandler = outputErrorLog;
-
-
app.use(router).mount( '#app');
errorLog.ts封裝對Error類型輸出為日志文件
獲取軟件安裝位置
remote.app.getPath('exe')獲取軟件安裝路徑,包含軟件名.exeexport const errorLogPath = path.join(remote.app.getPath('exe'), '../inoteError.log');輸出日志文件
flag: a代表末尾追加,確保每一行一個錯誤加上換行符'\n'fs.writeFileSync(errorLogPath, JSON.stringify(errorLog) + '\n', { flag: 'a' });errorLog.ts的封裝,對Error類型的封裝-
import { ComponentPublicInstance } from 'vue';
-
import dayjs from 'dayjs';
-
import fs from 'fs-extra';
-
import os from 'os';
-
import { remote } from 'electron';
-
import path from 'path';
-
import useMessage from '@/components/message';
-
-
function getShortStack(stack?: string): string {
-
const splitStack = stack?.split('\n ');
-
if (!splitStack) return '';
-
const newStack: string[] = [];
-
for (const line of splitStack) {
-
// 其他信息
-
if (line.includes('bundler')) continue;
-
-
// 只保留錯誤文件信息
-
if (line.includes('?!.')) {
-
newStack.push(line.replace(/webpack-internal:\/\/\/\.\/node_modules\/.+\?!/, ''));
-
} else {
-
newStack.push(line);
-
}
-
}
-
// 轉換string
-
return newStack.join('\n ');
-
}
-
-
export const errorLogPath = path.join(remote.app.getPath('exe'), '../inoteError.log');
-
-
export default function(error: unknown, vm: ComponentPublicInstance | null, info: string): void {
-
const { message, stack } = error as Error;
-
const { electron, chrome, node, v8 } = process.versions;
-
const { outerWidth, outerHeight, innerWidth, innerHeight } = window;
-
const { width, height } = window.screen;
-
-
// 報錯信息
-
const errorInfo = {
-
errorInfo: info,
-
errorMessage: message,
-
errorStack: getShortStack(stack)
-
};
-
-
// electron
-
const electronInfo = { electron, chrome, node, v8 };
-
-
// 瀏覽器窗口信息
-
const browserInfo = { outerWidth, outerHeight, innerWidth, innerHeight };
-
-
const errorLog = {
-
versions: remote.app.getVersion(),
-
date: dayjs().format( 'YYYY-MM-DD HH:mm'),
-
error: errorInfo,
-
electron: electronInfo,
-
window: {
-
type: os.type(),
-
platform: os.platform()
-
},
-
browser: browserInfo,
-
screen: { width, height }
-
};
-
-
useMessage( '程序出現異常', 'error');
-
-
if (process.env.NODE_ENV === 'production') {
-
fs.writeFileSync(errorLogPath, JSON.stringify(errorLog) + '\n', { flag: 'a' });
-
} else {
-
console.log(error);
-
console.log(errorInfo.errorStack);
-
}
-
}
使用此方法后封裝的結果是這樣的,
message插件具體看component
這個是之前的錯誤日志文件

獲取electron版本等信息
const appInfo = process.versions;打包
這個倒是沒什么好講的了,主要還是在
vue.config.js文件中進行配置一下,然后使用命令yarn electron:build即可,當然了,還有一個打包前清空的舊的打包文件夾的腳本deleteBuild.js
打包清空
dist_electron舊的打包內容,因為eslint的原因,這里就用eslint-disable關掉了幾個原理就是先獲取
vue.config.js中的打包配置,如果重新配置了路徑directories.output就動態去清空-
const rm = require('rimraf');
-
const path = require('path');
-
const pluginOptions = require('../../vue.config').pluginOptions;
-
-
let directories = pluginOptions.electronBuilder.builderOptions.directories;
-
let buildPath = '';
-
-
if (directories && directories.output) {
-
buildPath = directories.output;
-
}
-
-
// 刪除作用只用於刪除打包前的buildPath || dist_electron
-
// dist_electron是默認打包文件夾
-
rm(path.join(__dirname, `../../${buildPath || 'dist_electron'}`), () => {});
以上就是本篇主要開發內容了,歡迎支持我的開源項目electron-vue3-inote。
相關資料
github地址: https://github.com/heiyehk/electron-vue3-inote
文章參考: https://juejin.cn/post/6909723449246089224
❤️ 看完三件事
如果你覺得這篇內容對你挺有啟發,我想邀請你幫我三個小忙:
-
點個【在看】,或者分享轉發,讓更多的人也能看到這篇內容
-
關注公眾號【趣談前端】,定期分享 工程化 / 可視化 / 低代碼 / 優秀開源。

-
