iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 17
0
Modern Web

高效 Coding 術:Angular Schematics 實戰三十天系列 第 17

[高效 Coding 術:Angular Schematics 實戰三十天] Day16 - 實戰 ng update

練習目標

今天的練習非常簡單,我們要利用 ng update 來改變 app.component.ts 裡, title 屬性的值。

雖然這個練習非常地陽春、簡單,但大家還是能夠透過此練習學習到:

  • 如何實作 ng update 的 Schematic ,且真正做出來。
  • 如何正確地修改程式碼。
  • 如何撰寫測試 ng update 的 Schematic 的測試程式。

筆者認為,只要這幾個關鍵點都有學會,其他的東西就都不是什麼大問題了。

想學更多?可以試著閱讀 Angular Components 的原始碼

基礎設定

首先先在 /src/ng-update 底下新增一個 index.ts

import { Rule, Tree, SchematicContext } from '@angular-devkit/schematics';

export function updateToV020(_options): Rule {
  return (_tree: Tree, _context: SchematicContext) => {
    return _tree;
  }
}

然後在 /src 底下新增一個 migration.json

{
  "$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
  "schematics": {
    "migration020": {
      "version": "0.2.0",
      "description": "Updates to 0.2.0",
      "factory": "./ng-update/index#updateToV020"
    }
  }
}

接著打開 package.json ,加入 ng-update 的設定:

{
  "//": "略",
  "ng-update": {
    "migrations": "./src/migration.json"
  }
}

基礎設定好之後,可以先來寫個基本測試,以確認我們的設定有正常運作。

基礎測試

測試的部份可以從之前寫過的 index_spec.ts 複製到 /src/ng-update 底下後,再把測試案例改成剩下這樣:

it('測試 ng update', async () => {
  const tree = await runner.runExternalSchematicAsync('./src/migration.json', 'migration020', {}, appTree).toPromise();  
  expect(tree.files.length).toBeGreaterThan(0);
});

從上述程式碼中可以看出,之前原本都是使用 runSchematicAsync 來實際執行我們的 Schematic ,但在這裡筆者已經將其換成 runExternalSchematicAsync

這是因為 ng-update 要執行的是設定在 migration.json 裡的 Schematic,這個部分跟其它 Schematic 不一樣,所以我們必須改為使用 runExternalSchematicAsync

接著就可以執行 npm test 來驗證目前的設定是否正確:

Imgur

測試先行

基礎測試沒問題後,我們就可以先把要做到的功能寫成測試程式碼:

it('測試 ng update', async () => {
  const tree = await runner.runExternalSchematicAsync('./src/migration.json', 'migration020', {}, appTree).toPromise();  
  const componentContent = tree.readContent('/projects/hello/src/app/app.component.ts');
  expect(componentContent).toContain('title = \'Leo Chen\'');
});

這時如果我們再執行 npm test 來驗證結果的話,應該會出現以下畫面:

Imgur

這是正常的,因為實作根本就還沒做,怎麼可能會通過測試?!

所以這時候,我們就只需要實作出能夠把這個測試案例從紅燈變成綠燈的程式碼就夠了,而這也是 TDD ( Test-driven development ,測試驅動開發)入門心法之一。

功能實作

測試寫好之後,我們就可以打開同在 /src/ng-update 底下的 index.ts ,並將之前寫過的支援 Angular Project 的部份複製過來。

首先是好用的 readIntoSourceFile

function readIntoSourceFile(host: Tree, modulePath: string): ts.SourceFile {
  const text = host.read(modulePath);
  if (text === null) {
    throw new SchematicsException(`File ${modulePath} does not exist.`);
  }
  const sourceText = text.toString('utf-8');
  return ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true);
}

該引入的東西筆者就不再贅述囉!

接著是解析 Angular 專案的部份:

export function updateToV020(): Rule {
  return (_tree: Tree, _context: SchematicContext) => {
    
    const workspaceConfigBuffer = _tree.read('angular.json');
    if ( !workspaceConfigBuffer ) {
      throw new SchematicsException('Not an Angular CLI workspace');
    }

    const workspaceConfig = JSON.parse(workspaceConfigBuffer.toString());
    const projectName = workspaceConfig.defaultProject;
    const project = workspaceConfig.projects[projectName];
    const defaultProjectPath = buildDefaultPath(project);

    return _tree;
  }
}

需要特別留意的是, updateToV020 這裡沒有參數可以使用,所以就不用加 _options

最後是實際實作的部份:

export function updateToV020(): Rule {
  return (_tree: Tree, _context: SchematicContext) => {
    // 略
    
    // 把 app.component.ts 轉成 Typescript AST
    const componentPath = `${defaultProjectPath}/app.component.ts`;
    const componentSourceFile = readIntoSourceFile(_tree, componentPath);

    // 試著找出 title 屬性
    const classDeclaration = componentSourceFile.statements.find( node => ts.isClassDeclaration(node) )! as ts.ClassDeclaration;
    const allProperties = classDeclaration.members.filter( node => ts.isPropertyDeclaration(node) )! as ts.PropertyDeclaration[];    
    const titleProperty = allProperties.find( node => node.name.getText() === 'title' );
    
    // 如果有找到 title 屬性再修改程式碼
    if ( titleProperty ) {
      const initialLiteral = titleProperty.initializer as ts.StringLiteral;

      const componentRecorder = _tree.beginUpdate(componentPath);
      const startPos = initialLiteral.getStart();
      componentRecorder.remove(startPos, initialLiteral.getWidth());
      componentRecorder.insertRight(startPos, '\'Leo Chen\'');

      _tree.commitUpdate(componentRecorder);
    }

    return _tree;
  }
}

這邊需要留意的是, initialLiteral.posinitialLiteral.getStart() 所取得的位置不一樣, initialLiteral.getWidth()initialLiteral.getFullWidth() 取得的寬度也不一樣,請小心使用。

功能實作完成後,可以再輸入 npm test 驗證看看,應該要是可以把紅燈變成綠燈的噢!

結果:

Imgur

除了下 npm test 之外,也可以到其他 Angular 專案裡,輸入 schematics {relativePath}/src/migration.json:migration020 --debug=false 實際跑跑看!

如果想用 ng upadte 驗證的話,就只能先將 Package 上傳之後才能測試。想實測看看的話,可以先使用筆者的 Package ,並按照以下步驟測試:

  1. 輸入 npm install angular-schematics-30days-challenge@0.1.0
  2. 輸入 ng update angular-schematics-30days-challenge

學習即玩樂,親自動手玩玩看吧!

本日結語

今天的程式碼:https://github.com/leochen0818/angular-schematics-30days-challenge/tree/day16

ng update 所做的事情是真的稍微樸實無華且枯燥了一點,不過實際做完之後還是滿有成就感的!希望各位也能成功做出實際支援 ng update 的 Schematic 。

明天筆者將會分享如何打包、發佈自己的專案到 npm 上,敬請期待!

參考資料


上一篇
[高效 Coding 術:Angular Schematics 實戰三十天] Day15 - ng update?
下一篇
[高效 Coding 術:Angular Schematics 實戰三十天] Day17 - 打包與發佈
系列文
高效 Coding 術:Angular Schematics 實戰三十天32

尚未有邦友留言

立即登入留言