2018年1月29日

Vagrant

Vagrant 是一個管理 Virtual Machine 的 command line utility 工具。最初支援 VirtualBox,但現在也支援了 docker、Parallels、VMWare。

安裝

先安裝 VirtualBox: Download VirtualBox

然後在 Download Vagrant,根據不同 OS 下載 binary 套件直接安裝就好了。

vagrant CLI

vagrant 本身是一個 CLI tool,目的是為了能在 script 直接操作並使用 VM,能夠讓系統安裝及測試自動化。

如果需要一個的 CentOS 7 VM 可以用以下指令處理,vagrant 有個集中分享的 Vagrant Cloud repository,我們可以直接搜尋centos7 找到適合與最多人使用的 VM。

我們使用 geerlingguy/centos7 Vagrant box VM

mkdir vagrant_centos7
cd vagrant_centos7

# 以 init 指令下載 centos 7 Vagrantfile
vagrant init geerlingguy/centos7

# 啟動 VM
vagrant up

# VM 關機
vagrant halt

# 刪除 VM
vagrant destroy

# 查詢 VM 狀態
vagrant status

# 查看 ssh 資訊
vagrant ssh-config

其他常用的指令

# 列出所有 vagrant VMs
vagrant box list

# guest OS 及 host OS 的 port 對應表
vagrant port
    22 (guest) => 2222 (host)

# 以 ssh 登入 VM
vagrant ssh

Vagrantfile

剛剛使用的 CentOS 7 VM Vagranfile source code 也是透過定義 Vagrantfile 再分享到 Cloud。

而我們的 Vagrantfile 只需要這樣的設定,就能夠引用 geerlingguy 的 centos7

# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure("2") do |config|
  config.vm.box = "geerlingguy/centos7"
end

被引用的 Vagratfile 定義會被下載到 ~/.vagrant.d 這個目錄中,可在 boxes 目錄找到相關資訊

~/.vagrant.d/boxes/geerlingguy-VAGRANTSLASH-centos7

Vagrantfile 是採用 ruby 語法,當我們在某個目錄 (/Users/user/VirtualBoxVMs/vagrant_centos7/) 執行 vagrant 指令時,他會依照以下順序尋找 Vagrantfile

/Users/user/VirtualBoxVMs/vagrant_centos7/Vagrantfile
/Users/user/VirtualBoxVMs/Vagrantfile
/Users/user/Vagrantfile
/Users/Vagrantfile
/Vagrantfile

  • Configuration Version

Vagrant.configure 後面的數字,代表不同版本的 vagrant 語法,

  1. "1" 表示為 1.0.x 的語法
  2. "2" 表示為 1.1+ 到 2.0 版的語法
Vagrant.configure("2") do |config|
  # ...
end

  • Networking

VM 的網路設定決定了 VM 跟 host machine 的溝通介面,網路部分分為三種,Forwarded Ports、Private Network及 Public Network。

Forwarded Ports: forwarded_port

可開放 guest machine 的某個 Port,並轉換到 host machine 的另一個 Port,TCP 或 UDP 都可以。

guest machine 啟動了一個 web server,運作在 TCP Port 80,可 forward 到 host machine 的 TCP 8080

config.vm.network "forwarded_port", guest: 80, host: 8080

跟上面一樣,將 TCP 80 forward 到 TCP 8080,同時限制只能用 127.0.0.1 存取 8080 這個 forwarded port 設定,也可以加上 TCP/UDP 的 protocol 限制,預設為 TCP。

config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1"

config.vm.network "forwarded_port", guest: 80, host: 8080, host_ip: "127.0.0.1", protocol: "tcp"

Private Network

可透過 Internet 無法使用的 IP 存取 guest machine,在 VM 的網路設定通常稱為 NAT

  1. DHCP

    config.vm.network "private_network", type: "dhcp"
  2. Static IP

    config.vm.network "private_network", ip: "192.168.50.4"
  3. IPv6

config.vm.network "private_network", ip: "fde4:8dba:82e1::c4"

# 加上 netmask
config.vm.network "private_network",
    ip: "fde4:8dba:82e1::c4",
    netmask: "96"

Public Network

在 VM 的網路設定通常稱為 Bridge network,就是讓 VM 直接取得可讓其他機器存取的網路設定。

  1. DHCP

    config.vm.network "public_network"
    
    # 使用 DHCP 的設定作為 default route
    config.vm.network "public_network",
      use_dhcp_assigned_default_route: true
  2. Static IP

    config.vm.network "public_network", ip: "192.168.0.17"

    如果 host machine 有多個網路介面,在啟動 vagrant VM 時,會詢問要使用哪一個網路介面,可以在設定 Public Netork 時,直接決定是用哪一個網路介面。

config.vm.network "public_network", bridge: "en1: Wi-Fi (AirPort)"

# 可指定多個網路介面
config.vm.network "public_network", bridge: [
  "en1: Wi-Fi (AirPort)",
  "en6: Broadcom NetXtreme Gigabit Ethernet Controller",
]

可利用 shell 指令設定 ip

Vagrant.configure("2") do |config|
  config.vm.network "public_network", auto_config: false

  # manual ip
  config.vm.provision "shell",
    run: "always",
    inline: "ifconfig eth1 192.168.0.17 netmask 255.255.255.0 up"

  # manual ipv6
  config.vm.provision "shell",
    run: "always",
    inline: "ifconfig eth1 inet6 add fc00::17/7"
end

設定固定 IP 以及 default route

Vagrant.configure("2") do |config|
  config.vm.network "public_network", ip: "192.168.0.17"

  # default router
  config.vm.provision "shell",
    run: "always",
    inline: "route add default gw 192.168.0.1"

  # default router ipv6
  config.vm.provision "shell",
    run: "always",
    inline: "route -A inet6 add default gw fc00::1 eth1"

  # delete default gw on eth0
  config.vm.provision "shell",
    run: "always",
    inline: "eval `route -n | awk '{ if ($8 ==\"eth0\" && $2 != \"0.0.0.0\") print \"route del default gw \" $2; }'`"
end

  • Synced Folders

可在 guest 及 host machine 之間共用資料夾,vagrant 預設會分享 project 目錄,也就是存放 Vagrantfile 的目錄到 /vagrant。

# 前面是 host machine folder,後面是 guest machine path
config.vm.synced_folder "src/", "/srv/website"

# disable default /vagrant shared folder
config.vm.synced_folder ".", "/vagrant", disabled: true

# 修改 folder owner, group
config.vm.synced_folder "src/", "/srv/website",
  owner: "root", group: "root"

也可以使用 NFSRSync或是 SMB 這三種 protocol


  • Provisioning

這是在啟動 VM 時,自動安裝軟體、修改設定的功能。當 VM box 需要微調時,可以利用這個功能進行調整。Provisioning 可使用 shell script, ansible, chef, puppet, salt, docker 等指令,比較基本的是直接用 shell scripts

Provisioning 會在這三個時間點發生作用

  1. 第一次以 vagrant up 啟動 VM,Provisioning 會有作用,但如果是已經啟動過的 VM,就不會執行 provisioning,但可用 --provision 強制執行。

  2. 在 VM 運作中,執行 vagrant provision

  3. 執行 vagrant reload --provision


Vagrant Shell provisioner 可在 guest machine 執行某個 script。

Inline Scripts

直接在 Vagrantfile 撰寫 scipt command

Vagrant.configure("2") do |config|
  config.vm.provision "shell",
    inline: "echo Hello, World"
end

在上面定義 script,然後在 config.vm.provision 中執行

$script = <<SCRIPT
echo I am provisioning...
date > /etc/vagrant_provisioned_at
SCRIPT

Vagrant.configure("2") do |config|
  config.vm.provision "shell", inline: $script
end

External Script

可執行 host machine 的某一個 shell script,也可以用某個網址下載 script

Vagrant.configure("2") do |config|
  config.vm.provision "shell", path: "script.sh"
end

Vagrant.configure("2") do |config|
  config.vm.provision "shell", path: "https://example.com/provisioner.sh"
end

如果要執行 guest machine 的 script

Vagrant.configure("2") do |config|
  config.vm.provision "shell",
    inline: "/bin/sh /path/to/the/script/already/on/the/guest.sh"
end

Script Arguments

利用 args 指定 script 參數

Vagrant.configure("2") do |config|
  config.vm.provision "shell" do |s|
    s.inline = "echo $1"
    s.args   = "'hello, world!'"
  end
end

Vagrant.configure("2") do |config|
  config.vm.provision "shell" do |s|
    s.inline = "echo $1"
    s.args   = ["hello, world!"]
  end
end

  • Multi-Machines

Multi-Machines 功能可在一個 Vagrantfile 中定義多個 guest machines。

定義兩個機器,一台是 web,一台是 db

Vagrant.configure("2") do |config|
  config.vm.provision "shell", inline: "echo Hello"

  config.vm.define "web" do |web|
    web.vm.box = "apache"
  end

  config.vm.define "db" do |db|
    db.vm.box = "mysql"
  end
end

執行 vagrant up 會同啟動 web 及 db,也可以用 vagrant up web 只啟動 web

Reference

Vagrant Tutorial(1)雲端研發人員,你也需要虛擬機!

Vagrant Tutorial(2)跟著流浪漢把玩虛擬機

Vagrant Tutorial(3)細說虛擬機生滅狀態

Vagrant Tutorial(4)虛擬機,若即若離的國中之國

Vagrant Tutorial(5)客製化虛擬機內容的幾種方法

使用Vagrant進行伺服器環境部署

2018年1月22日

Ansible

目前在 DevOps 設定管理的工具中,以 Puppe、Chef、Salt、Ansible 四者最有名,而 Puppe 跟 Chef 兩者是以 ruby 開發, Salt 與 Ansible 都是以 python 開發的,在 20162017 DevOps 的統計中,Ansible 自 2015 由 10% 上升至 20% 及 21%,chef 及 puppet 在 2017 的比例有下降一些,但還是比較多人使用的工具。

Ansible is Simple IT Automation 最簡單的 IT 自動化工具,包含 自動化部署APP、自動化管理配置、自動化的持續交付、自動化的(AWS)雲服務管理。ansible 是利用 paramiko 開發的,paramiko是一個純Python實現的ssh協議庫。因此不需要在遠端主機上安裝client或agents,因為 ansible 單純地是以 ssh 來和遠程主機進行通訊。

在 Ender's Game 裡面,Ender 是利用一個的超光速即時的通訊系統,在後端指揮中心,遠端下達指令給前方的戰機及戰艦,這個系統被稱為 Ansible,用途是 instantaneous communication across any distance。因此 Ansible 就跟 devops 的目標一致,這樣的工具就是要讓隱身在後面的工程師,個個都像 Ender 一樣,一呼百應而且沒有絲毫的遲疑,當然瞬間的成敗也由 Ender 獨力承擔。

安裝

ansible 對於管理端的主機,稱為 control machine,必須要安裝 python 2.6+,被管理(託管)的主機,稱為 managed node,要安裝 sshd,也要安裝 python 2.6+。

在 macos 安裝 ansible 可使用 macports 或是 homebrew

sudo port install ansible

當然也可以因應不同 OS 用 yum, apt, pip, deb 進行安裝

安裝測試環境

可用 VM 來進行測試,目前可選用 virtualbox 或是 docker

  • vagrant

必須安裝 vagrant 及 virtualbox,可選用 vagrant 官方提供的 VM

mkdir vagrant_centos7
cd vagrant_centos7

vagrant init geerlingguy/centos7
vagrant up
# 關機
vagrant halt

# 重開機
vagrant reload

# ssh
vagrant ssh

# 移除
vagrant destory

取得 VM 的 ssh 設定

$ vagrant ssh-config
Host default
  HostName 127.0.0.1
  User vagrant
  Port 2222
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /Users/charley/VirtualBoxVMs/vagrant/.vagrant/machines/default/virtualbox/private_key
  IdentitiesOnly yes
  LogLevel FATAL

ansible.cfg 設定檔

$ vi ansible.cfg
[defaults]

inventory = hosts
remote_user = vagrant
private_key_file = .vagrant/machines/default/virtualbox/private_key
host_key_checking = False

ansible 會依照這四個順序尋找適合的 ansible.cfg

* ANSIBLE_CONFIG 環境變數
* 目前工作目錄的 ansible.cfg
* 使用者 Home 目錄的 .ansible.cfg
* 系統設定 /etc/ansible/ansible.cfg (如果是用macports 安裝則是在 /opt/local/etc/ansible/ansible.cfg)

hosts 設定檔

$ vi hosts
server1  ansible_ssh_host=127.0.0.1  ansible_ssh_port=2222

[local]
server1

利用 ansible echo 一段文字

$ ansible all -m command -a 'echo Hello World on Vagrant.'
server1 | SUCCESS | rc=0 >>
Hello World on Vagrant.
  • docker

準備一個有安裝了 sshd 的 docker VM,將 10022 對應到 TCP 22 (ssh),因為 CentOS 7 預設會安裝 python 2.7.5,就不需要處理 python 的安裝問題。

docker run -d \
 -p 10022:22\
 --sysctl net.ipv6.conf.all.disable_ipv6=1\
 -e "container=docker" --privileged=true -v /sys/fs/cgroup:/sys/fs/cgroup --name atest centosssh /usr/sbin/init

如果直接用 ssh client 連線

ssh root@localhost -p 10022

在另一個新的工作目錄,建立 ansible.cfg 設定檔

$ vi ansible.cfg
[defaults]

inventory = hosts
remote_user = root
host_key_checking = False

建立 hosts 設定檔

$ vi hosts
server1  ansible_ssh_host=127.0.0.1  ansible_ssh_port=10022 ansible_ssh_pass=max168kit

[local]
server1

測試 echo 指令

$ ansible all -m command -a 'echo Hello World on Docker.'
server1 | SUCCESS | rc=0 >>
Hello World on Docker.

使用 ansible

有兩種方式使用 ansible,分別是 ad-hoc command 及 playbook

  • ad-hoc command

一次只能使用一個指令,像是在 console mode,一次下達一個指令

$ ansible all -m ping
server1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

$ ansible all -m command -a "echo Hello World"
server1 | SUCCESS | rc=0 >>
Hello World
  • playbook

像是 shell script 一樣,可組合多個指令,這是一份使用 YAML 格式製作的文件。通常一個 playbook 中會有多個 Play, Task, Module

  1. Play: 某個特定的工作,裡面會包含多個 Task

  2. Task: 為了實現 Play,每一個 Play 都會有一個 Task 列表,每一個 Task 在所有對應的主機上都執行完成後,才會進行下一個 Task

  3. Module: 每一個 Task 的目的是執行一個 Module,Module 也就是 ansible 提供的一些操作指令,可到 Module Index 文件查詢可以使用的 Modules

以下是一個簡單的 hello world playbook,裡面有一個 Play: say 'hello world',兩個 Tasks,第一個 Task 使用了 command 這個 Module,第二個 Task 使用 debug

vi hello_world.yml

---

- name: say 'hello world'
  hosts: all
  tasks:

    - name: echo 'hello world'
      command: echo 'hello world'
      register: result

    - name: print stdout
      debug:
        msg: "{{ result.stdout }}"

執行 playbook

$ ansible-playbook hello_world.yml

PLAY [say 'hello world'] ***************************************************************************

TASK [Gathering Facts] *****************************************************************************
ok: [server1]

TASK [echo 'hello world'] **************************************************************************
changed: [server1]

TASK [print stdout] ********************************************************************************
ok: [server1] => {
    "msg": "hello world"
}

PLAY RECAP *****************************************************************************************
server1                    : ok=3    changed=1    unreachable=0    failed=0

常用的 Modules

- name: install the latest version of Apache
  yum:
    name: httpd
    state: latest

- name: remove the Apache package
  yum:
    name: httpd
    state: absent

- name: upgrade all packages
  yum:
    name: '*'
    state: latest

- name: upgrade all packages, excluding kernel & foo related packages
  yum:
    name: '*'
    state: latest
    exclude: kernel*,foo*

- name: install the 'Development tools' package group
  yum:
    name: "@Development tools"
    state: present
  • command 遠端執行某個 shell command,不支援變數 > , >, |, ; 和 & ,如果需要這些功能,要改用 shell
- name: Reboot at now
  command: /sbin/shutdown -r now
  
- name: create .ssh directory
  command: mkdir .ssh creates=.ssh/

- name: cat /etc/passwd
  command: cat passwd
  args:
    chdir: /etc


- name: check files number
  shell: ls /home/docker/ | wc -l

- name: kill all python process
  shell: kill -9 $(ps aux | grep python | awk '{ print $2 }')
  • copy 將 local file 傳送到遠端機器
- name: copy ssh public key to remote node
  copy:
    src: files/id_rsa.pub
    dest: /home/docker/.ssh/authorized_keys
    owner: docker
    group: docker
    mode: 0644
    
- name: copy nginx vhost and backup the original
  copy:
    src: files/ironman.conf
    dest: /etc/nginx/sites-available/default
    owner: root
    group: root
    mode: 0644
    backup: yes
  • file 遠端建立和刪除檔案、目錄、links
- name: touch a file, and set the permissions
  file:
    path: /etc/motd
    state: touch
    mode: "u=rw,g=r,o=r"

- name: create a directory, and set the permissions
  file:
    path: /home/docker/.ssh/
    state: directory
    owner: docker
    mode: "700"

- name: create a symlink file
  file:
    src: /tmp
    dest: /home/docker/tmp
    state: link
  • lineinfile 可用正規表示式對檔案進行插入或取代文字,類似 sed
- name: remove sudo permission of docker
  lineinfile:
    dest: /etc/sudoers
    state: absent
    regexp: '^docker'

- name: set localhost as 127.0.0.1
  lineinfile:
    dest: /etc/hosts
    regexp: '^127\.0\.0\.1'
    line: '127.0.0.1 localhost'
    owner: root
    group: root
    mode: 0644
- name: start nginx service
  service:
    name: nginx
    state: started

- name: stop nginx service
  service:
    name: nginx
    state: stopped

- name: restart network service
  service:
    name: network
    state: restarted
    args: eth0    
  • stat 檢查檔案狀態
- name: check the 'vimrc' target exists
  stat:
    path: /home/docker/.vimrc
  register: stat_vimrc

- name: touch vimrc
  file:
    path: /home/docker/.vimrc
    state: touch
          mode: "u=rw,g=r,o=r"
  when: stat_vimrc.stat.exists == false

- name: Use md5sum to calculate checksum
  stat:
    path: /path/to/something
    checksum_algorithm: md5sum

References

Red Hat併購DevOps新秀Ansible

YAML wiki

Ansible 自動化部署工具

Chef 自動化部署工具

現代 IT 人一定要知道的 Ansible 自動化組態技巧

ansible Getting Started

Ansible中文權威指南

七分鐘掌握 Ansible 核心觀念

Ansible for Devops

自動化工具——ansible中文指南

2018年1月15日

Typescript

TypeScript 是在 MS 工作的 Anders Hejlsberg (C#, TurboPascal 之父)提出的一個新的程式語言,不過他並不是一個無中生有的語言,TypeScript 是 JavaScript ES5 及 ES6 的 superset,可以跟既有的 JavaScript 程式完全相容,他主要是將若資料型別的 JavaScript 轉變為強資料型別的程式語言,在開發及編譯時,就能夠察覺一些程式語法的錯誤,同時增加物件導向的概念,它可以幫助 JavaScript 開發人員更容易撰寫及維護大規模的應用程式。

安裝

TypeScript 的編譯器 (tsc) 可透過 npm 安裝,另外 tsserver 是 node 執行檔,包含了 TypeScript 編譯器及 language service,介面為 JSON protocol,適用於 editors 及 IDE。

$ sudo npm install -g typescript
/opt/local/bin/tsc -> /opt/local/lib/node_modules/typescript/bin/tsc
/opt/local/bin/tsserver -> /opt/local/lib/node_modules/typescript/bin/tsserver
+ typescript@2.6.1
added 1 package in 2.927s

測試:建立一個新的 greeter.ts 檔案

function greeter(person) {
    return "Hello, " + person;
}

let user = "Jane User";

document.body.innerHTML = greeter(user);

透過 tsc 將 greeter.ts 編譯為 greeter.js

tsc greeter.ts

編譯後的 greeter.js 可在 console 用 nodejs 執行,或是放在一個網頁 greeter.html 裡面

$ node greeter.js
Hello, Jane User

greeter.html

<!DOCTYPE html>
<html>
    <head><title>TypeScript Greeter</title></head>
    <body>
        <script src="greeter.js"></script>
    </body>
</html>

開發的 IDE

雖然 Anders Hejlsberg 在官方網頁告訴我們要使用 Visual Studio plugin,但我們還是別的選擇

TypeScript Handbook

翻閱 TypeScript Handbook 手冊會發現,文件的編排方式,就像是在說明一個物件導向的程式語言一樣。

從基本的資料型別 Basic Types 開始,然後說明如何宣告變數,再來就是物件導向的核心: Interface, Classes 及 Functions,最後是 module 與 namespace。

Basic Types

Boolean, Number, String, Array, Tuple, Enum, Any, Void, Null and Undefined, never

// boolean
let isDone: boolean = false;

// number
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

// string  " 或是 ' 都可以
let color: string = "blue";
color = 'red';

特殊的 template strings `

let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ fullName }.

I'll be ${ age + 1 } years old next month.`;

// 等同

let sentence: string = "Hello, my name is " + fullName + ".\n\n" +
    "I'll be " + (age + 1) + " years old next month.";

Array,有兩種宣告方式

let list: number[] = [1, 2, 3];

let list: Array<number> = [1, 2, 3];

Tuple: 就是固定 elements 個數的 array,且各元素的資料型別要相同

// Declare a tuple type
let x: [string, number];

// Initialize it
x = ["hello", 10]; // OK

// Initialize it incorrectly
// error TS2322: Type '[number, string]' is not assignable to type '[string, number]'. Type 'number' is not assignable to type 'string'.
x = [10, "hello"]; // Error

Enum

// 預設第一個元素,數字為 0
enum Color {Red, Green, Blue}
let c: Color = Color.Green;

let colorName: string = Color[2];
alert(colorName); // Displays 'Blue' as it's value is 2 above

// 可強制設定 enum numbers
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

Any: 宣告變數時,不知道這個變數的資料型別是什麼,可以任意變換自己的資料型別

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean

類似 Object 的功能,但 Object 只能讓我們指定 value,不能使用該 value 資料型別的任何一個 functions

let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'

Void: 通常用在表示 function 沒有 return value

function warnUser(): void {
    alert("This is my warning message");
}

// 將變數宣告為 void,只能設定為 undefined 或是 null
let unusable: void = undefined;

Null and Undefined: 預設 null 及 undefined 是所有其他資料型別的 subtypes,換句話說,可以將 undefined 指定給 number,但如果編譯時加上 --strictNullChecks,就可以限制只能將 null 及 undefined 指定給 void

// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;

Never: 代表 type of values that never occur,例如可以設定某個只會 throw exception 的 function 的 return value 為 never。他是獨立的,不是任何一種資料型別的 subtype,即使是 any 也不能指定給 never 型別的變數。

// Function returning never must have unreachable end point
function error(message: string): never {
    throw new Error(message);
}

// Inferred return type is never
function fail() {
    return error("Something failed");
}

// Function returning never must have unreachable end point
function infiniteLoop(): never {
    while (true) {
    }
}

Type assertions: 利用 compiler 檢查(確認) 資料型別,有兩種寫法 (someValue) 或是 (someValue as string)

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;

let someValue2: any = "this is a string";

let strLength2: number = (someValue as string).length;
Variable Declaration

let 與 var 的差異

JavaScript Hositing: 在 JavaScript,變數可在使用後才被宣告,換句話說,變數可在宣告前,就使用它。在執行時期時,所有var變數都會自動被hoisting。如果程式中有參考到使用var定義過的變數時,會變成undefined,不會產生ERROR。

但 let 宣告的變數,不會被 Hositing,他只能作用在 { } 區塊範圍中。若程式中有參考到let定義過的變數時,因作用區塊不同會產生ERROR,此行為比較接近常用的程式語言寫法。

在 for 裡面,i 會持續被重新定義,所以在 setTimeout 後,最後使用的 i 都會是 5

for (var i = 0; i < 5 ; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}
$ node greeter.js
5
5
5
5
5

let 的變數不會被重新宣告,能夠維持 i 的實際變數 value

for (let i = 0; i < 5 ; i++) {
    setTimeout(function() { console.log(i); }, 100 * i);
}
$ node greeter.js
0
1
2
3
4

const: 不能被 re-assign 的變數

const numLivesForCat = 9;
const kitty = {
    name: "Aurora",
    numLives: numLivesForCat,
}

// Error,以 const 宣告的 kitty 是一個有 readonly 屬性的object
kitty = {
    name: "Danielle",
    numLives: numLivesForCat
};

// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
Functions

typescript 跟 javascript 一樣,可以建立 named 及 anonymous 兩種 functions

// Named function
function add(x, y) {
    return x + y;
}

// Anonymous function
let myAdd = function(x, y) { return x + y; };

爲 function 參數及 return value 加上 data type

function add(x: number, y: number): number {
    return x + y;
}

let myAdd = function(x: number, y: number): number { return x + y; };

增加 function type,下面的 (x: number, y: number) => number 就是 myAdd 的 function type,function type 裡面的變數名稱只是輔助使用幫助閱讀而已,實際上跟後面的程式本體沒有關係。

let myAdd: (x: number, y: number) => number =
    function(x: number, y: number): number { return x + y; };
    
let myAdd: (baseValue: number, increment: number) => number =
    function(x: number, y: number): number { return x + y; };

Optional 及 Default Parameters

function 的所有參數,在呼叫該 funciton 時,預設都是必要的。如果是 Optional 參數,要在定義時加上 ? ,也可以為參數寫上 default value

以下這兩個 function 的 function type 都是 (firstName: string, lastName?: string) => string

function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}

function buildName2(firstName: string, lastName = "Smith") {
    return firstName + " " + lastName;
}

Rest Parameters: ... 將剩下的參數集合為一個 array

function buildName(firstName: string, ...restOfName: string[]) {
    return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");

let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;

this: 在 javascript 的 function 被呼叫時,同時會設定 this 這個變數。但通常 function 會先被定義,而在後面才被呼叫,因此常常會弄錯 this 指定的對象。

Arrow function: ()=> 在 arrow function 中的 this,會指向該 function 定義時的 object,而不是使用該 function 時的 object

foo(x, y) => {
    x++; 
    y--; 
    return x+y;
}

沒有使用arrow function,在呼叫 says 時,setTimeout 裡面的 this 就不是 Animal 而是 window.this

//沒有使用arrow function

class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        setTimeout(function(){
            console.log(this.type + ' says ' + say)
        }, 1000)
    }
}

var animal = new Animal()
animal.says('hi')  //undefined says hi

將 function() 改為 () =>

class Animal {
    constructor(){
        this.type = 'animal'
    }
    says(say){
        setTimeout( () => {
            console.log(this.type + ' says ' + say)
        }, 1000)
    }
}
var animal = new Animal()
animal.says('hi')  //animal says hi

Overloads: 因為 javascript 是非常動態的語言,常常會遇到某個 function 會在不同狀況,回傳不同資料型別的資料。

定義該 function 時會很直覺地將該 function 的 return value 定義為 any。但這樣卻失去了 TypeScript 的強資料型別的檢查功能。

解決方式是在前面明確地將 function 的各種參數及 return 的狀況都定義出來。

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
let suits = ["hearts", "spades", "clubs", "diamonds"];

function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
    // Check to see if we're working with an object/array
    // if so, they gave us the deck and we'll pick the card
    if (typeof x == "object") {
        let pickedCard = Math.floor(Math.random() * x.length);
        return pickedCard;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}

let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
Interfaces

interface 就跟 java interface 功能一樣,可提供 function 定義的檢查,限制 function 必須在實作時,遵循 interface 的定義。

例如 printLabel 需要一個參數 lavelledObj,且該參數要有 label 屬性。

function printLabel(labelledObj: { label: string }) {
    console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

增加一個 interface 定義,讓 labelledObj 定義為 LabelledValue 型別,就能在編譯時,檢查 myObj 是否有遵循 LabelledValue 的介面定義。

interface LabelledValue {
    label: string;
}

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

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

Optional interface properties: interface 的屬性,可用 ? 代表該屬性可有可無

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

function createSquare(config: SquareConfig): {color: string; area: number} {
    let newSquare = {color: "white", area: 100};
    if (config.color) {
        newSquare.color = config.color;
    }
    if (config.width) {
        newSquare.area = config.width * config.width;
    }
    return newSquare;
}

let mySquare = createSquare({color: "black"});

readonly properties: 限制該屬性在 assign 後,就不能被修改

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

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

作用跟 const 很像,差別是 properties 是用 readonly,而 variables 是用 const


Function Types: 可利用 interface 描述 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;
}

因 compiler 的檢查機制,可以簡化 function 定義裡面原本要寫的 參數及 return value 的 data types

let mySearch: SearchFunc;
mySearch = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
}

Indexable Types: have an index signature that describes the types we can use to index into the object

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

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

let myStr: string = myArray[0];

Class Types: 就跟 c#, java 的 interface 與 class 的關係一樣,class 可 implements interfaces。interface 可定義 properties 及 functions

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

static 與 instance sides of classes 的差異

class 有兩種面向: static side 與 instance side

如果需要一個特殊的 constructor,以下是有問題的程式,因為當 class 實作 interface 時,只會檢查 instance side,但 constructor 是 static side。

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

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

必須將 static side 及 instance side 分成兩個 interfaces: ClockConstructor 是給 constructor 用的, ClockInterface 是 instance methods

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

interface ClockInterface {
    tick();
}

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);
digital.tick();
let analog = createClock(AnalogClock, 7, 32);
analog.tick();

Extending Interfaces: interfaces 可互相 extend,也就是可以從一個 interface 複製 members 到另一個

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

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

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
Classes

這是最簡單的 class,用 new 語法產生 instance

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");

Inheritance: Dog 繼承 Animal,多了 move 這個 method

class Animal {
    move(distanceInMeters: number = 0) {
        console.log(`Animal moved ${distanceInMeters}m.`);
    }
}

class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}

const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

更複雜的例子 Animal: Horse and Snake,用 super 呼叫上層的 method

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distanceInMeters: number = 0) {
        console.log(`${this.name} moved ${distanceInMeters}m.`);
    }
}

class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 5) {
        console.log("Slithering...");
        super.move(distanceInMeters);
    }
}

class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distanceInMeters = 45) {
        console.log("Galloping...");
        super.move(distanceInMeters);
    }
}

let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");

sam.move();
tom.move(34);

public(預設), private, protected

private: 不能從 class 外面使用該 member protected: 可在 subclass 使用該 member

class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}

class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
// console.log(howard.name); // error

也可以將 constructor 設定為 protected,表示該 class 不能直接被 instantiated,但還是可以被繼承

class Person {
    protected name: string;
    protected constructor(theName: string) { this.name = theName; }
}

// Employee can extend Person
class Employee extends Person {
    private department: string;

    constructor(name: string, department: string) {
        super(name);
        this.department = department;
    }

    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}

let howard = new Employee("Howard", "Sales");
// let john = new Person("John"); // Error: The 'Person' constructor is protected

readonly: 可將 properties 設定為 readonly,但 readonly properties 必須在宣告或是 constructor 初始化

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
// dad.name = "Man with the 3-piece suit"; // error! name is readonly.

可為 readonly 欄位加上 get set methods

let passcode = "secret passcode";

class Employee {
    private _fullName: string;

    get fullName(): string {
        return this._fullName;
    }

    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}

let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

如果直接編譯會發生 error,可用 tsconfig.json 或是編譯的參數解決

error TS1056: Accessors are only available when targeting ECMAScript 5 and higher.

注意編譯時要加上 --target ES5

tsc --target ES5 greeter.ts

static properties: 直接用 Grid.origin 存取 origin

class Grid {
    static origin = {x: 0, y: 0};
    calculateDistanceFromOrigin(point: {x: number; y: number;}) {
        let xDist = (point.x - Grid.origin.x);
        let yDist = (point.y - Grid.origin.y);
        return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
    }
    constructor (public scale: number) { }
}

abstract class: 不能直接 instantiated,只有部分已經實作的 methods

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log("roaming the earth...");
    }
}
Iterators

for..of 及 for..in

for..in 會回傳 a list of keys for..of 會回傳 a list of values

let list = [4, 5, 6];

for (let i in list) {
   console.log(i); // "0", "1", "2",
}

for (let i of list) {
   console.log(i); // "4", "5", "6"
}
Modules

自 ECMAScript 2015 開始,JavaScript 支援了 modules,modules 可讓 variables, functions, classes 運作在 modules 中,外面的程式只能 import 使用 export 的部分。

StringValidator.ts

export interface StringValidator {
    isAcceptable(s: string): boolean;
}

LettersOnlyValidator.ts

import { StringValidator } from "./StringValidator";

const lettersRegexp = /^[A-Za-z]+$/;

export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}

ZipCodeValidator.ts

import { StringValidator } from "./StringValidator";

const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

// 定義 ZipCodeValidator 時沒有 export,可將 export 獨立寫成一行
//export { ZipCodeValidator };
// export 時,以 as 語法 rename class name
export { ZipCodeValidator as mainValidator };

AllValidators.ts

// 可將其他三個 ts 的 export 合併在一起

export * from "./StringValidator"; // exports interface 'StringValidator'
export * from "./LettersOnlyValidator"; // exports class 'LettersOnlyValidator'
export * from "./ZipCodeValidator";  // exports class 'ZipCodeValidator'

Test.ts

import { StringValidator } from "./StringValidator";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// Show whether each string passed each validator
strings.forEach(s => {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
});
namespaces

namespaces 為 internal modules, modules 為 external modules。

將所有 validator 相關的程式放在 Validation 這個 namespace 裡面,但同樣要用 export 開放使用的介面。

namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }

    const lettersRegexp = /^[A-Za-z]+$/;
    const numberRegexp = /^[0-9]+$/;

    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }

    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
}

Declaration Files

在 TypeScript 要使用 JavaScript 的 libraries,必須要有該 libray 的定義檔

以 jQuery 為例,通常會使用 $('#id') 或是 jQuery('#id') 這樣的語法,在 TypeScript 編譯時,並不知道 $ 或是 jQuery 的意思,這時需要用 declare 語法定義 jQuery。

通常會把 Declaration File 放在獨立的檔案中,例如

// jQuery.d.ts

declare var jQuery: (string) => any;

然後再以 /// 語法引用

/// <reference path="./jQuery.d.ts" />

jQuery('#foo');

完整的 jQuery Declaration File 已經有人寫好了,可以直接下載 DefinitelyTyped/types/jquery/index.d.ts

但在 TypeScript 2.0+ 已經不建議這樣做,而是改用 @types 來管理所有 library 的 Declaration Files,可以用 npm 安裝 jquery 的 @types

npm install --save-dev @types/jquery

使用 jQuery

/// <reference types="jquery" />

$('#id');

編譯

tsc --target ES6 test.ts

如果要搜尋其他 libraries 的定義檔,可在這個網站 DefinitelyTyped: The repository for high quality TypeScript type definitions 搜尋

ref:

How to use jQuery with TypeScript

TypeScript 聲明文件

References

TypeScript新手入門

學習TypeScript:初體驗

我用 TypeScript 語言的七個月

How to use jQuery with TypeScript

Importing jqueryui with Typescript and RequireJS

Adding jQuery and jQueryUI to your TypeScript project


JS ECMAScript 6 compatibility table

2018年1月8日

ECMAScript 6, ES6

ECMAScript 是由 ECMA 組織通過的 ECMA-262 標準化的腳本式程式語言,在瀏覽器上被廣泛使用的 JavaScript 就是 ECMAScript 的其中一種實作的成品。換句話說,ECMAScript 是語言的標準規格,而比較常聽到的 JavaScript 是一這個語言標準的其中一種實作。

一般初學者學習的 JavaScript 是 ECMAScript 3.0 的語法,目前最新的標準 ECMAScript 6 (ES6) ,是在 2015年6月 發布,其後因標準每年6月固定的更新機制,2015, 2016, 2017 這三年都有發表新版的 ES6,所以後來就標記為 ES2015, ES2016, ES2017。

ECMAScript 和 JavaScript 的關係

1995/6 Sun 與 Netscape 合作發表了 JavaScript,1996/3 Netscape 發表支援 JavaScript 的網頁瀏覽器 Netscape Navigator 2.0,因為該語言的成功,1996/8 MS IE 3.0 支援了 JavaScript。

1996/11 Netscape 將 JavaScript 交給 ECMA 進行標準化,但由於 Java 是 Sun 的商標,JavaScript 是 Netscape 的商標,所以改名為 ECMAScript,也讓該規範更具有開放及中立性。

ECMAScript 是語言的標準規格,而比較常聽到的 JavaScript 是一這個語言標準的其中一種實作,還有其他實作的語言,包含 Adobe ActionScript, TypeScript 等等。

花了 15 年的 ES6 標準

1997/6 ECMA 發佈了 ECMAScript 1.0 後,1998/6 及 1999/12 分別發表了 ECMAScript 2.0 及 3.0,3.0 是非常成功也是最普及的版本。

2000年開始,開始制訂ES4,直到 2007/10 發表了 ES4 草案。但 ECMA Technical Committee 39 (TC39) 的部分成員因 ES4 太過激進,不願意通過這個版本。

2008/7 開會決定中止 ES4 的規格,將其中以小部分放到 ECMAScript 3.1 發佈,將專案代號改為 Harmony,後來ECMAScript 3.1 改為 ECMAScript 5。

2011/6 發表 ECMAScript 5.1,同時是 ISO/IEC 16262:2011 標準。

2013/2 發表了 ECMAScript 6 草案

2015/6 正式通過 ECMAScript 6

ES2015, ES2016, ES2017

2015 通過的 ES6 就是 ECMA2015,這是 ES6 的第一個正式版本。因 ECMA 委員會認為,ES6 的規範已經成熟了,後續會有微幅的修訂,為了讓這個修訂過程標準化,決定每年 6 月就發布一次該年度的最新正式版本。

所以在 2016/6 及 2017/6 都有新版本的 ES2016 及 ES2017,但其實都是 ES6 的標準。

ES6 可說是新一代JavaScript 語言的代名詞。

ECMAScript 6 compatibility table

Mozilla 給開發者的網頁技術文件 JavaScript

ES6 - Quick Guide

References

ECMAScript 6 簡介

ECMAScript wiki