背景
最近新项目的需求是开发单机桌面应用,并且已经完成了 electron + angular 的项目前端初始化。
一个项目中除了前端,还有数据库是必不可少的。如果使用 spring boot 搭建后台部署到服务器上,再通过配置 api 以联网去连接数据库的话,这么做大可不必且着实有损我们开发单机应用的初衷。
经过查询资料,又加上我们项目不大,最终决定用 typeorm + sqlite3 实现数据库功能。
读者可以先看文末的效果预览,再返回来看实现过程。
typeorm
TypeORM 是一种 ORM,可以在 NodeJS、Electron 等多种平台上运行,并且可以与 TypeScript 和 JavaScript(ES5、ES6、ES7、ES8)一起使用。它可以帮助开发任何类型的使用数据库的应用程序。是一种操作数据库的工具。更多详情请看 typeorm官方文档。
sqlite3
SQLite是一个C语言库,它实现了一个小型、快速、独立、高可靠性、功能齐全的SQL数据库引擎。是一个数据库。更多详情请看 sqlite官方文档。
项目结构
加上 typeorm 和 sqlite3 后,我们的项目结构就变成了这样:
electron + angular 项目中引入 typeorm + sqlite3
初始化 electron + angular 项目
此部分内容请点击这里。
将main.js 改写为 main.ts
首先,在根目录下新建 main.ts 文件,建好后可以看到 main.js 文件自动归到 main.ts 文件下了,就像这样:
如果你是跟着上述我的方案初始化的项目的话,直接复制我的代码到你的 main.ts 中即可。如果是自己的项目,那么就得按自己的情况将 js 的写法转换成 ts 的写法了。当然这也并不困难。
// main.ts 中
import {app, BrowserWindow, screen, globalShortcut} from 'electron';
import "reflect-metadata";
import * as path from 'path';
import * as url from 'url';
let win: BrowserWindow | null = null;
const args = process.argv.slice(1),
serve = args.some(val => val === '--dev');
async function createWindow(): Promise<BrowserWindow> {
const size = screen.getPrimaryDisplay().workAreaSize;
// Create the browser window.
win = new BrowserWindow({
x: 0,
y: 0,
width: size.width,
height: size.height,
webPreferences: {
nodeIntegration: true,
allowRunningInsecureContent: (serve),
contextIsolation: false,
},
});
if (serve) {
console.log('加载localhost:4200');
win.loadURL('http://localhost:4200');
} else {
console.log('加载dist/');
win.loadURL(url.format({
pathname: path.join(
__dirname,
'dist/angular-electron-app/index.html'),
protocol: 'file:',
slashes: true
}))
}
// 打开开发者工具
win.webContents.openDevTools()
// 当 window 被关闭,这个事件会被触发。
win.on('closed', () => {
// 取消引用 window 对象,如果你的应用支持多窗口的话,
// 通常会把多个 window 对象存放在一个数组里面,
// 与此同时,你应该删除相应的元素。
win = null
})
return win;
}
try {
//在ready事件里
app.on('ready', () => {
setTimeout(createWindow, 400)
globalShortcut.register('CommandOrControl+Shift+i', function () {
if (win !== null) {
if (win.webContents.isDevToolsOpened()) {
win.webContents.closeDevTools()
} else {
win.webContents.openDevTools()
}
}
})
});
// 当全部窗口关闭时退出。
app.on('window-all-closed', () => {
// 在 macOS 上,除非用户用 Cmd + Q 确定地退出,
// 否则绝大部分应用及其菜单栏会保持激活。
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// 在macOS上,当单击dock图标并且没有其他窗口打开时,
// 通常在应用程序中重新创建一个窗口。
if (win === null) {
createWindow();
}
});
} catch (e) {
// Catch Error
// throw e;
}
完成 main.ts 后,我们还需要去更新一下脚本:
// package.json 中
"scripts": {
...
"electron": "tsc main.ts & npm start & wait-on http-get://localhost:4200/ && electron . --dev",
...
},
脚本中新增加的命令 tsc main.ts
的作用就是将 main.ts 编译出 main.js 。
此时项目根目录下终端中运行 npm run electron
也是可以成功启动的。
创建实体
完成上述准备工作后,我们就可以开始创建实体了。首先要安装 typeorm 依赖:
npm install typeorm --save
然后新建两个文件夹:
mkdir ./src/assets/data ./src/assets/entities
在我们的 entities 文件夹下新建文件 item.entity.ts ,创建我们的 item 实体:
// entities/item.entity.ts 中
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity()
export class Item
{
@PrimaryGeneratedColumn()
id: number | undefined;
@Column({ type: 'varchar' })
name: string | undefined;
}
创建服务
熟悉 angular + spring boot 模式开发的朋友们都知道,通常情况下,前台向后台发起请求的代码基本都放在 service 中。所以这里我们也需要创建服务。
src/app 下新建 app.service.ts 文件,像这样:
这个 service 就可以视作一个渲染进程,在这里面我们需要调用 electron 中的某些类或者某些对象来与主进程进行通信,以实现对数据库的操作。
npm 库中可以找到一些包以帮助我们在 angular 中调用 electron 的对象,如 ngx-electron、ngx-electron-fresher 等等。但是经过多次尝试,发现最后总有不适配的地方导致无法调用。
最后在网上找到了一位国外大佬的仓库,从那里面摘了我们需要的东西出来。
读者可以点进文章末尾的仓库地址,克隆完项目后,将根目录/src/app 下的 core 文件夹复制一份后粘贴到本项目的根目录/src/app 下,就像这样:
然后就可以创建我们的服务了。
// app.service.ts 中
import { Injectable } from '@angular/core';
import { Item } from '../assets/entities/item.entity';
import { ElectronService } from './core/services';
import { Observable, of, throwError } from 'rxjs';
import { catchError } from 'rxjs/operators';
@Injectable()
export class AppService {
constructor(private _electronService: ElectronService) {}
getItems(): Observable<Item[]> {
return of(this._electronService.ipcRenderer.sendSync('get-items')).pipe(
catchError((error: any) => throwError(error.json))
);
}
addItem(item: Item): Observable<Item[]> {
return of(
this._electronService.ipcRenderer.sendSync('add-item', item)
).pipe(catchError((error: any) => throwError(error.json)));
}
deleteItem(item: Item): Observable<Item[]> {
return of(
this._electronService.ipcRenderer.sendSync('delete-item', item)
).pipe(catchError((error: any) => throwError(error.json)));
}
}
调用服务
有了服务层,我们可以直接去C层中调用对应服务即可。
// app.component.ts 中
import { Component, OnInit } from '@angular/core';
import {Item} from "../assets/entities/item.entity";
import {AppService} from "./app.service";
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
title = 'angular-electron-app';
itemList = [] as Item[];
constructor(private appService: AppService) {}
ngOnInit(): void {
console.log('component initialized');
this.appService.getItems().subscribe((items) => (this.itemList = items));
}
addItem(): void {
let item = new Item();
item.name = 'Item ' + this.itemList.length;
this.appService.addItem(item).subscribe((items) => (this.itemList = items));
}
deleteItem(): void {
const item = this.itemList[this.itemList.length - 1];
this.appService
.deleteItem(item)
.subscribe((items) => (this.itemList = items));
}
}
module 中要记得提供对应服务。
// app.module.ts 中
...
import {AppService} from "./app.service";
import {ElectronService} from "./core/services";
@NgModule({
...
providers: [AppService, ElectronService],
...
})
export class AppModule { }
最后别忘了做一个简单的界面。
// app.component.html 中
<div style="text-align:center">
<h1>
Welcome to {{ title }}!
</h1>
<button (click)="addItem()">Add Item</button>
<button *ngIf="itemList.length > 0" (click)="deleteItem()">Delete Item</button>
<h2>Here is the contents of the database: </h2>
<div>
<ul style="list-style: none">
<li *ngFor="let item of itemList">
{{ item.name }}
</li>
</ul>
</div>
</div>
连接数据库并执行简单操作
要连接数据库首先安装sqlite3,执行命令安装:
npm install sqlite3 --save
安装成功后就可以去到主进程(main.ts)中去连接数据库并执行一些简单的增删操作了。
// main.ts的createWindows()方法中补充配置
const connection = await createConnection({
type: 'sqlite',
synchronize: true,
logging: true,
logger: 'simple-console',
database: './src/assets/data/database.sqlite',
entities: [ Item ],
});
const itemRepo = connection.getRepository(Item);
ipcMain.on('get-items', async (event: any, ...args: any[]) => {
try {
event.returnValue = await itemRepo.find();
} catch (err) {
throw err;
}
});
ipcMain.on('add-item', async (event: any, _item: Item) => {
try {
const item = await itemRepo.create(_item);
await itemRepo.save(item);
event.returnValue = await itemRepo.find();
} catch (err) {
throw err;
}
});
ipcMain.on('delete-item', async (event: any, _item: Item) => {
try {
const item = await itemRepo.create(_item);
await itemRepo.remove(item);
event.returnValue = await itemRepo.find();
} catch (err) {
throw err;
}
});
效果预览
完成上述步骤后,根目录下执行 npm run electron
启动我们的项目即可。
希望这篇文章能对你有所帮助!
完整 main.ts
import {app, BrowserWindow, screen, globalShortcut, ipcMain} from 'electron';
import "reflect-metadata";
import * as path from 'path';
import * as url from 'url';
import {createConnection} from "typeorm";
import {Item} from "./src/assets/entities/item.entity";
let win: BrowserWindow | null = null;
const args = process.argv.slice(1),
serve = args.some(val => val === '--dev');
async function createWindow(): Promise<BrowserWindow> {
const connection = await createConnection({
type: 'sqlite',
synchronize: true,
logging: true,
logger: 'simple-console',
database: './src/assets/data/database.sqlite',
entities: [ Item ],
});
const itemRepo = connection.getRepository(Item);
const size = screen.getPrimaryDisplay().workAreaSize;
// Create the browser window.
win = new BrowserWindow({
x: 0,
y: 0,
width: size.width,
height: size.height,
webPreferences: {
nodeIntegration: true,
allowRunningInsecureContent: (serve),
contextIsolation: false,
},
});
if (serve) {
console.log('加载localhost:4200');
win.loadURL('http://localhost:4200');
} else {
console.log('加载dist/');
win.loadURL(url.format({
pathname: path.join(
__dirname,
'dist/angular-electron-app/index.html'),
protocol: 'file:',
slashes: true
}))
}
// 打开开发者工具
win.webContents.openDevTools()
// 当 window 被关闭,这个事件会被触发。
win.on('closed', () => {
// 取消引用 window 对象,如果你的应用支持多窗口的话,
// 通常会把多个 window 对象存放在一个数组里面,
// 与此同时,你应该删除相应的元素。
win = null
})
ipcMain.on('get-items', async (event: any, ...args: any[]) => {
try {
event.returnValue = await itemRepo.find();
} catch (err) {
throw err;
}
});
ipcMain.on('add-item', async (event: any, _item: Item) => {
try {
const item = await itemRepo.create(_item);
await itemRepo.save(item);
event.returnValue = await itemRepo.find();
} catch (err) {
throw err;
}
});
ipcMain.on('delete-item', async (event: any, _item: Item) => {
try {
const item = await itemRepo.create(_item);
await itemRepo.remove(item);
event.returnValue = await itemRepo.find();
} catch (err) {
throw err;
}
});
return win;
}
try {
//在ready事件里
app.on('ready', () => {
setTimeout(createWindow, 400)
globalShortcut.register('CommandOrControl+Shift+i', function () {
if (win !== null) {
if (win.webContents.isDevToolsOpened()) {
win.webContents.closeDevTools()
} else {
win.webContents.openDevTools()
}
}
})
});
// 当全部窗口关闭时退出。
app.on('window-all-closed', () => {
// 在 macOS 上,除非用户用 Cmd + Q 确定地退出,
// 否则绝大部分应用及其菜单栏会保持激活。
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// 在macOS上,当单击dock图标并且没有其他窗口打开时,
// 通常在应用程序中重新创建一个窗口。
if (win === null) {
createWindow();
}
});
} catch (e) {
// Catch Error
// throw e;
}
完整 package.json
{
"name": "angular-electron-app",
"version": "0.0.0",
"main": "main.js",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test",
"electron": "tsc main.ts & npm start & wait-on http-get://localhost:4200/ && electron . --dev",
"package": "npm run build && electron-forge package",
"make": "npm run build && electron-forge make"
},
"private": true,
"dependencies": {
"@angular/animations": "^15.2.0",
"@angular/common": "^15.2.0",
"@angular/compiler": "^15.2.0",
"@angular/core": "^15.2.0",
"@angular/forms": "^15.2.0",
"@angular/platform-browser": "^15.2.0",
"@angular/platform-browser-dynamic": "^15.2.0",
"@angular/router": "^15.2.0",
"electron-squirrel-startup": "^1.0.0",
"rxjs": "~7.8.0",
"sqlite3": "^5.1.6",
"tslib": "^2.3.0",
"typeorm": "^0.3.17",
"zone.js": "~0.12.0"
},
"devDependencies": {
"@angular-devkit/build-angular": "^15.2.9",
"@angular/cli": "~15.2.9",
"@angular/compiler-cli": "^15.2.0",
"@electron-forge/cli": "^6.4.2",
"@electron-forge/maker-deb": "^6.4.2",
"@electron-forge/maker-rpm": "^6.4.2",
"@electron-forge/maker-squirrel": "^6.4.2",
"@electron-forge/maker-zip": "^6.4.2",
"@electron-forge/plugin-auto-unpack-natives": "^6.4.2",
"@types/jasmine": "~4.3.0",
"electron": "^26.2.1",
"jasmine-core": "~4.5.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.0.0",
"typescript": "~4.9.4",
"wait-on": "^7.0.1"
}
}
仓库地址
https://github.com/HHepan/angular-electron-typeorm-sqlite3-app
参考资料
https://morioh.com/a/70e2077b4b8e/building-a-electron-app-usi...
https://github.com/maximegris/angular-electron/tree/angular15
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。