【JS進階】教你在中後臺系統玩轉ES6

_安歌發表於2019-07-23

原文連結: github.com/qiudongwei/…

本文是一篇應用型文章,根據實際的專案場景總結ES6的各種使用姿勢。文章不會對ES6語法中的特性或原理做過多的說明,重點從實際的應用場景去理解ES6的新語法、新特性。

一、變數宣告

為解決(或規範)JS中塊級作用域的問題,ES6新增了letconst兩種宣告變數的方式,與var的區別在於:

  • var宣告的變數,作用域會提升,變數可被重賦值;
  • letconst宣告的變數存在塊級作用域和暫時性死區,但不會變數提升;
  • let宣告的變數可被重賦值,const不行。

日常我們專案開發中,會在IDE儲存檔案或提交Git時會藉助Eslint之類的工具對程式碼規範進行校驗,其中一條常見的校驗提示是:宣告的變數xxx沒有被再賦值,建議使用const。 因此,一般我們用ES6宣告變數時,可以遵循下面的原則

  • 需要被重新賦值的變數,使用let命令;
  • 不會被再賦值的變數,使用const命令;
  • 拒絕使用var命令。
function submit(){
  let loading = true
  const postData = this.getParamData()
  // ...
  loading = false
}
複製程式碼

二、解構賦值

解構賦值是從字串、陣列或物件中提取值,對變數進行賦值的過程。可以應用在變數定義、函式引數等場景中。

// 解構陣列
let [name, age, sex='male'] = ['安歌''18']
// output -> name: '安歌', age: 18, sex='male'

// 物件解構
const ange = {name: '安歌', sex: 'male'}
let {
  name,
  age=18,
  sex
} = ange
// output -> name: '安歌', age: 18, sex: 'male'

// 字串解構
const hi = 'hello'
let [a, b, c, d, e] = hi
let { length } = hi
// output -> a: 'h', b: 'e' ... length: 5 
複製程式碼

三、資料結構Set和Map

Set資料結構要求其元素不可重複,這種唯一性特性在專案開發中能帶來很大便利;Map資料結構是一種鍵值對集合,它對鍵的資料型別提供了更廣泛的支援,同時有一系列極好用的API。

這裡我們以資料列表為場景:對銷售訂單進行對賬,要求限制只能對同一產品進行對賬。

【JS進階】教你在中後臺系統玩轉ES6

1. Set的不重複性

// 根據Set內元素不重複的特性,判斷申請對賬按鈕是否可用
function canSubmit() {
  const checkedListData = getCheckedList() // 獲取選中的資料項列表
  const productNos = new Set( // 取出商品ID列表
    checkedListData.map(each => each.product_no)
  )
  return productNos.size === 1
}
複製程式碼

2. Map的資料儲存

/*
 * 使用Map資料結構儲存勾選資料列表
 * 假設列表資料項 item={ id, order_no, product_no, product_name, isChecked }
 */
const checkedData = new Map()
// 選中操作
checkedData.set(item.id, item)

// 取消選中
checkedData.delete(item.id)

// 清空
checkedData.clear()

// 取出選中項ID集合
const recordIds = checkedData.keys()

// 如果需要對選中的資料進行二次篩選,可以取出資料集合
const data = checkedData.values()

// 從其他頁碼(比如第2頁)返回到已訪問過的頁面(比如第一頁),一般需要還原使用者的選中狀態
checkedData.has(item.id) && (item.isChecked = true)
複製程式碼

四、模板字串

ES6的模板字串允許我們在字串中嵌入變數,或者定義一個多行字串,有更好的可讀性。

const orderNo = 'ON90509000001'
const msg = `訂單${orderNo}對賬失敗!`
// output -> 訂單ON90509000001對賬失敗!
複製程式碼

五、基礎函式

1. 箭頭函式

箭頭函式,讓你的表達更簡潔。它有以下幾個特點:

  • 函式體內的this,是箭頭函式定義時所在上下文的this,這點不可變。但可以通過bindcallapply方法改變上下文的this指向;
  • 它不是一個建構函式,即不能使用new命令;
  • 函式體內不存在arguments物件,但可以使用rest引數代替;
  • 不能使用yield命令,因此箭頭函式不能用作Generator函式。
const obj = {
  name: '安歌',
  hello: function () {
    setTimeout(() => console.log(this.name), 1000)
  }
}
obj.hello() // output -> '安歌'
obj.hello.call({name: 'Ange'}) // output -> 'Ange'
複製程式碼

2. 預設引數

function add(x, y, fixed=2) {
  const result = (+x) + (+y)
  return result.toFixed(fixed)
}

add(1,2) // output -> '3.00'
複製程式碼

3. rest引數

function add(...args) {
  let sum = 0
  for (var val of args) {
    sum += val
  }
  return sum
}

add(2, 5, 3) // 10
複製程式碼

4. 引數解構

function submit() {
  this.$post('/login').then( ({code, data}) => {} )
}
複製程式碼

六、條件判斷

中後臺系統中許可權管理是必備的功能,從控制頁面的訪問許可權到某個功能的操作許可權。這裡以許可權管理為應用場景,看如何用ES6優雅地進行條件判斷。

1. 路由許可權控制

function hasPer(route) {
  const { permissions=[] } = userInfo // 獲取使用者許可權列表
  const routePer = route.meta.per // 預先為每個路由配置的訪問許可權
  return permissions.includes(routePer)
}
複製程式碼

2. 系統功能許可權控制(比如對賬操作)

function checkPer(per) {
  const { permissions=[] } = userInfo // 獲取使用者許可權列表
  return Array.isArray(per) ? 
    // per是個列表,permissions需要包含per裡面要求的所有許可權
    per.every(each => permissions.includes(each)) : 
    (
      // per是個正則,permissions需要存在某個許可權通過per的正則驗證
      per instanceof RegExp ? permissions.some(each => per.test(each)) : 
      // per是個字串,
      permissions.includes(per)
    )
}
複製程式碼

七、改寫for迴圈

假定我們有一個訂單資料列表:

const orderList = [
  {oid: 'ON0001', pid: 'PN0001', pname: '蘸醬短袖', clinet: '楊過', total: 100, date: '2019-07-10'},
  {oid: 'ON0002', pid: 'PN0002', pname: '椒鹽短袖', clinet: '小龍女', total: 122, date: '2019-07-10'},
  {oid: 'ON0003', pid: 'PN0001', pname: '蘸醬短袖', clinet: '郭襄', total: 100, date: '2019-07-14'},
  {oid: 'ON0004', pid: 'PN0003', pname: '炭炙短袖', clinet: '郭襄', total: 137 , date: '2019-07-18'},
  {oid: 'ON0005', pid: 'PN0001', pname: '蘸醬短袖', clinet: '小龍女', total: 100, date: '2019-07-20'}
]
複製程式碼

1. Map取出資料

// 取出列表中的所有訂單號
const oIds = orderList.map(each => each.oid)
複製程式碼

2. forEach改變源資料

// 給列表每個訂單加一個isCP屬性,預設值為false
orderList.forEach(each => each.isCP = false)
複製程式碼

3. 如何continue

如果購買顧客是楊過和小龍女,將其標識為一對CP(換句話說,迴圈列表過程,遇到郭襄就執行continue命令):

orderList.forEach(each => ['楊過', '小龍女'].includes(each.clinet) && (each.isCP = true))
複製程式碼

4. 如何break

商家搞優惠活動,按序給每個訂單總價核減5元(核減的5元返還給顧客),直到遇到第一個奇數總價的訂單(換句話說,虛幻列表過程,遇到第一個奇數總價就執行break命令,終止迴圈)。

orderList.some(each => each.total % 2 ? true : each.total -= 5)

// 可以驗證,當迴圈到{oid: 'ON0004', pid: 'PN0003', pname: '炭炙短袖', clinet: '郭襄', total: 137 , date: '2019-07-18'},這條資料之後,因為結果返回true,迴圈將結束,即後面的資料不會再被迴圈。
複製程式碼

八、物件與陣列互轉

假定場景:給每個訂單的對賬情況加上狀態樣式(successwarningdanger),有如下資料:

// 狀態碼說明: 100->對賬成功、101->對賬中、102->稽核中、103->對賬失敗、104->取消對賬
// 規定有如下樣式對映表: 
const style2StatusMap = {
  success: [100],
  warning: [101,102],
  danger: [103,104]
}
複製程式碼

實現功能:將樣式對映錶轉化為狀態對映表,形如:

const status2styleMap = {
  100: 'success',
  101: 'warning',
  102: 'warning',
  103: 'danger',
  104: 'danger'
}
複製程式碼

使用ES6的entriesreduce等API實現:

// 實現toPairs函式取出Map的鍵值對,函式返回形如[[key1,val1]...]的陣列
const toPairs = (obj) => Object.entries(obj)
// 實現head/last函式取出列表頭尾元素
const head = list => list[0]
const last = list => list.reverse()[0]

// 將物件轉換為陣列
const pairs = toPairs(style2StatusMap)
// 再將陣列轉化為物件
const status2styleMap = pairs.reduce((acc, curr) => {
  const style = head(curr)
  const status = last(curr)
  return status.reduce((accer, each) => (accer[each] = style,accer), acc)
}, {})

// output -> {100: "success", 101: "warning", 102: "warning", 103: "danger", 104: "danger"}
複製程式碼

九、資料採集

有時候我們需要在一個資料物件中取出部分的資料項,經過一定的組裝再傳遞給某個元件或傳送到伺服器。

假定場景:對一個對賬單進行修改,有一個form變數儲存表單資料,router上還帶有一些引數queryData。實現將修改的資料重新組裝後發給伺服器,但存在部分資料不需要傳送。

// 基礎資料
let form = { name: '賬單名', no: '對賬單編號', oNo: '訂單編號', pNo: '商品編號', pName: '商品名'@, pNum: '商品數量', applicant: 'ange@gmail.com(安歌)', ... }
const query= { id: '對賬單ID'}
複製程式碼

1. Object.assign提取資料

// 先覆寫applicant引數
Object.assign(form, {
  applicant: form.applicant.split('(')[0]
  // ...其他可能需要覆寫或追加的引數
}
複製程式碼

2. filter過濾資料

// 採集出需要的資料項
const exclude = ['pName', 'pPrice']
const formData = Object.keys(form)
  .filter(each => !exclude.includes(each))
  .reduce((acc, key) => (acc[key] = form[key], acc), {}
複製程式碼

3. 擴充套件運算子組合資料

// 使用擴充套件運算子組合form和query的引數
const postData = {
  ...formData,
  ...query
}
複製程式碼

十、非同步函式

ES6提供了Promise物件和async函式用於處理非同步操作,用於http請求的axios庫就是基於Promise實現的。利用Promise的特性,我們可以對http請求的返回內容進行攔截而不讓開發者有任何感知。

應用場景:中後臺系統的介面一般會有嚴格的許可權要求以及我們需要對介面異常進行捕獲。通過攔截封裝,可以在業務層程式碼拿到資料之前,先做一層驗證。

1. 基於Promise再封裝post

// 根據需要自定義建立Axios物件例項
const axios = new Axios.create() 
// 封裝post函式
const $post = (url, postData) => {
  return axios.post(url, postData) // 在未封裝的情況下,我們一般通過這種方式發起一個post請求
    .then(interceptors.bind(this)) // 針對許可權驗證的攔截
    .catch(reject.bind(this)) // 捕獲介面異常
}

// 業務層呼叫
$post('./get_order_list', postData).then(res => {})
複製程式碼

2. interceptors攔截器

// 預處理後端錯誤碼
const interceptors = (res) => {
  const code = res.data.code
  if(code === 100) { // 未登入
    console.log('請先登入') // or redirect to login_error_url
    return Promise.reject('LoginError')
  } else if (code === 101) { // 缺失訪問/修改/刪除許可權
    console.log('沒有訪問許可權') // or redirect to permission_error_url
    return Promise.reject('PermissionError')
  } else { // 正常將資料返回
    return response.data
  }
}
複製程式碼

3. 異常捕獲函式reject

// 針對介面異常的捕獲
const reject = (error) => {
  if(error instanceof Error) {
        const errMsg = error.message
        let type = 'error'
        let message = '伺服器出錯了',請聯絡管理員
        if(/timeout/.test(errMsg)) { // 伺服器請求超時
            message = '請求超時...'
            type = 'warning'
        }
        // ...
        console.log(message)
    }
   return Promise.reject(error)
}
複製程式碼

效果如下:

【JS進階】教你在中後臺系統玩轉ES6

4. Asyn函式讓非同步程式碼更優雅、簡潔

// 配合await關鍵字一起使用
const submit = async () => {
  const data = await $post(url)
  // ...
}

// 也可以用在Vue等框架的鉤子函式中
async mounted () {
  const data = await $post(url)
  // ...
}
複製程式碼

十一、動態載入(import)

系統中可能存在某種功能(比如列印)需要引入第三方庫(html2canvas.jsprint.js等),但有時候某些第三方庫可能體量驚人,而使用者訪問頁面又不一定會觸發該功能,這時候就可以考慮動態引入。

function print () {
  import('html2canvas.js').then((html2pdf) => {
    html2pdf().outputPdf()
  })
}
複製程式碼

import()返回一個Promise物件,只在執行時載入指定模組。另外一個常見的應用場景是:Vue的路由懶載入。

const productListModule = () => import(/* webpackChunkName: "product" */  'view/product/List')  // 商品列表
複製程式碼

十二、多維度組合排序

假定場景:在一個商品列表頁面,存在兩個可排序的列:單價(price)和款式(style)。如果先點選了按單價排序,再點選按款式排序,要求款式基於單價的排序上再排序。

// 基礎資料
const products = [
    { name: "椒鹽T恤", price: 3, style: 'Japanese' },
    { name: "蘸醬短袖", price: 5, style: 'Chinese' },
    { name: "碳炙短袖", price: 4, style: 'Chinese' },
    { name: "印花短袖", price: 8, style: 'England' },
    { name: "寫意短袖", price: 3, style: 'Chinese' },
    { name: "清蒸T恤", price: 4, style: 'Japanese' },
]

// 定義不同的排序規則
const byPrice = (a, b) => a.price - b.price
const byStyle = (a, b) => a.style > b.style ? 1 : a.style === b.style ? 0 : -1

// 利用reduce組合排序
const sortByFlattened = fns => (a,b) => 
    fns.reduce((acc, fn) => acc || fn(a,b), 0)

// 組合後的排序函式,排序優先順序按陣列內元素位置編號
const byPriceStyle = sortByFlattened([byPrice,byStyle])

console.log(products.sort(byPriceStyle))
複製程式碼

排序結果如下:先按價格升序,再按款式升序

【JS進階】教你在中後臺系統玩轉ES6

結語

以上僅是個人在日常開發中的ES6使用總結,並未包含全部ES6特性,尚有許多特性較少應用,歡迎大家一起交流,補充分享你們實際專案中應用ES6的情形

最後,如果您覺得本文對您有所啟發,請勿吝嗇您的點贊哈哈~

相關文章