不要讓自己的上限成為你的底線
本來以為有萬字的。。沒想到才堪堪近6000字。為了水文的嫌疑,只挑了重點的地方講,比如component
內的元件就挑了右鍵彈窗去說明
,建議在看本文的時候邊檢視專案,有不懂的可以在下方評論,謝謝。
github
github: https://github.com/heiyehk/electron-vue3-inote
包下載
release: https://github.com/heiyehk/electron-vue3-inote/releases
接上篇配置篇 【electron+vue3+ts實戰便箋exe】一、搭建框架配置,這裡更新了一下vue3的版本3.0.4
,本篇文章只講開發內容,主要還是vue3
方面,長文警告。ps:smartblue這個主題好好看。。。
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;
utils
/* eslint-disable @typescript-eslint/ban-types */
import { winURL } from '@/config';
import { BrowserWindow, remote } from 'electron';
type FunctionalControl = (this: any, fn: any, delay?: number) => (...args: any) => void;
type DebounceEvent = FunctionalControl;
type ThrottleEvent = FunctionalControl;
// 防抖函式
export const debounce: DebounceEvent = function(fn, delay = 1000) {
let timer: NodeJS.Timeout | null = null;
return (...args: any) => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
};
// 節流函式
export const throttle: ThrottleEvent = function(fn, delay = 500) {
let flag = true;
return (...args: any) => {
if (!flag) return;
flag = false;
setTimeout(() => {
fn.apply(this, args);
flag = true;
}, delay);
};
};
// 建立視窗
export const createBrowserWindow = (bwopt = {}, url = '/', devTools = true): BrowserWindow | null => {
let childrenWindow: BrowserWindow | null;
childrenWindow = new remote.BrowserWindow(bwopt);
if (process.env.NODE_ENV === 'development' && devTools) {
childrenWindow.webContents.openDevTools();
}
childrenWindow.loadURL(`${winURL}/#${url}`);
childrenWindow.on('closed', () => {
childrenWindow = null;
});
return childrenWindow;
};
// 過渡關閉視窗
export const transitCloseWindow = (): void => {
document.querySelector('#app')?.classList.remove('app-show');
document.querySelector('#app')?.classList.add('app-hide');
remote.getCurrentWindow().close();
};
// uuid
export const uuid = (): string => {
const S4 = () => {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
};
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資料庫,文件: https://www.w3cschool.cn/nedbintro/nedbintro-t9z327mh.html
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文件
https://developer.mozilla.org/zh-CN/docs/Web/API/Document/execCommand
首先在編輯頁對路由進行判斷是否存在,如果不存在就建立,否則就查詢並把查詢到的筆記傳給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
};
此處推廣一下一個98年大佬的vue3原始碼解析github: https://github.com/Kingbultsea/vue3-analysis
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')
獲取軟體安裝路徑,包含軟體名.exe
export 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'}`), () => {});
結尾
以上就是本篇主要開發內容了,有錯誤的地方可以在下方評論,我會及時改正。有不理解或者缺少的地方也可以在下方評論。順便,能讓我上個推薦嗎= =
人學始知道不學非自然
如果有不好的地方勿噴,及時評論及時改正,謝謝!內容有點多。