从零开始创建基于Angular的Electron应用程序

Author Avatar
EdgeNeko 1月 23, 2020
  • 在其它设备中阅读本文章

警告:阅读下面内容前请详细阅读

下面的内容是我在尝试构建RailgunMediaEncoder中尝试使用的方法, 我无法保证下面的方法绝对符合”最佳实践”.
USE IT AT YOUR OWN RISK.
如果有任何问题及建议,欢迎提出.

0. 开始之前

一个标准的Electron的应用由两部分组成: 用于在后台运行的主线程项目和在前台运行的ui线程项目. 这两部分是相对独立的, 其间的通讯只能通过ipc(线程间通讯)完成. 在开发时, 这两部分也应该作为两个独立的项目来管理.
这里我们主线程使用纯TypeScript完成主线程项目, 使用Angular + TypeScript完成ui线程项目.
下面的例子中我们希望的源代码目录结构:
Project Structure

1. 创建Angular项目

首先确保你已经全局安装了Angular CLI,如果没有, 使用下面的命令安装:

npm install -g @angular/cli

安装后,首先使用以下命令来创建一个空白项目(不包含app)

ng new --createApplication=false --newProjectRoot="./" HelloElectron

完成后,建议使用vscode打开项目目录(这里为HelloElectron)来继续我们的下一步操作.

cd HelloElectron
code .

继续执行以下命令来创建前端应用

ng generate app ui

在 cli 询问 ? Would you like to add Angular routing? (y/N)时, 输入y来添加路由, 其它选项按需选择.

以上命令跑完后, 运行ng serve --open 应该可以在浏览器中看到angular的hello world页面.
Hello world

接下来,打开angular.json修改生成设置.

  1. 修改outputPathdist/app/ui
  2. 修改outputHashingnone

打开ui/src/index.html, 修改base href:

  <base href="/">
  修改为:
  <base href="./">

2.配置Electron

安装以下必备库

  • Electron npm install --save-dev electron
  • ngx-electron npm install --save ngx-electron
  • @types/node npm install --save-dev @types/node
  • electron-builder (用于打包electron应用, 若手动打包可不安装) npm install --save-dev electron-builder

PS: 如果你深受The Girl Friend Wall困扰的话, 安装Electron时post-install部分可能会卡非常非常非常久(npm需要下载90MB左右的Electron包). 此时可以参考http-proxy设置代理.

3. 建立主线程项目

首先, 创建目录core/ , 在之中创建tsconfig.json, 内容参考如下

{
  "compileOnSave": false,
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2015",
    "sourceMap": true,
    "outDir": "../dist/app",
    "typeRoots": [
      "../node_modules/@types"
    ],
    "moduleResolution": "node",
    "experimentalDecorators": true
  }
}

创建tslint.json,配置可自行按需设定,或直接复制ui/tslint.json中的内容.

创建index.ts作为整个Electron应用的入口点, 内容如下

import * as path from 'path';
import {app, BrowserWindow} from 'electron';


let mainWindow: BrowserWindow = null;
// 单进程锁, 按需添加
function singleInstance() {
  app.requestSingleInstanceLock();

  app.on('second-instance', () => {
    if (mainWindow) {
      if (mainWindow.isMinimized()) {
        mainWindow.restore();
      }
      mainWindow.focus();
    }
  });
}

// 创建主窗口
function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1080,
    minWidth: 680,
    height: 840,
    frame: false,
    title: app.name,
    webPreferences: {
      nodeIntegration: true,
      experimentalFeatures: true
    }
  });

  mainWindow.loadURL(path.join('file://', __dirname, 'ui/index.html'));
  mainWindow.webContents.on('will-navigate', (event, url) => {
    if (url !== path.join('file://', __dirname, 'ui/index.html')) {
      event.preventDefault();
      mainWindow.loadURL(path.join('file://', __dirname, 'ui/index.html'));
    }
  });
}

function init() {
  singleInstance();
  app.on('ready', () => {
    createWindow();
    // 创建窗口后立即打开DevTools, 按需选择.
    //mainWindow.webContents.openDevTools();
  });

  app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
      app.quit();
    }
  });

  app.on('activate', () => {
    if (mainWindow === null) {
      createWindow();
    }
  });
}

init();

4. 配置生成脚本

打开package.json, 将script节替换为以下内容:

  "scripts": {
    "build": "tsc -p core && ng build",
    "build-prod": "tsc -p core && ng build --prod",
    "start": "npm run build && electron ./dist/app/index.js",
    "clean": "rm ./dist -rf",
    "lint": "echo Linting UI && ng lint && echo Linting Core... && tslint -p core"
  },

5. 配置自动打包(可选)

首先请确保你已经安装了electron-builder
在package.json中添加build

"build": {
    "appId": "net.example.HelloElectron",
    "productName": "Hello Electron",
    "directories": {
      "app": "dist/app",
      "output": "dist/pack",
    },
    "nsis": {
      "oneClick": false,
      "perMachine": true,
      "allowToChangeInstallationDirectory": true,
      "license": "LICENSE",
      "artifactName": "${productName}.Setup.${version}.${ext}"
    },
    "win": {
      "target": [
        {
          "target": "nsis"
        },
        {
          "target": "zip"
        }
      ]
    },
    "mac": {
      "target": "dmg"
    },
    "linux": {
      "target": [
        {
          "target": "deb"
        },
        {
          "target": "rpm"
        },
        {
          "target": "tar.gz"
        }
      ]
    }
  },

有关以上json的更多信息,可参见Configuration.

然后在script节添加publish:

    "publish": "cp package.release.json dist/app/package.json && electron-builder",

接下来,创建package.build.json, 内容参考如下

{
  "name": "HelloElectron",
  "version": "1.0.0",
  "author": "EdgeNeko",
  "homepage": "https://example.com",
  "private": true,
  "license": "MIT",
  "dependencies": {
  }
}

dependencies中,添加需要打包到最终应用的node库.
有关双package.json结构, 可参考此文档.

运行以下命令即可打包:

npm run clean
npm run build-prod
npm run publish

打包结果会保存在dist/pack

6. 示例:在ui线程调用Node

创建一个服务,名为NodeService, 内容如下:

import {Injectable} from '@angular/core';
import fs from 'fs';
import {ElectronService} from 'ngx-electron';

@Injectable({
  providedIn: 'root'
})
export class NodeService {

  public fs: typeof fs;
  public childProcess: typeof childProcess;

  constructor(
    electronService: ElectronService
  ) {
    this.fs = electronService.remote.require('fs');
  }
}

接下来,就可以通过注入NodeService并以nodeService.fs来调用node中的fs了.

7. 配置调试

这部分目前我还没有什么好的解决方案,如果米娜桑有解决方案欢迎在下面提出QAQ

如果你无法看到下面的disqus评论区, 请检查你的科学上网设置.