typescript 的 dirname/basename

node 是有一個 path 的 package,但這個 package 會依賴 process 來取得平台環境,以決定要用 win32 或是 posix 來處理路徑。

但我其實只是要簡單的取得父路徑跟從路徑取出檔案或目錄名稱,不需要這樣麻煩。上網找到這篇:Javascript functions for basename and dirname 。它的實作是使用 regular expression 來處理,所以傳回的並不是單純的字串,而是一個陣列 RegExMatchArray 。參考 MDN 的 String.prototype.match() 來做調整,簡單的說,陣列的第一個元素就是處理的結果,所以只要先判斷傳回值是否為  null ,然後再傳回第一個元素即可。

// Typescript
/**
 * 取得父路徑
 */
export function dirname(path: string): string {
  let newPath = '';
  // 找符合 '/aaa/bbb/ccc/' 的字串
  const regExMatchArray = path.match(/.*\//);
  // 沒有找到,傳回 '/'
  if (regExMatchArray === null ) {
    newPath = '/';
  }
  else {
    // 有找到
    newPath = regExMatchArray[0];
    // 看最後字元是否為 '/',是的話,就移除。
    if (newPath.endsWith('/')) {
      newPath = newPath.substring(0, newPath.length - 1);
    }
    // 最後結果的長度為0,加上 '/'
    if (newPath.length === 0) {
      newPath = '/';
    }
  }
  return newPath;
}

/**
 * 取得檔名或目錄名
 */
export function basename(path: string): string {
  // 把 '/' (含)之前的字元都替換成空字串
  return path.replace(/.*\//, '');
}

Angular 嵌入 redoc API 文件

原本是打算把 redoc-cli 產生出來的文件直接嵌進去的,可是這樣子在文件有變動時,就會又要再做一次產生、嵌入,這樣不太好。最好還是可以自動依據寫好的 OpenAPI specification 來自動產出,這才是比較好的作法。

第一個待解決的問題是怎麼把 OpenAPI specification JSON 放到 Angular 專案裡,並且可以讀出來使用。關於這個,我是找到 How To Read Local JSON Files In Angular 這篇文章,方法挺簡單的,就把 json 檔案丟到 src/assets 下,然後直接在程式裡用 import 。

// src/app/xxx/xxx.component.ts
import SampleJson from '../../assets/SampleJson.json’;

// 後續程式碼就直接引用 SampleJSON 即可。

但修改完,TypeScript compiler 會有錯誤訊息,這得要改 tsconfig.json 在 compilerOptions 裡加入 resolveJsonModule 跟 esModuleInterop

{ "compilerOptions": { "resolveJsonModule": true, "esModuleInterop": true } }

第二個問題是,angular template 裡不能直接寫 script tag,這個可以實作 AfterViewInit ,然後用 DOMElement 來動態插入 (參考來源:https://stackoverflow.com/questions/38088996/adding-script-tags-in-angular-component-template/43559644)。細節的說明,我就直接寫在下面程式的註解裡。

import {
  Component,
  OnInit,
  ViewEncapsulation,
  ElementRef,
  AfterViewInit } from '@angular/core’;
// (1) 剛剛前面提到的,引用 OpenAPI specification JSON
import APIJson from '../../assets/api.json';

@Component({
  selector: 'app-documentation',
  templateUrl: './documentation.component.html',
  styleUrls: ['./documentation.component.scss'],
  encapsulation: ViewEncapsulation.None
})
export class DocumentationComponent implements OnInit, AfterViewInit {  // (2) Component 要實作 AfterViewInit 這介面

  constructor(private elementRef: ElementRef) { }  // (3) 引入 ElementRef ,Angular 會幫忙作動態注入

  ngAfterViewInit(): void {
    // (4) 這裡就是在頁面載入後要作的事情
    this.insertSpec();
    this.insertScript();
  }

  insertScript(): void {
    // (5) 插入 script element,這裡要載入 redoc 的腳本
    const redocScript = document.createElement('script');
    redocScript.type = 'text/javascript';
    redocScript.src = 'https://unpkg.com/redoc@next/bundles/redoc.standalone.js’;  // (6) 這腳本的網址是閱讀 redoc-cli 的原始碼以後取得的。
    this.elementRef.nativeElement.appendChild(redocScript);

    // (7) 插入另外一個 script element,這裡主要是執行 redoc,讓 redoc 能解析 json 並顯示出文件。
    const initScript = document.createElement('script');
    initScript.type = 'text/javascript';
    initScript.textContent = `
    const __redoc_state=${JSON.stringify(APIJson)};
    var container = document.getElementById('redoc');
    Redoc.hydrate(__redoc_state, container);
    `;  // (8) 字串要允許多行,可以使用 backquote ` https://stackoverflow.com/questions/35225399/multiline-string
    this.elementRef.nativeElement.appendChild(initScript);
  }

  insertSpec(): void {
    // (9) 插入 element ,讓腳本知道要根據哪個 element 來作處理。
    let s = document.createElement('redoc');
    s.setAttribute('spec-url', 'assets/api.yml');
    this.elementRef.nativeElement.appendChild(s);
  }

  ngOnInit(): void {
  }
}

至此就大功告成啦。

P.S. redoc 在 1.x 時,是有支援 Angular 的,但到了 2.x ,就改用 react 了。

不是只有 console.log

這篇 JavaScript console is more than console.log() 講了幾種 console 的用法,console 不是只有 log 可以用。

  1. console.warn() / console.error() 可以依據訊息等級顯示出不同的顏色。
  2. console.group() / console.groupEnd() 可以讓這中間的 log 輸出都摺疊起來,避免太多。
  3. console.table() 是印出表格,表格的形式比較容易查看資料內容。
  4. console.trace() 是印出 stacktrace 資訊
  5. console.time() / console.timeEnd() 可以輸出這中間的指令花了多少時間。
  6. console.clear() 清理終端機輸出內容
  7. console.dir() 可以印出物件的屬性。

另外,現在瀏覽器的 js 都支援 backquote – template literals ,也就是 ` ` ,所以也可以這樣用 console.log(`obj=${JSON.stringify(x)}`);

NgRx

因為 NgRx 是用 Redux 的概念,所以先看 Redux

看完大致可以理解,就是 design pattern 裡講的 state pattern。大部份 Redux 例子都是搭配 React,看的還是霧煞煞,所以找 NgRx 搭配 Angular 的例子來看。

我覺得這三篇的例子蠻清楚的,很容易可以了解現有的 angular 碰到 ngrx 時,要怎麼結合。

原本 angular 的一個頁面是 component html 跟 component code ,現在加上 ngrx 以後,會使用 store 儲存狀態,寫 reducer 來處理狀態。當有事件觸發時,就使用 store.dispatch 去發送 action,reducer 在收到以後,會依據 action 來處理狀態,然後回傳狀態。這時候頁面會因為 binding ,而反映出新的狀態結果。

這三篇雖然清楚,但已經舊了,我在試驗時,就碰到兩個問題:

  1. 找不到 StoreModule.provideStore() 這個方法,這個已經改為 StoreModule.forRoot()
  2. Observable 找不到,要解決這個問題,除了得裝 rxjs 之外,還要裝 rxjs-compat 讓 rxjs 向前相容。

文章有提到 Redux DevTools 這個工具,可以從 Firefox addons/Chrome store NgRx 安裝,安裝以後,專案程式那邊也需要調整。調整的部份可以參考 @ngrx/store-devtools ,安裝好,修改 app.module.ts 之後,就可以使用了。使用的方式是先開啟專案的網址,然後再開 developer tools,這時會看到有個 Redux 分頁,試著觸發一些事件看看,這邊就會出現發送的 action 以及改變前後的狀態了。

在 javascript 裡將 dict/object 轉換為 xml

照一般的作法,會是用操作 DOM 的方式來產生出 XML,但這樣很繁瑣,找了好幾個函式庫,最後是找到 object-to-xml (Github網址),使用方法很簡單:

var objectToXML = require('object-to-xml');

var obj = { 
  '?xml version=\"1.0\" encoding=\"iso-8859-1\"?' : null,
  request : {
    '@' : {
      type : 'product',
      id : 12344556
    },
    '#' : {
      query : {
        vendor : 'redhat',
        name : 'linux'
      }
    }
  }
};

console.log(objectToXML(obj));
/*
<?xml version="1.0" encoding="iso-8859-1"?>
<request type="product" id="12344556">
  <query>
    <vendor>redhat</vendor>
    <name>linux</name>
  </query>
</request>
*/

在 ‘@’ 裡的,都是屬性,而在 ‘#’ 裡的則是子 element 。

在 typescript 裡,要用

import * as objectToXML from 'object-to-xml';

來匯入使用。

TypeScript 學習筆記(6) – Classes

主要看這篇:Classes

大致用法跟 C#/Java 差不多,幾個特別的地方:

  1. readonly :成員可以加上 readonly 來修飾,表示是唯讀。
  2. 有 getter/setter:可以用 get/set 這兩組 accessor 讓 method 變成屬性。
    class Employee {
        private _fullName: string;
    
        get fullName(): string {
            return this._fullName;
        }
    
        set fullName(newName: string) {
            this._fullName = newName;
        }
    }
    
  3. static:類似 java/c# 那樣的用法,原本 javascript 是沒有的。
  4. abstract 修飾:可以加在 class 前面,也可以加在 method 前面,用法跟 java/C# 相似。

後面提到的 constructor functions 是一個進階技巧,不過感覺上用到機會不大。

TypeScript 學習筆記(5) – Interface

今天是看這篇:Interfaces

TypeScript 有 anonymous interface ,就像下面的程式一樣:

// 有點 anonymous interface 的味道
function printLabel1(labelledObj: { label: string }) {
    console.log(labelledObj.label);
}

// 也可以明確的用 interface 寫
interface LabelledValue {
    label: string;
}

function printLabel2(labelledObj: LabelledValue) {
    console.log(labelledObj.label);
}

 

在宣告成員時,如果加入 ? ,表示可有可無,這個寫法應該是參考了 Ruby 的作法

interface SquareConfig {
    color?: string;
    width?: number;
}

 

用 readonly 修飾詞則是表示唯讀

interface Point {
    readonly x: number;
    readonly y: number;
}

 

如果要需告一個唯讀的陣列,可以用 ReadonlyArray

let ro: ReadonlyArray = a;

 

我覺得 Function types 比較不是那麼直覺,不容易看懂

interface SearchFunc {
    (source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
    let result = source.search(subString);
    return result > -1;
}

大抵來說就是 python 的 __call__ 或是 c++ 的 operator()。

Indexable type 也是不太直覺

interface StringArray {
    [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

大抵來說就是 python 的 __item__ 或是 c++ 的 operator[]。

要讓類別實作介面是用 implements 關鍵字

interface ClockInterface {
    currentTime: Date;
}

class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

 

下面這例子是想要限定 ClockInterface 裡的 constructor 必須符合 ClockConstructor 規範的作法,第一眼不太容易看懂。

interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
    tick();
}

// 傳進來的 ctor 必須符合 ClockConstructor 的規範,要有 hour 跟 minute 這兩個 number 型別的參數。
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute);
}

class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("tick tock");
    }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

 

介面也可以繼承,跟類別一樣,是用 extends,而且允許繼承多個

interface Shape {
    color: string;
}
interface PenStroke {
    penWidth: number;
}
interface Square extends Shape, PenStroke {
    sideLength: number;
}

 

蠻令我驚訝的是,Interface 可以繼承 class … @_@

TypeScript 學習筆記(4) – 變數

JavaScript 提出了兩個新的關鍵字:let 與 const。使用 let 可以避免使用 var 的一些奇妙現象;而 const 就是跟 C 的 const 一樣,避免再次指定值給這個變數。

Variable Declarations 裡提了許多 JavaScript 使用 var 宣告變數時的奇妙現象,然後解說為什麼要改用 let。這部份我偷懶跳過去了,我的想法是,就都改用 let 吧。

另外有提到 Destructuring,就是類似 Python 的 unpack :

// JavaScript/TypeScript
let input = [1, 2];
let [first, second] = input;

Python 則是:

# Python
first, second = (1, 2)

也跟 Python 3 一樣,有取 remain items 的效果:

let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]

Object destructuring 是個比較特別的東西,意思跟 destructuring 差不多,但對象是 object,也就是可以將 object 的某幾個屬性放到變數裡。

let o = {
    a: "foo",
    b: 12,
    c: "bar"
}
let { a, b } = o;
({ a, b } = { a: "baz", b: 101 });  // 這樣也可

也支援 remaining items 的方法:

let { a, ...passthrough } = o;

Object destructuring 還支援 default values,意思是如果 wholeObject 沒有 b 屬性時,就把 b 指定為 1001:

function keepWholeObject(wholeObject: { a: string, b?: number }) {
    let { a, b = 1001 } = wholeObject;
}

也可以應用到 function 上,我覺得這個例子比較清楚,可以應用在只取物件的某幾個屬性來使用時:

function f({ a, b } = { a: "", b: 0 }): void {
    // ...
}
f(); // ok, default to { a: "", b: 0 }

Spread 算是陣列相加的一個 syntax sugar,可以把 first, second 塞到 bothPlus 這陣列裡去:

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];

也可以應用在 object 上,目前想到可以用來做 object proxy 或是 composite 之類的,不過不適合也不一定。

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };

 

TypeScript 學習筆記(3) – 基本型別

今天是看這篇:Basic Types

裏面直接建議用 ES6 的 let 替代 var 了。

  • boolean:布林型態
  • number:數字型態,沒有 integer, long, float, double 之類的,只有 number,JavaScript 裡的數字都是 float。
  • string:字串
  • array:陣列,在型別後面加上 [],例如: let list: number[] = [1, 2, 3]; ,要注意的是,無法指定固定個數。
  • tuple:就 tuple ,有點像陣列,但能指定每個元素的型態,這跟 Python 不一樣。例如:
    let x: [string, number];
    let x = ["Hello", 100];
    let y: [string, number] = ["John", 200];
    y[2] = "Doe";
    console.log(y);  // 輸出 [ 'John', 200, 'Doe' ]
    
  • enum:列舉,用法跟其他語言大致相似:
    enum Color {Red, Green, Blue};
    let c: Color = Color.Green;
    // 比較特別的用法
    let colorName: string = Color[2];
    console.log(colorName);  // 輸出 Blue
    
  • any:任意型態,但要注意的是跟其他語言所指的 Object 型別不同,TypeScript 有獨立一個 Object 型別。說起來跟 C# 的 dynamic 比較相似。
    let notSure: any = 4;
    notSure.ifItExists(); // 通過編譯,因為可能在執行時期就有 ifItExists()
    notSure.toFixed(); // 通過編譯,因為可能在執行時期就有 toFixed()
    
    let prettySure: Object = 4;
    prettySure.toFixed(); // 編譯錯誤,編譯時會有 Object 型別沒有 toFixed 的錯誤。
  • void:就 void ,表示不會傳回任何東西
  • null:就 null ,可以把任何型態的變數指定為 null
  • undefined:就 undefined,可以把任何型別的變數指定為 undefined
  • never:表示絕對不會傳回變數,
    // 只會丟出例外
    function error(message: string): never {
        throw new Error(message);
    }
    // 裏面是不會結束的迴圈
    function infiniteLoop(): never {
        while (true) {
        }
    }

Type assertion ,有 C/C# 強制轉型的意味。

let someValue: any = "this is a string";
let strLength: number = (someValue).length;
// 也可以用類似 c# as 的語法
let strLength: number2 = (someValue as string).length;