2

背景

最近新项目的需求是开发单机桌面应用,并且已经完成了 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 后,我们的项目结构就变成了这样:

image.png

electron + angular 项目中引入 typeorm + sqlite3

初始化 electron + angular 项目

此部分内容请点击这里

将main.js 改写为 main.ts

首先,在根目录下新建 main.ts 文件,建好后可以看到 main.js 文件自动归到 main.ts 文件下了,就像这样:
image.png

如果你是跟着上述我的方案初始化的项目的话,直接复制我的代码到你的 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 文件,像这样:
image.png

这个 service 就可以视作一个渲染进程,在这里面我们需要调用 electron 中的某些类或者某些对象来与主进程进行通信,以实现对数据库的操作。

npm 库中可以找到一些包以帮助我们在 angular 中调用 electron 的对象,如 ngx-electron、ngx-electron-fresher 等等。但是经过多次尝试,发现最后总有不适配的地方导致无法调用。

最后在网上找到了一位国外大佬的仓库,从那里面摘了我们需要的东西出来。

读者可以点进文章末尾的仓库地址,克隆完项目后,将根目录/src/app 下的 core 文件夹复制一份后粘贴到本项目的根目录/src/app 下,就像这样:
image.png

然后就可以创建我们的服务了。

// 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 启动我们的项目即可。

re.gif

希望这篇文章能对你有所帮助!

完整 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


HHepan
164 声望13 粉丝

河北工业大学