在大前端的時代,開發 Web app 不再像以前使用一個 jQuery 的 CDN 這麼容易,從 html 模板的抉擇,css 預處理器的挑選,Javascript 模組化的方法,自動化工具的使用等等,都是一門學問。本文將從建置基本的前端開發環境起頭,簡單介紹個人愛用現代常用的前端開發工具。
(撰於 2017-03-10)
(以下環境皆以 macOS 為例)
Node.js 是一個 Javascript 的運行環境,基於 Google V8 Engine。在 Node.js 尚未出現前,Javascript 只能運行在瀏覽器客戶端,功能受限於瀏覽器沙盒(sandbox)與廠商實作。Node.js 推出後,Javascript 程式碼可以在伺服器端運行,模組(module)和套件(package)的觀念和生態圈也隨之建立。程式碼的交流/複用更為便利。
在 macOS 安裝 Node.js 非常簡單,在終端環境輸入指令來安裝最新版的 Node.js:
# The latest version of Node.js
brew install node
# 檢查是否安裝成功(成功則顯示最新版本版號)
node -v
### v7.7.1
同時 Node.js 也附帶如同 python、irb 的直譯式互動環境(REPL)可快速測試/開發一些功能。
# 進入 REPL 環境
node
# -- REPL 環境 --
> 1 + 2
### 3
> 'cat,mouse,dog'.split(/,/)
### [ 'cat', 'mouse', 'dog' ]
Node.js 提供豐富的原生模組,可以操作 filesystem、socket、os 等系統層的 API,讓 Javascript 躋身至與 Python、Ruby 之流同樣地位,成為流行的腳本語言(scripting language)。這裡列出前端開發者較常使用的幾個模組:
os
:作業系統相關的操作與資訊fs
:檔案系統的操作(移動/刪除/新增/檔案監控)path
: 路徑相關工具模組(path resolve/join/pase/normalize)assert
:斷言模組,通常與其他測試框架配合child_process
:產生子行程(進程)的模組,開發較複雜的自動化工具才會用到。
另外,Node.js 同時提供許多重要的全域(Global)物件與函式,在全域下(Global Scope)皆可取得。
global
:node 運行環境最上層的物件,類似瀏覽器端window
的存在。process
:記錄當前 node 運行環境的所有資訊。一般配合設置NODE_ENV
環境變數來區別不同的開發階段。__dirname
:模組所在目錄的名稱。實際上非全域物件,而是各模組皆有的變數。__filename
:模組的檔案名,Node.js 世界,一個檔案為一個模組。實際上非全域物件,而是各模組皆有的變數。require()
:用來引入(import)其他模組的函式。實際上非全域物件,而是各模組皆有的 method。module
:Node.js 遵循 CommonJS 定義的「模組」模組,最常用到module.exports
變數,這個變數會指向欲 export 的物件。
Node.js 模組 import/export 稍微複雜,可以參考用法教學、深入講解 require,或是直接閱讀官方文件。
模組化的實作規範在 Javascript 界可謂群魔亂舞,所以 ECMA 2015 (ES6) 提出新的模組化 API(imports/exports),未來甚至可在瀏覽器端使用。目前可透過 Babel 轉譯器的外掛搶先體驗。
蓬勃社群使得 Node.js 不斷精進,但也帶來軟體工程最痛苦的「版本相容」問題。許多時候,我們需要在最新的 Node 版本中測試新功能,但仍需要維護依賴舊版的專案。在不同 Node 版本環境間切換成本不低,幸虧有牛人寫了易用的版本管理工具 nvm
與 n
,讓版本切換變得輕鬆愉快。
以下主要介紹 nvm
的特色、安裝與簡易用法。nvm
的特色如下:
- 使用 shell script 寫成,無其他相依模組/環境。
- 每個不同版本的 node 有自己的 global modules 環境,不互相影響。
- 更新至新版時,一行指令就可以重新安裝相同的 global packages。
總之,先開啟你的終端機吧!
# 透過 homebrew 安裝 nvm(macOS)
brew install nvm
# 在使用者家目錄下,新增一個 .nvm 的工作目錄
mkdir ~/.nvm
# 使用預設編輯器開啟 ~/.bash_profile
$EDITOR ~/.bash_profile
在你預設(或者你最喜愛)的編輯器裡,將下列兩行設定加入 .bash_profile 中:
# 將下列設定加在 .bash_profile 中,讓 shell 讀取 nvm 設定
export NVM_DIR="$HOME/.nvm"
. "/usr/local/opt/nvm/nvm.sh"
離開編輯器,回到終端機畫面。
# 檢查設定是否正確
echo $NVM_DIR
### /Users/weihanglo/.nvm
# 重新讀取 .bash_profile,讓剛剛的設定生效
source ~/.bash_profile
現在可以開始安裝不同版本的 node 了!
# 安裝最新版的 Node.js
nvm install node
# 安裝/移除特定版本
nvm install v6.9.0
nvm install v7.6.0
nvm uninstall node
# 設定預設使用的版本
nvm alias default v7.6.0
# 切換至其他版本
nvm use v7.6.0
# or
nvm use default
# 列出 local 已經安裝的版本
nvm ls
### v6.9.0
### -> v7.6.0
### default -> v7.6.0
# 安裝新版 Node.js,並從其他版本安裝相同的 packages/modules
nvm install v7.7.1 --reinstall-packages-from=v7.4.0
使用 nvm 至今,個人唯一詬病的是 script 體積較大,拖慢 shell startup time,社群有人發現此問題,並提出解法。我稍作修改,去蕪存菁,只讀取 default version 的 binary,略提升 startup 時間(畢竟敝人的
.bashrc
已不瘦了),這段 script 提供大家參考。
常言道,成功的程式語言背後,有個支持它的生態圈。Python/R 透過科學與統計的模組生態,在資料科學界中獨霸一方;Docker 把 Go 語言從鬼門關前救回,Go 因此成為 Container 界的王者;Node.js/Javascript 則是藉由方便易用的 npm
,讓才華洋溢的宅男工程師們盡情交流,創造出成千上萬個模組。
以下列出 npm 的特色:
- 為 Node.js 預設的套件管理工具,安裝 Node.js 會一併安裝 npm。
- npm, Inc 提供套件伺服器 npm Registry 供開發者上傳/下載套件。(截止 2017.3.9 有 43 萬餘套件)。
- 提供
package.json
供使用者管理專案的相依模組/套件。 - 根據
package.json
的設定,進行更複雜的任務,如 test runner、build tool、watch file changes。
package.json
是一個 Node.js 模組最重要的檔案,記錄與此模組相關的設定,部分第三方套件的配置文件也可以寫在 package.json
裡,減少 project 設定檔過多的問題(例如:babel、browserslist)。
合法的 package.json
除了要是一個 JSON 格式檔案之外,還必須包含下列兩個重要的 fields:
name
:模組名稱,也是import 模組時的名稱。在 npm Registry 通常是以相同或近似的名稱註冊,命名慣例以-
(hyphen)取代 camelCase。version
:模組目前的版號,npm 遵循語意化版號的標準,減少套件更新異動造成的問題。
其他重要且建議填寫的 fields 有:
main
:程式進入點,也是模組進入點(該檔案的module.exports
),慣例為index.js
或main.js
。devepdencies
:該模組直接相依的第三方模組。規範採用語意化版號標準。devDependencies
: 該模組開發時會使用到相依模組,例如測試模組、打包模組。規範採語意化版號標準。scripts
:自定義的 shell 腳本。可透過npm run <command>
執行。license
:模組的授權條款,建議填寫。bin
:若模組有提供指令列程式,需在此配置指令名稱與對應檔案。
在此概述常用的版號語法:
1.2.3
:指定使用版號 1.2.3>1.2.3
:接受版號大於 1.2.3>=1.2.3
:接受版號大於等於 1.2.3~1.2.3
:接受 patch version,同義於>=1.2.3 <1.3.0
^1.2.3
:只接受 minor version 或 non-breaking changes,同義於>=1.2.0 <2.0.0
*
:接受任何版號。
~
(tilde)與^
(caret), 在版號寫法不同時,有不同結果,請參考 node-semver 官方文件。
一個合法的 package.json
範例:
{
"name": "electron-react-demo", // 模組名稱,以 hyphen 取代 camelCase
"version": "0.0.1", // 當前版號
"description": "A Electron Demo with React", // 模組簡介
"main": "main.js", // 程式進入點/模組進入點
"scripts": { // 自定義腳本
"start": "electron ." // 輸入 `npm run start` 或 `npm start` 時會執行的腳本
},
"author": "Weihang Lo", // 作者欄位
"license": "MIT", // 授權/版權條款
"dependencies": { // 直接相依的模組
"react": "~15.4.1", // 使用 patch version 的 react
"react-dom": "~15.4.1"
},
"devDependencies": { // 開發用的模組
"babel-preset-es2015": "^6.18.0", // 版號 >= 6.18.0 但小於 < 7.0.0
"babel-preset-react": "^6.16.0",
"electron": "1.4.12", // 使用指定版號的 electron
"mocha": "*", // 使用最新/任意版本。
"chai": "*"
}
}
還有許多沒介紹到的 package.json
設定,在 npm 官方文件 裡應有盡有!
npm 的指令列程式提供許多功能,其中最重要的兩類即是模組和執行腳本相關的指令。
安裝/移除/更新/列出 相依模組
npm install [--global] [--save] [--save-dev]
npm uninstall [--global] [--save] [--save-dev]
npm ls [--global] [--depth=<number>]
npm update [--global]
開始介紹前,先了解 npm 安裝模組的模式,分為 全域模式(globally) 與 本地模式(locally)
- 全域模式
--global
:安裝的模組通常是常用的指令列程式,例如 npm 本身。 - 本地模式:用來安裝與 project 相依的模組,會在 project 根目錄產生一個
node_modules
存放相依模組。
# 創建一個 npm 模組環境(互動式產生 package.json)
npm init
# 尋找同目錄下的 `package.json`,安裝該檔案內記錄的相依套件
npm install
# 安裝 axios 套件,不儲存相依關係。
npm install axios
# 安裝 bluebird 套件,並將相依版號寫入 `package.json` 的 `dependencies` field 中
npm install bluebird --save
# 安裝 mocha、chai 套件,並將相依版號寫入 `package.json` 的 `devDependencies` field 中(開發用套件)
npm install mocha chai --save-dev
# 安裝 Globally 的套件,在任何目錄都可直接使用該模組的指令列程式
npm install --global yarn
# 移除 bluebird 套件,並從 `package.json` 中移除相依關係
npm uninstall bluebird --save
# 列出 locally(project-wide) 的套件到第一階層(專案的相依套件的相依套件)
npm ls --depth=2
# 列出全域安裝的套件(只列出 user 直接安裝的套件)
npm ls --global --depth=0
# 更新 bluebird 套件至 package.json 內的指定版號
npm update bluebird
# 更新 package.json 記錄的套件至指定版號
npm update
# 更新所有全員安裝的套件
npm update --global
Facebook、Google 幾個大頭在 2016 年 10 月 開源了新一代的 Node.js 套件管理工具 Yarn,在速度、體驗、介面上皆略勝 npm 一籌。Yarn 也會自動產生
yarn.lock
檔案,精確記錄相依模組的版號,在套件管理上更安全安心,有興趣的童鞋可嘗試看看。
執行自定義腳本
npm run <command> [-- <args>...]
用來執行 package.json
的 scripts
field 中的自定義腳本。在執行開始前,npm run
會在既有的 PATH 環境變數加上 node_modules/.bin
,許多套件提供的 binary 執行檔可以直接執行,不需要再加上 ./node_modules/.bin/a-command
等冗長的相對路徑。因此,npm run
常作為 task/test runner、build tools 的入口。
# 在 `package.json` 裡
{
"scripts": {
"start": "echo 'Start my node.js app'",
"fail": "echo \"Oops! Failed on $1\"",
"serve": "serve"
}
}
# 回到終端環境,先安裝相依套件
npm install serve
# 等同於執行 `./node_modules/.bin/serve`,開啟一個 local http server
npm run serve
### Serving!
# `--` 可傳入參數等,同執行 `echo 'Oops! Failed $1`
npm run fail -- Example
### Oops! Failed on Example
# `start`、`test`、`restart` 等 script 可不透過 `run`,直接使用簡寫執行
npm run start
### Start my node.js app
npm start
### Start my node.js app
除了上述 npm 還有許多功能,族繁不及備載,詳情請參考 npm - Cli Commands。有關 npm run-script
,也可以瞧瞧這篇教學。
雖然 Node.js/npm 為 Javascript 生態圈帶來前所未有的繁榮盛況,但前端的世界還是處處充滿危機,不同瀏覽器廠商的實作參差不齊,開發者常搞不清楚可以用哪些 CSS 與 Javascript 的 features,開源社群為了消弭這些惱人的問題,開發出許多協助開發者的預處理器/轉譯器(transpiler)。
預處理器/轉譯器屬於 source-to-source compiler,雖然可以加速開發,但也需要引入 build tools 協助轉換語法,有關 build tools/task runner,會在自動化工具/打包工具與各位分享。
CSS 對程序猿來說,沒有繼承,沒有函式,沒有變數,全部的設定都在 global scope,完全符合設計不良的語言特性。有志青年打造了許多類似 CSS 的語言,提供變數、函式、mixin,再 compile 成 vanilla CSS,讓寫 CSS 能夠更輕鬆,更能專注商業邏輯。這些方便的工具我們通稱 CSS Preprocessor。
目前主流的 CSS Preprocessor 有 Less、Sass,以及 Stylus 等,每一套都有各自的擁護者,在此簡單比較 Sass 與 vanilla CSS,給客倌看看。
Vanilla CSS(同一個 nav 下的元素要分為三個 block 撰寫,且相同的 padding 要寫兩次)
nav ul {
margin: 0;
padding: 0;
list-style: none;
}
nav li {
padding: 6px 12px
display: inline-block;
}
nav a {
display: block;
padding: 6px 12px;
text-decoration: none;
}
Sass(SCSS syntax)支援 variable 與 nested element selector
$my-vertical-padding: 6px;
$my-horizontal-padding: 12px;
nav {
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
display: inline-block;
padding: $my-vertical-padding $my-horizontal-padding;
}
a {
display: block;
padding: $my-vertical-padding $my-horizontal-padding;
text-decoration: none;
}
}
Sass 是 Ruby 社群發展出來的 CSS 預處理器,在 Javascript 界通常使用 node-sass 做 CSS transform。Sass 是個人非常喜愛的 CSS 預處理器。
對前端開發者來說,最痛苦的莫過於在 Chrome 切好 layout,卻在 Safari 跑版。在 Firefox 做 css animation,卻在 Safari 動彈不得。這些問題來自於各瀏覽器實作不同,添加的 vendor prefix 也不一樣,我們可以透過 Sass 的 mixin 來解決 prefix 問題,但這不夠 fancy,CSS 後處理器概念營運而生,最有名的莫過於 postcss 的 Plugin autoprefixer,在 CSS 預處理器 compile 完成之後,需要的 property 加上 vendor prefix,完全不需要再寫一丁點 mixin。
順水推舟,postcss 是個生態豐富的 css transforming plugin system,許多瀏覽器未實作的 feature,透過 plugin 轉換,就可以使用各種 feature了。
Javascript 從出生到現在,一直是個被人嫌棄的語言,弱型別,隱式轉換,缺乏真正物件導向概念(只有 prototype oriented),變數可重複宣告,this
的語意更讓人摸不著頭緒。近年來,ECMAScript 的標準不斷往前走,加入了 作用域變數 scope variable(let
)、常數宣告 constant declaration(const
),自動綁定 this
的 arrow function,甚至原生的 class 等語言新特性。徹底改造整個 Javascript 的生態圈。
可惜又面臨同個問題,目前的瀏覽器/Node.js 環境不一定支援。社群又跳出來,寫了名為 Babel
(借用巴別塔的典故)的 transpiler,將最新的 Javascript 語法,轉換成當前瀏覽器相容的語法。透過 Babel,我們可歡樂地使用 ES6 的 class,不必擔心 IE 會 crash 了!
以下介紹 Babel 的簡易設定:
# 在你的專案目錄底下安裝 babel 套件
npm install --save-dev babel-cli babel-preset-env
# 使用預設編輯器開啟 .babelrc(Babel 設定檔)
$EDITOR ~/.bash_profile
在你預設(或者你最喜愛)的編輯器裡,將下列設定加入 .babelrc
(Babel 設定檔) 中:
# 在 .babelrc 中加入下列設定
{
"presets": ["env"]
}
接下來利用你喜愛的 task runner,把你的 code compile 成瀏覽器相容的 javascript 吧!
Babel 提供許多不同的預處理器(presets),例如 es2015、es2017,
env
是目前 Babel 官方推薦的 presets,可以透過設定browserslist
,依據不同生產環境決定哪些語法需要轉換,autoprefixer 與 eslint 同樣也支援browserslist
。
另一個有名且有前景的轉譯器是微軟出品的 TypeScript,支援繼承、抽象介面、裝飾器、型別檢查等 features,現代語言該有的應有盡有。由於是 Javascript 的 superset,在 TypeScript 裡寫 Javascript 完全合法,且 Google 的 Angular Framework,以及有名的 Reactive Programing Libary RxJS 也都採用 TypeScript。值得一試!
使用這麼多預處理器/轉譯器/自定義腳本,如果每次都需要自己 npm run compile
、babel script.js
豈不麻煩?為了減少重複性的任務(task),Javascript 生態圈發展出數套實用的 build tool/system,老牌的 Grunt 與較年輕的 Gulp,這裡選擇使用 Gulp。
另外,當我們想使用 npm 上的各種模組,卻很難直接在瀏覽器端引入這些 dependencies。打包工具如 Browserify
與 Webpack 提供我們將這些散落各處的 .js、.css、.html 打包起來的方法,便於 import 到瀏覽器客戶端。這裡主要介紹 Webpack。
Gulp 是一個直觀易懂的 build system,其概念是利用 Node.js 的 stream API,有如 pipeline 般將檔案傳遞到每個 plugin/transformer 中做對應的任務。而 Gulp 也有豐富的 plugin 生態系,提供許多主流預處理器的 plugin,讓結果如同 stream 一樣容易產出。
以下簡單示範 gulp 安裝與用法:
# 1. 首先先安裝 Global gulp command-line tool
npm install --global gulp-cli
# 2. 接著在 project 將 gulp 安裝為 devDependencies
npm install --save-dev gulp
在 project root 新增 gulpfile.js,並寫入這些設定:
// 3. gulpfile.js at project root
var gulp = require('gulp');
gulp.task('default', function() {
// 你的預設 task
});
回到終端環境:
# 4. 輸入 `gulp`,執行預設的 task:
gulp
### [14:20:30] Using gulpfile ~/Documents/gulp-demo/gulpfile.js
### [14:20:30] Starting 'default'...
### [14:20:30] Finished 'default' after 361 μs
Gulp 本身的 API 不多,語法也很簡單,這邊舉例並說明 gulpfile.js
實例:
// 確認有先安裝相依的 babel 套件 與 del 套件
// `npm install --save-dev gulp-babel babel-preset-env del`
var gulp = require('gulp'); // import gulp 套件(必須)
var babel = require('gulp-babel'); // import gulp-babel 插件
var del = require('del'); // import del 套件(用來 cleanup output)
gulp.task('babel', function () { // 建立一個叫做 babel 的任務
gulp.src('src/**/*.js') // `gulp.src` 會讀取給定路徑(src 下所有 js 檔)的檔案
.pipe(babel({ presets: ['env'] })) // 將上一步的檔案 pipe 給 babel plugin 處理
.pipe(gulp.dest('dist')); // 將上一步的檔案透過 `gulp.dest` 輸出到給定路徑(dist)
});
gulp.task('clean', function () { // 建立一個叫做 clean 的任務
del(['dist']); // 使用 `del` 套件,刪除輸出的目錄 (dist)
});
gulp.task('default', ['clean'], function () { // 建立預設任務,並設 clean 為相依任務(在該任務執行前執行)
gulp.watch('src/**/*.js', ['babel']); // 使用 `gulp.watch` 監視檔案異動,有異動就執行 babel 任務
});
在終端環境下,我們這樣做:
# 先寫一個假的 ES6 js file
mkdir src
echo " (() => console.log('Hello, world!'))() " > src/demo.js
# 執行 babel 任務
gulp babel
### [14:42:23] Using gulpfile ~/Documents/gulp-babel-demo/gulpfile.js
### [14:42:23] Starting 'babel'...
### [14:42:23] Finished 'babel' after 9.31 ms
# 測試是否正確 compile 成功
node src/demo.js
### Hello, world!
# 清除輸出檔案
gulp clean
### [14:56:18] Using gulpfile ~/Documents/gulp-babel-demo/gulpfile.js
### [14:56:18] Starting 'clean'...
### [14:56:18] Finished 'clean' after 4.34 ms
# 測試是否正確清除 `dist` 目錄
ls
### gulpfile.js node_modules package.json src
# 執行 `gulp`,會先執行 clean(相依任務),再執行 default(預設任務)
gulp
### [14:48:30] Using gulpfile ~/Documents/gulp-babel-demo/gulpfile.js
### [14:48:30] Starting 'clean'...
### [14:48:30] Finished 'clean' after 12 ms
### [14:48:30] Starting 'default'...
### [14:48:30] Finished 'default' after 9.36 ms
欲了解其他 Plugin/用法,可直接 Gulp 官網,或直接搜尋 gulp + <something>
,神人都幫你做好了。
Webpack 是近幾年來最熱門的打包工具,透過解析模組之間的相依關係,可以
- 把專案中 js、css、和其他靜態 assets 打包到同一個檔案中
- 將不同頁面/模組的程式碼分離(code splitting)
- 透過 loader system,轉換/編譯 Sass、Babel、TypeScript 甚至圖片等不同檔案 (多數情況下能取代 Gulp、Grunt 等工具)
- 運用不同的 Plugins,組合出適合自己的 Webpack 流程與設定。
Webpack 最簡單的設定,就是在 project root 新增一個 webpack.config.js
檔案,以下範例取自 Webpack 的核心觀念(使用 Webpack 2):
// in `webpack.config.js`
const webpack = require('webpack'); //to access built-in plugins
const path = require('path');
module.exports = {
entry: './path/to/my/entry/file.js', // 1. 程式進入點設定
output: { // 2. 打包輸出設定
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: { // 3. loaders(轉換檔案 -> Javascript)
rules: [
{ test: /\.jsx?$/, use: 'babel-loader' }
]
},
plugins: [ // 4. 其他有用的 plugins
new webpack.optimize.UglifyJsPlugin(),
]
};
Webpack 的簡易運作流程如下:
找到 entry file
-> 解析相依模組
-> 符合 test rules 的模組由 loaders 轉換處理
-> 將所有處理完成的檔案打包成 output 的 js 檔
其中,在 webpack.config.js
設定檔中,最核心四個概念如下:
entry
程式進入點,webpack 會從這個(些)檔案開始解析所有相依(require/import)的模組、CSS、圖片(沒錯,Webpack 可以 import 圖片,將圖片視為模組)。一個可以有多個 entries。
output
打包完成的 .js
輸出的路徑設定,可根據多個 entries 輸出對應的 output。也可以利用內建的 CommonsChunkPlugin
來分離不同區塊/模組/套件的 output js 檔案。
loaders
任何使用 import/require 等關鍵字的 dependencies 都會被 webpack 解析,但 webpack 只認得 Javascript,所以需要許多 loaders 協助轉換,例如 sass-loader
、css-loader
、babel-loader
等。 每條 rules
利用 Regex 的 test
來區分哪些檔案使用哪些 loaders 處理,use
field 則是指定對應的 loader,可以串連多個 loaders。
plugins
使用其他 plugins 來客製化 webpack 的打包結果,例如範例中內建的 UglifyJsPlugin
則是壓縮混淆最終輸出的 bundle.js,也有類似 extract-text-webpack-plugin
,將 CSS 檔從 bundle.js 中分離等實用的 plugins。
Webpack 近幾年風生水起,生態系也應運而生。礙於篇幅,不少非常實用設定,以及 loaders 與 plugins 例如 style-loader
、url-loader
、HtmlWebpackPlugin
等,在此無法一一贅述,有興趣可以參考這個教學,也別忘了 Webpack 2 最新的官網。
一份好的程式碼,除了可以正確無誤的執行,更要讓人易讀易維護,本節將介紹
- 如何使用現代化的 JS 測試框架,讓你不再害怕自己寫的程式碼。
- 選擇一個好用的靜態語法檢查器,提高可讀性,減少人為失誤。
所有工程師都知道測試的好處,也了解測試的必要性,但卻很少人主動寫測試。傳統 TDD(Trump-driven Test-driven development)的先寫測試,再寫程式的流程較不直觀,且易淪為為測試而測試,脫離現實。隨後崛起的 BDD(Behavior-driven development)則漸趨主流,強調測試只應在「程式行為不符預期時失敗」,是測「程式做了什麼」,而不是「程式如何做這些事」。
一些 BDD 的測試框架與 BDD 斷言庫也順勢產生,透過語意化的 API,讓測試員更能了解程式到底「幹了啥事」,增添寫測試的樂趣。這裡主要介紹 Mocha 測試框架,配合 Chai 斷言庫,達成「快樂寫測試,寫測試快樂」的最高境界。
# 安裝 mocha、chai 兩個套件
npm install --save-dev mocha chai
# 建立一個 test 目錄
mkdir test
# 使用預設編輯器開啟 test/test.js(第一個測試檔案)
$EDITOR test/test.js
在你預設(或者你最喜愛)的編輯器裡,將下列測試加入 test/test.js
const chai = require('chai'); // 引入 `chai`(提供 asset/expect/should 風格斷言)
chai.should(); // 使用 should 前,需先執行 `chai.should()`,將 should 加到每個 Object
describe('Array', function() { // Test Suite
describe('#indexOf()', function() { // mocha 可嵌套 Test Suite,讓意圖更清晰
it('should include 2', function() { // 實際的 Test Case
const array = [1,2,5];
array.should.include(2); // 使用 should 風格進行斷言
});
});
});
回到終端環境,執行以下指令:
# 使用 mocha 模組的 command-line tool,進行測試
./node_modules/.bin/mocha test/test.js
### Array
### #indexOf()
### ✓ should include 2
###
### 1 passing (10ms)
若希望直接執行
mocha
來測試,不想每次都加上模組路徑,可以
- 在全域安裝
mocha
套件npm install --global mocha
- 在
package.json
加入一個 run-scriptjavascript "scripts": { // ... "test": "mocha path/to/your/test/dir/" }
如果想建立 TDD 式的 setUp
、tearDown
hooks,可以如下設計:
describe('hooks', function () {
before(function() {
// 在這個 block 內所有 test case 之前執行
});
after(function () {
// 在這個 block 內所有 test case 之後執行
});
beforeEach(function () {
// 在這個 block 內每個 test case 之前執行
});
afterEach(function () {
// 在這個 block 內每個 test case 之後執行
});
});
有需要非同步(異步)的測試,請
- 在 test case 的 function params 加入
done
參數。 - 成功則調用
done()
。 - 失敗則調用
done(err)
,並加入 error 作為引數(argument)。
這裡以 Promise
為例:
describe('Async Tests', function () {
it('should complete in the furture', function (done) { // 加入 done 參數
someAsyncPromiseTest() // 一個異步的 Promise
.then((result) => {
assert.ok(result); // 測試斷言
done(); // 調用 done,通知 test runner 成功執行 async task
})
.catch(err, (err) => {
done(err); // 調用 done 並傳入 error,通知 test runner 執行異常
})
});
});
// 由於 `done` 是一個函式,上式也可簡寫成
describe('Async Tests', function () {
it('should complete in the furture', function (done) {
someAsyncPromiseTest()
.then((result) => {
result.should.equal('well done');
done();
})
.catch(done); // 直接傳入 done!
});
});
如果需要非同步的 hooks,同樣加入
done
參數,並調用 done。
其他相關 mocha 語法 API,mocha 網站寫得非常清楚。有關斷言的寫法,也可直接參考 chai 官網。
使用整合開發環境(IDE)的童鞋,想必對靜態語法檢查有深刻體會。使用靜態語法檢查,會對程式碼撰寫的風格有所限制,但同時可以可防止許多 bad code smell,例如:
- 一定要處理 error
- 避免修改以 const 宣告的變數
- 不要用
(
/
.
,
等符號當開頭(不寫分號唯一會遇到的問題!)
Javascript 的 linter 有非常多套,這裡選用目前最流行、客製化程度最高的 ESLint 作範例。
# 安裝 eslint
npm install --save-dev eslint
# 互動式建置 `.eslint.js` 配置文件
./node_modules/.bin/eslint --init
# 之後就可以直接用 eslint 來檢查你的程式碼了
./node_modules/.bin/eslint path/to/your/file.js
同樣地,可以在
package.json
加入一個 run-script,方便隨時 lint
"scripts": {
// ...
"lint": "eslint .; exit 0;" // 有錯誤的話 eslint exit code 是 1, 我們手動 exit 0 以避免 npm 報錯。
}
有些檔案不需要 linter 檢查(例如 test spec、其他套件的 config),可在 project root 加入
.eslintignore
忽略這些檔案(寫法同.gitignore
)。
通常我們會用一些大廠的設定,簡化我們的 Linter config,例如使用 Airbnb Javascript style,如果需要客製化 linter 可以參考 ESLint 如何配置。
許多文字編輯器有整合的 eslint 的 plugin(如 Atom、Visual Studio Code),可即時查看 lint 結果,讓開發者更容易檢查語法錯誤。
本想簡單介紹如何建置一個前端開發環境,無奈前端之龐大,初出茅廬的我,完全無法收斂文章內容。本文已盡量點到為止,在重要之處皆留下關鍵連結,給有興趣的人們挖了些坑,希望看倌透過這篇文章,能對前端工程紛擾的世界有所了解,也不吝指教交流!
- Node.js API Documentation
- NVM - Node Version Manager
- NPM - Node Package manager
- NPM Cli Commands
- NPM package.json spec
- Mozilla Developer Network
- Gulp - The streaming build system
- Webpack - A bundler for javascript and friends
- Mocha - Feature-rich Test Framework
- Chai - BDD/TDD Assertion Library
- ESLint - Pluggable JavaScript Linter