iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 18
0
Modern Web

從巨人的 Tip 看 Angular系列 第 18

[Day 18] 寫了 ng-template 卻在渲染後只留下註解!關於 HTML tag 不見的秘密

  • 分享至 

  • xImage
  •  

好的,昨天文章的最後,我在 HTML template 上使用了 這個 tag 來作為 AnchorDirective 的 host element,會這麼做的原因其實滿簡單的,就是 ng-template 這個元素實際上「並不會」被放進 DOM 中,所以也就不會影響 CSS 樣式,導致跑版的問題。。

也因為這個特性,所以在設計 Angular component 上有需要因為使用者操作來改變畫面版型的時候,就很適合以 ng-template 來當作 structural directives 的 host element。

然而今天要分享的內容沒有其他,就是來看一下為什麼 ng-template 會從 DOM 上消失。


先看看編譯後的內容

<ng-template [ngIf]="true">
  <p>Hello, world!</p>
</ng-template>

↑ Block 1

我將以 Block 1 的簡單程式碼作為範例,來分析 Angular 編譯後的結果,以及 Angular 會如何處理 ng-template 的 html tag。

function AppComponent_ng_template_0_Template(rf, ctx) {
  if (rf & 1) {
    _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementStart"](0, "p");
    _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtext"](1, "Hello, world!");
    _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵelementEnd"]();
  }
}

↑ Block 2

Block 2 是一個滿關鍵的點,Angular 的 compiler 會把 ng-template 的 tag 轉換成一個 function,並在這個 function 內將 ng-template 的 element 用 ɵɵelementStartɵɵelementEnd 等 instruction 將 elements 渲染出來。所以我們可以得知,實際上並不會有一個 ng-template 的 tag 被放進 DOM 中。

結束!

然後我們來看一下 Block 1 這個範例的 AppComponet 經過編譯後的其他內容:

AppComponent.ɵfac = function AppComponent_Factory(t) {
  return new (t || AppComponent)();
};
AppComponent.ɵcmp = _angular_core__WEBPACK_IMPORTED_MODULE_0__[
  "ɵɵdefineComponent"
]({
  type: AppComponent,
  selectors: [["app-root"]],
  decls: 1,
  vars: 1,
  consts: [[3, "ngIf"]],
  template: function AppComponent_Template(rf, ctx) {
    if (rf & 1) {
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵtemplate"](
        0,     // index
        AppComponent_ng_template_0_Template, // templateFn
        2,     // decls
        0,     // vars
        "ng-template",  // tagName
        0      // attrsIndex
      );
    }
    if (rf & 2) {
      _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵproperty"]("ngIf", true);
    }
  },
  directives: [_angular_common__WEBPACK_IMPORTED_MODULE_1__["NgIf"]],
  styles: [
    / ... 
  ],
});
/*@__PURE__*/ (function () {
  // ... 略
})();

↑ Block 3

Block 3 除了工廠方法之外,也有 AppComponent 的 definition,可以看到第一個元素(ng-template)就會將 Block 2 的AppComopnent_ng_template_0_Template function 傳入 ɵɵtemplate

然後我們就可以翻 Angular 的原始碼囉。

開始爬 code!

當瀏覽器開始執行 main.js 的程式碼來渲染整個應用程式時,會先從 NgModule 開始處理,一路處理到 AppComponent,接著處理 AppComponent 的 HTML Template,也就是 Block 3 的 AppComponent_Template 函式,因為範例只有一個 ng-template 的 tag,所以馬上就會執行 ɵɵtemplate。

我們來看一下 ɵɵtemplate 這個函式的實作內容:

export function ɵɵtemplate(
    index: number, templateFn: ComponentTemplate<any>|null, decls: number, vars: number,
    tagName?: string|null, attrsIndex?: number|null, localRefsIndex?: number|null,
    localRefExtractor?: LocalRefExtractor) {
  const lView = getLView();
  const tView = getTView();
  const adjustedIndex = index + HEADER_OFFSET;

  const tNode = tView.firstCreatePass ?
      templateFirstCreatePass(
          index, tView, lView, templateFn, decls, vars, tagName, attrsIndex, localRefsIndex) :
      tView.data[adjustedIndex] as TContainerNode;
  setCurrentTNode(tNode, false);

  const comment = lView[RENDERER].createComment(ngDevMode ? 'container' : '');
  appendChild(tView, lView, comment, tNode);
  attachPatchData(comment, lView);

  addToViewTree(lView, lView[adjustedIndex] = createLContainer(comment, lView, comment, tNode));

  if (isDirectiveHost(tNode)) {
    createDirectivesInstances(tView, lView, tNode);
  }

  if (localRefsIndex != null) {
    saveResolvedLocalsInData(lView, tNode, localRefExtractor);
  }
}

↑ Block 4:ɵɵtemplate 的實作內容

如同 ɵɵElementStart 方法一樣,ɵɵtemplate 也會先取得目前的 LView 與 TView 物件實體,接著會透過 templateFirstCreatePass 方法建立 TNode 物件並指派給 tNode 變數。若已經建立有相同的 TNode 物件被建立的話,Angular 會選擇將已存在的物件指派給 tNode,而非重新再建立一次。

另外,這個函式執行的時候也會產生一個 HTML 的註解,變數名稱為 comment。

建立完 tNode 與 comment 之後,Angular 會接著透過 appendChild 方法將 comment 放進 DOM Tree 內:

export function appendChild(
    tView: TView, lView: LView, childEl: RNode|RNode[], childTNode: TNode): void {
  const renderParent = getRenderParent(tView, childTNode, lView);
  if (renderParent != null) {
    const renderer = lView[RENDERER];
    const parentTNode: TNode = childTNode.parent || lView[T_HOST]!;
    const anchorNode = getNativeAnchorNode(parentTNode, lView);
    if (Array.isArray(childEl)) {
      for (let i = 0; i < childEl.length; i++) {
        nativeAppendOrInsertBefore(renderer, renderParent, childEl[i], anchorNode);
      }
    } else {
      nativeAppendOrInsertBefore(renderer, renderParent, childEl, anchorNode);
    }
  }
}

↑ Block 5

在瀏覽器上看的時候,就會有一個像下圖的 comment:

https://ithelp.ithome.com.tw/upload/images/20201003/20129148GRsiQT0mYG.png

↑ Image 1

如此一來原先在 HTML template file 內寫的 ng-template tag 就消失不見了,只留下一些註解跟開發者說:「嘿!這裡可能會有東西喔!」。

至於其他被放進 ng-template 內的元素,瀏覽器會呼叫 executeTemplate 這個函式(link)來執行編譯後的 template function(Block 2 的 AppComponent_ng_template_0_Template 函式),再來就會依序呼叫 ɵɵElementStart、ɵɵtext、ɵɵElementEnd 來將放在 ng-template 內的元素渲染出來囉 ?


以上就是今天要跟各位分享的內容囉,明天會再接著介紹其他關於 ng-template 的進階用法!

普通的星期六 ?

以下按照入團順序列出我們團隊夥伴的系列文章!

請自由參閱 ?


上一篇
[Day 17] 透過 directive 來動態插入 Component!
下一篇
[Day 19] ngTemplate 與它的小夥伴們
系列文
從巨人的 Tip 看 Angular30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言