Angular 17+ 高级教程 – 大杂烩
前言
本篇记入一些小东西。
Angular 废弃 API 列表
Docs – Deprecated APIs and features
Using Tailwind CSS with Angular
依照这个教程做可以了:Install Tailwind CSS with Angular
postcss 和 autoprefixer 即便不安装也可以跑,但 tailwindcss 一定要。
原因是 Angular CLI 里面是有安装了 postcss 和 autoprefixer 的
而 tailwindcss 是 under peerDependencies
如果有 tailwind.config.js 但是没用 yarn install tailwindcss,那是会 warning 的
DomSanitizer
DomSanitizer 是 Angular built-in 的消毒器。
DomSanitizer Provider
它是一个 Root Level Provider,用法非常简单。
export class AppComponent { constructor() { const domSanitizer = inject(DomSanitizer); // 1. 里面包含了一些不安全的东西,e.g. script, style, template const unsafeHtml = ` <h1>Hello World</h1> <script>alert('abc')</script> <style>*{}</style> <template>0</template> <p>Lorem ipsum dolor sit amet.</p> `; // 2. 使用 domSanitizer.sanitize 对 unsafeHtml 消毒 const safeHtml = domSanitizer.sanitize(SecurityContext.HTML, unsafeHtml); console.log('unsafe', unsafeHtml); console.log('safe', safeHtml); } }
效果
sanitize 会把不安全的东西消除掉,比如 <script>, <style>, <template> 等等。
不只是 HTML 可以消毒,还有其它的:
-
Style (v10.0 之后就废弃了,现在已经不消毒 style 了)
我没有考古出相关信息。
-
Script
domSanitizer.sanitize(SecurityContext.SCRIPT, unsafeScript)
尝试消毒 unsafe script 会直接报错。
-
URL & Resource URL
首先要懂得区分 URL 和 Resource URL
比如 <img src> 就是 URL,<iframe src> 则是 Resource URL。
URL 可以消毒,Resource URL 不消毒直接会报错。
DomSanitizer used by Angular Internal
App HTML
<div [innerHTML]="unsafeHtml"></div>
App 组件
export class AppComponent { unsafeHtml = ` <h1>Hello World</h1> <script>alert('abc')</script> <style>*{}</style> <template>0</template> <p>Lorem ipsum dolor sit amet.</p> `; }
run compilation
yarn run ngc -p tsconfig.json
App Definition
在做 binding [innerHtml] 时,Angular 会使用 ɵɵsanitizeHtml 函数。
ɵɵsanitizeHtml 函数的源码在 sanitization.ts。
除了 HTML 还有其它的也会使用消毒,我就不一一列出来了。
提醒:如果我们自己使用 Renderer2.setProperty(el, 'innerHTML', 'raw html'),它内部不会自动替我们消毒哦,我们需要先消毒了 'raw html' 才传进去。
bypassSecurityTrust
上面有提到 sanitize 不一定会消毒成功变成 safe value,有时候它会直接报错 (也没有检查哦),提醒我们不安全而已。
所以即便我们给的 string 是安全的也没用,它依然直接报错。这是就需要 bypass。
export class AppComponent { constructor() { const domSanitizer = inject(DomSanitizer); const unsafeScript = ''; const safeScript = domSanitizer.sanitize(SecurityContext.SCRIPT, unsafeScript); console.log(safeScript); } }
直接报错,即使 unsafeScript 只是 empty string。
这种情况下就需要用 bypassSecurityTrust 方法。
export class AppComponent { constructor() { const domSanitizer = inject(DomSanitizer); const unsafeScript = ''; const bypassScript = domSanitizer.bypassSecurityTrustScript(unsafeScript); console.log('bypassScript', bypassScript); const safeScript = domSanitizer.sanitize(SecurityContext.SCRIPT, bypassScript); console.log('safeScript', safeScript); } }
效果
HTML,Style,Script,URL,Resource URL 都有对应的 bypass 方法
提醒:bypass 就是 skip 掉消毒,它不是 white list 的概念哦,没用一半一半的。相关 Github Issue – Extensible Sanitizer。
Renderer2 和 inject(DOCUMENT)
Angular 项目通常是运行在游览器上的,但如果项目有需要做 server-side rendering (SSR),那 Angular 会运行在服务端环境 (比如 Node.js)。
这两个环境有两大特点:
-
服务端没有 BOM 和 DOM
比如 document, window 这些在游览器环境才存在
-
服务端只负责渲染,没有 event listener
游览器才能交互,才能有事件监听
假如我们的 Angular 项目要支持两个环境,首先在渲染阶段,我们要刻意避开使用任何游览器独有的特性,比如 docuiment 和 window。
什么叫渲染阶段呢?基本上除了 event handle 以外,Constructor, PreOrderHooks, ContentHooks, ViewHooks 都属于渲染阶段。
所以在 constructor, OnInit, AfterContentInit, AfterViewInit 这些方法里面,我们都不可以使用 document, window 这些。
AfterRenderHooks 则不同,它在 server-side rendering 时是不被调用的,所以 afterNextRender, afterRender 函数里可以使用 document, window 这些。
Add class to body through Renderer2 and DOCUMENT
绝大部分的 DOM 操作,我们都可以通过 Angular MVVM way 去解决,比如 Template Binding Syntax。
但 Angular 的范围始终局限在 <app-root /> 里面,如果我们想给 body element 添加一个 class 该怎么做到呢?(要支持 server-side rendering)
解决方法是使用 Renderer2 和 DOCUMENT 代理
export class AppComponent { constructor() { const renderer = inject(Renderer2); const document = inject(DOCUMENT); renderer.addClass(document.body, 'dark-theme'); } }
在游览器环境下,inject(DOCUMENT) 拿到的是游览器的 document 对象。
在服务端环境下,inject(DOCUMENT) 拿到的是由服务端创建出来的 document 对象。
parseDocument 和 createHtmlDocument 是创建 Document 对象的方法。它底层是用 domino (一个基于 Mozilla's dom.js 的库) 来实现的。
我没有研究过服务端 document 和游览器 document 具体有没有区别,但我感觉它们肯定是不一样的,虽然它们 interface 一样,这部分等以后我研究了 Angular Server-side rendering 再补上呗。
那我们可以不可以直接 document.body.classList.add('dark-theme') 添加 class?
我觉得是可以的,但是更安全的做法是使用 Renderer2。
Renderer2 是 Angular 封装的渲染 Service,但凡我们需要 manipulation DOM,不管是 for render 还是 for add event listener 都尽量使用 Renderer2 就对了。
我们看个例子,体会一下使用 Renderer2 和直接操作 DOM 的区别
renderer.listen(this.inputElementRef().nativeElement, 'keydown.enter', () => console.log('enter'));
看到吗,它可以监听 keydown.enter 事件,这个是 Angular 扩展的,如果我们用原生 element.addEventListener 就做不到这个。
总结
-
不需要支持 Server-side rendering 的话,我们可以随意使用 DOM 和 BOM。
-
要支持 Server-side rendering 的话,在渲染阶段,我们要避开 DOM 和 BOM。
-
inject(DOCUMENT) 可以让我们在 Server-side rendering 时使用 document 对象,这个 document 是用 domino 生成的,通常用它来 query element (比如 document.body)。
-
Renderer2 不仅仅可以用于 Server-side rendering,它也适用于游览器环境,inject(DOCUMENT) 负责 "read",那 Renderer2 就是负责 "write"。
它可以 addClass, setAttribute, appendChild, listen 等等
PLATFORM_ID, isPlatformBrowser, isPlatformServer
Angular 支持 SSR (Server-side rendering),也就是说我们的程序有可能会运行在 server-side (e.g. Node.js),不一定是 browser。
如果运行在 server-side,那就不能调用 browser 的 BOM。所以,我们的程序需要有能力区分这两种环境,并做出相应的处理。
PLATFORM_ID token, isPlatformBrowser 函数, isPlatformServer 函数,这些变是 Angular 提供给我们用于区分当前执行环境的。
export class AppComponent { constructor() { const platformId = inject(PLATFORM_ID); // 'browser' const isRunningOnBrowser = isPlatformBrowser(platformId); // true const isRunningOnServer = isPlatformServer(platformId); // false } }
用法非常简单,注入 PLATFORM_ID token,它会拿到一个 string,然后把这个 value 传给 isPlatformBrowser 函数或者 isPlatformServer 函数,它们会返回 boolean。