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 可以消毒,还有其它的:

  1. Style (v10.0 之后就废弃了,现在已经不消毒 style 了)

    我没有考古出相关信息。

  2. Script

    domSanitizer.sanitize(SecurityContext.SCRIPT, unsafeScript)

    尝试消毒 unsafe script 会直接报错。

  3. 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)。

这两个环境有两大特点:

  1. 服务端没有 BOM 和 DOM

    比如 document, window 这些在游览器环境才存在

  2. 服务端只负责渲染,没有 event listener 

    游览器才能交互,才能有事件监听

假如我们的 Angular 项目要支持两个环境,首先在渲染阶段,我们要刻意避开使用任何游览器独有的特性,比如 docuiment 和 window。

什么叫渲染阶段呢?基本上除了 event handle 以外,ConstructorPreOrderHooksContentHooksViewHooks 都属于渲染阶段。

所以在 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 就做不到这个。

总结

  1. 不需要支持 Server-side rendering 的话,我们可以随意使用 DOM 和 BOM。

  2. 要支持 Server-side rendering 的话,在渲染阶段,我们要避开 DOM 和 BOM。

  3. inject(DOCUMENT) 可以让我们在 Server-side rendering 时使用 document 对象,这个 document 是用 domino 生成的,通常用它来 query element (比如 document.body)。

  4. 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。

 

posted @ 2024-02-06 18:26  兴杰  阅读(46)  评论(0编辑  收藏  举报