Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 一区二区在线视频观看,在线性爱视频,免费国产小视频在线观看

          整合營銷服務(wù)商

          電腦端+手機(jī)端+微信端=數(shù)據(jù)同步管理

          免費(fèi)咨詢熱線:

          前端實(shí)戰(zhàn):從零用electron+vue3+ts開發(fā)桌面端便簽應(yīng)用

          端時間我的一個朋友為了快速熟悉 Vue3 開發(fā), 特意使用 electron+vue3+ts 開發(fā)了一個桌面端應(yīng)用, 并在 github 上開源了, 接下來我就帶大家一起了解一下這個項(xiàng)目, 在文章末尾我會放 github的地址, 大家如果想學(xué)習(xí)vue3 + ts + electron 開發(fā), 可以本地 clone 學(xué)習(xí)參考一下.

          關(guān)注并將「趣談前端」設(shè)為星標(biāo)

          每天定時分享技術(shù)干貨/優(yōu)秀開源/技術(shù)思維

          image.png

          技術(shù)棧

          以上是我們看到的便簽軟件使用界面, 整體技術(shù)選型如下:

          • 腳手架 vue-cli
          • 前端框架和語言規(guī)范 vue + typescript
          • 桌面端開發(fā)框架 electron
          • electron支持插件 vue-cli-plugin-electron-builder
          • 數(shù)據(jù)庫 NeDB | 一款NoSQL嵌入式數(shù)據(jù)庫
          • 代碼格式規(guī)范 eslint

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


          具體實(shí)現(xiàn)過程, 內(nèi)容很長, 建議先點(diǎn)贊收藏, 再一步步學(xué)習(xí), 接下來會就該項(xiàng)目的每一個重點(diǎn)細(xì)節(jié)做詳細(xì)的分析.

          開發(fā)思路

          1.頁面:

          • 列表頁index.vue 頭部、搜索、內(nèi)容部分,只能有一個列表頁存在
          • 設(shè)置頁setting.vue 設(shè)置內(nèi)容和軟件信息,和列表頁一樣只能有一個存在
          • 編輯頁 editor.vue icons功能和背景顏色功能,可以多個編輯頁同時存在

          2.動效:

          • 打開動效,有一個放大、透明度的過渡,放不了動圖這里暫時不演示了。
          • 標(biāo)題過渡效果
          • 切換indexsetting時頭部不變,內(nèi)容過渡

          3.數(shù)據(jù)儲存:

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

          4.錯誤采集:采集在使用中的錯誤并彈窗提示

          5.編輯顯示:document暴露 execCommand 方法,該方法允許運(yùn)行命令來操縱可編輯內(nèi)容區(qū)域的元素。

          在開發(fā)的時候還遇到過好多坑,這些都是在electron環(huán)境中才有,比如

          1. @input觸發(fā)2次,加上v-model觸發(fā)3次。包括創(chuàng)建一個新的electron框架也是這樣,別人電腦上不會出現(xiàn)這個問題,猜測是electron緩存問題
          2. vue3碰到空屬性報(bào)錯時無限報(bào)錯,在普通瀏覽器(edge和chrome)是正常一次
          3. 組件無法正常渲染不報(bào)錯,只在控制臺報(bào)異常
          4. 打包后由于electron的緩存導(dǎo)致打開需要10秒左右,清除c盤軟件緩存后正常

          其他的不記得了。。

          這里暫時不提供vue3和electron介紹,有需要的可以先看看社區(qū)其他的有關(guān)文章或者后期再詳細(xì)專門提供。軟件命名為i-notes

          electron-vue里面的包環(huán)境太低了,所以是手動配置electron+vue3(雖然說是手動。。其實(shí)就兩個步驟)

          目錄結(jié)構(gòu)

          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環(huán)境

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

          npm install -g @vue/cli 
          

          創(chuàng)建vue3項(xiàng)目

          vue create electron-vue-notes
          
          # 后續(xù)
          ? Please pick a preset: (Use arrow keys)
            Default ([Vue 2] babel, eslint)
            Default (Vue 3 Preview) ([Vue 3] babel, eslint)
          > Manually select features 
          # 手動選擇配置
          
          # 后續(xù)所有配置
          ? 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 
          

          創(chuàng)建完之后的目錄是這樣的

          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 
          

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

          項(xiàng)目的一些基礎(chǔ)配置

          eslint

          使用eslint主要是規(guī)范代碼風(fēng)格,不推薦tslint是因?yàn)閠slint已經(jīng)不更新了,tslint也推薦使用eslint 安裝eslint

          npm i eslint -g 
          

          進(jìn)入項(xiàng)目之后初始化eslint

          eslint --init
          
          # 后續(xù)配置
          ? 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,規(guī)則rules可以根據(jù)自己的喜歡配置 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配置,根據(jù)自己的喜好進(jìn)行配置,單行多少個字符、單引號、分號、逗號結(jié)尾等等

          module.exports = {
            printWidth: 120,
            singleQuote: true,
            semi: true,
            trailingComma: 'none'
          }; 
          

          tsconfig.json

          如果這里沒有配置識別@/路徑的話,在項(xiàng)目中使用會報(bào)錯

          "paths": {
            "@/*": [
              "src/*"
            ]
          } 
          

          package.json

          "author": "heiyehk",
          "description": "I便箋個人開發(fā)者h(yuǎn)eiyehk獨(dú)立開發(fā),在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

          因?yàn)樾枰鲆恍┐蜷_和關(guān)閉的動效,因此我們需要配置electronframe無邊框透明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 () => {
            // 這里注釋掉是因?yàn)闀惭btools插件,需要屏蔽掉,有能力的話可以打開注釋
            // 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 
          

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

          常規(guī)配置

          這里配置一些常用的開發(fā)內(nèi)容和一些輪子代碼, 大家可以參考 reset.cssscommon.css 這兩個文件.

          config

          這個對應(yīng)項(xiàng)目中的config文件夾

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

          browser.options

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

          /**
           * 軟件數(shù)據(jù)和配置
           * C:\Users\{用戶名}\AppData\Roaming
           * 共享
           * C:\ProgramData\Intel\ShaderCache\i-notes{xx}
           * 快捷方式
           * C:\Users\{用戶名}\AppData\Roaming\Microsoft\Windows\Recent
           * 電腦自動創(chuàng)建緩存
           * 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的配置項(xiàng)
           * @param type 單獨(dú)給編輯窗口的配置
           */
          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
            };
          };
          
          /**
           * 開發(fā)環(huán)境: http://localhost:8080
           * 正式環(huán)境: file://${__dirname}/index.html
           */
          const winURL = globalEnv === 'development' ? 'http://localhost:8080' : `file://${__dirname}/index.html`;
          
          export { browserWindowOption, winURL }; 
          

          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: '設(shè)置'
                  }
                }
              ]
            }
          ];
          
          const router = createRouter({
            history: createWebHashHistory(process.env.BASE_URL),
            routes
          });
          
          export default router; 
          

          main.vue

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

          // bad
          <transition name="fade">
            <keep-alive>
              <router-view />
            </keep-alive>
          </transition> 
          

          上面的這種寫法在vue3中會在控制臺報(bào)異常,記不住寫法的可以看看控制臺

          <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標(biāo)簽注意需要寫上lang="ts"代表是ts語法。router的寫法也不一樣,雖然在vue3中還能寫vue2的格式,但是不推薦使用。這里是獲取routename屬性,來進(jìn)行一個頁面過渡的效果。

          <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的內(nèi)容,在頁面刷新或者進(jìn)入的時候根據(jù)useRouter().name的切換進(jìn)行放大的過渡效果,后面的內(nèi)容會更簡潔一點(diǎn)。

          header.vue

          onBeforeRouteUpdate

          頭部組件還有一個標(biāo)題過渡的效果,根據(jù)路由導(dǎo)航獲取當(dāng)前路由的mate.title變化進(jìn)行過渡效果。vue3中路由守衛(wèi)需要從vue-route導(dǎo)入使用。

          import { onBeforeRouteUpdate, useRoute } from 'vue-router';
          ...
          onBeforeRouteUpdate((to, from, next) => {
            title.value = to.meta.title;
            currentRouteName.value = to.name;
            next();
          }); 
          

          computed

          這里是計(jì)算不同的路由下標(biāo)題內(nèi)邊距的不同,首頁是有個設(shè)置入口的按鈕,而設(shè)置頁面是只有兩個按鈕,computed會返回一個你需要的新的值

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

          emit子傳父和props父傳子

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

          setup(props, content) {} 
          

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

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

          props示例

          emit示例

          export default defineComponent({
            props: {
              test: String
            },
            emits: ['option-click', 'on-close'],
            // 如果只用emit的話可以使用es6解構(gòu)
            // 如: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圖釘固定屏幕前面

          先獲取當(dāng)前屏幕實(shí)例

          這里需要注意的是,需要從remote獲取當(dāng)前窗口信息

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

          import { remote } from 'electron';
          ...
          // 獲取窗口固定狀態(tài)
          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關(guān)閉窗口

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

          // 過渡關(guān)閉窗口
          export const transitCloseWindow = (): void => {
            document.querySelector('#app')?.classList.remove('app-show');
            document.querySelector('#app')?.classList.add('app-hide');
            remote.getCurrentWindow().close();
          }; 
          

          noteDb數(shù)據(jù)庫

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

          yarn add nedb @types/nedb 
          

          數(shù)據(jù)儲存在nedb中,定義字段,并在根目錄的shims-vue.d.ts加入類型

          /**
           * 儲存數(shù)據(jù)庫的
           */
          interface DBNotes {
            className: string; // 樣式名
            content: string; // 內(nèi)容
            readonly createdAt: Date; // 創(chuàng)建時間,這個時間是nedb自動生成的
            readonly uid: string; // uid,utils中的方法生成
            readonly updatedAt: Date; // update,自動創(chuàng)建的
            readonly _id: string; // 自動創(chuàng)建的
          } 
          

          對nedb的封裝

          這里的QueryDBshims-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> {
            /**
             * 默認(rèn)儲存位置
             * 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
                 * 當(dāng)數(shù)據(jù)存儲被創(chuàng)建時,數(shù)據(jù)將自動從文件中加載到內(nèi)存,不必去調(diào)用loadDatabase
                 * 注意所有命令操作只有在數(shù)據(jù)加載完成后才會被執(zhí)行
                 */
                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類型,查詢條件,可以使用空對象{}。
             * 支持使用比較運(yùn)算符($lt, $lte, $gt, $gte, $in, $nin, $ne)
             * 邏輯運(yùn)算符($or, $and, $not, $where)
             * 正則表達(dá)式進(jìn)行查詢。
             */
            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監(jiān)聽

          創(chuàng)建exeConfig.state.ts

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

          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的使用是直接在項(xiàng)目中引入useStore,但是是沒有state類型提示的,所以需要手動去推導(dǎo)state的內(nèi)容。這里的S代表state的類型,然后傳入vuexexport declare class Store<S> { readonly state: S; }

          想要查看某個值的類型的時候在vscode中ctrl+鼠標(biāo)左鍵點(diǎn)進(jìn)去就能看到,或者鼠標(biāo)懸浮該值

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

          index.vue

          • 這里在防止沒有數(shù)據(jù)的時候頁面空白閃爍,使用一個圖片和列表區(qū)域去控制顯示,拿到數(shù)據(jù)之后就顯示列表,否則就只顯示圖片。
          • 在這個頁面對editor.vue進(jìn)行了createNewNote創(chuàng)建便箋筆記、updateNoteItem_className更新類型更改顏色、updateNoteItem_content更新內(nèi)容、removeEmptyNoteItem刪除、whetherToOpen是否打開(在editor中需要打開列表的操作)通信操作
          • 以及對軟件失去焦點(diǎn)進(jìn)行監(jiān)聽getCurrentWindow().on('blur'),如果失去焦點(diǎn),那么在右鍵彈窗打開的情況下進(jìn)行去除。
          • deleteActiveItem_{uid}刪除便箋筆記內(nèi)容,這里在component封裝了一個彈窗組件messageBox,然后在彈窗的時候提示是否刪除不在詢問的功能操作。
            • 如果勾選不在詢問,那么在store=>exeConfig.state中做相應(yīng)的更改
            • 這里在設(shè)置中會進(jìn)行詳細(xì)的介紹

          開發(fā)一個vue3右鍵彈窗插件

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

          1. 定義好插件類型,比如需要哪些屬性MenuOptions
          2. 判斷是否需要在觸發(fā)之后立即關(guān)閉還是繼續(xù)顯示
          3. 在插入body時判斷是否存在,否則就刪除重新顯示
          import { createApp, h, App, VNode, RendererElement, RendererNode } from 'vue';
          import './index.css';
          
          type ClassName = string | string[];
          
          interface MenuOptions {
            /**
             * 文本
             */
            text: string;
          
            /**
             * 是否在使用后就關(guān)閉
             */
            once?: boolean;
          
            /**
             * 單獨(dú)的樣式名
             */
            className?: ClassName;
          
            /**
             * 圖標(biāo)樣式名
             */
            iconName?: ClassName;
          
            /**
             * 函數(shù)
             */
            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: () => {
                          // 如果只是一次,那么點(diǎn)擊之后直接關(guān)閉
                          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 鼠標(biāo)事件
             */
            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();
                  }
                }
              });
            }
          
            /**
             * 鼠標(biāo)右鍵懸浮
             * @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中使用創(chuàng)建右鍵的方式,這里需要額外的說明一下,打開窗口需要進(jìn)行一個窗口通信判斷,ipcMain需要從remote中獲取

          • 每個便箋筆記都有一個uid,也就是utils中生成的
          • 每個在打開筆記的時候也就是編輯頁,需要判斷該uid的窗口是否已經(jīng)打開
          • 窗口之間用ipcRendereripcMain去通信
          • 判斷通信失敗的方法,用一個定時器來延時判斷是否通信成功,因?yàn)闆]有判斷通信失敗的方法
          • countFlag = true就說明打開窗口,countFlag = false說明沒有打開窗口

          ipcRenderer和ipcMain通信

          on是一直處于通信狀態(tài),once是通信一次之后就關(guān)閉了

          // countFlag是一個狀態(tài)來標(biāo)記收到東西沒
          // index問editor打開了沒有
          ipcRenderer.send('你好')
          
          // 這時候editor收到消息了
          remote.ipcMain.on('你好', e => {
            // 收到消息后顯示
            remote.getCurrentWindow().show();
            // 然后回index消息
            e.sender.send('你好我在的');
          });
          
          // index在等editor消息
          ipcRenderer.on('你好我在的', () => {
            // 好的我收到了
            countFlag = true;
          });
          
          // 如果沒收到消息,那標(biāo)記一直是false,根據(jù)定時器來做相應(yīng)操作 
          

          右鍵彈窗的使用

          這里的打開筆記功能會把選中的筆記uid當(dāng)作一個query參數(shù)跳轉(zhuǎn)到編輯頁

          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 {
                    // 根據(jù)彈窗組件進(jìn)行判斷
                    onConfirm();
                  }
                }
              }
            ]);
          };
          ... 
          

          editor.vue重點(diǎn)

          這個editor.vue是view/文件夾下的,以下對本頁面統(tǒng)稱編輯頁,更好區(qū)分editor組件和頁面 開發(fā)思路

          • 打開新增編輯頁窗口時就生成uid并向數(shù)據(jù)庫nedb添加數(shù)據(jù),并向列表頁通信ipcRenderer.send('createNewNote', res)
          • 需要使用富文本,能實(shí)時處理格式document.execCommand
          • 頁面加載完時進(jìn)行聚焦createRangegetSelection
          • 對列表頁實(shí)時更新,編輯的時候防抖函數(shù)debounce可以控制輸入更新,這個時間在設(shè)置是可控
          • 圖釘固定header.vue已經(jīng)說明
          • 選項(xiàng)功能能選擇顏色,打開列表之后需要判斷是否已經(jīng)打開列表窗口
          • 點(diǎn)擊關(guān)閉的時候需要刪除數(shù)據(jù)庫本條數(shù)據(jù),如果沒有輸入內(nèi)容就刪除數(shù)據(jù)庫uid內(nèi)容并向列表頁通信removeEmptyNoteItem
          • 在列表頁時關(guān)閉本窗口的一個通信deleteActiveItem_{uid}
          • 列表頁打開筆記時,攜帶uid,在編輯頁根據(jù)是否攜帶uid查詢該條數(shù)據(jù)庫內(nèi)容

          富文本編輯做成了一個單獨(dú)的組件,使編輯頁的代碼不會太臃腫

          document.execCommand文檔 developer.mozilla.org/zh-CN/docs/…[6]

          首先在編輯頁對路由進(jìn)行判斷是否存在,如果不存在就創(chuàng)建,否則就查詢并把查詢到的筆記傳給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
              }
            });
            // 插入數(shù)據(jù)庫并向列表頁通信
            ...
          } 
          

          富文本聚焦和ref獲取dom節(jié)點(diǎn)

          原理是通過getSelection選擇光標(biāo)和createRange文本范圍兩個方法,選中富文本節(jié)點(diǎn)。獲取

          import { defineComponent, onMounted, ref, Ref, watch } from 'vue';
          ...
          // setup中創(chuàng)建一個和<div ref="editor">同名的變量,就可以直接拿到dom節(jié)點(diǎn),一定要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監(jiān)聽

          這里需要注意的是因?yàn)樵诟附M件傳給子組件,然后子組件進(jìn)行更新一次會導(dǎo)致富文本無法撤回,相當(dāng)于重新給富文本組件賦值渲染了一次,因此這里就只用一次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('');
          
              // 監(jiān)聽從父組件傳來的內(nèi)容,因?yàn)槭菑臄?shù)據(jù)庫查詢所以會有一定的延遲
              watch(props, nv => {
                if (!editorContent.value) {
                  // 只賦值一次
                  editorContent.value = nv.content;
                }
              });
            }
          }); 
          

          editor組件的防抖子傳父

          exeConfig.syncDelay是設(shè)置里面的一個時間,可以動態(tài)根據(jù)這個時間來調(diào)節(jié)儲存進(jìn)數(shù)據(jù)庫和列表的更新,獲取富文本組件的html然后儲存到數(shù)據(jù)庫并傳到列表頁更新

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

          富文本組件的粘貼純文本

          vue自帶的粘貼事件,@paste獲取到剪切板的內(nèi)容,然后獲取文本格式的內(nèi)容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方式

          官方和網(wǎng)上的例子是這樣:

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

          直接獲取dom節(jié)點(diǎn),但其實(shí)不管這個editor是什么,只要從setupreturn,就會直接標(biāo)記instance變量名,強(qiáng)行把內(nèi)容替換成dom節(jié)點(diǎn),甚至不用定義可以看看下面例子

          <div ref="test"></div> 
          
          import { defineComponent, getCurrentInstance, onMounted } from 'vue';
          ...
          setup(props, { emit }) {
            onMounted(() => {
              console.log(getCurrentInstance().refs);
              // 得到的是test dom以及其他定義的節(jié)點(diǎn)
            });
            return {
              test: ''
            }
          }) 
          

          但是為了規(guī)范還是使用下面這樣

          <div ref="dom"></div> 
          
          const dom = ref(null);
          return {
            dom
          }; 
          

          setting.vue

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

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

          • 自動縮小: 編輯頁失去焦點(diǎn)時自動最小化,獲得焦點(diǎn)重新打開
          • 靠邊隱藏: 把軟件拖動到屏幕邊緣時,自動隱藏到邊上,類似QQ那樣的功能
          • 同步設(shè)置: 打算使用nestjs做同步服務(wù),后面可能會出一篇有關(guān)的文章,但是功能一定會做的

          directives自定義指令

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

          使用方法

          <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;
              }
            }
          }) 
          

          原生點(diǎn)擊復(fù)制

          原理是先隱藏一個input標(biāo)簽,然后點(diǎn)擊的之后選擇它的內(nèi)容,在使用document.execCommand('copy')復(fù)制就可以

          <a @click="copyEmail">復(fù)制</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打開文件夾和打開默認(rèn)瀏覽器鏈接

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

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

          打開默認(rèn)瀏覽器鏈接

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

          錯誤收集

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

          • 版本號
          • 時間
          • 錯誤
          • electron版本
          • Windows信息
          • 軟件寬高信息
          • 屏幕寬高

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

          vue3 errorHandler

          main.ts我們需要進(jìn)行一下改造,并使用errorHandler進(jìn)行全局的錯誤監(jiān)控

          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);
              }
            }
            // 轉(zhuǎn)換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;
          
            // 報(bào)錯信息
            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('程序出現(xiàn)異常', 'error');
          
            if (process.env.NODE_ENV === 'production') {
              fs.writeFileSync(errorLogPath, JSON.stringify(errorLog) + '\n', { flag: 'a' });
            } else {
              console.log(error);
              console.log(errorInfo.errorStack);
            }
          } 
          

          使用此方法后封裝的結(jié)果是這樣的,message插件具體看component

          這個是之前的錯誤日志文件

          獲取electron版本等信息

          const appInfo = process.versions; 
          

          打包

          這個倒是沒什么好講的了,主要還是在vue.config.js文件中進(jìn)行配置一下,然后使用命令yarn electron:build即可,當(dāng)然了,還有一個打包前清空的舊的打包文件夾的腳本

          deleteBuild.js

          打包清空dist_electron舊的打包內(nèi)容,因?yàn)?span style="color: #1E6BB8; --tt-darkmode-color: #1E6BB8;">eslint的原因,這里就用eslint-disable關(guān)掉了幾個

          原理就是先獲取vue.config.js中的打包配置,如果重新配置了路徑directories.output就動態(tài)去清空

          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是默認(rèn)打包文件夾
          rm(path.join(__dirname, `../../${buildPath || 'dist_electron'}`), () => {}); 
          

          以上就是本篇主要開發(fā)內(nèi)容了,歡迎支持我的開源項(xiàng)目electron-vue3-inote

          相關(guān)資料

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

          ?? 看完三件事

          如果你覺得這篇內(nèi)容對你挺有啟發(fā),歡迎點(diǎn)贊+關(guān)注, 估計(jì)作者持續(xù)創(chuàng)作更高質(zhì)量的內(nèi)容.


          —— 執(zhí)劍天涯,從你的點(diǎn)滴積累開始,所及之處,必精益求精。


          在實(shí)際業(yè)務(wù)開發(fā)中,時常會有這種一段Html格式的標(biāo)簽,看下圖的情況 :


          在 Flutter 中,有點(diǎn)發(fā)愁,因?yàn)?Flutter 提供的 Text 與 RichText 還解析不了這種格式的,但是你也不能使用 WebView 插件,如果使用了,你會在每一個Item中嵌入一個瀏覽器內(nèi)核,再強(qiáng)的手機(jī),也會卡,當(dāng)然肯定不能這樣做,因?yàn)檫@樣就是錯誤的做法。

          小編經(jīng)過大量的嘗試與思考,終于寫出來了一個插件可以來解析了,現(xiàn)分享給大家。

          1 基本使用實(shí)現(xiàn)

          1.2 添加依賴

          小編依舊,來個pub方式:

          dependencies:
            flutter_html_rich_text: ^1.0.0
          

          1.3 加載解析 HTML 片段標(biāo)簽

          核心方法如下:

          ///htmlText 就是你的 HTML 片段了
           HtmlRichText(
            htmlText: txt,
           ),

          如下代碼清單 1-3-1 就是上述圖中的效果:

          /// 代碼清單 1-3-1
          class TestHtmlPage extends StatefulWidget {
            @override
            _TestPageState createState() => _TestPageState();
          }
          
          class _TestPageState extends State<TestHtmlPage> {
          
            String txt =
                "<p>長途輪 <h4>高速驅(qū)動</h4><span style='background-color:#ff3333'>"
                "<span style='color:#ffffff;padding:10px'> 3條立減 購胎抽獎</span></span></p>"
                "<p>長途高速驅(qū)動輪<span ><span style='color:#cc00ff;'> 3條立減 購胎抽獎</span></span></p>";
          
            @override
            Widget build(BuildContext context) {
              return Scaffold(
                ///一個標(biāo)題
                appBar: AppBar(title: Text('A頁面'),),
                body: Center(
                  ///一個列表
                  child: ListView.builder(
                    itemBuilder: (BuildContext context, int postiont) {
                      return buildItemWidget(postiont);
                    },
                    itemCount: 100,
                  ),
                ),
              );
            }
          
            ///ListView的條目
            Widget buildItemWidget(int postiont) {
              return Container(
                ///內(nèi)容邊距
                padding: EdgeInsets.all(8),
                child: Column(
                  ///子Widget左對齊
                  crossAxisAlignment: CrossAxisAlignment.start,
          
                  ///內(nèi)容包裹
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Text(
                      "測試標(biāo)題 $postiont",
                      style: TextStyle(fontWeight: FontWeight.w500),
                    ),
          
                    ///html富文本標(biāo)簽
                    Container(
                      margin: EdgeInsets.only(top: 8),
                      child: HtmlRichText(
                        htmlText: txt,
                      ),
                    )
                  ],
                ),
              );
            }
          }
          

          以下是解析思考 燒腦的實(shí)踐


          2 燒腦思考實(shí)踐一

          Flutter 應(yīng)用程序被 Android iOS平臺加載,在原生 Android 中,使用TextView就可輕松實(shí)現(xiàn)解析(如下代碼清單2-1),當(dāng)然在iOS中使用UILabel也可輕松實(shí)現(xiàn)(如下代碼清單2-2)。

          // Android 原生 TextView加載Html的核心方法
          //代碼清單2-1
          // MxgsaTagHandler 定義的一個 TagHandler 用來處理點(diǎn)擊事件
          lTextView.setText(Html.fromHtml(myContent, null, new MxgsaTagHandler(context)));
          lTextView.setClickable(true);
          lTextView.setMovementMethod(LinkMovementMethod.getInstance());
          

          iOS UILable

          // iOS 原生 UILabel加載Html的核心方法
          //代碼清單2-2
          //返回的HTML文本 如 <font color = 'red'></font>
          NSString *str = @"htmlText";
          NSString *HTMLString = [NSString stringWithFormat:@"<html><body>%@</body></html>", str ];
          
          
          NSDictionary *options = @{NSDocumentTypeDocumentAttribute : NSHTMLTextDocumentType,
                                    NSCharacterEncodingDocumentAttribute : @(NSUTF8StringEncoding)
                                    };
          NSData *data = [HTMLString dataUsingEncoding:NSUTF8StringEncoding];
          
          NSMutableAttributedString * attributedString = [[NSMutableAttributedString alloc] initWithData:data options:options documentAttributes:nil error:nil];
          
          NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];   // 調(diào)整行間距
          paragraphStyle.lineSpacing = 8.0;
          paragraphStyle.alignment = NSTextAlignmentJustified;
          [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, attributedString.length)];
          
          [attributedString addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:15] range:NSMakeRange(0, attributedString.length)];
          
          
          _uiLabel.backgroundColor = [UIColor cyanColor];
          _uiLabel.numberOfLines = 0;
          _uiLabel.attributedText = attributedString;
          [_uiLabel sizeToFit];
          
          

          然后對于 Flutter 來講是可以順利的加載原生 View的 ,如下代碼清單 2-3所示就是在Flutter中通過 AndroidView 與 UiKitView來實(shí)現(xiàn)。

            //Flutter中加載原生View核心方法
            //代碼清單2-3
            buildAndroidView() {
              return AndroidView(
                //設(shè)置標(biāo)識
                viewType: "com.studyon./text_html",
                //參數(shù)的編碼方式
                creationParamsCodec: const StandardMessageCodec(),
              );
            }
          
            /// 通過 UiKitView 來加載 iOS原生View
            buildUIKitView() {
              return UiKitView(
                //標(biāo)識
                viewType: "com.studyon./text_html",
                //參數(shù)的編碼方式
                creationParamsCodec: const StandardMessageCodec(),
              );
            }
          



          于是小編開發(fā)了第一波操作,開發(fā)了這樣的一個插件來調(diào)用原生 View 實(shí)現(xiàn)渲染富文本標(biāo)簽,這個插件使用方式很簡單,如下所示:

          HTMLTextWidet(
            htmlText: "測試一下",
           )

          這一步操作真是所謂的騷操作,其實(shí)小編在開發(fā)前就覺得不太合適,不過以小編的個性,非得嘗試驗(yàn)證一下,現(xiàn)結(jié)果出來了,就是在加載時,由于應(yīng)用在列表中,使用 HTMLTextWidet 會有短暫的黑屏效果,而且內(nèi)存出吃不消,如下圖所示:


          為什么會黑屏,閑魚技術(shù)團(tuán)隊(duì)有過論述在《Flutter中嵌入Native組件的正確姿勢》 以及 文章 《深入了解Flutter界面開發(fā)中有詳細(xì)論述》 。

          所以結(jié)果是 :不可行。


          3 燒腦思考實(shí)踐二

          用 Java 的思想來解析 String 的方式來處理 HTML 字符串,處理成小片段,然后使用Text結(jié)合 流式布局 Wrap 來組合,核心代碼如下清單 3-1 所示為解析:

          /*
             解析標(biāo)簽
             */
          List<TagColorModel> findBackGroundColor(String htmlStr) {
            List<TagColorModel> tagColorModelList = [];
            List<String> colorSpiltList = [];
            String driverAdvertisement = htmlStr;
            if (driverAdvertisement != null) {
          
              colorSpiltList = driverAdvertisement.split("background-color");
          
              for (var i = 0; i < colorSpiltList.length; i++) {
                TagColorModel itemColorModel = TagColorModel();
                String colorsStr = colorSpiltList[i];
                List<String> itemSpiltList = colorsStr.split(":#");
                for (var j = 0; j < itemSpiltList.length; ++j) {
                  String item = itemSpiltList[j];
                  String itemColor = "";
                  String itemText = "";
                  try {
                    if (item.length >= 6) {
                      itemColor = item.toString().substring(0, 6);
                      if (itemColor.trim().toUpperCase() == "FFFFFF") {
                        itemColorModel.backGroundColor = ColorUtils.getRandomColor();
                      } else {
                        itemColorModel.backGroundColor = new Color(
                          int.parse(itemColor.trim(), radix: 16) + 0xFF000000);
                      }
                      int startIndex = item.indexOf("\">");
                      int endIndex = item.indexOf("</");
                      if (startIndex != -1 && endIndex >= startIndex) {
                        LogUtil.e("startIndex  $startIndex  endIndex $endIndex ");
                        itemText = item.substring(startIndex + 2, endIndex);
                        LogUtil.e("itemColor  $itemColor  itemText $itemText ");
                        itemColorModel.text = itemText;
                        tagColorModelList.add(itemColorModel);
                      }
                    }
                  } catch (e) {
                    ///解析異常的 不必處理
                  }
                }
              }
            }
            LogUtil.e("${tagColorModelList.length} \n\n ");
            return tagColorModelList;
          }
          

          然后 TagColorModel 的定義如下代碼清單 3-2所示:

          ///代碼清單 3-2 
          class TagColorModel {
            ///背景
            Color backGroundColor;
           ///文本顏色
            Color textColor;
           ///文本
            String text;
          
            TagColorModel(
                {this.text = "",
                this.backGroundColor = Colors.transparent,
                this.textColor = Colors.white});
          }

          然后就是使用 Wrap 來使用解析的內(nèi)容,如下代碼清單3-3所示:

          ///代碼清單 3-3
          ///獲取背景顏色
            List<TagColorModel> colorList = findBackGroundColor(htmlStr);
          
            List<Widget> tagList = [];
          
            for (var i = 0; i < colorList.length; ++i) {
              TagColorModel model = colorList[i];
          
              tagList.add(Container(
                margin: EdgeInsets.only(right: 2, left: 4, top: 4),
                padding: EdgeInsets.only(left: 6, right: 6),
                decoration: BoxDecoration(
                  color: model.backGroundColor,
                  borderRadius: BorderRadius.all(Radius.circular(2)),
                ),
                child: Text(
                  "${model.text}",
                  style: TextStyle(fontSize: 12, color: model.textColor),
                ),
              ));
            }
          
            ///然后再使用 Wrap 包裹
            Wrap(
          	alignment: WrapAlignment.spaceBetween,
           	 children: tagList,
            ),
          

          實(shí)踐結(jié)果:可行,但是有兼容性差,效率低。

          當(dāng)然閑魚團(tuán)隊(duì)在文章《如何低成本實(shí)現(xiàn)Flutter富文本,看這一篇就夠了!》 中也有詳細(xì)論述過,與上述的思路差不多。

          4 燒腦思考實(shí)踐三

          當(dāng)在Flutter中 Dart 從網(wǎng)站中提取數(shù)據(jù)時,html依賴庫是一個不錯的選擇,html 是一個開源的 Dart 包,主要用于從 HTML 中提取數(shù)據(jù),從中獲取節(jié)點(diǎn)的屬性、文本和 HTML以及各種節(jié)點(diǎn)的內(nèi)容。

          
          dependencies:
            html: ^0.14.0+3
          

          于是乎小編也開始嘗試,首先是使用 Html 庫解析 HTML文本塊,將解析的 Document 通過遞歸方式遍歷出來所有的 node 節(jié)點(diǎn),如下代碼清單4-1所示:

          代碼清單4-1
          import 'package:html/parser.dart' as parser;
          import 'package:html/dom.dart' as dom;
          
          List<Widget> parse(String originHtmlString) {
            // 空格替換 去除所有 br 標(biāo)簽用 \n 代替,
            originHtmlString = originHtmlString.replaceAll('<br/>', '\n');
            originHtmlString = originHtmlString.replaceAll('<br>', '\n');
            originHtmlString = originHtmlString.replaceAll('<br />', '\n');
          
            ///html 依賴庫解析
            dom.Document document = parser.parse(originHtmlString);
            ///獲取 DOM 中的 node 節(jié)點(diǎn)
            dom.Node cloneNode = document.body.clone(true);
          
           // 注意: 先序遍歷找到所有關(guān)鍵節(jié)點(diǎn)(由于是引用傳值,所以需要重新獲取一遍 hashCode)
            List<dom.Node> keyNodeList = new List<dom.Node>();
            int nodeIndex = 0;
            ///遞歸遍歷
            parseNodesTree(cloneNode, callBack: (dom.Node childNode) {
              if (childNode is dom.Element &&
                  truncateTagList.indexOf(childNode.localName) != -1) {
                print('TEST: truncate tag nodeIndex = ${nodeIndex++}');
                keyNodeList.add(childNode);
                // 注意: 對于占據(jù)整行的圖片也作為關(guān)鍵節(jié)點(diǎn)處理
              } else if (childNode is dom.Element &&
                  childNode.localName == 'img' &&
                  checkImageNeedNewLine(childNode)) {
                print('TEST: one line image nodeIndex = ${nodeIndex++}');
                keyNodeList.add(childNode);
              }
            });
          
          }
          
          ///遞歸遍歷
          void parseNodesTree(dom.Node node,
              {NodeTreeCallBack callBack = printNodeName}) {
            ///遍歷 Node 節(jié)點(diǎn)
            for (var i = 0; i < node.nodes.length; ++i) {
              dom.Node item = node.nodes[i];
              callBack(item);
              parseNodesTree(item, callBack: callBack);
            }
          }
          

          然后就是將 得出的 node 節(jié)點(diǎn) 與 Flutter 組件映射,文本使用 TextSpan ,圖片使用 Image ,然后將 樣式使用 TextStyle 映射,然后最后將解析的結(jié)果組件使用 Wrap 來包裹,就達(dá)到了現(xiàn)在的插件 flutter_html_rich_text 。


          綜合實(shí)現(xiàn)思路就是 使用 HTML 庫完善了【燒腦思考實(shí)踐二】中的解析。

          解析篇幅較長,大家有興趣可以看下 github 源碼。



          完畢

          單標(biāo)簽

          網(wǎng)頁(程序)如果要和用戶產(chǎn)生互動,則必須借助一定的中介,這個中介一般是:文本輸入框、按鈕、多選框、單選框。而表單則是這些中介和放置這些中介的空間(<form action=”” methon=””></form>)。

          在網(wǎng)頁中,這些文本輸入框、按鈕等等必須放置在由<form></form>這個標(biāo)簽所定義的空間中,否則沒有實(shí)際意義。所以,由<form></form>標(biāo)簽所定義的空間就是表單存在的空間。


          【各種輸入類型】

          1. 文字輸入框:每個表單之所以會有不同的類型,原因就在于type="表單類型"設(shè)定的不同而已,我們就先來看看第一個類型:文字輸入列。文字輸入列的形態(tài)就是type="text,其使用方法如下:

          呈現(xiàn)結(jié)果

          姓名:

          原始碼

          <form action=http://www.baidu.com/nameproject.aspmethon=”post”>

          姓名:<input type="text" name="name" size="20">

          </form>

          它有下列可設(shè)定之屬性:

          • name="名稱",是設(shè)定此一欄位的名稱,程式中常會用到。
          • size="數(shù)值",是設(shè)定此一欄位顯現(xiàn)的寬度。
          • value="預(yù)設(shè)內(nèi)容",是設(shè)定此一欄位的預(yù)設(shè)內(nèi)容。
          • align="對齊方式",是設(shè)定此一欄位的對齊方式,其值有:top(向上對齊)、middle(向中對齊)、bottom(向下對齊)、right(向右對齊)、left(向左對齊)、texttop(向文字頂部對齊)、baseline(向文字底部對齊)、absmiddle(絕對置中)、absbottom(絕對置下)等。
          • maxlength="數(shù)值",是設(shè)定此一欄位可設(shè)定輸入的最大長度。


          1. 單選框:利用type="radio"就會產(chǎn)生單選核取表單,單選核取表單通常是好幾個選項(xiàng)一起擺出來供使用者點(diǎn)選,一次只能從中選一個,故為單選核取表單。

          呈現(xiàn)結(jié)果

          性別:男 女

          原始碼

          <form>

          性別:

          男 <input type="radio" name="sex" value="boy">

          女 <input type="radio" name="sex" value="girl">

          </form>

          它有下列可設(shè)定之屬性:

          • name="名稱",是設(shè)定此一欄位的名稱,程式中常會用到。
          • value="內(nèi)容",是設(shè)定此一欄位的內(nèi)容、值或是意義。
          • align="對齊方式",是設(shè)定此一欄位的對齊方式,其值有:top(向上對齊)、middle(向中對齊)、bottom(向下對齊)、right(向右對齊)、left(向左對齊)、texttop(向文字頂部對齊)、baseline(向文字底部對齊)、absmiddle(絕對置中)、absbottom(絕對置下)等。
          • checked,是設(shè)定此一欄位為預(yù)設(shè)選取值。


          1. 復(fù)選框:利用type=" checkbox "就會產(chǎn)生復(fù)選核取表單,復(fù)選核取表單通常是好幾個選項(xiàng)一起擺出來供使用者點(diǎn)選,一次可以同時選好幾個,故為復(fù)選核取表單。

          呈現(xiàn)結(jié)果

          喜好: 電影 看書

          原始碼

          <form>

          喜好:

          <input type="checkbox" name="sex" value="movie">電影

          <input type="checkbox" name="sex" value="book">看書

          </form>

          它有下列可設(shè)定之屬性:

          • name="名稱",是設(shè)定此一欄位的名稱,程式中常會用到。
          • value="內(nèi)容",是設(shè)定此一欄位的內(nèi)容、值或是意義。
          • align="對齊方式",是設(shè)定此一欄位的對齊方式,其值有:top(向上對齊)、middle(向中對齊)、bottom(向下對齊)、right(向右對齊)、left(向左對齊)、texttop(向文字頂部對齊)、baseline(向文字底部對齊)、absmiddle(絕對置中)、absbottom(絕對置下)等。
          • checked,是設(shè)定此一欄位為預(yù)設(shè)選取值。


          1. 密碼表單:利用type=" password "就會產(chǎn)生一個密碼表單,密碼表單和文字輸入表單長得幾乎一樣,差別就在于密碼表單在輸入時全部會以星號來取代輸入的文字,以防他人偷窺。

          呈現(xiàn)結(jié)果

          請輸入密碼:

          原始碼

          <form>

          請輸入密碼:<input type="password" name="input">

          </form>

          它有下列可設(shè)定之屬性:

          • name="名稱",是設(shè)定此一欄位的名稱,程式中常會用到。
          • size="數(shù)值",是設(shè)定此一欄位顯現(xiàn)的寬度。
          • value="預(yù)設(shè)內(nèi)容",是設(shè)定此一欄位的預(yù)設(shè)內(nèi)容,不過呈現(xiàn)出來仍是星號。
          • align="對齊方式",是設(shè)定此一欄位的對齊方式,其值有:top(向上對齊)、middle(向中對齊)、bottom(向下對齊)、right(向右對齊)、left(向左對齊)、texttop(向文字頂部對齊)、baseline(向文字底部對齊)、absmiddle(絕對置中)、absbottom(絕對置下)等。
          • maxlength="數(shù)值",是設(shè)定此一欄位可設(shè)定輸入的最大長度。


          1. 送出按鈕:通常我們表單填完之后,都會有一個送出按鈕以及清除重寫的按鈕,分別是利用type=" submit "及type=" reset "來產(chǎn)生,相當(dāng)?shù)暮唵我子谩?/li>

          呈現(xiàn)結(jié)果

          原始碼

          <form>

          <input type="submit" value="送出資料">

          <input type="reset" value="重新填寫">

          </form>

          它有下列可設(shè)定之屬性:

          • name="名稱",是設(shè)定此一按鈕的名稱。
          • value="文字",是設(shè)定此一按鈕上要呈現(xiàn)的文字,若是沒有設(shè)定,瀏覽器也會自動替您加上“送出查詢”、“重設(shè)”等字樣。
          • align="對齊方式",是設(shè)定此一欄位的對齊方式,其值有:top(向上對齊)、middle(向中對齊)、bottom(向下對齊)、right(向右對齊)、left(向左對齊)、texttop(向文字頂部對齊)、baseline(向文字底部對齊)、absmiddle(絕對置中)、absbottom(絕對置下)等。


          1. 按鈕元件:表單中或是java script常會用到按鈕來作一些效果,因此,我們可以利用type=" button "來產(chǎn)生一個按鈕,相當(dāng)簡單。

          呈現(xiàn)結(jié)果

          請按下按鈕:

          原始碼

          <form>

          請按下按鈕:<input type="button" name="ok" value="我同意">

          </form>

          它有下列可設(shè)定之屬性:

          • name="名稱",是設(shè)定此一按鈕的名稱。
          • value="文字",是設(shè)定此一按鈕上要呈現(xiàn)的文字。
          • align="對齊方式",是設(shè)定此一欄位的對齊方式,其值有:top(向上對齊)、middle(向中對齊)、bottom(向下對齊)、right(向右對齊)、left(向左對齊)、texttop(向文字頂部對齊)、baseline(向文字底部對齊)、absmiddle(絕對置中)、absbottom(絕對置下)等。


          1. 隱藏欄位:表單中有時有些東西因?yàn)槟承┮蛩兀幌胱屖褂谜呖吹剑虺淌叫枰獏s又不得不存在,此時,我們就可以利用type=" hidden "來產(chǎn)生一個隱藏的欄位。

          呈現(xiàn)結(jié)果

          隱藏欄位:

          原始碼

          <form>

          隱藏欄位:<input type="hidden" name="nosee" value="看不到">

          </form>

          它有下列可設(shè)定之屬性:

          • name="名稱",是設(shè)定此一欄位的名稱。
          • value="文字",是設(shè)定此一欄位的值、文字或意義。

          【大量文字輸入元件】

          1. 有時候我們會希望讓使用者輸入比較大量的文字,此時,文字輸入列就顯得不敷使用,因此我們就可以利用<textarea></textarea>來產(chǎn)生一個可以輸入大量文字的元件,夾在兩個標(biāo)簽中的文字會出現(xiàn)在框框中,可作為預(yù)設(shè)文字。

          呈現(xiàn)結(jié)果

          請輸入您的意見:

          原始碼

          <form>

          請輸入您的意見:<br>

          <textarea name="talk" cols="20" rows="3"></textarea>

          </form>

          它有下列可設(shè)定之屬性:

          • name="名稱",是設(shè)定此一欄位的名稱。
          • wrap="設(shè)定值",是設(shè)定此一欄位的換行模式。設(shè)定值有三種:off(輸入文字不會自動換行)、virtual(輸入文字在螢?zāi)簧蠒詣訐Q行,不過若是使用者沒有自行按下enter換行,送出資料時,也視為沒有換行)、physical(輸入文字會自動換行,送出資料時,會將螢?zāi)簧系淖詣訐Q行,視為換行效果送出)。
          • cols="數(shù)值",是設(shè)定此一欄位的行數(shù)(橫向字?jǐn)?shù))。
          • rows="數(shù)值",是設(shè)定此一欄位的列數(shù)(垂直字?jǐn)?shù))。


          【下拉式選單】

          1. 下拉式選單令整個網(wǎng)頁看起來有很專業(yè)的感覺,我們只要利用<select name="名稱">便可以產(chǎn)生一個下拉式選單,另外,還需要配合<option>標(biāo)簽來產(chǎn)生選項(xiàng),這樣才算完整喔!

          呈現(xiàn)結(jié)果

          您喜歡看書嗎?:

          非常喜歡

          還算喜歡

          不太喜歡

          非常討厭

          原始碼

          <form>

          您喜歡看書嗎?:

          <select name="like">

          <option value="非常喜歡">非常喜歡

          <option value="還算喜歡">還算喜歡

          <option value="不太喜歡">不太喜歡

          <option value="非常討厭">非常討厭

          </select>

          </form>

          它有下列可設(shè)定之屬性:

          1. size="數(shù)值",是設(shè)定此一欄位的大小,預(yù)設(shè)值為1,若是您的選項(xiàng)有四個,然后您將size設(shè)成4,那么,下拉式選單便會變成選項(xiàng)方塊,將四個選項(xiàng)一起呈現(xiàn)在方塊中。

          multiple,是設(shè)定此一欄位為復(fù)選,可以一次選好幾個選項(xiàng)。

          ....................................................................

          我的微信公眾號:UI嚴(yán)選 —越努力,越幸運(yùn)


          主站蜘蛛池模板: 国产在线精品一区二区| 久久精品国产一区二区三| 中文字幕日本精品一区二区三区| 久久国产精品免费一区| 五月婷婷一区二区| 国产a∨精品一区二区三区不卡| 怡红院美国分院一区二区| 一区二区视频在线| 果冻传媒董小宛一区二区| 亚洲一区视频在线播放| 色天使亚洲综合一区二区| 91video国产一区| 消息称老熟妇乱视频一区二区| 亚洲国产综合精品一区在线播放| 无码一区二区三区爆白浆| 亚洲一区无码中文字幕乱码| 国产乱码精品一区二区三区中文| 国产99精品一区二区三区免费 | 在线视频一区二区| 免费视频一区二区| 国99精品无码一区二区三区| 国产观看精品一区二区三区| 丰满岳妇乱一区二区三区| 八戒久久精品一区二区三区| 韩国精品一区视频在线播放| 天堂一区二区三区精品| 国精产品一区一区三区有限在线| 精品无码人妻一区二区三区品 | 一区二区三区免费精品视频| 波多野结衣中文一区二区免费| 国产传媒一区二区三区呀| 国产一区二区三区在线观看免费| 福利一区二区视频| 国产免费伦精品一区二区三区| 香蕉免费看一区二区三区| 国产成人无码AV一区二区| 久久久久无码国产精品一区| 久久精品亚洲一区二区三区浴池| 色窝窝无码一区二区三区成人网站| 亚洲码一区二区三区| 手机福利视频一区二区|