# demo-electron-js **Repository Path**: pish7/demo-electron-js ## Basic Information - **Project Name**: demo-electron-js - **Description**: Electron 基础项目,使用 javascript - **Primary Language**: JavaScript - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2019-12-03 - **Last Updated**: 2021-05-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # demo-electron-js ## 介绍 Electron 基础项目,使用 javascript。 本项目主要是为了记录开发流程,不会过多地关注细节 ## 功能描述 通过 gitee api 进行关键词搜索,界面上有一个输入框和一个按钮,在输入框中输入关键词,点击搜索按钮,即可显示出搜索结果列表。点击列表项可以打开网址。 ## 开发步骤 ### 初始化项目 1. 先在远程仓库创建项目,并使用 git clone 命令将项目克隆到本地 1. 进入本地目录,执行 npm init 初始化项目 ### 实现 Gitee 搜索 #### 搭建基础目录结构 在项目根路径下创建 app 和 test 两个文件夹,app 用于存放项目源文件,test 用于存放测试文件。 #### 安装测试框架 本项目使用 mocha、chai 和 spectron 进行测试,下面一并进行安装 ```shell npm i -D mocha chai spectron ``` #### 安装 Axios 本项目的网络请求使用 axios ```shell npm i -S axios ``` #### 编写 gitee.js 和相应的测试文件 gitee.js 实现一个 search 方法用于向 gitee 搜索关键字并返回结果 - test/gitee.test.js ```js const { describe, it } = require('mocha') const { assert } = require('chai') const gitee = require('../app/gitee') describe('测试 Gitee', function() { this.timeout(10000) describe('搜索', function() { it('正常关键词搜索', async () => { const result = await gitee.search('electron') assert.isArray(result) for (let i = 0; i < 10 || i == result.length; i++) { assert.isString(result[i].name, 'name必须是一个字符串') assert.isString(result[i].url, 'url必须是一个字符串') assert.match( result[i].url, /^https:\/\/gitee.com\/[^\s]*/, 'url格式错误' ) assert.isNotNull( /electron/i.exec(result[i].name + result[i].description), '关键词不匹配' ) } }) it('无效关键词搜索', async () => { const result = await gitee.search( 'dsfkjgads!@#%$#%&^$#@354645!#$%^&$#@' ) assert.isNull(result) }) }) }) ``` - app/gitee.js ```js const axios = require('axios') /** * Gitee搜索的返回值 * @typedef {Object} Gitee~Search * @property {string} name 项目名称 * @property {string} description 项目描述 * @property {string} url 项目地址 */ /** * 在 Gitee 上搜索指定的关键词,并返回结果 * @param {string} keyword 搜索的关键词 * @return {Array.|null} 搜索到的结果 */ async function search(keyword) { try { let result = await axios.get( 'https://gitee.com/api/v5/search/repositories', { params: { q: keyword, }, } ) if (result.status == 200 && result.data && result.data.length) { result = result.data.map(val => { return { name: val.full_name, description: val.description, url: val.html_url, } }) return result } } catch (e) { console.error(e) } return null } module.exports = { search, } ``` #### 添加测试命令 向 package.json 中添加一个测试命令 ```json "scripts": { "test": "mocha" } ``` #### 执行测试 ```shell npm test ``` ### 开发第一个窗口 第一个窗口只是一个简单的测试窗口,未实现具体功能 #### 安装 electron 和 devtron devtron 是一个开发辅助工具 ```shell npm i -D electron devtron ``` #### 编写主进程程序和窗口 HTML 这里先实现一个简单的主进程程序,让 electron 程序运行起来 - app/main.js ```js 'use strict' const { app, BrowserWindow, Menu } = require('electron') /** * 窗口句柄 */ let win = null /** * 当前环境是否为开发环境 * @const * @type {boolean} */ const isDevelopment = process.env.NODE_ENV == 'development' /** * 创建窗口 */ function createWindow() { // 创建一个窗口 win = new BrowserWindow({ minWidth: 400, minHeight: 300, backgroundColor: '#2e2c29', show: false, webPreferences: { nodeIntegration: true, }, }) // 装载界面文件 win.loadFile(`${__dirname}/index.html`) // 窗口关闭 win.on('closed', () => { win = null }) // 渲染进程第一次完成绘制 win.on('ready-to-show', () => { win.show() }) } // 全部窗口关闭时程序退出 app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } }) app.on('ready', () => { Menu.setApplicationMenu(null) // 移除菜单 createWindow() // 创建窗口 // 如果运行在开发环境中,则打开开发工具 if (isDevelopment) { require('devtron').install() // 安装 devtron win.webContents.openDevTools() // 打开开发者工具 } }) ``` - app/index.html ```html demo-electron-js
hello world
``` #### 修改 package.json 以运行程序 ```json "main": "app/main.js", "scripts": { "start": "set NODE_ENV=development&& electron .", } ``` - main 字段指向主进程启动文件 - start 命令添加环境变量设置,让程序运行在开发模式下 #### 运行第一个窗口 ```shell npm start ``` ### 测试第一个窗口 #### 编写窗口测试文件 - test/ui.test.js ```js const { describe, it, before, after } = require('mocha') const { assert } = require('chai') const { Application } = require('spectron') const electronPath = require('electron') const path = require('path') let entryPointPath = path.join(__dirname, '..') // 入口脚本路径 let app describe('首页', function() { this.timeout(10000) before(() => { app = new Application({ path: electronPath, args: [entryPointPath], webdriverOptions: { deprecationWarnings: false, // 关闭 moveTo 弃用警告 }, }) return app.start() }) after(() => { if (app && app.isRunning()) { return app.stop() } }) describe('显示的界面元素', function() { it('开发者工具已关闭', async () => { await app.client.waitUntilWindowLoaded() const devToolsAreOpen = await app.browserWindow.isDevToolsOpened() assert.equal(devToolsAreOpen, false) }) it('启动一个窗口', async () => { await app.client.waitUntilWindowLoaded() const count = await app.client.getWindowCount() assert.equal(count, 1) }) it('窗口标题', async () => { await app.client.waitUntilWindowLoaded() const title = await app.client.getTitle() assert.equal(title, 'demo-electron-js') }) it('Hello', async () => { await app.client.waitUntilWindowLoaded() const hello = await app.client.getText('//body/div') assert.equal(hello, 'hello world') }) }) }) ``` #### 测试第一个窗口 ```shell npm test ``` ### 开发窗口页面 接下来开发完整的窗口页面功能,页面上可以在输入框中输入搜索内容,并点击搜索按钮,在获取到结果后会将结果显示在页面中,并且每个项目都可点击,并由系统的默认浏览器打开链接 #### 编写界面文件 - app/index.html ```html demo-electron-js

Gitee Search

``` - app/index.css ```css * { margin: 0; padding: 0; } body { padding: 0 1em; } /* 头部 */ header { position: fixed; top: 1em; left: 1em; right: 1em; background-color: #fff; padding-bottom: 0.4em; } /* 标题 */ h1 { margin-bottom: 0.5em; } /* 搜索区 */ div.search { padding-right: 4em; position: relative; } div.search input[type='search'] { display: block; padding-left: 0.5em; width: 100%; height: 2em; } div.search button.search { position: absolute; right: 0; top: 0; width: 4em; height: 2em; } /* 主体内容 */ main { box-sizing: border-box; padding-top: 7em; height: 100vh; max-height: 100vh; overflow: hidden; } .content-wrap { max-height: 100%; overflow-y: scroll; } /* 无内容提示 */ .empty { position: fixed; left: 0; right: 0; top: 8em; text-align: center; color: #999; } /* 列表项 */ #content * { color: rgb(19, 102, 170); } #content > div { padding: 0.5em 0; border-bottom: 1px dotted #aaa; } #content a { text-decoration: none; } #content a:hover { color: rgb(24, 117, 194); text-decoration: underline; } #content .name { font-weight: bold; } #content .description { font-size: 80%; } ``` - app/index.js ```js const { shell } = require('electron') const gitee = require('./gitee') /** 输入框 */ const inputBar = document.querySelector('.search [type="search"]') /** 搜索按钮 */ const searchBtn = document.querySelector('button.search') /** “无内容”提示文字 */ const empty = document.querySelector('.empty') /** * 清空主体显示区域 */ function clearContent() { // 清除列表项 const mainArea = document.getElementById('content') // 主体区 let firstChild = mainArea.firstChild while (firstChild) { mainArea.removeChild(firstChild) firstChild = mainArea.firstChild } // 关闭“无内容”提示 empty.style.setProperty('display', 'none') } /** * 根据提供的数据更新界面中主体区域的显示 * @param {{Array.|null}} list 数据列表 */ function updateContentFromList(list) { const mainArea = document.getElementById('content') // 主体区 const template = document.getElementById('template-item') // 模板 if (list && list.length) { // 显示列表项 for (const item of list) { const clone = document.importNode(template.content, true) // 复制一个节点以及其子孙节点 const a = clone.querySelector('a') const name = clone.querySelector('.name') const description = clone.querySelector('.description') name.innerHTML = item.name description.innerHTML = item.description a.setAttribute('href', item.url) a.addEventListener('click', handleVisitLink) mainArea.appendChild(clone) } } else { // 打开“无内容”提示 empty.style.setProperty('display', 'block') } } /** * 处理链接的点击事件 * @event * @param {Event} e 事件对象 */ function handleVisitLink(e) { e.preventDefault() shell.openExternal(e.currentTarget.href) // 由系统的默认浏览器打开链接 } /** * 执行搜索(直接从输入框中获取搜索关键词),并更新界面显示 * @event */ async function handleSearch() { const text = inputBar.value if (text && text.length) { inputBar.value = '' clearContent() const list = await gitee.search(text) updateContentFromList(list) } } inputBar.addEventListener('keyup', e => { if (e.key == 'Enter') { handleSearch() } }) searchBtn.addEventListener('click', handleSearch) ``` ### 窗口集成测试 #### 编写测试文件 - test/ui.test.js ```js const { describe, it, before, after } = require('mocha') const { assert } = require('chai') const { Application } = require('spectron') const electronPath = require('electron') const path = require('path') let entryPointPath = path.join(__dirname, '..') // 入口脚本路径 let app // 测试首页页面元素 describe('首页', function() { this.timeout(10000) before(() => { app = new Application({ path: electronPath, args: [entryPointPath], webdriverOptions: { deprecationWarnings: false, // 关闭 moveTo 弃用警告 }, }) return app.start() }) before(() => { return app.client.waitUntilWindowLoaded() }) after(() => { if (app && app.isRunning()) { return app.stop() } }) // 正常启动窗口 describe('窗口启动', function() { it('开发者工具已关闭', async () => { const devToolsAreOpen = await app.browserWindow.isDevToolsOpened() assert.equal(devToolsAreOpen, false) }) it('启动一个窗口', async () => { const count = await app.client.getWindowCount() assert.equal(count, 1) }) it('窗口标题', async () => { const title = await app.client.getTitle() assert.equal(title, 'demo-electron-js') }) }) // 正常显示定义的界面元素 describe('显示的界面元素', function() { it('H1标题', async () => { const hello = await app.client.getText('h1') assert.equal(hello, 'Gitee Search') }) it('搜索输入框', async () => { const exist = await app.client.$('input[type="search"]').isExisting() assert(exist) }) it('搜索按钮', async () => { const exist = await app.client.$('button.search').isExisting() assert(exist) }) it('内容空提示信息', async () => { const exist = await app.client.$('.empty').isExisting() assert(exist) }) }) }) // 测试搜索功能 describe('搜索', function() { this.timeout(10000) before(() => { app = new Application({ path: electronPath, args: [entryPointPath], webdriverOptions: { deprecationWarnings: false, // 关闭 moveTo 弃用警告 }, }) return app.start() }) before(() => { return app.client.waitUntilWindowLoaded() }) after(() => { if (app && app.isRunning()) { return app.stop() } }) // 输入框回车搜索正常关键字 describe('输入框回车搜索', function() { it('输入框输入内容', async () => { await app.client.setValue('input[type="search"]', 'electron') const value = await app.client.$('input[type="search"]').getValue() assert.equal(value, 'electron') }) it('回车搜索', async () => { await app.client.$('input[type="search"]').keys('Enter') await app.client.waitUntil(async () => { return await app.client.$('#content a').isExisting() }, 10000) }) it('输入框内容清空', async () => { const value = await app.client.$('input[type="search"]').getValue() assert.isEmpty(value) }) it('不显示无内容消息提示', async () => { const display = await app.client.$('.empty').getHTML() assert.include(display, 'display: none') }) }) // 点击搜索按钮搜索正常关键字 describe('点击搜索', function() { it('输入框输入内容', async () => { await app.client.setValue('input[type="search"]', 'electron') const value = await app.client.$('input[type="search"]').getValue() assert.equal(value, 'electron') }) it('点击搜索', async () => { await app.client.$('button.search').click() await app.client.waitUntil(async () => { return await app.client.$('#content a').isExisting() }, 10000) }) it('输入框内容清空', async () => { const value = await app.client.$('input[type="search"]').getValue() assert.isEmpty(value) }) it('不显示无内容消息提示', async () => { const display = await app.client.$('.empty').getHTML() assert.include(display, 'display: none') }) }) // 搜索无效关键字 describe('无效搜索', function() { it('无内容显示', async () => { await app.client.setValue( 'input[type="search"]', '@#$%#$%EWRFEWEerewrt21511313@#$%#' ) await app.client.$('button.search').click() await app.client.pause(5000) const html = await app.client.$('#content a') assert.equal(html.state, 'failure') }) it('输入框内容清空', async () => { const value = await app.client.$('input[type="search"]').getValue() assert.isEmpty(value) }) it('显示无内容消息提示', async () => { const display = await app.client.$('.empty').getHTML() assert.include(display, 'display: block') }) }) }) ``` ### 测试覆盖率统计 #### 安装 nyc ```shell npm i -D nyc ``` #### 调整测试代码目录结构 将单元测试和集成测试的代码分开存放,nyc 只统计单元测试的覆盖率 ```shell tree test /F E:\DEMO-ELECTRON-JS\TEST ├───ui │ ui.test.js │ └───unit gitee.test.js ``` - 由于目录结构的调整,测试文件中的模块导入语句需要作相应的调整 #### 调整测试命令 - package.json ```json "scripts": { "test": "mocha test/**", "nyc": "nyc mocha test/unit" } ``` ### 应用打包 本项目使用 electron-builder 来打包程序 #### 安装 electron-builder ```shell npm i -D electron-builder ``` #### 更新 package.json 与打包有关的参数直接写在 package.json 中即可,配置项有很多,包括设置程序图标等,这里只是极简打包,生成一个可运行的单文件绿色软件 ```json "scripts": { "package": "electron-builder" }, "author": { "name": "PISH", "email": "pish@163.com" }, "build": { "appId": "io.gitee.pish7.demo-electron-js", "productName": "demo-electron-js", "win": { "target": "portable" } } ``` #### 执行打包 ```shell npm run package ```