前端實戰:electron+vue3+ts開發桌面端便簽應用


 

前端時間我的一個朋友為了快速熟悉 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

接下來我們來看看具體的演示效果:


具體實現過程, 內容很長, 建議先點贊收藏, 再一步步學習, 接下來會就該項目的每一個重點細節做詳細的分析.

開發思路

  1. 頁面:

  • 列表頁index.vue 頭部、搜索、內容部分,只能有一個列表頁存在

  • 設置頁setting.vue 設置內容和軟件信息,和列表頁一樣只能有一個存在

  • 編輯頁 editor.vue icons功能和背景顏色功能,可以多個編輯頁同時存在

  • 動效:

    • 打開動效,有一個放大、透明度的過渡,放不了動圖這里暫時不演示了。

    • 標題過渡效果

    • 切換indexsetting時頭部不變,內容過渡

  • 數據儲存:數據的創建和更新都在編輯頁editor.vue進行,這個過程中在儲存進nedb之后才通信列表頁index.vue更新內容,考慮到性能問題,這里使用了防抖防止連續性的更新而導致卡頓(不過貌似沒有這個必要。。也算是一個小功能吧,然后可以設置這個更新速度)

  • 錯誤采集:采集在使用中的錯誤並彈窗提示

  • 編輯顯示:document暴露 execCommand 方法,該方法允許運行命令來操縱可編輯內容區域的元素。

  • 在開發的時候還遇到過好多坑,這些都是在electron環境中才有,比如

    1. @input觸發2次,加上v-model觸發3次。包括創建一個新的electron框架也是這樣,別人電腦上不會出現這個問題,猜測是electron緩存問題

    2. vue3碰到空屬性報錯時無限報錯,在普通瀏覽器(edge和chrome)是正常一次

    3. 組件無法正常渲染不報錯,只在控制台報異常

    4. 打包后由於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(雖然說是手動。。其實就兩個步驟)

    目錄結構

    1.  
      electron-vue-notes
    2.  
      ├── public
    3.  
      │   ├── css
    4.  
      │   ├── font
    5.  
      │   └── index.html
    6.  
      ├── src
    7.  
      │   ├── assets
    8.  
      │   │   └── empty-content.svg
    9.  
      │   ├── components
    10.  
      │   │   ├── message
    11.  
      │   │   ├── rightClick
    12.  
      │   │   ├── editor.vue
    13.  
      │   │   ├── header.vue
    14.  
      │   │   ├── input.vue
    15.  
      │   │   ├── messageBox.vue
    16.  
      │   │   ├──  switch.vue
    17.  
      │   │   └── tick.vue
    18.  
      │   ├── config
    19.  
      │   │   ├── browser.options.ts
    20.  
      │   │   ├── classNames.options.ts
    21.  
      │   │   ├── editorIcons.options.ts
    22.  
      │   │   ├── index.ts
    23.  
      │   │   └── shortcuts.keys.ts
    24.  
      │   ├── inotedb
    25.  
      │   │   └── index.ts
    26.  
      │   ├── less
    27.  
      │   │   └── index.less
    28.  
      │   ├── router
    29.  
      │   │   └── index.ts
    30.  
      │   ├── script
    31.  
      │   │   └── deleteBuild.js
    32.  
      │   ├── store
    33.  
      │   │   ├── exeConfig.state.ts
    34.  
      │   │   └── index.ts
    35.  
      │   ├── utils
    36.  
      │   │   ├── errorLog.ts
    37.  
      │   │   └── index.ts
    38.  
      │   ├── views
    39.  
      │   │   ├── editor.vue
    40.  
      │   │   ├── index.vue
    41.  
      │   │   ├── main.vue
    42.  
      │   │   └── setting.vue
    43.  
      │   ├── App.vue
    44.  
      │   ├── background.ts
    45.  
      │   ├── main.ts
    46.  
      │   └── shims-vue.d.ts
    47.  
      ├── .browserslistrc
    48.  
      ├── .eslintrc.js
    49.  
      ├── .prettierrc.js
    50.  
      ├── babel.config.js
    51.  
      ├── inoteError.log
    52.  
      ├── LICENSE
    53.  
      ├──  package-lock.json
    54.  
      ├──  package.json
    55.  
      ├── README.md
    56.  
      ├── tsconfig.json
    57.  
      ├── vue.config.js
    58.  
      └── yarn.lock 

    使用腳手架搭建vue3環境

    沒有腳手架的可以先安裝腳手架

    npm install -g @vue/cli 
    

    創建vue3項目

    1.  
      vue create electron-vue-notes
    2.  
       
    3.  
      # 后續
    4.  
      ? Please pick a preset: (Use arrow keys)
    5.  
        Default ([Vue  2] babel, eslint)
    6.  
        Default (Vue  3 Preview) ([Vue 3] babel, eslint)
    7.  
      > Manually  select features 
    8.  
      # 手動選擇配置
    9.  
       
    10.  
      # 后續所有配置
    11.  
      ? Please pick a preset: Manually  select features
    12.  
      ? Check the features needed  for your project: Choose Vue version, Babel, TS, Router, CSS Pre-processors, Linter
    13.  
      ? Choose a version of Vue.js that you want to start the project with  3.x (Preview)
    14.  
      ? Use class-style component syntax? Yes
    15.  
      ? Use Babel alongside TypeScript (required  for modern mode, auto-detected polyfills, transpiling JSX)? Yes
    16.  
      ? Use history mode  for router? (Requires proper server setup for index fallback in production) No
    17.  
      ? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by  default): Less
    18.  
      ? Pick a linter / formatter config: Prettier
    19.  
      ? Pick additional lint features: Lint on save, Lint and fix on commit
    20.  
      ? Where do you prefer placing config  for Babel, ESLint, etc.? In dedicated config files
    21.  
      ? Save this as a preset  for future projects? (y/N) n 

    創建完之后的目錄是這樣的

    1.  
      electron-vue-notes
    2.  
      ├── public
    3.  
      │   ├── favicon.ico
    4.  
      │   └── index.html
    5.  
      ├── src
    6.  
      │   ├── assets
    7.  
      │   │   └── logo.png
    8.  
      │   ├── components
    9.  
      │   │   └── HelloWorld.vue
    10.  
      │   ├── router
    11.  
      │   │   └── index.ts
    12.  
      │   ├── views
    13.  
      │   │   ├── About.vue
    14.  
      │   │   └── Home.vue
    15.  
      │   ├── App.vue
    16.  
      │   ├── main.ts
    17.  
      │   └── shims-vue.d.ts
    18.  
      ├── .browserslistrc
    19.  
      ├── .eslintrc.js
    20.  
      ├── babel.config.js
    21.  
      ├──  package.json
    22.  
      ├── README.md
    23.  
      ├── tsconfig.json
    24.  
      └── yarn.lock 

    安裝electron的依賴

    1.  
      # yarn
    2.  
      yarn add vue-cli-plugin-electron-builder electron
    3.  
       
    4.  
      # npm 或 cnpm
    5.  
      npm i vue-cli-plugin-electron-builder electron 

    安裝完之后完善一些配置,比如別名eslintprettier等等基礎配置,還有一些顏色icons等等具體可以看下面

    項目的一些基礎配置

    eslint

    使用eslint主要是規范代碼風格,不推薦tslint是因為tslint已經不更新了,tslint也推薦使用eslint 安裝eslint

    npm i eslint -g 
    

    進入項目之后初始化eslint

    1.  
      eslint --init
    2.  
       
    3.  
      # 后續配置
    4.  
      ? How would you like to use ESLint? To check syntax and find problems
    5.  
      ? What  type of modules does your project use? JavaScript modules (import/export)
    6.  
      ? Which framework does your project use? Vue.js
    7.  
      ? Does your project use TypeScript? Yes
    8.  
      ? Where does your code run? Browser, Node
    9.  
      ? What format do you want your config file to be in? JavaScript
    10.  
      The config that you 've selected requires the following dependencies:
    11.  
       
    12.  
      eslint-plugin-vue@latest @typescript-eslint/eslint-plugin@latest @typescript-eslint/parser@latest
    13.  
      ? Would you like to install them now with npm? (Y/n) y 
    14.  
       

    修改eslint配置,·.eslintrc.js,規則rules可以根據自己的喜歡配置 eslint.org/docs/user-g…[4]

    1.  
      module.exports = {
    2.  
        root:  true,
    3.  
        env: {
    4.  
          node:  true
    5.  
        },
    6.  
        extends: [
    7.  
           'plugin:vue/vue3-essential',
    8.  
           'eslint:recommended',
    9.  
           'plugin:prettier/recommended',
    10.  
           'plugin:@typescript-eslint/eslint-recommended',
    11.  
           '@vue/typescript/recommended',
    12.  
           '@vue/prettier',
    13.  
           '@vue/prettier/@typescript-eslint'
    14.  
        ],
    15.  
        parserOptions: {
    16.  
          ecmaVersion:  2020
    17.  
        },
    18.  
        rules: {
    19.  
          quotes: [ 1, 'single'],
    20.  
          semi:  1,
    21.  
           '@typescript-eslint/camelcase': 0,
    22.  
           '@typescript-eslint/no-explicit-any': 0,
    23.  
           'no-irregular-whitespace': 2,
    24.  
           'no-case-declarations': 0,
    25.  
           'no-undef': 0,
    26.  
           'eol-last': 1,
    27.  
           'block-scoped-var': 2,
    28.  
           'comma-dangle': [2, 'never'],
    29.  
           'no-dupe-keys': 2,
    30.  
           'no-empty': 1,
    31.  
           'no-extra-semi': 2,
    32.  
           'no-multiple-empty-lines': [1, { max: 1, maxEOF: 1 }],
    33.  
           'no-trailing-spaces': 1,
    34.  
           'semi-spacing': [2, { before: false, after: true }],
    35.  
           'no-unreachable': 1,
    36.  
           'space-infix-ops': 1,
    37.  
           'spaced-comment': 1,
    38.  
           'no-var': 2,
    39.  
           'no-multi-spaces': 2,
    40.  
           'comma-spacing': 1
    41.  
        }
    42.  
      }; 

    prettier

    在根目錄增加.prettierrc.js配置,根據自己的喜好進行配置,單行多少個字符、單引號、分號、逗號結尾等等

    1.  
      module.exports = {
    2.  
        printWidth:  120,
    3.  
        singleQuote:  true,
    4.  
        semi:  true,
    5.  
        trailingComma:  'none'
    6.  
      }; 

    tsconfig.json

    如果這里沒有配置識別@/路徑的話,在項目中使用會報錯

    1.  
      "paths": {
    2.  
         "@/*": [
    3.  
           "src/*"
    4.  
        ]
    5.  

    package.json

    1.  
      "author": "heiyehk",
    2.  
      "description": "I便箋個人開發者heiyehk獨立開發,在Windows中更方便的記錄文字。",
    3.  
      "main": "background.js",
    4.  
      "scripts": {
    5.  
         "lint": "vue-cli-service lint",
    6.  
         "electron:build": "vue-cli-service electron:build",
    7.  
         "electron:serve": "vue-cli-service electron:serve"
    8.  

    配置入口文件background.ts

    因為需要做一些打開和關閉的動效,因此我們需要配置electronframe無邊框透明transparent的屬性

    1.  
      /* eslint-disable @typescript-eslint/no-empty-function */
    2.  
      'use strict';
    3.  
       
    4.  
      import { app, protocol, BrowserWindow, globalShortcut } from 'electron';
    5.  
      import {
    6.  
        createProtocol
    7.  
         // installVueDevtools
    8.  
      } from  'vue-cli-plugin-electron-builder/lib';
    9.  
       
    10.  
      const isDevelopment = process.env.NODE_ENV !== 'production';
    11.  
       
    12.  
      let win: BrowserWindow | null;
    13.  
      protocol.registerSchemesAsPrivileged([
    14.  
        {
    15.  
          scheme:  'app',
    16.  
          privileges: {
    17.  
            secure:  true,
    18.  
            standard:  true
    19.  
          }
    20.  
        }
    21.  
      ]);
    22.  
       
    23.  
      function createWindow() {
    24.  
        win =  new BrowserWindow({
    25.  
          frame:  false, // 無邊框
    26.  
          hasShadow:  false,
    27.  
          transparent:  true, // 透明
    28.  
          width:  950,
    29.  
          height:  600,
    30.  
          webPreferences: {
    31.  
            enableRemoteModule:  true,
    32.  
            nodeIntegration:  true
    33.  
          }
    34.  
        });
    35.  
       
    36.  
         if (process.env.WEBPACK_DEV_SERVER_URL) {
    37.  
          win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
    38.  
           if (!process.env.IS_TEST) win.webContents.openDevTools();
    39.  
        }  else {
    40.  
          createProtocol( 'app');
    41.  
          win.loadURL( 'http://localhost:8080');
    42.  
        }
    43.  
       
    44.  
        win.on( 'closed', () => {
    45.  
          win = null;
    46.  
        });
    47.  
      }
    48.  
       
    49.  
      app.on( 'window-all-closed', () => {
    50.  
         if (process.platform !== 'darwin') {
    51.  
          app.quit();
    52.  
        }
    53.  
      });
    54.  
       
    55.  
      app.on( 'activate', () => {
    56.  
         if (win === null) {
    57.  
          createWindow();
    58.  
        }
    59.  
      });
    60.  
       
    61.  
      app.on( 'ready', async () => {
    62.  
         // 這里注釋掉是因為會安裝tools插件,需要屏蔽掉,有能力的話可以打開注釋
    63.  
         // if (isDevelopment && !process.env.IS_TEST) {
    64.  
         //   try {
    65.  
         //     await installVueDevtools();
    66.  
         //   } catch (e) {
    67.  
         //     console.error('Vue Devtools failed to install:', e.toString());
    68.  
         //   }
    69.  
         // }
    70.  
        createWindow();
    71.  
      });
    72.  
       
    73.  
      if (isDevelopment) {
    74.  
         if (process.platform === 'win32') {
    75.  
          process.on( 'message', data => {
    76.  
             if (data === 'graceful-exit') {
    77.  
              app.quit();
    78.  
            }
    79.  
          });
    80.  
        }  else {
    81.  
          process.on( 'SIGTERM', () => {
    82.  
            app.quit();
    83.  
          });
    84.  
        }
    85.  

    啟動

    yarn electron:serve 
    

    到這里配置就算是成功搭建好這個窗口了,但是還有一些其他細節需要進行配置,比如electron打包配置,模塊化的配置等等

    常規配置

    這里配置一些常用的開發內容和一些輪子代碼, 大家可以參考 reset.csss 和 common.css 這兩個文件.

    config

    這個對應項目中的config文件夾

    1.  
      config
    2.  
      ├── browser.options.ts # 窗口的配置
    3.  
      ├── classNames.options.ts # 樣式名的配置,背景樣式都通過這個文件渲染
    4.  
      ├── editorIcons.options.ts # 編輯頁面的一些editor圖標
    5.  
      ├── index.ts # 導出
    6.  
      └── shortcuts.keys.ts # 禁用的一些快捷鍵,electron是基於chromium瀏覽器,所以也存在一些瀏覽器快捷鍵比如F5 

    browser.options

    這個文件的主要作用就是配置主窗口和編輯窗口區分開發正式的配置,寬高等等,以及要顯示的主頁面

    1.  
      /**
    2.  
       * 軟件數據和配置
    3.  
       * C:\Users\{用戶名}\AppData\Roaming
    4.  
       * 共享
    5.  
       * C:\ProgramData\Intel\ShaderCache\i-notes{xx}
    6.  
       * 快捷方式
    7.  
       * C:\Users\{用戶名}\AppData\Roaming\Microsoft\Windows\Recent
    8.  
       * 電腦自動創建緩存
    9.  
       * C:\Windows\Prefetch\I-NOTES.EXE{xx}
    10.  
       */
    11.  
       
    12.  
      /** */
    13.  
      const globalEnv = process.env.NODE_ENV;
    14.  
       
    15.  
      const devWid = globalEnv === 'development' ? 950 : 0;
    16.  
      const devHei = globalEnv === 'development' ? 600 : 0;
    17.  
       
    18.  
      // 底部icon: 40*40
    19.  
      const editorWindowOptions = {
    20.  
        width: devWid ||  290,
    21.  
        height: devHei ||  350,
    22.  
        minWidth:  250
    23.  
      };
    24.  
       
    25.  
      /**
    26.  
       * BrowserWindow的配置項
    27.  
       * @param type 單獨給編輯窗口的配置
    28.  
       */
    29.  
      const browserWindowOption = (type?: 'editor'): Electron.BrowserWindowConstructorOptions => {
    30.  
         const commonOptions = {
    31.  
          minHeight:  48,
    32.  
          frame:  false,
    33.  
          hasShadow:  true,
    34.  
          transparent:  true,
    35.  
          webPreferences: {
    36.  
            enableRemoteModule:  true,
    37.  
            nodeIntegration:  true
    38.  
          }
    39.  
        };
    40.  
         if (!type) {
    41.  
           return {
    42.  
            width: devWid ||  350,
    43.  
            height: devHei ||  600,
    44.  
            minWidth:  320,
    45.  
            ...commonOptions
    46.  
          };
    47.  
        }
    48.  
         return {
    49.  
          ...editorWindowOptions,
    50.  
          ...commonOptions
    51.  
        };
    52.  
      };
    53.  
       
    54.  
      /**
    55.  
       * 開發環境: http://localhost:8080
    56.  
       * 正式環境: file://${__dirname}/index.html
    57.  
       */
    58.  
      const winURL = globalEnv === 'development' ? 'http://localhost:8080' : `file://${__dirname}/index.html`;
    59.  
       
    60.  
      export { browserWindowOption, winURL }; 

    router

    增加meta中的title屬性,顯示在軟件上方頭部

    1.  
      import { createRouter, createWebHashHistory } from 'vue-router';
    2.  
      import { RouteRecordRaw } from 'vue-router';
    3.  
      import main from '../views/main.vue';
    4.  
       
    5.  
      const routes: Array<RouteRecordRaw> = [
    6.  
        {
    7.  
          path:  '/',
    8.  
          name:  'main',
    9.  
          component: main,
    10.  
          children: [
    11.  
            {
    12.  
              path:  '/',
    13.  
              name:  'index',
    14.  
              component: () =>  import('../views/index.vue'),
    15.  
              meta: {
    16.  
                title:  'I便箋'
    17.  
              }
    18.  
            },
    19.  
            {
    20.  
              path:  '/editor',
    21.  
              name:  'editor',
    22.  
              component: () =>  import('../views/editor.vue'),
    23.  
              meta: {
    24.  
                title:  ''
    25.  
              }
    26.  
            },
    27.  
            {
    28.  
              path:  '/setting',
    29.  
              name:  'setting',
    30.  
              component: () =>  import('../views/setting.vue'),
    31.  
              meta: {
    32.  
                title:  '設置'
    33.  
              }
    34.  
            }
    35.  
          ]
    36.  
        }
    37.  
      ];
    38.  
       
    39.  
      const router = createRouter({
    40.  
        history: createWebHashHistory(process.env.BASE_URL),
    41.  
        routes
    42.  
      });
    43.  
       
    44.  
      export  default router; 

    main.vue

    main.vue文件主要是作為一個整體框架,考慮到頁面切換時候的動效,分為頭部和主體部分,頭部作為一個單獨的組件處理,內容區域使用router-view渲染。html部分,這里和vue2.x有點區別的是,在vue2.x中可以直接

    1.  
      // bad
    2.  
      <transition name= "fade">
    3.  
        <keep-alive>
    4.  
          <router-view />
    5.  
        </keep-alive>
    6.  
      </transition> 

    上面的這種寫法在vue3中會在控制台報異常,記不住寫法的可以看看控制台????????

    1.  
      <router-view v-slot= "{ Component }">
    2.  
        <transition name= "main-fade">
    3.  
          <div class= "transition" :key="routeName">
    4.  
            <keep-alive>
    5.  
              <component :is= "Component" />
    6.  
            </keep-alive>
    7.  
          </div>
    8.  
        </transition>
    9.  
      </router-view> 

    然后就是ts部分了,使用vue3的寫法去寫,script標簽注意需要寫上lang="ts"代表是ts語法。router的寫法也不一樣,雖然在vue3中還能寫vue2的格式,但是不推薦使用。這里是獲取routename屬性,來進行一個頁面過渡的效果。

    1.  
      <script lang= "ts">
    2.  
      import { defineComponent, ref, onBeforeUpdate } from 'vue';
    3.  
      import { useRoute } from 'vue-router';
    4.  
      import Header from '@/components/header.vue';
    5.  
       
    6.  
      export  default defineComponent({
    7.  
        components: {
    8.  
          Header
    9.  
        },
    10.  
        setup() {
    11.  
           const routeName = ref(useRoute().name);
    12.  
       
    13.  
          onBeforeUpdate(() => {
    14.  
            routeName.value = useRoute().name;
    15.  
          });
    16.  
       
    17.  
           return {
    18.  
            routeName
    19.  
          };
    20.  
        }
    21.  
      });
    22.  
      </script> 

    less部分

    1.  
      <style lang= "less" scoped>
    2.  
      .main-fade-enter,
    3.  
      .main-fade-leave-to {
    4.  
      display: none;
    5.  
      opacity: 0;
    6.  
      animation: main-fade 0.4s reverse;
    7.  
      }
    8.  
      .main-fade-enter-active,
    9.  
      .main-fade-leave-active {
    10.  
      opacity: 0;
    11.  
      animation: main-fade 0.4s;
    12.  
      }
    13.  
      @keyframes main-fade {
    14.  
      from {
    15.  
      opacity: 0;
    16.  
      transform: scale( 0.96);
    17.  
      }
    18.  
      to {
    19.  
      opacity: 1;
    20.  
      transform: scale( 1);
    21.  
      }
    22.  
      }
    23.  
      </style>

    以上就是main.vue的內容,在頁面刷新或者進入的時候根據useRouter().name的切換進行放大的過渡效果,后面的內容會更簡潔一點。

    header.vue

    onBeforeRouteUpdate

    頭部組件還有一個標題過渡的效果,根據路由導航獲取當前路由的mate.title變化進行過渡效果。vue3中路由守衛需要從vue-route導入使用。

    1.  
      import { onBeforeRouteUpdate, useRoute } from 'vue-router';
    2.  
      ...
    3.  
      onBeforeRouteUpdate((to, from, next) => {
    4.  
        title.value = to.meta.title;
    5.  
        currentRouteName.value = to.name;
    6.  
        next();
    7.  
      }); 

    computed

    這里是計算不同的路由下標題內邊距的不同,首頁是有個設置入口的按鈕,而設置頁面是只有兩個按鈕,computed會返回一個你需要的新的值

    1.  
      // 獲取首頁的內邊距
    2.  
      const computedPaddingLeft = computed(() => {
    3.  
         return currentRouteName.value === 'index' ? 'padding-left: 40px;' : '';
    4.  
      }); 

    emit子傳父和props父傳子

    vue3沒有了this,那么要使用emit怎么辦呢?在入口setup中有2個參數

    setup(props, content) {} 
    

    props是父組件傳給子組件的內容,props常用的emitprops都在content中。

    ????這里需要注意的是,使用propsemit需要先定義,才能去使用,並且會在vscode中直接調用時輔助彈窗顯示

    props示例

    emit示例

    1.  
      export  default defineComponent({
    2.  
        props: {
    3.  
          test: String
    4.  
        },
    5.  
        emits: [ 'option-click', 'on-close'],
    6.  
         // 如果只用emit的話可以使用es6解構
    7.  
         // 如:setup(props, { emit })
    8.  
        setup(props, content) {
    9.  
          console.log(props.test, content.emit( 'option-click'));
    10.  
        }
    11.  
      }) 

    electron打開窗口

    1.  
      import { browserWindowOption } from '@/config';
    2.  
      import { createBrowserWindow, transitCloseWindow } from '@/utils';
    3.  
      ...
    4.  
      const editorWinOptions = browserWindowOption('editor');
    5.  
      // 打開新窗口
    6.  
      const openNewWindow = () => {
    7.  
        createBrowserWindow(editorWinOptions,  '/editor');
    8.  
      }; 

    electron圖釘固定屏幕前面

    先獲取當前屏幕實例

    ????這里需要注意的是,需要從remote獲取當前窗口信息

    判斷當前窗口是否在最前面isAlwaysOnTop(),然后通過setAlwaysOnTop()屬性設置當前窗口最前面。

    1.  
      import { remote } from 'electron';
    2.  
      ...
    3.  
      // 獲取窗口固定狀態
    4.  
      let isAlwaysOnTop = ref( false);
    5.  
      const currentWindow = remote.getCurrentWindow();
    6.  
      isAlwaysOnTop.value = currentWindow.isAlwaysOnTop();
    7.  
       
    8.  
      // 固定前面
    9.  
      const drawingPin = () => {
    10.  
         if (isAlwaysOnTop.value) {
    11.  
          currentWindow.setAlwaysOnTop( false);
    12.  
          isAlwaysOnTop.value =  false;
    13.  
        }  else {
    14.  
          currentWindow.setAlwaysOnTop( true);
    15.  
          isAlwaysOnTop.value =  true;
    16.  
        }
    17.  
      }; 

    electron關閉窗口

    這里是在utils封裝了通過對dom的樣式名操作,達到一個退出的過渡效果,然后再關閉。

    1.  
      // 過渡關閉窗口
    2.  
      export  const transitCloseWindow = (): void => {
    3.  
        document.querySelector( '#app')?.classList.remove('app-show');
    4.  
        document.querySelector( '#app')?.classList.add('app-hide');
    5.  
        remote.getCurrentWindow(). close();
    6.  
      }; 

    noteDb數據庫

    安裝nedb數據庫,文檔: www.w3cschool.cn/nedbintro/n…[5]

    yarn add nedb @types/nedb 
    

    數據儲存在nedb中,定義字段,並在根目錄的shims-vue.d.ts加入類型

    1.  
      /**
    2.  
       * 儲存數據庫的
    3.  
       */
    4.  
      interface DBNotes {
    5.  
        className:  string; // 樣式名
    6.  
        content:  string; // 內容
    7.  
        readonly createdAt: Date;  // 創建時間,這個時間是nedb自動生成的
    8.  
        readonly uid:  string; // uid,utils中的方法生成
    9.  
        readonly updatedAt: Date;  // update,自動創建的
    10.  
        readonly _id:  string; // 自動創建的
    11.  

    對nedb的封裝

    自我感覺這里寫的有點爛。。。勿噴,持續學習中

    這里的QueryDBshims-vue.d.ts定義好的類型

    這里的意思是QueryDB<T>是一個對象,然后這個對象傳入一個泛型T,這里keyof T獲取這個對象的key(屬性)值,?:代表這個key可以是undefined,表示可以不存在。T[K]表示從這個對象中獲取這個K的值。

    1.  
      type QueryDB<T> = {
    2.  
        [K in keyof T]?: T[K];
    3.  
      }; 
    1.  
      import Datastore from 'nedb';
    2.  
      import path from 'path';
    3.  
      import { remote } from 'electron';
    4.  
       
    5.  
      /**
    6.  
       * @see https://www.npmjs.com/package/nedb
    7.  
       */
    8.  
      class INoteDB<G = any> {
    9.  
         /**
    10.  
         * 默認儲存位置
    11.  
         * C:\Users\{Windows User Name}\AppData\Roaming\i-notes
    12.  
         */
    13.  
         // dbPath = path.join(remote.app.getPath('userData'), 'db/inote.db');
    14.  
         // dbPath = './db/inote.db';
    15.  
        dbPath = this.path;
    16.  
       
    17.  
        _db: Datastore<Datastore.DataStoreOptions> = this.backDatastore;
    18.  
       
    19.  
        get path() {
    20.  
           if (process.env.NODE_ENV === 'development') {
    21.  
             return path.join(__dirname, 'db/inote.db');
    22.  
          }
    23.  
           return path.join(remote.app.getPath('userData'), 'db/inote.db');
    24.  
        }
    25.  
       
    26.  
        get backDatastore() {
    27.  
           return new Datastore({
    28.  
             /**
    29.  
             * autoload
    30.  
             * default: false
    31.  
             * 當數據存儲被創建時,數據將自動從文件中加載到內存,不必去調用loadDatabase
    32.  
             * 注意所有命令操作只有在數據加載完成后才會被執行
    33.  
             */
    34.  
            autoload:  true,
    35.  
            filename: this.dbPath,
    36.  
            timestampData:  true
    37.  
          });
    38.  
        }
    39.  
       
    40.  
        refreshDB() {
    41.  
          this._db = this.backDatastore;
    42.  
        }
    43.  
       
    44.  
        insert<T extends G>(doc: T) {
    45.  
           return new Promise((resolve: (value: T) => void) => {
    46.  
            this._db.insert(doc, (error: Error | null, document: T) => {
    47.  
               if (!error) resolve(document);
    48.  
            });
    49.  
          });
    50.  
        }
    51.  
       
    52.  
         /**
    53.  
         * db.find(query)
    54.  
         * @param {Query<T>} query:object類型,查詢條件,可以使用空對象{}。
    55.  
         * 支持使用比較運算符($lt, $lte, $gt, $gte, $in, $nin, $ne)
    56.  
         * 邏輯運算符($or, $and, $not, $where)
    57.  
         * 正則表達式進行查詢。
    58.  
         */
    59.  
        find(query: QueryDB<DBNotes>) {
    60.  
           return new Promise((resolve: (value: DBNotes[]) => void) => {
    61.  
            this._db.find(query, (error: Error | null, document: DBNotes[]) => {
    62.  
               if (!error) resolve(document as DBNotes[]);
    63.  
            });
    64.  
          });
    65.  
        }
    66.  
       
    67.  
         /**
    68.  
         * db.findOne(query)
    69.  
         * @param query
    70.  
         */
    71.  
        findOne(query: QueryDB<DBNotes>) {
    72.  
           return new Promise((resolve: (value: DBNotes) => void) => {
    73.  
            this._db.findOne(query, (error: Error | null, document) => {
    74.  
               if (!error) resolve(document as DBNotes);
    75.  
            });
    76.  
          });
    77.  
        }
    78.  
       
    79.  
         /**
    80.  
         * db.remove(query, options)
    81.  
         * @param {Record<keyof DBNotes, any>} query
    82.  
         * @param {Nedb.RemoveOptions} options
    83.  
         * @return {BackPromise<number>}
    84.  
         */
    85.  
        remove(query: QueryDB<DBNotes>, options?: Nedb.RemoveOptions) {
    86.  
           return new Promise((resolve: (value: number) => void) => {
    87.  
             if (options) {
    88.  
              this._db.remove(query, options, (error: Error | null, n: number) => {
    89.  
                 if (!error) resolve(n);
    90.  
              });
    91.  
            }  else {
    92.  
              this._db.remove(query, (error: Error | null, n: number) => {
    93.  
                 if (!error) resolve(n);
    94.  
              });
    95.  
            }
    96.  
          });
    97.  
        }
    98.  
       
    99.  
        update<T extends G>(query: T, updateQuery: T, options: Nedb.UpdateOptions = {}) {
    100.  
           return new Promise((resolve: (value: T) => void) => {
    101.  
            this._db.update(
    102.  
              query,
    103.  
              updateQuery,
    104.  
              options,
    105.  
              (error: Error | null, numberOfUpdated: number, affectedDocuments: T) => {
    106.  
                 if (!error) resolve(affectedDocuments);
    107.  
              }
    108.  
            );
    109.  
          });
    110.  
        }
    111.  
      }
    112.  
       
    113.  
      export  default new INoteDB(); 

    使用refreactive代替vuex,並用watch監聽

    創建exeConfig.state.ts

    refreactive引入的方式就可以達到vuexstate效果,這樣就可以完全舍棄掉vuex。比如軟件配置,創建exeConfig.state.tsstore中,這樣在外部.vue文件中進行更改也能去更新視圖。

    1.  
      import { reactive, watch } from 'vue';
    2.  
       
    3.  
      const exeConfigLocal = localStorage.getItem('exeConfig');
    4.  
       
    5.  
      export let exeConfig = reactive({
    6.  
        syncDelay:  1000,
    7.  
        ...
    8.  
        switchStatus: {
    9.  
           /**
    10.  
           * 開啟提示
    11.  
           */
    12.  
          textTip:  true
    13.  
        }
    14.  
      });
    15.  
       
    16.  
      if (exeConfigLocal) {
    17.  
        exeConfig = reactive(JSON.parse(exeConfigLocal));
    18.  
      else {
    19.  
        localStorage.setItem( 'exeConfig', JSON.stringify(exeConfig));
    20.  
      }
    21.  
       
    22.  
      watch(exeConfig, e => {
    23.  
        localStorage.setItem( 'exeConfig', JSON.stringify(e));
    24.  
      }); 

    vuex番外

    vuex的使用是直接在項目中引入useStore,但是是沒有state類型提示的,所以需要手動去推導state的內容。這里的S代表state的類型,然后傳入vuexexport declare class Store<S> { readonly state: S; }

    想要查看某個值的類型的時候在vscode中ctrl+鼠標左鍵點進去就能看到,或者鼠標懸浮該值

    1.  
      declare module  'vuex' {
    2.  
         type StoreStateType = typeof store.state;
    3.  
        export function useStore<S = StoreStateType>(): Store<S>;
    4.  

    index.vue

    • 這里在防止沒有數據的時候頁面空白閃爍,使用一個圖片和列表區域去控制顯示,拿到數據之后就顯示列表,否則就只顯示圖片。

    • 在這個頁面對editor.vue進行了createNewNote創建便箋筆記、updateNoteItem_className更新類型更改顏色、updateNoteItem_content更新內容、removeEmptyNoteItem刪除、whetherToOpen是否打開(在editor中需要打開列表的操作)通信操作

    • 以及對軟件失去焦點進行監聽getCurrentWindow().on('blur'),如果失去焦點,那么在右鍵彈窗打開的情況下進行去除。

    • deleteActiveItem_{uid}刪除便箋筆記內容,這里在component封裝了一個彈窗組件messageBox,然后在彈窗的時候提示是否刪除不在詢問的功能操作。

      • ????如果勾選不在詢問,那么在store=>exeConfig.state中做相應的更改

      • 這里在設置中會進行詳細的介紹

    開發一個vue3右鍵彈窗插件

    vue3也發布了有段時間了,雖然還沒有完全穩定,但后面的時間出現的插件開發方式說不定也會多起來。插件開發思路

    1. 定義好插件類型,比如需要哪些屬性MenuOptions

    2. 判斷是否需要在觸發之后立即關閉還是繼續顯示

    3. 在插入body時判斷是否存在,否則就刪除重新顯示

    1.  
      import { createApp, h, App, VNode, RendererElement, RendererNode } from 'vue';
    2.  
      import './index.css';
    3.  
       
    4.  
      type ClassName = string | string[];
    5.  
       
    6.  
      interface MenuOptions {
    7.  
         /**
    8.  
         * 文本
    9.  
         */
    10.  
        text:  string;
    11.  
       
    12.  
         /**
    13.  
         * 是否在使用后就關閉
    14.  
         */
    15.  
        once?: boolean;
    16.  
       
    17.  
         /**
    18.  
         * 單獨的樣式名
    19.  
         */
    20.  
        className?: ClassName;
    21.  
       
    22.  
         /**
    23.  
         * 圖標樣式名
    24.  
         */
    25.  
        iconName?: ClassName;
    26.  
       
    27.  
         /**
    28.  
         * 函數
    29.  
         */
    30.  
        handler(): void;
    31.  
      }
    32.  
       
    33.  
      type RenderVNode = VNode<
    34.  
        RendererNode,
    35.  
        RendererElement,
    36.  
        {
    37.  
          [key:  string]: any;
    38.  
        }
    39.  
      >;
    40.  
       
    41.  
      class CreateRightClick {
    42.  
        rightClickEl?: App<Element>;
    43.  
        rightClickElBox?: HTMLDivElement | null;
    44.  
       
    45.  
        constructor() {
    46.  
          this.removeRightClickHandler();
    47.  
        }
    48.  
       
    49.  
         /**
    50.  
         * 渲染dom
    51.  
         * @param menu
    52.  
         */
    53.  
        render(menu: MenuOptions[]): RenderVNode {
    54.  
           return h(
    55.  
             'ul',
    56.  
            {
    57.  
              class: [ 'right-click-menu-list']
    58.  
            },
    59.  
            [
    60.  
              ...menu. map(item => {
    61.  
                 return h(
    62.  
                   'li',
    63.  
                  {
    64.  
                    class: item.className,
    65.  
                     // vue3.x中簡化了render,直接onclick即可,onClick也可以
    66.  
                    onclick: () => {
    67.  
                       // 如果只是一次,那么點擊之后直接關閉
    68.  
                       if (item.once) this.remove();
    69.  
                       return item.handler();
    70.  
                    }
    71.  
                  },
    72.  
                  [
    73.  
                     // icon
    74.  
                    h( 'i', {
    75.  
                      class: item.iconName
    76.  
                    }),
    77.  
                     // text
    78.  
                    h(
    79.  
                       'span',
    80.  
                      {
    81.  
                        class:  'right-click-menu-text'
    82.  
                      },
    83.  
                      item.text
    84.  
                    )
    85.  
                  ]
    86.  
                );
    87.  
              })
    88.  
            ]
    89.  
          );
    90.  
        }
    91.  
       
    92.  
         /**
    93.  
         * 給右鍵的樣式
    94.  
         * @param event 鼠標事件
    95.  
         */
    96.  
        setRightClickElStyle(event: MouseEvent,  len: number): void {
    97.  
           if (!this.rightClickElBox) return;
    98.  
          this.rightClickElBox.style.height =  `${len * 36}px`;
    99.  
           const { clientX, clientY } = event;
    100.  
           const { innerWidth, innerHeight } = window;
    101.  
           const { clientWidth, clientHeight } = this.rightClickElBox;
    102.  
          let cssText =  `height: ${len * 36}px;opacity: 1;transition: all 0.2s;`;
    103.  
           if (clientX + clientWidth < innerWidth) {
    104.  
            cssText +=  `left: ${clientX + 2}px;`;
    105.  
          }  else {
    106.  
            cssText +=  `left: ${clientX - clientWidth}px;`;
    107.  
          }
    108.  
           if (clientY + clientHeight < innerHeight) {
    109.  
            cssText +=  `top: ${clientY + 2}px;`;
    110.  
          }  else {
    111.  
            cssText +=  `top: ${clientY - clientHeight}px;`;
    112.  
          }
    113.  
          cssText +=  `height: ${len * 36}px`;
    114.  
          this.rightClickElBox.style.cssText = cssText;
    115.  
        }
    116.  
       
    117.  
        remove(): void {
    118.  
           if (this.rightClickElBox) {
    119.  
            this.rightClickElBox.remove();
    120.  
            this.rightClickElBox = null;
    121.  
          }
    122.  
        }
    123.  
       
    124.  
        removeRightClickHandler(): void {
    125.  
          document.addEventListener( 'click', e => {
    126.  
             if (this.rightClickElBox) {
    127.  
               const currentEl = e.target as Node;
    128.  
               if (!currentEl || !this.rightClickElBox.contains(currentEl)) {
    129.  
                this.remove();
    130.  
              }
    131.  
            }
    132.  
          });
    133.  
        }
    134.  
       
    135.  
         /**
    136.  
         * 鼠標右鍵懸浮
    137.  
         * @param event
    138.  
         * @param menu
    139.  
         */
    140.  
        useRightClick = (event: MouseEvent, menu: MenuOptions[] = []): void => {
    141.  
          this.remove();
    142.  
           if (!this.rightClickElBox || !this.rightClickEl) {
    143.  
             const createRender = this.render(menu);
    144.  
            this.rightClickEl = createApp({
    145.  
              setup() {
    146.  
                 return () => createRender;
    147.  
              }
    148.  
            });
    149.  
          }
    150.  
           if (!this.rightClickElBox) {
    151.  
            this.rightClickElBox = document.createElement( 'div');
    152.  
            this.rightClickElBox.id =  'rightClick';
    153.  
            document.body.appendChild(this.rightClickElBox);
    154.  
            this.rightClickEl.mount( '#rightClick');
    155.  
          }
    156.  
          this.setRightClickElStyle(event, menu.length);
    157.  
        };
    158.  
      }
    159.  
       
    160.  
      export  default CreateRightClick; 

    右鍵彈窗插件配合electron打開、刪除便箋筆記

    在使用的時候直接引入即可,如在index.vue中使用創建右鍵的方式,這里需要額外的說明一下,打開窗口需要進行一個窗口通信判斷,ipcMain需要從remote中獲取

    • 每個便箋筆記都有一個uid,也就是utils中生成的

    • 每個在打開筆記的時候也就是編輯頁,需要判斷該uid的窗口是否已經打開

    • 窗口之間用ipcRendereripcMain去通信

    • 判斷通信失敗的方法,用一個定時器來延時判斷是否通信成功,因為沒有判斷通信失敗的方法

    • countFlag = true就說明打開窗口,countFlag = false說明沒有打開窗口

    ipcRendereripcMain通信

    ????on是一直處於通信狀態,once是通信一次之后就關閉了

    1.  
      // countFlag是一個狀態來標記收到東西沒
    2.  
      // index問editor打開了沒有
    3.  
      ipcRenderer.send( '你好')
    4.  
       
    5.  
      // 這時候editor收到消息了
    6.  
      remote.ipcMain.on( '你好', e => {
    7.  
         // 收到消息后顯示
    8.  
        remote.getCurrentWindow().show();
    9.  
         // 然后回index消息
    10.  
        e.sender.send( '你好我在的');
    11.  
      });
    12.  
       
    13.  
      // index在等editor消息
    14.  
      ipcRenderer.on( '你好我在的', () => {
    15.  
         // 好的我收到了
    16.  
        countFlag =  true;
    17.  
      });
    18.  
       
    19.  
      // 如果沒收到消息,那標記一直是false,根據定時器來做相應操作 

    右鍵彈窗的使用

    ????這里的打開筆記功能會把選中的筆記uid當作一個query參數跳轉到編輯頁

    1.  
      import CreateRightClick from '@/components/rightClick';
    2.  
      ...
    3.  
      const rightClick = new CreateRightClick();
    4.  
      ...
    5.  
      const contextMenu = (event: MouseEvent, uid: string) => {
    6.  
        rightClick.useRightClick(event, [
    7.  
          {
    8.  
            text:  '打開筆記',
    9.  
            once:  true,
    10.  
            iconName: [ 'iconfont', 'icon-newopen'],
    11.  
            handler: () => {
    12.  
              let countFlag =  false;
    13.  
              ipcRenderer.send( `${uid}_toOpen`);
    14.  
              ipcRenderer.on( `get_${uid}_toOpen`, () => {
    15.  
                countFlag =  true;
    16.  
              });
    17.  
              setTimeout(() => {
    18.  
                 if (!countFlag) openEditorWindow(uid);
    19.  
              },  100);
    20.  
            }
    21.  
          },
    22.  
          {
    23.  
            text:  '刪除筆記',
    24.  
            once:  true,
    25.  
            iconName: [ 'iconfont', 'icon-delete'],
    26.  
            handler: () => {
    27.  
              deleteCurrentUid.value = uid;
    28.  
               if (exeConfig.switchStatus.deleteTip) {
    29.  
                deleteMessageShow.value =  true;
    30.  
              }  else {
    31.  
                 // 根據彈窗組件進行判斷
    32.  
                onConfirm();
    33.  
              }
    34.  
            }
    35.  
          }
    36.  
        ]);
    37.  
      };
    38.  
      ... 

    editor.vue重點

    這個editor.vue是view/文件夾下的,以下對本頁面統稱編輯頁,更好區分editor組件和頁面 開發思路

    • 打開新增編輯頁窗口時就生成uid並向數據庫nedb添加數據,並向列表頁通信ipcRenderer.send('createNewNote', res)

    • 需要使用富文本,能實時處理格式document.execCommand

    • 頁面加載完時進行聚焦createRangegetSelection

    • 對列表頁實時更新,編輯的時候防抖函數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" />  
    1.  
      const routeUid = useRoute().query.uid as string;
    2.  
      if (routeUid) {
    3.  
         // 查詢
    4.  
        uid.value = routeUid;
    5.  
        getCurUidItem(routeUid);
    6.  
      else {
    7.  
         // 生成uid並把uid放到地址欄
    8.  
         const uuidString = uuid();
    9.  
        uid.value = uuidString;
    10.  
        useRouter().push({
    11.  
          query: {
    12.  
            uid: uuidString
    13.  
          }
    14.  
        });
    15.  
         // 插入數據庫並向列表頁通信
    16.  
        ...
    17.  

    富文本聚焦和ref獲取dom節點

    原理是通過getSelection選擇光標和createRange文本范圍兩個方法,選中富文本節點。獲取

    1.  
      import { defineComponent, onMounted, ref, Ref, watch } from 'vue';
    2.  
      ...
    3.  
      // setup中創建一個和<div ref="editor">同名的變量,就可以直接拿到dom節點,一定要return!!!
    4.  
      let editor: Ref<HTMLDivElement | null> = ref(null);
    5.  
       
    6.  
      onMounted(() => {
    7.  
        focus();
    8.  
      });
    9.  
       
    10.  
      const focus = () => {
    11.  
         const range = document.createRange();
    12.  
         range.selectNodeContents(editor.value as HTMLDivElement);
    13.  
         range.collapse(false);
    14.  
         const selecton = window.getSelection() as Selection;
    15.  
        selecton.removeAllRanges();
    16.  
        selecton.addRange( range);
    17.  
      };
    18.  
       
    19.  
      ...
    20.  
      return {
    21.  
        editor,
    22.  
        ...
    23.  

    editor組件的父傳子以及watch監聽

    ????這里需要注意的是因為在父組件傳給子組件,然后子組件進行更新一次會導致富文本無法撤回,相當於重新給富文本組件賦值渲染了一次,因此這里就只用一次props.content

    1.  
      export  default defineComponent({
    2.  
        props: {
    3.  
          content: String,
    4.  
          className: String
    5.  
        },
    6.  
        emits: [ 'on-input'],
    7.  
        setup(props, { emit }) {
    8.  
          let editor: Ref<HTMLDivElement | null> = ref(null);
    9.  
           const bottomIcons = editorIcons;
    10.  
           const editorContent: Ref<string | undefined> = ref('');
    11.  
       
    12.  
           // 監聽從父組件傳來的內容,因為是從數據庫查詢所以會有一定的延遲
    13.  
          watch(props, nv => {
    14.  
             if (!editorContent.value) {
    15.  
               // 只賦值一次
    16.  
              editorContent.value = nv.content;
    17.  
            }
    18.  
          });
    19.  
        }
    20.  
      }); 

    editor組件的防抖子傳父

    exeConfig.syncDelay是設置里面的一個時間,可以動態根據這個時間來調節儲存進數據庫和列表的更新,獲取富文本組件的html然后儲存到數據庫並傳到列表頁更新

    1.  
      const changeEditorContent = debounce((e: InputEvent) => {
    2.  
         const editorHtml = (e.target as Element).innerHTML;
    3.  
        emit( 'on-input', editorHtml);
    4.  
      }, exeConfig.syncDelay); 

    富文本組件的粘貼純文本

    vue自帶的粘貼事件,@paste獲取到剪切板的內容,然后獲取文本格式的內容e.clipboardData?.getData('text/plain')並插入富文本

    1.  
      const paste = (e: ClipboardEvent) => {
    2.  
         const pasteText = e.clipboardData?.getData('text/plain');
    3.  
        console.log(pasteText);
    4.  
        document.execCommand( 'insertText', false, pasteText);
    5.  
      }; 

    (????????????額外的)getCurrentInstance選擇dom方式

    官方和網上的例子是這樣:

    <div ref="editor"></div>  
    1.  
      setup(props, { emit }) {
    2.  
        let editor = ref(null);
    3.  
         return { editor }
    4.  
      }) 

    直接獲取dom節點,但其實不管這個editor是什么,只要從setupreturn,就會直接標記instance變量名,強行把內容替換成dom節點,甚至不用定義可以看看下面例子

    <div ref="test"></div>  
    1.  
      import { defineComponent, getCurrentInstance, onMounted } from 'vue';
    2.  
      ...
    3.  
      setup(props, { emit }) {
    4.  
        onMounted(() => {
    5.  
          console.log(getCurrentInstance().refs);
    6.  
           // 得到的是test dom以及其他定義的節點
    7.  
        });
    8.  
         return {
    9.  
          test:  ''
    10.  
        }
    11.  
      }) 

    但是為了規范還是使用下面這樣

    <div ref="dom"></div>  
    1.  
      const dom = ref(null);
    2.  
      return {
    3.  
        dom
    4.  
      }; 

    setting.vue

    這里的話需要用到exeConfig.state.ts的配置信息,包括封裝的inputswitchtick組件

    在這里說明一下,自動縮小靠邊隱藏同步設置暫時還沒有開發的

    • 自動縮小: 編輯頁失去焦點時自動最小化,獲得焦點重新打開

    • 靠邊隱藏: 把軟件拖動到屏幕邊緣時,自動隱藏到邊上,類似QQ那樣的功能

    • 同步設置: 打算使用nestjs做同步服務,后面可能會出一篇有關的文章,但是功能一定會做的

    directives自定義指令

    根據是否開啟提示的設置寫的一個方便控制的功能,這個功能是首先獲取初始化的節點高度,放置在dom的自定義數據上面data-xx,然后下次顯示的時候再重新獲取賦值css顯示,當然這里也是用了一個過渡效果

    使用方法

    <div v-tip="switch"></div>  
    1.  
      export  default defineComponent({
    2.  
        components: {
    3.  
          Tick,
    4.  
          Input,
    5.  
          Switch
    6.  
        },
    7.  
        directives: {
    8.  
          tip(el, { value }) {
    9.  
             const { height } = el.dataset;
    10.  
             // 儲存最初的高度
    11.  
             if (!height && height !== '0') {
    12.  
              el.dataset.height = el.clientHeight;
    13.  
            }
    14.  
             const clientHeight = height || el.clientHeight;
    15.  
            let cssText =  'transition: all 0.4s;';
    16.  
             if (value) {
    17.  
              cssText +=  `height: ${clientHeight}px;opacity: 1;`;
    18.  
            }  else {
    19.  
              cssText +=  'height: 0;opacity: 0;overflow: hidden;';
    20.  
            }
    21.  
            el.style.cssText = cssText;
    22.  
          }
    23.  
        }
    24.  
      }) 

    原生點擊復制

    原理是先隱藏一個input標簽,然后點擊的之后選擇它的內容,在使用document.execCommand('copy')復制就可以

    1.  
      <a @click= "copyEmail">復制</a>
    2.  
      <input class= "hide-input" ref="mailInput" type="text" value="heiyehk@foxmail.com" /> 
    1.  
      const mailInput: Ref<HTMLInputElement | null> = ref(null);
    2.  
      const copyEmail = () => {
    3.  
         if (copyStatus.value) return;
    4.  
        copyStatus.value =  true;
    5.  
        mailInput.value?. select();
    6.  
        document.execCommand( 'copy');
    7.  
      };
    8.  
       
    9.  
      return {
    10.  
        copyEmail
    11.  
        ...
    12.  

    electron打開文件夾和打開默認瀏覽器鏈接

    打開文件夾使用shell這個方法

    1.  
      import { remote } from 'electron';
    2.  
       
    3.  
      remote.shell.showItemInFolder( 'D:'); 

    打開默認瀏覽器鏈接

    1.  
      import { remote } from 'electron';
    2.  
       
    3.  
      remote.shell.openExternal( 'www.github.com'); 

    錯誤收集

    收集一些使用中的錯誤,並使用message插件進行彈窗提示,軟件寬高和屏幕寬高只是輔助信息。碰到這些錯誤之后,在軟件安裝位置輸出一個inoteError.log的錯誤日志文件,然后在設置中判斷文件是否存在,存在就打開目錄選中。

    • 版本號

    • 時間

    • 錯誤

    • electron版本

    • Windows信息

    • 軟件寬高信息

    • 屏幕寬高

    比如這個框中的才是主要的信息

    vue3 errorHandler

    main.ts我們需要進行一下改造,並使用errorHandler進行全局的錯誤監控

    1.  
      import { createApp } from 'vue';
    2.  
      import App from './App.vue';
    3.  
      import router from './router';
    4.  
      import outputErrorLog from '@/utils/errorLog';
    5.  
       
    6.  
      const app = createApp(App);
    7.  
       
    8.  
      // 錯誤收集方法
    9.  
      app.config.errorHandler = outputErrorLog;
    10.  
       
    11.  
      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類型的封裝

    1.  
      import { ComponentPublicInstance } from 'vue';
    2.  
      import dayjs from 'dayjs';
    3.  
      import fs from 'fs-extra';
    4.  
      import os from 'os';
    5.  
      import { remote } from 'electron';
    6.  
      import path from 'path';
    7.  
      import useMessage from '@/components/message';
    8.  
       
    9.  
      function getShortStack(stack?:  string): string {
    10.  
         const splitStack = stack?.split('\n    ');
    11.  
         if (!splitStack) return '';
    12.  
         const newStack: string[] = [];
    13.  
         for (const line of splitStack) {
    14.  
           // 其他信息
    15.  
           if (line.includes('bundler')) continue;
    16.  
       
    17.  
           // 只保留錯誤文件信息
    18.  
           if (line.includes('?!.')) {
    19.  
            newStack.push(line.replace(/webpack-internal:\/\/\/\.\/node_modules\/.+\?!/,  ''));
    20.  
          }  else {
    21.  
            newStack.push(line);
    22.  
          }
    23.  
        }
    24.  
         // 轉換string
    25.  
         return newStack.join('\n    ');
    26.  
      }
    27.  
       
    28.  
      export  const errorLogPath = path.join(remote.app.getPath('exe'), '../inoteError.log');
    29.  
       
    30.  
      export  default function(error: unknown, vm: ComponentPublicInstance | null, info: string): void {
    31.  
         const { message, stack } = error as Error;
    32.  
         const { electron, chrome, node, v8 } = process.versions;
    33.  
         const { outerWidth, outerHeight, innerWidth, innerHeight } = window;
    34.  
         const { width, height } = window.screen;
    35.  
       
    36.  
         // 報錯信息
    37.  
         const errorInfo = {
    38.  
          errorInfo: info,
    39.  
          errorMessage: message,
    40.  
          errorStack: getShortStack(stack)
    41.  
        };
    42.  
       
    43.  
         // electron
    44.  
         const electronInfo = { electron, chrome, node, v8 };
    45.  
       
    46.  
         // 瀏覽器窗口信息
    47.  
         const browserInfo = { outerWidth, outerHeight, innerWidth, innerHeight };
    48.  
       
    49.  
         const errorLog = {
    50.  
          versions: remote.app.getVersion(),
    51.  
          date: dayjs().format( 'YYYY-MM-DD HH:mm'),
    52.  
          error: errorInfo,
    53.  
          electron: electronInfo,
    54.  
          window: {
    55.  
             type: os.type(),
    56.  
            platform: os.platform()
    57.  
          },
    58.  
          browser: browserInfo,
    59.  
          screen: { width, height }
    60.  
        };
    61.  
       
    62.  
        useMessage( '程序出現異常', 'error');
    63.  
       
    64.  
         if (process.env.NODE_ENV === 'production') {
    65.  
          fs.writeFileSync(errorLogPath, JSON.stringify(errorLog) +  '\n', { flag: 'a' });
    66.  
        }  else {
    67.  
          console.log(error);
    68.  
          console.log(errorInfo.errorStack);
    69.  
        }
    70.  

    使用此方法后封裝的結果是這樣的,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就動態去清空

    1.  
      const rm = require('rimraf');
    2.  
      const path = require('path');
    3.  
      const pluginOptions = require('../../vue.config').pluginOptions;
    4.  
       
    5.  
      let directories = pluginOptions.electronBuilder.builderOptions.directories;
    6.  
      let buildPath =  '';
    7.  
       
    8.  
      if (directories && directories.output) {
    9.  
        buildPath = directories.output;
    10.  
      }
    11.  
       
    12.  
      // 刪除作用只用於刪除打包前的buildPath || dist_electron
    13.  
      // dist_electron是默認打包文件夾
    14.  
      rm(path.join(__dirname,  `../../${buildPath || 'dist_electron'}`), () => {}); 

    以上就是本篇主要開發內容了,歡迎支持我的開源項目electron-vue3-inote。

    相關資料

    github地址: https://github.com/heiyehk/electron-vue3-inote

    文章參考:  https://juejin.cn/post/6909723449246089224

    ❤️ 看完三件事

    如果你覺得這篇內容對你挺有啟發,我想邀請你幫我三個小忙:

    • 點個【在看】,或者分享轉發,讓更多的人也能看到這篇內容

    • 關注公眾號【趣談前端】,定期分享 工程化 / 可視化 / 低代碼 / 優秀開源。

    從零搭建全棧可視化大屏制作平台V6.Dooring

    從零設計可視化大屏搭建引擎

    Dooring可視化搭建平台數據源設計剖析

    可視化搭建的一些思考和實踐

    基於Koa + React + TS從零開發全棧文檔編輯器(進階實戰


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM