# demo-electron-ts-sass **Repository Path**: pish7/demo-electron-ts-sass ## Basic Information - **Project Name**: demo-electron-ts-sass - **Description**: Electron 基础项目,使用 typescript 和 sass。 - **Primary Language**: TypeScript - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2019-12-04 - **Last Updated**: 2021-05-17 ## Categories & Tags **Categories**: Uncategorized **Tags**: Electron, Project ## README # demo-electron-ts-sass ## 介绍 Electron 基础项目,使用 typescript 和 sass。 本项目主要是为了记录开发流程,不会过多地关注细节 ## 功能描述 通过 gitee api 进行关键词搜索,界面上有一个输入框和一个按钮,在输入框中输入关键词,点击搜索按钮,即可显示出搜索结果列表。点击列表项可以打开网址。 ## 开发步骤 ### 初始化项目 1. 先在远程仓库创建项目,并使用 git clone 命令将项目克隆到本地 1. 进入本地目录,执行 npm init 初始化项目 ### 实现 Gitee 搜索 #### 搭建基础目录结构 在项目根路径下创建 src 和 test/unit 两个文件夹,src 用于存放项目源文件,test 用于存放测试文件。 #### 安装测试框架 本项目使用 mocha、chai、spectron 和 nyc 进行测试,下面一并进行安装 ```shell npm i -D mocha chai spectron nyc ``` #### 安装 typescript ```shell npm i -D typescript ts-node ``` - ts-node 用于 mocha 运行测试文件 #### 安装 Axios 本项目的网络请求使用 axios ```shell npm i -S axios ``` #### 安装 typescript 类型声明文件 ```shell npm i -D @types/node @types/mocha @types/chai ``` #### 初始化 typescript 配置 ```shell npx tsc --init ``` #### 编写 gitee.ts 和相应的测试文件 gitee.ts 实现一个 search 方法用于向 gitee 搜索关键字并返回结果 - test/unit/gitee.test.ts ```typescript import { describe, it } from 'mocha' import { assert } from 'chai' import * as gitee from '../../src/gitee' describe('测试 Gitee', function() { this.timeout(10000) describe('搜索', function() { it('正常关键词搜索', async () => { const result = (await gitee.search('electron')) as gitee.ISearchItem[] 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) }) }) }) ``` - src/gitee.ts ```typescript import axios from 'axios' /** * Gitee搜索的返回值 */ export interface ISearchItem { name: string // 项目名称 description: string // 项目描述 url: string // 项目地址 } /** * 在 Gitee 上搜索指定的关键词,并返回结果 * @param {string} keyword 搜索的关键词 * @return {Promise} 搜索到的结果 */ export async function search(keyword: string): Promise { 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) { const rtn: ISearchItem[] = result.data.map((val: any) => { return { name: val.full_name, description: val.description, url: val.html_url, } }) return rtn } } catch (e) { console.error(e) } return null } ``` #### 配置 mocha 由于源文件都是使用 typescript 编写,要使用 mocha 测试需要先对 mocha 进行配置。创建 .mocharc.json 文件并作如下配置: ```json { "extension": ["ts"], "spec": ["test/**/*.ts", "test/**/*.js"], "require": ["ts-node/register"] } ``` - [Mocha 配置文件](https://mochajs.org/#configuring-mocha-nodejs) - [Mocha 配置选项](https://mochajs.org/#command-line-usage) - [Mocha Typescript 配置示例](https://github.com/mochajs/mocha-examples/blob/master/packages/typescript/.mocharc.json) #### 添加测试命令 向 package.json 中添加一个测试命令 ```json "scripts": { "test": "mocha" } ``` #### 执行测试 ```shell npm test ``` ### 使用 nyc 统计测试覆盖率 #### 安装插件 由于源码使用 typescript 编写,可以添加一个扩展让 nyc 正常工作 ```shell npm i -D @istanbuljs/nyc-config-typescript ``` - [nyc Github](https://github.com/istanbuljs/nyc#typescript-projects) - [@istanbuljs/nyc-config-typescript](https://www.npmjs.com/package/@istanbuljs/nyc-config-typescript) - [nyc homepage](https://istanbul.js.org/docs/tutorials/typescript/) #### 配置 nyc 创建一个 .nycrc.json 文件,并对 nyc 进行配置: ```json { "extends": "@istanbuljs/nyc-config-typescript", "reporter": ["html", "text"] } ``` - 按照官方说明,还需要安装 source-map-support - 按照官方说明,还需要配置 tsconfig.json 并在 mocha 的配置文件中添加以下配置 ```json "require": ["ts-node/register", "source-map-support/register"] ``` #### 添加 nyc 命令 - package.josn ```json "scripts": { "nyc": "nyc mocha" } ``` #### 执行测试 ```shell npm run nyc ``` ### 使用 gulp-typescript 编译 ts 文件 #### 安装 Gulp 下面安装 gulp 及相关扩展 ```shell cnpm i -D gulp-cli gulp gulp-typescript gulp-sourcemaps ``` - [gulp-typescript](https://www.npmjs.com/package/gulp-typescript) - gulp-sourcemaps 还将用于后面的 gulp-sass #### 编写 Gulp 配置文件 - gulpfile.js ```js const { series, parallel, src, dest, watch } = require('gulp') const ts = require('gulp-typescript') const tsProject = ts.createProject('./tsconfig.json') const sourcemaps = require('gulp-sourcemaps') // 转译 TypeScript 到 JavaScript exports.ts = function ts() { return src(['src/**/*.ts'], { base: './src', }) .pipe(sourcemaps.init()) .pipe(tsProject()) .js.pipe(sourcemaps.write()) .pipe(dest('app/')) } // 定义默认任务 const defaultTask = series(this.ts) exports.default = defaultTask // 定义监视任务 exports.auto = function auto() { return watch('src/**', { ignoreInitial: false }, defaultTask) } ``` #### 添加 build 命令 - package.json ```json "scripts": { "build": "gulp" } ``` #### 转译 typescript 文件 ```shell npm run build ``` ### 开发第一个窗口 第一个窗口只是一个简单的测试窗口,未实现具体功能 #### 安装 electron 和 devtron ```shell npm i -D electron devtron ``` - devtron 是一个开发辅助工具 #### 编写主进程程序和窗口 HTML 这里先实现一个简单的主进程程序,让 electron 程序运行起来 - src/main.ts ```typescript import { app, BrowserWindow, Menu } from 'electron' /** * 窗口句柄 * @type {BrowserWindow|null} */ let win: BrowserWindow | null = null /** * 当前环境是否为开发环境 * @const * @type {boolean} */ const isDevelopment = process.env.NODE_ENV == 'development' /** * 创建窗口 */ function createWindow() { // 创建一个窗口 win = new BrowserWindow({ minWidth: 400, minHeight: 300, show: false, webPreferences: { nodeIntegration: true, }, }) // 装载界面文件 win.loadFile(`${__dirname}/views/index.html`) // 窗口关闭 win.on('closed', () => { win = null }) // 渲染进程第一次完成绘制 win.on('ready-to-show', () => { win && win.show() }) } // 全部窗口关闭时程序退出 app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() } }) app.on('ready', () => { createWindow() // 创建窗口 // 如果运行在开发环境中,则打开开发工具 if (isDevelopment) { require('devtron').install() // 安装 devtron win && win.webContents.openDevTools() // 打开开发者工具 } else { Menu.setApplicationMenu(null) // 移除菜单 } }) ``` - src/views/index.html ```html demo-electron-ts-sass

Gitee Search

``` #### 修改 gulpfile.js 以复制静态文件 - gulpfile.js ```js const { series, parallel, src, dest, watch } = require('gulp') const ts = require('gulp-typescript') const tsProject = ts.createProject('./tsconfig.json') const sourcemaps = require('gulp-sourcemaps') // 复制文件 exports.copyStatic = function copyStatic() { return src(['src/assets/**', 'src/views/**'], { base: './src', }).pipe(dest('app/')) } // 转译 TypeScript 到 JavaScript exports.ts = function ts() { return src(['src/**/*.ts'], { base: './src', }) .pipe(sourcemaps.init()) .pipe(tsProject()) .js.pipe(sourcemaps.write()) .pipe(dest('app/')) } // 定义默认任务 const defaultTask = series(this.copyStatic, this.ts) exports.default = defaultTask // 定义监视任务 exports.auto = function auto() { return watch('src/**', { ignoreInitial: false }, defaultTask) } ``` #### 修改 package.json 以运行程序 - package.json ```json "main": "app/main.js", "scripts": { "start": "set NODE_ENV=development&& electron .", } ``` - main 字段指向主进程启动文件 - start 命令添加环境变量设置,让程序运行在开发模式下 #### 运行第一个窗口 ```shell npm run build npm start ``` ### 测试第一个窗口 #### 编写窗口测试文件 - test/ui/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-ts-sass') }) }) // 正常显示定义的界面元素 describe('显示的界面元素', function() { it('H1标题', async () => { const hello = await app.client.getText('h1') assert.equal(hello, 'Gitee Search') }) }) }) ``` #### 测试第一个窗口 ```shell npm test ``` ### 开发窗口页面 接下来开发完整的窗口页面功能,页面上可以在输入框中输入搜索内容,并点击搜索按钮,在获取到结果后会将结果显示在页面中,并且每个项目都可点击,并由系统的默认浏览器打开链接 #### 编写界面文件 - src/views/index.html ```html demo-electron-ts-sass

Gitee Search

``` - src/style/index.scss ```scss * { 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; input[type='search'] { display: block; padding-left: 0.5em; width: 100%; height: 2em; } 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); } > div { padding: 0.5em 0; border-bottom: 1px dotted #aaa; } a { text-decoration: none; } a:hover { color: rgb(24, 117, 194); text-decoration: underline; } .name { font-weight: bold; } .description { font-size: 80%; } } } } ``` - src/index.ts ```typescript import { shell } from 'electron' import * as gitee from './gitee' /** 输入框 */ const inputBar = document.querySelector( '.search [type="search"]' ) as HTMLInputElement /** 搜索按钮 */ const searchBtn = document.querySelector('button.search') as HTMLButtonElement /** “无内容”提示文字 */ const empty = document.querySelector('.empty') as HTMLDivElement /** 主体区 */ const mainArea = document.getElementById('content') as HTMLDivElement /** 模板 */ const template = document.getElementById( 'template-item' ) as HTMLTemplateElement /** * 清空主体显示区域 */ function clearContent() { // 清除列表项 let firstChild = mainArea.firstChild while (firstChild) { mainArea.removeChild(firstChild) firstChild = mainArea.firstChild } // 关闭“无内容”提示 empty.style.setProperty('display', 'none') } /** * 根据提供的数据更新界面中主体区域的显示 * @param {ISearchItem[]|null} list 数据列表 */ function updateContentFromList(list: gitee.ISearchItem[] | null) { if (list && list.length) { // 显示列表项 for (const item of list) { const clone = document.importNode(template.content, true) // 复制一个节点以及其子孙节点 const a = clone.querySelector('a') as HTMLElement const name = clone.querySelector('.name') as HTMLElement const description = clone.querySelector('.description') as HTMLElement 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: Event) { e.preventDefault() shell.openExternal((e.currentTarget as HTMLAnchorElement).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) ``` #### 添加 sass 编译工具 ```shell npm i -D sass gulp-sass ``` - [sass](https://www.npmjs.com/package/sass) - [gulp-sass](https://www.npmjs.com/package/gulp-sass) #### 修改 gulpfile.js 以支持 sass 编译 - gulpfile.js ```js const { series, parallel, src, dest, watch } = require('gulp') const ts = require('gulp-typescript') const tsProject = ts.createProject('./tsconfig.json') const sass = require('gulp-sass') const sourcemaps = require('gulp-sourcemaps') sass.compiler = require('sass') // 复制文件 exports.copyStatic = function copyStatic() { return src(['src/assets/**', 'src/views/**'], { base: './src', }).pipe(dest('app/')) } // 转译 TypeScript 到 JavaScript exports.ts = function ts() { return src(['src/**/*.ts'], { base: './src', }) .pipe(sourcemaps.init()) .pipe(tsProject()) .js.pipe(sourcemaps.write()) .pipe(dest('app/')) } // 转译 scss 到 css exports.build_scss = function build_scss() { return src(['src/**/*.scss'], { base: './src', }) .pipe(sourcemaps.init()) .pipe(sass().on('error', sass.logError)) .pipe(sourcemaps.write()) .pipe(dest('app/')) } // 定义默认任务 const defaultTask = series(this.copyStatic, this.ts, this.build_scss) exports.default = defaultTask // 定义监视任务 exports.auto = function auto() { return watch('src/**', { ignoreInitial: false }, defaultTask) } ``` #### 编译并运行程序 ```shell npm run build npm start ``` ### 窗口集成测试 #### 编写测试文件 - test/ui/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-ts-sass') }) }) // 正常显示定义的界面元素 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 只运行单元测试 - package.json ```json "scripts": { "test": "mocha test/**", "nyc": "nyc mocha test/unit", } ``` - .mocharc.json ```json { "extension": ["ts"], "require": ["ts-node/register"] } ``` ### 应用打包 本项目使用 electron-builder 来打包程序 #### 安装 electron-builder ```shell npm i -D electron-builder ``` #### 更新 package.json 与打包有关的参数直接写在 package.json 中即可,配置项有很多,包括设置程序图标等,这里只是极简打包,生成一个可运行的单文件绿色软件 ```json "scripts": { "package": "npm run build && electron-builder" }, "build": { "appId": "io.gitee.pish7.demo-electron-ts-sass", "productName": "demo-electron-ts-sass", "win": { "target": "portable" } } ``` #### 执行打包 ```shell npm run package ```