0%

先说结论:测试从失败风险开始,不从框架开始

资深工程师设计测试时,先问“哪些失败最昂贵、在哪一层能最低成本且稳定地发现”,再选择 Vitest、Jest、Testing Library、Playwright 或其他工具。测试金字塔、测试奖杯和覆盖率都是辅助模型,不能代替项目风险判断。

测试的核心是 test oracle(测试预言):系统出现什么可观察结果才算正确。AI 可以生成大量测试代码,却无法从缺失的需求中自动发明可信的权限规则、报警可靠性、命令状态语义或设备安全行为。如果 oracle 错了,测试执行得再快、覆盖率再高,也只是在稳定验证错误假设。

Vibe Coding 更适合把测试中的机械工作自动化:生成 fixture、组合边界值、搭建 MSW handler、录制 E2E 步骤、整理失败和寻找未覆盖分支。工程师仍然负责失败风险、oracle、环境边界、发布门槛和测试维护成本。

三层掌握标准

层级 必须掌握或会用 验证方式 AI 的位置
必须闭卷掌握 可观察行为、test oracle、测试边界、隔离与替身、异步结果、稳定性和失败定位 面对一个需求能画出风险表,并说明为何选择单元、组件、集成或 E2E 不能替代业务和安全语义
会使用工具验证 TypeScript/ESLint、Vitest/Jest、Testing Library、MSW/adapter、Playwright/Cypress、CI 报告 测试失败能指出具体规则,能保留 trace、日志、截图或协议差异 可执行和汇总,结果需人工复核
可由 AI 初步完成 用例与 fixture 初稿、边界数据、E2E 脚本、失败聚类、覆盖缺口和维护性建议 审查断言、selector、数据来源、Mock 边界和失败价值 加速生成与分类,不决定是否允许发布

不依赖 AI 时,至少要能独立写出一个纯逻辑测试、一个组件行为测试,并读懂一条浏览器 E2E 的失败证据。否则很难审查智能体生成的测试是否真正降低风险。

从风险映射到最低成本的测试层级

风险或示例 最低有效层级 候选工具 为什么不能只靠 E2E
坐标转换、格式化、校验、设备状态机 单元/属性或参数化测试 Vitest、Jest 浏览器链路反馈慢,难穷举边界和非法迁移
按钮、表单、错误提示和键盘操作 组件行为测试 Vue/React Testing Library E2E 定位成本高,无法快速覆盖组件状态组合
API 错误、超时和数据协议 组件集成或 adapter 测试 MSW、fake adapter、真实 schema 样本 只在完整环境触发故障不稳定,难控制精确响应
登录、关键任务创建、审批与结果查询 少量关键 E2E Playwright、Cypress E2E 必要但昂贵;底层分支仍应由更快测试覆盖
前后端字段和事件版本不兼容 契约测试 schema/OpenAPI/事件契约工具 UI 成功路径可能碰巧没有触发不兼容字段
CSS、主题、图表或三维视图异常 视觉回归 + 关键语义断言 Playwright/Cypress 截图、视觉平台 像素差异有噪声,且截图不能证明交互和数据正确
键盘、语义和读屏退化 组件/E2E 无障碍检查 + 人工检查 Testing Library、axe、浏览器工具 自动规则只能发现部分问题,不能代替真实辅助技术体验
资源、交互、内存或 Web3D 性能回归 可重复性能场景 Lighthouse、Playwright、Performance API、自定义采样 常规功能 E2E 通过不表示性能预算达标

“越接近用户越好”与“单元测试越多越好”都过于绝对。合理分布取决于架构、失败成本、依赖数量、反馈速度、环境控制能力和 flaky 风险。同一条关键工作流通常需要少量 E2E 证明连接成立,再用更低层测试覆盖状态和错误组合。

常用工具怎样选择

工具或层级 适合解决 选型边界
TypeScript、ESLint 类型关系、未处理分支、危险模式和一致性 不能验证运行时 JSON、浏览器行为和业务结果
Vitest Vite 项目的单元/组件测试,复用 Vite 解析与配置,兼容许多 Jest 风格 API 不因为 API 熟悉就假设所有 Jest 插件和行为完全等价
Jest 已有成熟 Jest 生态、非 Vite 项目或迁移成本高的代码库 为追新工具迁移不必然产生业务价值;先看速度、维护和插件依赖
Testing Library 从角色、名称、文本和用户交互验证 Vue/React 组件的可观察行为 不是完整浏览器 E2E;仍需理解框架调度、DOM 环境和异步边界
MSW 或受控 adapter 在网络/SDK 边界提供真实协议形状、错误、超时和状态组合 handler 仍可能偏离后端,需要契约样本或集成测试校准
Playwright 多浏览器上下文、隔离、并行、trace/video/screenshot 和现代 E2E 测试数量、真实依赖和环境矩阵会提高时间与维护成本
Cypress 交互式调试、时间旅行体验和既有团队生态 选型应看现有基础、目标浏览器、运行模型和团队排障效率
契约/视觉/无障碍/性能测试 处理特定高成本回归 只有对应风险和负责人存在时才值得加入,不应为了工具齐全而堆叠

Vitest 与 Jest、Playwright 与 Cypress 都不是“新工具必然替代旧工具”的关系。新项目可以按构建栈和团队经验选择;既有项目应先量化当前痛点、迁移成本和收益。Jasmine、Karma 等历史笔记仍可用于维护旧项目,但不再作为这篇总入口的默认工具链。

什么值得单元测试、组件测试和 E2E 测试

单元测试:压缩规则和边界的反馈时间

纯函数、数据转换、校验、排序、权限判断、状态机和重试策略适合单元测试。它们输入输出明确,能够快速覆盖边界值和非法状态。若一个规则只能通过启动浏览器和后端才能测试,通常说明领域逻辑与框架或 I/O 绑得太紧。

单元测试不要求“一个类就是一个单元”。更实用的边界是一项稳定行为。例如设备状态机从 offline 不能直接进入 running,测试应断言迁移结果,而不是私有方法的调用次数。

组件测试:验证用户可观察行为

组件测试关注用户能看到和操作的结果:按钮是否可用、错误是否可理解、表单是否保留输入、键盘能否操作、加载与空状态是否正确。优先使用 role、label、可见文本等语义 selector;确实没有合适语义时再使用稳定的 data-testid

Vue 和 React 的调度方式不同,但 Testing Library 的原则相同:像用户一样查询和交互,等待可观察结果,不通过 wrapper.vm、组件私有状态或实现函数名定义 oracle。

集成测试:验证拥有的边界

前端通常需要验证 API schema、WebSocket 事件、缓存、路由和状态容器怎样连接。使用 MSW 或受控 adapter 可以准确构造成功、错误、超时、重复和乱序响应,同时避免 Mock fetch、Axios 私有实现或 SDK 内部调用。

Mock server 仍可能与真实后端漂移。关键协议应由 OpenAPI/schema、生产样本脱敏回放、契约测试或受控环境集成校准。

E2E:证明关键链路在真实浏览器中连接成立

E2E 适合登录、关键任务创建、审批、命令提交与状态反馈等少量高价值流程。它覆盖浏览器、路由、网络、存储和渲染的真实组合,但反馈慢、环境依赖多、失败定位成本高,不适合穷举所有状态机分支。

稳定 E2E 需要确定数据、独立账号/空间、可重复清理、语义 selector 和明确等待条件。不要使用任意 sleep(3000) 猜测系统何时完成。

Snapshot 只在意图清晰时使用

Snapshot 适合稳定的序列化结构、协议输出或需要审阅的有限 UI 片段。大范围组件 HTML snapshot 往往把大量无关变化混在一起,审核者容易直接更新文件而不理解行为。关键规则应使用有名字的显式断言。

Mock、异步与 flaky test

Mock 保留在自己拥有的接口

  • 为时间、随机数、网络、存储和设备 SDK 定义小接口,在边界替换实现。
  • 优先模拟外部行为和协议结果,不逐层 Mock 每个内部函数。
  • Mock 返回值必须符合真实 schema,并定期用契约或集成测试校准。
  • 如果修改一次内部重构会破坏大量测试,而用户行为没变,测试大概率耦合了实现。

异步测试等待结果,不等待猜测的时间

Promise、框架更新、网络响应和 timer 是不同队列。等待页面出现 role="alert"、按钮解除禁用或任务状态变为完成,比等待固定毫秒更稳定。只有“时间本身”是依赖时才使用 fake timer,例如退避、超时或倒计时;使用后还要恢复真实 timer,避免污染其他用例。

对于无法取消的请求、组件卸载后的回调和快速切换 ID 的竞态,要设计能控制先后顺序的测试。测试不仅断言最终页面,还要证明旧响应不会覆盖新状态。

flaky test 是待诊断缺陷,不是加重试即可

先把不稳定失败分类:

  1. 测试缺陷:任意 sleep、共享数据、选择器脆弱、清理不完整。
  2. 产品竞态:真实异步顺序、重复事件、状态所有权或取消逻辑有问题。
  3. 环境不稳定:浏览器、服务、网络、资源或执行机负载不受控。
  4. 真实间歇缺陷:只有特定时序或数据组合才出现的产品问题。

自动重试可以收集更多证据,不能把红灯洗成绿灯。临时 quarantine 必须写明负责人、原因、失败证据和解除条件;长期隔离但无人处理,等于删除发布保护。

工业与机器人前端需要额外测试什么

风险 需要验证的行为 建议证据
设备/任务状态迁移 非法迁移拒绝,断连后状态不伪装成运行中 纯状态机用例、事件日志
权限与二次确认 无权限用户不可下发,危险操作需要正确确认 组件/集成/E2E,服务端授权仍须独立验证
WebSocket 乱序、重复和断连 旧状态不覆盖新状态,重复消息幂等,重连后能核对状态 MSW/adapter、录制事件回放
报警与审计 高频遥测采样时报警不被覆盖或静默丢失 有界缓冲测试、计数与回放
外部数据 payload 作为 unknown 校验版本、类型、范围和设备 ID schema 样本、无效数据测试
命令状态 区分已接收、已批准、已下发、执行中、成功、失败和未知 契约测试、状态机、浏览器 trace
WebGL 画面 canvas 非空、关键相机/选择交互正确、资源卸载后不持续增长 screenshot + canvas 像素检查、renderer.info、生命周期测试
性能 长会话队列和内存有上界,关键交互满足项目预算 固定数据回放、Performance trace、回归报告

视觉回归只能说明像素发生变化,不能证明三维对象语义、坐标或设备状态正确。对 WebGL 应组合截图、canvas 像素非空检查、相机/拾取行为和领域数据断言,并固定 GPU/浏览器环境以控制噪声。

普通浏览器测试默认连接 fake、模拟器或只读环境。未经明确环境门控、权限、确定性安全控制和人工批准流程,E2E 不得触发真实设备运动。浏览器自动化不是机器人安全系统。

Vibe Coding 时代怎样让 AI 参与测试

AI 可以起草或自动化 工程师必须确认 输出证据
根据需求草拟风险表和边界值 需求是否完整,哪些失败影响发布或安全 风险、来源、优先级和负责人
生成 fixture、参数化数据、MSW handler 数据隐私、schema 真实性和极端值含义 fixture 来源、契约版本和失败样本
生成组件/E2E 脚本和 selector 建议 test oracle、关键旅程、语义 selector 和环境边界 可运行测试、trace、截图和断言说明
检查缺少断言、重复 setup 和实现耦合 修改是否保留业务意图,是否误删必要隔离 审查 diff 和前后失败行为
聚类 CI 失败、重跑和摘要 flaky 模式 测试缺陷、产品竞态、环境问题还是间歇缺陷 原始日志、trace、出现频率和分类理由

给 AI 的输入至少包括业务规则、失败后果、现有边界、禁止副作用、环境和验收条件。只提供组件源码并要求“补齐全部测试”,得到的往往是覆盖率友好、oracle 薄弱的实现细节测试。

智能体适合持续执行固定套件、收集 trace/video、比较失败和提出缺口;工程师需要抽查它是否通过放宽断言、增加重试、修改 fixture 或跳过用例来获得绿色结果。

AI 生成测试审查案例

以下测试可能迅速提高行覆盖率,却没有表达用户或业务风险:

1
2
3
4
5
6
it('works', () => {
const wrapper = shallowMount(DevicePanel)
expect(wrapper.exists()).toBe(true)
expect(wrapper.html()).toMatchSnapshot()
expect(wrapper.vm.refresh).toBeDefined()
})

至少存在四个问题:

  • exists() 只证明组件挂载,没有证明任何产品行为。
  • 大 snapshot 没有指出哪些变化是错误,容易被机械更新。
  • wrapper.vm.refresh 耦合私有实现,重构会失败,真实行为损坏却可能通过。
  • 没有加载、错误、超时、权限或重试路径,也没有清晰 test oracle。

更有价值的测试从产品行为出发。下面使用 Vue Testing Library 与 MSW 风格 API;imports、server 生命周期和 matcher 配置需要按项目补齐:

1
2
3
4
5
6
7
it('shows a retry action when loading the device fails', async () => {
server.use(http.get('/api/devices/robot-1', () => HttpResponse.error()))
render(DevicePanel, { props: { deviceId: 'robot-1' } })

expect(await screen.findByRole('alert')).toHaveTextContent('设备状态加载失败')
expect(screen.getByRole('button', { name: '重试' })).toBeEnabled()
})

这段示例仍不是完整测试,但 oracle 已经明确:加载失败时,用户看到可理解的报警,并能执行重试。React Testing Library 可以使用同样的 role-based 查询和行为断言;价值来自产品规则,不来自框架语法。

下一步应继续验证点击重试后的请求和状态、重复点击策略、权限、超时与错误日志。AI 可以补初稿,但“失败时是否允许重试、重试是否有副作用”必须来自产品和系统约束。

CI 中怎样分层执行

阶段 建议内容 目标
本地 / pre-commit formatter、lint、受影响的单元与组件测试 数秒到数分钟内发现低成本错误,不阻塞开发流
Pull Request 完整静态检查、单元/组件/集成、少量浏览器 smoke 保护主要规则和关键连接,保留可定位报告
受保护分支 / Nightly 更广浏览器矩阵、视觉/无障碍/性能、长时间与故障场景 承担耗时、噪声更高但有价值的回归
Release / 受控环境 契约、迁移、模拟器或硬件门控流程 验证发布组合和高风险外部依赖

这不是必须照抄的流水线。小型库、普通内容站点和机器人调试平台的风险完全不同。CI 分层需要平衡反馈时间、执行成本、失败定位、环境稀缺性和发布后果。

每个失败门槛都应有负责人和诊断产物。只输出“E2E failed”而没有 trace、网络日志、截图和版本信息,会把自动化节省的时间重新花在猜测上。

面试准备:讲风险和证据,不背 matcher

资深测试题可以统一使用:风险 -> 测试层级 -> oracle -> 隔离方式 -> 失败证据 -> 维护取舍

面试问题 回答重点 不完整回答
如何测试一个复杂组件 列出用户行为和错误状态,说明哪些纯逻辑先抽离,网络怎样控制,保留哪些浏览器验证 “用 Jest 测方法、用 Cypress 测页面”
单元、集成和 E2E 怎样分配 按失败成本、依赖、反馈速度和 flaky 风险选择最低有效层级 “遵循 70/20/10”
Mock 何时有害 偏离真实协议、耦合实现、把集成风险隐藏为绿色 “单测必须隔离全部依赖”
如何治理 flaky test 分类原因、保存 trace、修复竞态/数据/环境,隔离有负责人和期限 “失败就重跑三次”
覆盖率有什么价值和限制 帮助发现未执行代码,不能证明 oracle、边界和断言质量 “达到 80% 就能发布”
怎样审查 AI 生成测试 检查风险来源、oracle、selector、Mock、失败是否有意义、是否通过放宽断言变绿 “AI 能自动补全测试”

记住常用 matcher 和框架生命周期足以读写代码,不必把完整 API 当作面试主线。真正的追问通常是测试为何稳定、为何能发现目标故障、失败后怎样定位,以及维护成本是否匹配风险。

实践任务:关键工作流测试治理

选择设备状态页、视觉任务页或机器人调试流程中的一条关键旅程。

交付物

  • 一份包含失败后果、概率、检测层级和优先级的工作流风险表。
  • 设备/任务状态机的纯逻辑测试,以及一个 Vue 或 React 组件行为测试。
  • 一条 Playwright 或 Cypress 关键路径,保留失败 trace、截图和网络证据。
  • 断连、超时、重复/乱序消息用例,普通执行只连接 fake 或模拟器。
  • CI 分层方案、一次 flaky test 复盘,以及 AI 初稿与人工审查 diff。

验收标准

  • 每个重要失败能指出被破坏的业务规则,而不只是 DOM 或私有方法变化。
  • 测试之间数据隔离并能重复运行,没有任意固定 sleep。
  • E2E 失败保留 trace 和环境版本,能够区分产品、测试和环境问题。
  • 覆盖率只作为辅助信号,不以降低断言或排除关键代码换取数字。
  • 涉及真实设备的动作默认关闭,并有独立受控流程。

面试价值

证明能够按风险设计单元、组件、集成和 E2E 边界,治理不稳定测试,并监督 AI 生成物,而不只是熟悉某个测试框架 API。

性能回归的指标、实验和 trace 解释见 前端性能诊断与优化

暂时不死磕什么

  • 不背 Vitest/Jest/Jasmine 的所有 matcher、配置项和 CLI 参数。
  • 不追求 100% 覆盖率,也不为提高数字测试没有风险的 getter 和模板细节。
  • 不为每个分支创建 E2E;组合爆炸应优先在更低层覆盖。
  • 不给所有组件生成大 snapshot,并在失败时无脑更新。
  • 不为了工具热度迁移稳定套件,先量化反馈速度、维护成本和缺口。
  • 不断言私有方法、框架内部状态和调用次数,除非它们本身就是公开契约。
  • 不把普通浏览器自动化扩展成未经门控的真实设备控制。

自检清单

  • [ ] 能从失败风险选择最低有效测试层级,而不是先报框架名。
  • [ ] 能定义清晰 test oracle,并区分用户行为与实现细节。
  • [ ] 能说明 Vitest/Jest、Playwright/Cypress 的条件化选型,而不是宣布唯一赢家。
  • [ ] 能用语义 selector、可观察异步结果和受控 adapter 编写稳定测试。
  • [ ] 能区分测试缺陷、产品竞态、环境问题和真实间歇缺陷。
  • [ ] 能审查 AI 是否生成了空洞断言、失真的 Mock、任意 sleep 或放宽门槛。
  • [ ] 能展示工业消息乱序、报警保留、命令状态或 WebGL 中至少一项领域风险测试。
  • [ ] 能拿出 CI 失败证据、flaky 复盘和 AI 初稿到修正版的差异。

参考资料

官方文档用于查询 API,项目风险表、失败 trace、真实契约和复盘用于决定测什么。两者不能互相替代。

XMLHttpRequest

XMLHttpRequest 可以在不刷新页面的情况下请求特定 URL,获取数据。尽管名称如此,XMLHttpRequest 可以用于获取任何类型的数据,而不仅仅是 XML。它甚至支持 HTTP 以外的协议(包括 file:// 和 FTP),尽管可能受到更多出于安全等原因的限制。

1
2
3
4
5
6
7
8
9
function reqListener () {
console.log(this.responseText);
}

var oReq = new XMLHttpRequest();
oReq.addEventListener("load", reqListener);
oReq.open("GET", "http://www.example.org/getsomething");
oReq.responseType = "json";
oReq.send();

发送
1
2
3
var xhr = new XMLHttpRequest;
xhr.open("POST", url, false);
xhr.send(data);

ResizeObserver

示例 ResizeObserver
调用构造方法返回一个ResizeObserver对象,传入回调函数(entries)=>{},即当监听到目标width height改变时,执行该函数,参数entries为ResizeObserverEntry接口的集合,可以访问真正在观察的 Element 或 SVGElement 最新的大小

1
2
3
4
5
6
7
8
9
10
11
12
const observer = new ResizeObserver(entries=>{
for (let entry of entries) {
if(entry.contentBoxSize) { // 内容盒尺寸
h1Elem.style.fontSize = `${Math.max(1.5, entry.contentBoxSize.inlineSize / 200)}rem`; // 前文某标题const h1Elem = document.querySelector('h1');
pElem.style.fontSize = `${Math.max(1, entry.contentBoxSize.inlineSize / 600)}rem`; // 前文某段文字const pElem = document.querySelector('p');
} else {
h1Elem.style.fontSize = `${Math.max(1.5, entry.contentRect.width / 200)}rem`;
pElem.style.fontSize = `${Math.max(1, entry.contentRect.width / 600)}rem`;
}
}
})

localStorage

1
2
3
localStorage.setItem('qqsCustomizedList', '['id','name']')
localStorage.getItem('qqsCustomizedList')
localStorage.clear()

关于内容盒和边框盒(存目)

课外:关于传统web app和SPA的选择

将单页面应用植入到ASP.Net

方式一:项目模板和spa中间件:

1
2
3
4
5
% 安装spa template pack
dotnet new -i Microsoft.DotNet.Web.Spa.ProjectTemplates
% 使用模板创建项目
dotnet new --list
dotnet new angular -n MyDotnetNgProject

angular is short for template”ASP.NET Core with Angular”, -n 指定项目名称,键入dotnet new —help查看选项参数说明。
项目大致结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
│   appsettings.json
│ MyDotNetNgProject.csproj
│ Program.cs
│ Startup.cs
├───ClientApp
│ │ angular.json
│ │ package.json
│ │ tsconfig.json
│ │ tslint.json
│ ├───e2e
│ │ └───src
│ └───src
│ ├───app
│ │ ├───counter
│ │ ├───fetch-data
│ │ ├───home
│ │ └───nav-menu
│ ├───assets
│ └───environments
├───Controllers
├───obj
├───Pages
├───Properties
│ launchSettings.json
└───wwwroot

整个前端项目以ClientApp目录为根目录,在ASP的Startup.Configure中调用spa中间件,如下
1
2
3
4
5
6
7
8
9
10
11
app.UseSpa(spa => {
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501

spa.Options.SourcePath = "ClientApp";

if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});

方式二:前端发布到wwwroot,使用静态文件和默认页

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseDefaultFiles();
app.UseStaticFiles();

app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}

UseDefaultFiles的搜索规则是
1
2
3
4
default.htm
default.html
index.htm
index.html

另外可以使用DefaultFilesOptions添加自定义默认页,详见 ASP.NET Core 中的静态文件

Trouble Shooting

Issue: The Angular CLI process did not start listening for requests within the timeout period of 0 seconds.

当前的angular模板是基于Angular 8.2.12(记于2020.8.28),在迁移ng9,ng10项目时出现上述问题。

issue#16961

较深入的分析issue#18062

workaround: edit start script

1
"start": "echo start && ng serve  --host 0.0.0.0",

路由问题

拓展

ASP.NET Core 上的单页面应用的身份验证简介

1
dotnet new angular -o AspNgIndividualAuthProj -au Individual

Caution! 模板生成同时提供认证授权功能的应用,具体是使用一个叫IdentityServer的框架,IdentityServer封装了提供重定向endpint,生成JWT令牌,校验令牌等功能。 通常情况下,后台服务应对接独立的SSO,下面的大部分内容对此的可参考性很有限

MSAL + Angular + .Net Core + AD B2C

Microsoft Authentication Library(微软身份认证库MSAL)

微软栗子真多—> Azure-Sample

1
npm i msal @azure/msal-angular -save-dev

引入MSAL模块 @app.module
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
...
import { Configuration } from 'msal';
import {
MsalModule,
MsalInterceptor,
MSAL_CONFIG,
MSAL_CONFIG_ANGULAR,
MsalService,
MsalAngularConfiguration
} from '@azure/msal-angular';

import { msalConfig, msalAngularConfig } from './app-config';
import { AppRoutingModule } from './app-routing.module';
import { ProfileComponent } from './profile/profile.component';

function MSALConfigFactory(): Configuration {
return msalConfig;
}

function MSALAngularConfigFactory(): MsalAngularConfiguration {
return msalAngularConfig;
}

@NgModule({
declarations: [
AppComponent,
HomeComponent,
ProfileComponent,
],
imports: [
...
MsalModule
],
providers: [
{
provide: HTTP_INTERCEPTORS,
useClass: MsalInterceptor,
multi: true
},
{
provide: MSAL_CONFIG,
useFactory: MSALConfigFactory
},
{
provide: MSAL_CONFIG_ANGULAR,
useFactory: MSALAngularConfigFactory
},
MsalService
],
bootstrap: [AppComponent]
})
export class AppModule { }

另 使用oidc-client的栗子参考oidc-client

关于.csproj

参考:理解 C# 项目 csproj 文件格式的本质和编译流程

  • PropertyGroup, 声明编译过程中用到的变量,如一些路径,所谓的group,为了增强可读性,而将一组变量放在一个PropertyGroup中,其他的再放一个PropertyGroup
    1
    2
    3
    4
    5
    <PropertyGroup>
    ...
    <SpaRoot>ClientApp\</SpaRoot>
    </PropertyGroup>

  • ItemGroup, 顾名思义,存放集合的项,一个group中各项属性名相同,可以认为是类型为XX(即属性名)的一个集合,下例是第三方的package,另外也可以放其他模块所需的任意内容,用相应的属性标识出
    1
    2
    3
    4
    5
    6
    <ItemGroup>
    <PackageReference Include="EntityFrameworkCore.SqlServer.HierarchyId" Version="1.1.1" />
    <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.2" />
    <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.1.7" />
    <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="3.1.7" />
    </ItemGroup>
  • Target
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
      <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">
    <!-- Ensure Node.js is installed -->
    <Exec Command="node --version" ContinueOnError="true">
    <Output TaskParameter="ExitCode" PropertyName="ErrorCode" />
    </Exec>
    <Error Condition="'$(ErrorCode)' != '0'" Text="Node.js is required to build and run this project. To continue, please install Node.js from https://nodejs.org/, and then restart your command prompt or IDE." />
    <Message Importance="high" Text="Restoring dependencies using 'npm'. This may take several minutes..." />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    </Target>

    <Target Name="PublishRunWebpack" AfterTargets="ComputeFilesToPublish">
    <!-- As part of publishing, ensure the JS resources are freshly built in production mode -->
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm install" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build -- --prod" />
    <Exec WorkingDirectory="$(SpaRoot)" Command="npm run build:ssr -- --prod" Condition=" '$(BuildServerSideRenderer)' == 'true' " />

    <!-- Include the newly-built files in the publish output -->
    <ItemGroup>
    <DistFiles Include="$(SpaRoot)dist\**; $(SpaRoot)dist-server\**" />
    <DistFiles Include="$(SpaRoot)node_modules\**" Condition="'$(BuildServerSideRenderer)' == 'true'" />
    <ResolvedFileToPublish Include="@(DistFiles->'%(FullPath)')" Exclude="@(ResolvedFileToPublish)">
    <RelativePath>%(DistFiles.Identity)</RelativePath>
    <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
    <ExcludeFromSingleFile>true</ExcludeFromSingleFile>
    </ResolvedFileToPublish>
    </ItemGroup>
    </Target>
    上例是ASP.NET Core with Angular模板项目使用的build target,两种编译环境,而且其中还有条件语句,有点厉害

领域驱动设计(Domain-Driven Design, DDD), 对于复杂的系统的开发, 从需求梳理到编码实现,将涉及的专业知识转换成模型、逻辑是必不可少的环节,这些信息和知识会汇集到若干核心的“领域”,接下来的设计重心会落在核心领域范畴内,确定域界地沟通方式,迭代地在域内完成实现,而不似单纯地服务端的功能堆叠。

应该首先从官方网站获取知识

webassembly知识储备或许会涵盖“编译原理”,Rust,v8开发,

WebAssembly或称wasm是一个实验性的低端编程语言,应用于浏览器内的客户端。WebAssembly是便携式的抽象语法树,被设计来提供比JavaScript更快速的编译及运行。WebAssembly将让开发者能运用自己熟悉的编程语言编译,再藉虚拟机引擎在浏览器内运行。 —-维基百科

背景参考《一个白学家眼里的 WebAssembly》

曾几何时,有“一切可以由js实现的,终将用js实现”,而webassembly技术为编译型语言(c/c++,jave,c#等)抢夺浏览器战场打开了传送门。

优势:

为什么wasm效率比js高?因为wasm是二进制指令格式,比执行js代码天然地节省解释和JIT的开销,但效率提高多少要看计算的复杂程度,简单场景或不相上下

课外:为了提高浏览器性能,曾出现过从 NaCl、PNaCl 到 ASM.js,这些技术作为wasm的前辈,有以下特点————(于航《WebAssembly入门》)

  1. 源码中都使用了类型明确的变量;
  2. 应用都拥有独立的运行时环境,并且与原有的 JavaScript 运行时环境分离;
  3. 支持将原有的 C/C++ 应用通过某种方式转换到基于这些技术的实现,并可以直接运行在 Web 浏览器中。

现状是,四大厂(Mozilla,Google,Microsoft,Apple)共同倾力开发, WebAssembly 技术已成为 W3C 的标准, 其MVP版本(Minimum Viable Product)被主流浏览器支持

Webassembly MDN

工具链Emscripten,Rust, AssemblyScript

In case of conflict, consider users over authors over implementors over specifiers over theoretical purity.

helloworld.wasm

准备一个新的开发环境:
启动一个ubuntu的docker

1
2
docker pull ubuntu
sudo docker run -it -u root --name labdocker -v labdocker_home:/var/labdocker_home ubuntu bash

上面的语句映射的是docker中的/var/labdocker_home和宿主的/var/lib/docker/volumes/labdocker_home/_data wtf???
给‘空’的ubuntu安装必要工具

1
2
apt-get update
apt-get install -y nodejs npm git

编译工具链依赖

1
apt-get install -y cmake python3.8

安装emsdk

1
2
3
4
5
6
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
git pull
./emsdk install latest // 这一步依赖python
./emsdk activate latest
source ./emsdk_env.sh // 定义环境变量

You always have to source ./emsdk_env.sh first in a new terminal session

输入emcc -v查看信息
emcc
看起来比较正常,说明工具安装成功

emsdk setup

另,其实安装emsdk不是必须的,docker hub中有现成的

1
2
3
docker pull emscripten/emsdk
docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) \
emscripten/emsdk emcc helloworld.cpp -o helloworld.js

创建一个c项目

1
2
3
mkdir /home/Workspace/hello
cd /home/Workspace/hello
echo '' > hello.c

回忆起c的hello world

1
2
3
4
#include<stdio.h>
int main(int argc, char** argv){
printf("hello hello~/n");
}

走你

1
emcc hello.c -s WASM=1 -o hello.html
  • 参数-s WASM=1 要求生成.wasm否则编译成asm.js
  • -o hello.html Emscripten 生成一个我们所写程序的HTML页面,并带有 wasm 和 JavaScript 文件
    编译生成hello.wasm hello.js hello.html 可以用emrun运行这个html(直接用浏览器打开会认为读取file://文件违反policy) 因为docker没有映射端口,拷出来用http-server运行,是这样婶的:
    emscript_helloworld

FFmpeg.js 的实现

原文:Build FFmpeg WebAssembly version (= ffmpeg.wasm) 此链接国内网络或无法访问,可参考国内博客的类似文章,keyword:”webassemby” + “ffmpeg”

关于ffmpeg,video和视频流那篇曾用其进行视频的转码和输出流。

FFmpeg 是一个开放源代码的自由软件,可以运行音频和视频多种格式的录影、转换、流功能,包含了libavcodec(用于多个项目中音频和视频的解码器库),以及libavformat(音频与视频格式转换库)。————维基百科
clone FFmpeg 源码

1
git clone https://github.com/FFmpeg/FFmpeg

源码根路径INSTALL.md为构建/安装说明

1
2
./configure
make & make install

在windows上开发使用cygwin集成linux开发环境(本不应绕此远路),运行cygwin.setup安装程序安装以下库

1
2
3
4
gcc-core
mingw-gcc-core
binutils
gdb

执行configure加以下参数

1
bash ./configure --enable-cross-compile --disable-x86asm

configure大约需要执行十几分钟(或更久,取决于cpu加网速)

make issue: ./libavutil/mem.h:342:1: warning: ‘alloc_size’ attribute ignored on a function returning ‘int’ [-Wattributes]
342 | av_alloc_size(2, 3) int av_reallocp_array(void ptr, size_t nmemb, size_t size);
| ^~~~~
make: **
[ffbuild/common.mak:60: libavformat/mov_esds.o] Interrupt

旧版本函数参数不一致导致的问题,已被修复,见[FFmpeg-devel] avutil/mem: Fix invalid use of av_alloc_size

——————two weeks later————————

1
2
3
4
5
git clone https://github.com/FFmpeg/FFmpeg
cd FFmpeg
.configure - --disable-x86asm // 这一步是根据configure生成MakeFile
make // 编译
make install // 将编译产出复制到(安装)到linux的相应位置

测试FFmpeg可以尝试在终端使用媒体编辑功能

1
2
3
ffmpeg --help
ffmpeg -i test.mov -strict -2 -vf crop=720:405:0:451 out.mp4
// 从test.mov中裁剪720*405大小区域,裁剪偏移(0451)输出为out.mp4

我们目的是要生成.wasm放到浏览器中,这里用到Emscripten

Emscripten compiles C and C++ to WebAssembly using LLVM and Binaryen. Emscripten output can run on the Web, in Node.js, and in wasm runtimes. ——— 《Emscripten ReadMe》

回到make这步的输出项中:
ffmegoutput
这个泛着绿光的ffmpeg就是Binaryen(二进制文件)
LLVM(low level virtual machine)不限于字面意思的编译环境
关于emcc

  • Emcc 使用 Clang 和 LLVM 编译生成 Wasm或者asm.js
  • Emscripten SDK (emsdk) 配置 .emscriten, 用于管理多份SDK和工具,指定当前正在使用的编译代码(Active Tool/SDK)。

工具链依赖

1
apt-get install -y cmake python3.8

安装emsdk

1
2
3
4
5
6
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
git pull
./emsdk install latest // 这一步依赖python
./emsdk activate latest
source ./emsdk_env.sh // 定义环境变量

You always have to source ./emsdk_env.sh first in a new terminal session

输入emcc -v查看信息
emcc
看起来比较正常,说明工具安装成功

emsdk setup

另,其实安装emsdk不是必须的,docker hub中有现成的

1
2
3
docker pull emscripten/emsdk
docker run --rm -v $(pwd):/src -u $(id -u):$(id -g) \
emscripten/emsdk emcc helloworld.cpp -o helloworld.js

将FFmpeg源码编译成LLVM二进制码, 相比之前的configure && make这里不仅要使用emconfigure 命令,而且要设置下述的若干参数,于是做一个build .sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/bash -x

# verify Emscripten version
emcc -v

# configure FFMpeg with Emscripten
CFLAGS="-s USE_PTHREADS"
LDFLAGS="$CFLAGS -s INITIAL_MEMORY=33554432" # 33554432 bytes = 32 MB
CONFIG_ARGS=(
--target-os=none # use none to prevent any os specific configurations
--arch=x86_32 # use x86_32 to achieve minimal architectural optimization
--enable-cross-compile # enable cross compile
--disable-x86asm # disable x86 asm
--disable-inline-asm # disable inline asm
--disable-stripping # disable stripping
--disable-programs # disable programs build (incl. ffplay, ffprobe & ffmpeg)
--disable-doc # disable doc
--extra-cflags="$CFLAGS"
--extra-cxxflags="$CFLAGS"
--extra-ldflags="$LDFLAGS"
--nm="llvm-nm -g"
--ar=emar
--as=llvm-as
--ranlib=llvm-ranlib
--cc=emcc
--cxx=em++
--objcc=emcc
--dep-cc=emcc
)
emconfigure ./configure "${CONFIG_ARGS[@]}"

# build dependencies
emmake make -j4

# build ffmpeg.wasm
mkdir -p wasm/dist
ARGS=(
-I. -I./fftools
-Llibavcodec -Llibavdevice -Llibavfilter -Llibavformat -Llibavresample -Llibavutil -Llibpostproc -Llibswscale -Llibswresample
-Qunused-arguments
-o wasm/dist/ffmpeg.js fftools/ffmpeg_opt.c fftools/ffmpeg_filter.c fftools/ffmpeg_hw.c fftools/cmdutils.c fftools/ffmpeg.c
-lavdevice -lavfilter -lavformat -lavcodec -lswresample -lswscale -lavutil -lm
-s USE_SDL=2 # use SDL2
-s USE_PTHREADS=1 # enable pthreads support
-s INITIAL_MEMORY=33554432 # 33554432 bytes = 32 MB
)
emcc "${ARGS[@]}"

EMCC_DEBUG

设置EMCC_DEBUG变量使用调试模式编译wasm

1
2
3
4
5
EMCC_DEBUG=1 emcc dip.cc 
-s WASM=1
-O3
--no-entry
-o dip.wasm

issues expected magic word 00 61 73 6d, found

获取的wasm模块的MIME type不是application/wasm

关于wasm的调用

WebAssembly还没有和 \