阅读 2179

【JS进阶】教你在中后台系统玩转ES6

原文链接: 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。

这里我们以数据列表为场景:对销售订单进行对账,要求限制只能对同一产品进行对账。

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)
}
复制代码

效果如下:

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))
复制代码

排序结果如下:先按价格升序,再按款式升序

结语

以上仅是个人在日常开发中的ES6使用总结,并未包含全部ES6特性,尚有许多特性较少应用,欢迎大家一起交流,补充分享你们实际项目中应用ES6的情形

最后,如果您觉得本文对您有所启发,请勿吝啬您的点赞哈哈~

关注下面的标签,发现更多相似文章
评论