子君

子君 查看完整档案

西安编辑哈佛大学  |  计算机 编辑前端有的玩  |  前端工程师 编辑 github.com/snowzijun 编辑
编辑

微信公众号: 前端有得玩
微信账号:snowzijun
github仓库: https://github.com/snowzijun
寄语:不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。

个人动态

子君 发布了文章 · 10月12日

适合Vue用户的React教程,你值得拥有

双节旅游人如山,不如家中代码闲。
学以致用加班少,王者荣耀家中玩。

小编日常工作中使用的是Vue,对于React只是做过简单的了解,并没有做过深入学习。趁着这个双节假期,小编决定好好学一学React,今天这篇文章就是小编在学习React之后,将ReactVue的用法做的一个对比,通过这个对比,方便使用Vue的小伙伴可以快速将Vue中的写法转换为React的写法。

本文首发于公众号【前端有的玩】,玩前端,面试找工作,就在【前端有的玩】,欢迎关注

插槽,在React中没找到??

在使用Vue的时候,插槽是一个特别常用的功能,通过定义插槽,可以在调用组件的时候将外部的内容传入到组件内部,显示到指定的位置。在Vue中,插槽分为默认插槽,具名插槽和作用域插槽。其实不仅仅Vue,在React中其实也有类似插槽的功能,只是名字不叫做插槽,下面我将通过举例来说明。

默认插槽

现在项目需要开发一个卡片组件,如下图所示,卡片可以指定标题,然后卡片内容可以用户自定义,这时候对于卡片内容来说,就可以使用插槽来实现,下面我们就分别使用VueReact来实现这个功能

Vue实现

  1. 首先实现一个card组件,如下代码所示

    <template>
      <div class="card">
        <div class="card__title">
          <span>{{ title }}</span>
        </div>
        <div class="card__body">
          <slot></slot>
        </div>
      </div>
    </template>
    <script>
    export default {
      props: {
        title: {
          type: String,
          default: ''
        }
      }
    }
    </script>
    

    可以看到上面我们使用了<slot></slot>,这个就是组件的默认插槽,在使用组件的时候,传入的内容将会被放到<slot></slot>所在位置

  2. 在外部使用定义的card组件

    <template>
      <div>
        <my-card>
          <div>我将被放在card组件的默认插槽里面</div>
        </my-card>
      </div>
    </template>
    <script>
    import MyCard from '../components/card'
    export default {
      components: {
        MyCard
      }
    }
    </script>
    

    如上代码,就可以使用组件的默认插槽将外部的内容应用到组件里面指定的位置了。

React实现

虽然在React里面没有插槽的概念,但是React里面也可以通过props.children拿到组件标签内部的子元素的,就像上面代码<my-card>标签内的子元素,通过这个我们也可以实现类似Vue默认插槽的功能,一起看看代码。

  1. 使用React定义Card组件

    import React from 'react'
    
    export interface CardProps {
      title: string,
      children: React.ReactNode
    }
    
    export default function(props: CardProps) {
    
      return (
        <div className="card">
          <div className="card__title">
            <span>{props.title}</span>
          </div>
          <div className="card__body">
            {/**每个组件都可以获取到 props.children。它包含组件的开始标签和结束标签之间的内容 */}
            {props.children}
          </div>
        </div>
      );
    }
    1. 在外部使用Card组件
    import React from 'react'
    import Card from './components/Card'
    
    export default function () {
    
      return (
        <div>
          <Card title="标题">
            <div>我将被放在card组件的body区域内容</div>
          </Card>
        </div>
      );
    }

具名插槽

继续以上面的Card组件为例,假如我们现在需求发生了变化,组件的title也可以使用插槽,这时候对于Vue就可以使用具名插槽了,而React也是有办法实现的哦。

Vue实现

Vue的具名插槽主要解决的是一个组件需要多个插槽的场景,其实现是为<slot>添加name属性来实现了。

  1. 我们就上面的需求对card组件进行修改
<template>
  <div class="card">
    <div class="card__title">
      <!--如果传入了title,则使用title属性,否则使用具名插槽-->
      <span v-if="title">{{ title }}</span>
      <slot v-else name="title"></slot>
    </div>
    <div class="card__body">
      <!--对于内容区域依然使用默认插槽-->
      <slot></slot>
    </div>
  </div>
</template>
<script>
export default {
  props: {
    title: {
      type: String,
      default: ''
    }
  }
}
</script>
  1. card组件修改完之后,我们再去调整一下使用card组件的地方
<template>
  <div>
    <my-card>
      <!--通过v-slot:title 使用具名插槽-->
      <template v-slot:title>
        <span>这里是标题</span>
      </template>
      <div>我将被放在card组件的默认插槽里面</div>
    </my-card>
  </div>
</template>
<script>
import MyCard from '../components/card'
export default {
  components: {
    MyCard
  }
}
</script>
React实现

React连插槽都没有, 更别提具名插槽了,但是没有不代表不能模拟出来。对于Reactprops,我们不仅仅可以传入普通的属性,还可以传入一个函数,这时候我们就可以在传入的这个函数里面返回JSX,从而就实现了具名插槽的功能。

  1. 对原有的Card组件进行修改
import React from 'react'

export interface CardProps {
  title?: string,
  // 加入了一个renderTitle属性,属性类型是Function
  renderTitle?: Function,
  children: React.ReactNode
}

export default function(props: CardProps) {

  const {title, renderTitle} = props
  // 如果指定了renderTtile,则使用renderTitle,否则使用默认的title
  let titleEl = renderTitle ? renderTitle() : <span>{title}</span>

  return (
    <div className="card">
      <div className="card__title">{titleEl}</div>
      <div className="card__body">
        {/**每个组件都可以获取到 props.children。它包含组件的开始标签和结束标签之间的内容 */}
        {props.children}
      </div>
    </div>
  );
}
  1. 这时候就可以在外部自定义title
import React from 'react'
import Card from './components/Card'

export default function () {
  return (
    <div>
      <Card  renderTitle={
        () => {
          return <span>我是自定义的标题</span>
        }
      }>
        <div>我将被放在card组件的body区域内容</div>
      </Card>
    </div>
  );
}

作用域插槽

有时让插槽内容能够访问子组件中才有的数据是很有用的,这个就是Vue提供作用域插槽的原因。我们继续使用上面的Card组件为例,现在我基于上面的卡片组件开发了一个人员信息卡片组件,用户直接使用人员信息卡片组件就可以将人员信息显示到界面中,但是在某些业务模块需要自定义人员信息显示方式,这时候我们就需要使用到作用域插槽了。

Vue实现
  1. 实现用户信息卡片组件,里面使用了作用域插槽
<template>
  <custom-card title="人员信息卡片">
    <div class="content">
      <!--这里使用了作用域插槽,将userInfo传出去了-->
      <slot name="userInfo" :userInfo="userInfo">
        <!--如果没有使用插槽,则显示默认内容-->
        <span>姓名: {{ userInfo.name }}</span>
        <span>性别: {{ userInfo.sex }}</span>
        <span>年龄: {{ userInfo.age }}</span>
      </slot>
    </div>
  </custom-card>
</template>
<script>
import CustomCard from '../card'
export default {
  components: {
    CustomCard
  },
  data() {
    return {
      userInfo: {
        name: '张三',
        sex: '男',
        age: 25
      }
    }
  }
}
</script>
  1. 在外部使用人员信息组件
<template>
  <div>
    <user-card>
      <template v-slot:userInfo="{ userInfo }">
        <div class="custom-user">
          <ul>
            <li>姓名: {{ userInfo.name }}</li>
            <li>年龄: {{ userInfo.age }}</li>
          </ul>
        </div>
      </template>
    </user-card>
  </div>
</template>
<script>
import UserCard from '../components/user-card'
export default {
  components: {
    UserCard
  }
}
</script>
React实现

在具名插槽那一小节我们通过给组件传入了一个函数,然后在函数中返回JSX的方式来模拟了具名插槽,那么对于作用域插槽,我们依然可以使用函数的这种方式,而作用域插槽传递的参数我们可以使用给函数传参的方式来替代

  1. 实现人员信息卡片组件

    import React, { useState } from 'react'
    
    import Card from './Card'
    
    interface UserCardProps {
      renderUserInfo?: Function
    }
    
    export interface UserInfo {
      name: string;
      age: number;
      sex: string;
    }
    
    export default function(props: UserCardProps) {
      const [userInfo] = useState<UserInfo>({
        name: "张三",
        age: 25,
        sex: "男",
      });
    
      const content = props.renderUserInfo ? (
        props.renderUserInfo(userInfo)
      ) : (
        <div>
          <span>姓名: {userInfo.name}</span>
          <span>年龄: {userInfo.age}</span>
          <span>性别: {userInfo.sex}</span>
        </div>
      );
    
      return <Card title="人员信息">
        {content}
      </Card>
    }
  2. 在外部使用人员信息卡片组件

    import React from 'react'
    import UserCard, { UserInfo } from "./components/UserCard";
    
    export default function () {
    
      return (
        <div>
          <UserCard
            renderUserInfo={(userInfo: UserInfo) => {
              return (
                <ul>
                  <li>姓名: {userInfo.name}</li>
                </ul>
              );
            }}
          ></UserCard>
        </div>
      );
    }

Context, React中的provide/inject

通常我们在项目开发中,对于多组件之间的状态管理,在Vue中会使用到Vuex,在React中会使用到redux或者Mobx,但对于小项目来说,使用这些状态管理库就显得比较大材小用了,那么在不使用这些库的情况下,如何去完成数据管理呢?比如面试最常问的祖孙组件通信。在Vue中我们可以使用provide/inject,在React中我们可以使用Context

假设有这样一个场景,系统现在需要提供一个换肤功能,用户可以切换皮肤,现在我们分别使用VueReact来实现这个功能。

Vue中的provide/inject

Vue中我们可以使用provide/inject来实现跨多级组件进行传值,就以上面所说场景为例,我们使用provide/inject来实现以下

首先,修改App.vue内容为以下内容

<template>
  <div id="app">
    <router-view />
  </div>
</template>

<script>
export default {
  data() {
    return {
      themeInfo: {
        theme: 'dark'
      }
    }
  },
  provide() {
    return {
      theme: this.themeInfo
    }
  }
}
</script>

然后在任意层级的子组件中像下面这样使用

<template>
  <div :class="`child-${theme.theme}`">
  </div>
</template>
<script>
export default {
  inject: ['theme']
}
</script>

这样就可以实现theme在所有子组件中进行共享了

React中的Context

Vue中我们使用provide/inject实现了组件跨层级传值功能,在React中也提供了类似的功能即Context,下面我们使用Context来实现相同的功能。

在项目src目录下新建context目录,添加MyContext.js文件,然后添加以下内容

import {createContext} from 'react'
// 定义 MyContext,指定默认的主题为`light`
export const MyContext = createContext({
  theme: 'light'
})

MyContext提供了一个Provider,通过Provider可以将theme共享到所有的子组件。现在我们在所有的组件的共同父组件比如App.js上面添加MyContext.Providertheme共享出去

import { MyContext } from '@/context/MyContext';

export default function() {
  
  const [theme, setTheme] = useState('dark')
  
  return (
    <MyContext.Provider
        value={{
          theme
        }}
      >
        <Children></Children>
     </MyContext.Provider>
    )
  }

然后这时候就可以直接在所有的子组件里面使用定义的主题theme

import React, { useContext } from 'react'
import { MyContext } from '@/context/MyContext';

export default function() {
   const {theme}  = useContext(MyContext)
   return <div className={`child-${theme}`}>
}

没有了v-model,但也不影响使用

我们知道ReactVue都是单向数据流的,即数据的流向都是由外层向内层组件进行传递和更新的,比如下面这段React代码就是标准的单向数据流.

import React, { useState } from "react";

export default function(){
  const [name] = useState('子君')
  return <input value={name}></input>
}

vue中使用v-model

如上代码,我们在通过通过value属性将外部的值传递给了input组件,这个就是一个简单的单向数据流。但是在使用Vue的时候,还有两个比较特殊的语法糖v-model.sync,这两个语法糖可以让Vue组件拥有双向数据绑定的能力,比如下面的代码

<template>
   <input v-model="name"/>
</template>
<script>
  export default {
    data() {
      return {
        name:'子君'
      }
    }
  }
</script>

通过v-model,当用户修改input的值的时候,外部的name的值也将同步被修改。但这是Vue的语法糖啊,React是不支持的,所以React应该怎么办呢?这时候再想想自定义v-modelv-model实际上是通过定义value属性同时监听input事件来实现的,比如这样:

<template>
  <div class="custom-input">
     <input :value="value" @input="$_handleChange"/>
  </div>
</template>
<script>
  export default {
    props:{
      value:{
        type: String,
        default: ''
      }
    },
    methods:{
      $_handleChange(e) {
        this.$emit('input', e.target.value)
      }
    }
  }
</script>

react寻找v-model替代方案

同理,React虽然没有v-model语法糖,但是也可以通过传入属性然后监听事件来实现数据的双向绑定。

import React, { useState } from 'react'

export default function() {
  const [name, setName] = useState('子君')

  const handleChange = (e) => {
    setName(e.target.value)
  }
  return <div>
    <input value={name} onChange={handleChange}></input>
  </div>
}

小编刚开始使用react,感觉没有v-model就显得比较麻烦,不过麻烦归麻烦,代码改写也要写。就像上文代码一样,每一个表单元素都需要监听onChange事件,越发显得麻烦了,这时候就可以考虑将多个onChange事件合并成一个,比如像下面代码这样

import React, { useState } from 'react'

export default function () {
  const [name, setName] = useState('子君')
  const [sex, setSex] = useState('男')

  const handleChange = (e:any, method: Function) => {
    method(e.target.value)
  }
  return <div>
    <input value={name} onChange={(e) => handleChange(e, setName)}></input>
    <input value={sex} onChange={(e) => handleChange(e, setSex)}></input>
  </div>
}

没有了指令,我感觉好迷茫

Vue中我们一般绘制页面都会使用到templatetemplate里面提供了大量的指令帮助我们完成业务开发,但是在React中使用的是JSX,并没有指令,那么我们应该怎么做呢?下面我们就将Vue中最常用的一些指令转换为JSX里面的语法(注意: 在Vue中也可以使用JSX)

v-showv-if

Vue中我们隐藏显示元素可以使用v-show或者v-if,当然这两者的使用场景是有所不同的,v-show是通过设置元素的display样式来显示隐藏元素的,而v-if隐藏元素是直接将元素从dom中移除掉。

  1. 看一下Vue中的v-showv-if的用法

    <template>
      <div>
        <span v-show="showName">姓名:{{ name }}</span>
        <span v-if="showDept">{{ dept }}</span>
      </div>
    </template>
    <script>
    export default {
      data() {
        return {
          name: '子君',
          dept: '银河帝国',
          showName: false,
          showDept: true
        }
      }
    }
    </script>
    
  2. v-showv-if转换为JSX中的语法

    Vue中指令是为了在template方便动态操作数据而存在的,但是到了React中我们写的是JSX,可以直接使用JS,所以指令是不需要存在的,那么上面的v-show,v-if如何在JSX中替代呢

    import React, { useState } from 'react'
    
    export default function() {
      const [showName] = useState(false)
    
      const [showDept] = useState(true)
    
      const [userInfo] = useState({
        name:'子君',
        dept: '银河帝国'
      })
    
      return (
        <div>
          {/**模拟 v-show */}
          <span style={{display: showName ? 'block' : 'none'}}>{userInfo.name}</span>
          {/**模拟 v-if */}
          {showDept ? <span>{userInfo.dept}</span>: undefined}
        </div>
      )
    }

v-for

v-forVue中是用来遍历数据的,同时我们在使用v-for的时候需要给元素指定keykey的值一般是数据的id或者其他唯一且固定的值。不仅在Vue中,在React中也是存在key的,两者的key存在的意义基本一致,都是为了优化虚拟DOMdiff算法而存在的。

  1. Vue中使用v-for

    <template>
      <div>
        <ul>
          <li v-for="item in list" :key="item.id">
            {{ item.name }}
          </li>
        </ul>
      </div>
    </template>
    <script>
    export default {
      data() {
        return {
          list: [
            {
              id: 1,
              name: '子君'
            },
            {
              id: '2',
              name: '张三'
            },
            {
              id: '3',
              name: '李四'
            }
          ]
        }
      }
    }
    </script>
    
  2. React中使用v-for的替代语法

    react中虽然没有v-for,但是JSX中可以直接使用JS,所以我们可以直接遍历数组

    import React from 'react'
    
    export default function() {
      const data = [
        {
          id: 1,
          name: "子君",
        },
        {
          id: "2",
          name: "张三",
        },
        {
          id: "3",
          name: "李四",
        },
      ];
    
      return (
        <div>
          <ul>
            {
            data.map(item => {
              return <li key={item.id}>{item.name}</li>
            })
          }
          </ul>
        </div>
      )
    }

v-bindv-on

v-bindVue中是动态绑定属性的,v-on是用于监听事件的,因为React也有属性和事件的概念,所以我们在React也能发现可替代的方式。

  1. Vue中使用v-bindv-on

    <template>
      <div>
        <!--:value是v-bind:value的简写, @input是v-on:input的简写-->
        <input :value="value" @input="handleInput" />
      </div>
    </template>
    <script>
    export default {
      data() {
        return {
          value: '子君'
        }
      },
      methods: {
        handleInput(e) {
          this.value = e.target.value
        }
      }
    }
    </script>
    
  2. React中寻找替代方案

    Vue中,作者将事件和属性进行了分离,但是在React中,其实事件也是属性,所以在本小节我们不仅看一下如何使用属性和事件,再了解一下如何在React中自定义事件

    • 开发一个CustomInput组件

      import React from 'react'
      
      export interface CustomInputProps {
        value: string;
        //可以看出 onChange是一个普通的函数,也被定义到了组件的props里面了
        onChange: ((value: string,event: React.ChangeEvent<HTMLInputElement>) => void) | undefined;
      }
      
      export default function(props: CustomInputProps) {
        
        function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
          // props.onChange是一个属性,也是自定义的一个事件
          props.onChange && props.onChange(e.target.value, e)
        }
      
        return (
          <input value={props.value} onChange={handleChange}></input>
        )
      }
    • 使用CustomInput组件

      import React, { useState } from 'react'
      
      import CustomInput from './components/CustomInput'
      
      export default function() {
       const [value, setValue] =  useState('')
      
       function handleChange(value: string) {
         setValue(value)
       }
      
        return (
          <div>
            <CustomInput value={value} onChange={handleChange}></CustomInput>
          </div>
        )
      }

总结

刚开始从Vue转到React的时候,其实是有点不适应的,但是当慢慢的习惯之后,就会发现VueReact是存在很多共性的,可以参考的去学习。当然无论Vue还是React,上手比较快,但是想深入学习还是需要下功夫的,后续小编将会对VueReact的用法在做更深入的介绍,敬请期待。

查看原文

赞 24 收藏 14 评论 1

子君 发布了文章 · 9月21日

面试遇坎,每日一题我精选了这些题目的答案

薪资太低欲辞职,面试做题心甚难。

屡屡面试屡遇坎,每日一题快来看。

每日工作之余,我会将自己整理的一些前端面试题笔试题整理成每日一题,然后在公众号中推送给大家,每天仅需几分钟做一道题,经过日积月累,在换工作的时候一定能让你拿到一个比较好的offer。今天这篇文章是我将近期每日一题中比较好的题目及粉丝们分享的一些答案进行的整理,分享给更多的掘友,希望可以帮助到你。同时关注公众号【前端有的玩】,每天早上八点四十分,准时推送每日一题。

题目一

题目

现在有小编每个月老婆给的零花钱清单,但是因为某些原因,有些月份没有零花钱,如下数据所示


// 一月,二月, 五月的零花钱

{1:200, 2:140, 5:400}

请将上面的数据格式转换为[200, 140, null, null, 400, null, null, null, null, null, null, null],其中数组的长度为12,对应十二个月,请完善下面代码


const obj = { 1: 200, 2: 140, 5: 400 };

function translate(obj) {

// 请在此处添加代码

}

// 输出 [200, 140, null, null, 400, null, null, null, null, null, null, null]

console.log(translate(obj));

答案

这道题答案可以有许多中,以下罗列了几个群友贡献的答案,为您提供参考

答案一

const obj = { 1: 200, 2: 140, 5: 400 };

function translate(obj) {

return Array.from({ length: 12 }).map((_, index) => obj[index + 1] || null);

}

// 输出 [200, 140, null, null, 400, null, null, null, null, null, null, null]

console.log(translate(obj));
答案二

const obj = { 1: 200, 2: 140, 5: 400 };

function translate(obj) {

return Object.assign(Array(13).fill(null), obj).slice(1);

}

// 输出 [200, 140, null, null, 400, null, null, null, null, null, null, null]

console.log(translate(obj));
答案三

const obj = { 1: 200, 2: 140, 5: 400 };

function translate(obj) {

// 请在此处添加代码

let result = Array(12).fill(null)

Object.entries(obj).forEach(([key, value]) => {

result[key - 1] = value;

});

return result;

}

// 输出 [200, 140, null, null, 400, null, null, null, null, null, null, null]

console.log(translate(obj));

题目二

题目

请输出1400之间所有数字中包含的1的个数,比如数字1中包含了一个1, 数字11中包含了两个1,数字20中不包含1,数字121中共包含了131


function getCount() {

}

// 输出 180

console.log(getCount())

答案

答案一

这个答案比较经典,性能也算是很不错的了


const sum1s = num => {

let numstr

if (!num) return 0

if (typeof num === 'string') numstr = num

else numstr = String(num)

if (Number(numstr) === 0) return 0

const curr =

numstr[0] > 1

? 10 ** (numstr.length - 1) +

numstr[0] * (numstr.length - 1) * 10 ** (numstr.length - 2)

: sum1s(10 ** (numstr.length - 1) - 1) + 1

return curr + sum1s(numstr.substr(1))

}

// 输出 180

console.log(sum1s(400))
答案二

这个用到了正则,不过对于长字符串正则可能性能会有点点差


function countOne(num){

// num为正整数,方法有点儿暴力

return Array.from({length:num},(v,i)=>i+1).join('').replace(/[^1]/g,'').length

}

console.log(countOne(400))
答案三

下面这个答案算是中规中矩的答案了,将每一个数字转换为字符串然后统计1的个数


function getCount() {

let count = 0

for(let i=1;i<400;i++) {

count = count + `${i}`.split('1').length - 1

}

return count

}

// 输出 180

console.log(getCount())

题目三

垂帘画阁画帘垂,谁系怀思怀系谁?影弄花枝花弄影,丝牵柳线柳牵丝。这是一首回文诗,即每一句诗正向反向读都是一样的。下面这道题是一道回文数字,即数字正向反向读都是一样的,比如11,1221,2112等等。

题目

请打印出1 - 10000 之间的所有回文数字。其中1~9因为只有一位数字,所以不算回文数字。

答案

答案一

const palindrome = length => {

const res = []

const digits = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']

const add = (current, length) => {

if (length <= 1) return

digits.forEach(digit => {

res.push(digit + current + digit)

add(digit + current + digit, length - 2)

})

}

digits.forEach(num => {

add(num, length - 1)

res.push(num + num)

add(num + num, length - 2)

})

return res.filter(num => !num.startsWith('0'))

}

// 总共189个

console.log(palindrome(4))
答案二

function palindrome (max) {

return Array(max + 1).fill('').reduce((a, c, i) => {

if (i > 10) {

const arr = Array.from(`${i}`)

const [x, y] = [`${i}`, arr.reverse().join('')]

x === y && a.push(i)

}

return a

}, [])

}

// 总共189个

console.log(palindrome(10000))
答案三

const result = [...Array(10000).keys()].filter((x) => x> 10 && x === Number(x.toString().split('').reverse().join('')) )

console.log(result)

题目四

题目

请实现下面代码中的函数fn,使其可以输出指定id对应的所有父id及其自身id


const data = [

{

id: 1,

name: '222',

children: [{

id: 2,

name: '34',

children: [{

id: 112,

name: '334',

}, {

id: 113,

name: '354',

}

]

}]

}

]

function fn(id) {

}

// 输出 [1, 2, 112]

console.log(fn(112))

答案

答案一

const data = [

{

id: 1,

name: '222',

children: [{

id: 2,

name: '34',

children: [{

id: 112,

name: '334',

}, {

id: 113,

name: '354',

}

]

}]

}

]

function fn(id) {

const res = []

const find = _ => {

if (!_) return

return _.find(item => (item.id === id || find(item.children)) && res.push(item.id))

}

find(data)

return res.reverse()

}

console.log(fn(112))
答案二

const fn = (id, ancestors = [], current = data) => {

for (let i = 0; i < current.length; i++) {

if (current[i].id === id) return ancestors.concat(id)

if (current[i].children && current[i].children.length) {

const ret = fn(id, ancestors.concat(current[i].id), current[i].children)

if (ret) return ret

}

}

}

console.log(fn(112))
答案三

function fn(id) {

const arr = []

const getIds = (ids) => {

for (const v of ids) {

arr.push(v.id)

if (v.id === id) {

return

} else if (v.children) {

getIds(v.children)

} else {

arr.pop()

}

}

}

getIds(data)

return arr

}

console.log(fn(112))

题目五

题目

请实现函数,将entry转换为output的数据格式


const entry = {

'a.b.c.dd': 'abcdd',

'a.d.xx': 'adxx',

'a.e': 'ae'

}

// 要求转换成如下对象

const output = {

a: {

b: {

c: {

dd: 'abcdd'

}

},

d: {

xx: 'adxx'

},

e: 'ae'

}

答案

答案一

function transform(obj) {

const res = {}

for (let [keys, value] of Object.entries(obj)) {

keys

.split('.')

.reduce((prev, cur, idx, arr) =>

prev[cur] = prev[cur] || (arr[idx + 1] ? {} : value)

, res)

}

return res

}
答案二

const transform = (input: { [P in string]: string }): Object => {

const ret = {}

Object.entries(input).forEach(([keys, val]) => {

let root = ret

keys.split('.').forEach((key, ind, arr) => {

if (ind === arr.length - 1) root[key] = val

else {

root[key] = root[key] || {}

root = root[key]

}

})

})

return ret

}
答案三

const entry = {

'a.b.c.dd': 'abcdd',

'a.d.xx': 'adxx',

'a.e': 'ae',

}

const convert = (data) => {

let res = {}

const entries = Object.entries(data)

for (let i = 0; i < entries.length; i++) {

let temp = res

let [key, value] = entries[i]

const everyOne = key.split('.')

for (let j = 0; j < everyOne.length; j++) {

if (j === everyOne.length - 1) {

temp[everyOne[j]] = value

}

temp[everyOne[j]] = temp[everyOne[j]] || {}

temp = temp[everyOne[j]]

}

}

return res

}

console.log(convert(entry))

总结

这次整理的这些每日一题都是一些编程题,其中有些还是我们日常开发中可能会遇到的问题,通过做这些题也可以检验一下自己对这些实用编程技巧的掌握程度。每日一题来源于公众号【前端有的玩】,工作日每天早上八点四十分准时推送,每日一题,每天成长一点点。

结语

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高
查看原文

赞 12 收藏 8 评论 0

子君 发布了文章 · 9月7日

使用Vue3.0,我收获了哪些知识点(一)

前端发展百花放,一技未熟百技出。
茫然不知何下手,关注小编胜百书。

近期工作感觉很忙,都没有多少时间去写文章,今天这篇文章主要是将自己前期学习Vue3.0时候整理的一些笔记内容进行了汇总,通过对本文的阅读,你将可以自己完成Vue3.0环境搭建,同时还会对Vue3.0的一些新的特性进行了解,方便自己进行Vue3.0的学习。本文首发于公众号【前端有的玩】,关注===会了,还有更多面试题等你来刷哦。

本文所有的示例均使用ant design vue2.0实现,关于ant design vue2.0请参考 https://2x.antdv.com/docs/vue/introduce-cn/

初始化环境

在前面的文章中,我们通过vite搭建了一个开发环境,但是实际上现在vite并没有完善到支撑一个完整项目的地步,所以本文我们依然选择使用vue-cli脚手架进行环境搭建。

小编使用的vue-cli版本是4.5.4,如果您的版本比较旧可以通过npm update @vue/cli来升级脚手架版本,如果没有安装可以通过npm install @vue/cli -g进行安装

使用脚手架新建项目

  1. 在工作空间打开终端(cmd),然后通过vue create my-vue3-test 命令初始化项目
  2. 在第一步先选择Manually select features,进行手动选择功能
  3. 然后通过Space和上下键依次选择

    Choose Vue version
    Babel
    TypeScript
    Router
    Vuex
    CSS Pre-processors
    Linter/Formatter

    然后回车

    1. 然后提示选择Vue版本,选择3.x(Preview)
    2. Use class-style component syntax?选择n,即输入n然后回车
    3. 然后提示Use Babel alongside TypeScript,输入y`
    4. Use history mode for router输入n
    5. 然后css预处理器选择Less
    6. eslint选择ESLint + Prettier
    7. 然后是Lint on saveIn dedicater config files
    8. 最后一路回车即可完成项目搭建

启动项目

新建完项目之后,进入到项目中cd my-vue3-test,然后执行 yarn serve即可启动项目

启动之后即可通过访问 http://localhost:8080/ 访问项目

配置ant design vue

在当前Vue3.0正式版还未发布之际,国内比较出名的前端UI库中率先将Vue3.0集成到自家的UI库中的,PC端主要是ant-design-vue,移动端主要是vant, 本文所有示例代码都会基于ant-design-vue来进行,首先我们先安装ant-design-vue

  1. 安装依赖

    yarn add ant-design-vue@2.0.0-beta.6
    yarn add babel-plugin-import -D
  2. 配置ant-design-vue按需加载

    进入项目根目录,然后打开babel.config.js文件,将里面的内容修改为

    module.exports = {
      presets: ["@vue/cli-plugin-babel/preset"],
      plugins: [
        // 按需加载
        [
          "import",
          // style 为 true 加载 less文件
          { libraryName: "ant-design-vue", libraryDirectory: "es", style: "css" }
        ]
      ]
    };
  3. 尝试使用vue3 + antdv来添加一个小页面, 我们直接将views/Home.vue文件里面的代码替换为
<template>
  <a-form layout="inline" :model="state.form">
    <a-form-item>
      <a-input v-model:value="state.form.user" placeholder="Username">
        <template v-slot:prefix
          ><UserOutlined style="color:rgba(0,0,0,.25)"
        /></template>
      </a-input>
    </a-form-item>
    <a-form-item>
      <a-input
        v-model:value="state.form.password"
        type="password"
        placeholder="Password"
      >
        <template v-slot:prefix
          ><LockOutlined style="color:rgba(0,0,0,.25)"
        /></template>
      </a-input>
    </a-form-item>
    <a-form-item>
      <a-button
        type="primary"
        :disabled="state.form.user === '' || state.form.password === ''"
        @click="handleSubmit"
      >
        登录
      </a-button>
    </a-form-item>
  </a-form>
</template>
<script>
import { UserOutlined, LockOutlined } from "@ant-design/icons-vue";
import { Form, Input, Button } from "ant-design-vue";
import { reactive } from "vue";

export default {
  components: {
    UserOutlined,
    LockOutlined,
    [Form.name]: Form,
    [Form.Item.name]: Form.Item,
    [Input.name]: Input,
    [Button.name]: Button
  },
  setup() {
    const state = reactive({
      form: {
        user: "",
        password: ""
      }
    });

    function handleSubmit() {
      console.log(state.form);
    }

    return {
      state,
      handleSubmit
    };
  }
};
</script>

然后重启一下项目,就可以发现已经可以正常使用ant-design-vue了。

Vue3.0新体验之setup

对于Vue3.0的问世,最吸引大家注意力的便是Vue3.0Composition API,对于Componsition API,可以说是两极分化特别严重,一部分人特别喜欢这个新的设计与开发方式,而另一部分人则感觉使用Composition API很容易写出来意大利面式的代码(可能这部分人不知道兰州拉面吧)。到底Composition API是好是坏,小编不做评论,反正我只是一个搬砖的。而本小节介绍的setup就是Composition API的入口。

setup介绍

setupVue3.0提供的一个新的属性,可以在setup中使用Composition API,在上面的示例代码中我们已经使用到了setup,在上文代码中我们在setup中通过reactive初始化了一个响应式数据,然后通过return返回了一个对象,对象中包含了声明的响应式数据和一个方法,而这些数据就可以直接使用到了template中了,就像上文代码中的那样。关于reactive,我将会在下一小节为你带来说明。

setup 的参数说明

setup函数有两个参数,分别是propscontext

  1. props

    propssetup函数的第一个参数,是组件外部传入进来的属性,与vue2.0props基本是一致的,比如下面代码

    export default {
      props: {
        value: {
          type: String,
          default: ""
        }
      },
      setup(props) {
        console.log(props.value)
      }
    }

    但是需要注意的是,在setup中,props是不能使用解构的,即不能将上面的代码改写成

    setup({value}) {
        console.log(value)
     }

    虽然template中使用的是setup返回的对象,但是对于props,我们不需要在setup中返回,而是直接可以在template使用,比如上面的value,可以直接在template写成

    <custom-component :value="value"></custom-component>
  2. context

    contextsetup函数的第二个参数,context是一个对象,里面包含了三个属性,分别是

    • attrs

      attrsVue2.0this.$attrs是一样的,即外部传入的未在props中定义的属性。对于attrsprops一样,我们不能对attrs使用es6的解构,必须使用attrs.name的写法

    • slots

      slots对应的是组件的插槽,与Vue2.0this.$slots是对应的,与propsattrs一样,slots也是不能解构的。

    • emit

      emit对应的是Vue2.0this.$emit, 即对外暴露事件。

setup 返回值

setup函数一般会返回一个对象,这个对象里面包含了组件模板里面要使用到的data与一些函数或者事件,但是setup也可以返回一个函数,这个函数对应的就是Vue2.0render函数,可以在这个函数里面使用JSX,对于Vue3.0中使用JSX,小编将在后面的系列文章中为您带来更多说明。

最后需要注意的是,不要在setup中使用this,在setup中的this和你真正要用到的this是不同的,通过propscontext基本是可以满足我们的开发需求的。

了解Composition API,先从reactiveref开始

在使用Vue2.0的时候,我们一般声明组件的属性都会像下面的代码一样

export default {
  data() {
    return {
      name: '子君',
      sex: '男'
    }
  }
}

然后就可以在需要用到的地方比如computed,watch,methods,template等地方使用,但是这样存在一个比较明显的问题,即我声明data的地方与使用data的地方在代码结构中可能相距很远,有一种君住长江头,我住长江尾,日日思君不见君,共饮一江水的感觉。而Composition API的诞生的一个很重要的原因就是解决这个问题。在尤大大在关于Composition API的动机中是这样描述解决的问题的:

  1. 随着功能的增长,复杂组件的代码变得越来越难以阅读和理解。这种情况在开发人员阅读他人编写的代码时尤为常见。根本原因是 Vue 现有的 API 迫使我们通过选项组织代码,但是有的时候通过逻辑关系组织代码更有意义。
  2. 目前缺少一种简洁且低成本的机制来提取和重用多个组件之间的逻辑。

现在我们先了解一下Compositon API中的reactiveref

介绍reactive

Vue2.6中, 出现了一个新的api,Vue.observer,通过这个api可以创建一个响应式的对象,而reactive就和Vue.ovserver的功能基本是一致的。首先我们先来看一个例子

<template>
  <!--在模板中通过state.name使用setup中返回的数据-->
  <div>{{ state.name }}</div>
</template>
<script>
import { reactive } from "vue";
export default {
  setup() {
    // 通过reactive声明一个可响应式的对象
    const state = reactive({
      name: "子君"
    });
    // 5秒后将子君修改为 前端有的玩
    setTimeout(() => {
      state.name = "前端有的玩";
    }, 1000 * 5);
    // 将state添加到一个对象中然后返回
    return {
      state
    };
  }
};
</script>

上面的例子就是reactive的一个基本的用法,我们通过上面的代码可以看到reactiveVue.observer声明可响应式对象的方法是很像的,但是他们之间还是存在一些差别的。我们在使用vue2.0的时候,最常见的一个问题就是经常会遇到一些数据明明修改了值,但是界面却并没有刷新,这时候就需要使用Vue.set来解决,这个问题是因为Vue2.0使用的Object.defineProperty无法监听到某些场景比如新增属性,但是到了Vue3.0中通过Proxy将这个问题解决了,所以我们可以直接在reactive声明的对象上面添加新的属性,一起看看下面的例子

<template>
  <div>
    <div>姓名:{{ state.name }}</div>
    <div>公众号:{{ state.gzh }}</div>
  </div>
</template>
<script>
import { reactive } from "vue";
export default {
  setup() {
    const state = reactive({
      name: "子君"
    });
    // 5秒后新增属性gzh 前端有的玩
    setTimeout(() => {
      state.gzh = "前端有的玩";
    }, 1000 * 5);
    return {
      state
    };
  }
};
</script>

上面的例子虽然在state中并没有声明gzh属性,但是在5s后我们可以直接给state添加gzh属性,这时候并不需要使用Vue.set来解决新增属性无法响应的问题。

在上面的代码中,reactive通过传入一个对象然后返回了一个state,需要注意的是state与传入的对象是不用的,reactive对原始的对象并没有进行修改,而是返回了一个全新的对象,返回的对象是Proxy的实例。需要注意的是在项目中尽量去使用reactive返回的响应式对象,而不是原始对象。

const obj = {}
const state = reactive(obj)
// 输出false
console.log(obj === state)

介绍ref

假如现在我们需要在一个函数里面声明用户的信息,那么我们可能会有两种不一样的写法

// 写法1
let name = '子君'
let gzh = '前端有的玩'
// 写法2
let userInfo = {
  name: '子君',
  gzh: '前端有的玩'
}

上面两种不同的声明方式,我们使用的时候也是不同的,对于写法1我们直接使用变量就可以了,而对于写法2,我们需要写成userInfo.name的方式。我们可以发现userInfo的写法与reactive是比较相似的,而Vue3.0也提供了另一种写法,就像写法1一样,即ref。先来看一个例子。

<template>
  <div>
    <div>姓名:{{ name }}</div>
  </div>
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    const name = ref("子君");
    console.log('姓名',name.value)
    // 5秒后修改name为 前端有的玩
    setTimeout(() => {
      name.value = "前端有的玩";
    }, 1000 * 5);
    return {
      name
    };
  }
};
</script>

通过上面的代码,可以对比出来reactiveref的区别

  1. reactive传入的是一个对象,返回的是一个响应式对象,而ref传入的是一个基本数据类型(其实引用类型也可以),返回的是传入值的响应式值
  2. reactive获取或修改属性可以直接通过state.prop来操作,而ref返回值需要通过name.value的方式来修改或者读取数据。但是需要注意的是,在template中并不需要通过.value来获取值,这是因为template中已经做了解套。

Vue3.0优雅的使用v-model

v-model并不是vue3.0新推出的新特性,在Vue2.0中我们已经大量的到了v-model,但是V3V2还是有很大的区别的。本节我们将主要为大家带来如何在Vue3.0中使用v-model,Vue3.0中的v-model提供了哪些惊喜以及如何在Vue3.0中自定义v-model

Vue2.0Vue3.0中使用v-model

Vue2.0中如何实现双向数据绑定呢?常用的方式又两种,一种是v-model,另一种是.sync,为什么会有两种呢?这是因为一个组件只能用于一个v-model,但是有的组件需要有多个可以双向响应的数据,所以就出现了.sync。在Vue3.0中为了实现统一,实现了让一个组件可以拥有多个v-model,同时删除掉了.sync。如下面的代码,分别是Vue2.0Vue3.0使用v-model的区别。

  1. Vue2.0中使用v-model

    <template>
      <a-input v-model="value" placeholder="Basic usage" />
    </template>
    <script>
    export default {
      data() {
        return {
          value: '',
        };
      },
    };
    </script>
  2. Vue3.0中使用v-model

    <template>
      <!--在vue3.0中,v-model后面需要跟一个modelValue,即要双向绑定的属性名-->
      <a-input v-model:value="value" placeholder="Basic usage" />
    </template>
    <script>
    export default {
      // 在Vue3.0中也可以继续使用`Vue2.0`的写法
      data() {
        return {
          value: '',
        };
      },
    };
    </script>

    vue3.0中,v-model后面需要跟一个modelValue,即要双向绑定的属性名,Vue3.0就是通过给不同的v-model指定不同的modelValue来实现多个v-model。对于v-model的原理,下文将通过自定义v-model来说明。

自定义v-model

使用Vue2.0自定义一个v-model示例
  1. 组件代码
<template>
  <div class="custom-input">
    <input :value="value" @input="$_handleChange" />
  </div>
</template>
<script>
export default {
  props: {
    value: {
      type: String,
      default: ''
    }
  },
  methods: {
    $_handleChange(e) {
      this.$emit('input', e.target.value)
    }
  }
}
</script>
  1. 在代码中使用组件

    <template>
        <custom-input v-model="value"></custom-input>
    </template>
    <script>
        export default {
        data() {
          return {
            value: ''
          }
        }
      }
    </script>

    Vue2.0中我们通过为组件设置名为value属性同时触发名为input的事件来实现的v-model,当然也可以通过model来修改属性名和事件名,可以看我以前的文章中有详解。

使用Vue3.0自定义一个v-model示例
  1. 组件代码

    <template>
      <div class="custom-input">
        <input :value="value" @input="_handleChangeValue" />
      </div>
    </template>
    <script>
    export default {
      props: {
        value: {
          type: String,
          default: ""
        }
      },
      name: "CustomInput",
      setup(props, { emit }) {
        function _handleChangeValue(e) {
          // vue3.0 是通过emit事件名为 update:modelValue来更新v-model的
          emit("update:value", e.target.value);
        }
        return {
          _handleChangeValue
        };
      }
    };
    </script>
    
    1. 在代码中使用组件

      <template>
        <!--在使用v-model需要指定modelValue-->
        <custom-input v-model:value="state.inputValue"></custom-input>
      </template>
      <script>
      import { reactive } from "vue";
      import CustomInput from "../components/custom-input";
      export default {
        name: "Home",
        components: {
          CustomInput
        },
        setup() {
          const state = reactive({
            inputValue: ""
          });
          return {
            state
          };
        }
      };
      </script>

到了Vue3.0中,因为一个组件支持多个v-model,所以v-model的实现方式有了新的改变。首先我们不需要使用固定的属性名和事件名了,在上例中因为是input输入框,属性名我们依然使用的是value,但是也可以是其他任何的比如name,data,val等等,而在值发生变化后对外暴露的事件名变成了update:value,即update:属性名。而在调用组件的地方也就使用了v-model:属性名来区分不同的v-model

总结

在本文中我们主要讲解了开发环境的搭建,setup,reactive,ref,v-model等的介绍,同时通过对比Vue3.0Vue2.0的不同,让大家对Vue3.0有了一定的了解,在下文中我们将为大家带来更多的介绍,比如技术属性,watch,生命周期等等,敬请期待。本文首发于公众号【前端有的玩】,学习Vue,面试刷题,尽在【前端有的玩】,`乘兴裸辞心甚爽,面试工作屡遭难。
幸得每日一题伴,点击关注莫偷懒。`,下周一新文推送,不见不散。

结语

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高
查看原文

赞 26 收藏 20 评论 9

子君 发布了文章 · 8月31日

当裸辞遇到面试难,这些面试题你需要了解一下

乘兴裸辞心甚爽,面试工作屡遭难。
幸得每日一题伴,点击关注莫偷懒。

又要到金九银十的跳槽季了,为了让更多的小伙伴可以在面试的时候取的更好的offer,所以自上月起我每天都会在自己的公众号【前端有的玩】里面推送一到两道面试题,方便找工作的小伙伴每日都会有新的收获。本文就是小编将前期的一些比较经典的每日一题进行了梳理,欢迎大家一起来看看。本文内容首发于公众号【前端有的玩】,关注 === 学会。

类数组面试题

什么是类数组,类数组就是 拥有length属性,且其他属性(索引)为非负整数的对象,且不具备数组所用于的方法。比如我们常用的document.querySelector返回的NodeLists就是一个类数组。这道题就是和类数组相关的内容.

题目

请说出以下代码输出的内容,需要区分nodejs,chrome以及chrome去掉splice之后的输出内容

var obj = {
    '2': 3,
    '3': 4,
    'length': 2,
    'splice': Array.prototype.splice,
    'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)

答案

这道题一共问了三种情况下面的输出,下面依次说明答案

  1. node下面输出

    { '2': 1,
      '3': 2,
      length: 4,
      splice: [Function: splice],
      push: [Function: push] }
  2. chrome下面输出

    [empty × 2, 1, 2, splice: ƒ, push: ƒ]
  3. chrome去掉splice下面输出

    {2: 1, 3: 2, length: 4, push: ƒ}

通过上面输出的内容,可以看出相同的代码,不同情况输出的内容是有所不同的,下面进行详细分解。

题解

在解答题目之前,我们再看看这段代码

const arr = new Array(2)
// 输出  2 [empty * 2]
console.log(arr.length, arr)
arr.push(1)
// 输出  3 [empty * 2, 1]
console.log(arr.length, arr)

可以看到push方法会将数组的length + 1, 然后将值放在索引为length - 1的位置,比如上面的代码,因为在初始化数组的时候,已经将数组长度指定为了2, 所以在push之后length就变成了3,然后arr[3 - 1] = 1

MDN上面对push的方法的解释是:

push 方法具有通用性。该方法和 call()apply() 一起使用时,可应用在类似数组的对象上。push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。

根据MDN解释,push既可以使用到数组中,也可以使用到类数组中。而根据前文中对类数组的解释,可以看到题目中的obj就是一个标准的类数组,那就可以在obj上面使用数组的push方法。

再看obj.push(1), 因为obj.length = 2, 所以会将length + 1就变成了3, 这时候 索引值时obj[3 - 1] = 1obj[2] = 1, 同理 obj.push(2) 也一样的。因为在obj中已经有了属性(索引)23,所以在push的时候会覆盖掉23上面的默认值。

所以在nodejs中就会输出

{ '2': 1,
  '3': 2,
  length: 4,
  splice: [Function: splice],
  push: [Function: push] }

但是在chrome控制台中输出

[empty × 2, 1, 2, splice: ƒ, push: ƒ]

很奇怪,为什么会输出这样呢?这一块有一个很特殊的陷阱,就是chrome控制台是如何判断打印的内容是数组还是其他对象呢?对于这个,chrome就是通过判断对象上面是否有splicelength这两个属性来判断的,所以如果你将splice去掉之后,就会输出以下内容

{2: 1, 3: 2, length: 4, push: ƒ}

你也可以试试下面的代码:

console.log({splice:function(){},length:1})
console.log({slice:function(){},length:1})

逻辑面试题之小鼠喝毒药

小编当年毕业的时候面试就遇到过好几次逻辑类的面试题,这道题就是一道逻辑类的面试题,一起来看看。

题目

有16瓶水,其中只有一瓶水有毒,小白鼠喝一滴之后一小时会死,请问最少用多少只小白鼠,在1小时内一定可以找出有毒的水?

答案与题解

答案是至少需要4只小鼠,怎么理解呢?我们可以用二进制去推理一下:

假设有4只小鼠,分别是甲乙丙丁,使用二进制来表示小鼠喝药的顺序,1代表喝药,0代表不喝药

甲: 1111 1111 0000 0000

乙: 1111 0000 1111 0000

丙: 1100 1100 1100 1100

丁: 1010 1010 1010 1010

那么我们就可以这样去判断:

  1. 甲乙丙丁都死了,说明第一瓶有毒
  2. 甲乙丙死了,说明第二瓶有毒
  3. 甲乙丁死了,说明第三瓶有毒
  4. 甲乙死了,说明第四瓶有毒
  5. 甲丙丁死了,说明第五瓶有毒
  6. 。。。 依次类推

其实对于这道题,可以使用2n次方来判断,比如有32瓶水,那么就是25次方,所以就需要5只小鼠。

arguments 面试题

ES6中,我们如果一个函数参数个数不确定,我们一般会使用扩展运算符即function(...rest){},得到一个参数数组rest,但是在ES6之前,我们是不能使用扩展运算符的,这时候就需要考虑使用arguments

题目

请说出以下程序输出的内容(chrome输出内容)

let obj = {
  arg: 18,
  foo: function(func) {
    func()
    arguments[0]()
  }
}

var age = 10
function fn() {
  console.log(this.age)
}

obj.foo(fn)

答案

本题的答案是:

// 第一个输出 10
func()
// 第一个输出 undefined
arguments[0]()

有点出乎意料了吗?

先来解释一下第一个,为什么不是输出18呢,虽然func()是在foo函数里面调用的,但是并没有显式指明作用域,这时候会使用默认作用域window,而对于浏览器来说,在全局通过var声明的变量会自动挂载到window上面,所以var age = 10相当于window.age = 10, 而第一个func()里面的this.age相当于window.age

第二个可能许多人有点蒙,为啥是undefined,先看一下下面的代码

const arr = [function() {console.log(this[1])}, '我是子君']
// 输出 我是子君
console.log(arr[0]())

我们通过arr[0]获取到函数,这时候函数的作用域就是这个数组,所以再调用的时候,this就是arr, 所以this[1]就是数组第二项。

这时候回过头来看arguments,这个其实是一个类数组,里面存的是函数传入的参数,第一项就是传入的函数,和上面例子一样,arguments[0]的作用域就是arguments,而arguments上面并没有age属性,所以是undefined

this指向问题

this指向问题一直是比较混乱的,在箭头函数出现之前,this的指向与代码在哪里定义并没有关系,而是取决于是被谁执行的,正因为此,所以许多开发人员经常会搞不清楚this到底是谁。下面的两道题都是和this指向相关的问题。

题目一(青铜)

请说出以下代码输出的内容

let num = 1;
let obj = {
    num: 2,
    add: function() {
        this.num = 3;
        (function() {
            console.log(this.num);
            this.num = 4;
        })();
        console.log(this.num);
    },
    sub: function() {
        console.log(this.num)
    }
}
obj.add();
console.log(obj.num);
console.log(num);
const sub = obj.sub;
sub();

题目二(黄金)

请说出以下代码输出的内容

var num = 10
const obj = {num: 20}
obj.fn = (function (num) {
  this.num = num * 3
  num++
  return function (n) {
    this.num += n
    num++
    console.log(num)
  }
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)

答案

题目一

输出结果: 1,3,3,4,4, 你答对了吗?下面我们来看看代码解析

var num = 1;
let obj = {
    num: 2,
    add: function() {
        this.num = 3;
          // 这里的立即指向函数,因为我们没有手动去指定它的this指向,所以都会指向window
        (function() {
            // 所有这个 this.num 就等于 window.num
            console.log(this.num);
            this.num = 4;
        })();
        console.log(this.num);
    },
    sub: function() {
        console.log(this.num)
    }
}
// 下面逐行说明打印的内容

/**
 * 在通过obj.add 调用add 函数时,函数的this指向的是obj,这时候第一个this.num=3
 * 相当于 obj.num = 3 但是里面的立即指向函数this依然是window,
 * 所以 立即执行函数里面console.log(this.num)输出1,同时 window.num = 4
 *立即执行函数之后,再输出`this.num`,这时候`this`是`obj`,所以输出3
 */
obj.add() // 输出 1 3

// 通过上面`obj.add`的执行,obj.name 已经变成了3
console.log(obj.num) // 输出3
// 这个num是 window.num
console.log(num) // 输出4
// 如果将obj.sub 赋值给一个新的变量,那么这个函数的作用域将会变成新变量的作用域
const sub = obj.sub
// 作用域变成了window window.num 是 4
sub() // 输出4
题目二

输出结果为: 22236530, 你答对了吗? 下面我们解析一下

var num = 10
const obj = {num: 20}
obj.fn = (function (num) {
  this.num = num * 3
  num++
  return function (n) {
    this.num += n
    num++
    console.log(num)
  }
})(obj.num)
var fn = obj.fn
fn(5)
obj.fn(10)
console.log(num, obj.num)

我们把上面的代码分为以下几步进行分析

  1. 先看第三行代码,是一个赋值操作,我们知道赋值操作是从右向左的,而=号右边是一个立即执行函数,所以会优先执行立即执行函数,立即执行函数没有手动指定this,这时候this = window,而立即函数的参数num是传进来的obj.num,所以num参数默认值是 20
  2. 第四行相当于window.num = 20 * 3
  3. 第五行为传入的参数加一,所以 num = 20 + 1
  4. 第六行return了一个函数,而这个函数就是obj.fn的值, 但是因为return的函数引用了立即执行函数里面的num,所以形成了闭包。这时候

    obj.fn = function(n) {
      this.num += n
      // 这个num是立即执行函数里面的num
      num++
      console.log(num)
    }
  5. var fn = obj.fn, 将obj.fn赋值给新的变量,而这个变量的作用域是window
  6. 在调用fn(5)的时候, 在第二步,window.num的值已经变成了60, 然后因为这时候fnthiswindow, this.num += n相当于window.num += n, 即window.num = 65
  7. num++, 因为闭包的原因,第三步num21,所以这一步 num变成了22, 同时输出22
  8. 然后obj.fn(10),这时候fnthisobj,obj.num默认值是20, this.num += n相当于 obj.num += 10
  9. 和第七步一样, num + 1 输出 23
  10. console.log(num, obj.num)相当于 console.log(window.num, obj.num),从上面几步可知, window.num = 65, obj.num = 30
扩展题

如果将上面两道题的 var改成 let, 又会输出什么结果呢?

数据类型转换问题

虽然在日常开发中,我们隐氏类型转换用的比较少(不一定),但是这个还是面试常问问题,掌握还是要掌握的,一起来看看这道题目吧.

题目(王炸/青铜,我也不知道)

请说出以下代码输出的内容

console.log([] + [])
console.log({} + [])
console.log([] == ![])
console.log(true + false)

答案

一起来看看答案吧

  1. 第一行代码
// 输出 "" 空字符串
console.log([] + [])

这行代码输出的是空字符串"", 包装类型在运算的时候,会先调用valueOf方法,如果valueOf返回的还是包装类型,那么再调用toString方法

// 还是 数组
const val = [].valueOf()
// 数组 toString 默认会将数组各项使用逗号 "," 隔开, 比如 [1,2,3].toSting 变成了"1,2,3",空数组 toString 就是空字符串
const val1 = val.toString() // val1 是空字符串

所以上面的代码相当于

console.log("" + "")
  1. 第二行代码

    // 输出 "[object Object]"
    console.log({} + [])

    和第一题道理一样,对象 {}隐氏转换成了[object Object],然后与""相加

  2. 第三行代码

    // 输出 true
    console.log([] == ![])

    对于===, 会严格比较两者的值,但是对于==就不一样了

    1. 比如 null == undefined
    2. 如果非numbernumber比较,会将其转换为number
    3. 如果比较的双方中由一方是boolean,那么会先将boolean转换为number

所以对于上面的代码,看下面一步一步分析

// 这个输出 false
console.log(![])
// 套用上面第三条 将 false 转换为 数值
// 这个输出 0
console.log(Number(false))
// 包装类型与 基本类型 == 先将包装类型通过 valueOf toString 转换为基本类型 
// 输出 ""
console.log([].toString())
// 套用第2条, 将空字符串转换为数值、
// 输出 0
console.log(Number(""))
// 所以
console.log(0 == 0)
  1. 第四行代码

    // 输出 1
    console.log(true + false)

    两个基本类型相加,如果其中一方是字符,则将其他的转换为字符相加,否则将类型转换为Number,然后相加, Number(true)1, Number(false)0, 所以结果是 1

总结

面试造火箭,工作拧螺丝。虽然我只想拧螺丝,但是我却需要通过造火箭来找到拧螺丝的工作,每日一题,每天都有新的面试题目,欢迎关注公众号【前端有的玩】,拉你进入前端技术交流群,每日一题等着你来一起答题。

结语

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高
查看原文

赞 20 收藏 16 评论 1

子君 发布了文章 · 8月24日

学习Vue3.0,先从搭建环境开始

Bug源测试,上线来几个。愿君多修改,今夜眼难合。

这是小编关于Vue3.0系列文章的第二篇,本文将带您从零搭建一个基于Vue3.0viteVue3.0开发环境,通过本文的学习,你将学习到以下内容:

  1. 使用vite初始化Vue3.0项目
  2. 配置ts
  3. 配置vue-router
  4. 配置vuex
  5. 使用Vue3.0开发一个TodoList示例
您可以通过微信搜索【前端有的玩】关注我的公众号加我微信好友,手摸手和你一起学习Vue3.0

使用vite初始化项目

vite 介绍

vite是尤大大在今年新鼓捣出来的一个工具,尤大大对vite的描述是这样的: Vite is an opinionated web dev build tool that serves your code via native ES Module imports during dev and bundles it with Rollup for production. 翻译成中文就是:Vite 是一个由原生 ES Module 驱动的 Web 开发构建工具。在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于 Rollup 打包。

上面这段话提到了一个关键字ES Module,这个是什么呢?详细的介绍大家可以访问 https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules 进行查看。此处我们长话短说。在最早的时候,还没有前端工程化,然后我们写javascript都是写到一个文件,然后通过script标签去引用,后来随着前端发展越来越壮大,js之间依赖越来越复杂,这时候就需要有一种可以将JavaScript 程序拆分为可按需导入的单独模块的机制来维护这个依赖,随之就诞生了AMD,CMD等等,而ES Module就是浏览器支持的原生模块依赖的功能。

为什么要用vite

为什么尤大大要推出vite,在我们使用webpack的时候,每次开发时候启动项目都需要几十秒甚至超过一分钟,比较慢,而且热更新也比较慢,而vite的主要特点就是快,官网对于vite的特点是这样描述的

  1. 快速的冷启动
  2. 即时的模块热更新
  3. 真正的按需编译

到底有多快呢,我们先新建一个项目试试

初始化vite项目

  1. 初始化项目, 在工作空间打开终端窗口,对于window用户即cmd,然后执行下面命令

    yarn create vite-app my-vue3

    执行之后就会输出以下内容,可以看到新建项目特别快,仅仅用了1.63s

  2. 初始化完项目,通过cd my-vue3进行到项目里面,然后再执行yarn安装依赖(此处建议使用淘宝镜像,比较快)
  3. 依赖安装完需要通过yarn dev启动项目

    是不是瞬间体验到了秒启项目的感觉,启动之后就可以通过http://localhost:3000来访问项目了

查看项目结构

使用vscode打开项目之后,可以查看到新建的项目结构与vue-cli4创建的项目结构基本一样,都是我们很熟悉的App.vuemain.js

查看main.js文件内容

打开main.js

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')

发现创建Vue的方式变了,原来是通过new Vue的方法来初始化Vue,在Vue3.0中,修改为了通过createApp的方式,关于Vue3.0的更多使用方式,我们将在后面的系列文章中逐渐为您带来讲解。

配置typescript

typescript现在已经成为了前端必备技能之一,大量的项目也开始基于typescript进行开发。在使用Vue2.0的时候,因为Vue2.0没有对typescript进行支持,所以使用ts开发功能显示有些别扭。但到了Vue3,其自身源码便是基于ts开发的,所以对ts天生有着很好的支持。使用vite配置typescript很简单,只需要进行以下几步操作.

  1. 安装 typescript

    yarn add typescript -D
  2. 初始化tsconfig.json

    # 然后在控制台执行下面命令
    npx tsc --init
  3. main.js修改为main.ts,同时将index.html里面的引用也修改为main.ts, 通过还需要修改App.vueHelloWorld.vue文件,修改方式如下

    <!--将 <script> 修改为 <script lang="ts">-->
    <script lang="ts">
    import HelloWorld from './components/HelloWorld.vue'
    
    export default {
      name: 'App',
      components: {
        HelloWorld
      }
    }
    </script>
    

    修改完之后,重启就可以访问项目了。虽然这样配置是可以了,但是打开main.ts会发现import App from App.vue会报错: Cannot find module './App.vue' or its corresponding type declarations.,这是因为现在ts还没有识别vue文件,需要进行下面的配置:

    1. 在项目根目录添加shim.d.ts文件
    2. 添加以下内容

      declare module "*.vue" {
        import { Component } from "vue";
        const component: Component;
        export default component;
      }

接下来你就可以开开心心的在组件中使用ts

配置 vue-router

Vue2.0中我们路由一般会选择使用vue-router,在Vue3.0依然可以使用vue-router,不过和Vue3.0一样当前vue-router的版本也是beta版本,在本文撰写的时候,版本是4.0.0-beta7

安装vue-router

因为当前vue-router针对vue3.0的版本还是beta版本,所以不能直接通过yarn add vue-router进行安装,而是需要带上版本号

yarn add vue-router@4.0.0-beta.7

配置vue-router

在项目src目录下面新建router目录,然后添加index.ts文件,在文件中添加以下内容

import {createRouter, createWebHashHistory} from 'vue-router'

// 在 Vue-router新版本中,需要使用createRouter来创建路由
export default createRouter({
  // 指定路由的模式,此处使用的是hash模式
  history: createWebHashHistory(),
  // 路由地址
  routes: []
})

与新的Vue3.0初始化方式发生变化一样,vue-router的初始化方式也发生了变化,变成了通过createRouter来初始化路由。

router引入到main.ts

修改main.ts文件内容如下

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import router from './router/index'

const  app = createApp(App)
// 通过use 将 路由插件安装到 app 中
app.use(router)
app.mount('#app')

配置 vuex

vue-router一样,新的vuex当前也处于beta版本,当前版本是4.0.0-beta.4

安装vuex

yarn add vuex@4.0.0-beta.4

配置vuex

在项目src目录下面新建store目录,并添加index.ts文件,文件中添加以下内容

import { createStore } from 'vuex'

interface State {
  userName: string
}

export default createStore({
  state(): State {
    return {
      userName: "子君",
    };
  },
});

引入到main.ts

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
import router from './router/index'
import store from './store/index'

const  app = createApp(App)
app.use(router)
app.use(store)
app.mount('#app')

开发TodoList

通过上面的一系列操作,我们的开发环境就已经配置完成了,接下来我们就通过新的开发环境先开发一个TodoList,来验证一下是否正常。

添加todolist页面

  1. 首先我们先在src目录下面新建一个views目录,然后在其中新建文件todo-list.vue,并为文件添加以下内容

    
    <template>
      <div class="todo-list">
        <div>
          <label>新增待办</label>
           <input v-model="state.todo" @keyup.enter="handleAddTodo">
        </div>
        <div>
          <h3>待办列表({{todos.length}})</h3>
          <ul>
            <li v-for="item in todos" :key="item.id" @click="handleChangeStatus(item, true)">
              <input type="checkbox">
              <label>{{item.text}}</label>
            </li>
          </ul>
        </div>
        <div><h3>已办列表({{dones.length}})</h3></div>
        <ul>
          <li v-for="item in dones" :key="item.id" @click="handleChangeStatus(item, false)">
              <input type="checkbox" checked>
              <label>{{item.text}}</label>
            </li>
        </ul>
      </div>
    </template>
    <script lang="ts">
     // 在vue2中 data 在vue3中使用 reactive代替
    import { reactive, computed } from 'vue'
    import { useRouter } from 'vue-router'
    export default {
      // setup相当于vue2.0的 beforeCreate和 created,是vue3新增的一个属性,所有的操作都在此属性中完成
      setup(props, context) {
        // 通过reactive 可以初始化一个可响应的数据,与Vue2.0中的Vue.observer很相似
        const state = reactive({
          todoList: [{
            id: 1,
            done: false,
            text: '吃饭'
          },{
            id: 2,
            done: false,
            text: '睡觉'
          },{
            id: 3,
            done: false,
            text: '打豆豆'
          }],
          todo: ''
        })
        // 使用计算属性生成待办列表
        const todos = computed(() => {
          return state.todoList.filter(item => !item.done)
        })
    
        // 使用计算属性生成已办列表
        const dones = computed(() => {
          return state.todoList.filter(item => item.done)
        })
    
        // 修改待办状态
        const handleChangeStatus = (item ,status) => {
          item.done = status
        }
        
        // 新增待办
        const handleAddTodo = () => {
          if(!state.todo) {
            alert('请输入待办事项')
            return
          }
          state.todoList.push({
            text: state.todo,
            id: Date.now(),
            done: false
          })
          state.todo = ''
        }
    
            // 在Vue3.0中,所有的数据和方法都通过在setup 中 return 出去,然后在template中使用
        return {
          state,
          todos,
          dones,
          handleChangeStatus,
          handleAddTodo
        }
      }
    }
    </script>
    <style scoped>
    .todo-list{
      text-align: center;
    }
    
    .todo-list ul li {
      list-style: none;
    }
    </style>

    调整路由

    1. 首先将App.vue文件内容修改为

      <template>
        <router-view></router-view>
      </template>
      
      <script lang="ts">
      
      export default {
        name: 'App'
      }
      </script>
    2. 然后修改 router/index.ts文件,添加新的路由

      import {createRouter, createWebHashHistory} from 'vue-router'
      
      // 在 Vue-router新版本中,需要使用createRouter来创建路由
      export default createRouter({
        // 指定路由的模式,此处使用的是hash模式
        history: createWebHashHistory(),
        // 路由地址
        routes: [{
          path: '/todolist',
          // 必须添加.vue后缀
          component: () => import('../views/todo-list.vue')
        }]
      })

      这时候我们就可以通过http://localhost:3000/#/todolist来访问TodoList了,效果如下图所示

总结

到此,我们Vue3.0的开发环境算是搭建完成了,当然现在还有好多好多要完善的东西,比如我们还需要去调整一下typescript的配置,然后添加eslint等等。同时如何在组件中跳转路由,使用vuex还没有去讲解,不过至少我们已经起步了,更多的内容将会在下一篇文章中讲到。本文首发于公众号【前端有的玩】,欢迎关注加我好友,我们一起探讨Vue3.0

查看原文

赞 59 收藏 39 评论 12

子君 赞了文章 · 8月19日

好孩子的编码习惯

前言

我经常能听到一些对话
狗腿子A:哇 我刚刚去改**项目的代码,看的我有点怀疑人生
狗腿子B: 我现在项目的跟屎山一样
狗腿子C: 我隔壁那哥们每天写代码都特别随性,我有点按耐不住我的刀
.....
image.png
今天跟大家聊聊一些 我眼中 好孩子的编码习惯,而不是代码风格习惯 ,当然还是强烈建议大家代码风格跟psr-12psr-1靠齐。

psr-1基础编码规范 、psr-12编码规范托充
This specification extends, expands and replaces PSR-2, the coding style guide and requires adherence to PSR-1, the basic coding standard.

离职流程.png

推荐一本《代码整洁之道》,这本书我已经书都快翻烂了,墙裂推荐!!!

image.png

不过度的if嵌套判断

案例背景
有个函数需要判断用户是否参与活动
流程图1.png

案例代码

    if (用户 == VIP) {
        if (用户的过期时间 <= 1个月内) {
            if (用户没参加过任务) {
                return true;
            }
        } else {
            return false;
        }
    } else {
        return true
    }
    

面对这种多条件的判断可以试着用拦截法逆向思维
拦截法只要符合条件立马返回结果,不再嵌套的if。可以理解成横向判断变成纵向判断。

舒适感
从上往下看 > 从左往右看

逆向思维 大家上学的时候都了解过,与其漫天去找符合的条件还不如找不符合条件,这样的逻辑代码可以少很多。

    if (用户 != VIP) {
        return true;
    } 
    
    if (用户参加过任务) {
        return false;
    }
    
    if (用户的过期时间 <= 1个月内) {
        return true;    
    }
    
    return false;
    

不过度的try-catch嵌套

我遇到过很多项目都过度嵌套try-catch导致最上层的try-catchcatch了寂寞。
image.png

案例代码

function insertUser($data)
{
    try {
        userIsInValid();
    } catch (Exception $exception) {
    }
}

function userIsInValid()
{
    try {
        //逻辑判断
    } catch (Exception $exception) {
        return true;
    }
    return true;
    
}

这样的代码没有问题,但是如果假设userIsInValid真的发生代码级的错误没法知道那里出问题,虽然不会破坏业务的健壮性。
可能有人说了在Excetion加个日志,但是如果嵌套的try-catch多了,排查日志也是一件很痛苦的事情。

1.尽可能业务最上层包裹异常 除非网络IO请求函数。
2.如果非要异常嵌套 需要定义每个异常的类型。
3.尽可能根据特定的异常进行catch 不建议直接catch Exception。
4.异常和日志是个cp,还是不要忘记了。
image.png

<?php

function insertUser($data)
{
    try {
        userIsInValid();
    } catch (Exception $exception) {
        // 日志
        // 业务处理
    } catch (HttpException $httpException) {
       // 日志
       // 业务处理
    }
}

function userIsInValid()
{
    //
    return true;
}

不要用if-else做错误类型判断

案例代码 (来源某个网民前段时间咨询)

<?php

.....
if ($code === 'NOTENOUGH') {
    packApiData(400014, 'Company have no enough money to pay', [], '企业余额不足');
} elseif ($code === 'AMOUNT_LIMIT') {
    packApiData(400015, 'Amount limit', [], '金额超限或被微信风控拦截');
} elseif ($code === 'OPENID_ERROR') {
    packApiData(400016, 'Appid and Openid does not match', [], 'Openid格式错误或不属于此公众号');
} elseif ($code === 'SEND_FAILED') {
    // 付款错误,要查单来看最终结果
    if ($orderInfo[1]['status'] == 'SUCCESS') {
        // 还是成功给了,扣回余额
        
        packApiData(200, 'success', [$orderInfo[1]]);
    } else {
        packApiData(400017, 'Weixin pay failed', [], '微信支付付款失败');
    }
} elseif ($code === 'SYSTEMERROR') {
    packApiData(400018, 'Weixin pay server error', [], '微信支付服务器错误');
} elseif ($code === 'NAME_MISMATCH') {
    packApiData(400019, 'Real name mismatch', [], '微信用户的真名校验失败');
} elseif ($code === 'FREQ_LIMIT') {
    packApiData(400020, 'Api request frequently', [], '微信支付接口调用过于频繁,请稍候再请求');
} elseif ($code === 'MONEY_LIMIT') {
    packApiData(400021, 'Company have reached total payment limit', [], '已经达到今日付款总额上限');
} elseif ($code === 'V2_ACCOUNT_SIMPLE_BAN') {
    packApiData(400022, 'This payment account has no real name', [], '用户的微信支付账户未实名');
} elseif ($code === 'SENDNUM_LIMIT') {
    packApiData(400023, 'The number of times the user paid today exceeded the limit', [], '该用户今日收款次数超过限制');
}

这样的代码可能写起来特别舒服,但是后期进行业务的增加改写和时间的沉淀,容易变成让人害怕的屎山代码。

image.png

我们用mapping错误码来调整下

function packApiDataByOrderError($code)
{
    $errorCodeMappins = [
        "NOTENOUGH" => [
            "code" => 400014,
            "wx_message" => "Company have no enough money to pay",
            "error_message" => "企业余额不足"
        ],

        "AMOUNT_LIMIT" => [
            "code" => 400015,
            "wx_message" => "Amount limit",
            "error_message" => "金额超限或被微信风控拦截"
        ],

        .....
    ];

    if (array_key_exists($code, $errorCodeMappins)) {
        packApiData(
            $errorCodeMappins[$code]['code'],
            $errorCodeMappins[$code]['wx_message'],
            [],
            $errorCodeMappins[$code]['error_message']
        );
    }

    packApiData(
        999999,
        "undefined message",
        [],
        "未知错误"
    );
}

建议errorCodeMappins不要放在函数内,可以放在类顶部或者专门枚举类。
通过errorCode 可以避免调整主流程代码,能够保证主流程的代码比较精简也能对不同的code进行错误的定义

if ($code == "SEND_FAILED") {
    // 付款错误,要查单来看最终结果
    if ($orderInfo[1]['status'] == 'SUCCESS') {
        // 还是成功给了,扣回余额
        PDOQuery($dbcon, 'UPDATE user SET money=money-? WHERE open_id=?', [$payAmount, $openId], [PDO::PARAM_INT, PDO::PARAM_STR]);
        packApiData(200, 'success', [$orderInfo[1]]);
    } else {
        packApiData(400017, 'Weixin pay failed', [], '微信支付付款失败');
    }
}

packApiDataByOrderError($code);

在合适的场景使用设计模式

上述可能只能针对错误码进行改造,如果万一我们需要不同的错误进行逻辑处理还怎么办。这时候可以考虑用设计模式 (比如用以多态取代条件表达式)

设计模式固好但不要过度使用,不然整个项目更难维护,你要坚信未来的你队友不知道是什么样的生物

image.png

$callbackCodeMappings = [
    "SEND_FAILED" => OrderSendFailed::class,
];

if (array_key_exists($code, $callbackCodeMappings)) {
    $class = new $callbackCodeMappings[$code];
    $class->handle();
}


interface OrderStateImp
{
    public function handle($context);
}

class OrderSendFailed implements  OrderStateImp
{
    public function handle($context)
    {

    }
}

$callbackCodeMappings同样建议配置专门枚举文件内。
给出得代码比较粗糙,其实可以更加健壮性的做一些判断

统一处理浮点运算结果

由于php是弱对象语言,所以面对一堆情况总能出现,这个订单数据怎么不对了,接口有问题。

$int = 0.58; var_dump(intval($int * 100));
output:57

在浮点数里面 58是被视为57.999999999999999999999……9999无限接近58
再intval强制转换乘整型的时候就默认采用截取法取整

所以最好养成一个好习惯每次在计算浮点数的时候用
BC Math

$int = 0.58;
intval(strval($int * 100))

或者使用BC MATH

bcmul(0.58, 100, 0);

image.png

鼓励用全局错误码来控制错误

写接口的我们对以下的json格式特别熟悉

{
    "success": true,
    "error_code": 0,
    "message": "",
    "results": []
}

对以下的代码也已经熟悉

if (***) {
    $this->error(999,"****", []);
}

这样的结果的错误码容易重复没有统一管理,事实上唯一错误码应该有以下帮助。
1.前端可以根据错误码做逻辑处理
2.根据错误码能直接快速定位到错误代码

建议

<?php

namespace App\ErrorCode;

class UserErrorCode
{
    const USER_DISABLE_ERROR = [
        "error_code" => 1050001,
        "message" => "用户已被停用"
    ];
}

$this->error(UserErrorCode::USER_DISABLE_ERROR);

错误码建议

1-2位 - 项目码 | 3-4位 - 模块码 | 5-7位具体业务错误码

可靠的命名规范

不可靠的命名总会让人误导。
比如变量命名为userArrayList 我以为是个数组列表变量,事实上这个特么是个对象列表。

1.做有意义的区分
比如 singleUserItemuserItem有啥区别
比如 getUserListgetUsers有啥区别
image.png

2.可以通过搜索翻译能知道的变量含义
不要把变量贴入搜索翻译会出现七七八八的东西
3.如果真的不知道该怎么翻试试用拼音把别硬凹了
比如之前做百度的一个接口对接
变量命名为hundredDegree而不是baidu
image.png
其他的可以参照《代码简洁之道》

擅用middleware

middleware可以理解成观察者模式,我们开发的接口总会遇到很多同样操作,比如
1.身份检测
2.权限判断
3.请求参数filter调整
4.记录接口信息
5.接口限流
我见过挨个接口去实现、也见过初始化一个ControllerBase的类,实现这些,子类的Controller去继承这些。
其实我们可以抽离成middleware去实现
image.png

好处可以根据不同接口对middleware进行组合选择,而不是对代码进行各特殊化处理.

函数的单一职责

最最最最后也是最重要的,代码的恶心大多数来源于函数的职责不清晰,有全都塞在一起的、东一块西一块的。
其实关于单一职责有很多文章在描述,如何去检验或者去写符合标准的单一职责。
画流程图
如果你能把业务的流程图画的特别清晰,那么你的函数的职责也就定下来了。
image.png

<?php

// 兑换逻辑
function doExchange()
{
    if (checkIsLock()) {
        
    }
    lock();
    if (!checkUserIsExchange()) {
        
    }
    costUserPoint();
    exchangeGoods();
}
// 判断是否悲观锁
function checkIsLock(){}
// 上悲观锁
function lock(){}
// 判断用户是否可以兑换
function checkUserIsExchange(){}
// 扣除积分
function costUserPoint(){}
// 兑换商品
function exchangeGoods(){}

最后

上述为洪光光心中的好孩子的习惯,也有可能是你眼中坏孩子的习惯。如果你认为是坏孩子的习惯或者认为还有其他好孩子的习惯欢迎评论撕逼讨论。
毕竟
image.png

留个彩蛋 看看大家怎么实现
写一个函数returnScoreResult,请根据输入的分数,返回对应的成绩的等级。
1.如果分数小于0或者大于100 返回 【无效分数】
2.如果分数>=0,<60 返回 【不及格】
3.如果分数>=60,<70 返回【及格】
4.如果分数>=70,<80 返回 【一般】
5.如果分数>=80, <90 返回 【良好】
5.如果分数>=90, <100 返回 【优秀】
6.如果分数=100 返回【满分】

查看原文

赞 38 收藏 24 评论 7

子君 发布了文章 · 8月19日

学习Vue3.0,先来了解一下Proxy

产品经理身旁过,需求变更逃不过。
测试姐姐眯眼笑,今晚bug必然多。

据悉Vue3.0的正式版将要在本月(8月)发布,从发布到正式投入到正式项目中,还需要一定的过渡期,但我们不能一直等到Vue3正式投入到项目中的时候才去学习,提前学习,让你更快一步掌握Vue3.0,升职加薪迎娶白富美就靠它了。不过在学习Vue3之前,还需要先了解一下Proxy,它是Vue3.0实现数据双向绑定的基础。

本文是作者关于Vue3.0系列的第一篇文章,后续作者将会每周发布一篇Vue3.0相关,如果喜欢,麻烦给小编一个赞,谢谢

了解代理模式

一个例子

作为一个单身钢铁直男程序员,小王最近逐渐喜欢上了前台小妹,不过呢,他又和前台小妹不熟,所以决定委托与前端小妹比较熟的UI小姐姐帮忙给自己搭桥引线。小王于是请UI小姐姐吃了一顿大餐,然后拿出一封情书委托它转交给前台小妹,情书上写的 我喜欢你,我想和你睡觉,不愧钢铁直男。不过这样写肯定是没戏的,UI小姐姐吃人嘴短,于是帮忙改了情书,改成了我喜欢你,我想和你一起在晨辉的沐浴下起床,然后交给了前台小妹。虽然有没有撮合成功不清楚啊,不过这个故事告诉我们,小王活该单身狗。

其实上面就是一个比较典型的代理模式的例子,小王想给前台小妹送情书,因为不熟所以委托UI小姐姐UI小姐姐相当于代理人,代替小王完成了送情书的事情。

引申

通过上面的例子,我们想想Vue的数据响应原理,比如下面这段代码


const xiaowang = {
  love: '我喜欢你,我想和你睡觉'
}
// 送给小姐姐情书
function sendToMyLove(obj) {
    console.log(obj.love)
    return '流氓,滚'
}
console.log(sendToMyLove(xiaowang))

如果没有UI小姐姐代替送情书,显示结局是悲惨的,想想Vue2.0的双向绑定,通过Object.defineProperty来监听的属性 get,set方法来实现双向绑定,这个Object.defineProperty就相当于UI小姐姐

const xiaowang = {
  loveLetter: '我喜欢你,我想和你睡觉'
}
// UI小姐姐代理
Object.defineProperty(xiaowang,'love', {
  get() {
    return xiaowang.loveLetter.replace('睡觉','一起在晨辉的沐浴下起床')
  }
})

// 送给小姐姐情书
function sendToMyLove(obj) {
    console.log(obj.love)
    return '小伙子还挺有诗情画意的么,不过老娘不喜欢,滚'
}
console.log(sendToMyLove(xiaowang))

虽然依然是一个悲惨的故事,因为送奔驰的成功率可能会更高一些。但是我们可以看到,通过Object.defineproperty可以对对象的已有属性进行拦截,然后做一些额外的操作。

存在的问题

Vue2.0中,数据双向绑定就是通过Object.defineProperty去监听对象的每一个属性,然后在get,set方法中通过发布订阅者模式来实现的数据响应,但是存在一定的缺陷,比如只能监听已存在的属性,对于新增删除属性就无能为力了,同时无法监听数组的变化,所以在Vue3.0中将其换成了功能更强大的Proxy

了解Proxy

ProxyES6新推出的一个特性,可以用它去拦截js操作的方法,从而对这些方法进行代理操作。

用Proxy重写上面的例子

比如我们可以通过Proxy对上面的送情书情节进行重写:

const xiaowang = {
  loveLetter: '我喜欢你,我想和你睡觉'
}
const proxy = new Proxy(xiaowang, {
  get(target,key) {
    if(key === 'loveLetter') {
      return target[key].replace('睡觉','一起在晨辉的沐浴下起床')
    }
  }
})
// 送给小姐姐情书
function sendToMyLove(obj) {
    console.log(obj.loveLetter)
    return '小伙子还挺有诗情画意的么,不过老娘不喜欢,滚'
}
console.log(sendToMyLove(proxy))

再看这样一个场景

请分别使用Object.definePropertyProxy完善下面的代码逻辑.

function observe(obj, callback) {}

const obj = observe(
  {
    name: '子君',
    sex: '男'
  },
  (key, value) => {
    console.log(`属性[${key}]的值被修改为[${value}]`)
  }
)

// 这段代码执行后,输出 属性[name]的值被修改为[妹纸]
obj.name = '妹纸'

// 这段代码执行后,输出 属性[sex]的值被修改为[女]
obj.sex = '女'

看了上面的代码,希望大家可以先自行实现以下,下面我们分别用Object.definePropertyProxy去实现上面的逻辑.

  1. 使用Object.defineProperty
/**
 * 请实现这个函数,使下面的代码逻辑正常运行
 * @param {*} obj 对象
 * @param {*} callback 回调函数
 */
function observe(obj, callback) {
  const newObj = {}
  Object.keys(obj).forEach(key => {
    Object.defineProperty(newObj, key, {
      configurable: true,
      enumerable: true,
      get() {
        return obj[key]
      },
      // 当属性的值被修改时,会调用set,这时候就可以在set里面调用回调函数
      set(newVal) {
        obj[key] = newVal
        callback(key, newVal)
      }
    })
  })
  return newObj
}

const obj = observe(
  {
    name: '子君',
    sex: '男'
  },
  (key, value) => {
    console.log(`属性[${key}]的值被修改为[${value}]`)
  }
)

// 这段代码执行后,输出 属性[name]的值被修改为[妹纸]
obj.name = '妹纸'

// 这段代码执行后,输出 属性[sex]的值被修改为[女]
obj.name = '女'
  1. 使用Proxy
function observe(obj, callback) {
  return new Proxy(obj, {
    get(target, key) {
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      callback(key, value)
    }
  })
}

const obj = observe(
  {
    name: '子君',
    sex: '男'
  },
  (key, value) => {
    console.log(`属性[${key}]的值被修改为[${value}]`)
  }
)

// 这段代码执行后,输出 属性[name]的值被修改为[妹纸]
obj.name = '妹纸'

// 这段代码执行后,输出 属性[sex]的值被修改为[女]
obj.name = '女'

通过上面两种不同实现方式,我们可以大概的了解到Object.definePropertyProxy的用法,但是当给对象添加新的属性的时候,区别就出来了,比如

// 添加公众号字段
obj.gzh = '前端有的玩'

使用Object.defineProperty无法监听到新增属性,但是使用Proxy是可以监听到的。对比上面两段代码可以发现有以下几点不同

  • Object.defineProperty监听的是对象的每一个属性,而Proxy监听的是对象自身
  • 使用Object.defineProperty需要遍历对象的每一个属性,对于性能会有一定的影响
  • Proxy对新增的属性也能监听到,但Object.defineProperty无法监听到。

初识Proxy

概念与语法

MDN中,关于Proxy是这样介绍的: Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。什么意思呢?Proxy就像一个拦截器一样,它可以在读取对象的属性,修改对象的属性,获取对象属性列表,通过for in循环等等操作的时候,去拦截对象上面的默认行为,然后自己去自定义这些行为,比如上面例子中的set,我们通过拦截默认的set,然后在自定义的set里面添加了回调函数的调用

Proxy的语法格式如下

/**
* target: 要兼容的对象,可以是一个对象,数组,函数等等
* handler: 是一个对象,里面包含了可以监听这个对象的行为函数,比如上面例子里面的`get`与`set`
* 同时会返回一个新的对象proxy, 为了能够触发handler里面的函数,必须要使用返回值去进行其他操作,比如修改值
*/
const proxy = new Proxy(target, handler)

在上面的例子里面,我们已经使用到了handler里面提供的getset方法了,接下来我们一一看一下handler里面的方法。

handler 里面的方法列表

handler里面的方法可以有以下这十三个,每一个都对应的一种或多种针对proxy代理对象的操作行为

  1. handler.get

    当通过proxy去读取对象里面的属性的时候,会进入到get钩子函数里面

  2. handler.set

    当通过proxy去为对象设置修改属性的时候,会进入到set钩子函数里面

  3. handler.has

    当使用in判断属性是否在proxy代理对象里面时,会触发has,比如

    const obj = {
      name: '子君'
    }
    console.log('name' in obj)
  4. handler.deleteProperty

    当使用delete去删除对象里面的属性的时候,会进入deleteProperty`钩子函数

  5. handler.apply

    proxy监听的是一个函数的时候,当调用这个函数时,会进入apply钩子函数

  6. handle.ownKeys

    当通过Object.getOwnPropertyNames,Object.getownPropertySymbols,Object.keys,Reflect.ownKeys去获取对象的信息的时候,就会进入ownKeys这个钩子函数

  7. handler.construct

    当使用new操作符的时候,会进入construct这个钩子函数

  8. handler.defineProperty

    当使用Object.defineProperty去修改属性修饰符的时候,会进入这个钩子函数

  9. handler.getPrototypeOf

    当读取对象的原型的时候,会进入这个钩子函数

  10. handler.setPrototypeOf

    当设置对象的原型的时候,会进入这个钩子函数

  11. handler.isExtensible

    当通过Object.isExtensible去判断对象是否可以添加新的属性的时候,进入这个钩子函数

  12. handler.preventExtensions

    当通过Object.preventExtensions去设置对象不可以修改新属性时候,进入这个钩子函数

  13. handler.getOwnPropertyDescriptor

    在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时会进入这个钩子函数

Proxy提供了十三种拦截对象操作的方法,本文主要挑选其中一部分在Vue3中比较重要的进行说明,其余的建议可以直接阅读MDN关于Proxy的介绍。

详细介绍

get

当通过proxy去读取对象里面的属性的时候,会进入到get钩子函数里面

当我们从一个proxy代理上面读取属性的时候,就会触发get钩子函数,get函数的结构如下

/**
 * target: 目标对象,即通过proxy代理的对象
 * key: 要访问的属性名称
 * receiver: receiver相当于是我们要读取的属性的this,一般情况
 *           下他就是proxy对象本身,关于receiver的作用,后文将具体讲解
 */
handle.get(target,key, receiver)
示例

我们在工作中经常会有封装axios的需求,在封装过程中,也需要对请求异常进行封装,比如不同的状态码返回的异常信息是不同的,如下是一部分状态码及其提示信息:

// 状态码提示信息
const errorMessage = {
  400: '错误请求',
  401: '系统未授权,请重新登录',
  403: '拒绝访问',
  404: '请求失败,未找到该资源'
}

// 使用方式
const code = 404
const message = errorMessage[code]
console.log(message)

但这存在一个问题,状态码很多,我们不可能每一个状态码都去枚举出来,所以对于一些异常状态码,我们希望可以进行统一提示,如提示为系统异常,请联系管理员,这时候就可以使用Proxy对错误信息进行代理处理

// 状态码提示信息
const errorMessage = {
  400: '错误请求',
  401: '系统未授权,请重新登录',
  403: '拒绝访问',
  404: '请求失败,未找到该资源'
}

const proxy = new Proxy(errorMessage, {
  get(target,key) {
    const value = target[key]
    return value || '系统异常,请联系管理员'
  }
})

// 输出 错误请求
console.log(proxy[400])
// 输出 系统异常,请联系管理员
console.log(proxy[500])

set

当为对象里面的属性赋值的时候,会触发set

当给对象里面的属性赋值的时候,会触发set,set函数的结构如下

/**
 * target: 目标对象,即通过proxy代理的对象
 * key: 要赋值的属性名称
 * value: 目标属性要赋的新值
 * receiver: 与 get的receiver 基本一致
 */
handle.set(target,key,value, receiver)
示例

某系统需要录入一系列数值用于数据统计,但是在录入数值的时候,可能录入的存在一部分异常值,对于这些异常值需要在录入的时候进行处理, 比如大于100的值,转换为100, 小于0的值,转换为0, 这时候就可以使用proxyset,在赋值的时候,对数据进行处理

const numbers = []
const proxy = new Proxy(numbers, {
  set(target,key,value) {
    if(value < 0) {
      value = 0
    }else if(value > 100) {
      value = 100
    }
    target[key] = value
    // 对于set 来说,如果操作成功必须返回true, 否则会被视为失败
    return true
  }
})

proxy.push(1)
proxy.push(101)
proxy.push(-10)
// 输出 [1, 100, 0]
console.log(numbers)
对比Vue2.0

在使用Vue2.0的时候,如果给对象添加新属性的时候,往往需要调用$set, 这是因为Object.defineProperty只能监听已存在的属性,而新增的属性无法监听,而通过$set相当于手动给对象新增了属性,然后再触发数据响应。但是对于Vue3.0来说,因为使用了Proxy, 在他的set钩子函数中是可以监听到新增属性的,所以就不再需要使用$set

const obj = {
  name: '子君'
}
const proxy = new Proxy(obj, {
  set(target,key,value) {
    if(!target.hasOwnProperty(key)) {
      console.log(`新增了属性${key},值为${value}`)
    }
    target[key] = value
    return true
  }
})
// 新增 公众号 属性
// 输出 新增了属性gzh,值为前端有的玩
proxy.gzh = '前端有的玩'

has

当使用in判断属性是否在proxy代理对象里面时,会触发has
/**
 * target: 目标对象,即通过proxy代理的对象
 * key: 要判断的key是否在target中
 */
 handle.has(target,key)
示例

一般情况下我们在js中声明私有属性的时候,会将属性的名字以_开头,对于这些私有属性,是不需要外部调用,所以如果可以隐藏掉是最好的,这时候就可以通过has在判断某个属性是否在对象时,如果以_开头,则返回false

const obj =  {
  publicMethod() {},
  _privateMethod(){}
}
const proxy = new Proxy(obj, {
  has(target, key) {
    if(key.startsWith('_')) {
      return false
    }
    return Reflect.get(target,key)
  }
})

// 输出 false
console.log('_privateMethod' in proxy)

// 输出 true
console.log('publicMethod' in proxy)

deleteProperty

当使用delete去删除对象里面的属性的时候,会进入deleteProperty`拦截器
/**
 * target: 目标对象,即通过proxy代理的对象
 * key: 要删除的属性
 */
 handle.deleteProperty(target,key)
示例

现在有一个用户信息的对象,对于某些用户信息,只允许查看,但不能删除或者修改,对此使用Proxy可以对不能删除或者修改的属性进行拦截并抛出异常,如下

const userInfo = {
  name: '子君',
  gzh: '前端有的玩',
  sex: '男',
  age: 22
}
// 只能删除用户名和公众号
const readonlyKeys = ['name', 'gzh']
const proxy = new Proxy(userInfo, {
  set(target,key,value) {
    if(readonlyKeys.includes(key)) {
      throw new Error(`属性${key}不能被修改`)
    }
    target[key] = value
    return true
  },
   deleteProperty(target,key) {
    if(readonlyKeys.includes(key)) {
      throw new Error(`属性${key}不能被删除`)
      return
    }
    delete target[key]
    return true
  }
})
// 报错 
delete proxy.name
对比Vue2.0

其实与$set解决的问题类似,Vue2.0是无法监听到属性被删除的,所以提供了$delete用于删除属性,但是对于Proxy,是可以监听删除操作的,所以就不需要再使用$delete

其他操作

在上文中,我们提到了Proxyhandler提供了十三个函数,在上面我们列举了最常用的三个,其实每一个的用法都是基本一致的,比如ownKeys,当通过Object.getOwnPropertyNames,Object.getownPropertySymbols,Object.keys,Reflect.ownKeys去获取对象的信息的时候,就会进入ownKeys这个钩子函数,使用这个我们就可以对一些我们不像暴露的属性进行保护,比如一般会约定_开头的为私有属性,所以在使用Object.keys去获取对象的所有key的时候,就可以把所有_开头的属性屏蔽掉。关于剩余的那些属性,建议大家多去看看MDN中的介绍。

Reflect

在上面,我们获取属性的值或者修改属性的值都是通过直接操作target来实现的,但实际上ES6已经为我们提供了在Proxy内部调用对象的默认行为的API,即Reflect。比如下面的代码

const obj = {}
const proxy = new Proxy(obj, {
  get(target,key,receiver) {
    return Reflect.get(target,key,receiver)
  }
})

大家可能看到上面的代码与直接使用target[key]的方式没什么区别,但实际上Reflect的出现是为了让Object上面的操作更加规范,比如我们要判断某一个prop是否在一个对象中,通常会使用到in,即

const obj = {name: '子君'}
console.log('name' in obj)

但上面的操作是一种命令式的语法,通过Reflect可以将其转变为函数式的语法,显得更加规范

Reflect.has(obj,'name')

除了has,get之外,其实Reflect上面总共提供了十三个静态方法,这十三个静态方法与Proxyhandler上面的十三个方法是一一对应的,通过将ProxyReflect相结合,就可以对对象上面的默认操作进行拦截处理,当然这也就属于函数元编程的范畴了。

总结

有的同学可能会有疑惑,我不会ProxyReflect就学不了Vue3.0了吗?其实懂不懂这个是不影响学习Vue3.0的,但是如果想深入 去理解Vue3.0,还是很有必要了解这些的。比如经常会有人在使用Vue2的时候问,为什么我数组通过索引修改值之后,界面没有变呢?当你了解到Object.defineProperty的使用方式与限制之后,就会恍然大悟,原来如此。本文之后,小编将为大家带来Vue3.0系列文章,欢迎关注,一起学习。同时本文首发于公众号【前端有的玩】,用玩的姿势学前端,就在【前端有的玩】

查看原文

赞 36 收藏 22 评论 0

子君 收藏了文章 · 8月4日

了解JS压缩图片,这一篇就够了

image

前言

公司的移动端业务需要在用户上传图片是由前端压缩图片大小,再上传到服务器,这样可以减少移动端上行流量,减少用户上传等待时长,优化用户体验。

插播一下,本文案例已整理成插件,已上传 npm ,可通过 npm install js-image-compressor -D 安装使用,可以从 github 下载。

JavaScript 操作压缩图片原理不难,已有成熟 API,然而在实际输出压缩后结果却总有意外,有些图片竟会越压缩越大,加之终端(手机)类型众多,有些手机压缩图片甚至变黑。

压缩小龙女,哈哈哈😂

所以本文将试图解决如下问题:

  • 弄清 Image 对象、data URLCanvasFile(Blob)之间的转化关系;
  • 图片压缩关键技巧;
  • 超大图片压缩黑屏问题。

转化关系

在实际应用中有可能使用的情境:大多时候我们直接读取用户上传的 File 对象,读写到画布(canvas)上,利用 CanvasAPI 进行压缩,完成压缩之后再转成 File(Blob) 对象,上传到远程图片服务器;不妨有时候我们也需要将一个 base64 字符串压缩之后再变为 base64 字符串传入到远程数据库或者再转成 File(Blob) 对象。一般的,它们有如下转化关系:

js-image-compressor-flow-chat

具体实现

下面将按照转化关系图中的转化方法一一实现。

file2DataUrl(file, callback)

用户通过页面标签 <input type="file" /> 上传的本地图片直接转化 data URL 字符串形式。可以使用 FileReader 文件读取构造函数。FileReader 对象允许 Web 应用程序异步读取存储在计算机上的文件(或原始数据缓冲区)的内容,使用 FileBlob 对象指定要读取的文件或数据。该实例方法 readAsDataURL 读取文件内容并转化成 base64 字符串。在读取完后,在实例属性 result 上可获取文件内容。

function file2DataUrl(file, callback) {
  var reader = new FileReader();
  reader.onload = function () {
    callback(reader.result);
  };
  reader.readAsDataURL(file);
}

Data URL 由四个部分组成:前缀(data:)、指示数据类型的 MIME 类型、如果非文本则为可选的 base64 标记、数据本身:

data:<mediatype>,<data>

比如一张 png 格式图片,转化为 base64 字符串形式:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAgAElEQVR4XuxdB5g

file2Image(file, callback)

若想将用户通过本地上传的图片放入缓存并 img 标签显示出来,除了可以利用以上方法转化成的 base64 字符串作为图片 src,还可以直接用 URL 对象,引用保存在 FileBlob 中数据的 URL。使用对象 URL 的好处是可以不必把文件内容读取到 JavaScript 中 而直接使用文件内容。为此,只要在需要文件内容的地方提供对象 URL 即可。

function file2Image(file, callback) {
  var image = new Image();
  var URL = window.webkitURL || window.URL;
  if (URL) {
    var url = URL.createObjectURL(file);
    image.onload = function() {
      callback(image);
      URL.revokeObjectURL(url);
    };
    image.src = url;
  } else {
    inputFile2DataUrl(file, function(dataUrl) {
      image.onload = function() {
        callback(image);
      }
      image.src = dataUrl;
    });
  }
}

注意:要创建对象 URL,可以使用 window.URL.createObjectURL() 方法,并传入 FileBlob 对象。如果不再需要相应数据,最好释放它占用的内容。但只要有代码在引用对象 URL,内存就不会释放。要手工释放内存,可以把对象 URL 传给 URL.revokeObjectURL()

url2Image(url, callback)

通过图片链接(url)获取图片 Image 对象,由于图片加载是异步的,因此放到回调函数 callback 回传获取到的 Image 对象。

function url2Image(url, callback) {
  var image = new Image();
  image.src = url;
  image.onload = function() {
    callback(image);
  }
}

image2Canvas(image)

利用 drawImage() 方法将 Image 对象绘画在 Canvas 对象上。

drawImage 有三种语法形式:

void ctx.drawImage(image, dx, dy);
void ctx.drawImage(image, dx, dy, dWidth, dHeight);
void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);

参数:

  • image 绘制到上下文的元素;
  • sx 绘制选择框左上角以 Image 为基准 X 轴坐标;
  • sy 绘制选择框左上角以 Image 为基准 Y 轴坐标;
  • sWidth 绘制选择框宽度;
  • sHeight 绘制选择框宽度;
  • dxImage 的左上角在目标 canvasX 轴坐标;
  • dyImage 的左上角在目标 canvasY 轴坐标;
  • dWidthImage 在目标 canvas 上绘制的宽度;
  • dHeightImage 在目标 canvas 上绘制的高度;

canvas-draw-image

function image2Canvas(image) {
  var canvas = document.createElement('canvas');
  var ctx = canvas.getContext('2d');
  canvas.width = image.naturalWidth;
  canvas.height = image.naturalHeight;
  ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
  return canvas;
}

canvas2DataUrl(canvas, quality, type)

HTMLCanvasElement 对象有 toDataURL(type, encoderOptions) 方法,返回一个包含图片展示的 data URL 。同时可以指定输出格式和质量。

参数分别为:

  • type 图片格式,默认为 image/png
  • encoderOptions在指定图片格式为 image/jpegimage/webp 的情况下,可以从 01 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92,其他参数会被忽略。
function canvas2DataUrl(canvas, quality, type) {
  return canvas.toDataURL(type || 'image/jpeg', quality || 0.8);
}

dataUrl2Image(dataUrl, callback)

图片链接也可以是 base64 字符串,直接赋值给 Image 对象 src 即可。

function dataUrl2Image(dataUrl, callback) {
  var image = new Image();
  image.onload = function() {
    callback(image);
  };
  image.src = dataUrl;
}

dataUrl2Blob(dataUrl, type)

data URL 字符串转化为 Blob 对象。主要思路是:先将 data URL 数据(data) 部分提取出来,用 atob 对经过 base64 编码的字符串进行解码,再转化成 Unicode 编码,存储在Uint8Array(8位无符号整型数组,每个元素是一个字节) 类型数组,最终转化成 Blob 对象。

function dataUrl2Blob(dataUrl, type) {
  var data = dataUrl.split(',')[1];
  var mimePattern = /^data:(.*?)(;base64)?,/;
  var mime = dataUrl.match(mimePattern)[1];
  var binStr = atob(data);
  var arr = new Uint8Array(len);

  for (var i = 0; i < len; i++) {
    arr[i] = binStr.charCodeAt(i);
  }
  return new Blob([arr], {type: type || mime});
}

canvas2Blob(canvas, callback, quality, type)

HTMLCanvasElementtoBlob(callback, [type], [encoderOptions]) 方法创造 Blob 对象,用以展示 canvas 上的图片;这个图片文件可以被缓存或保存到本地,由用户代理端自行决定。第二个参数指定图片格式,如不特别指明,图片的类型默认为 image/png,分辨率为 96dpi。第三个参数用于针对image/jpeg 格式的图片进行输出图片的质量设置。

function canvas2Blob(canvas, callback, quality, type){
  canvas.toBlob(function(blob) {
    callback(blob);
  }, type || 'image/jpeg', quality || 0.8);
}

为兼容低版本浏览器,作为 toBlobpolyfill 方案,可以用上面 data URL 生成 Blob 方法 dataUrl2Blob 作为HTMLCanvasElement 原型方法。

if (!HTMLCanvasElement.prototype.toBlob) {
 Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
  value: function (callback, type, quality) {
    let dataUrl = this.toDataURL(type, quality);
    callback(dataUrl2Blob(dataUrl));
  }
 });
}

blob2DataUrl(blob, callback)

Blob 对象转化成 data URL 数据,由于 FileReader 的实例 readAsDataURL 方法不仅支持读取文件,还支持读取 Blob 对象数据,这里复用上面 file2DataUrl 方法即可:

function blob2DataUrl(blob, callback) {
  file2DataUrl(blob, callback);
}

blob2Image(blob, callback)

Blob 对象转化成 Image 对象,可通过 URL 对象引用文件,也支持引用 Blob 这样的类文件对象,同样,这里复用上面 file2Image 方法即可:

function blob2Image(blob, callback) {
  file2Image(blob, callback);
}

upload(url, file, callback)

上传图片(已压缩),可以使用 FormData 传入文件对象,通过 XHR 直接把文件上传到服务器。

function upload(url, file, callback) {
  var xhr = new XMLHttpRequest();
  var fd = new FormData();
  fd.append('file', file);
  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4 && xhr.status === 200) {
      // 上传成功
      callback && callback(xhr.responseText);
    } else {
      throw new Error(xhr);
    }
  }
  xhr.open('POST', url, true);
  xhr.send(fd);
}

也可以使用 FileReader 读取文件内容,转化成二进制上传

function upload(url, file) {
  var reader = new FileReader();
  var xhr = new XMLHttpRequest();

  xhr.open('POST', url, true);
  xhr.overrideMimeType('text/plain; charset=x-user-defined-binary');

  reader.onload = function() {
    xhr.send(reader.result);
  };
  reader.readAsBinaryString(file);
}

实现简易图片压缩

在熟悉以上各种图片转化方法的具体实现,将它们封装在一个公用对象 util 里,再结合压缩转化流程图,这里我们可以简单实现图片压缩了:
首先将上传图片转化成 Image 对象,再将写入到 Canvas 画布,最后由 Canvas 对象 API 对图片的大小和尺寸输出调整,实现压缩目的。

/**
 * 简易图片压缩方法
 * @param {Object} options 相关参数
 */
(function (win) {
  var REGEXP_IMAGE_TYPE = /^image\//;
  var util = {};
  var defaultOptions = {
    file: null,
    quality: 0.8
  };
  var isFunc = function (fn) { return typeof fn === 'function'; };
  var isImageType = function (value) { return REGEXP_IMAGE_TYPE.test(value); };

  /**
   * 简易图片压缩构造函数
   * @param {Object} options 相关参数
   */
  function SimpleImageCompressor(options) {
    options = Object.assign({}, defaultOptions, options);
    this.options = options;
    this.file = options.file;
    this.init();
  }

  var _proto = SimpleImageCompressor.prototype;
  win.SimpleImageCompressor = SimpleImageCompressor;

  /**
   * 初始化
   */
  _proto.init = function init() {
    var _this = this;
    var file = this.file;
    var options = this.options;

    if (!file || !isImageType(file.type)) {
      console.error('请上传图片文件!');
      return;
    }

    if (!isImageType(options.mimeType)) {
      options.mimeType = file.type;
    }

    util.file2Image(file, function (img) {
      var canvas = util.image2Canvas(img);
      file.width = img.naturalWidth;
      file.height = img.naturalHeight;
      _this.beforeCompress(file, canvas);

      util.canvas2Blob(canvas, function (blob) {
        blob.width = canvas.width;
        blob.height = canvas.height;
        options.success && options.success(blob);
      }, options.quality, options.mimeType)
    })
  }

  /**
   * 压缩之前,读取图片之后钩子函数
   */
  _proto.beforeCompress = function beforeCompress() {
    if (isFunc(this.options.beforeCompress)) {
      this.options.beforeCompress(this.file);
    }
  }

  // 省略 `util` 公用方法定义
  // ...

  // 将 `util` 公用方法添加到实例的静态属性上
  for (key in util) {
    if (util.hasOwnProperty(key)) {
      SimpleImageCompressor[key] = util[key];
    }
  }
})(window)

这个简易图片压缩方法调用和入参:

var fileEle = document.getElementById('file');

fileEle.addEventListener('change', function () {
  file = this.files[0];

  var options = {
    file: file,
    quality: 0.6,
    mimeType: 'image/jpeg',
    // 压缩前回调
    beforeCompress: function (result) {
      console.log('压缩之前图片尺寸大小: ', result.size);
      console.log('mime 类型: ', result.type);
      // 将上传图片在页面预览
      // SimpleImageCompressor.file2DataUrl(result, function (url) {
      //   document.getElementById('origin').src = url;
      // })
    },
    // 压缩成功回调
    success: function (result) {
      console.log('压缩之后图片尺寸大小: ', result.size);
      console.log('mime 类型: ', result.type);
      console.log('压缩率: ', (result.size / file.size * 100).toFixed(2) + '%');

      // 生成压缩后图片在页面展示
      // SimpleImageCompressor.file2DataUrl(result, function (url) {
      //   document.getElementById('output').src = url;
      // })

      // 上传到远程服务器
      // SimpleImageCompressor.upload('/upload.png', result);
    }
  };

  new SimpleImageCompressor(options);
}, false);

如果看到这里的客官不嫌弃这个 demo 太简单可以戳这里试试水。如果你有足够的耐心多传几种类型图片就会发现还存在如下问题:

  • 压缩输出图片寸尺固定为原始图片尺寸大小,而实际可能需要控制输出图片尺寸,同时达到尺寸也被压缩目的;
  • png 格式图片同格式压缩,压缩率不高,还有可能出现“不减反增”现象;
  • 有些情况,其他格式转化成 png 格式也会出现“不减反增”现象;
  • 大尺寸 png 格式图片在一些手机上,压缩后出现“黑屏”现象;

越压缩越膨胀😂

改进版图片压缩

俗话说“罗马不是一天建成的”,通过上述实验,我们发现了很多不足,下面将逐条问题分析,寻求解决方案。

压缩输出图片寸尺固定为原始图片尺寸大小,而实际可能需要控制输出图片尺寸,同时达到尺寸也被压缩目的;

为了避免压缩图片变形,一般采用等比缩放,首先要计算出原始图片宽高比 aspectRatio,用户设置的高乘以 aspectRatio,得出等比缩放后的宽,若比用户设置宽的小,则用户设置的高为为基准缩放,否则以宽为基准缩放。

var aspectRatio = naturalWidth / naturalHeight;
var width = Math.max(options.width, 0) || naturalWidth;
var height = Math.max(options.height, 0) || naturalHeight;
if (height * aspectRatio > width) {
  height = width / aspectRatio;
} else {
  width = height * aspectRatio;
}

输出图片的尺寸确定了,接下来就是按这个尺寸创建一个 Canvas 画布,将图片画上去。这里可以将上面提到的 image2Canvas 方法稍微做一下改造:

function image2Canvas(image, destWidth, destHeight) {
  var canvas = document.createElement('canvas');
  var ctx = canvas.getContext('2d');
  canvas.width = destWidth || image.naturalWidth;
  canvas.height = destHeight || image.naturalHeight;
  ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
  return canvas;
}
png 格式图片同格式压缩,压缩率不高,还有可能出现“不减反增”现象

一般的,不建议将 png 格式图片压缩成自身格式,这样压缩率不理想,有时反而会造成自身质量变得更大。

因为我们在“具体实现”中两个有关压缩关键 API

  • toBlob(callback, [type], [encoderOptions]) 参数 encoderOptions 用于针对image/jpeg 格式的图片进行输出图片的质量设置;
  • toDataURL(type, encoderOptions 参数encoderOptions 在指定图片格式为 image/jpegimage/webp 的情况下,可以从 01 的区间内选择图片的质量。

均未对 png 格式图片有压缩效果。

有个折衷的方案,我们可以设置一个阈值,如果 png 图片的质量小于这个值,就还是压缩输出 png 格式,这样最差的输出结果不至于质量太大,在此基础上,如果压缩后图片大小 “不减反增”,我们就兜底处理输出源图片给用户。当图片质量大于某个值时,我们压缩成 jpeg 格式。

// `png` 格式图片大小超过 `convertSize`, 转化成 `jpeg` 格式
if (file.size > options.convertSize && options.mimeType === 'image/png') {
  options.mimeType = 'image/jpeg';
}
// 省略一些代码
// ...
// 用户期待的输出宽高没有大于源图片的宽高情况下,输出文件大小大于源文件,返回源文件
if (result.size > file.size && !(options.width > naturalWidth || options.height > naturalHeight)) {
  result = file;
}
大尺寸 png 格式图片在一些手机上,压缩后出现“黑屏”现象;

由于各大浏览器对 Canvas 最大尺寸支持不同

浏览器最大宽高最大面积
Chrome32,767 pixels268,435,456 pixels(e.g.16,384 x 16,384)
Firefox32,767 pixels472,907,776 pixels(e.g.22,528 x 20,992)
IE8,192 pixelsN/A
IE Mobile4,096 pixelsN/A

如果图片尺寸过大,在创建同尺寸画布,再画上图片,就会出现异常情况,即生成的画布没有图片像素,而画布本身默认给的背景色为黑色,这样就导致图片“黑屏”情况。

这里可以通过控制输出图片最大宽高防止生成画布越界,并且用透明色覆盖默认黑色背景解决解决“黑屏”问题:

// ...
// 限制最小和最大宽高
var maxWidth = Math.max(options.maxWidth, 0) || Infinity;
var maxHeight = Math.max(options.maxHeight, 0) || Infinity;
var minWidth = Math.max(options.minWidth, 0) || 0;
var minHeight = Math.max(options.minHeight, 0) || 0;

if (maxWidth < Infinity && maxHeight < Infinity) {
  if (maxHeight * aspectRatio > maxWidth) {
    maxHeight = maxWidth / aspectRatio;
  } else {
    maxWidth = maxHeight * aspectRatio;
  }
} else if (maxWidth < Infinity) {
  maxHeight = maxWidth / aspectRatio;
} else if (maxHeight < Infinity) {
  maxWidth = maxHeight * aspectRatio;
}

if (minWidth > 0 && minHeight > 0) {
  if (minHeight * aspectRatio > minWidth) {
    minHeight = minWidth / aspectRatio;
  } else {
    minWidth = minHeight * aspectRatio;
  }
} else if (minWidth > 0) {
  minHeight = minWidth / aspectRatio;
} else if (minHeight > 0) {
  minWidth = minHeight * aspectRatio;
}

width = Math.floor(Math.min(Math.max(width, minWidth), maxWidth));
height = Math.floor(Math.min(Math.max(height, minHeight), maxHeight));

// ...
// 覆盖默认填充颜色 (#000)
var fillStyle = 'transparent';
context.fillStyle = fillStyle;

到这里,上述的意外问题被我们一一解决了,如需体验改进版的图片压缩 demo 的小伙伴可以戳这里

总结

我们梳理了通过页面标签 <input type="file" /> 上传本地图片到图片被压缩整个过程,也覆盖到了在实际使用中还存在的一些意外情况,提供了相应的解决方案。将改进版图片压缩整理成插件,已上传 npm ,可通过 npm install js-image-compressor -D 安装使用,可以从 github 下载。整理匆忙,如有问题欢迎大家指正,完~

查看原文

子君 回答了问题 · 8月3日

ELEMENT-UI的v-loading 早异步的时候不生效

谢邀。
我刚看了你的截图,你第一个截图使用的是Promise.then,然后第二个截图使用的是asyn await,要搞清楚为什么第一个loading不生效,第二个生效,就要搞清楚这两者之间的区别。
不论是Promise还是async await,他们都是解决异步回调地狱的方法。说道异步,可能大家第一反应会想到setTimeout,现在我用setTimeout来代替这两者来说明两者的区别。

对于第一张图的Promise, 改成setTimeout就变成了这样

this.roleUserLoading = true
setTimeout(() =>{
  console.log('这里相当于Promise.then')
},1000)
this.roleUserLoading = false

对于第二张图的async await, 改成setTimeout就变成了这样

this.roleUserLoading = true
setTimeout(() =>{
  console.log('这里相当于await')
  this.roleUserLoading = false
},1000)

对比上面的代码,我们会发现this.roleUserLoading一个在setTimeout外面,一个在里面,在外面的因为setTimout是异步的,所以首先会执行

this.roleUserLoading = true
this.roleUserLoading = false

然后在1秒后才执行setTimeout的内容,而上面改变roleUserLoading的值是瞬间完成的,所以看不到loading

现在再说一下async awaitawait后面的代码都会等待await调用的异步执行完之后再执行,就相当于把await后面的所有代码都放到了setTimeout里面了,这时候loading就必须等待setTimeout被调用执行之后才会被关闭。你明白了吗?

那如何在promise上面也可以用loading呢,我们可以像下面代码这样

promise.then(() => {}).finally(() =>{
  this.roleUserLoading = false
})

如果觉得有用,麻烦给我一个赞,谢谢,同时也可以关注我的公众号【前端有的玩】,拉你进前端交流群,一起学习前端技术。

关注 3 回答 2

子君 发布了文章 · 8月3日

Vue中使用装饰器,我是认真的

产品上线事繁多,测试产品催不离。
休问Bug剩多少,眼圈如漆身如泥。

作为一个曾经的Java coder, 当我第一次看到js里面的装饰器(Decorator)的时候,就马上想到了Java中的注解,当然在实际原理和功能上面,Java的注解和js的装饰器还是有很大差别的。本文题目是Vue中使用装饰器,我是认真的,但本文将从装饰器的概念开发聊起,一起来看看吧。

通过本文内容,你将学到以下内容:

  1. 了解什么是装饰器
  2. 在方法使用装饰器
  3. class中使用装饰器
  4. Vue中使用装饰器
本文首发于公众号【前端有的玩】,不想当咸鱼,想要换工作,关注公众号,带你每日一起刷大厂面试题,关注 === 大厂offer

什么是装饰器

装饰器是ES2016提出来的一个提案,当前处于Stage 2阶段,关于装饰器的体验,可以点击 https://github.com/tc39/proposal-decorators查看详情。装饰器是一种与类相关的语法糖,用来包装或者修改类或者类的方法的行为,其实装饰器就是设计模式中装饰者模式的一种实现方式。不过前面说的这些概念太干了,我们用人话来翻译一下,举一个例子。

在日常开发写bug过程中,我们经常会用到防抖和节流,比如像下面这样

class MyClass {
  follow = debounce(function() {
    console.log('我是子君,关注我哦')
  }, 100)
}

const myClass = new MyClass()
// 多次调用只会输出一次
myClass.follow()
myClass.follow()

上面是一个防抖的例子,我们通过debounce函数将另一个函数包起来,实现了防抖的功能,这时候再有另一个需求,比如希望在调用follow函数前后各打印一段日志,这时候我们还可以再开发一个log函数,然后继续将follow包装起来

/**
 * 最外层是防抖,否则log会被调用多次
 */
class MyClass {
  follow = debounce(
    log(function() {
      console.log('我是子君,关注我哦')
    }),
    100
  )
}

上面代码中的debouncelog两个函数,本质上是两个包装函数,通过这两个函数对原函数的包装,使原函数的行为发生了变化,而js中的装饰器的原理就是这样的,我们使用装饰器对上面的代码进行改造

class MyClass {
  @debounce(100)
  @log
  follow() {
    console.log('我是子君,关注我哦')
  }
}

装饰器的形式就是 @ + 函数名,如果有参数的话,后面的括号里面可以传参

在方法上使用装饰器

装饰器可以应用到class上或者class里面的属性上面,但一般情况下,应用到class属性上面的场景会比较多一些,比如像上面我们说的log,debounce等等,都一般会应用到类属性上面,接下来我们一起来具体看一下如何实现一个装饰器,并应用到类上面。在实现装饰器之前,我们需要先了解一下属性描述符

了解一下属性描述符

在我们定义一个对象里面的属性的时候,其实这个属性上面是有许多属性描述符的,这些描述符标明了这个属性能不能修改,能不能枚举,能不能删除等等,同时ECMAScript将这些属性描述符分为两类,分别是数据属性和访问器属性,并且数据属性与访问器属性是不能共存的。

数据属性

数据属性包含一个数据值的位置,在这个位置可以读取和写入值。数据属性包含了四个描述符,分别是

  1. configurable

    表示能不能通过delete删除属性,能否修改属性的其他描述符特性,或者能否将数据属性修改为访问器属性。当我们通过let obj = {name: ''}声明一个对象的时候,这个对象里面所有的属性的configurable描述符的值都是true

  2. enumerable

    表示能不能通过for in或者Object.keys等方式获取到属性,我们一般声明的对象里面这个描述符的值是true,但是对于class类里面的属性来说,这个值是false

  3. writable

    表示能否修改属性的数据值,通过将这个修改为false,可以实现属性只读的效果。

  4. value

    表示当前属性的数据值,读取属性值的时候,从这里读取;写入属性值的时候,会写到这个位置。

访问器属性

访问器属性不包含数据值,他们包含了gettersetter两个函数,同时configurableenumerable是数据属性与访问器属性共有的两个描述符。

  1. getter

    在读取属性的时候调用这个函数,默认这个函数为undefined

  2. setter

    在写入属性值的时候调用这个函数,默认这个函数为undefined

了解了这六个描述符之后,你可能会有几个疑问: 我如何去定义修改这些属性描述符?这些属性描述符与今天的文章主题有什么关系?接下来是揭晓答案的时候了。

使用Object.defineProperty

了解过vue2.0双向绑定原理的同学一定知道,Vue的双向绑定就是通过使用Object.defineProperty去定义数据属性的gettersetter方法来实现的,比如下面有一个对象

let obj = {
  name: '子君',
  officialAccounts: '前端有的玩'
}

我希望这个对象里面的用户名是不能被修改的,用Object.defineProperty该如何定义呢?

Object.defineProperty(obj,'name', {
  // 设置writable 是 false, 这个属性将不能被修改
  writable: false
})
// 修改obj.name
obj.name = "君子"
// 打印依然是子君
console.log(obj.name)

通过Object.defineProperty可以去定义或者修改对象属性的属性描述符,但是因为数据属性与访问器属性是互斥的,所以一次只能修改其中的一类,这一点需要注意。

定义一个防抖装饰器

装饰器本质上依然是一个函数,不过这个函数的参数是固定的,如下是防抖装饰器的代码

/**
*@param wait 延迟时长
*/
function debounce(wait) {
  return function(target, name, descriptor) {
    descriptor.value = debounce(descriptor.value, wait)
  }
}
// 使用方式
class MyClass {
  @debounce(100)
  follow() {
    console.log('我是子君,我的公众号是 【前端有的玩】,关注有惊喜哦')
  }
}

我们逐行去分析一下代码

  1. 首先我们定义了一个 debounce函数,同时有一个参数wait,这个函数对应的就是在下面调用装饰器时使用的@debounce(100)
  2. debounce函数返回了一个新的函数,这个函数即装饰器的核心,这个函数有三个参数,下面逐一分析

    1. target: 这个类属性函数是在谁上面挂载的,如上例对应的是MyClass
    2. name: 这个类属性函数的名称,对应上面的follow
    3. descriptor: 这个就是我们前面说的属性描述符,通过直接descriptor上面的属性,即可实现属性只读,数据重写等功能
  3. 然后第三行 descriptor.value = debounce(descriptor.value, wait), 前面我们已经了解到,属性描述符上面的value对应的是这个属性的值,所以我们通过重写这个属性,将其用debounce函数包装起来,这样在函数调用follow时实际调用的是包装后的函数

通过上面的三步,我们就实现了类属性上面可使用的装饰器,同时将其应用到了类属性上面

class上使用装饰器

装饰器不仅可以应用到类属性上面,还可以直接应用到类上面,比如我希望可以实现一个类似Vue混入那样的功能,给一个类混入一些方法属性,应该如何去做呢?

// 这个是要混入的对象
const methods = {
  logger() {
    console.log('记录日志')
  }
}

// 这个是一个登陆登出类
class Login{
  login() {}
  logout() {}
}

如何将上面的methods混入到Login中,首先我们先实现一个类装饰器

function mixins(obj) {
  return function (target) {
    Object.assign(target.prototype, obj)  
  }
}

// 然后通过装饰器混入
@mixins(methods)
class Login{
  login() {}
  logout() {}
}

这样就实现了类装饰器。对于类装饰器,只有一个参数,即target,对应的就是这个类本身。

了解完装饰器,我们接下来看一下如何在Vue中使用装饰器。

Vue中使用装饰器

使用ts开发Vue的同学一定对vue-property-decorator不会感到陌生,这个插件提供了许多装饰器,方便大家开发的时候使用,当然本文的中点不是这个插件。其实如果我们的项目没有使用ts,也是可以使用装饰器的,怎么用呢?

配置基础环境

除了一些老的项目,我们现在一般新建Vue项目的时候,都会选择使用脚手架vue-cli3/4来新建,这时候新建的项目已经默认支持了装饰器,不需要再配置太多额外的东西,如果你的项目使用了eslint,那么需要给eslint配置以下内容。

  parserOptions: {
    ecmaFeatures:{
      // 支持装饰器
      legacyDecorators: true
    }
  }

使用装饰器

虽然Vue的组件,我们一般书写的时候export出去的是一个对象,但是这个并不影响我们直接在组件中使用装饰器,比如就拿上例中的log举例。

function log() {
  /**
   * @param target 对应 methods 这个对象
   * @param name 对应属性方法的名称
   * @param descriptor 对应属性方法的修饰符
   */
  return function(target, name, descriptor) {
    console.log(target, name, descriptor)
    const fn = descriptor.value
    descriptor.value = function(...rest) {
      console.log(`这是调用方法【${name}】前打印的日志`)
      fn.call(this, ...rest)
      console.log(`这是调用方法【${name}】后打印的日志`)
    }
  }
}

export default {
  created() {
    this.getData()
  },
  methods: {
    @log()
    getData() {
      console.log('获取数据')
    }
  }
}

看了上面的代码,是不是发现在Vue中使用装饰器还是很简单的,和在class的属性上面使用的方式一模一样,但有一点需要注意,在methods里面的方法上面使用装饰器,这时候装饰器的target对应的是methods

除了在methods上面可以使用装饰器之外,你也可以在生命周期钩子函数上面使用装饰器,这时候target对应的是整个组件对象。

一些常用的装饰器

下面小编罗列了几个小编在项目中常用的几个装饰器,方便大家使用

1. 函数节流与防抖

函数节流与防抖应用场景是比较广的,一般使用时候会通过throttledebounce方法对要调用的函数进行包装,现在就可以使用上文说的内容将这两个函数封装成装饰器, 防抖节流使用的是lodash提供的方法,大家也可以自行实现节流防抖函数哦

import { throttle, debounce } from 'lodash'
/**
 * 函数节流装饰器
 * @param {number} wait 节流的毫秒
 * @param {Object} options 节流选项对象
 * [options.leading=true] (boolean): 指定调用在节流开始前。
 * [options.trailing=true] (boolean): 指定调用在节流结束后。
 */
export const throttle =  function(wait, options = {}) {
  return function(target, name, descriptor) {
    descriptor.value = throttle(descriptor.value, wait, options)
  }
}

/**
 * 函数防抖装饰器
 * @param {number} wait 需要延迟的毫秒数。
 * @param {Object} options 选项对象
 * [options.leading=false] (boolean): 指定在延迟开始前调用。
 * [options.maxWait] (number): 设置 func 允许被延迟的最大值。
 * [options.trailing=true] (boolean): 指定在延迟结束后调用。
 */
export const debounce = function(wait, options = {}) {
  return function(target, name, descriptor) {
    descriptor.value = debounce(descriptor.value, wait, options)
  }
}

封装完之后,在组件中使用

import {debounce} from '@/decorator'

export default {
  methods:{
    @debounce(100)
    resize(){}
  }
}

2. loading

在加载数据的时候,为了个用户一个友好的提示,同时防止用户继续操作,一般会在请求前显示一个loading,然后在请求结束之后关掉loading,一般写法如下

export default {
  methods:{
    async getData() {
      const loading = Toast.loading()
      try{
        const data = await loadData()
        // 其他操作
      }catch(error){
        // 异常处理
        Toast.fail('加载失败');
      }finally{
        loading.clear()
      }  
    }
  }
}

我们可以把上面的loading的逻辑使用装饰器重新封装,如下代码

import { Toast } from 'vant'

/**
 * loading 装饰器
 * @param {*} message 提示信息
 * @param {function} errorFn 异常处理逻辑
 */
export const loading =  function(message = '加载中...', errorFn = function() {}) {
  return function(target, name, descriptor) {
    const fn = descriptor.value
    descriptor.value = async function(...rest) {
      const loading = Toast.loading({
        message: message,
        forbidClick: true
      })
      try {
        return await fn.call(this, ...rest)
      } catch (error) {
        // 在调用失败,且用户自定义失败的回调函数时,则执行
        errorFn && errorFn.call(this, error, ...rest)
        console.error(error)
      } finally {
        loading.clear()
      }
    }
  }
}

然后改造上面的组件代码

export default {
  methods:{
    @loading('加载中')
    async getData() {
      try{
        const data = await loadData()
        // 其他操作
      }catch(error){
        // 异常处理
        Toast.fail('加载失败');
      }  
    }
  }
}

3. 确认框

当你点击删除按钮的时候,一般都需要弹出一个提示框让用户确认是否删除,这时候常规写法可能是这样的

import { Dialog } from 'vant'

export default {
  methods: {
    deleteData() {
      Dialog.confirm({
        title: '提示',
        message: '确定要删除数据,此操作不可回退。'
      }).then(() => {
        console.log('在这里做删除操作')
      })
    }
  }
}

我们可以把上面确认的过程提出来做成装饰器,如下代码

import { Dialog } from 'vant'

/**
 * 确认提示框装饰器
 * @param {*} message 提示信息
 * @param {*} title 标题
 * @param {*} cancelFn 取消回调函数
 */
export function confirm(
  message = '确定要删除数据,此操作不可回退。',
  title = '提示',
  cancelFn = function() {}
) {
  return function(target, name, descriptor) {
    const originFn = descriptor.value
    descriptor.value = async function(...rest) {
      try {
        await Dialog.confirm({
          message,
          title: title
        })
        originFn.apply(this, rest)
      } catch (error) {
        cancelFn && cancelFn(error)
      }
    }
  }
}

然后再使用确认框的时候,就可以这样使用了

export default {
  methods: {
    // 可以不传参,使用默认参数
    @confirm()
    deleteData() {
      console.log('在这里做删除操作')
    }
  }
}

是不是瞬间简单多了,当然还可以继续封装很多很多的装饰器,因为文章内容有限,暂时提供这三个。

装饰器组合使用

在上面我们将类属性上面使用装饰器的时候,说道装饰器可以组合使用,在Vue组件上面使用也是一样的,比如我们希望在确认删除之后,调用接口时候出现loading,就可以这样写(一定要注意顺序)

export default {
  methods: {
    @confirm()
    @loading()
    async deleteData() {
      await delete()
    }
  }
}
本节定义的装饰器,均已应用到这个项目中 https://github.com/snowzijun/vue-vant-base, 这是一个基于Vant开发的开箱即用移动端框架,你只需要fork下来,无需做任何配置就可以直接进行业务开发,欢迎使用,喜欢麻烦给一个star

我是子君,今天就写这么多,本文首发于【前端有的玩】,这是一个专注于前端技术,前端面试相关的公众号,同时关注之后即刻拉你加入前端交流群,我们一起聊前端,欢迎关注。

结语

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高
查看原文

赞 24 收藏 17 评论 0

子君 发布了文章 · 7月27日

封装element-ui表格,我是这样做的

日日加班至夜半,环视周围无人走;

明晚八点准时走,谁不打卡谁是狗。

使用过element-ui的表格的同学应该都有这样的体会,做一个简单的表格还比较容易,但如果这个表格包含了顶部的按钮,还有分页,甚至再包含了行编辑,那开发工作量就成倍的增加,特别是在开发管理系统的时候,表格一个接一个的去开发, 即浪费时间,还对个人没有什么提升。今天小编带来了自己封装的一个表格,让你用JSON就可以简单的生成表格。

本文主要集中于使用说明与核心代码说明,完整代码请访问 https://github.com/snowzijun/vue-element-table,如果觉得有用,麻烦给小编一个star,你的每一个star都是对小编的支持,当前功能比较简陋,本仓库将持续更新。同时您也可以微信搜索【前端有的玩】公众号,小编拉你进前端技术交流群。

表格需求

一般管理系统对表格会有以下需求

  1. 可以分页(需要有分页条)
  2. 可以多选(表格带复选框)
  3. 顶部需要加一些操作按钮(新增,删除等等)
  4. 表格每行行尾有操作按钮
  5. 表格行可以编辑

如下图为一个示例表格

如果我们直接使用element-ui提供的组件的话,那么开发一个这样的表格就需要使用到以下内容

  1. 需要使用表格的插槽功能,开发每一行的按钮
  2. 需要通过样式调整顶部按钮,表格,分页条的布局样式
  3. 需要监听分页的事件然后去刷新表格数据
  4. 顶部按钮或操作按钮如果需要获取表格数据,需要调用表格提供的api
  5. 对于有行编辑的需求,还需要通过插槽去渲染行编辑的内容,同时要控制行编辑的开关

不仅仅开发表格比较麻烦,而且还要考虑团队协作,如果每个人实现表格的方式存在差别,那么可能后期的维护成本也会变得很高。那怎么办呢?

项目安装

安装插件

在使用element-ui的项目中,可以通过以下命令进行安装

npm install vue-elementui-table -S

在项目中使用

main.js中添加以下代码

import ZjTable from 'vue-element-table'

Vue.use(ZjTable)

然后即可像下文中的使用方式进行使用

表格配置

为了满足团队快速开发的需要,小编对上面提出来的需求进行了封装,然后使用的时候,开发人员只需要配置一些JSON便可以完成以上功能的开发。

基础配置

一个基础的表格包含了数据和列信息,那么如何用封装的表格去配置呢?

<template>
  <zj-table
    :columns="columns"
    :data="data"
    :pagination="false"
  />
</template>
<script>
export default {
  data() {
    return {
      // 表格的列信息, 数组每一项代表一个字段,可以使用element 列属性的所有属性,以下仅为示例
      columns: Object.freeze([
        {
          // 表头显示的文字
          label: '姓名',
          // 对应数据里面的字段
          prop: 'name'
        },
        {
          label: '性别',
          prop: 'sex',
          // 格式化表格,与element-ui 的表格属性相同
          formatter(row, column, cellValue) {
            return cellValue === 1 ? '男' : '女'
          }
        },
        {
          label: '年龄',
          prop: 'age'
        }
      ]),
      data: [
        {
          name: '子君',
          sex: 1,
          age: 18
        }
      ]
    }
  }
}
</script>

通过上面的配置,就可以完成一个基础表格的开发,完整代码见 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/base.vue,效果如下图所示

表格默认会显示复选框,也可以通过配置selectable属性来关闭掉

添加分页

简单的表格用封装之后的或未封装的开发工作量区别并不大,我们继续为表格添加上分页

<template>
    <!--
    current-page.sync 表示页码, 添加上 .sync 在页码发生变化时自动同步页码
    page-size.sync 每页条数
    total  总条数
    height="auto" 配置height:auto, 表格高度会根据内容自动调整,如果不指定,表格将保持充满父容器,同时表头会固定,不跟随滚动条滚动
    @page-change 无论pageSize currentPage 哪一个变化,都会触发这个事件
  -->
  <zj-table
    v-loading="loading"
    :columns="columns"
    :data="data"
    :current-page.sync="currentPage"
    :page-size.sync="pageSize"
    :total="total"
    height="auto"
    @page-change="$_handlePageChange"
  />
</template>
<script>
export default {
  data() {
    return {
      columns: Object.freeze([
        // 列字段与上例一样,此处省略
      ]),
      data: [],
      // 当前页码
      currentPage: 1,
      // 每页条数
      pageSize: 10,
      // 总条数
      total: 0,
      // 是否显示loading
      loading: false
    }
  },
  created() {
    this.loadData()
  },
  methods: {
    // 加载表格数据
    loadData() {
      this.loading = true
      setTimeout(() => {
        // 假设总条数是40条
        this.total = 40
        const { currentPage, pageSize } = this
        // 模拟数据请求获取数据
        this.data = new Array(pageSize).fill({}).map((item, index) => {
          return {
            name: `子君${currentPage + (index + 1) * 10}`,
            sex: Math.random() > 0.5 ? 1 : 0,
            age: Math.floor(Math.random() * 100)
          }
        })
        this.loading = false
      }, 1000)
    },
    $_handlePageChange() {
      // 因为上面设置属性指定了.sync,所以这两个属性会自动变化
      console.log(this.pageSize, this.currentPage)
      // 分页发生变化,重新请求数据
      this.loadData()
    }
  }
}
</script>

完整代码请参考 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/pagination.vue

通过封装,表格将自带分页功能,通过上面代码,实现效果如下所示,是不是变得简单了一些。接下来我们继续给表格添加按钮

添加顶部按钮

表格上面可能会有新增,删除等等按钮,怎么办呢,接下来我们继续通过配置去添加按钮

<template>
  <zj-table
    :buttons="buttons"
  />
</template>
<script>
export default {
  data() {
    return {
      buttons: Object.freeze([
        {
          // id 必须有而且是在当前按钮数组里面是唯一的
          id: 'add',
          text: '新增',
          type: 'primary',
          icon: 'el-icon-circle-plus',
          click: this.$_handleAdd
        },
        {
          id: 'delete',
          text: '删除',
          // rows 是表格选中的行,如果没有选中行,则禁用删除按钮, disabled可以是一个boolean值或者函数
          disabled: rows => !rows.length,
          click: this.$_handleRemove
        },
        {
          id: 'auth',
          text: '这个按钮根据权限显示',
          // 可以通过返回 true/false来控制按钮是否显示
          before: (/** rows */) => {
            return true
          }
        },
        // 可以配置下拉按钮哦
        {
          id: 'dropdown',
          text: '下拉按钮',
          children: [
            {
              id: 'moveUp',
              text: '上移',
              icon: 'el-icon-arrow-up',
              click: () => {
                console.log('上移')
              }
            },
            {
              id: 'moveDown',
              text: '下移',
              icon: 'el-icon-arrow-down',
              disabled: rows => !rows.length,
              click: () => {
                console.log('下移')
              }
            }
          ]
        }
      ])
    }
  },
  created() {},
  methods: {
    // 新增
    $_handleAdd() {
      this.$alert('点击了新增按钮')
    },
    // 顶部按钮会自动将表格所选的行传出来
    $_handleRemove(rows) {
      const ids = rows.map(({ id }) => id)
      this.$alert(`要删除的行id为${ids.join(',')}`)
    },
    // 关注作者公众号
    $_handleFollowAuthor() {}
  }
}
</script>

表格顶部可以添加普通的按钮,也可以添加下拉按钮,同时还可以通过before来配置按钮是否显示,disabled来配置按钮是否禁用,上面完整代码见 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/button.vue

通过上面的代码就可以配置出下面的表格,是不是很简单呢?

表格顶部可以有按钮,行尾也是可以添加按钮的,一起来看看

行操作按钮

一般我们会将一些单行操作的按钮放在行尾,比如编辑,下载等按钮,那如何给行尾配置按钮呢?

<template>
  <zj-table
    :columns="columns"
  />
</template>
<script>
export default {
  data() {
    return {
      columns: Object.freeze([
        {
          // 可以指定列的宽度,与element-ui原生用法一致
          width: 220,
          label: '姓名',
          prop: 'name'
        },
        // 行编辑按钮,在表格末尾出现,自动锁定右侧
        {
          width: 180,
          label: '操作',
          // 通过 actions 指定行尾按钮
          actions: [
            {
              id: 'follow',
              text: '关注作者',
              click: this.$_handleFollowAuthor
            },
            {
              id: 'edit',
              text: '编辑',
              // 可以通过before控制按钮是否显示,比如下面年龄四十岁的才会显示编辑按钮
              before(row) {
                return row.age < 40
              },
              click: this.$_handleEdit
            },
            {
              id: 'delete',
              text: '删除',
              icon: 'el-icon-delete',
              disabled(row) {
                return row.sex === 0
              },
              // 为了拿到this,这里需要用箭头函数
              click: () => {
                this.$alert('女生被禁止删除了')
              }
            }
          ]
        }
      ])
    }
  },
  methods: {
    // 关注作者公众号
    $_handleFollowAuthor() {
            console.log('微信搜索【前端有的玩】,这是对小编最大的支持')
    },
    /**
     * row 这一行的数据
     */
    $_handleEdit(row, column) {
      this.$alert(`点击了姓名为【${row.name}】的行上的按钮`)
    }
  }
}
</script>

行操作按钮会被冻结到表格最右侧,不会跟随滚动条滚动而滚动,上面完整代码见, https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/button.vue

通过上面的代码就可以完成以下效果

最后再来一起看看行编辑

行编辑

比如上例,我希望点击行尾的编辑按钮的时候,可以直接在行上面编辑用户的姓名与性别,如何配置呢?

<template>
  <zj-table
    ref="table"
    :columns="columns"
    :data="data"
  />
</template>
<script>
export default {
  data() {
    return {
      columns: Object.freeze([
        {
          label: '姓名',
          prop: 'name',
          editable: true,
          field: {
            componentType: 'input',
            rules: [
              {
                required: true,
                message: '请输入姓名'
              }
            ]
          }
        },
        {
          label: '性别',
          prop: 'sex',
          // 格式化表格,与element-ui 的表格属性相同
          formatter(row, column, cellValue) {
            return cellValue === '1' ? '男' : '女'
          },
          editable: true,
          field: {
            componentType: 'select',
            options: [
              {
                label: '男',
                value: '1'
              },
              {
                label: '女',
                value: '0'
              }
            ]
          }
        },
        {
          label: '年龄',
          prop: 'age',
          editable: true,
          field: {
            componentType: 'number'
          }
        },
        {
          label: '操作',
          actions: [
            {
              id: 'edit',
              text: '编辑',
              // 如果当前行启用了编辑,则不显示编辑按钮
              before: row => {
                return !this.editIds.includes(row.id)
              },
              click: this.$_handleEdit
            },
            {
              id: 'save',
              text: '保存',
              // 如果当前行启用了编辑,则显示保存按钮
              before: row => {
                return this.editIds.includes(row.id)
              },
              click: this.$_handleSave
            }
          ]
        }
      ]),
      data: [
        {
          // 行编辑必须指定rowKey字段,默认是id,如果修改为其他字段,需要给表格指定row-key="字段名"
          id: '0',
          name: '子君',
          sex: '1',
          age: 18
        },
        {
          // 行编辑必须指定rowKey字段,默认是id,如果修改为其他字段,需要给表格指定row-key="字段名"
          id: '1',
          name: '子君1',
          sex: '0',
          age: 18
        }
      ],
      editIds: []
    }
  },
  methods: {
    $_handleEdit(row) {
      // 通过调用 startEditRow 可以开启行编辑
      this.$refs.table.startEditRow(row.id)
      // 记录开启了行编辑的id
      this.editIds.push(row.id)
    },
    $_handleSave(row) {
      // 点击保存的时候,通过endEditRow 结束行编辑
      this.$refs.table.endEditRow(row.id, (valid, result, oldRow) => {
        // 如果有表单验证,则valid会返回是否验证成功
        if (valid) {
          console.log('修改之后的数据', result)
          console.log('原始数据', oldRow)
          const index = this.editIds.findIndex(item => item === row.id)
          this.editIds.splice(index, 1)
        } else {
          // 如果校验失败,则返回校验的第一个输入框的异常信息
          console.log(result)
          this.$message.error(result.message)
        }
      })
    }
  }
}
</script>

不需要使用插槽就可以完成行编辑,是不是很开心。上述完整代码见 https://github.com/snowzijun/vue-element-table/blob/master/example/views/demo/row-edit.vue

效果如下图所示:

其他功能

除了上面的功能之外,表格还可以配置其他许多功能,比如

  1. 可以指定字段为链接列,需要给列配置link属性
  2. 可以通过插槽自定义顶部按钮,行操作按钮,行字段等
  3. 可以在按钮区域右侧通过插槽配置其他内容
  4. 其他等等

表格开发说明

通过上面的代码示例,我们已经知道了封装之后的表格可以完成哪些事情,接下来一起来看看表格是如何实现的。完整代码见 https://github.com/snowzijun/vue-element-table/tree/master/src/components/zj-table

表格布局

整个表格是通过JSX来封装的,因为JSX使用起来更加灵活。对于我们封装的表格,我们从竖向可以分为三部分,分别是顶部按钮区,中间表格区,底部分页区,如何去实现三个区域的布局呢,核心代码如下

render(h) {
    // 按钮区域
    const toolbar = this.$_renderToolbar(h)
    // 表格区域
    const table = this.$_renderTable(h)
    // 分页区域
    const page = this.$_renderPage(h)

    return (
      <div class="zj-table" style={{ height: this.tableContainerHeight }}>
        {toolbar}
        {table}
        {page}
      </div>
    )
  }

通过三个render函数分别渲染对应区域,然后将三个区域组合在一起。

渲染表格列

通过前文的讲解,我们可以将表格的列分为以下几种

  1. 常规列
  2. 行编辑列
  3. 操作按钮列
  4. 插槽列
  5. 链接列(文档后续完善)
  6. 嵌套列(文档后续完善)
    $_renderColumns(h, columns) {
      // 整体是否排序
      let sortable = this.sortable ? 'custom' : false
      return columns
        .filter(column => {
          const { hidden } = column
          if (hidden !== undefined) {
            if (typeof hidden === 'function') {
              return hidden({
                columns,
                column
              })
            }
            return hidden
          }
          return true
        })
        .map(column => {
          const {
            useSlot = false,
            // 如果存在操作按钮,则actions为非空数组
            actions = [],
            // 是否可编辑列, 对于可编辑列需要动态启用编辑
            editable = false,
            // 是否有嵌套列
            nests,
            // 是否可点击
            link = false
          } = column
          let newSortable = sortable
          if (column.sortable !== undefined) {
            newSortable = column.sortable ? 'custom' : false
          }
          column = {
            ...column,
            sortable: newSortable
          }
          if (nests && nests.length) {
            // 使用嵌套列
            return this.$_renderNestColumn(h, column)
          } else if (editable) {
            // 使用编辑列
            return this.$_renderEditColumn(h, column)
          } else if (useSlot) {
            // 使用插槽列
            return this.$_renderSlotColumn(h, column)
          } else if (actions && actions.length > 0) {
            // 使用操作列
            column.sortable = false
            return this.$_renderActionColumn(h, column)
          } else if (link) {
            // 使用链接列
            return this.$_renderLinkColumn(h, column)
          } else {
            // 使用默认列
            return this.$_renderDefaultColumn(h, column)
          }
        })
    },

行编辑列

当前表格行编辑支持input,select,datepicker,TimeSelect,InputNumber等组件,具体渲染代码如下所示

// 编辑单元格
    $_renderEditCell(h, field) {
      const components = {
        input: Input,
        select: ZjSelect,
        date: DatePicker,
        time: TimeSelect,
        number: InputNumber
      }
      const componentType = field.componentType
      const component = components[componentType]
      if (component) {
        return this.$_renderField(h, field, component)
      } else if (componentType === 'custom') {
        // 如果自定义,可以通过component指定组件
        return this.$_renderField(h, field, field.component)
      }
      return this.$_renderField(h, field, Input)
    },
    $_renderField(h, field, Component) {
      // 编辑行的id字段
      const { rowId, events = {}, nativeEvents = {} } = field

      const getEvents = events => {
        const newEvents = {}
        Object.keys(events).forEach(key => {
          const event = events[key]
          newEvents[key] = (...rest) => {
            const args = [
              ...rest,
              {
                rowId,
                row: this.editRowsData[rowId],
                value: this.editRowsData[rowId][field.prop]
              }
            ]
            return event(...args)
          }
        })
        return newEvents
      }
      // 事件改写
      const newEvents = getEvents(events)
      const newNativeEvents = getEvents(nativeEvents)
      return (
        <Component
          size="small"
          on={newEvents}
          nativeOn={newNativeEvents}
          v-model={this.editRowsData[rowId][field.prop]}
          {...{
            attrs: field,
            props: field
          }}
        />
      )
    }

总结

这个表格包含了许多功能,文章长度优先,如果觉得有用,可以通过访问 https://github.com/snowzijun/vue-element-table 查看完整代码,本仓库代码及文档小编将持续完善,欢迎star。

结语

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高
查看原文

赞 27 收藏 16 评论 2

子君 赞了文章 · 7月23日

那些年遇到的刁钻JavaScript面试题(可防踩坑)

仅以此文纪念这些年失败的面试和逝去的头发。。

第 1 题:console.log(2 + '2')

答案:'22'

解析:
这是比较常规的面试题了,主要考察的是 JavaScript 中的隐式类型转换。在 JS 中 + 主要有两个作用:数字相加和字符串拼接,当 + 两边不都为数字时会把它们都转为字符串再拼接,所以第一个 2 会先被转成 '2' 再与第二个 '2' 拼接。

第 2 题:console.log(2 - '2')

答案:0

解析:
+ 不同,- 没有操作字符串而只有 “减法” 的功能,当 - 两边有非数字时会先把其转换成数字再相减。所以,本题中的 '2' 先被转成数字 2,最终 2 - 2 等于 0。当操作数没法转换成数字时则会导致结果为 NaN,比如 'foo' - 2 = NaN

*/% 的行为也和 - 类似。

第 3 题:console.log(true + 1)

答案:2

解析:
有了第1题的经验,我们很容易就认为 true1 都会被转成字符串,但实际上 JS 中 true == 1, false == 0 ,所以 true 会被转成 1 再执行加法。

注意:true 只等于 1false 只等于 0,等于其他数字是不成立的,如 true == 2false

第 4 题:console.log(NaN === NaN)

答案:false

解析:
NaN 表示一个不为数字的值(Not a number)。我们只需要记住:NaN和所有值都不等,包括它自己,不管是用 == 还是 === 判断!判断一个值是否为 NaN 只能用 isNaN() 或者 Number.isNaN()

第 5 题:1. console.log(5 < 6 < 7) 2. console.log(7 > 6 > 5)

答案:(1) true;(2) false

解析:
根据第三题的经验 true == 1false == 05 < 6 === true1 < 7,所以 1 为 true7 > 6 === true1 > 5 === false,所以 2 为 false

第 6 题:0.1 + 0.2 = ?

答案:0.30000000000000004

解析:
问题的关键不在于答案里面有几个 0,而在于它不等于 0.3!Javascript 中的数字使用的是 64 位双精度浮点型(可参考 ECMAScript 规范)。如同十进制不能精确表示 1/3 对应的小数,二进制也没有办法精确表示 0.10.2 这种小数,比如 0.1 转成二进制为:0.0001100110011001100110011001100110011001100110011001101,是无限循环的小数,而因为计算机不可能无限分配内存去存储这个数,一般只能精确到多少位,所以造成精度丢失在所难免,最终导致计算结果有所偏差。

第 7 题:[1, 2, 3] + [4, 5, 6] = ?

答案:1,2,34,5,6

解析:
本题主要考察隐式类型转换和数组转字符串,我们已经知道 + 两边如果不都为数字则会把它们转成字符串再拼接,而 [1, 2, 3].toString() === '1,2,3',因为最终结果为 '1,2,3' + '4,5,6' === '1,2,34,5,6'

如果我们想要进行数组拼接可以:

[1, 2, 3].concat([4, 5, 6]);

// 或者使用spread opertator
[...[1, 2, 3], ...[4, 5, 6]];

第 8 题:求打印结果

(function () {
  var a = b = 100;
})();

console.log(a);
console.log(b);
答案:报错(Uncaught ReferenceError: a is not defined

解析:
由于赋值表达式是从右往左执行的,相当于 var a = (b = 100);,所以先执行 b = 100,由于函数体中并没有局部变量 b,所以会定义一个全局变量 b 并赋值 100;接着执行 a = b,会把b的值赋值给局部变量 a。执行 console.log(a) 时会直接报错,因为全局作用域中并没有定义 a,由于报错导致程序中断所以 console.log(b) 没有执行。如果把 console.log(b) 放在前面就是先打印 100 再报错了。

使用严格模式('use strict')可以避免b这种意外全局变量的创建。

第 9 题:求打印结果

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log(i);
  }, 0);
}
答案:5 5 5 5 5

解析:
这是个经典问题,考察对作用域和 event loop 的理解。用 var 定义的变量的作用域是函数作用域(functional scope),所以上面代码相当于:

var i;

for (i = 0; i < 5; i++) {
  ...
}

最终 console.log(i) 里面的 i 引用的是外面的那个,而由于event loop机制,setTimeout 的回调函数会被push到task queue里面等call stack里面的for循环结束了再执行,此时 i 已经变成了5,因此最终打印结果变成5个5。随着ES6的普及,var 使用的机会越来越少,letconst 成为主流,本题如果想打印 0 1 2 3 4,只要把 var 换成 let 就行了。

第 10 题:求打印结果

function foo() {
  return x;

  function x() {}
}

console.log(typeof foo());
答案:'function'

解析:
本题考察的是对函数提升的理解。一般定义一个函数有两种方式:

  1. 函数声明(function declaration):function fn(){ ... }
  2. 函数表达式(function expression):var fn = function(){ ... }

其中函数声明会存在函数提升的现象,而函数表达式则没有(函数表达式会存在变量提升,但初始化并不会被提升,所以不具有函数提升的效果),即JS在编译阶段会把对函数的定义提升到作用域顶部(实际上并不会修改代码结构,而是在内存中进行处理),所以本题的代码等价于:

function foo() {
  function x() {
    console.log(`hi`);
  }
  
  return x;
}

所以,结果打印 function。函数提升的主要作用是可以在函数定义之前就进行调用。

第 11 题:求打印结果

var x = 1;
function x(){}

console.log(typeof x);
答案:'number'

解析:
本题还是考察变量提升和函数提升,以及它们的优先级。函数提升的优先级要高于变量提升,所以函数被提升到作用域最顶部,接下来才是变量定义,因此本题等价于:

function x(){}
var x;
x = 1;

console.log(typeof x);

所以,x 最终是数字。

第 12 题:求打印结果

const fn = () => arguments;

console.log(fn("hi"));
答案:报错 Uncaught ReferenceError: arguments is not defined

解析:
本题主要考察箭头函数的特点。箭头函数没有自己的 thisarguments,而是引用的外层作用域中的,而全局没有定义 arguments 变量,所以报错。

在箭头函数中如果要访问参数集,建议使用 Rest parameters:(...args) => { }

第 13 题:求打印结果

const fn = function () {
  return
  {
    message: "hello";
  }
};

console.log(fn());
答案:undefined

解析:

在 JavaScript 中,如果 return 关键词和返回值之间存在换行符(Line Terminator),则 return 后面会自动插入 ';',参考 ASI (Automatic semicolon insertion)。所以本题代码等同于:

const fn = function () {
  return;
  {
    message: "hello";
  }
};

console.log(fn());

结果因此为 undefined

第 14 题:求打印结果

setTimeout(() => {
  console.log("a");
}, 1);

setTimeout(() => {
  console.log("b");
}, 0);
答案:有可能是 'a' 'b',也有可能是 'b' 'a',取决于 js 运行环境。
  • 在 Node.js 中,0ms1ms是等价的,因为 0 会被转成 1(可参考Node源码),所以在 node 中运行结果是 'a' 'b'
  • Chrome 和 node 类似,结果也是 'a' 'b'
  • Firefox 中会打印 'b' 'a'

该题属于“回”字有多少种写法那一类的,并无多大的实际价值 😢。

第 15 题:event.targetevent.currentTarget 的区别

答案:event.target 是真正触发 event 的元素,而 event.currentTarget 是绑定 event handler 的元素。

例如:

<div id="container">
  <button>click me</button>
</div>
const container = document.getElementById("container");
container.addEventListener("click", function (e) {
  console.log("target =", e.target);
  console.log("currentTarget =", e.currentTarget);
});

target

第 16 题:求打印结果

function(){
  console.log('hi');
}()
答案:报错:Uncaught SyntaxError: Function statements require a function name

解析:
本题主要考察对 IIFE 语法的理解。本题代码等价于:

function(){
  console.log('hi');
}

()

所以报语法错误,而正确的 IIFE 语法应该是 (function(){...})()

第 17 题:求打印结果

const arr = [1, 2, 3];
arr[-1] = -1;
console.log(arr[arr.indexOf(100)]);
答案:-1

解析:
本题主要考察对 JavaScript 对象的理解和数组的 indexOf() 方法。首先,数组本质还是一个 JavaScript 对象,那就可以设置 属性,就算数组的索引没有 -1,但 -1 仍可作为对象的 key 存在,所以 x[-1] = -1 没有问题。接着,indexOf() 方法所要查找的值如果在数组中不存在则返回 -1,所以最终相当于求 console.log(arr[-1]),得到最终答案为 -1

第 18 题:求数组排序后的结果

const arr = [5, 22, 1, 14, 2, 56, 132, 88, 12];
console.log(arr.sort());
答案:[1, 12, 132, 14, 2, 22, 5, 56, 88]

解析:

本题主要考察对数组 sort() 方法的理解。 sort() 默认是把元素转成字符串,再比较 UTF-16 编码的单元值序列进行升序排列。比如 212 的 UTF-16 编码分别为 5049, 50,而 49 < 50,所以 12 排在 2 之前。

如果想按照实际的数字大小升排列需要传入一个比较函数:

// 升序
arr.sort((a, b) => a - b);
// 降序
arr.sort((a, b) => b - a);

第 19 题:求 x 的值使下列等式同时为 true

x * x === 0;
x + 1 === 1;
x - 1 === -1;
x / x === 1;
答案:Number.MIN_VALUE

解析:

Number.MIN_VALUE 是 JavaScript 能表示的最小的正数,也是最接近 0 的值,所以很多行为和 0 类似,例如前 3 条等式,但是它毕竟不是 0,所以可以作为除数,因此等式 4 也成立。与之相对的还有 Number.MAX_VALUE,是 JavaScript 中能表示的最大数。

第 20 题:console.log(9999999999999999)

答案:10000000000000000

解析:

看到答案有点懵,直觉告诉我们这肯定又和 JavaScript 中的某些最大数的限制有关。没错,JS 中有个 Number.MAX_SAFE_INTEGER,它的值为 2^53 - 1,即 9007199254740991。这个数的存在还是因为 JS 使用的 64 位双精度浮点型数,它能表示的区间仅仅为 -(2^53 - 1) ~ 2^53 - 1,超过这个区间的数就不“安全”了,不安全表现为无法准确的表示和比较这些数,比如 Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 结果为 trueNumber.isSafeInteger() 可以用来判断一个数是否 “安全”。

当我们需要使用更大的数时建议使用 BigInt

第 21 题:原型链的顶层是什么?

答案:null

解析:

一般认为原型链顶层是 Object.prototype,但其实 Object.prototype 还是有 __proto__ 的内部属性的,而 Object.prototype.__proto__ 等于 null。所以答案为 null 更为准确。

参考 Annotated ECMAScript 5.1 - Properties of the Object Prototype Object

第 22 题:如何阻止给一个对象设置属性

比如:

const obj = {};

// todo: 让 obj.p = 1 无效

obj.p = 1;

答案:
至少有四种方法:

  1. Object.freeze(obj)
  2. Object.seal(obj)
  3. Object.preventExtensions(obj)
  4. Object.defineProperty(obj, 'p', { writable: false })

解析:

  1. Object.freeze() 最为严格,它会完全禁止对象做任何修改,包括:增加新属性、修改已有属性、修改其原型
  2. Object.seal() 的规则宽松一点:允许修改 writable 的属性,但不允许新增和删除属性,且已有属性都会被标记为不可配置的(non-configurable)
  3. Object.preventExtensions() 更加宽松,可以阻止对象新增属性和修改其 __proto__(不能给 __proto__ 重新赋值)
  4. Object.defineProperty() 将属性 p 定义为不可写的,因此无法再给 p 设置新的值(writable 默认为 false,可以省略)

第 23 题:判断一个字符串是否为回文(palindrome,翻转过后和原来相等),忽略大小写

基础算法题,至少有2种方法:

解法1:将数字转成字符串,再转成数组,翻转后再比较:

function palindrome(str) {
  str = str.toLowerCase();
  return str.split("").reverse().join("") === str;
}

解法2:for循环,头尾比较

function palindrome(str) {
  for (var i = 0; i < str.length / 2; i++) {
    const left = str[i];
    const right = str[str.length - 1 - i];
    if (left.toLowerCase() !== right.toLowerCase()) return false;
  }

  return true;
}

这道题的升级版,是判断一个数字是否为回文,且不能将数字转成字符串。思路是通过取余的方法获取到每一位的数字,再构造一个反过来的数和原数进行比较:

function palindrome(num) {
  let copy = num;
  let currentDigit = 0;
  let reversedNum = 0;
  do {
    currentDigit = copy % 10;
    reversedNum = reversedNum * 10 + currentDigit;
    copy = parseInt(copy / 10);
  } while (copy !== 0);

  return num === reversedNum;
}

好了,先想到这些。如果本文对你有帮助,给个赞吧!

查看原文

赞 38 收藏 27 评论 0

子君 赞了文章 · 7月22日

玩转混合加密

数据加密,是一门历史悠久的技术,指通过加密算法和加密密钥将明文转变为密文,而解密则是通过解密算法和解密密钥将密文恢复为明文。它的核心是密码学。数据加密仍是计算机系统对信息进行保护的一种最可靠的办法。它利用密码技术对信息进行加密,实现信息隐蔽,从而起到保护信息的安全的作用。

本文阿宝哥将介绍如何对数据进行混合加密,即使用对称加密算法与非对称加密算法对数据进行加密,从而进一步保证数据的安全性。阅读完本文,你将了解以下内容:

  • 什么是对称加密、对称加密的过程、对称加密的优缺点及 AES 对称加密算法的使用;
  • 什么是非对称加密、非对称加密的过程、非对称加密的优缺点及 RSA 非对称加密算法的使用;
  • 什么是混合加密、混合加密的过程及如何实现混合加密。

在最后的 阿宝哥有话说 环节,阿宝哥还将简单介绍一下什么是消息摘要算法和什么是 MD5 算法及其用途与缺陷。好的,现在让我们步入正题。为了让刚接触混合加密的小伙伴更好地了解并掌握混合加密,阿宝哥将乘坐 “时光机” 带大家来到某个发版的夜晚...

那一晚我们团队的小伙伴正在等服务端数据升级,为了让大家 “忘记” 这个漫漫的升级过程,阿宝哥就立马组织了一场关于混合加密的技术分享会。在阿宝哥 “威逼利诱” 之下,团队的小伙伴们很快就到齐了,之后阿宝哥以以下对话拉开了分享会的序幕:

几分钟过后,小哥讲完了,基本关键点都有回答上来,但还遗漏了一些内容。为了让小伙伴们更好地理解对称加密,阿宝哥对小哥表述的内容进行了重新梳理,下面让我们来一起认识一下对称加密。

一、对称加密

1.1 什么是对称加密

对称密钥算法(英语:Symmetric-key algorithm)又称为对称加密、私钥加密、共享密钥加密,是密码学中的一类加密算法。这类算法在加密和解密时使用相同的密钥,或是使用两个可以简单地相互推算的密钥。

1.2 对称加密的优点

算法公开、计算量小、加密速度快、加密效率高,适合对大量数据进行加密的场景。 比如 HLS(HTTP Live Streaming)普通加密场景中,一般会使用 AES-128 对称加密算法对 TS 切片进行加密,以保证多媒体资源安全。

1.3 对称加密的过程

发送方使用密钥将明文数据加密成密文,然后发送出去,接收方收到密文后,使用同一个密钥将密文解密成明文读取。

1.4 对称加密的使用示例

常见的对称加密算法有 AES、ChaCha20、3DES、Salsa20、DES、Blowfish、IDEA、RC5、RC6、Camellia。这里我们以常见的 AES 算法为例,来介绍一下 AES(Advanced Encryption Standard)对称加密与解密的过程。

下面阿宝哥将使用 crypto-js 这个库来介绍 AES 算法的加密与解密,该库提供了 CryptoJS.AES.encrypt() 方法用于实现 AES 加密,而 AES 解密对应的方法是 CryptoJS.AES.decrypt()

基于上述两个方法阿宝哥进一步封装了 aesEncrypt()aesDecrypt() 这两个方法,它们分别用于 AES 加密与解密,其具体实现如下所示:

1.4.1 AES 加密方法
// AES加密
function aesEncrypt(content) {
  let text = CryptoJS.enc.Utf8.parse(JSON.stringify(content));
  let encrypted = CryptoJS.AES.encrypt(text, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return encrypted.toString();
}
1.4.2 AES 解密方法
// AES解密
function aesDecrypt(content) {
  let decrypt = CryptoJS.AES.decrypt(content, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return decrypt.toString(CryptoJS.enc.Utf8);
}
1.4.3 AES 加密与解密示例

在以上示例中,我们在页面上创建了 3 个 textarea,分别用于存放明文、加密后的密文和解密后的明文。当用户点击 加密 按钮时,会对用户输入的明文进行 AES 加密,完成加密后,会把密文显示在密文对应的 textarea 中,当用户点击 解密 按钮时,会对密文进行 AES 解密,完成解密后,会把解密后的明文显示在对应的 textarea 中。

以上示例对应的完整代码如下所示:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>AES 对称加密与解密示例</title>
    <style>
      .block {
        flex: 1;
      }
    </style>
  </head>
  <body>
    <h3>阿宝哥:AES 对称加密与解密示例(CBC 模式)</h3>
    <div style="display: flex;">
      <div class="block">
        <p>①明文加密 => <button onclick="encrypt()">加密</button></p>
        <textarea id="plaintext" rows="5" cols="15"></textarea>
      </div>
      <div class="block">
        <p>②密文解密 => <button onclick="decrypt()">解密</button></p>
        <textarea id="ciphertext" rows="5" cols="15"></textarea>
      </div>
      <div class="block">
        <p>③解密后的明文</p>
        <textarea id="decryptedCiphertext" rows="5" cols="15"></textarea>
      </div>
    </div>
    <!-- 引入 CDN Crypto.js AES加密 -->
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/core.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/enc-base64.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/cipher-core.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/aes.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/pad-pkcs7.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/enc-utf8.min.js"></script>
    <!-- 引入 CDN Crypto.js 结束 -->
    <script>
      const key = CryptoJS.enc.Utf8.parse("0123456789abcdef"); // 密钥
      const iv = CryptoJS.enc.Utf8.parse("abcdef0123456789"); // 初始向量
      const plaintextEle = document.querySelector("#plaintext");
      const ciphertextEle = document.querySelector("#ciphertext");
      const decryptedCiphertextEle = document.querySelector(
        "#decryptedCiphertext"
      );

      function encrypt() {
        let plaintext = plaintextEle.value;
        ciphertextEle.value = aesEncrypt(plaintext);
      }

      function decrypt() {
        let ciphertext = ciphertextEle.value;
        decryptedCiphertextEle.value = aesDecrypt(ciphertext).replace(/\"/g,'');
      }

      // AES加密
      function aesEncrypt(content) {
        let text = CryptoJS.enc.Utf8.parse(JSON.stringify(content));
        let encrypted = CryptoJS.AES.encrypt(text, key, {
          iv: iv,
          mode: CryptoJS.mode.CBC,
          padding: CryptoJS.pad.Pkcs7,
        });
        return encrypted.toString();
      }

      // AES解密
      function aesDecrypt(content) {
        let decrypt = CryptoJS.AES.decrypt(content, key, {
          iv: iv,
          mode: CryptoJS.mode.CBC,
          padding: CryptoJS.pad.Pkcs7,
        });
        return decrypt.toString(CryptoJS.enc.Utf8);
      }
    </script>
  </body>
</html>

在上面的示例中,我们通过 AES 对称加密算法,对 “我是阿宝哥” 明文进行加密,从而实现信息隐蔽。

那么使用对称加密算法就可以解决我们前面的问题么?答案是否定,这是因为对称加密存在一些的缺点。

1.5 对称加密的缺点

通过使用对称加密算法,我们已经把明文加密成密文。虽然这解决了数据的安全性,但同时也带来了另一个新的问题。因为对称加密算法,加密和解密时使用的是同一个密钥,所以对称加密的安全性就不仅仅取决于加密算法本身的强度,更取决于密钥是否被安全的传输或保管。

另外对于实际应用场景,为了避免单一的密钥被攻破,从而导致所有的加密数据被破解,对于不同的数据,我们一般会使用不同的密钥进行加密,这样虽然提高了安全性,但也增加了密钥管理的难度。

由于对称加密存在以上的问题,因此它并不是一种好的解决方案。为了找到更好的方案,阿宝哥开始了另一轮新的对话。

二、非对称加密

2.1 什么是非对称加密

非对称加密算法需要两个密钥:公开密钥(publickey:简称公钥)和私有密钥(privatekey:简称私钥)。公钥与私钥是一对,如果用公钥对数据进行加密,只有用对应的私钥才能解密。 因为加密和解密使用的是两个不同的密钥,所以这种算法叫作非对称加密算法。

2.2 非对称加密的优点

安全性更高,公钥是公开的,私钥是自己保存的,不需要将私钥提供给别人。

2.3 非对称加密的过程

2.4 非对称加密的使用示例

常见的非对称加密算法有 RSA、Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)。这里我们以常见的 RSA 算法为例,来介绍一下 RSA 非对称加密与解密的过程。

RSA 是 1977 年由罗纳德·李维斯特(Ron Rivest)、阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的。当时他们三人都在麻省理工学院工作。RSA 就是他们三人姓氏开头字母拼在一起组成的。

下面阿宝哥将使用 jsencrypt 这个库来介绍 RSA 算法的加密与解密,该库提供了 encrypt() 方法用于实现 RSA 加密,而 RSA 解密对应的方法是 decrypt()

2.4.1 创建公私钥

使用 jsencrypt 这个库之前,我们需要先生成公钥和私钥。接下来阿宝哥以 macOS 系统为例,来介绍一下如何生成公私钥。

首先我们先来生成私钥,在命令行输入以下命令:

$ openssl genrsa -out rsa_1024_priv.pem 1024

在该命令成功运行之后,在当前目录下会生成一个 rsa_1024_priv.pem 文件,该文件的内容如下:

-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu
/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+glOUtBXFcUnutWBbnf9qIDkKP
...
bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ==
-----END RSA PRIVATE KEY-----

然后我们来生成公钥,同样在命令行输入以下命令:

$ openssl rsa -pubout -in rsa_1024_priv.pem -out rsa_1024_pub.pem

在该命令成功运行之后,在当前目录下会生成一个 rsa_1024_pub.pem 文件,该文件的内容如下:

-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR
JpTJnwjiwxkuJZe1HTIIuLbu/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+g
lOUtBXFcUnutWBbnf9qIDkKP2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xs
cyhRIeiXxs13vlSHVwIDAQAB
-----END PUBLIC KEY-----
2.4.2 创建 RSA 加密器和解密器

创建完公私钥之后,我们就可以进一步创建 RSA 加密器和解密器,具体代码如下:

const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR
...
cyhRIeiXxs13vlSHVwIDAQAB
-----END PUBLIC KEY-----`;

const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu
...
bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ==
-----END RSA PRIVATE KEY-----`;

const encryptor = new JSEncrypt(); // RSA加密器
encryptor.setPublicKey(PUBLIC_KEY);

const decryptor = new JSEncrypt(); // RSA解密器
decryptor.setPrivateKey(PRIVATE_KEY);
2.4.3 RSA 加密与解密示例

在以上示例中,我们在页面上创建了 3 个 textarea,分别用于存放明文、加密后的密文和解密后的明文。当用户点击 加密 按钮时,会对用户输入的明文进行 RSA 加密,完成加密后,会把密文显示在密文对应的 textarea 中,当用户点击 解密 按钮时,会对密文进行 RSA 解密,完成解密后,会把解密后的明文显示在对应的 textarea 中。

以上示例对应的完整代码如下所示:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>RSA 对称加密与解密示例</title>
    <style>
      .block {
        flex: 1;
      }
    </style>
  </head>
  <body>
    <h3>阿宝哥:RSA 对称加密与解密示例</h3>
    <div style="display: flex;">
      <div class="block">
        <p>①明文加密 => <button onclick="encrypt()">加密</button></p>
        <textarea id="plaintext" rows="5" cols="15"></textarea>
      </div>
      <div class="block">
        <p>②密文解密 => <button onclick="decrypt()">解密</button></p>
        <textarea id="ciphertext" rows="5" cols="15"></textarea>
      </div>
      <div class="block">
        <p>③解密后的明文</p>
        <textarea id="decryptedCiphertext" rows="5" cols="15"></textarea>
      </div>
    </div>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/jsencrypt/2.3.1/jsencrypt.min.js"></script>
    <script>
      const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR
JpTJnwjiwxkuJZe1HTIIuLbu/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+g
lOUtBXFcUnutWBbnf9qIDkKP2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xs
cyhRIeiXxs13vlSHVwIDAQAB
-----END PUBLIC KEY-----`;
      const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu
/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+glOUtBXFcUnutWBbnf9qIDkKP
2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xscyhRIeiXxs13vlSHVwIDAQAB
AoGAKOarYKpuc5IYXdArEuHmnFaa2pm7XK8LVTuXVrNuuoPkpfw61Fs4ke3T0yKg
x6G3gq7Xm1tTEROAgMtaxqwo1D5n1H0nkyDFggLB0K9Ws0frp7HENtSQwdNSry1A
iD8TLxkhoWo7BS0VViLT1gKOfnw4YeMJP+CcOQ+DQjCsUMECQQD0Nc0vKBTlK6GT
28gcIMVoQy2KicjiH222A9/TLCNAQ9DEeZDYEptuTfrlbggfWdgQ3nc6CyvGf6c5
6uBPi/+5AkEA86oqqZPi7ekkUVHx0VSkp0mTlD1tAPhDE8cLX8vyImGExS+tTznz
ROyzm3T1M1PisdQIU8Wd5rqvHP6dB0enjwJAauhKpMQ1MYYCPApQ9g9anCQcgbOD
34nGq5HSoE2IOQ/3Cqv1PsIWjRlSJrIemCrqrafWJfDR/xnPCUnLXMd68QJAPNwG
1d4zMvslcA5ImOFMUuBEtST2geSAVINFqwK0krPKxrmWzxAJW/DHF5AJ4m0UVRhB
kDLusn90V4iczgGurwJAZUz6w01OeoLhsOuWNvkbTq+IV0NQ5GAEGA721Ck5zp86
bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ==
-----END RSA PRIVATE KEY-----`;

      const encryptor = new JSEncrypt(); // RSA加密器
      encryptor.setPublicKey(PUBLIC_KEY);

      const decryptor = new JSEncrypt(); // RSA解密器
      decryptor.setPrivateKey(PRIVATE_KEY);

      const plaintextEle = document.querySelector("#plaintext");
      const ciphertextEle = document.querySelector("#ciphertext");
      const decryptedCiphertextEle = document.querySelector(
        "#decryptedCiphertext"
      );

      function encrypt() {
        let plaintext = plaintextEle.value;
        ciphertextEle.value = encryptor.encrypt(plaintext);
      }

      function decrypt() {
        let ciphertext = ciphertextEle.value;
        decryptedCiphertextEle.value = decryptor.decrypt(ciphertext);
      }
    </script>
  </body>
</html>

在上面的示例中,我们通过 RSA 非对称加密算法,对 “我是阿宝哥” 明文进行加密,从而实现信息隐蔽。

那么使用非对称加密算法就可以解决我们前面的问题么?答案是否定,这是因为非对称加密也存在一些的缺点。

2.5 非对称加密的缺点

非对称加密算法加密和解密花费时间长、速度慢,只适合对少量数据进行加密。因为我们要提供的是通用的解决方案,即要同时考虑到少量数据和大量数据的情况,所以非对称加密也不是一个好的解决方案。为了解决问题,阿宝哥又重新开启了一轮新的对话。

三、混合加密

3.1 什么是混合加密

混合加密是结合 对称加密非对称加密 各自优点的一种加密方式。其具体的实现思路是先使用 对称加密算法 对数据进行加密,然后使用非对称加密算法对 对称加密的密钥 进行非对称加密,之后再把加密后的密钥和加密后的数据发送给接收方。

为了让小伙伴们更加直观理解上述的过程,阿宝哥花了点心思画了一张图,用来进一步说明混合加密的过程,下面我们就一起来看图吧。

3.2 混合加密的过程

3.3 混合加密的实现

了解完 “混合加密数据传输流程”,阿宝哥跟小伙伴一起来实现一下上述的混合加密流程。这里我们会基于前面介绍过的对称加密和非对称加密的示例进行开发,即以下示例会直接利用前面非对称加密示例中用到的公私钥。

3.3.1 创建生成随机 AES 密钥的函数
function getRandomAESKey() {
  return (
    Math.random().toString(36).substring(2, 10) +
    Math.random().toString(36).substring(2, 10)
  );
}
3.3.2 创建 AES 加密和解密函数
// AES加密
function aesEncrypt(key, iv, content) {
  let text = CryptoJS.enc.Utf8.parse(JSON.stringify(content));
  let encrypted = CryptoJS.AES.encrypt(text, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return encrypted.toString();
}

// AES解密
function aesDecrypt(key, iv, content) {
  let decrypt = CryptoJS.AES.decrypt(content, key, {
    iv: iv,
    mode: CryptoJS.mode.CBC,
    padding: CryptoJS.pad.Pkcs7,
  });
  return decrypt.toString(CryptoJS.enc.Utf8);
}
3.3.3 创建 RSA 加密器和解密器
const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR
...
cyhRIeiXxs13vlSHVwIDAQAB
-----END PUBLIC KEY-----`;

const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu
...
bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ==
-----END RSA PRIVATE KEY-----`;

const rsaEncryptor = new JSEncrypt(); // RSA加密器
rsaEncryptor.setPublicKey(PUBLIC_KEY);

const rsaDecryptor = new JSEncrypt(); // RSA解密器
rsaDecryptor.setPrivateKey(PRIVATE_KEY);
3.3.4 创建混合加密加密和解密函数
function hybirdEncrypt(data) {
  const iv = getRandomAESKey();
  const key = getRandomAESKey();
  const encryptedData = aesEncrypt(key, iv, data);
  const encryptedIv = rsaEncryptor.encrypt(iv);
  const encryptedKey = rsaEncryptor.encrypt(key);
  return {
    iv: encryptedIv,
    key: encryptedKey,
    data: encryptedData,
   };
}

function hybirdDecrypt(encryptedResult) {
  const iv = rsaDecryptor.decrypt(encryptedResult.iv);
  const key = rsaDecryptor.decrypt(encryptedResult.key);
  const data = encryptedResult.data;
  return aesDecrypt(key, iv, data);
}
3.3.5 混合加密与解密示例

以上步骤完成之后,我们基本已经完成了混合加密的功能,在看完整代码之前,我们先来看一下实际的运行效果:

备注:密文解密下方对应的 textarea 文本框中,除了加密的数据之外,还会包含使用 RSA 加密过的 AES CBC 模式中的 iv 和 key。

在以上示例中,我们在页面上创建了 3 个 textarea,分别用于存放明文、加密后的数据和解密后的明文。当用户点击 加密 按钮时,会对用户输入的明文进行混合加密,完成加密后,会把加密的数据显示在密文对应的 textarea 中,当用户点击 解密 按钮时,会对密文进行 混合解密,即先使用 RSA 私钥解密 AES 的 key 和 iv,然后再使用它们对 AES 加密过的密文进行 AES 解密,完成解密后,会把解密后的明文显示在对应的 textarea 中。

以上示例对应的完整代码如下所示:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>混合加密与解密示例</title>
    <style>
      .block {
        flex: 1;
      }
    </style>
  </head>
  <body>
    <h3>阿宝哥:混合加密与解密示例</h3>
    <div style="display: flex;">
      <div class="block">
        <p>①明文加密 => <button onclick="encrypt()">加密</button></p>
        <textarea id="plaintext" rows="5" cols="15"></textarea>
      </div>
      <div class="block">
        <p>②密文解密 => <button onclick="decrypt()">解密</button></p>
        <textarea id="ciphertext" rows="5" cols="15"></textarea>
      </div>
      <div class="block">
        <p>③解密后的明文</p>
        <textarea id="decryptedCiphertext" rows="5" cols="15"></textarea>
      </div>
    </div>
    <!-- 引入 CDN Crypto.js AES加密 -->
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/core.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/enc-base64.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/md5.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/evpkdf.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/cipher-core.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/aes.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/pad-pkcs7.min.js"></script>
    <script data-original="https://cdn.bootcdn.net/ajax/libs/crypto-js/4.0.0/enc-utf8.min.js"></script>
    <!-- 引入 CDN Crypto.js 结束 -->
    <script data-original="https://cdn.bootcdn.net/ajax/libs/jsencrypt/2.3.1/jsencrypt.min.js"></script>
    <script>
      const PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDocWYwnJ4DYur0BjxFjJkLv4QR
JpTJnwjiwxkuJZe1HTIIuLbu/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+g
lOUtBXFcUnutWBbnf9qIDkKP2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xs
cyhRIeiXxs13vlSHVwIDAQAB
-----END PUBLIC KEY-----`;
      const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDocWYwnJ4DYur0BjxFjJkLv4QRJpTJnwjiwxkuJZe1HTIIuLbu
/yHyHLhc2MAHKL0Ob+8tcKXKsL1oxs467+q0jA+glOUtBXFcUnutWBbnf9qIDkKP
2uoDdZ//LUeW7jibVrVJbXU2hxB8bQpBkltZf/xscyhRIeiXxs13vlSHVwIDAQAB
AoGAKOarYKpuc5IYXdArEuHmnFaa2pm7XK8LVTuXVrNuuoPkpfw61Fs4ke3T0yKg
x6G3gq7Xm1tTEROAgMtaxqwo1D5n1H0nkyDFggLB0K9Ws0frp7HENtSQwdNSry1A
iD8TLxkhoWo7BS0VViLT1gKOfnw4YeMJP+CcOQ+DQjCsUMECQQD0Nc0vKBTlK6GT
28gcIMVoQy2KicjiH222A9/TLCNAQ9DEeZDYEptuTfrlbggfWdgQ3nc6CyvGf6c5
6uBPi/+5AkEA86oqqZPi7ekkUVHx0VSkp0mTlD1tAPhDE8cLX8vyImGExS+tTznz
ROyzm3T1M1PisdQIU8Wd5rqvHP6dB0enjwJAauhKpMQ1MYYCPApQ9g9anCQcgbOD
34nGq5HSoE2IOQ/3Cqv1PsIWjRlSJrIemCrqrafWJfDR/xnPCUnLXMd68QJAPNwG
1d4zMvslcA5ImOFMUuBEtST2geSAVINFqwK0krPKxrmWzxAJW/DHF5AJ4m0UVRhB
kDLusn90V4iczgGurwJAZUz6w01OeoLhsOuWNvkbTq+IV0NQ5GAEGA721Ck5zp86
bKkRJNJ2PpfWA45Vdq6u+izrn9e2TabKjWIfTfT/ZQ==
-----END RSA PRIVATE KEY-----`;

      const rsaEncryptor = new JSEncrypt(); // RSA加密器
      rsaEncryptor.setPublicKey(PUBLIC_KEY);

      const rsaDecryptor = new JSEncrypt(); // RSA解密器
      rsaDecryptor.setPrivateKey(PRIVATE_KEY);

      const plaintextEle = document.querySelector("#plaintext");
      const ciphertextEle = document.querySelector("#ciphertext");
      const decryptedCiphertextEle = document.querySelector(
        "#decryptedCiphertext"
      );

      function getRandomAESKey() {
        return (
          Math.random().toString(36).substring(2, 10) +
          Math.random().toString(36).substring(2, 10)
        );
      }

      // AES加密
      function aesEncrypt(key, iv, content) {
        let text = CryptoJS.enc.Utf8.parse(JSON.stringify(content));
        let encrypted = CryptoJS.AES.encrypt(text, key, {
          iv: iv,
          mode: CryptoJS.mode.CBC,
          padding: CryptoJS.pad.Pkcs7,
        });
        return encrypted.toString();
      }

      // AES解密
      function aesDecrypt(key, iv, content) {
        let decrypt = CryptoJS.AES.decrypt(content, key, {
          iv: iv,
          mode: CryptoJS.mode.CBC,
          padding: CryptoJS.pad.Pkcs7,
        });
        return decrypt.toString(CryptoJS.enc.Utf8);
      }

      function hybirdEncrypt(data) {
        const iv = getRandomAESKey();
        const key = getRandomAESKey();
        const encryptedData = aesEncrypt(key, iv, data);
        const encryptedIv = rsaEncryptor.encrypt(iv);
        const encryptedKey = rsaEncryptor.encrypt(key);
        return {
          iv: encryptedIv,
          key: encryptedKey,
          data: encryptedData,
        };
      }

      function hybirdDecrypt(encryptedResult) {
        const iv = rsaDecryptor.decrypt(encryptedResult.iv);
        const key = rsaDecryptor.decrypt(encryptedResult.key);
        const data = encryptedResult.data;
        return aesDecrypt(key, iv, data);
      }

      function encrypt() {
        let plaintext = plaintextEle.value;
        const encryptedResult = hybirdEncrypt(plaintext);
        ciphertextEle.value = JSON.stringify(encryptedResult);
      }

      function decrypt() {
        let ciphertext = ciphertextEle.value;
        const encryptedResult = JSON.parse(ciphertext);
        decryptedCiphertextEle.value = hybirdDecrypt(encryptedResult).replace(/\"/g,'');
      }
    </script>
  </body>
</html>

3.4 混合加密方案分析

通过这个示例,相信大家对混合加密已经有了一定的了解。但在实际 Web 项目中,我们一般不会在客户端进行数据解密,而是会把数据提交到服务端,然后由服务端进行数据解密和数据处理。

HTTP 协议对大多数 Web 开发者来说,都不会陌生。HTTP 协议是基于请求和响应,具体如下图所示:

在对数据安全要求较高的场景或传输敏感数据时,我们就可以考虑利用前面的混合加密方案对提交到服务端的数据进行混合加密,当服务端接收到对应的加密数据时,再使用对应的解密算法对加密的数据进行解密,从而进一步进行数据处理。

但是如果服务端也要返回敏感数据时,应该怎么办呢?这里阿宝哥给大家介绍一种方案,该方案只需使用一对公私钥。当然该方案仅供大家参考,如果你有好的方案,欢迎给阿宝哥留言或跟阿宝哥交流哟。

下面我们来看一下该方案的具体操作流程:

① 生成一个唯一的 reqId(请求 ID),用于标识当前请求;

② 分别生成一个随机的 AES Key 和 AES IV(采用 AES CBC 模式);

③ 采用 RSA 非对称加密算法,分别对 AES Key 和 AES IV 进行 RSA 非对称加密;

④ 采用随机生成的 AES Key 和 AES IV 对敏感数据进行 AES 对称加密;

⑤ 把 reqId 作为 key,AES Key 和 AES IV 组成的对象作为 value 保存到 Map 或 {} 对象中;

⑥ 把 reqId、加密后的 AES Key、AES IV 和加密后的数据保存到对象中提交到服务端;

⑦ 当服务端接收到数据后,对接收的数据进行解密,然后使用客户端传过来的解密后的 AES Key 和 AES IV 对响应数据进行 AES 对称加密;

⑧ 服务端在完成数据加密后,把 reqId 和加密后的数据包装成响应对象,返回给客户端;

⑨ 当客户端成功接收服务端的响应后,先获取 reqId,进而从保存 AES Key 和 IV 的 Map 获取该 reqId 对应的 AES 加密信息;

⑩ 客户端使用当前 reqId 对应的加密信息,对服务端返回的数据进行解密,当完成解密之后,从 Map 或 {} 对象中删除已有记录。

现在我们来对上述流程做个简单分析,首先 AES 加密信息都是随机生成的且根据每个请求独立地保存到内存中,把 AES 加密信息中的 Key 和 IV 提交到服务端的时候都会使用 RSA 非对称加密算法进行加密。

在服务端返回数据的时候,会使用当前请求对应的 AES 加密信息对返回的结果进行加密,同时返回当前请求对应的 reqId(请求 ID)。即服务端不需要再生成新的 AES 加密信息,来对响应数据进行加密,这样就不需要在响应对象中传递 AES 加密信息。

该方案看似挺完美的,由于我们加密的信息还是存在内存中,如果使用开发者工具对 Web 应用进行调试时,那么还是可以看到每个请求对应的加密信息。那么这个问题该如何解决呢?能不能防止使用开发者工具对我们的 Web 应用进行调试,答案是有的。

不过这里阿宝哥就不继续展开了,后面可能会单独写一篇文章来介绍如何防止使用开发者工具调试 Web 应用,感兴趣的小伙伴可以给我留言哟。

四、阿宝哥有话说

4.1 什么是消息摘要算法

其实在日常工作中,除了对称加密和非对称加密算法之外。还有一种用得比较广的消息摘要算法。消息摘要算法是密码学算法中非常重要的一个分支,它通过对所有数据提取指纹信息以实现数据签名、数据完整性校验等功能,由于其不可逆性,有时候会被用做敏感信息的加密。消息摘要算法也被称为哈希(Hash)算法或散列算法。

任何消息经过散列函数处理后,都会获得唯一的散列值,这一过程称为 “消息摘要”,其散列值称为 “数字指纹”,其算法自然就是 “消息摘要算法”了。 换句话说,如果其数字指纹一致,就说明其消息是一致的。

(图片来源 —— https://zh.wikipedia.org/wiki...

消息摘要算法的主要特征是加密过程不需要密钥,并且经过加密的数据无法被解密,目前可以解密逆向的只有 CRC32 算法,只有输入相同的明文数据经过相同的消息摘要算法才能得到相同的密文。 消息摘要算法不存在密钥的管理与分发问题,适合于分布式网络上使用。消息摘要算法主要应用在 “数字签名” 领域,作为对明文的摘要算法。著名的摘要算法有 RSA 公司的 MD5 算法和 SHA-1 算法及其大量的变体。

消息摘要算法拥有以下特点:

  • 无论输入的消息有多长,计算出来的消息摘要的长度总是固定的。 例如应用 MD5 算法摘要的消息有 128 个比特位,用 SHA-1 算法摘要的消息最终有 160 个比特位的输出,SHA-1的变体可以产生 192 个比特位和 256 个比特位的消息摘要。一般认为,摘要的最终输出越长,该摘要算法就越安全。
  • 消息摘要看起来是 “随机的”。 这些比特看上去是胡乱的杂凑在一起的,可以用大量的输入来检验其输出是否相同,一般,不同的输入会有不同的输出,而且输出的摘要消息可以通过随机性检验。一般地,只要输入的消息不同,对其进行摘要以后产生的摘要消息也必不相同;但相同的输入必会产生相同的输出。
  • 消息摘要函数是单向函数,即只能进行正向的信息摘要,而无法从摘要中恢复出任何的消息,甚至根本就找不到任何与原信息相关的信息。
  • 好的摘要算法,没有人能从中找到 “碰撞” 或者说极度难找到,虽然 “碰撞” 是肯定存在的(碰撞即不同的内容产生相同的摘要)。

4.2 什么是 MD5 算法

MD5(Message Digest Algorithm 5,消息摘要算法版本5),它由 MD2、MD3、MD4 发展而来,由 Ron Rivest(RSA 公司)在 1992 年提出,目前被广泛应用于数据完整性校验、数据(消息)摘要、数据签名等。MD2、MD4、MD5 都产生 16 字节(128 位)的校验值,一般用 32 位十六进制数表示。MD2 的算法较慢但相对安全,MD4 速度很快,但安全性下降,MD5 比 MD4 更安全、速度更快。

随着计算机技术的发展和计算水平的不断提高,MD5 算法暴露出来的漏洞也越来越多。1996 年后被证实存在弱点,可以被加以破解,对于需要高度安全性的数据,专家一般建议改用其他算法,如 SHA-2。2004 年,证实 MD5 算法无法防止碰撞(collision),因此不适用于安全性认证,如 SSL 公开密钥认证或是数字签名等用途。

4.2.1 MD5 特点
  • 稳定、运算速度快。
  • 压缩性:输入任意长度的数据,输出长度固定(128 比特位)。
  • 运算不可逆:已知运算结果的情况下,无法通过通过逆运算得到原始字符串。
  • 高度离散:输入的微小变化,可导致运算结果差异巨大。
4.2.2 MD5 散列

128 位的 MD5 散列在大多数情况下会被表示为 32 位十六进制数字。以下是一个 43 位长的仅 ASCII 字母列的MD5 散列:

MD5("The quick brown fox jumps over the lazy dog")
= 9e107d9d372bb6826bd81d3542a419d6

即使在原文中作一个小变化(比如把 dog 改为 cog,只改变一个字符)其散列也会发生巨大的变化:

MD5("The quick brown fox jumps over the lazy cog")
= 1055d3e698d289f2af8663725127bd4b

接着我们再来举几个 MD5 散列的例子:

         MD5("") -> d41d8cd98f00b204e9800998ecf8427e 
MD5("semlinker") -> 688881f1c8aa6ffd3fcec471e0391e4d
   MD5("kakuqo") -> e18c3c4dd05aef020946e6afbf9e04ef
4.2.3 MD5 算法的用途

文件分发防篡改

在互联网上分发软件安装包时,出于安全性考虑,为了防止软件被篡改,比如在软件安装程序中添加木马程序。软件开发者通常会使用消息摘要算法,比如 MD5 算法产生一个与文件匹配的数字指纹,这样接收者在接收到文件后,就可以利用一些现成的工具来检查文件完整性。

消息传输防篡改

假设在网络上你需要发送电子文档给你的朋友,在文件发送前,先对文档的内容进行 MD5 运算,得出该电子文档的 “数字指纹”,并把该 “数字指纹” 随电子文档一同发送给对方。当对方接收到电子文档之后,也使用 MD5 算法对文档的内容进行哈希运算,在运算完成后也会得到一个对应 “数字指纹”,当该指纹与你所发送文档的 “数字指纹” 一致时,表示文档在传输过程中未被篡改。

4.2.4 MD5 算法的缺陷

哈希碰撞是指不同的输入却产生了相同的输出,好的哈希算法,应该没有人能从中找到 “碰撞” 或者说极度难找到,虽然 “碰撞” 是肯定存在的。

2005 年山东大学的王小云教授发布算法可以轻易构造 MD5 碰撞实例,此后 2007 年,有国外学者在王小云教授算法的基础上,提出了更进一步的 MD5 前缀碰撞构造算法 “chosen prefix collision”,此后还有专家陆续提供了MD5 碰撞构造的开源的库。

2009 年,中国科学院的谢涛和冯登国仅用了 220.96 的碰撞算法复杂度,破解了 MD5 的碰撞抵抗,该攻击在普通计算机上运行只需要数秒钟。

MD5 碰撞很容易构造,基于 MD5 来验证数据完整性已不可靠,考虑到近期谷歌已成功构造了 SHA-1(英语:Secure Hash Algorithm 1,中文名:安全散列算法1)的碰撞实例,对于数据完整性,应使用 SHA256 或更强的算法代替。

其实 MD5 的相关知识还有挺多,比如 MD5 密文反向查询、密码加盐和实现内容资源防盗链等。这里阿宝哥就不继续展开了,感兴趣的小伙伴可以阅读阿宝哥之前写的 ”一文读懂 MD5 算法“ 这篇文章。

五、参考资源

查看原文

赞 16 收藏 6 评论 0

子君 发布了文章 · 7月22日

让Vue项目更丝滑的几个小技巧

阵阵键盘声,隐隐测试言。产品不稳定,今夜无人还。

在开发Vue的过程中,我们经常会遇到一些这样那样的问题,然后要卡好半天,等问题解决了才发现原来一些细节知识点还是没有掌握好。今天小编就整理了几个在项目中会用到的一些实战技巧点,希望可以帮助到正在努力赚钱的你。江湖规矩,先赞后看,艳遇不断。

本文首发于公众号【前端有的玩】,关注我,我们一起玩前端,每天都有不一样的干货知识点等着你哦

数据不响应,可能是用法有问题

前几天有朋友给我发了一段代码,然后说Vuebug,他明明写的没问题,为啥数据就不响应呢,一定是Vuebug?我感觉他比尤雨溪要牛逼,高攀不起,就没有理他了。但是确实有时候我们在开发时候会遇到数据不响应的情况,那怎么办呢?比如下面这段代码:

<template>
  <div>
    <div>
      <span>用户名: {{ userInfo.name }}</span>
      <span>用户性别: {{ userInfo.sex }}</span>
      <span v-if="userInfo.officialAccount">
        公众号: {{ userInfo.officialAccount }}
      </span>
    </div>
    <button @click="handleAddOfficialAccount">添加公众号</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      userInfo: {
        name: '子君',
        sex: '男'
      }
    }
  },
  methods: {
    // 在这里添加用户的公众号
    handleAddOfficialAccount() {
      this.userInfo.officialAccount = '前端有的玩'
    }
  }
}
</script>

在上面的代码中,我们希望给用户信息里面添加公众号属性,但是通过this.userInfo.officialAccount = '前端有的玩' 添加之后,并没有生效,这是为什么呢?

这是因为在Vue内部,数据响应是通过使用Object.definePrototype监听对象的每一个键的getter,setter方法来实现的,但通过这种方法只能监听到已有属性,新增的属性是无法监听到的,但我就是想监听,小编你说咋办吧。下面小编提供了四种方式,如果有更多方式,欢迎下方评论区告诉我。

1. 将本来要新增的属性提前在data中定义好

比如上面的公众号,我可以提前在userInfo里面定义好,这样就不是新增属性了,就像下面这样

data() {
    return {
      userInfo: {
        name: '子君',
        sex: '男',
        // 我先提前定义好
        officialAccount: ''
      }
    }
  }

2. 直接替换掉userInfo

虽然无法给userInfo里面添加新的属性,但是因为userInfo已经定义好了,所以我直接修改userInfo的值不就可以了么,所以也可以像下面这样写

this.userInfo = {
  // 将原来的userInfo 通过扩展运算法复制到新的对象里面
  ...this.userInfo,
  // 添加新属性
  officialAccount: '前端有的玩'
}

3. 使用Vue.set

其实上面两种方法都有点取巧的嫌疑,其实对于新增属性,Vue官方专门提供了一个新的方法Vue.set用来解决新增属性无法触发数据响应。

Vue.set 方法定义

/**
* target 要修改的对象
* prpertyName 要添加的属性名称
* value 要添加的属性值
*/
Vue.set( target, propertyName, value )

上面的代码使用Vue.set可以修改为

import Vue from 'vue'

// 在这里添加用户的公众号
handleAddOfficialAccount() {
  Vue.set(this.userInfo,'officialAccount', '前端有的玩')
}

但是每次要用到set方法的时候,还要把Vue引入进来,好麻烦,所以为了简便起见,Vue又将set方法挂载到了Vue的原型链上了,即Vue.prototype.$set = Vue.set,所以在Vue组件内部可以直接使用this.$set代替Vue.set

this.$set(this.userInfo,'officialAccount', '前端有的玩')

小编发现有许多同学不知道什么时候应该用Vue.set,其实只有当你要赋值的属性还没有定义的时候需要使用Vue,set,其他时候一般不会需要使用。

4. 使用$forceUpdate

我觉得$forceUpdate的存在,让许多前端开发者不会再去注意数据双向绑定的原理,因为不论什么时候,反正我修改了data之后,调用一下$forceUpdate就会让Vue组件重新渲染,bug是不会存在的。但是实际上这个方法并不建议使用,因为它会引起许多不必要的性能消耗。

针对数组的特定方式

其实不仅仅是对象,数组也存在数据修改之后不响应的情况,比如下面这段代码

<template>
  <div>
    <ul>
      <li v-for="item in list" :key="item">
        {{ item }}
      </li>
    </ul>
    <button @click="handleChangeName">修改名称</button>
  </div>
</template>
<script>
export default {
  data() {
    return {
      list: ['张三', '李四']
    }
  },
  methods: {
    // 修改用户名称
    handleChangeName() {
      this.list[0] = '王五'
    }
  }
}
</script>

上面的代码希望将张三的名字修改为王五,实际上这个修改并不能生效,这是因为Vue不能检测到以下变动的数组:

  1. 当你利用索引直接设置一个项时,例如:this.list[index] = newValue
  2. 修改数组的length属性,例如:this.list.length = 0

所以在上例中通过this.list[0] = '王五' 是无法触发数据响应的,那应该怎么办呢?像上面提到的Vue.set$forceUpdate都可以解决这个问题,比如Vue.set可以这样写

Vue.set(this.list,0,'王五')
复制代码

除了那些方法之外,Vue还针对数组提供了变异方法

在操作数组的时候,我们一般会用到数据提供的许多方法,比如push,pop,splice等等,在Vue中调用数组上面提供的这些方法修改数组的值是可以触发数据响应的,比如上面的代码改为以下代码即可触发数据响应

this.list.splice(0,1,'王五')

实际上,如果Vue仅仅依赖gettersetter,是无法做到在数组调用push,pop等方法时候触发数据响应的,因此Vue实际上是通过劫持这些方法,对这些方法进行包装变异来实现的。

Vue对数组的以下方法进行的包装变异:

  • push
  • pop
  • shift
  • unshift
  • splice
  • sort
  • reverse

所以在操作数组的时候,调用上面这些方法是可以保证数据可以正常响应,下面是Vue源码中包装数组方法的代码:

var original = arrayProto[method];
  def(arrayMethods, method, function mutator () {
    // 将 arguments 转换为数组
    var args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];
    var result = original.apply(this, args);
    // 这儿的用法同dependArray(value),就是为了取得dep
    var ob = this.__ob__;
    var inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break
      case 'splice':
        inserted = args.slice(2);
        break
    }
    // 如果有新的数据插入,则插入的数据也要进行一个响应式
    if (inserted) { ob.observeArray(inserted); }
   // 通知依赖进行更新
    ob.dep.notify();
    return result
  });

文本格式化,filter更简单

使用filter 简化逻辑

我想把时间戳显示成yyyy-MM-DD HH:mm:ss的格式怎么办?是需要在代码中先将日期格式化之后,再渲染到模板吗?就像下面这样

<template>
  <div>
    {{ dateStr }}
    <ul>
      <li v-for="(item, index) in getList" :key="index">
        {{ item.date }}
      </li>
    </ul>
  </div>
</template>
<script>
import { format } from '@/utils/date'
export default {
  data() {
    return {
      date: Date.now(),
      list: [
        {
          date: Date.now()
        }
      ]
    }
  },
  computed: {
    dateStr() {
      return format(this.date, 'yyyy-MM-DD HH:mm:ss')
    },
    getList() {
      return this.list.map(item => {
        return {
          ...item,
          date: format(item.date, 'yyyy-MM-DD HH:mm:ss')
        }
      })
    }
  }
}
</script>

像上面的写法,针对每一个日期字段都需要调用format,然后通过计算属性进行转换?这时候可以考虑使用Vue提供的filter去简化

<template>
  <div>
    <!--使用过滤器-->
    {{ dateStr | formatDate }}
    <ul>
      <li v-for="(item, index) in list" :key="index">
        <!--在v-for中使用过滤器-->
        {{ item.date | formatDate }}
      </li>
    </ul>
  </div>
</template>
<script>
import { format } from '@/utils/date'
export default {
  filters: {
    formatDate(value) {
      return format(value, 'yyyy-MM-DD HH:mm:ss')
    }
  },
  data() {
    return {
      date: Date.now(),
      list: [
        {
          date: Date.now()
        }
      ]
    }
  }
}
</script>

通过上面的修改是不是就简单多了

注册全局filter

有些过滤器使用的很频繁,比如上面提到的日期过滤器,在很多地方都要使用,这时候如果在每一个要用到的组件里面都去定义一遍,就显得有些多余了,这时候就可以考虑Vue.filter注册全局过滤器

对于全局过滤器,一般建议在项目里面添加filters目录,然后在filters目录里面添加

// filters\index.js

import Vue from 'vue'
import { format } from '@/utils/date'

Vue.filter('formatDate', value => {
  return format(value, 'yyyy-MM-DD HH:mm:ss')
})

然后将filters里面的文件引入到main.js里面,这时候就可以在组件里面直接用了,比如将前面的代码可以修改为

<template>
  <div>
    <!--使用过滤器-->
    {{ dateStr | formatDate }}
    <ul>
      <li v-for="(item, index) in list" :key="index">
        <!--在v-for中使用过滤器-->
        {{ item.date | formatDate }}
      </li>
    </ul>
  </div>
</template>
<script>
export default {
  data() {
    return {
      date: Date.now(),
      list: [
        {
          date: Date.now()
        }
      ]
    }
  }
}
</script>

是不是更简单了

开发了插件库,来安装一下

在使用一些UI框架的时候,经常需要使用Vue.use来安装, 比如使用element-ui时候,经常会这样写:

import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI,{size: 'small'});

使用了Vue.use之后,element-ui就可以直接在组件里面使用了,好神奇哦(呸,娘炮)。接下来我们实现一个简化版的element来看如何去安装。

了解Vue.use的用法

Vue.use是一个全局的方法,它需要在你调用 new Vue() 启动应用之前完成,Vue.use的参数如下

/**
* plugin: 要安装的插件 如 ElementUI
* options: 插件的配置信息 如 {size: 'small'}
*/
Vue.use(plugin, options)

模拟element-ui的安装逻辑

想一下,使用Vue.use(ElementUI,{size: 'small'}) 之后我们可以用到哪些element-ui提供的东西

  1. 可以直接在组件里面用element-ui的组件,不需要再import
  2. 可以直接使用v-loading指令
  3. 通过this.$loading在组件里面显示loading
  4. 其他...
// 这个是一个按钮组件
import Button from '@/components/button'

// loading 指令
import loadingDirective from '@/components/loading/directive'

// loading 方法
import loadingMethod from '@/components/loading'

export default {
  /**
   * Vue.use 需要插件提供一个install方法
   * @param {*} Vue Vue
   * @param {*} options 插件配置信息
   */
  install(Vue, options) {
    console.log(options)
    // 将组件通过Vue.components 进行注册
    Vue.components(Button.name, Button)

    // 注册全局指令
    Vue.directive('loading', loadingDirective)

    // 将loadingMethod 挂载到 Vue原型链上面,方便调用
    Vue.prototype.$loading = loadingMethod
  }
}

通过上面的代码,已经实现了一个丐版的element-ui插件,这时候就可以在main.js里面通过Vue.use进行插件安装了。大家可能会有疑问,为什么我要用这种写法,不用这种写法我照样可以实现功能啊。小编认为这种写法有两个优势

  1. 标准化,通过提供一种统一的开发模式,无论对插件开发者还是使用者来说,都有一个规范去遵循。
  2. 插件缓存,Vue.use在安装插件的时候,会对插件进行缓存,即一个插件如果安装多次,实际上只会在第一次安装时生效。

插件的应用场景

  1. 添加全局方法或者 property。
  2. 添加全局资源:指令/过滤器/过渡等。
  3. 通过全局混入来添加一些组件选项。
  4. 添加 Vue 实例方法,通过把它们添加到Vue.prototype上实现。
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如element-ui

提高Vue渲染性能,了解一下Object.freeze

当一个 Vue 实例被创建时,它将 data 对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。但是这个过程实际上是比较消耗性能的,所以对于一些有大量数据但只是展示的界面来说,并不需要将property加入到响应式系统中,这样可以提高渲染性能,怎么做呢,你需要了解一下Object.freeze

Vue官网中,有这样一段话:这里唯一的例外是使用 Object.freeze(),这会阻止修改现有的 property,也意味着响应系统无法再_追踪_变化。这段话的意思是,如果我们的数据使用了Object.freeze,就可以让数据脱离响应式系统,那么该如何做呢?

比如下面这个表格,因为只是渲染数据,这时候我们就可以通过Object.freeze来优化性能

<template>
  <el-table :data="tableData" >
    <el-table-column prop="date" label="日期" width="180" />
    <el-table-column prop="name" label="姓名" width="180" />
    <el-table-column prop="address" label="地址" />
  </el-table>
</template>
<script>
export default {
  data() {
    const data = Array(1000)
      .fill(1)
      .map((item, index) => {
        return {
          date: '2020-07-11',
          name: `子君${index}`,
          address: '大西安'
        }
      })
    return {
      // 在这里我们用了Object.freeze
      tableData: Object.freeze(data)
    }
  }
}
</script>

有的同学可能会有疑问,如果我这个表格的数据是滚动加载的,你这样写我不就没法再给tableData添加数据了吗?是,确实没办法去添加数据了,但还是有办法解决的,比如像下面这样

export default {
  data() {
    return {
      tableData: []
    }
  },
  created() {
    setInterval(() => {
      const data = Array(1000)
        .fill(1)
        .map((item, index) => {
          // 虽然不能冻结整个数组,但是可以冻结每一项数据
          return Object.freeze({
            date: '2020-07-11',
            name: `子君${index}`,
            address: '大西安'
          })
        })
      this.tableData = this.tableData.concat(data)
    }, 2000)
  }
}

合理的使用Object.freeze,是可以节省不少渲染性能,特别对于IE浏览器,效果还是很明显的,赶快去试试吧。

最后如果你现在需要开发移动端项目,可以了解一下小编整理的一个开箱即用框架 vue-vant-base,也许可以帮到你哦

结语

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高

扫码关注,愿你遇到那个心疼你付出的人~

微信公众号宣传图.gif

查看原文

赞 45 收藏 32 评论 5

子君 关注了专栏 · 7月21日

前端工匠公众号

我是浪里行舟,每周分享至少两篇前端文章,致力于打造一系列能够帮助初中级工程师提高的优质文章

关注 7018

子君 关注了用户 · 7月21日

阿宝哥 @angular4

http://www.semlinker.com/
聚焦全栈,专注分享 Angular、TypeScript、Node.js/Java 、Spring 技术栈等全栈干货

欢迎各位小伙伴关注本人公众号全栈修仙之路

关注 2195

子君 关注了用户 · 7月21日

浪里行舟 @langlixingzhou

微信:frontJS,记得备注sf
公众号:前端工匠
文章首发地址:https://github.com/ljianshu/Blog(包括源代码和思维导图)

关注 3434

子君 发布了文章 · 7月21日

实战技巧,Vue原来还可以这样写

两只黄鹂鸣翠柳,一堆bug上西天。

每天上班写着重复的代码,当一个cv仔,忙到八九点,工作效率低,感觉自己没有任何提升。如何能更快的完成手头的工作,今天小编整理了一些新的Vue使用技巧。你们先加班,我先下班陪女神去逛街了。

本文首发于公众号【前端有的玩】,关注我,我们一起玩前端,每天都有不一样的干货知识点等着你哦

hookEvent,原来可以这样监听组件生命周期

1. 内部监听生命周期函数

今天产品经理又给我甩过来一个需求,需要开发一个图表,拿到需求,瞄了一眼,然后我就去echarts官网复制示例代码了,复制完改了改差不多了,改完代码长这样


<template>
  <div class="echarts"></div>
</template>
<script>
  export default {
   mounted() {
     this.chart = echarts.init(this.$el)
      // 请求数据,赋值数据 等等一系列操作...
      // 监听窗口发生变化,resize组件
     window.addEventListener('resize',this.$_handleResizeChart)
  },
  updated() {
    // 干了一堆活
  },
  created() {
     // 干了一堆活
  },
  beforeDestroy() {
    // 组件销毁时,销毁监听事件
    window.removeEventListener('resize', this.$_handleResizeChart)
  },
  methods: {
    $_handleResizeChart() {
     this.chart.resize()
    },
  // 其他一堆方法
 }
}
</script>

功能写完开开心心的提测了,测试没啥问题,产品经理表示做的很棒。然而code review时候,技术大佬说了,这样有问题。

大佬:这样写不是很好,应该将监听resize事件与销毁resize事件放到一起,现在两段代码分开而且相隔几百行代码,可读性比较差
我:那我把两个生命周期钩子函数位置换一下,放到一起?
大佬:hook听过没?
我:Vue3.0才有啊,咋,咱要升级Vue?

然后技术大佬就不理我了,并向我扔过来一段代码

export default {
  mounted() {
    this.chart = echarts.init(this.$el)
    // 请求数据,赋值数据 等等一系列操作...
    // 监听窗口发生变化,resize组件
    window.addEventListener('resize', this.$_handleResizeChart)
    // 通过hook监听组件销毁钩子函数,并取消监听事件
    this.$once('hook:beforeDestroy', () => {
      window.removeEventListener('resize', this.$\_handleResizeChart)
    })
  },
  updated() {},
  created() {},
  methods: {
    $_handleResizeChart() {
      this.chart.resize()
    }
  }
}

看完代码,恍然大悟,大佬不愧是大佬,原来`Vue`还可以这样监听生命周期函数。

_在`Vue`组件中,可以用过`$on\`,\`$once`去监听所有的生命周期钩子函数,如监听组件的`updated`钩子函数可以写成 `this.$on('hook:updated', () => {})`_

2. 外部监听生命周期函数

今天同事在公司群里问,想在外部监听组件的生命周期函数,有没有办法啊?

为什么会有这样的需求呢,原来同事用了一个第三方组件,需要监听第三方组件数据的变化,但是组件又没有提供change事件,同事也没办法了,才想出来要去在外部监听组件的updated钩子函数。查看了一番资料,发现Vue支持在外部监听组件的生命周期钩子函数。

<template>
   <!--通过@hook:updated监听组件的updated生命钩子函数-->
   <!--组件的所有生命周期钩子都可以通过@hook:钩子函数名 来监听触发-->
   <custom-select @hook:updated="$_handleSelectUpdated" />
</template>
<script>
  import CustomSelect from '../components/custom-select'
  export default {
     components: {
        CustomSelect
     },
   methods: {
     $_handleSelectUpdated() {
       console.log('custom-select组件的updated钩子函数被触发')
     }
   }
 }
</script>

小项目还用Vuex?用Vue.observable手写一个状态管理吧

在前端项目中,有许多数据需要在各个组件之间进行传递共享,这时候就需要有一个状态管理工具,一般情况下,我们都会使用Vuex,但对于小型项目来说,就像Vuex官网所说:“如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex”。这时候我们就可以使用Vue2.6提供的新API Vue.observable手动打造一个Vuex

1. 创建 store

import Vue from 'vue'
// 通过Vue.observable创建一个可响应的对象
export const store = Vue.observable({
  userInfo: {},
  roleIds: []
})
// 定义 mutations, 修改属性
export const mutations = {
   setUserInfo(userInfo) {
     store.userInfo = userInfo
   },
   setRoleIds(roleIds) {
     store.roleIds = roleIds
   }
}

2. 在组件中引用

<template>
   <div>
     {{ userInfo.name }}
   </div>
</template>
<script>
  import { store, mutations } from '../store'
  export default {
    computed: {
      userInfo() {
        return store.userInfo 
      }
   },
   created() {
     mutations.setUserInfo({
       name: '子君'
     })
   }
}
</script>

开发全局组件,你可能需要了解一下Vue.extend

Vue.extend是一个全局Api,平时我们在开发业务的时候很少会用到它,但有时候我们希望可以开发一些全局组件比如Loading,Notify,Message等组件时,这时候就可以使用Vue.extend

同学们在使用element-uiloading时,在代码中可能会这样写

// 显示loading
const loading = this.$loading()
// 关闭loading
loading.close()

这样写可能没什么特别的,但是如果你这样写

const loading = this.$loading()
const loading1 = this.$loading()
setTimeout(() => {
  loading.close()
}, 1000 * 3)

这时候你会发现,我调用了两次loading,但是只出现了一个,而且我只关闭了loading,但是loading1也被关闭了。这是怎么实现的呢?我们现在就是用Vue.extend + 单例模式去实现一个loading

1. 开发loading组件

<template>
  <transition name="custom-loading-fade">
    <!--loading蒙版-->
    <div v-show="visible" class="custom-loading-mask">
      <!--loading中间的图标-->
      <div class="custom-loading-spinner">
        <i class="custom-spinner-icon"></i>
        <!--loading上面显示的文字-->
        <p class="custom-loading-text">{{ text }}</p>
      </div>
    </div>
  </transition>
</template>
<script>
export default {
  props: {
  // 是否显示loading
    visible: {
      type: Boolean,
      default: false
    },
    // loading上面的显示文字
    text: {
      type: String,
      default: ''
    }
  }
}
</script>

开发出来loading组件之后,如果需要直接使用,就要这样去用

<template>
  <div class="component-code">
    <!--其他一堆代码-->
    <custom-loading :visible="visible" text="加载中" />
  </div>
</template>
<script>
export default {
  data() {
    return {
      visible: false
    }
  }
}
</script>

但这样使用并不能满足我们的需求

  1. 可以通过js直接调用方法来显示关闭
  2. loading可以将整个页面全部遮罩起来

2.通过Vue.extend将组件转换为全局组件

1. 改造loading组件,将组件的props改为data

export default {
  data() {
    return {
      text: '',
      visible: false
    }
  }
}

2. 通过Vue.extend改造组件

// loading/index.js
import Vue from 'vue'
import LoadingComponent from './loading.vue'

// 通过Vue.extend将组件包装成一个子类
const LoadingConstructor = Vue.extend(LoadingComponent)

let loading = undefined

LoadingConstructor.prototype.close = function() {
  // 如果loading 有引用,则去掉引用
  if (loading) {
    loading = undefined
  }
  // 先将组件隐藏
  this.visible = false
  // 延迟300毫秒,等待loading关闭动画执行完之后销毁组件
  setTimeout(() => {
    // 移除挂载的dom元素
    if (this.$el && this.$el.parentNode) {
      this.$el.parentNode.removeChild(this.$el)
    }
    // 调用组件的$destroy方法进行组件销毁
    this.$destroy()
  }, 300)
}

const Loading = (options = {}) => {
  // 如果组件已渲染,则返回即可
  if (loading) {
    return loading
  }
  // 要挂载的元素
  const parent = document.body
  // 组件属性
  const opts = {
    text: '',
    ...options
  }
  // 通过构造函数初始化组件 相当于 new Vue()
  const instance = new LoadingConstructor({
    el: document.createElement('div'),
    data: opts
  })
  // 将loading元素挂在到parent上面
  parent.appendChild(instance.$el)
  // 显示loading
  Vue.nextTick(() => {
    instance.visible = true
  })
  // 将组件实例赋值给loading
  loading = instance
  return instance
}

export default Loading

3. 在页面使用loading

import Loading from './loading/index.js'
export default {
  created() {
    const loading = Loading({ text: '正在加载。。。' })
    // 三秒钟后关闭
    setTimeout(() => {
      loading.close()
    }, 3000)
  }
}

通过上面的改造,loading已经可以在全局使用了,如果需要像element-ui一样挂载到Vue.prototype上面,通过this.$loading调用,还需要改造一下

4. 将组件挂载到Vue.prototype上面

Vue.prototype.$loading = Loading
// 在export之前将Loading方法进行绑定
export default Loading

// 在组件内使用
this.$loading()

自定义指令,从底层解决问题

什么是指令?指令就是你女朋友指着你说,“那边搓衣板,跪下,这是命令!”。开玩笑啦,程序员哪里会有女朋友。

通过上一节我们开发了一个loading组件,开发完之后,其他开发在使用的时候又提出来了两个需求

  1. 可以将loading挂载到某一个元素上面,现在只能是全屏使用
  2. 可以使用指令在指定的元素上面挂载loading

有需求,咱就做,没话说

1.开发v-loading指令

import Vue from 'vue'
import LoadingComponent from './loading'
// 使用 Vue.extend构造组件子类
const LoadingContructor = Vue.extend(LoadingComponent)

// 定义一个名为loading的指令
Vue.directive('loading', {
  /**
   * 只调用一次,在指令第一次绑定到元素时调用,可以在这里做一些初始化的设置
   * @param {*} el 指令要绑定的元素
   * @param {*} binding 指令传入的信息,包括 {name:'指令名称', value: '指令绑定的值',arg: '指令参数 v-bind:text 对应 text'}
   */
  bind(el, binding) {
    const instance = new LoadingContructor({
      el: document.createElement('div'),
      data: {}
    })
    el.appendChild(instance.$el)
    el.instance = instance
    Vue.nextTick(() => {
      el.instance.visible = binding.value
    })
  },
  /**
   * 所在组件的 VNode 更新时调用
   * @param {*} el
   * @param {*} binding
   */
  update(el, binding) {
    // 通过对比值的变化判断loading是否显示
    if (binding.oldValue !== binding.value) {
      el.instance.visible = binding.value
    }
  },
  /**
   * 只调用一次,在 指令与元素解绑时调用
   * @param {*} el
   */
  unbind(el) {
    const mask = el.instance.$el
    if (mask.parentNode) {
      mask.parentNode.removeChild(mask)
    }
    el.instance.$destroy()
    el.instance = undefined
  }
})

2.在元素上面使用指令

<template>
  <div v-loading="visible"></div>
</template>
<script>
export default {
  data() {
    return {
      visible: false
    }
  },
  created() {
    this.visible = true
    fetch().then(() => {
      this.visible = false
    })
  }
}
</script>

3.项目中哪些场景可以自定义指令

  1. 为组件添加loading效果
  2. 按钮级别权限控制 v-permission
  3. 代码埋点,根据操作类型定义指令
  4. input输入框自动获取焦点
  5. 其他等等。。。

深度watchwatch立即触发回调,我可以监听到你的一举一动

在开发Vue项目时,我们会经常性的使用到watch去监听数据的变化,然后在变化之后做一系列操作。

1.基础用法

比如一个列表页,我们希望用户在搜索框输入搜索关键字的时候,可以自动触发搜索,此时除了监听搜索框的change事件之外,我们也可以通过watch监听搜索关键字的变化

<template>
  <!--此处示例使用了element-ui-->
  <div>
    <div>
      <span>搜索</span>
      <input v-model="searchValue" />
    </div>
    <!--列表,代码省略-->
  </div>
</template>
<script>
export default {
  data() {
    return {
      searchValue: ''
    }
  },
  watch: {
    // 在值发生变化之后,重新加载数据
    searchValue(newValue, oldValue) {
      // 判断搜索
      if (newValue !== oldValue) {
        this.$_loadData()
      }
    }
  },
  methods: {
    $_loadData() {
      // 重新加载数据,此处需要通过函数防抖
    }
  }
}
</script>

2.立即触发

通过上面的代码,现在已经可以在值发生变化的时候触发加载数据了,但是如果要在页面初始化时候加载数据,我们还需要在created或者mounted生命周期钩子里面再次调用$_loadData方法。不过,现在可以不用这样写了,通过配置watch的立即触发属性,就可以满足需求了

// 改造watch
export default {
  watch: {
    // 在值发生变化之后,重新加载数据
    searchValue: {
    // 通过handler来监听属性变化, 初次调用 newValue为""空字符串, oldValue为 undefined
      handler(newValue, oldValue) {
        if (newValue !== oldValue) {
          this.$_loadData()
        }
      },
      // 配置立即执行属性
      immediate: true
    }
  }
}

3.深度监听(我可以看到你内心的一举一动)

一个表单页面,需求希望用户在修改表单的任意一项之后,表单页面就需要变更为被修改状态。如果按照上例中watch的写法,那么我们就需要去监听表单每一个属性,太麻烦了,这时候就需要用到watch的深度监听deep

export default {
  data() {
    return {
      formData: {
        name: '',
        sex: '',
        age: 0,
        deptId: ''
      }
    }
  },
  watch: {
    // 在值发生变化之后,重新加载数据
    formData: {
      // 需要注意,因为对象引用的原因, newValue和oldValue的值一直相等
      handler(newValue, oldValue) {
        // 在这里标记页面编辑状态
      },
      // 通过指定deep属性为true, watch会监听对象里面每一个值的变化
      deep: true
    }
  }
}

随时监听,随时取消,了解一下$watch

有这样一个需求,有一个表单,在编辑的时候需要监听表单的变化,如果发生变化则保存按钮启用,否则保存按钮禁用。这时候对于新增表单来说,可以直接通过watch去监听表单数据(假设是formData),如上例所述,但对于编辑表单来说,表单需要回填数据,这时候会修改formData的值,会触发watch,无法准确的判断是否启用保存按钮。现在你就需要了解一下$watch

export default {
  data() {
    return {
      formData: {
        name: '',
        age: 0
      }
    }
  },
  created() {
    this.$_loadData()
  },
  methods: {
    // 模拟异步请求数据
    $_loadData() {
      setTimeout(() => {
        // 先赋值
        this.formData = {
          name: '子君',
          age: 18
        }
        // 等表单数据回填之后,监听数据是否发生变化
        const unwatch = this.$watch(
          'formData',
          () => {
            console.log('数据发生了变化')
          },
          {
            deep: true
          }
        )
        // 模拟数据发生了变化
        setTimeout(() => {
          this.formData.name = '张三'
        }, 1000)
      }, 1000)
    }
  }
}

根据上例可以看到,我们可以在需要的时候通过this.$watch来监听数据变化。那么如何取消监听呢,上例中this.$watch返回了一个值unwatch,是一个函数,在需要取消的时候,执行 unwatch()即可取消

函数式组件,函数是组件?

什么是函数式组件?函数式组件就是函数是组件,感觉在玩文字游戏。使用过React的同学,应该不会对函数式组件感到陌生。函数式组件,我们可以理解为没有内部状态,没有生命周期钩子函数,没有this(不需要实例化的组件)。

在日常写bug的过程中,经常会开发一些纯展示性的业务组件,比如一些详情页面,列表界面等,它们有一个共同的特点是只需要将外部传入的数据进行展现,不需要有内部状态,不需要在生命周期钩子函数里面做处理,这时候你就可以考虑使用函数式组件。

1. 先来一个函数式组件的代码

export default {
  // 通过配置functional属性指定组件为函数式组件
  functional: true,
  // 组件接收的外部属性
  props: {
    avatar: {
      type: String
    }
  },
  /**
   * 渲染函数
   * @param {*} h
   * @param {*} context 函数式组件没有this, props, slots等都在context上面挂着
   */
  render(h, context) {
    const { props } = context
    if (props.avatar) {
      return <img data-original={props.avatar}></img>
    }
    return <img data-original="default-avatar.png"></img>
  }
}

在上例中,我们定义了一个头像组件,如果外部传入头像,则显示传入的头像,否则显示默认头像。上面的代码中大家看到有一个render函数,这个是Vue使用JSX的写法,关于JSX,小编将在后续文章中会出详细的使用教程。

2.为什么使用函数式组件

  1. 最主要最关键的原因是函数式组件不需要实例化,无状态,没有生命周期,所以渲染性能要好于普通组件
  2. 函数式组件结构比较简单,代码结构更清晰

3. 函数式组件与普通组件的区别

  1. 函数式组件需要在声明组件是指定functional
  2. 函数式组件不需要实例化,所以没有this,this通过render函数的第二个参数来代替
  3. 函数式组件没有生命周期钩子函数,不能使用计算属性,watch等等
  4. 函数式组件不能通过$emit对外暴露事件,调用事件只能通过context.listeners.click的方式调用外部传入的事件
  5. 因为函数式组件是没有实例化的,所以在外部通过ref去引用组件时,实际引用的是HTMLElement
  6. 函数式组件的props可以不用显示声明,所以没有在props里面声明的属性都会被自动隐式解析为prop,而普通组件所有未声明的属性都被解析到$attrs里面,并自动挂载到组件根元素上面(可以通过inheritAttrs属性禁止)

4.我不想用JSX,能用函数式组件吗?

Vue2.5之前,使用函数式组件只能通过JSX的方式,在之后,可以通过模板语法来生命函数式组件

<!--在template 上面添加 functional属性-->
<template functional>
  <img :data-original="props.avatar ? props.avatar : 'default-avatar.png'" />
</template>
<!--根据上一节第六条,可以省略声明props-->

结语:

不要吹灭你的灵感和你的想象力; 不要成为你的模型的奴隶。 ——文森特・梵高

扫码关注,愿你遇到那个心疼你付出的人~

微信公众号宣传图.gif

查看原文

赞 45 收藏 30 评论 2

子君 关注了用户 · 7月20日

胖胖胖胖虎阿 @user_uhdtcoer

No BB,Show code。

关注 162

子君 关注了用户 · 7月20日

EMQX @emqx

5G 时代,万物互联消息引擎
Scalable and Reliable Real-time MQTT Messaging Engine for IoT in 5G Era

关注 406