解决 HTML Canvas 元素在高像素密度/高分辨率屏幕上显示模糊的问题

最近在使用 HTML Canvas 元素时发现通过 fillText() 绘制的文字明显发虚,像是以低分辨率渲染后放大所致,再仔细一检查,发现事实上包括 fillRect() 在内的其他图案也都存在相同的问题。

初步推测和显示设备使用了较高的 DPI 有关,随进行了一番搜索,在 Stackoverflow 、Medium 等站点都有一些提问和回答。然而意外的是,自己能找到的一些解答,至少在自己这边并不能解决问题。

经过摸索,找到了能够解决自己问题的办法,分享于此,以便后人。

以下为 Angular component 代码片段,语言为 TypeScript。

@ViewChild('canvas', { static: true }) canvas: ElementRef; // Canvas 元素
@ViewChild('canvasDiv', { static: true }) canvasDiv: ElementRef; // Canvas 元素的父元素,一个 div
private ctx: CanvasRenderingContext2D; // Canvas context

private dpiRatio: number; // DPI 缩放比例
private canvasDivWidth: number;  // 父元素宽度

……

this.dpiRatio = window.devicePixelRatio;

this.canvasDivWidth = this.canvasDiv.nativeElement.offsetWidth;

// 分别设置 canvas 的宽度和样式宽度,canvas 的显示尺寸将会减半
this.ctx = this.canvas.nativeElement.getContext('2d');
this.ctx.canvas.width = this.canvasWidth * this.dpiRatio;
this.ctx.canvas.style.width = this.canvasWidth + 'px';

// 在绘制时考虑缩放因素
this.ctx.font = 12 * this.dpiRatio + 'px Roboto';
this.ctx.fillText('HIDPI文字', x  * this.dpiRatio, y * this.dpiRatio);

一个悬而未决的问题是,canvas context 本身的 transfer 函数似乎不能做到整体的放大,因此这里作为临时方案暂时在绘制每一个图形时都乘以了缩放倍率。这里应该有更好的办法,不过限于时间,暂且搁置。

Angular 9+ 升级小记 —— 应付 MSAL-Angular 与 ngx-restangular 的兼容性问题

之前几次升级 Angular 版本的经历总体还算顺利,因此原本对 Angular 8 升级到 9 的过程也比较乐观。虽然 Angular 9 开始将默认启动新的 Angular Ivy,但考虑到已经正式发布数月,甚至 Android 10 都已问世,自己的项目又没有用特别冷门的依赖,依然没有担心会遇到问题。可惜墨菲定律无处不在,最后还是花了比想象中更多的时间完成升级。

为此,本文简单介绍自己在从 Angular 8 升级到 9 最终到 10 时遇到的问题及相应的解决方法,仅供参考。

首先,Angular 官方文档提供了详细的升级说明,建议在升级项目前首先通览相关文档,了解可能存在的问题。可以从下面的链接找到升级到 Angular 10 的最新信息。

https://angular.io/guide/updating-to-version-10

下面是自己在升级时实际操作的几个主要步骤:

  • 更新 Angular 8 相关依赖至最新版
    • ng update @angular/core@8 @angular/cli@8
    • ng build –prod –aot
    • 确认变更
  • 更新 @azure/msal-angular 至最新版 1.0.0
    • 安装 1.0.0 版并为新版的接口变化修改代码(1)
    • ng build –prod –aot
    • 确认变更
  • 升级至 Angular 9,检查并乎略一些依赖版本问题
    • ng update @angular/core@9 @angular/cli@9 –force(2)
    • ng build –prod –aot
    • 确认变更
    • 升级 Angular Material
      • ng update @angular/material@9
      • 检查 Angular Material 引用问题(3)
    • 增加 ngcc 作为 postinstall 脚本(4)
    • ng build –prod –aot
    • 确认变更
    • ng add @angular/localize (5)
    • ng serve –ssl 启动并检查应用基本功能
  • 升级至 Angular 10,检查并乎略一些依赖版本问题
    • ng update @angular/core @angular/cli –force
    • 临时手段解决 ngx-restangular 编译问题(6)
    • ng build –prod –aot
    • 确认变更
    • ng update @angular/material
    • ng build –prod –aot
    • 确认变更
    • ng serve –ssl 启动并检查应用基本功能
  • 大功告成

以下具体说明上述步骤中的一些关键操作。

(1)MSAL Angular 代码更改

在升级 Angular 9 之前,自己的项目使用 MSAL Angular 0.1.4 实现对 Microsoft 账户或 ADD 的验证。当时该库的文档和范例代码质量就差强人意,缺少说明和链接失效比比皆是。更重要的是,该库并不兼容 Angular Ivy,这也是没有更早升级 Angular 9 的原因之一。等着 MSAL Angular 花了几个月的时间终于正式发布 1.0.0 版本才开始升级,依然遇到几个问题:

  • 方法接口变化,getUser() 改为 getAccount(),displayableId 成员取消
  • 服务初始化方式变化,结构虽然较过去清晰,但文档和示例代码不足,甚至还有语法拼写错误
  • 请求验证成功后返回的 payload 格式发生变化,token 变量改为 rawIdToken
  • loginRedirect() 似乎不再正常工作,仍需进一步排查原因

无论如何,能够兼容 Angular 9 已经不易,总比 ngx-restangular 那样根本无法成功编译要好些。MSAL Angular 已经两个月没有新版本发布,不知今后的更新是否能解决一些问题。

(2)升级至 Angular 9

这步本身没有什么问题,不过需要检查一些报警的依赖版本冲突。自己的项目中主要是 tslib 已经到了 2.0 而 Angular 9 要求 1.10.0,以及 Angular http 停在了大版本7。确认没有问题后就以 –force 参数强制升级。

(3)检查 Angular Material 引用问题

新版本 Angular Material 要求每个引用都明确指定具体组件,如:

 import { MatSpinner } from '@angular/material/progress-spinner';

而不允许在一句引用中同时引入多个组件,如:

import { MatSpinner, MatSnackBar } from '@angular/material';

正常情况下 ng update 会自动更正代码,不过自己在操作时似乎遇到一些问题,没有顺利完成,只好收到批量做了一些处理。

(4)增加 ngcc 作为 postinstall 脚本

在 package.json 中增加 “postinstall”: “ngcc” 有助于避免可能的库兼容问题。参见 ngcc 的和 ModuleWithProviders 迁移的相关说明。

(5)增加 @angular/localize

如果项目中使用了 Angular 自带的 i18n 功能,可能需要执行 ng add @angular/localize 添加必要的依赖。不过这并非必要步骤,可以在遇到提示时才采取行动。

(6)解决 ngx-restangular 5.0.0 编译问题

很可惜,ngcc 不是银弹。至少,它无法解决 ngx-restangular 的兼容问题。

ngx-restangular 是自己项目中使用的主要第三方库之一,帮助使用 REST API 的使用。这个项目本身算不上太活跃,但基本功能还算充分。可惜的是,半年没有更新的它,果然无法在 Ivy 下正确编译,报错如下:

ERROR in node_modules/ngx-restangular/lib/ngx-restangular.module.d.ts:8:97 - error TS2314: Generic type 'ModuleWithProviders<T>' requires 1 type argument(s).

static forRoot(providers?: any[], configFunction?: (provider: any, ...arg: any[]) => void): ModuleWithProviders;

搜索尝试一些方法无果后,不得已手动修改库文件,增加类型 <RestangularModule> 在 node_modules/ngx-restangular/lib/ngx-restangular.module.d.ts 第八行末尾:

static forRoot(providers?: any[], configFunction?: (provider: any, ...arg: any[]) => void): ModuleWithProviders<RestangularModule>;

在 CI 管道中,则暂时每次都在 ng build 之前以 sed 命令完成代码更改:

sed -i 's/ModuleWithProviders;/ModuleWithProviders<RestangularModule>;/g' node_modules/ngx-restangular/lib/ngx-restangular.module.d.ts

于是,暂时绕过了编译错误。ngx-restangular 的功能本身倒似乎没有问题,可以正常工作。

经过功能确认和测试后,一个 Angular 8 应用终于顺利升级为了 Angular 10 应用。尽管并非一帆风顺,但 Ivy 确实明显提升了编译性能并减小了体积,总管没有白费功夫。如有需要,还可以参考 AngularGo GitHub 地址),了解升级过程中的配置与代码更改。

Angular on Azure Web App Service (IIS)

部署于 Azure App Service 的 Web App 事实上运行于 IIS (Internet Information Services),因此也可以通过 web.config 文件来配置它的行为。

由于 Angular 是一种 SPA Web 框架,需要对 IIS 做一些额外的 URL Rewrite 配置,才能在浏览器刷新页面时依然成功载入内容,参见以下代码。

规则 Index Rule 的作用是把所有匹配的 HTTP 请求全都重定向至 “/” 路径,之后 Angular 自己的路由(Routes)将会继续工作,完成内容的载入和跳转。可以参考此处了解如何实现 lazy loading 。

staticContent 节点则定义了允许的 mimeType,否则直接访问 contentType 为 application/json 等类型的请求将返回 404 Not Found。

AngularGo —— 一个开源 Angular SPA 模板

在接触 Angular 后,这些年工作业余也用 Angular 做了若干实际项目。不过,由于 Angular 在国内的流行度不高,各种原创内容和参考资料也相对较少。虽然网络上也能找到各种各样的技术文章,官方的文档也很全面,但总的来讲,信息还是有些分散。

很惭愧,几年里没有做什么特别有技术含量的工作。 只是提炼出一个很基础的模板,帮助开发者快速创建一个 SPA(Single Page Application)站点,或是供对 Android 感兴趣的读者了解 Android 的语法和基本的框架机制。该模板先分享于此 —— AngularGo on GitHub

该模板基于目前最新的 Angular 8,且会持续跟进更新。在 Angular CLI 自动创建的范例项目的基础上,目前版本的 AngularGo 还包含以下内容:

  • Angular 8 以及相关依赖的最新版本配置
  • 一种可能的源文件结构示例
  • 一种与 Cordova 共享代码库的可能方式
  • 基础 Angular 组件及依赖注入的使用范例
  • 基础 Angular Material 控件的使用范例
  • 支持桌面和移动设备的抽屉式侧滑菜单
  • 支持 lazy loading 的 app-routing 全局路由
  • 支持 authentication guard 的模块路由
  • 支持 bearer token 验证的 Restangular 初始化及 service 用例
  • 基于 HttpClient 的用户注册与登录 API 调用
  • 基于 scss 的 Angular Material 样式(尚未采用 BEM 命名规则)
  • Azure Application Insights 集成
  • 适用于 Windows server/ Azure App Service 的 web.config 配置

AngularGo 还很初步,很多细节因水平有限和时间限制写得也比较粗糙,想必会有其他优秀的开源模板提供了更好的实现。从某种意义上来讲,该模板一方面是对自己经验得一个整理,同时期望能起到一个抛砖引玉的作用。希望对读者有帮助,也欢迎批评指教。

Angular、.NET Web Api 验证(Authentication)与跨来源资源共享(Cors)

验证是任何 Web 服务都必须考虑的基础问题。关于 .NET 平台的验证与授权,按理说微软已经提供了相当充分的文档与示例代码。

.NET Identity 框架提供了现成的数据库结构以及一系列接口,可以实现用户注册、登录登出、口令生成、邮箱的确认、密码重置等基本功能,也可以与社交媒体账户集成实现用户注册与登陆。

需要注意的是,.NET MVC 与 .NET Core 所使用的接口代码和 .NET Web Api 有所不同,如果混合阅读文档与代码,可能会造成混淆与理解上的困扰。不同版本的文档对邮箱验证等功能的处理逻辑也有差别,不可简单合并使用。

后端若部署至 Azure 等云平台,口令的生成也可能会遇到在本地不会发生的问题,报错“ The data protection operation was unsuccessful. This may have been caused by not having the user profile loaded for the current thread’s user context, which may be the case when the thread is impersonating.”。例如,.NET MVC 版示例代码在用户登录并验证邮箱后生成口令时会试图调用 /Token 接口,触发错误。网上针对此问题有一些解决方案,却都不太有效。事实上,可暂时忽略对邮箱是否得到验证的检查,直接前台调用 /Token 接口,就能避免该错误。对邮箱的确认可以在之后实际执行处理时按需进行。

微软官方的示例代码都没有涉及 Cors(cross origin resource sharing) 问题,即使是 Angular 项目模板,由于前端默认与后端共存于同一个解决方案,也不存在这一问题。然而,如果前后端分离,前端 Angular 程序的域名很可能与后端不同。

微软针对 .NET Web Api 的 Cors 有一些文档,但很遗憾,如果有接口通过 bearer token 进行验证与授权,那些文档中的解决方案很可能会引发新的问题。

  • .NET Web Api 中对 允许跨源访问:介绍多种允许 Cross origin request 的方式

由于 Chrome 等一些浏览器在一定条件下出于安全原因为首先发送 OPTIONS method 的 HTTP 请求(如 headers 中指定 “Content-Type”: “application/x-www-form-urlencoded” 时),情况变得更加复杂。

如果 OPTIONS method 没有被允许,则会遇到 405 错误代码。不当的设置可能会在允许跨源访问资源时却阻碍了用户登录等验证 API。又或是无意中重复允许 Cors origin 设置,会提示 The ‘Access-Control-Allow-Origin’ header contains multiple values 错误而无法获得资源。

网络上简单明了且实际有效的解决方案并不好找,经过反复尝试与分析,要避免所有这类问题,采用全局设置恐怕是最简单的方法之一。

  • 不要像下面这样通过 Web.config 来设置 Cors

  • 不要像下面这样通过 ApiController 属性来设置 Cors

  • 不要在 WebApiConfig 等类中通过诸如 config.EnableCors(); 等代码方式设置 Cors
  • 安装 Microsoft.Owin.Cors 的 NuGet 包
  • 在 startup.cs 的 ConfigureAuth 方法开始时调用语句 app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
  • Angular 前端发送请求时, headers 中只需指定 “Content-Type”: “application/x-www-form-urlencoded” 一项即可
  • 如果使用 RestAngular 获取资源,只需按如下方式设置 Content-Type 与 Authorization 即可

至此,运行于 Azure App Service 的后端 .NET Web Api 将可以与非同源的 Angular 程序交互,实现本地用户账号的注册登陆等操作,可以通过 bearer token 实现资源请求过程中的验证与授权。

Angular 快速入门

之前做过一些 AngularJS 的小型项目,用的工具和流程却大多是过时的一套,大半精力也仍花费在后台业务逻辑。时至今日虽然晚了一些,打算跟上时代,尝试一下 Angular 的开发。

有几个关注点:

  • 以 Angular 4 入手,系统学习 Angular 框架的概念与机制
  • 完全的前后端分离
  • 使用 Angular CLI 创建模板
  • 借助 VSTS 管理项目代码与发布
  • Angular 部分完全使用 VS Code 开发

Angular 4 并不是最新版本,大版本升级的周期是每6个月,但从 Angular 2 起每个大版本都能基本兼容,Angular 4 的文档也相对丰富,故选择从这里开始。完全前后端分离可以更高效分工,带来一系列好处,如提高团队合作效率。而 Angular CLI 几乎已经成为默认的选项,这次也打算应用。至于 VSTS,最近的项目经历感觉没有最大程度发挥其价值,所以希望在也与项目中进一步探索其可能性。VS Code 虽已成为自己的常用工具,但能否完全担当起开发之职,也会是这次的一个课题。

起动步骤极为简单。

在安装 Node.js 后,通过命令行安装 Angular CLI:

CD 定位至希望的文件夹后,新建 Angular 项目:

运行新建的项目,并以 open 选项打开浏览器页面,默认端口为4200:

此时,通过 VS Code 对代码做任何修改并保存后都会立即体现在程序上。

至此,尝试 Angular 的第一步已经踏出。今后将不定期更新心得感想。