2018/01/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/01/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/01/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/01/08

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