筆記:Best practices for REST API design

來源:Best practices for REST API design

下面是看完以後,我的整理、摘要以及一點自己的想法:

  • 盡可能使用 JSON (application/json),除非有特殊需求,才來考慮使用其他格式,例如 form (上傳檔案) 或是 xml
  • 使用名詞,而非動詞:這樣才能搭配 HTTP verbs 的 POST/DELETE/GET/PUT,POST 放新資料到伺服器上,DELETE 移除資料,GET 取得資料,PUT 是更新資料
  • 時使用複數,表示取得一堆資料。
  • 階層物件可以使用巢狀資源,例如 /articles/article_id/comments/ 。P.S. 碰到這種情況其實也可以考慮使用 GraphQL
  • 漂亮的處理錯誤並回傳錯誤代碼,盡可能利用 HTTP 狀態碼而不要另外建立新的錯誤代碼。伺服器端在回傳錯誤時,可以帶上錯誤訊息,讓客戶端便於判斷、處理。
  • 允許篩選、排序跟分頁。篩選跟分頁可以避免一次拿過多資料,導致伺服器傳輸過多資料,也可以提升速度,拿的少,自然就快,對吧~
  • 安全性,SSL/TLS 是必要的,再來就是處理好使用者能存取的資源權限,這部份是實作 API 時必須要考慮到的。
  • 要用 cache,實作時要想,這資料會很常變動嗎?會很常被索取嗎?不常變動又很常被索取,就放到 cache 吧。
  • 幫 API 編上版號,這可以避免影響到舊有的 API,一方面也可以單純化,不用考慮到過多相容性的問題。例如:/v1, /v2

Angular i18n 的另外一個選擇 ngx-translate

Angular i18n 的官方作法並不讓人滿意,我後來找到 ngx-translate

先說明一下他的作法,他把這些翻譯好的字詞放到 JSON 翻譯檔去,在執行時就可以透過 http client 去拉取回來作動態的替換。

好處是,程式只要建置一次,不需要針對個別語言再次建置,網頁伺服器那邊也不需要特別寫設定去處理,而且可以做到動態切換語言。壞處是會有額外的 HTTP 請求,會增加流量。

ngx-translate 額外好的地方是,他提供了相應的工具、plugin,相當的方便。

那麼怎麼使用呢?我推薦看這篇【Angular】ngx-translate 多語系實務應用 ,他這篇的缺點是萃取字串的部分是手動,我建議萃取字串的部分可以用原作者 biesbjergngx-translate-extract  ,就不用自己找字串找的太累。

先進行安裝

npm install @ngx-translate/core —save
npm install @ngx-translate/http-loader --save

然後改 app.module.ts

import { TranslateModule, TranslateLoader, TranslateCompiler } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { TranslateMessageFormatCompiler } from 'ngx-translate-messageformat-compiler';
import { HttpClientModule, HttpClient } from '@angular/common/http';

// … 省略 …

// 這主要是告訴 ngx-translate 翻譯檔該怎麼載入,用 TranslateHttpLoader 是表示以 HTTP 方式去下載、載入
export function HttpLoaderFactory(http: HttpClient) {
  return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}

// … 省略 …

// 設定
const translateConfig = {
  defaultLanguage: 'en-US',  // 預設是英文
  loader: {
    provide: TranslateLoader,
    useFactory: HttpLoaderFactory,  // 前面寫的 Factory
    deps: [HttpClient]
  },
  compiler: {
    provide: TranslateCompiler,
    useClass: TranslateMessageFormatCompiler
  }
};

@NgModule({
  // …
  imports: [
    BrowserAnimationsModule,
    BrowserModule,
    TranslateModule.forRoot(translateConfig),  // 模組帶設定
    // …
  ],
  // …
})

在 HTML 裡,使用

// 方法 1
{{ ‘your_translation_key’ | translate }}

// 方法 2
<div [translate]=“‘your_translation_key’”></div>

// 方法 3,適用於字串要用 HTML
<div [innerHTML]="'HELLO' | translate"></div>

他還可以帶參數,只是這邊我看不太懂,暫時也還用不到。

在程式裡,使用

// 先匯入
import {TranslateService} from '@ngx-translate/core';
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';

// … 省略 …
export class AppComponent implements OnInit {
  l10n__title = '';

  constructor(public translate: TranslateService) {
  }

  ngOnInit(): void {
    const self = this;

    // 讓 ngx-translate-extract 可以抓到字串用的
    _('your_translation_key');

    // 取得字串都要使用 translate.get() 來預先取得
    this.translate.get('your_translation_key').subscribe((res: string) => {
      self.l10n__title = res;
    });
  }
}

這裡要特別說明一點,也是我當初用的時候搞錯的地方,就是上面用到的 your_translation_key 並不是字串,而是你自定義的代碼,裡面有使用 ‘.’ 的時候,在產出的 JSON 裡會變成 nested object ,舉個例子,假設這個 key 是 login.title,那麼 JSON 翻譯檔就會是

{
  "login": {
    "title": ""
  }
}

接下來講 ngx-translate-extract,ngx-translate-extract 的安裝

npm install @biesbjerg/ngx-translate-extract --save-dev

裝好以後,在 package.json 的 scripts 裡加入 (要產生什麼語言,請替換 {en,da,de,fi,nb,nl,sv} 這個字串,以繁體中文來說,是zh-TW或zh-Hant,只有英文跟繁體中文的話,就放 {en-US,zh-TW}。 )

// package.json
...
"scripts": {
  "i18n:init": "ngx-translate-extract --input ./src --output ./src/assets/i18n/template.json --key-as-default-value --replace --format json",
  "i18n:extract": "ngx-translate-extract --input ./src --output ./src/assets/i18n/{en,da,de,fi,nb,nl,sv}.json --clean --format json"
}
...

然後建立存放字串的資料夾:mkdir -p src/assets/i18n,執行 npm run i18n:extract 以後,就可以在 src/assets/i18n 裡看到翻譯檔了。

bfg

因為 git repository 裡有敏感資訊,為了安全起見,必須要把這些敏感資訊移掉。
找到這兩篇:

裡面大同小異,主要提到兩個方法,一個是 git filter-branch,一個是 bfg,這邊只介紹 bfg。 bfg 是一個相對於 git filter-branch 來說簡單、快速的工具,除了可以清理敏感資訊之外,也可以清掉超級大的檔案。

MacOS 的安裝可以用 brew:brew install bfg

使用步驟如下:

  1. 移除掉你要處理的檔案或是先清理掉你要處理的檔案內容,然後提交、推送到 repository 去。這段官方有特別說明 (在 Your current files are sacred… 這個小節),我是已經做完下面步驟,上 gitlab 網頁看檔案怎麼沒消失,花了點時間才在網頁上注意到這段,所以這個步驟要先作!!
  2. 切到另外一個目錄,例如 $HOME/tmp,對 repository 作 mirror,這樣拿下來的檔案結構不是一般的檔案佈局,而是 .git 下的佈局:git clone –mirror your_repository.git ,這時候 $HOME/tmp 下會有一個 your_repository.git
  3. 幾種使用方法:
    • 刪除檔案:bfg –delete-files your_file your_repository.git
    • 移除超過指定大小的檔案:bfg –strip-blobs-bigger-than 50M your_repository.git
    • 清理敏感資訊:先新增一個 passwords.txt,裡面放置要移除的文字,範例可以參考這個 gist ,然後執行 bfg –replace-text passwords.txt your_repository.txt
    • 刪除資料夾:bfg –delete-folders .git –delete-files .git –no-blob-protection your_repository.git
  4. 切到 your_repository.git 下,依照指示執行 git reflog expire –expire=now –all && git gc –prune=now –aggressive
  5. 推送回去:git push
  6. 大功告成

把敏感設定的內容跟 Angular 程式分離

以我用過 Django / Docker 的經驗,一般都是利用環境變數來作分離,程式在執行時,會去讀取環境變數來當作設定,盡量不依靠設定檔,進而達到 12 factor –  config 的要求。

到了 Angular ,就需要做出調整。Angular 是需要先進行建置產出 javascript / html / css ,再把這些產出的檔案放到網頁伺服器,瀏覽器再去下載,所以完全沒辦法使用環境變數作為設定,必須要把設定寫到檔案裡,再做建置。那設定寫到檔案的話,其實我們都知道,這些敏感設定內容不適合放到 git repository,即使是 private repository,這樣會有安全上的風險。那這樣就兩難了啊?

說來很妙,我是覺得官方早就應該有比較好的作法了,但是,並沒有。不管怎麼樣,我還是找到解決方法了,基本上跟我想的也差不多。

這兩篇文章的作法大同小異,這邊作簡單的說明:

  1. 移除 src/environment/environment.ts
  2. 新增一個腳本,這個腳本會讀取環境變數,並產生一個新的 src/environment/environment.ts
  3. 修改 package.json
    1. 在 scripts 裡增加 config,這個 config 會去執行步驟 2 的腳本,用以產生 environment.ts
    2. 修改 scripts 裡的 start / build,在執行原來的指令前,先執行 npm run config 去產生 environment.ts

下面兩個片段是關鍵的改動部分:

// set-env.ts
// 放在專案根目錄下
import { writeFile } from 'fs';
import { name, version } from './package.json';


// Configure Angular `environment.ts` file path
const targetPath = './src/environments/environment.ts';


// 載入 colors 模組,主要是用來顯示
const colors = require('colors');


// 可以把敏感內容放到 .env 檔案裡,nodejs 會把內容讀出來,放到環境變數裡
// 也可以在執行 npm run 之前,先設定好環境變數
const result = require('dotenv').config();
if (result.error) {
  // throw result.error;
}


// 原來 environment.ts 裡的內容,把該替換的,用 template literals 來替換

// https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Template_literals
const envConfigFile = `export const environment = {
  production: ${process.env.PRODUCTION},
  VERSION: '${version}’,
};

`;


// 顯示
console.log(colors.magenta('The file `environment.ts` will be written with the following content: \n'));
console.log(colors.grey(envConfigFile)); writeFile(targetPath, envConfigFile, (err) => {
  if (err) {
    throw console.error(err);
  } else {
    console.log(colors.magenta(`Angular environment.ts file generated correctly at ${targetPath} \n`));
  }
});
// package.json
// 只摘錄重要的部分
{
  “scripts”: {
    "config": "ts-node -O '{\"module\": \"commonjs\"}' ./set-env.ts",
    "start": "npm run config && ng serve",
    "build": "npm run config && ng build”
  }
}

電影流水帳(2020/05/01~2020/05/10)

Emma Stone
Emma Stone,飾演 Zombieland: Double Tap 裡的 Wichita
  • J’ai perdu mon corps (IMDB, Wikipedia),英譯:I lost my body,台譯:隻手探險。
  • Zombieland: Double Tap (IMDB, Wikipedia),台譯:屍樂園-髒比雙拼。

J’ai perdu mon corps

這是部故事很有意思的電影,只是有點小悶。

故事從一隻手開始,這隻手莫名其妙的會動,然後開始他的旅程。在這個旅程中,開始穿插著他主人的故事。原來這隻手的主人 Naoufel 是個孤兒,小時候學過鋼琴,然後因為意外,父母車上吵架發生了車禍,導致 Naoufel 變成了孤兒。後來長大以後,Naoufel 當了披薩外送員,某次外送,他外送到一個女孩子家,可是遲到了,女孩子有點兒生氣,但還是原諒了他,他們透過對講機聊起了天。Naoufel 對這女孩子很有好感,依據聊天的內容,用了點詭計取得了她的名字 – Gabrielle ,以及上班地點。接著 Naoufel 跟蹤 Gabrielle,得知她有個叔叔,她的叔叔似乎需要人手,Naoufel 毛遂自薦,當了她叔叔的學徒,然後跟 Gabrielle 有了進一步的認識以及交往。就在 Naoufel 進一步告訴了 Gabrielle 自己是怎麼接近她以及想要進一步交往時,Gabrielle 拒絕了他。Naoufel 很難過,他去了朋友的舞會,喝的爛醉。隔天早上宿醉的他回到 Gabrielle 叔叔的工廠,要開始工作,可是,意外往往就是這麼發生。宿醉的 Naoufel 一個不小心,讓自己的手被鋸斷了,Naoufel 被送到醫院,這隻手則是意外的動了起來。

這隻手最後是找到了他的主人,只是 Naoufel 已經決定離去,他跑到屋頂去。Gabrielle 來找 Naoufel 時,沒看到 Naoufel ,也跟著到了屋頂,想說會不會在之前告白的地方。雪開始下了,在屋頂除了 Naoufel 蓋的小木屋、覆蓋著雪的屋頂地上的腳印之外,只剩下之前 Naoufel 很珍藏的錄音機。她撿起來邊聽著這錄音,一邊找尋著 Naoufel 。在邊聽的時候,知道了 Naoufel 為什麼一直聽這錄音,同時也聽到了Naoufel 留給她的話。從足跡來看,原本以為 Naoufel 是跳樓自盡,但搭配著錄音機裡的聲音,她知道 Naoufel 是成功的從這屋頂跳到另外一棟大樓去了…

Zombieland: Double Tap

失望的續集電影,故事普普通通。

故事的一開始,Little Rock 厭倦了四人一起的生活,Columbus 向 Wichita 求婚,Wichita 不願意正面回應,就跟 Little Rock 離開了。Wichita 跟 Little Rock 在路上遇到一個男孩子 Berkeley,Little Rock 為了擺脫姊姊,在半夜裡丟下 Wichita ,跟 Berkeley 一起上路走了。隔天醒來的 Wichita 感到錯愕,只能回去找 Columbus 跟 Tallahassee 。被 Wichita 跟 Little Rock 丟下的 Columbus 跟 Tallahassee 去了購物中心,意外遇到一個沒被感染的女孩 Madison ,帶了她回去。Columbus 跟 Madison 發生了關係,回來的 Wichita 感情很受傷,才離開沒兩天,Columbus 居然就跟別的女人上床了,而且還是個傻大姊。

回來的 Wichita 表明了很擔心 Little Rock ,Columbus 跟 Tallahassee 雖然不喜歡之前被丟棄,但基於家人的立場,還是決定幫忙 Wichita,於是四人就出發去找 Little Rock 了。一開始沒多久,Madison 就疑似被僵屍咬到,看起來就是要變成僵屍,Columbus 只能痛下殺手。三人繼續旅程,循著線索去找 Little Rock ,然後他們來到一間擺放著貓王遺物的旅館,在這裡他們遇到 Nevada 。聊了以後,才知道 Nevada 經營著這家旅館,並且保存著貓王的遺物。因為同樣愛貓王的關係,Tallahassee 跟 Nevada 還蠻契合的。隔天,兩個人 Flagstaff 跟 Albuquerque 來找 Nevada,相互打過照面以後,發現這兩個人意外的跟 Columbus 與 Tallahassee 蠻相似的,一個老愛碎碎念,一個愛殺僵屍。這時候僵屍突然殺過來,Flagstaff 跟 Albuquerque 出去殺退僵屍,只是回來時,兩人已經被感染。Nevada 等人只好幹掉 Flagstaff 跟 Albuquerque ,隨後 Columbus, Tallahassee 跟 Wichita 告別 Nevada 繼續上路。上路以後,意外遇到 Madison ,所以 Madison 真的只是過敏,而不是被僵屍咬了。四人再次會合,上路去找 Little Rock。終於,他們根據 Nevada 的線索,找到了 Little Rock 所在地。Little Rock 所到的地方是一群愛好和平的人所居住的村落,所有人都不能帶武器。他們進去想要勸退 Little Rock,可是看來 Little Rock 心意已決。Tallahassee 說既然已經找到 Little Rock,我任務也結束了,那我要離開囉,就獨自開車離去。

Tallahassee 在月夜下孤身一人很愜意的開著車,突然他看到一群僵屍往 Little Rock 的所在地跑去,他覺得大事不妙,就回去示警。四人決定設下陷阱來對付僵屍,經過一番大戰,眼看四人就要命喪僵屍手下,Nevada 開著車過來救了四人。五個人依據僵屍的行動模式重新擬定了作戰策略,跟住在這邊的人一起對抗僵屍,最後打敗了僵屍。

故事裡,有兩個地方讓我覺得很有趣,第一個是喜歡立下規則的 Columbus 居然打破了自己的規則,他沒有殺死 Madison,而是對空鳴槍把她嚇走,不過這也讓他們後續能有交通工具繼續旅程,這或許是編劇想告訴我們的,「有時候打破規則也無妨」。第二個是,兩人遇到跟自己個性相似的人,Tallahassee 是很不爽,而 Columbus 則是有惺惺相惜的感覺,那我呢?我要是看到跟自己相似的人是會生氣?覺得這人不長進?還是會覺得太好了,這樣應該比較好溝通?這倒是蠻值得好好想想的。

Angular i18n

Angular 官方提供的套件以及教學:Angular Internalization (i18n)

文章裡一大堆,簡單整理如下:

  1. 安裝:ng add @angular/localize
    • 這個步驟會幫你在 polyfills.ts 裡加入必要的 import
  2. 使用,這部份分為 HTML 跟程式
    • HTML,在需要多國語言的標籤加入 i18n 的屬性
      • <span i18n>Hello world</span>
      • <span i18n=“@@span_hello”>Hello world</span> ,用 i18n=“@@span_hello” 的好處是,產出的翻譯檔裡不會是一個隨意的數字,而是一個對開發者來說比較明確的名稱。
      • <input i18n-placeholder> 這個是有些屬性本身需要多國語言的,就在前面加上 “i18n-“
    • 程式,字串前方加上 $localize ,並且改用 back quote,例如:$localize`hello world`
  3. 萃取,用 ng xi18n –output-path src/i18n 就可以把 HTML 裡有標 i18n 的字串萃取到 src/i18n/messages.xlf 裡
  4. 翻譯,先把上個步驟取得的 messages.xlf 複製為 messages.zh_Hant.xlf ,再去編輯。這邊提供一個簡易的網頁工具 – tiny-translator,在處理上會方便很多。開啟以後,要先建立專案,然後上傳 .xlf 檔案,接著就可以進行翻譯了。翻譯好,再下載下來即可。
  5. 合併,程式開發中難免會有增刪,每次用 xi18n 基本上都是重新萃取一次,等於是又要再搞一次合併的功夫,這太累。tiny-translator 的作者有提供另外一個工具 – xliffmerge 
    • 安裝:npm install -g ngx-i18nsupport
    • 在 package.json 的 scripts 裡加入 “extract-i18n”: “ng xi18n –output-path src/i18n && xliffmerge –profile xliffmerge.json en de” ,裡面的 en, de 等 locale 請依照自己的需求作調整
    • 新增 xliffmerge.json ,這個檔案請參考後面。
    • 使用 npm run extract-i18n 就可以自動萃取字串並且作合併了。
  6. 專案的建置,主要是修改 angular.json,有三個部分:
    • projects / your_project_name 加入 “i18n”: {“sourceLocale”: “en”, “locales”: {“de”: “src/i18n/messages.de.xlf”}}
    • projects / your_project_name / architect / build / configurations 裡加入 “de”: {“localize”: [“de”]}
    • projects / your_project_name / architect / serve / configurations 裡加入 “de”: {“browserTarget”: “ng-hosting:build:de”}
  7. 要執行 ng build / ng serve 時,就可以用
    • ng build –configuration=production,de
    • ng serve –configuration=de
  8. 補充一個我覺得很重要的部分,就是一個語言要建置一次,所以一般的佈署會是這樣的,建置好 zh ,放在 zh/ 目錄下,建置好 de,放在 de/ 目錄下,然後在 nginx/apache 的設定裡去依照 header 的 language 去導向到對應的目錄去。這篇 Deploying an i18n Angular app with angular-cli 的後面有教怎麼去設定 apache / nginx。

看到這邊,你可能會想,那程式裡標上 $localize 的字串呢?嗯,ng xi18n 並不會把這些字串萃取出來 (issue),所以這部份得自己手動處理 😣 

P.S. 用 ngx-i18nsupport 的 tooling 可以把上面講的簡化掉,像是加入 npm package、在 package.json 加入 extract-i18n 、在 angular.json 加入設定等等,一次就搞定了,我是已經用了才看到這個 tooling ,有點相見恨晚。

// xliffmerge.json
{
  "xliffmergeOptions": {
    "srcDir": "src/i18n",
    "genDir": "src/i18n",
    "i18nFile": "messages.xlf",
    "i18nBaseFile": "messages",
    "i18nFormat": "xlf",
    "encoding": "UTF-8",
    "defaultLanguage": "en",
    "languages": ["en", "de"],
    "removeUnusedIds": true,
    "supportNgxTranslate": false,
    "ngxTranslateExtractionPattern": "@@|ngx-translate",
    "useSourceAsTarget": true,
    "targetPraefix": "",
    "targetSuffix": "",
    "beautifyOutput": false,
    "preserveOrder": true,
    "allowIdChange": false,
    "autotranslate": false,
    "apikey": "",
    "apikeyfile": "",
    "verbose": false,
    "quiet": false
  }
}

電影流水帳(2020/04/11~2020/04/30)

Golshifteh Farahani ,驚天營救裡的 Nik,很酷。
  • Extraction (IMDB, Wikipedia),台譯:驚天營救。
  • The Nightmare Before Christmas (IMDB, Wikipedia),台譯:聖誕驚魂夜。

Extraction

昨天晚上看完驚天營救,這是在臉書社團看到大家推薦才看的。

電影的內容是描述傭兵Tyler 受雇去拯救一個毒梟的兒子所經歷的過程。

Tyler 心裡是有缺憾的,他的孩子生了淋巴癌,但他不忍心看著兒子在眼前死去而去出任務,後來他生了淋巴癌的兒子走完人生最後一段,所以 Tyler 心裡一直耿耿於懷,這也是後來他為什麼知道沒有錢之後,還願意繼續拯救這個孩子的原因。

劇情還蠻簡單的,A毒梟跟B毒梟為了搶地盤,B毒梟就把A毒梟兒子綁架走,A毒梟在監獄裡,又沒有足夠的錢,只要自己的手下 Saju 去救人。Saju 無奈,只能找傭兵來幫忙,然後再半途搶人,翻臉不付錢。B毒梟掌控了整個城市跟警察,在他得知 Tylor 救出那個孩子後,就封鎖了整個城市。Tylor 在逃出的過程裡面是非常驚險的,不但被 Saju 追,也被警察追殺。在封城的情況下,Tylor 跟那個孩子幾乎已經被困住了。後來他請 Nik 找了以前欠他人情的朋友來幫他逃出去,Tylor 的朋友知恩圖報,在整個城市被封鎖的情況下還願意來幫他。只是,Tylor 的朋友已經跟 B 毒梟談好條件,只要孩子交出來,他跟 Tylor 都能全身而退。Tylor 不願意,兩人打鬥了起來,聽到打鬥聲的孩子出來拿了槍,殺了 Tylor 的朋友。這下可好,該怎麼殺出重圍呢?Tylor 找了 Saju 來幫忙,然後聯絡了 Nik ,在隔天一早,他們以聲東擊西的方式,試圖殺出重圍。經過一番血戰,Tylor 跟 Saju 將孩子交到 Nik 手上,Saju 戰死,Tylor 力盡掉到河裡去。

八個月之後,酷酷的女主角出現在廁所裡,把 B 毒梟幹掉了。然後場景帶到孩子去游泳,孩子學男主角一樣潛在水裡想事情,浮上來的時候看到了男主角模糊的身影,所以最後 Tylor 活著還是死了呢?如果活著,為什麼 Nik 還要去殺死 B 毒梟?如果死了,孩子又怎麼會看到這模糊的身影呢?

The Nightmare Before Christmas

很久以前的片了,從 Netflix 上了這片以後,就一直掛在「我的片單」裡,終於找時間看完它。

Jack 是萬聖小鎮上的搞怪王,但是長期都做同樣的事情以後,他覺得乏了,想找些新花樣。他出鎮外去逛,逛啊逛,發現了聖誕小鎮,知道了聖誕老人跟聖誕節,他非常的開心,趕緊回到鎮上講這件事情,要大家進行準備,期望能取代聖誕節。大家非常樂意的配合,只是,方向偏了,大家準備的東西,像是禮物等等的,跟 Jack 想的不一樣。時間過的很快,轉眼就到聖誕節了。Jack綁架了聖誕老人,開心的帶著萬聖小鎮大夥製作的禮物,坐上雪橇挨家挨戶去送禮物。只是,這禮物太可怕了,人類沒辦法接受,於是軍隊發射了炮火,要把 Jack 打下來。Jack 有些吃驚,本來以為大家是在放煙火,後來才知道,人類並不歡迎這樣的禮物。被炮火打下來以後,他發現自己錯了,還是應該找回聖誕老人,就回去小鎮。被 Jack 綁架的聖誕老人被囚禁在 Oogie Boogie 那邊,暗地裡愛慕 Jack 的 Sally 一直默默的支持 Jack,在 Jack 出發去送禮物後,她感覺事情不對,跑去 Oogie Boogie 那邊找原來的聖誕老人,哪知道自己也陷入了險境。趕回來的 Jack ,看到這樣的景況,打敗了 Oogie Boogie,救出聖誕老人以及 Sally,然後將聖誕老人送回聖誕小鎮,進而拯救了因為自己而崩壞的聖誕節。

我還滿喜歡 Tim Burton 拍的這類電影,像是剪刀手愛德華、陰間大法師、斷頭谷、怪奇孤兒院等,都是我蠻喜歡的電影。剛剛查維基百科時,蠻驚訝的是,2001 年的星球崛起也是他拍的啊。

螢幕錄製與字幕合成

螢幕錄製的軟體很多,像是 FonePaw、Soundflower、ApowerREC、Nonosnap、Kap、Camtasia Studio、iShowU HD 等等的,我是使用 Quicktime player 。

執行 Quicktime palyer 以後,會要你選存放位置,我是不管他,按「取消」。用滑鼠點按「檔案」 > 「新增螢幕錄製」,接著畫面會出現一個工具列視窗,請先選定錄製全螢幕或是錄製指定區域,然後再按下選項:

  • 勾選使用 MacBookPro 的麥克風
  • 勾選滑鼠點按

這邊可以加選計時器,這可以讓錄製的動作在指定秒數後開始,按下錄製就可以開始錄製影片了。錄製的時候,要注意找個比較安靜的地方,說話的聲音也要提高音量,這樣錄製的效果會比較好。

錄製完成以後,可以使用 iMovie 編輯,在 iMovie 裡可以加入過場動畫,調整音量,也可以逐句上字幕。調整音量是點按上方選單的「修改」> 「增強」

因為我懶得逐句上字幕,所以使用別的方式。首先先把製作好的影片匯出,先回到主畫面的計劃案,點選你計劃案右下角的 「…」,選「分享計劃案」>「檔案」,這樣就可以把檔案匯出到指定資料夾去了。

再來是抽取出音訊,先安裝 ffmpeg :brew install ffmpeg

在終端機輸入 ffmpeg -i xxx.mp4 xxx.mp3

要調整聲音的話,也可以在這邊作(參考自ffmpeg的wiki):ffmpeg -i input.wav -filter:a “volume=10dB” output.wav

取出以後,再來是試著從音訊轉出字幕,我試了兩個方法,第一個方法是用布丁大大web speech to text ,這有蠻多教學文章的:

在這邊就可以上傳音訊檔案,然後播放時,就會自動轉出字幕,這邊由於是利用到播放跟麥克風,所以在轉換時,一樣需要一個比較安靜的地方,同時音訊檔的音量也需要大一點,轉出來效果會比較好。

第二個方法是自己手打,因為環境有點吵雜的關係,我後來是用這方法。這邊是用了 oTranscribe 這個網站,一樣是要上傳音訊,oTranscribe 提供了便利的介面,讓你可以邊聽邊打。完成以後,可以匯出文字檔。這邊大致講一下怎麼使用:

  • ESC: Play/Pauser
  • F1: 倒轉
  • F2: 快轉
  • Ctrl +j (在Mac是用Command+j) : 插入時間

如果你需要教學的話,這裡有:oTranscribe 線上逐字稿工具,支援讀取電腦影片製作字幕

取得文字檔以後,還需要轉換為通用的 srt 檔案,這一樣有網站提供這功能: Subtitle tools – Convert Subtitles to Srt

將剛剛取得的文字檔上傳上去,按下「Convert to Srt」,然後就可以下載了。

最後是將字幕合併到影片裡。

  1. 安裝 handbrake: brew cask install handbrake
  2. 從啟動台找到 HandBrake,執行
  3. 選擇前面匯出的 mp4,再選擇 “Subtitles” 頁籤,點按「Tracks」> 「Add External Subtitles track」加入前面取得的 .srt 檔案
  4. 調整字幕的語言,再勾選 Burned In 
  5. 在下方填入輸出的檔名,並選擇要輸出的資料夾。
  6. 按下上方的「Start」

需要教學的話,一樣有:SRT 字幕筆記

這樣就完成啦。

其他的參考資料:

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)}`);