Modern Front-End Workflow

ECMAScript 6

&

Babel

 

ECMAScript 6

  • ECMAScript 是 JavaScript 的語言標準規格,後者是前者的實作品

  • Node.js 是 ECMAScript 在伺服器端的實作品語言

  • ECMAScript 6 是 2015 年時正式發表的新一代版本標準,有許多新的語法以及功能,又名為 ECMASript 2015

  • 後面還有 ES7 (ES 2016)、ES8 (ES 2017) 在規劃與實作中,慣例將之統稱為 ES6+

  • 簡言之,ES6 就是新版的 JavaScript 規格

ECMAScript 6

  • 各家瀏覽器的支援還不夠完美與普遍,目前仍有部分功能未被實作完成

  • 某些使用者可能永遠也不會更新到支援 ES6+ 的瀏覽器

  • 因此我們需要依賴工具將我們的 ES6+ 程式碼轉譯成 ES5 環境能運行的版本,讓我們能放心無虞的使用 ES6+ 語法或功能來開發

Babel

  • Babel 是目前最主流的 JavaScript Transpiler

  • 可以幫我們處理 ES6+ to ES5、React JSX 轉換等工作

  • 可以單獨運行,也可以配合 Webpack 這種前端打包工具一起使用

  • Online Playground

let & const 變數宣告

  • var 語法的變數提升 (Hoisting) 特性對程式碼可維護性是有害的,使用 let 與 const 語法來完全取代 var 語法

  • 使用 let 與 const 宣告的變數只在宣告位置的區塊中有效

  • 使用 let 來宣告普通的變數,使用 const 來宣告並立即賦值一個不再改變的常數
if (true) {
  var a = 10;
}
console.log(a); // 10

if (true) {
  let b = 20;
  console.log(b); // 20
}
console.log(b); // Uncaught ReferenceError: b is not defined

const c;  // SyntaxError: Missing initializer in const declaration

Template String

  • 除了單引號與雙引號之外,ES6 定義的字串種類

  • 使用反引號「`」當作字串開合符號

  • 支援直接輸入換行

  • 支援直接嵌入變數或值

const name = 'Zet';
let str;

str = '我的名字是' + name + ',大家好,\n' + 
'這是一個單引號字串,\n' + 
'這段字串有三行。';

str = `我的名字是${name},大家好,
這是一個模板字串,
這串字串有三行。`

Spread Operator

  • 把陣列或物件展開為個別項目的便捷運算語法

  • 通常用於「合併集合」與「函數的參數填入」

const array = [4, 5, 6];
[1, 2, 3, ...array];    // [1, 2, 3, 4, 5, 6];
[1, ...array, 2, 3];    // [1, 4, 5, 6, 2, 3];
[...array, ...array];   // [4, 5, 6, 4, 5, 6];

const object = { b: 2, c: 3 };
{ a: 1, ...object };    // { a: 1, b: 2, c: 3 }
{ b: 100, ...object };  // { b: 2, c: 3 }
{ ...object, b: 100 };  // { b: 100, c: 3 }
function addAll(x, y, z) {
  return x + y + z;
}
 
const numbers = [1, 2, 3];
console.log(addAll(numbers));     // "1,2,3undefinedundefined"
console.log(addAll(...numbers));  // 6

Destructuring

  • 將集合進行解構賦值的便捷語法

const object = { a: 1, b: 2 };
const { a, b } = object;
console.log(a); // 1
console.log(b); // 2
function getPersonInfoString({ name, phone }) {
  return `名字:${name},電話:${phone}`;
}

console.log(getPersonInfoString({   
  name: 'Zet',
  phone: '0912345678'
})); 
// "名字:Zet,電話:0912345678"

Object 簡寫表示法

  • 針對一個 Property 僅填入一個變數時:

    • 以變數的名稱當做 Object key

    • 以變數的值當作 Object Value

const a = 100;
console.log({ a });     // { a: 100 }
console.log({ a: a });  // { a: 100 }

Object 屬性名表達式

  • 使用 [] 來定義 {} 中的 Object key,可以填入一個表達式

const propertyName = 'email';

const object = {};
object[propertyName] = 'test@gmail.com';
console.log(object); 
// { email: 'test@gmail.com' }

console.log({
  [propertyName]: 'test@gmail.com',
  ['ab' + 'cd']: true
});
// { email: 'test@gmail.com', abcd: true }

Function 參數預設值

  • ES6 允許直接為函數的參數指定預設值

  • 可以配合物件解構賦值使用

function hello(name = 'Zet') {
  console.log(`Hello ${name}`);
}

hello();        // Hello Zet
hello('DMoon'); // Hello Dmoon
function getPersonInfoString({ name = 'Zet', phone }) {
  return `名字:${name},電話:${phone}`;
}

console.log(getPersonInfoString({   
  phone: 0912345678
})); 
// "名字:Zet,電話:0912345678"

Rest Parameters

  • 使用「...」 符號填入函數參數定義,代表接收剩餘的所有參數,並存入指定名稱的陣列中

function sum(...numbers) {
  let result = 0;
  numbers.forEach(function (number) {
    result += number;
  });
  return result;
}

console.log(sum(1));  // 1
console.log(sum(1, 2, 3, 4, 5));  // 15

Arrow Function

  • Arrow Function (箭頭函數) 是一種 ES6 新定義的特殊匿名函數,用於簡寫或是適用某些特殊需要的情況

  • 參數只有一個值時,可以省略包住參數的小括號

  • 表達式只有一行且函數要 return 一個值時,可以省略大括號與 return 語法,但當回傳值是一個物件時,大括號外需再用小括號包住

const printName = (name) => {
  console.log(name);
};

printName('Zet'); // "Zet"
const getDouble = number => number * 2;

const getDouble = function (number) {
  return number * 2;
}

console.log(getDouble(100));  // 200
const getObject = (key, value) => ({
  [key]: value
});

console.log(getObject('name', 'Zet'));  // { name: 'Zet' }

Arrow Function

  • Arrow Function 擁有自動綁定外部 this 到函數內部的特性

  • 手動使用 .bind() 方法指定綁定的 this 對 Arrow Function 是無效的

$(document).ready(function() {
  console.log(this);  // document DOM

  $('#btn1').click(function() {
    console.log($(this).attr('id'));  // "btn1"
    console.log(this);                // #btn1 DOM        
  });

  $('#btn2').click((event) => {
    console.log($(this).attr('id'));  // undefined        
    console.log(this);                // document DOM

    console.log($(event.target).attr('id')); // "btn2"
  });

  let printThis = () => {
    console.log(this);
  };

  printThis(); // document DOM
  
  printThis = printThis.bind(100);
  printThis(); // document DOM

});

Class

  • JavaScript 沒有真正的類別
  • JavaScript 基於 Prototype 來實現類似物件導向的設計模式
  • 傳統寫法是透過 Constructor Function 定義並產生新 Instance
function Macbook(data) {
  this.brand = 'Apple';
  this.model = data.model;
  this.cpu = data.cpu;
  this.ram = data.ram;
  this.ssd = data.ssd;
}

Macbook.models = {
  2014: ['2014 Mid'],
  2015: ['2015 Early'],
  2016: [
    '2016 Late Without TouchBar', 
    '2016 Late With TouchBar'
  ]
};

Macbook.getModelsByYear = function(year) {
  return Macbook.models[year];
};


Macbook.prototype.reboot = function () {
  console.log('do reboot...');
};
var mbpr = new Macbook({
  model: '2016 Late Without TouchBar',
  cpu: 'i7-6660U',
  ram: '16G',
  ssd: '512G'
});

console.log(mbpr);
/* 
Macbook {
  brand: "Apple", 
  model: "2016 Late Without TouchBar", 
  cpu: "i7-6660U", 
  ram: "16G", 
  ssd: "512G"
} */

mbpr.reboot();
// "do reboot..."

console.log(Macbook.getModelsByYear(2016));
/* [
  "2016 Late Without TouchBar", 
  "2016 Late With TouchBar"
] */

Class

  • ES6+ 提供了 Class 語法糖,能使用更貼近物件導向語言的寫法
class Macbook {
  brand = 'Apple';
  constructor({model, cpu, ram, ssd}) {
    this.model = model;
    this.cpu = cpu;
    this.ram = ram;
    this.ssd = ssd;
  }
  
  reboot() {
    console.log('do reboot...');
  }
  
  static models = {
    2014: ['2014 Mid'],
    2015: ['2015 Early'],
    2016: [
      '2016 Late Without TouchBar', 
      '2016 Late With TouchBar'
    ]
  };
  
  static getModelsByYear(year) {
    return Macbook.models[year];
  }
}
class MacbookPro extends Macbook{
  constructor(data) {
    super(data);
  }
  
  isPro = true;
}

const mbpr = new MacbookPro({
  model: '2016 Late Without TouchBar',
  cpu: 'i7-6660U',
  ram: '16G',
  ssd: '512G'
});

console.log(mbpr);
/* 
MacbookPro {
  brand: "Apple", 
  model: "2016 Late Without TouchBar", 
  cpu: "i7-6660U", 
  isPro: true,
  ram: "16G", 
  ssd: "512G"
} */

Promise

  • Promise 是一種非同步程式設計的解決方案,比傳統使用 Callback Function 的方式要更合理與強大
  • Promise 是一個容器,裡面保存了某個未來才會結束的事件
  • 透過原生的 Promise API 來操作這些非同步事件的流程與資料傳遞
const timeout = (delay) => new Promise((resolve, reject) => {
  if (typeof delay == 'number') {
    setTimeout(() => {
      resolve(`${delay}ms passed away.`)
    }, delay);
  } else {
    reject('input is a non-number value.');
  }
});
timeout(3000)
.then(value => {
  console.log(value);
}).catch(error => {
  console.error(error);
});
// "3000ms passed away."
timeout('abc')
.then(value => {
  console.log(value);
}).catch(error => {
  console.error(error);
});
// "input is a non-number value."

Promise

  • 以下是搭配 jQuery Ajax 的範例情境
const getArticleList = () => new Promise((resolve, reject) => {
  $.ajax({
    url: '/api/article/list',
    dataType: 'json',
    success: (result) => resolve(result)
  });
});

const getArticle = (id) => new Promise((resolve, reject) => {
  $.ajax({
    url: `/api/article/detail/${id}`,
    dataType: 'json',
    success: (result) => resolve(result)
  });
});

const getAuthor = (id) => new Promise((resolve, reject) => {
  $.ajax({
    url: `/api/author/${id}`,
    dataType: 'json',
    success: (result) => resolve(result)
  });
});

getArticleList()
.then(articles => getArticle(articles[0].id))
.then(article => getAuthor(article.authorId))
.then(author => console.log(author))
.catch(error => console.error(error));

Promise

  • 平行發起 Promise 處理
    • Promise.all():等待所有 promise 皆被 resolve 再一起回傳結果
    • Promise.race():有第一個 promise 被 resolve 則就馬上回傳該結果
const getDelayAPI = (delay) => new Promise((resolve, reject) => {
  $.ajax({
    url: `/api/delay/${delay}`,
    dataType: 'json',
    success: (result) => resolve(result)
  });
});

Promise.all([
  getDelayAPI(1000), 
  getDelayAPI(2000), 
  getDelayAPI(3000)
]).then(results => console.log(results));

Promise.race([
  getDelayAPI(1000), 
  getDelayAPI(2000), 
  getDelayAPI(3000)
]).then(result => console.log(result));

Asnyc / Await

  • async function 是基於 Promise 的擴充語法
  • 能夠讓 Promise 非同步操作更接近同步的語意
    •  async 關鍵字
      • 表示這是一個 asnyc function,該 function 會自動變成一個 會回傳 promise 的 function
    •  await 關鍵字
      • 表示在此處等待這個 promise 的 resolve 或 reject 結果
const getAuthorByArticle = async() => {
  try {
    const articles = await getArticleList();
    const article = await getArticle(articles[0].id);
    const author = await getAuthor(article.authorId);
    console.log(author);
  } catch (error) {
    console.error(error);
  }
}

Fetch API

Fetch API

  • Fetch 是目標取代 XHR 的新 AJAX 技術標準,已經被部分瀏覽器實作

  • 是一個 HTML BOM 的 API,並非來自 ECMAScript 標準

  • Fetch 原生基於 Promise 實現,因此也可以直接搭配 async / await 使用

const getArticleList = async() => {
  const response = await fetch('/api/article/list');
  return await response.json();
} 

const getArticle = async(id) => {
  const response = await fetch(`/api/article/detail/${id}`);
  return await response.json();
} 

const getAuthor = async(id) => {
  const response = await fetch(`/api/author/${id}`);
  return await response.json();
}

try {
  const articles = await getArticleList();
  const article = await getArticle(articles[0].id);
  const author = await getAuthor(article.authorId);
  console.log(author);
} catch (error) {
  console.error(error);
}

Fetch API

  • 以下是以 Fetch 發送一個 Post Request 的範例,並且將回傳的資料以普通文字解析處理 

const fetchPost = async(data) => {
  const response = await fetch('/api/post', {
    method: 'post',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(data)
  });
  return await response.text();
}

Module System

Module System

  • 模組(Module)代表的是某些程式或功能被封裝成一個封閉狀態的包裹,與外界只能透過事先定義好的接口進行溝通,而不受其他因素而被外界干擾或影響

  • 模組內定義傳入參數的格式、模組的功能與處理過程,以及最後的輸出結果是什麼

  • 模組外則在需要的地方引入這個模組,並根據事先定義好的接口使用之

為什麼需要模組機制?

  • 命名衝突

    • 全域命名空間下容易造成變數命名衝突,尤其是跟第三方套件衝突時更難解決

  • 程式碼間的順序與依賴性維護困難

    • 開發人員必須自行理解並解決不同 JS 檔案之間,以及與第三方套件的相互依賴關係

  • 在大型專案中各種資源難以管理,長期積累的問題導致程式碼庫混亂不堪

但是...

  • JavaScript 這個語言本身長時間以來都沒有設計這樣子的模組機制功能

  • 直到 ES6 中才制訂了原生標準的模組機制規範

社群推廣的模組機制規範

  • AMD

  • CommonJS

    • 語法簡潔易用

    • Node.js 採用的模組規範,最初設計給後端使用

    • 必須經過 Browserify 或 Webpack 等工具進行處理之後,才能在前端瀏覽器上正常運行

    • 目前 ES5 的模組規範主流

  • ES6 原生的模組機制融合了上列兩者的優點,並更加先進易用

CommonJS

  • require(模組檔案路徑 或 npm 套件路徑)

  • module.exports = 該模組輸出的結果

// main.js
const num = 10;
const double = require('./double.js');
console.log(double(num)); // 20
// double.js
const num = 100;
module.exports = (n) => {
  return n * 2;
};

ES6 Module

  • export 語法主要分為兩種

    • export 一個具名的變數或函數,可以有多個 export

    • export default 一個具名或不具名的值或函數,只能有一個 export default

  • import 語法主要分為兩種,可以混著同時寫

    • import 引入後的變數或函數命名 from 'npm套件路徑或模組檔案路徑'

      • 引入 export default 的東西

    • import { 子模組名稱, 子模組名稱 } from  'npm套件路徑或模組檔案路徑'

      • ​引入 export 的東西,名稱必須與 export 時的名稱相同,可以一次引入多個

import str, { number, callback } from './modules.js';
console.log(str);      // "default export value"
console.log(number);   // 1
console.log(callback); // (msg) => alert(msg)
// module.js
export const number = 1;
export const callback = (msg) => alert(msg);
export default 'default export value';

NPM

NPM ( Node Module Manager )

  • NPM 本身是 Node.js(後端)的套件管理系統

  • 因為 Node.js 的逐漸崛起以及 NPM 的普及,前端的 JS 或 CSS 套件也被放上了 NPM,漸漸也成為了前端的套件管理系統主流,通吃前後端

  • Bower 等純前端套件的管理系統逐漸式微

package.json

  • 專案根目錄中的 package.json 檔案,負責記錄與 npm 相關的資訊

    • dependencies:相依模組們的名稱與版本號

    • scripts:自定義的快捷指令集

// package.json
{
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "jquery": "^3.1.1"
  }
}
  • npm install

    • 依照 package.json 中的 dependencies 資訊完整安裝所有套件

  • ​npm install  套件名稱  --save

    • ​安裝指定的套件,並寫入 package.json 中紀錄

    • 應該每次都記得要加上 --save

  • ​npm install  套件名稱  -g

    • 安裝全域型的套件,通常是 CLI 程式

透過模組系統引入 NPM Module

// CommonJS
const $ = require('jquery');

// ES6 Module
import $ from 'jquery'

$('button').click(function(){
  alert('clicked.')
});

Webpack

打包 ( Bundle ) 這件事

  • 對前端程式碼或是圖片、字型等 Assets 檔案進行一些轉譯、合併、壓縮、額外加工處理

     

Webpack

  • Wepack 是一個前端模組的整合與打包工具

  • 同時支援 AMD、CommonJS、ES6 Module 等模組規範

  • 能讓一些原本只能在 Node 上使用的 package 在前端中也能使用

  • 高效能的 Bundle 速度

  • 整合樣式表(CSS/SCSS/LESS 等)

  • 能夠處理圖片以及字型檔

  • 豐富的額外插件

webpack.config.js

  • Wepack 的設定檔,我們將運行的配置寫在這段程式碼中,然後 Webpack 執行時就會按照該配置來進行 bundle 的動作並產出靜態檔案。

     

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: [
    'babel-polyfill',
    './src/index.js'
  ],

  output: {
    path: '/build',
    filename: 'bundle.js',
  },

  module: {
    loaders: [{
      test: /\.(js)$/,
      loader: 'babel-loader',
      exclude: /node_modules/,
      query: {
        presets: ['latest', 'stage-0']
      }
    }]
  },

  devtool: 'eval-source-map'
};

Reference

  • http://es6.ruanyifeng.com/
  • http://huli.logdown.com/posts/292655-javascript-promise-generator-async-es6
  • https://eyesofkids.gitbooks.io/javascript-start-from-es6/content/part4/ajax_fetch.html

本週作業

  • 繳交期限:至 3/10 的中午 12:00,截止後會暫時移除各位的編輯權
  • 作業計算量化分數,並在批改完後公開
  • 所有學員分別獨立一個 private repo 繳交
  • 推到 homework 分支,並與 master 分支比較發起 PR,推錯地方視同作業繳交失敗
  • 基本要求
    • 範例程式只使用了 ES5 語法以及 $.ajax,請自行依課程內容所教,判斷何處適合使用 ES6 語法並改寫之
    • 替換 $.ajax 成為 fetch API (配合 async / await)
    • 僅可以更改 src 資料夾中的程式碼
  • 額外要求(徵酌加分)
    • 程式碼縮排保持一致美觀
    • 你可以在程式碼中加上你的註解,說明你為什麼覺得這裡要這樣改或為什麼不改,幫助作業的批改者理解你的思考以及判斷依據

EXMA-Training-ES6+

By tz5514

EXMA-Training-ES6+

  • 1,595