解决 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 地址),了解升级过程中的配置与代码更改。

Die Alltägliche

No. 17

这次的构想是居家办公途中听到呼喊而回头望向后方的瞬间

_

又是一年。于是今年也再度拿起画笔,趁着悬而未立,趁着多少还能确保一些能够自己支配的业余时间,再随着自己的性子去做些什么,留下些什么。画画有意思的地方在于可以随心所欲,暂时享受造物主的特权。

回想起来,上次把笔记本电脑作为道具画在画里竟然已经是九年之前。这次也算是回归初心。Surface Laptop 2 的宣传语是 A touch above ordinary,倒也恰好与标题契合。画的另一个小道具 Xperia 1 是最近一年的主力机,爱不释手。

过去一年是神奇的一年。非要说的话,或许是在过去很多年里,在今后很多年内,都很不寻常。为一些坏的情况做了准备,但最坏的情况最终竟也没有发生。对一些好的结果有所期望,但最好的结果似乎也不会实现。失望、希望、踌躇、前进。既然总有些事无法克服,那就只好尽量与之和平共处。不知不觉中,可能已经习惯了这样的状态。

迄今为止都在拼命地向前冲刺,告诉自己说,要做的更好,要变得更好。但怎样才算是更好,又怎样才能界定呢。是世俗意义的成功,还是想象中的自我实现?年初的时候还跟朋友聊到说,仍以为自己不是特别普通。实际又如何呢。也许,更多是一种相信,一份期待吧。对幻想中的美好。

嗯。加油。

要再厉害一点。

Du bist besonders.

_

_

_

One more thing…

Ver. Hoodie

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

最终一致性(eventual consistency)引

一致性设计在分布式系统中是一个重要问题。如果一个系统同时使用多个子数据系统来存储与读取数据,就必须设计满足功能需求的一致性定义。如果系统对不同数据子系统进行操作的结果不一致,不但可能会使用户困惑,更可能引发更严重的数据问题或系统错误。一致性有多种级别,适用于不同的业务场景。对于金融等对数据一致性要求较高的行业,传统的事务可以提供较高的一致性保证。对于分布式系统等对性能(performance)和可用性(availability)要求较高的场景,牺牲一定的强一致性来换取更好的用户体验也可接受。最终一致性的具体行为根据实际需求不同,并无严格而精确的定义。本文仅作为一个引子,介绍最终一致性及与其相关的一些基本概念,方便读者对最终一致性有初步的认识,以便进一步阅读其他文章与资料,设计适合自身业务需求的系统数据一致性。

衍生数据系统

衍生数据(derived data)指的是可以由另一些数据通过某种可以复现的处理来再次生成的数据。通常,衍生数据用于加速读取数据库中的某些数据。索引(index)和缓存(cache)都是典型的衍生数据。如果衍生数据丢失,仍可以通过原始数据再次生成。

单个数据库系统中的索引属于其所存储数据的衍生数据。有时,数据库的索引可能会由于性能或成本考量而被维护在另一个系统之中,此时,该索引系统即为一种衍生数据系统。该索引系统中的数据可以通过数据库中存储的数据重新生成(即使往往开销过大而不实际),反之则不行。这种情况下,如何维护源数据库数据与索引数据库系统之间数据的一致性,值得系统设计者考虑。

事务与 ACID

事务(transaction)是一种传统的确保数据一致性的方式。在一个系统中,各种类型的错误都可能会破坏数据的一致性。网络问题、硬件故障、远程服务不可用等原因都会导致数据操作部分或彻底失败。

事务可以将一组数据读写操作合并为一组,构成一个逻辑单元,作为一个单独的数据操作执行。一次事务操作要么完全成功(提交),要么彻底失败(中止并回滚)。如果事务失败,它能被安全地再次执行。

ACID 是原子性(atomicity)、一致性(consistency)、隔离性(isolation)与持久性(durability)的首字母缩写,它通常用来描述事务的性质。

原子性减轻了多步操作中途出错时造成的问题。如果一此事务失败,原子性将确保没有数据发生变化,事务可以被安全地重新提交。

一致性事实上不是数据库的一种属性,而由具体的应用需求定义。一个应用程序可以借助原子性与隔离性来实现一致性,而这可能不仅仅是单个数据库系统的问题。一致性有多种等级,在此不具体展开介绍。

隔离性与竞争(race condition)及并发(concurrency)有关,用于解决多个事务同时执行时的相互干涉。

持久性在数据库系统中指数据应当在事务提交成功后能安全而长久地被保存,且不会因系统故障而丢失。

事务可以在很大程度上确保数据质量。然而,最近流行的 NoSQL 数据库大多不提供事务支持,以免它过高的性能开销损害它们试图提供的高可用性。 从事务的角度来看,数据存储与索引是不同的数据库对象。 许多数据库系统,包括微软 Azure Cosmos DB 等分布式数据库都提供了单对象事务支持,但并未提供完全的多对象事务支持。

异构分布式事务

异构分布式事务( heterogeneous distributed transaction)指的是事务的参与者可能由两种或多种不同的技术组成。例如,由不同供应商提供的数据库,各司其职的数据库与索引系统,甚至是消息分发系统等非数据库系统。异构分布式事务也需要满足提交(commit)的原子性,这比实现数据库内部的事务更为困难。

两阶段提交(2PC,Two-Phase Commit)是一种著名的强一致性保证。对于一次事务请求,所有子系统都将确保数据被写入或最新的数据被读取。对于由一个索引系统和一个数据库系统构成的异构分布式系统,在完成一次两阶段提交后,索引系统中存储的数据必然能反映数据库系统的最新情况,对数据库系统的更新操作也总会反应在索引系统中。

重试策略

重试(retry)即再次执行某一操作。在设计数据系统时,重试策略也是一个需要考虑的问题。不恰当的重试可能会导致以下问题:

  • 如果重试请求的响应因网络问题而没能返回,再次重试可能会使开销倍增
  • 如果问题在于系统超负载,重试可能会导致问题进一步恶化
  • 重试只能解决暂时性问题(如死锁、隔离性被违反/isolation violation,或短暂的网络故障等),因此在重试前有必要判断重试是否可能解决问题

因此,在设计重试策略时,超时、最大重试次数、两次重试之间的间隔等都需要纳入考虑。

最终一致性与BASE

与事务提供的 ACID 属性不同,最终一致性提供的一致性保证也被称为 BASE(Basic Available, Soft state, Eventual consistency)。BASE 确保如果系统之后没有更多的数据操作,最终对系统的所有访问都将返回相同的结果。BASE 更多地是确保系统使用可用,但在系统数据收敛之前,它可能返回不同的结果。 最终一致性的实际行为由具体应用定义,无法统一阐述。

采用最终一致性的数据系统通常不要求数据操作失败时执行回滚(rollback)。用户或系统日志将得知操作失败,但在另一次成功的操作之前,数据的不一致问题并不会被自动修复。

之所以最终一致性会出现,很大程度上是因为如今的网路应用常常要处理规模巨大的数据与请求。此时,更强的一致性往往并不现实,高可用性反而是更系统更核心的设计目标。

_

以上是讨论最终一致性之前可能需要了解的一些前置概念的简述,以帮助读者快速了解系统数据一致性设计时的一些主题,并能有目标地查阅更多资料与文献。水平有限,如果纰漏,欢迎批评指正。

Weißer Ritter

第16弹,「Weißer Ritter」。

_

许久没有拿起画笔,终于借着周末得闲,踩着参加工作整六年的点,完成了新作品。 前一阵写了防御性驾驶心得,趁热打铁,这次的画便以车作为主题。和以往一样,图文无关。

某人说,猫多可爱,还不如画猫。于是决定配上黑猫。黑猫安静、深邃、外冷内热,最重要是黑不溜秋没有花纹比较方便,比之前美短简单不少。

我也喜欢猫。

_

人们喜欢纪念整数,即使那也不过是人生中平常的一天。 离开校园之后,自己的时间似乎就开始加速。日复一日,转瞬即逝。至今还清楚记得六年前的情景。虽然之前已有实习,但正式开始为期数十年的人生新阶段,总会引人遐想。如此不加筛选地记着各种各样的细节,也不知到底算是负担还是财富。所以,偶尔也羡慕那些很容易忘记烦恼的人。

回望过去的这些年,对自己的行动力颇为惊讶。一方面,自己做了若干不寻常的决定,直接改变了自己的人生轨迹。每一次都在之前无法想象,以至于现在的生活在几年前从未曾设想过。那些选择并不容易,过程也并非事事如意,但现在想来自己在每一次选择时都并没有经历过多的犹豫,直到现在也并没有后悔,往好的方面可说是未留遗憾。这便是好的。另一方面,一切又似乎都按部就班。每个月的计划、每一年的目标、五年一期的中期规划,尽管遭遇到了大大小小的失败,也总结反思出不少经验与教训,一路坚持,到最后竟也没有和最初的预期发生太大的偏差。甚至今天再读自己当年的初心,也并没有任何的违和感。前一阵被吐槽说想法始终过于 naive,但我并不讨厌这个评价。这个世界总是需要有些人在某些事上 naive ,不是么。相比过分老成,也许还是保持天真,看到的世界会更有意思。

_

漫无目的地写下些有的没的,尚不成篇,像是在回答自己,又算不上是答案。那些只是引子。手指敲击着键盘,心中已思绪万千。回忆已似潮水涌上心头,每一段都很有画面感。回想起过去每天经过的路,回想起生活了六年的地方,回想起曾经的自己。

有轨电车,我有些想念。

_

防御性驾驶心得

从考取驾照至今已有四年,在幸运地“抽取”到稀有而高价的牌照后开始日常驾车通勤生活,现在也是三年有余。 三年说长不长,说短不短。 一贯谨慎的做事风格,加上更重要的好运气,除了停在小区时被人追尾外,尚未发生道路交通事故。之前在申请公司车库停车许可时强制参加了防御性驾驶线上培训,回想起来,确实对降低事故风险有帮助。于是借此机会,结合自己的实际经验,聊聊防御性驾驶。

防御性驾驶(defensive driving)起源于汽车大国美国,大意是整理出一套驾驶行为的指导思想,让司机在驾车过程中尽可能多地做好准备,以应对突如其来的危险。防御性驾驶的相关内容在互联网上可以找到很多,这里不再赘述,更多地介绍自己在实践防御性驾驶三年来总结的心得。

了解车辆并做好维护保养

如今汽车越来越普及,驾驶汽车不再像过去那样是专业人员或具有较高技术素养的人的专利。遗憾的是,不善于操作机器、不愿意阅读说明的人越来越多地成为司机。对很多人来说,汽车是朝夕相处的交通工具,充分了解自己的汽车,不仅有助于最大程度上发挥汽车的性能,延长汽车的使用寿命,更可以确保自己的人身安全。

为了了解汽车,最有效的手段便是阅读由汽车厂商专人编写的使用手册,了解汽车的性能、功能、保养与应急措施。其中,最低限度是充分熟悉车辆各种指示灯的操作方式及使用场景,了解包括换挡、手刹、自动启停、定速巡航等运动功能的操作逻辑与效果。对于提供了应急报警功能的汽车,要了解其使用方式。以上这些不仅要知晓,还要实践,以加深记忆。

另外,应根据车辆保养手册建议的周期对车辆进行维护与保养,一方面确保能及早发现车辆硬件问题,一方面可以延长发动机等关键零部件的寿命。

熟悉交通规则并在严格遵守

能够考取驾照,理论上应该已经掌握了基本的道路交通规则。然而再一次遗憾的是,目前的现状是,在没有监控摄像的道路上,无视交通规则的司机占了绝大多数,是道路安全的极大隐患,也是防御性驾驶尤为重要的一个主要原因——如果其他人大多危险行车,能够保护自己安全的,就只有自己了。

在驾校学习并实践的交通规则通常无法涵盖实际情况,因此,在开始开车上路后,依然需要保持学习的心态,复习遗忘的知识,并主动学习新遇到的交通标识。

交通规则是一套需要所有道路参与者共同遵守的协议,它经过长期演化,出发点在于从理论上尽可能减少事故风险,提高道路利用效率。出于各种各样的实际原因,100% 遵守交通规则来讲并不现实,在有些情况下也可能不是最优选择。然而,就绝大多数日常驾驶场景来讲,遵守交通规则对减少事故有非常显著的正面效果,也是防御性驾驶的前提。不闯红灯;按照道路车速行驶,尽量与车流速度一致;如非必要,不(大幅度)超速;不违规变道;进入隧道后开启大灯等,既是交通法规的内容,也对安全驾驶有重要作用。

确保身体状态并始终充分掌握环境

大量的事故起于反应不及,而司机的身体状态对反应速度有关键影响。在任何情况下都不得酒后驾车,在服用嗜睡效果的药物,或缺乏睡眠的状态下,也应避免开车。有能力始终保持高度的注意力,也是防御性驾驶的根基。

每一个司机都应充分了解汽车提供的观察外部环境的途径,包括但不限于左右后视镜、内后视镜、提供声音警报的测距雷达,或是能够在屏幕上投影的盲区摄像头画面等。司机在驾驶过程中应尽可能多地利用所有这些设备,根据当时地车流密度与环境复杂度,以恰当的频率,持续观察车辆周围地环境,包括道路与障碍物,自身与相近车道上的其他机动车,以及道路周边的非机动车与行人等。此处的要点在于保持较高的观察频率,例如,根据情况,每隔数秒至数十秒确认两侧后视镜,以及在观察时尽量广地确保观察范围,而不仅仅集中于车辆四周。一位有经验司机,应当有能力通过观察,在脑中建立以自车为中心,后方数十上百米、前方数百米的概况,能在较早期就发现并意识到接近自车的物体,并随时留意路口、人行横道、学校医院厂区出入口等可能有其他车辆与行人闯入的高风险地带,尽早做出减速等相应的防御操作。在十字路口右转穿越人行横道但左侧停有大型车辆等有明显有障碍物遮挡视线的场景,应减速并通过改变观察角度,错开车辆A柱盲区,提前确认通行路线上没有行人或非机动车闯入。

这里有两个容易忽略的技巧。其一,利用车内后视镜观察车辆后方。以个人经验来讲,车内后视镜能够提供远优于左右后视镜的观察效率。首先,车内后视镜面积较大,距离人眼较近,司机甚至无需明显转动头部就能确认后视镜画面。其次,对于大部分车辆来说,车内后视镜可以透过车辆后挡风玻璃观察到包括当前车道在内的左右至少五车道的情况,且一定的变形效果可以方便司机确认后方车辆是否在加速或减速。充分利用内后视镜可以让司机较仅使用两侧后视镜提前掌握后方车辆的变道与超车意图并做好准备,显著降低因后方车辆急速超车而造成的危险。令人匪夷所思的是,如此实用的后视镜使用在驾校中几乎不会教授,大部分人对此一无所知,或是把内后视镜当作观察后排的工具,或是用杂物堆满后挡风玻璃,物理上限制内后视镜的功能。尽管内后视镜被严重忽略,它仍然是落实防御性驾驶的重要工具。其二,根据声音来判断两侧车辆。车厢静音水平虽说是越高越好,通常情况下,四周其他车辆的发动机噪音仍无法被完全隔绝。因此,可以将发动机噪音作为辅助渠道来判断左右两侧是否有车辆从后方靠近。在不方便观察后视镜,或者存在视觉盲区时,声音可以很好地辅助司机判断两侧是否有车辆正在加速,意图超车。

以上这些手段看似麻烦,增加了驾驶的复杂程度,却都能实实在在为司机争取到多半秒一秒的反应时间。很多时候,宝贵的零点几秒就能避免一次事故。

善用灯光提示其他车辆

在自己尽可能掌握其他车辆的行动后,下一步则是尽量让其他车辆也掌握自己的行动。按理说鸣笛也是一种有效手段,不过现状是道路上常常禁止鸣笛,住宅区内反而肆无忌惮,鸣笛的使用场景有限。灯光虽然也有其局限性,但通常距离也更远,合理使用,能够避免一些不必要的事故。

建议始终打开日间行车灯或示廓灯、在暴雨与大雾天气打开雾灯、在隧道中打开前光灯,这些都是告知周围自车位置的有效手段,能帮助对方提早做出判断。对于有自动大灯功能的汽车,建议始终使用自动大灯,以免忘记使用车灯。很多人过低地估计了日间行车灯与示廓灯的作用,实际上,对于视力一般,或是夜间视力一般的人,开启小功率的车灯可以非常有效地帮助他们识别,最终提升自己的行车安全。

在变道前,应提前两三秒打开转向灯。在转向开始后才使用转向灯和不使用转向灯无异,都是有严重事故隐患的危险行为。换个角度讲,给相邻车道司机增加两秒的准备时间,除非对方打算趁机加速通过,否则很难会让对方因反应不及而发生车祸。如果从后视镜发现对方开始加速,因立即推迟变道,因为这种行为极易引发事故。

在准备超车和正在加速即将越过侧面车辆时,可通过快速闪烁闪光灯提醒对方。事实上,如果大多数人可以做到变道前提前打开转向灯,这一步骤并非必须。然而,由于不打灯就变道的比例是如此之高,为了防御,就必须自己主动发出提醒,警告侧前方车辆不要突然变道,告知他们侧后方有车辆正在加速前进。除了使用灯光,还应观察前车是否有正在向车道边缘偏离等变道征兆,在超车时保持高度警惕。

预留提前量给自己和他人

对于上下高架匝道、变道、急刹车等行车状态变化,应尽可能早地做好准备。一方面,提前做好准备,有助于自己能更镇定地驾驶,不会因为慌乱而做出错误操作。一方面,结合灯光等方式,其他车辆也能更早地意识到情况变化,有反应的时间。

预留提前量不仅适用于驾驶途中的具体操作,也适用于整段旅程。对于上下班通勤,适当提前出门,预留充足的时间,可以避免因赶时间而仓促开车,增加事故风险。时间宽裕也会减弱超车动机。根据实际经验与观察,很多时候超车带来的收益远小于想象。简单的数学计算可知,对于长达数十公里的市内通勤,超车可以带来的时间节省往往不会超过十分钟;在拥堵的高架道路上反复左右变道超车的“高手”最后很可能也就比原先提前三五个车位而已。

在条件允许的情况下,适当提前或延后旅程,避开每天有规律的道路拥挤时段,可以有效降低事故风险。一方面,更少的车辆总数和更低的车流密度直接减少事故发生的可能性,车辆间距增大,反应时间增加;另一方面,危险性车的驾驶员通常也会随着路面上车辆的减少而成比例减少,单一危险驾驶行为本身的风险也会被进一步稀释。另一方面,更通畅的道路对提升驾驶心情,甚至降低油耗都有帮助。

保持心态平和并经常反思

经常驾车,难免遇到各类马路杀手或野蛮驾驶行为。保持心态平和,不与其他司机怄气,也是减少事故隐患的重要一点。人在冲动时往往会忽视周围环境,在驾驶时这是大忌。相互挑衅还可能造成冲突升级,诱发对方做出过激行为,得不偿失。

另外,人非圣贤,在驾驶过程中总会偶尔有做出危险动作的情况发生。即使没有被监控摄像拍到,甚至并没有违法,也应对问题进行反思,理解做出危险行为的原因和可能的风险,并提醒自己今后注意,这样才能避免驾驶风格随着驾龄增加而越来越粗犷,事故风险越来越高。

以上便是自己对于防御性驾驶的一点心得。一家之言,也尚未可以自称老手,仅供参考。愿事故越来越少,驾驶越来越轻松,而不再是负担。

——写于自动驾驶尚未普及之前