Electron 和 Angular 项目升级: Angular4+Electron1.7.8 升级到 Angular13+Electron2
原项目 Angular 和 Electron 版本:
- @angular/cli: 1.4.9
- @angular/core: 4.4.6
- Electron: 1.7.8
升级后 Angular 和 Electron 版本:
- Angular: 13.3.1
- Electron: 21.2.1
流程:
angular-electron 这是一个结合 Angular 和 Electron 的项目。以此为基准环境,不需要从头构建。这个项目的 Angular13 对应的 electron 为 v18,再手动到升级 electron21.2.1,不升级也没有问题。将之前项目的核心代码复制过来,查看每一个文件,找出所有的问题。原项目文件结构如图:
将 src 复制到对应目录下。主进程文件 main.ts 放置再 app 文件夹中。
原先主进程文件 main.ts 在项目根目录下,主进程 main.ts 中使用的静态资源在 src/assets 中。现在主进程 main.ts 更换位置,所以主进程中使用的静态资源需要放置在 app 中,并且打包编译后需要用到。
现目录文件结构如图:
然后开始检查代码,首先发现的是一些语法问题,这些因语法检查而发生错误的代码,并不代码写得不对。比如变量命令需要遵循驼峰规则,字符串需要单引号,语句结束需要分号等待。这样错误应该暂时忽略。而报错的原因是,新项目的 package.json 会安装一些 eslint 相关的插件解析语法问题,建议删除这些插件或者不安装这些插件。如果保留这些插件,则代码需要符合 TypeScript 语法规则。根据 Angular 更新指南 和Electron 重大更改,以及 Angular 中文官网 和 Electron 官网 检查所有文件后,总结问题如下:
Angular 问题:
Q1: import { Http } from '@angular/http';
报错找不到模块“@angular/http”或其相应的类型声明。ts(2307)
A1: 如果使用了旧版的 HttpModule 和 Http 服务,请切换到 HttpClientModule 和 HttpClient 服务。HttpClient 简化了默认的 ergonomics(不再需要映射(map)到JSON),并且现在支持类型化的返回值和拦截器。修改为 import { HttpClient } from '@angular/common/http';
。
Q2: import { Renderer } from "@angular/core";
报错 ““@angular/core”” 没有导出的成员“Renderer”。 你是否指的是“Renderer2”?ts(2724)。
A2: 无论在哪里使用了 Renderer,现在都使用 Renderer2。
Q3: import { Observable } from 'rxjs/Rx';
找不到模块“rxjs/Rx”或其相应的类型声明。ts(2307)。
A3: 修改为 import { Observable } from 'rxjs';
Subscription、Subject、Observable 类似。
Q4: import { RequestOptions, Headers } from '@angular/http';
找不到模块“@angular/http”或其相应的类型声明。ts(2307)。
A4: 已被 HttpRequest, HttpHeaders 代替。 import { HttpHeaders } from '@angular/common/http';
。
Q5: this.http.post().map((res:any)=>res.json()).catch((error: any) => Observable.throw())
。
A5: this.http.post().pipe(catchError((error: HttpErrorResponse) => { return throwError(error || 'Server error'); }));
不再需要映射(map)到JSON,请看这里 making-a-jsonp-request。
Q6: renderer.listenGlobal()
Renderer2 没有 listenGlobal 方法。
A6: 使用 renderer.listen() 函数来实现全局事件。
Q7: this.renderer.invokeElementMethod(this.el.nativeElement, "focus");
Renderer2 没有 invokeElementMethod 方法。
A7: does not use invokeElementMethod,以下是链接中代码。
invokeElementMethod(eleRef: ElementRef, method: string) {
if (isPlatformBrowser(this.platformId)) {
eleRef.nativeElement[method]();
}
}
this.invokeElementMethod(this.el.nativeElement, "focus");
Q8: import { ComponentFactoryResolver } from "@angular/core";
“ComponentFactoryResolver”已弃用。ts(6385) let subFactory = this._cfr.resolveComponentFactory(data.component);let componentRef: any = this.detailHost.viewContainerRef.createComponent(subFactory);
。
A8: 一个简单的注册表,它将 Components 映射到生成的 ComponentFactory 类,该类可用于创建组件的实例。用于获取给定组件类型的工厂,然后使用工厂的 create() 方法创建该类型的组件。Angular 不再需要组件工厂。请使用可以直接使用 Component 类的其他 API。弃用可以不改或者修改为: let componentRef: any = this.detailHost.viewContainerRef.createComponent(data.component);
。
Q9: @ViewChild(DetailContentDirective) detailHost: DetailContentDirective;
createView() 输出 this.detailHost 为 undefined, 应该为 {viewContainerRef: ViewContainerRef_},低版本 angular 中可能会在 ngAfterViewInit() 之前完成。
A9: 官方文档中指出,视图查询是在 ngAfterViewInit() 回调函数被调用之前设置的。因此,不应该依赖在 ngAfterViewInit() 之前访问视图查询。
Q10: import 'zone.js/dist/zone-mix'
报错 Module not found: Error: Can’t resolve ‘fs’。
A10: 修改为 import 'zone.js'
。
Q11: import 'rxjs/add/operator/map';
报错 Module not found: Error: Package path ./add/operator/map is not exported from package。
A11: map catch 与 http 返回值有关,将 http 的 response 的数据通过 map 映射为 json,选择不通过 map 映射。
Q12: this.router.events.filter 类型“Observable<Event_2>”上不存在属性“filter”。ts(2339)。
A12: 修改后:
import { filter } from 'rxjs/operators';
this.router.events.pipe(
filter(event => event instanceof NavigationEnd)
).subscribe(() => {});
Q13: Observable.fromEvent().subscribe((event) => {});
类型“typeof Observable”上不存在属性“fromEvent”。ts(2339)。
A13: import { fromEvent } from 'rxjs';
直接导入 fromEvent,不需要通过 Observable。
Electron 问题:
Q1: menu.popup(win) 类型“BrowserWindow”与类型“PopupOptions”不具有相同的属性。ts(2559)。
A1: menu.popup(browserWindow?: BrowserWindow, options?: PopupOptions); => popup(options?: PopupOptions)。
Q2: dialog.showOpenDialog({ title: "选择目录", properties: ['openDirectory'] }, (files) => {})
类型“{ title: string; properties: string[]; }”的参数不能赋给类型“BrowserWindow”的参数。对象文字可以只指定已知属性,并且“properties”不在类型“BrowserWindow”中。ts(2345)。
A2: 结构修改后代码:
dialog.showOpenDialog(mainWindow, {
properties: ['openFile', 'openDirectory']
}).then(result => {
console.log(result.canceled)
console.log(result.filePaths)
}).catch(err => {
console.log(err)
})
Q3: require('electron-reload')(__dirname, {});
。
A3: 修改代码后重新加载程序: require('electron-reloader')(module);
。
Q4: 渲染进程中 import { remote } from 'electron';
模块““electron””没有导出的成员“remote”。ts(2305) this.remote = window.require('electron').remote;
类型“typeof CrossProcessExports”上不存在属性“remote”。ts(2339) remote.shell(),remote.app(),remote.dialog()。包括 import { app } from 'electron';
和 import { shell } from 'electron';
。
A4: remote 模块在 Electron 12 废弃,并将在 Electron 14 被移除 由@electronic/remote 模块替代。在主进程中执行 import { app, shell, dialog } from 'electron';
,相关用法 1.remote.app.getPath()
改为 app.getPath()
。2.shell.openItem()
改为 shell.openPath()
。3.remote.dialog; dialog.showOpenDialog()
改为 dialog.showOpenDialogSync()
,并且需要在主进程执行。
Q5: 所有渲染进程(angular)中使用 import * as fs from 'fs';
或 let fs = require('fs');
或 import * as fs from 'fs-extra';
,fs.readFileSync 和 fs.writeFile 方法报错。
A5: 所有 fs.readFileSync 和 fs.writeFile 操作需要在主进程执行。
Q6: 当引入 jquery 与 webuploader 时,<script src="./assets/js/jquery-3.3.1.min.js"></script><script src="./assets/js/webuploader/webuploader.js"></script>
,报错 webuploader 找不到 $。
A6: 由于 Electron 主进程修改了 module 对象,引入 jquery 失败,在引入jquery 之前 <script>delete window.module;</script>
。
Q7: <div><button>Action 1</button> <button>Action 2</button></div>
页面上两个button 之间的空格消失。
A7: <div ngPreserveWhitespaces><button>Action 1</button> <button>Action 2</button></div>
。https://angular.cn/api/core/Component#preserving-whitespace
以上是一些主要问题,或许还有一些不那么重要的问题,但是可能也会引起程序错误。
问题分为 Angular 升级带来的问题,Electron 升级带来的问题,包括 Angular 升级引起第三方模块不兼容的问题。还有一些 css 样式可能会变化,以及一些 TypeScript 语法问题。
框架升级是一件非常麻烦的事,特别是跨许多大版本升级,需要注意的事项很多,如果项目很大,升级后需要完整测试。