安于现状i

安于现状i 查看完整档案

杭州编辑  |  填写毕业院校不知名公司  |  前端工程师 编辑 huahua0406.github.io/ 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

安于现状i 赞了文章 · 2020-12-02

React Hook + TS 购物车实战(性能优化、闭包陷阱、自定义hook)

前言

本文由一个基础的购物车需求展开,一步一步带你深入理解 React Hook 中的坑和优化

通过本篇文章你可以学到:

✨React Hook + TypeScript 编写业务组件的实践

✨ 如何利用 React.memo优化性能

✨ 如何避免 Hook 带来的闭包陷阱

✨ 如何抽象出简单好用的自定义hook

预览地址

https://sl1673495.github.io/r...

代码仓库

本文涉及到的代码已经整理到 github 仓库中,用 cra 搭建了一个示例工程,关于性能优化的部分可以打开控制台查看重渲染的情况。

https://github.com/sl1673495/...

需求分解

作为一个购物车需求,那么它必然涉及到几个需求点:

  1. 勾选、全选与反选。
  2. 根据选中项计算总价。

gif1

需求实现

获取数据

首先我们请求到购物车数据,这里并不是本文的重点,可以通过自定义请求 hook 实现,也可以通过普通的 useState + useEffect 实现。

const getCart = () => {
  return axios('/api/cart')
}
const {
  // 购物车数据
  cartData,
  // 重新请求数据的方法
  refresh,
} = useRequest < CartResponse > getCart

勾选逻辑实现

我们考虑用一个对象作为映射表,通过checkedMap这个变量来记录所有被勾选的商品 id:

type CheckedMap = {
  [id: number]: boolean,
}
// 商品勾选
const [checkedMap, setCheckedMap] = useState < CheckedMap > {}
const onCheckedChange: OnCheckedChange = (cartItem, checked) => {
  const { id } = cartItem
  const newCheckedMap = Object.assign({}, checkedMap, {
    [id]: checked,
  })
  setCheckedMap(newCheckedMap)
}

计算勾选总价

再用 reduce 来实现一个计算价格总和的函数

// cartItems的积分总和
const sumPrice = (cartItems: CartItem[]) => {
  return cartItems.reduce((sum, cur) => sum + cur.price, 0)
}

那么此时就需要一个过滤出所有选中商品的函数

// 返回已选中的所有cartItems
const filterChecked = () => {
  return (
    Object.entries(checkedMap)
      // 通过这个filter 筛选出所有checked状态为true的项
      .filter((entries) => Boolean(entries[1]))
      // 再从cartData中根据id来map出选中列表
      .map(([checkedId]) => cartData.find(({ id }) => id === Number(checkedId)))
  )
}

最后把这俩函数一组合,价格就出来了:

// 计算礼享积分
const calcPrice = () => {
  return sumPrice(filterChecked())
}

有人可能疑惑,为什么一个简单的逻辑要抽出这么几个函数,这里我要解释一下,为了保证文章的易读性,我把真实需求做了简化。

在真实需求中,可能会对不同类型的商品分别做总价计算,因此filterChecked这个函数就不可或缺了,filterChecked 可以传入一个额外的过滤参数,去返回勾选中的商品的子集,这里就不再赘述。

全选反选逻辑

有了filterChecked函数以后,我们也可以轻松的计算出派生状态checkedAll,是否全选:

// 全选
const checkedAll =
  cartData.length !== 0 && filterChecked().length === cartData.length

写出全选和反全选的函数:

const onCheckedAllChange = (newCheckedAll) => {
  // 构造新的勾选map
  let newCheckedMap: CheckedMap = {}
  // 全选
  if (newCheckedAll) {
    cartData.forEach((cartItem) => {
      newCheckedMap[cartItem.id] = true
    })
  }
  // 取消全选的话 直接把map赋值为空对象
  setCheckedMap(newCheckedMap)
}

如果是

  • 全选 就把checkedMap的每一个商品 id 都赋值为 true。
  • 反选 就把checkedMap赋值为空对象。

渲染商品子组件

{
  cartData.map((cartItem) => {
    const { id } = cartItem
    const checked = checkedMap[id]
    return (
      <ItemCard
        key={id}
        cartItem={cartItem}
        checked={checked}
        onCheckedChange={onCheckedChange}
      />
    )
  })
}

可以看出,是否勾选的逻辑就这样轻松的传给了子组件。

React.memo 性能优化

到了这一步,基本的购物车需求已经实现了。

但是现在我们有了新的问题。

这是 React 的一个缺陷,默认情况下几乎没有任何性能优化。

我们来看一下动图演示:

gif2

购物车此时有 5 个商品,看控制台的打印,每次都是以 5 为倍数增长每点击一次 checkbox,都会触发所有子组件的重新渲染。

如果我们有 50 个商品在购物车中,我们改了其中某一项的checked状态,也会导致 50 个子组件重新渲染。

我们想到了一个 api: React.memo,这个 api 基本等效于 class 组件中的shouldComponentUpdate,如果我们用这个 api 让子组件只有在 checked 发生改变的时候再重新渲染呢?

好,我们进入子组件的编写:

// memo优化策略
function areEqual(prevProps: Props, nextProps: Props) {
  return prevProps.checked === nextProps.checked
}

const ItemCard: FC<Props> = React.memo((props) => {
  const { checked, onCheckedChange } = props
  return (
    <div>
      <checkbox
        value={checked}
        onChange={(value) => onCheckedChange(cartItem, value)}
      />
      <span>商品</span>
    </div>
  )
}, areEqual)

在这种优化策略下,我们认为只要前后两次渲染传入的 props 中的checked相等,那么就不去重新渲染子组件。

React Hook 的陈旧值导致的 bug

到这里就完成了吗?其实,这里是有 bug 的。

我们来看一下 bug 还原:

gif3

如果我们先点击了第一个商品的勾选,再点击第二个商品的勾选,你会发现第一个商品的勾选状态没了。

在勾选了第一个商品后,我们此时的最新的checkedMap其实是

{ 1: true }

而由于我们的优化策略,第二个商品在第一个商品勾选后没有重新渲染,

注意 React 的函数式组件,在每次渲染的时候都会重新执行,从而产生一个闭包环境。

所以第二个商品拿到的onCheckedChange还是前一次渲染购物车这个组件的函数闭包中的,那么checkedMap自然也是上一次函数闭包中的最初的空对象。

const onCheckedChange: OnCheckedChange = (cartItem, checked) => {
  const { id } = cartItem
  // 注意,这里的checkedMap还是最初的空对象!!
  const newCheckedMap = Object.assign({}, checkedMap, {
    [id]: checked,
  })
  setCheckedMap(newCheckedMap)
}

因此,第二个商品勾选后,没有按照预期的计算出正确的checkedMap

{
  1: true,
  2: true
}

而是计算出了错误的

{ 2: true }

这就导致了第一个商品的勾选状态被丢掉了。

这也是 React Hook 的闭包带来的臭名昭著陈旧值的问题。

那么此时有一个简单的解决方案,在父组件中用React.useRef把函数通过一个引用来传递给子组件。

由于ref在 React 组件的整个生命周期中只存在一个引用,因此通过 current 永远是可以访问到引用中最新的函数值的,不会存在闭包陈旧值的问题。

  // 要把ref传给子组件 这样才能保证子组件能在不重新渲染的情况下拿到最新的函数引用
  const onCheckedChangeRef = React.useRef(onCheckedChange)
  // 注意要在每次渲染后把ref中的引用指向当次渲染中最新的函数。
  useEffect(() => {
    onCheckedChangeRef.current = onCheckedChange
  })

  return (
    <ItemCard
      key={id}
      cartItem={cartItem}
      checked={checked}
+     onCheckedChangeRef={onCheckedChangeRef}
    />
  )

子组件

// memo优化策略
function areEqual(prevProps: Props, nextProps: Props) {
  return prevProps.checked === nextProps.checked
}

const ItemCard: FC<Props> = React.memo((props) => {
  const { checked, onCheckedChangeRef } = props
  return (
    <div>
      <checkbox
        value={checked}
        onChange={(value) => onCheckedChangeRef.current(cartItem, value)}
      />
      <span>商品</span>
    </div>
  )
}, areEqual)

到此时,我们的简单的性能优化就完成了。

自定义 hook 之 useChecked

那么下一个场景,又遇到这种全选反选类似的需求,难道我们再这样重复写一套吗?这是不可接受的,我们用自定义 hook 来抽象这些数据以及行为。

并且这次我们通过 useReducer 来避免闭包旧值的陷阱(dispatch 在组件的生命周期中保持唯一引用,并且总是能操作到最新的值)。

import { useReducer, useEffect, useCallback } from 'react'

interface Option {
  /** 用来在map中记录勾选状态的key 一般取id */
  key?: string
}

type CheckedMap = {
  [key: string]: boolean
}

const CHECKED_CHANGE = 'CHECKED_CHANGE'

const CHECKED_ALL_CHANGE = 'CHECKED_ALL_CHANGE'

const SET_CHECKED_MAP = 'SET_CHECKED_MAP'

type CheckedChange<T> = {
  type: typeof CHECKED_CHANGE
  payload: {
    dataItem: T
    checked: boolean
  }
}

type CheckedAllChange = {
  type: typeof CHECKED_ALL_CHANGE
  payload: boolean
}

type SetCheckedMap = {
  type: typeof SET_CHECKED_MAP
  payload: CheckedMap
}

type Action<T> = CheckedChange<T> | CheckedAllChange | SetCheckedMap
export type OnCheckedChange<T> = (item: T, checked: boolean) => any

/**
 * 提供勾选、全选、反选等功能
 * 提供筛选勾选中的数据的函数
 * 在数据更新的时候自动剔除陈旧项
 */
export const useChecked = <T extends Record<string, any>>(
  dataSource: T[],
  { key = 'id' }: Option = {}
) => {
  const [checkedMap, dispatch] = useReducer(
    (checkedMapParam: CheckedMap, action: Action<T>) => {
      switch (action.type) {
        case CHECKED_CHANGE: {
          const { payload } = action
          const { dataItem, checked } = payload
          const { [key]: id } = dataItem
          return {
            ...checkedMapParam,
            [id]: checked,
          }
        }
        case CHECKED_ALL_CHANGE: {
          const { payload: newCheckedAll } = action
          const newCheckedMap: CheckedMap = {}
          // 全选
          if (newCheckedAll) {
            dataSource.forEach((dataItem) => {
              newCheckedMap[dataItem.id] = true
            })
          }
          return newCheckedMap
        }
        case SET_CHECKED_MAP: {
          return action.payload
        }
        default:
          return checkedMapParam
      }
    },
    {}
  )

  /** 勾选状态变更 */
  const onCheckedChange: OnCheckedChange<T> = useCallback(
    (dataItem, checked) => {
      dispatch({
        type: CHECKED_CHANGE,
        payload: {
          dataItem,
          checked,
        },
      })
    },
    []
  )

  type FilterCheckedFunc = (item: T) => boolean
  /** 筛选出勾选项 可以传入filter函数继续筛选 */
  const filterChecked = useCallback(
    (func: FilterCheckedFunc = () => true) => {
      return (
        Object.entries(checkedMap)
          .filter((entries) => Boolean(entries[1]))
          .map(([checkedId]) =>
            dataSource.find(({ [key]: id }) => id === Number(checkedId))
          )
          // 有可能勾选了以后直接删除 此时id虽然在checkedMap里 但是dataSource里已经没有这个数据了
          // 先把空项过滤掉 保证外部传入的func拿到的不为undefined
          .filter(Boolean)
          .filter(func)
      )
    },
    [checkedMap, dataSource, key]
  )
  /** 是否全选状态 */
  const checkedAll =
    dataSource.length !== 0 && filterChecked().length === dataSource.length

  /** 全选反选函数 */
  const onCheckedAllChange = (newCheckedAll: boolean) => {
    dispatch({
      type: CHECKED_ALL_CHANGE,
      payload: newCheckedAll,
    })
  }

  // 数据更新的时候 如果勾选中的数据已经不在数据内了 就删除掉
  useEffect(() => {
    filterChecked().forEach((checkedItem) => {
      let changed = false
      if (!dataSource.find((dataItem) => checkedItem.id === dataItem.id)) {
        delete checkedMap[checkedItem.id]
        changed = true
      }
      if (changed) {
        dispatch({
          type: SET_CHECKED_MAP,
          payload: Object.assign({}, checkedMap),
        })
      }
    })
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dataSource])

  return {
    checkedMap,
    dispatch,
    onCheckedChange,
    filterChecked,
    onCheckedAllChange,
    checkedAll,
  }
}

这时候在组件内使用,就很简单了:

const {
  checkedAll,
  checkedMap,
  onCheckedAllChange,
  onCheckedChange,
  filterChecked,
} = useChecked(cartData)

我们在自定义 hook 里把复杂的业务逻辑全部做掉了,包括数据更新后的无效 id 剔除等等。快去推广给团队的小伙伴,让他们早点下班吧。

自定义 hook 之 useMap

有一天,突然又来了个需求,我们需要用一个 map 来根据购物车商品的 id 来记录另外的一些东西,我们突然发现,上面的自定义 hook 把 map 的处理等等逻辑也都打包进去了,我们只能给 map 的值设为true / false,灵活性不够。

我们进一步把useMap也抽出来,然后让useCheckedMap基于它之上开发。

useMap

import { useReducer, useEffect, useCallback } from 'react'

export interface Option {
  /** 用来在map中作为key 一般取id */
  key?: string
}

export type MapType = {
  [key: string]: any
}

export const CHANGE = 'CHANGE'

export const CHANGE_ALL = 'CHANGE_ALL'

export const SET_MAP = 'SET_MAP'

export type Change<T> = {
  type: typeof CHANGE
  payload: {
    dataItem: T
    value: any
  }
}

export type ChangeAll = {
  type: typeof CHANGE_ALL
  payload: any
}

export type SetCheckedMap = {
  type: typeof SET_MAP
  payload: MapType
}

export type Action<T> = Change<T> | ChangeAll | SetCheckedMap
export type OnValueChange<T> = (item: T, value: any) => any

/**
 * 提供map操作的功能
 * 在数据更新的时候自动剔除陈旧项
 */
export const useMap = <T extends Record<string, any>>(
  dataSource: T[],
  { key = 'id' }: Option = {}
) => {
  const [map, dispatch] = useReducer(
    (checkedMapParam: MapType, action: Action<T>) => {
      switch (action.type) {
        // 单值改变
        case CHANGE: {
          const { payload } = action
          const { dataItem, value } = payload
          const { [key]: id } = dataItem
          return {
            ...checkedMapParam,
            [id]: value,
          }
        }
        // 所有值改变
        case CHANGE_ALL: {
          const { payload } = action
          const newMap: MapType = {}
          dataSource.forEach((dataItem) => {
            newMap[dataItem[key]] = payload
          })
          return newMap
        }
        // 完全替换map
        case SET_MAP: {
          return action.payload
        }
        default:
          return checkedMapParam
      }
    },
    {}
  )

  /** map某项的值变更 */
  const onMapValueChange: OnValueChange<T> = useCallback((dataItem, value) => {
    dispatch({
      type: CHANGE,
      payload: {
        dataItem,
        value,
      },
    })
  }, [])

  // 数据更新的时候 如果map中的数据已经不在dataSource内了 就删除掉
  useEffect(() => {
    dataSource.forEach((checkedItem) => {
      let changed = false
      if (
        // map中包含此项
        // 并且数据源中找不到此项了
        checkedItem[key] in map &&
        !dataSource.find((dataItem) => checkedItem[key] === dataItem[key])
      ) {
        delete map[checkedItem[key]]
        changed = true
      }
      if (changed) {
        dispatch({
          type: SET_MAP,
          payload: Object.assign({}, map),
        })
      }
    })
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [dataSource])

  return {
    map,
    dispatch,
    onMapValueChange,
  }
}

这是一个通用的 map 操作的自定义 hook,它考虑了闭包陷阱,考虑了旧值的删除。

在此之上,我们实现上面的useChecked

useChecked

import { useCallback } from 'react'
import { useMap, CHANGE_ALL, Option } from './use-map'

type CheckedMap = {
  [key: string]: boolean;
}

export type OnCheckedChange<T> = (item: T, checked: boolean) => any

/**
 * 提供勾选、全选、反选等功能
 * 提供筛选勾选中的数据的函数
 * 在数据更新的时候自动剔除陈旧项
 */
export const useChecked = <T extends Record<string, any>>(
  dataSource: T[],
  option: Option = {}
) => {
  const { map: checkedMap, onMapValueChange, dispatch } = useMap(
    dataSource,
    option
  )
  const { key = 'id' } = option

  /** 勾选状态变更 */
  const onCheckedChange: OnCheckedChange<T> = useCallback(
    (dataItem, checked) => {
      onMapValueChange(dataItem, checked)
    },
    [onMapValueChange]
  )

  type FilterCheckedFunc = (item: T) => boolean
  /** 筛选出勾选项 可以传入filter函数继续筛选 */
  const filterChecked = useCallback(
    (func?: FilterCheckedFunc) => {
      const checkedDataSource = dataSource.filter(item =>
        Boolean(checkedMap[item[key]])
      )
      return func ? checkedDataSource.filter(func) : checkedDataSource
    },
    [checkedMap, dataSource, key]
  )
  /** 是否全选状态 */
  const checkedAll =
    dataSource.length !== 0 && filterChecked().length === dataSource.length

  /** 全选反选函数 */
  const onCheckedAllChange = (newCheckedAll: boolean) => {
    // 全选
    const payload = !!newCheckedAll
    dispatch({
      type: CHANGE_ALL,
      payload,
    })
  }

  return {
    checkedMap: checkedMap as CheckedMap,
    dispatch,
    onCheckedChange,
    filterChecked,
    onCheckedAllChange,
    checkedAll,
  }
}

总结

本文通过一个真实的购物车需求,一步一步的完成优化、踩坑,在这个过程中,我们对 React Hook 的优缺点一定也有了进一步的认识。

在利用自定义 hook 把通用逻辑抽取出来后,我们业务组件内的代码量大大的减少了,并且其他相似的场景都可以去复用。

React Hook 带来了一种新的开发模式,但是也带来了一些陷阱,它是一把双刃剑,如果你能合理使用,那么它会给你带来很强大的力量。

感谢你的阅读,希望这篇文章可以给你启发。

查看原文

赞 11 收藏 7 评论 0

安于现状i 赞了文章 · 2020-12-01

JavaScript数组去重(12种方法,史上最全)

数组去重,一般都是在面试的时候才会碰到,一般是要求手写数组去重方法的代码。如果是被提问到,数组去重的方法有哪些?你能答出其中的10种,面试官很有可能对你刮目相看。
在真实的项目中碰到的数组去重,一般都是后台去处理,很少让前端处理数组去重。虽然日常项目用到的概率比较低,但还是需要了解一下,以防面试的时候可能回被问到。

注:写的匆忙,加上这几天有点忙,还没有非常认真核对过,不过思路是没有问题,可能一些小细节出错而已。

数组去重的方法

一、利用ES6 Set去重(ES6中最常用)

function unique (arr) {
  return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
 //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

不考虑兼容性,这种去重的方法代码最少。这种方法还无法去掉“{}”空对象,后面的高阶方法会添加去掉重复“{}”的方法。

二、利用for嵌套for,然后splice去重(ES5中最常用)

function unique(arr){            
        for(var i=0; i<arr.length; i++){
            for(var j=i+1; j<arr.length; j++){
                if(arr[i]==arr[j]){         //第一个等同于第二个,splice方法删除第二个
                    arr.splice(j,1);
                    j--;
                }
            }
        }
return arr;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
    //[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}]     //NaN和{}没有去重,两个null直接消失了

双层循环,外层循环元素,内层循环时比较值。值相同时,则删去这个值。
想快速学习更多常用的ES6语法,可以看我之前的文章《学习ES6笔记──工作中常用到的ES6语法》

三、利用indexOf去重

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array = [];
    for (var i = 0; i < arr.length; i++) {
        if (array .indexOf(arr[i]) === -1) {
            array .push(arr[i])
        }
    }
    return array;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
   // [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]  //NaN、{}没有去重

新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组。

四、利用sort()

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return;
    }
    arr = arr.sort()
    var arrry= [arr[0]];
    for (var i = 1; i < arr.length; i++) {
        if (arr[i] !== arr[i-1]) {
            arrry.push(arr[i]);
        }
    }
    return arrry;
}
     var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
        console.log(unique(arr))
// [0, 1, 15, "NaN", NaN, NaN, {…}, {…}, "a", false, null, true, "true", undefined]      //NaN、{}没有去重

利用sort()排序方法,然后根据排序后的结果进行遍历及相邻元素比对。

五、利用对象的属性不能相同的特点进行去重(这种数组去重的方法有问题,不建议用,有待改进)

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var arrry= [];
     var  obj = {};
    for (var i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) {
            arrry.push(arr[i])
            obj[arr[i]] = 1
        } else {
            obj[arr[i]]++
        }
    }
    return arrry;
}
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
        console.log(unique(arr))
//[1, "true", 15, false, undefined, null, NaN, 0, "a", {…}]    //两个true直接去掉了,NaN和{}去重

六、利用includes

function unique(arr) {
    if (!Array.isArray(arr)) {
        console.log('type error!')
        return
    }
    var array =[];
    for(var i = 0; i < arr.length; i++) {
            if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值
                    array.push(arr[i]);
              }
    }
    return array
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
    //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]     //{}没有去重

七、利用hasOwnProperty

function unique(arr) {
    var obj = {};
    return arr.filter(function(item, index, arr){
        return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
    })
}
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
        console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}]   //所有的都去重了

利用hasOwnProperty 判断是否存在对象属性

八、利用filter

function unique(arr) {
  return arr.filter(function(item, index, arr) {
    //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
    return arr.indexOf(item, 0) === index;
  });
}
    var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
        console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {…}, {…}]

九、利用递归去重

function unique(arr) {
        var array= arr;
        var len = array.length;

    array.sort(function(a,b){   //排序后更加方便去重
        return a - b;
    })

    function loop(index){
        if(index >= 1){
            if(array[index] === array[index-1]){
                array.splice(index,1);
            }
            loop(index - 1);    //递归loop,然后数组去重
        }
    }
    loop(len-1);
    return array;
}
 var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]

十、利用Map数据结构去重

function arrayNonRepeatfy(arr) {
  let map = new Map();
  let array = new Array();  // 数组用于返回结果
  for (let i = 0; i < arr.length; i++) {
    if(map .has(arr[i])) {  // 如果有该key值
      map .set(arr[i], true); 
    } else { 
      map .set(arr[i], false);   // 如果没有该key值
      array .push(arr[i]);
    }
  } 
  return array ;
}
 var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]

创建一个空Map数据结构,遍历需要去重的数组,把数组的每一个元素作为key存到Map中。由于Map中不会出现相同的key值,所以最终得到的就是去重后的结果。

十一、利用reduce+includes

function unique(arr){
    return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]

十二、[...new Set(arr)]

[...new Set(arr)] 
//代码就是这么少----(其实,严格来说并不算是一种,相对于第一种方法来说只是简化了代码)

PS:有些文章提到了foreach+indexOf数组去重的方法,个人觉得都是大同小异,所以没有写上去。

查看原文

赞 547 收藏 426 评论 47

安于现状i 关注了问题 · 2020-10-27

react hooks 的 useState 如何获取最新的 state?

直接上代码吧,类似于之前class组件this.setState的第二个callback参数

import React, { useState, useEffect } from 'react';
import fetchData from 'services';

function useList() {
  const [params, setParams] = useState({});
  const [data, setData] = useState({});
  const getData = () => {
    fetchData(params).then(res => {
      setData(res);
    });
  };
  useEffect(() => {
    getData();
  }, []);
  return {
    params,
    setParams,
    data,
    setData,
    getData,
  };
}

function Foo() {
  const { setParams, getData } = useList();
  const handleClick = () => {
    setParams({ mockData: 2333333 });
    // 如何让getData里params是最新的???
    getData();
  };
  return (
    <button onClick={handleClick}>click me</button>
  );
}

关注 5 回答 4

安于现状i 关注了问题 · 2020-09-28

解决nginx代理api接口地址,但是接口一直404

环境:

在同一台linux服务器上部署了两个node服务,一个服务监听4000端口,负责渲染ejs模板,一个服务监听8080端口,负责提供api

nginx配置:

location / {
    proxy_pass  http://58.87.67.xx:4000;   #node server
}

location api/ {
    rewrite  ^.+api/?(.*)$ /$1 break;
    proxy_pass  http://58.87.67.xx:8080;    #node api server
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

服务器已添加https证书
需求:

 在前端发起以api开头的请求(如/api/index),nginx rewrite请求地址转发至8080端口服务,然后node来负责请求处理,重写之后的地址则没有/api(如/index)

现遇到的问题是请求的接口全部404,不知道是不是匹配规则写的是否有问题,也查了很多文档,但是都没有解决问题,还请各位大佬帮忙看一下,多谢!

关注 5 回答 2

安于现状i 赞了文章 · 2020-09-26

彻彻底底教会你使用Redux-saga(包含样例代码)

Redux-saga使用心得总结(包含样例代码),

本文的原文地址:原文地址

本文的样例代码地址:样例代码地址 ,欢迎star


最近将项目中redux的中间件,从redux-thunk替换成了redux-saga,做个笔记总结一下redux-saga的使用心得,阅读本文需要了解什么是redux,redux中间件的用处是什么?如果弄懂上述两个概念,就可以继续阅读本文。

  • redux-thunk处理副作用的缺点
  • redux-saga写一个hellosaga
  • redux-saga的使用技术细节
  • redux-saga实现一个登陆和列表样例

1.redux-thunk处理副作用的缺点

(1)redux的副作用处理

redux中的数据流大致是:

UI—————>action(plain)—————>reducer——————>state——————>UI

default

redux是遵循函数式编程的规则,上述的数据流中,action是一个原始js对象(plain object)且reducer是一个纯函数,对于同步且没有副作用的操作,上述的数据流起到可以管理数据,从而控制视图层更新的目的。

但是如果存在副作用,比如ajax异步请求等等,那么应该怎么做?

如果存在副作用函数,那么我们需要首先处理副作用函数,然后生成原始的js对象。如何处理副作用操作,在redux中选择在发出action,到reducer处理函数之间使用中间件处理副作用。

redux增加中间件处理副作用后的数据流大致如下:

UI——>action(side function)—>middleware—>action(plain)—>reducer—>state—>UI

default

在有副作用的action和原始的action之间增加中间件处理,从图中我们也可以看出,中间件的作用就是:

转换异步操作,生成原始的action,这样,reducer函数就能处理相应的action,从而改变state,更新UI。

(2)redux-thunk

在redux中,thunk是redux作者给出的中间件,实现极为简单,10多行代码:

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

这几行代码做的事情也很简单,判别action的类型,如果action是函数,就调用这个函数,调用的步骤为:

action(dispatch, getState, extraArgument);

发现实参为dispatch和getState,因此我们在定义action为thunk函数是,一般形参为dispatch和getState。

(3)redux-thunk的缺点

hunk的缺点也是很明显的,thunk仅仅做了执行这个函数,并不在乎函数主体内是什么,也就是说thunk使
得redux可以接受函数作为action,但是函数的内部可以多种多样。比如下面是一个获取商品列表的异步操作所对应的action:

export default ()=>(dispatch)=>{
    fetch('/api/goodList',{ //fecth返回的是一个promise
      method: 'get',
      dataType: 'json',
    }).then(function(json){
      var json=JSON.parse(json);
      if(json.msg==200){
        dispatch({type:'init',data:json.data});
      }
    },function(error){
      console.log(error);
    });
};

从这个具有副作用的action中,我们可以看出,函数内部极为复杂。如果需要为每一个异步操作都如此定义一个action,显然action不易维护。

action不易维护的原因:

  • action的形式不统一
  • 就是异步操作太为分散,分散在了各个action中

2.redux-saga写一个hellosaga

跟redux-thunk,redux-saga是控制执行的generator,在redux-saga中action是原始的js对象,把所有的异步副作用操作放在了saga函数里面。这样既统一了action的形式,又使得异步操作集中可以被集中处理。

redux-saga是通过genetator实现的,如果不支持generator需要通过插件babel-polyfill转义。我们接着来实现一个输出hellosaga的例子。

(1)创建一个helloSaga.js文件

export function * helloSaga() {
  console.log('Hello Sagas!');
}

(2)在redux中使用redux-saga中间件

在main.js中:

import { createStore, applyMiddleware } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { helloSaga } from './sagas'
const sagaMiddleware=createSagaMiddleware();
const store = createStore(
 reducer,
 applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(helloSaga);
//会输出Hello, Sagas!

和调用redux的其他中间件一样,如果想使用redux-saga中间件,那么只要在applyMiddleware中调用一个createSagaMiddleware的实例。唯一不同的是需要调用run方法使得generator可以开始执行。

3.redux-saga的使用技术细节

redux-saga除了上述的action统一、可以集中处理异步操作等优点外,redux-saga中使用声明式的Effect以及提供了更加细腻的控制流。

(1)声明式的Effect

redux-saga中最大的特点就是提供了声明式的Effect,声明式的Effect使得redux-saga监听原始js对象形式的action,并且可以方便单元测试,我们一一来看。

  • 首先,在redux-saga中提供了一系列的api,比如take、put、all、select等API ,在redux-saga中将这一系列的api都定义为Effect。这些Effect执行后,当函数resolve时返回一个描述对象,然后redux-saga中间件根据这个描述对象恢复执行generator中的函数。

首先来看redux-thunk的大体过程:

action1(side function)—>redux-thunk监听—>执行相应的有副作用的方法—>action2(plain object)

2

转化到action2是一个原始js对象形式的action,然后执行reducer函数就会更新store中的state。

而redux-saga的大体过程如下:

action1(plain object)——>redux-saga监听—>执行相应的Effect方法——>返回描述对象—>恢复执行异步和副作用函数—>action2(plain object)

default

对比redux-thunk我们发现,redux-saga中监听到了原始js对象action,并不会马上执行副作用操作,会先通过Effect方法将其转化成一个描述对象,然后再将描述对象,作为标识,再恢复执行副作用函数。

通过使用Effect类函数,可以方便单元测试,我们不需要测试副作用函数的返回结果。只需要比较执行Effect方法后返回的描述对象,与我们所期望的描述对象是否相同即可。

举例来说,call方法是一个Effect类方法:

import { call } from 'redux-saga/effects'

function* fetchProducts() {
  const products = yield call(Api.fetch, '/products')
  // ...
}

上述代码中,比如我们需要测试Api.fetch返回的结果是否符合预期,通过调用call方法,返回一个描述对象。这个描述对象包含了所需要调用的方法和执行方法时的实际参数,我们认为只要描述对象相同,也就是说只要调用的方法和执行该方法时的实际参数相同,就认为最后执行的结果肯定是满足预期的,这样可以方便的进行单元测试,不需要模拟Api.fetch函数的具体返回结果。

import { call } from 'redux-saga/effects'
import Api from '...'

const iterator = fetchProducts()

// expects a call instruction
assert.deepEqual(
  iterator.next().value,
  call(Api.fetch, '/products'),
  "fetchProducts should yield an Effect call(Api.fetch, './products')"
)

(2)Effect提供的具体方法

下面来介绍几个Effect中常用的几个方法,从低阶的API,比如take,call(apply),fork,put,select等,以及高阶API,比如takeEvery和takeLatest等,从而加深对redux-saga用法的认识(这节可能比较生涩,在第三章中会结合具体的实例来分析,本小节先对各种Effect有一个初步的了解)。

引入:

import {take,call,put,select,fork,takeEvery,takeLatest} from 'redux-saga/effects'
  • take

take这个方法,是用来监听action,返回的是监听到的action对象。比如:

const loginAction = {
   type:'login'
}

在UI Component中dispatch一个action:

dispatch(loginAction)

在saga中使用:

const action = yield take('login');

可以监听到UI传递到中间件的Action,上述take方法的返回,就是dipath的原始对象。一旦监听到login动作,返回的action为:

{
  type:'login'
}
  • call(apply)

call和apply方法与js中的call和apply相似,我们以call方法为例:

call(fn, ...args)

call方法调用fn,参数为args,返回一个描述对象。不过这里call方法传入的函数fn可以是普通函数,也可以是generator。call方法应用很广泛,在redux-saga中使用异步请求等常用call方法来实现。

yield call(fetch,'/userInfo',username)

  • put

在前面提到,redux-saga做为中间件,工作流是这样的:

UI——>action1————>redux-saga中间件————>action2————>reducer..

从工作流中,我们发现redux-saga执行完副作用函数后,必须发出action,然后这个action被reducer监听,从而达到更新state的目的。相应的这里的put对应与redux中的dispatch,工作流程图如下:

default

从图中可以看出redux-saga执行副作用方法转化action时,put这个Effect方法跟redux原始的dispatch相似,都是可以发出action,且发出的action都会被reducer监听到。put的使用方法:

 yield put({type:'login'})
  • select

put方法与redux中的dispatch相对应,同样的如果我们想在中间件中获取state,那么需要使用select。select方法对应的是redux中的getState,用户获取store中的state,使用方法:

const state= yield select()
  • fork

fork方法在第三章的实例中会详细的介绍,这里先提一笔,fork方法相当于web work,fork方法不会阻塞主线程,在非阻塞调用中十分有用。

  • takeEvery和takeLatest

takeEvery和takeLatest用于监听相应的动作并执行相应的方法,是构建在take和fork上面的高阶api,比如要监听login动作,好用takeEvery方法可以:

takeEvery('login',loginFunc)

takeEvery监听到login的动作,就会执行loginFunc方法,除此之外,takeEvery可以同时监听到多个相同的action。

takeLatest方法跟takeEvery是相同方式调用:

takeLatest('login',loginFunc)

与takeLatest不同的是,takeLatest是会监听执行最近的那个被触发的action。

4.redux-saga实现一个登陆和列表样例

接着我们来实现一个redux-saga样例,存在一个登陆页,登陆成功后,显示列表页,并且,在列表页,可

以点击登出,返回到登陆页。例子的最终展示效果如下:

login

样例的功能流程图为:

default

接着我们按照上述的流程来一步步的实现所对应的功能。

(1)LoginPanel(登陆页)

登陆页的功能包括

  • 输入时时保存用户名
  • 输入时时保存密码
  • 点击sign in 请求判断是否登陆成功

I)输入时时保存用户名和密码

用户名输入框和密码框onchange时触发的函数为:

 changeUsername:(e)=>{
    dispatch({type:'CHANGE_USERNAME',value:e.target.value});
 },
changePassword:(e)=>{
  dispatch({type:'CHANGE_PASSWORD',value:e.target.value});
}

在函数中最后会dispatch两个action:CHANGE_USERNAME和CHANGE_PASSWORD

在saga.js文件中监听这两个方法并执行副作用函数,最后put发出转化后的action,给reducer函数调用:

function * watchUsername(){
  while(true){
    const action= yield take('CHANGE_USERNAME');
    yield put({type:'change_username',
    value:action.value});
  }
}
function * watchPassword(){
  while(true){
    const action=yield take('CHANGE_PASSWORD');
    yield put({type:'change_password',
    value:action.value});
  }
}

最后在reducer中接收到redux-saga的put方法传递过来的action:change_username和change_password,然后更新state。

II)监听登陆事件判断登陆是否成功

在UI中发出的登陆事件为:

toLoginIn:(username,password)=>{
  dispatch({type:'TO_LOGIN_IN',username,password});
}

登陆事件的action为:TO_LOGIN_IN.对于登入事件的处理函数为:

 while(true){
    //监听登入事件
    const action1=yield take('TO_LOGIN_IN');
    const res=yield call(fetchSmart,'/login',{
      method:'POST',
      body:JSON.stringify({
        username:action1.username,
        password:action1.password
    })
    if(res){
      put({type:'to_login_in'});
    }
});

在上述的处理函数中,首先监听原始动作提取出传递来的用户名和密码,然后请求是否登陆成功,如果登陆成功有返回值,则执行put的action:to_login_in.

(2) LoginSuccess(登陆成功列表展示页)

登陆成功后的页面功能包括:

  • 获取列表信息,展示列表信息
  • 登出功能,点击可以返回登陆页面

I)获取列表信息

import {delay} from 'redux-saga';

function * getList(){
  try {
   yield delay(3000);
   const res = yield call(fetchSmart,'/list',{
     method:'POST',
     body:JSON.stringify({})
   });
   yield put({type:'update_list',list:res.data.activityList});
 } catch(error) {
   yield put({type:'update_list_error', error});
 }
}

为了演示请求过程,我们在本地mock,通过redux-saga的工具函数delay,delay的功能相当于延迟xx秒,因为真实的请求存在延迟,因此可以用delay在本地模拟真实场景下的请求延迟。

II)登出功能

const action2=yield take('TO_LOGIN_OUT');
yield put({type:'to_login_out'});

与登入相似,登出的功能从UI处接受action:TO_LOGIN_OUT,然后转发action:to_login_out

(3) 完整的实现登入登出和列表展示的代码

function * getList(){
  try {
   yield delay(3000);
   const res = yield call(fetchSmart,'/list',{
     method:'POST',
     body:JSON.stringify({})
   });
   yield put({type:'update_list',list:res.data.activityList});
 } catch(error) {
   yield put({type:'update_list_error', error});
 }
}

function * watchIsLogin(){
  while(true){
    //监听登入事件
    const action1=yield take('TO_LOGIN_IN');
    
    const res=yield call(fetchSmart,'/login',{
      method:'POST',
      body:JSON.stringify({
        username:action1.username,
        password:action1.password
      })
    });
    
    //根据返回的状态码判断登陆是否成功
    if(res.status===10000){
      yield put({type:'to_login_in'});
      //登陆成功后获取首页的活动列表
      yield call(getList);
    }
    
    //监听登出事件
    const action2=yield take('TO_LOGIN_OUT');
    yield put({type:'to_login_out'});
  }
}

通过请求状态码判断登入是否成功,在登陆成功后,可以通过:

yield call(getList)

的方式调用获取活动列表的函数getList。这样咋一看没有什么问题,但是注意call方法调用是会阻塞主线程的,具体来说:

  • 在call方法调用结束之前,call方法之后的语句是无法执行的
  • 如果call(getList)存在延迟,call(getList)之后的语句 const action2=yieldtake('TO_LOGIN_OUT')在call方法返回结果之前无法执行
  • 在延迟期间的登出操作会被忽略。

用框图可以更清楚的分析:

default

call方法调用阻塞主线程的具体效果如下动图所示:

login_1

白屏时为请求列表的等待时间,在此时,我们点击登出按钮,无法响应登出功能,直到请求列表成功,展示列表信息后,点击登出按钮才有相应的登出功能。也就是说call方法阻塞了主线程。

(4) 无阻塞调用

我们在第二章中,介绍了fork方法可以类似与web work,fork方法不会阻塞主线程。应用于上述例子,我们可以将:

yield call(getList)

修改为:

yield fork(getList)

这样展示的结果为:

login_2

通过fork方法不会阻塞主线程,在白屏时点击登出,可以立刻响应登出功能,从而返回登陆页面。

5.总结

通过上述章节,我们可以概括出redux-saga做为redux中间件的全部优点:

  • 统一action的形式,在redux-saga中,从UI中dispatch的action为原始对象
  • 集中处理异步等存在副作用的逻辑
  • 通过转化effects函数,可以方便进行单元测试
  • 完善和严谨的流程控制,可以较为清晰的控制复杂的逻辑。
查看原文

赞 44 收藏 37 评论 0

安于现状i 关注了问题 · 2020-09-20

为什么React会自动渲染JSX写法中的数组元素?

背景:
我定义了一个变量,想给它赋值两个同级的h1标签

clipboard.png

然后编译报错了,提示就是所谓的顶级标签只能一个的问题。

clipboard.png

但是我又不想在外面给它套一层标签,类似如下的形式:

options = (
  <div>
    <h1>titleOne</h1>
    <h1>titleTwo</h1>
  </div>
)

发现
测试了一会,便发现了下面的写法。如图将元素包括在数组中,便可以实现同级元素渲染了

clipboard.png

问题
觉得很神奇,遂来问问原理,react是如何神奇的识别 大括号里的数组元素,并依次遍历渲染出来的?

关注 4 回答 2

安于现状i 赞了文章 · 2020-09-11

基于vue-cli的webpack打包优化实践及探索

转眼已经是2019年,短短三四年时间,webpack打包工具成为了前端开发中必备工具,曾经一度的面试题都是问,请问前端页面优化的方式有哪些?大家也是能够信手拈来的说出缓存、压缩文件、CSS雪碧图以及部署CDN等等各种方法,但是今天不一样了,可能你去面试问的就是,请问你是否知道webpack的打包原理,webpack的打包优化方法有哪些?所以该说不说的,笔者闲着没事研究了一下webpack的打包优化,可能大家都有看过类似的优化文章~ 但是笔者还是希望能够给大家一些新的启发~

1、准备工作:测速与分析bundle

既然我们要优化webpack打包,肯定要提前对我们的bundle文件进行分析,分析各模块的大小,以及分析打包时间的耗时主要是在哪里,这里主要需要用到两个webpack插件,speed-measure-webpack-plugin和webpack-bundle-analyzer,前者用于测速,后者用于分析bundle文件。

具体配置

const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
const smp = new SpeedMeasurePlugin({
  outputFormat:"human",
});
module.exports = {
configureWebpack: smp.wrap({
    plugins: [
      new webpack.ProvidePlugin({
        $: "zepto",
        Zepto: "zepto",
      }),
      new BundleAnalyzerPlugin(),
    ],
    optimization: {
      splitChunks: {
        cacheGroups: {
          echarts: {
            name: "chunk-echarts",
            test: /[\\/]node_modules[\\/]echarts[\\/]/,
            chunks: "all",
            priority: 10,
            reuseExistingChunk: true,
            enforce: true,
          },
          demo: {
            name: "chunk-demo",
            test: /[\\/]src[\\/]views[\\/]demo[\\/]/,
            chunks: "all",
            priority: 20,
            reuseExistingChunk: true,
            enforce: true,
          },
          page: {
            name: "chunk-page",
            test: /[\\/]src[\\/]/,
            chunks: "all",
            priority: 10,
            reuseExistingChunk: true,
            enforce: true,
          },
          vendors: {
            name: "chunk-vendors",
            test: /[\\/]node_modules[\\/]/,
            chunks: "all",
            priority: 5,
            reuseExistingChunk: true,
            enforce: true,
          },
        },
      },
    },
  })
}

由于是基于vue-cli脚手架的,所以其实vue-cli中已经帮你做了一些优化的工作,可以看到,原先项目最初的配置设置了splitchunk,进行代码分割,这在大型项目中是很有必要的,毕竟你不希望你的用户阻塞加载一个5MB大小的JS文件,所以做代码分割和懒加载是很有必要的。
说远了,我们来看看这个配置,你需要用smp对配置进行再包裹,因为SpeedMeasurePlugin会对你的其他Plugin对象包裹一层代理,这样的目的是为了能够知道plugin开始和结束的时间~
其次,BundleAnalyzerPlugin就跟普通的plugin一样,加载plugins数组的后面即可。
接下来我们看一下最初的打包时间以及包内容分析:

image.png

image.png

可以看到项目中较大的三个包,其中两个包是我们的第三方依赖,three.js、lottie、lodash、echarts等。

2、开始逐步优化

2.1缩小文件查找和处理范围

这是webpack优化中的常规操作,基本就是对模块和文件查找的优化,以及减少loader对一些不必要模块的处理,但是vue-cli中的loader并没有暴露给我们操作,所以其内置的loader处理无法由我们进行优化,但是其实vue-cli中的配置项已经对loader的查找路径进行了优化,如果你的项目也是使用了vue-cli,你可以通过以下命令行查看你现有的配置文件是怎样的:

npx vue-cli-service inspect > output.js

具体可以翻阅vuecli官方文档。

resolve:{
  modules: [path.resolve(__dirname, 'node_modules')],
  alias:{
    'three':path.resolve(__dirname, './node_modules/three/build/three.min.js'),
    'zepto$':path.resolve(__dirname, './node_modules/zepto/dist/zepto.min.js'),
    'swiper$':path.resolve(__dirname, './node_modules/swiper/dist/js/swiper.min.js'),
    'lottie-web$':path.resolve(__dirname, './node_modules/lottie-web/build/player/lottie.min.js'),
    'lodash$':path.resolve(__dirname, './node_modules/lodash/lodash.min.js'),
  }
},
module:{
  noParse:/^(vue|vue-router|vuex|vuex-router-sync|three|zepto|swiper|lottie-web|lodash)$/
},
  • 通过modules指定查找第三方模块的路径。
  • 通过alias指定第三方模块直接查找到打包构建好的压缩js文件。
  • 通过module指定noparse,对第三方模块不再进行分析依赖。

优化效果:2s?
image.png

可以看到时间就减少了两三秒,在30s波动,感觉没有多大差别。

2.2尝试使用happypack

由于在进行webpack优化前,翻阅了很多有关webapck优化的文章,所以笔者也想尝试一下用happypack来优化打包时间。
在想要用happypack进行的打包之前,大抵有这两种说法:
1、webpack4中已经默认是多线程打包了,所以happypack打包效果不明显;
2、vue不支持happypack打包,需要设置thread-loader。
但是笔者想了一下,还是试试看把,大不了我只对JS和CSS文件设置happypack。
但是问题又来了,vue-cli内置封装了loader,这个时候我要怎么拿到它的配置,改写里面的loader配置呢。
通过翻阅vue-cli的官方文档我们可以看到以下使用介绍:

configureWebpack
Type: Object | Function
如果这个值是一个对象,则会通过 webpack-merge 合并到最终的配置中。
如果这个值是一个函数,则会接收被解析的配置作为参数。该函数及可以修改配置并不返回任何东西,也可以返回一个被克隆或合并过的配置版本。

为此,笔者特地调试进了vue-cli的源码一探究竟:
流程介绍:
由于我们执行命令行vue-cli-service build,其实是先去node_modules的.bin文件夹下查找相应的可执行文件,.bin下的vue-cli-service会映射到相应的第三方库内的执行文件。
所以我们可以找到这个可执行文件的地址:
/node_modules/@vue/cli-service/bin/vue-cli-service.js
找到了入口,接下来我们想要进入nodejs的调试,在以往的开发中,我们会通过node --inspect app.js的方式启动一个后台服务,然后在谷歌浏览器里进入调试界面(F12选择绿色的那个小按钮)
但是这里却犯了难,由于我们的打包构建是一次执行的,不同于一个后台服务,是实时监听的,服务一直启动着。查阅了一下,如果是普通的nodejs文件想要调试的话,需要通过这样的方式:

node --inspect-brk=9229 app.js

所以,为了强行走进去vue-cli的源码进行调试,可看vue-cli的处理流程,我们需要这样输入以下命令行:

node --inspect-brk=9229 node_modules/@vue/cli-service/bin/vue-cli-service.js build 

上面的这个命令行,等价于vue-cli-service build。
通过这样的方式,我们终于走进了vue-cli的源码,看了它的执行流程,你可以在对应的位置打下断点,查看此时的作用域内的变量数据。
image.png
可以看到vue-cli源码里的这一段操作,会执行我们传入的函数,判断函数有没有返回值来决定是否要merge进其内部配置的config。
通过这段代码我们可以看出,如果我们configWepack配置为函数,之后通过参数的形式获取到config配置项,本身是一个对象,对象是保留引用的形式,所以如果我们直接对传入的config对象进行修改,就可以实现我们最初的目标!修改vue-cli内置的loader!
当然,除了断点进入里面看配置,刚才也说了,我们可以通过命令行输出为一个output文件查看现有的配置。
这里可以给大家截图看一下vue-cli内部的配置:
image.png
可能有点废话了,但是通过断点的方式,我们可以看到vue-cli其实已经对js文件设置了exclude,同时也帮我们设置好了cache-loader,意味着webpack常规的优化方式之一,使用cache-loader缓存它也帮我们做了。
回到最初的起点,我们想要处理的是针对JS和CSS的loader,于是模仿大多数的配置,我进行了以下修改:

  configureWebpack:(config)=>{
    console.log("webpack config start");
    let originCssRuleLoader = config.module.rules[6].oneOf[0].use;
    let newCssRuleLoader = 'happypack/loader?id=css';
    config.module.rules[6].oneOf[0].use = newCssRuleLoader
    config.module.rules[6].oneOf[1].use = newCssRuleLoader
    config.module.rules[6].oneOf[2].use = newCssRuleLoader
    config.module.rules[6].oneOf[3].use = newCssRuleLoader
    ...//other code
 }

尝试对css的loader配置进行修改。之后对plugins进行一下配置:

plugins: [
    new HappyPack({
      id: 'css',
      threads: 4,
      loaders: originCssRuleLoader
    }),
  ],

本以为这样就OK了,但是很遗憾的告诉大家,报错了...
image.png
可以看到报错的内容,是在处理vue文件的时候,出了错误。
如何解决
笔者百度了,也谷歌了,大抵是说happypack不支持vue-loader,同时,根据报错也查了一下处理的方案,通过设置parallel参数,也还是无效。
笔者甚至怀疑是自己的happypack配置不对,于是我把配置原样移植配置到另一个非vue项目中,一切运行正常。
答案:此题无解~
原因分析:
由于vue文件中会含有CSS,所以vue-loader会提取出其中的css,交给其他loader处理,vue-loader-plugin会通过在vue文件后面加上查询字符串来告诉其他loader,针对这个文件要做处理。意味着什么呢?我们的vue-loader在处理文件的时候,通知其他loader处理,但是此时的loader配置已经被我们改写成了happypack,而vue又与happypack不兼容,最终导致了报错。很遗憾的告诉大家,vue-cli接入happypack--失败。
(注:这一部分主要是笔者在webpack优化过程中的探索,虽然最终不能让自己的webpack打包很好的优化,但是在这个探索的过程中,我们也可以学到很多~包括 vue-cli对配置对象的处理?如何调试普通文件nodejs代码?vue-loader中对vue文件的处理流程?vue-loader-plugin帮我们做了什么事?而这些都是要自己慢慢翻阅,慢慢踩坑去了解的~)

2.3使用dllplugin

和大多数的webpack优化教程一样,笔者也尝试了利用dllplugin进行优化,该插件的本质,是提取出我们常用的第三方模块,单独打成一个文件包,之后插入到我们的html页面中,这样我们以后每次打包,都不需要针对第三方模块进行处理,毕竟第三方模块动辄成千上万行。
流程介绍:

  • 1、配置webpack.dll.js针对第三方库打包
  • 2、vue.config.js中配置plugin
  • 3、html中引入dll打包出来的js文件。(一般采用部署CDN的方式)

由于项目中有很多大型的第三方库,类似three、echart等,所以笔者进行了以下配置:(webpack.dll.js)

const webpack = require("webpack")
const path = require("path")
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    entry: {
        vuebundle: [
            'vue',
            'vue-router',
            'vuex',
        ],
        utils:[
            'lodash',
            'swiper',
            'lottie-web',
            'three',
        ],
        echarts:[
            'echarts/lib/echarts',
            "echarts/lib/chart/bar",
            "echarts/lib/chart/line",
            "echarts/lib/component/tooltip",
            "echarts/lib/component/title",
            "echarts/lib/component/legend",
        ]

    },
    output: {
        path: path.resolve(__dirname, './static/'),
        filename: '[name].dll.js',
        library: '[name]_library'
    },
    plugins: [
        new webpack.DllPlugin({
            path: path.join(__dirname, 'build', '[name]-manifest.json'),
            name: '[name]_library'
        })
    ]
}

针对不同的库的大小进行划分,打了三个包,为啥不打成一个包?一个包那就太大了,你并不希望你的用户加载一个大型JS文件包而阻塞,影响页面性能。
接下里是vue.config.js的配置:

plugins: [
      new webpack.ProvidePlugin({
        $: "zepto",
        Zepto: "zepto",
      }),
      new DllReferencePlugin({
        manifest: require('./build/echarts-manifest.json'),
      }),
      new DllReferencePlugin({
        manifest: require('./build/utils-manifest.json'),
      }),
      new DllReferencePlugin({
        manifest: require('./build/vuebundle-manifest.json'),
      }),
      new BundleAnalyzerPlugin(),
    ]

引入了DllPlugin。接下来配置HTML:
(由于笔者没将DLL打包出来的js文件上传到CDN,所以只能本地自己起个node服务器返回静态资源了)

  <body>
     <div id="app"></div>
    <!-- built files will be auto injected -->
    <script type="text/javascript" data-original="http://localhost:3000/echarts.dll.js"></script>
    <script type="text/javascript" data-original="http://localhost:3000/utils.dll.js"></script>
    <script type="text/javascript" data-original="http://localhost:3000/vuebundle.dll.js"></script>
  </body>

然后npm run serve,开始页面调试和开发~
舒服~
优化结果:
image.png
由于少了大型第三方库,所以时间控制在了20s左右了。优化相对比较明显~

3、优化与探索总结

优化到这,基本就结束了。
webpack常见的优化方式,优化路径查找、设置缓存、happypack以及dllplugin,前两项vue-cli已经帮我们做了一些,而happypack由于不和vue兼容,导致无法接入,dllplugin通过单独提取第三方库,取得了明显优化。
当然,笔者也尝试剔除了一些项目中无用的代码,不过也是不痛不痒。
webpack优化方式总结:

  • 1、优化模块查找路径
  • 2、剔除不必要的无用的模块
  • 3、设置缓存:缓存loader的执行结果(cacheDirectory/cache-loader)
  • 4、设置多线程:HappyPack/thread-loader
  • 5、dllplugin提取第三方库

当然,这是针对开发的优化,如果是针对部署上的优化呢?我们可以设置splitchunk、按需加载、部署CDN等,这里就不展开了。

最后

希望这篇文章能够大家有所收获~ webpack已经是前端仔必备技能了~有空大家钻研一下webpack的配置和原理,也是会有所收获的!谢谢观看~

查看原文

赞 31 收藏 23 评论 0

安于现状i 赞了文章 · 2020-09-11

揭秘vue——vue-cli3全面配置

目录

创建项目

配置环境变量

  通过在package.json里的scripts配置项中添加--mode xxx来选择不同环境

  在项目根目录中新建.env, .env.production, .env.analyz等文件

  只有以 VUE_APP_ 开头的变量会被 webpack.DefinePlugin 静态嵌入到客户端侧的包中,代码中可以通过process.env.VUE_APP_BASE_API访问

  NODE_ENV 和 BASE_URL 是两个特殊变量,在代码中始终可用

.env serve默认的环境变量

NODE_ENV = 'development'
VUE_APP_BASE_API = 'https://demo.cn/api'
VUE_APP_SRC = 'https://wechat-timg.oss-cn-hangzhou.aliyuncs.com/demo'

.env.production build默认的环境变量

NODE_ENV = 'production'

VUE_APP_BASE_API = 'https://demo.com/api'
VUE_APP_SRC = 'https://img-wechat.oss-cn-hangzhou.aliyuncs.com/demo'

.env.analyz 用于webpack-bundle-analyzer打包分析

NODE_ENV = 'production'
IS_ANALYZ = 'analyz'

VUE_APP_BASE_API = 'https://demo.com/api'
VUE_APP_SRC = 'https://img-wechat.oss-cn-hangzhou.aliyuncs.com/demo'

  修改package.json

"scripts": {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "analyz": "vue-cli-service build --mode analyz",
  "lint": "vue-cli-service lint"
}

配置vue.config.js

const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV);

module.exports = {
  baseUrl: './', // 默认'/',部署应用包时的基本 URL
  outputDir: process.env.outputDir || 'dist', // 'dist', 生产环境构建文件的目录
  assetsDir: '',  // 相对于outputDir的静态资源(js、css、img、fonts)目录
  lintOnSave: false,
  runtimeCompiler: true, // 是否使用包含运行时编译器的 Vue 构建版本
  productionSourceMap: false,  // 生产环境的 source map
  parallel: require('os').cpus().length > 1,
  pwa: {}
};

配置proxy跨域

const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV);
module.exports = {
    devServer: {
        // overlay: {
        //   warnings: true,
        //   errors: true
        // },
        open: IS_PROD,
        host: '0.0.0.0',
        port: 8000,
        https: false,
        hotOnly: false,
        proxy: {
          '/api': {
            target: process.env.VUE_APP_BASE_API || 'http://127.0.0.1:8080',
            changeOrigin: true
          }
        }
    }
}

修复HMR(热更新)失效

module.exports = {
    chainWebpack: config => {
        // 修复HMR
        config.resolve.symlinks(true);
    }
}

添加别名

const path =  require('path');
const resolve = (dir) => path.join(__dirname, dir);
const IS_PROD = ['production', 'prod'].includes(process.env.NODE_ENV);

module.exports = {
    chainWebpack: config => {
        // 添加别名
        config.resolve.alias
          .set('@', resolve('src'))
          .set('assets', resolve('src/assets'))
          .set('components', resolve('src/components'))
          .set('layout', resolve('src/layout'))
          .set('base', resolve('src/base'))
          .set('static', resolve('src/static'));
    }
}

添加打包分析

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
    chainWebpack: config => {
        // 打包分析
        if (process.env.IS_ANALYZ) {
          config.plugin('webpack-report')
            .use(BundleAnalyzerPlugin, [{
              analyzerMode: 'static',
            }]);
        }
    }
}

配置externals

  防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖


module.exports = {
    configureWebpack: config => {
        config.externals = {
          'vue': 'Vue',
          'element-ui': 'ELEMENT',
          'vue-router': 'VueRouter',
          'vuex': 'Vuex',
          'axios': 'axios'
        }
    }
}

去掉console.log

方法一:
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
module.exports = {
    configureWebpack: config => {
        if (IS_PROD) {
            const plugins = [];
            plugins.push(
                new UglifyJsPlugin({
                    uglifyOptions: {
                        compress: {
                            warnings: false,
                            drop_console: true,
                            drop_debugger: false,
                            pure_funcs: ['console.log']//移除console
                        }
                    },
                    sourceMap: false,
                    parallel: true
                })
            );
            config.plugins = [
                ...config.plugins,
                ...plugins
            ];
        }
    }
}
方法二:使用babel-plugin-transform-remove-console插件
npm i --save-dev babel-plugin-transform-remove-console

在babel.config.js中配置

const plugins = [];
if(['production', 'prod'].includes(process.env.NODE_ENV)) {  
  plugins.push("transform-remove-console")
}

module.exports = {
  presets: [["@vue/app",{"useBuiltIns": "entry"}]],
  plugins: plugins
};

开启gzip压缩

npm i --save-dev compression-webpack-plugin
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;

module.exports = {
    configureWebpack: config => {
        if (IS_PROD) {
            const plugins = [];
            plugins.push(
                new CompressionWebpackPlugin({
                    filename: '[path].gz[query]',
                    algorithm: 'gzip',
                    test: productionGzipExtensions,
                    threshold: 10240,
                    minRatio: 0.8
                })
            );
            config.plugins = [
                ...config.plugins,
                ...plugins
            ];
        }
    }
}

  还可以开启比gzip体验更好的Zopfli压缩详见https://webpack.js.org/plugins/compression-webpack-plugin

npm i --save-dev @gfx/zopfli brotli-webpack-plugin
const CompressionWebpackPlugin = require('compression-webpack-plugin');
const zopfli = require("@gfx/zopfli");
const BrotliPlugin = require("brotli-webpack-plugin");
const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;

module.exports = {
    configureWebpack: config => {
        if (IS_PROD) {
            const plugins = [];
            plugins.push(
                new CompressionWebpackPlugin({
                    algorithm(input, compressionOptions, callback) {
                      return zopfli.gzip(input, compressionOptions, callback);
                    },
                    compressionOptions: {
                      numiterations: 15
                    },
                    minRatio: 0.99,
                    test: productionGzipExtensions
                })
            );
            plugins.push(
                new BrotliPlugin({
                    test: productionGzipExtensions,
                    minRatio: 0.99
                })
            );
            config.plugins = [
                ...config.plugins,
                ...plugins
            ];
        }
    }
}

为sass提供全局样式,以及全局变量

  可以通过在main.js中Vue.prototype.$src = process.env.VUE_APP_SRC;挂载环境变量中的配置信息,然后在js中使用$src访问。

  css中可以使用注入sass变量访问环境变量中的配置信息

module.exports = {
    css: {
        modules: false,
        extract: IS_PROD,
        sourceMap: false,
        loaderOptions: {
          sass: {
            // 向全局sass样式传入共享的全局变量
            data: `@import "~assets/scss/variables.scss";$src: "${process.env.VUE_APP_SRC}";`
          }
        }
    }
}

在scss中引用

.home {
    background: url($src + '/images/500.png');
}

添加IE兼容

npm i --save @babel/polyfill

  在main.js中添加

import '@babel/polyfill';

配置babel.config.js

const plugins = [];

module.exports = {
  presets: [["@vue/app",{"useBuiltIns": "entry"}]],
  plugins: plugins
};

完整配置

vue-cli3-config github持续更新中


☞☞☞揭秘vue系列☜☜☜


查看原文

赞 120 收藏 91 评论 2

安于现状i 关注了问题 · 2020-09-05

react-redux中的mapDispatchToProps写法

mapDispatchToProps不是映射dispatch到props上吗

const mapDispatchToProps = (dispatch) => ({
    showPlayer: (status) => {
        dispatch(showPlayer(status));
    },
    changeSong: (song) => {
        dispatch(changeSong(song));
    }
});

像这样。。。。
为什么看到有的地方直接写成一个对象比如

const mapDispatchToProps = {
   showPlayer(status);
   changeSong(song);
};

这样直接写action也可以吗 ,不需要dispatch? 还是说react-redux会自动帮你dispatch?
刚开始接触react 不是很清楚

关注 5 回答 3

安于现状i 关注了问题 · 2020-08-31

webpack中 ~符号是什么作用?

自己下编写vue组件的过程中,看到有的人是这样css引用文件的,那么这个~起什么作用呢?
图片描述

关注 6 回答 5

认证与成就

  • 获得 55 次点赞
  • 获得 11 枚徽章 获得 1 枚金徽章, 获得 4 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-12-15
个人主页被 1.3k 人浏览