2020/05/28 周四

node package.json中版本前的 ~ 与 ^ 分别代表什么

来看看element ui的package.json,其中async-validator是 ~ 开头,而其他都是 ^ 开头,有什么区别呢?

  1. "dependencies": {
  2. "async-validator": "~1.8.1",
  3. "babel-helper-vue-jsx-merge-props": "^2.0.0",
  4. "deepmerge": "^1.2.0",
  5. "normalize-wheel": "^1.0.1",
  6. "resize-observer-polyfill": "^1.5.0",
  7. "throttle-debounce": "^1.0.1"
  8. },

版本格式 1.8.1 对应 major.minor.patch

  • major:表示版本有了一个大更改。
  • minor:表示增加了新的功能,并且可以向后兼容。
  • patch:表示修复了bug,并且可以向后兼容。

~:他会更新到当前minor version(也就是中间的那位数字)中最新的版本,也就是只变动patch到最新版本,它不会自动更新minor版本, 波浪符号是曾经npm安装时候的默认符号,现在已经变为了插入符号。

^:这个符号就显得非常的灵活了,他将会把当前库的版本更新到当前major version(也就是第一位数字)中最新的版本。也就是更新minor到最新版本,他不会自动更新major版本。

当我们使用最新的Node运行’npm instal —save xxx’,的时候,会优先考虑使用插入符号(^)而不是波浪符号(~)了。

可以手动安装指定版本

  1. # 安装最新版本
  2. npm instlal xxx
  3. # 默认情况下,会安装最新版本npm包,等价于
  4. npm install xxx@latest
  5. # 安装指定版本
  6. npm install xxx@[指定版本号]
  7. # 安装未来版本,未正式发布的beta版本
  8. npm install xxx@next

参考:Node.js中package.json中库的版本号详解(^和~区别)

vue element表单组件简单实现

先来写一个调用示例,把el-前缀换成z-,然后我们需要实现z-form, z-form-item, z-input组件

  1. <template>
  2. <!-- /elementForm -->
  3. <div>
  4. <z-form ref="ruleForm" v-model="form" :rules="rules">
  5. {{ form }}
  6. <z-form-item label="姓名" prop="name">
  7. <z-input v-model="form.name" placeholder="请输入姓名"></z-input>
  8. </z-form-item>
  9. <z-form-item label="电话" prop="mobile">
  10. <z-input v-model="form.mobile" placeholder="请输入电话"></z-input>
  11. </z-form-item>
  12. <button @click="submitForm('ruleForm')">提交</button>
  13. <button @click="resetForm('ruleForm')">重置</button>
  14. </z-form>
  15. </div>
  16. </template>
  17. <script>
  18. export default {
  19. components: {
  20. ZInput: () => import("./ZInput"),
  21. ZFormItem: () => import("./ZFormItem"),
  22. ZForm: () => import("./ZForm")
  23. },
  24. data() {
  25. return {
  26. form: {
  27. name: "",
  28. mobile: ""
  29. },
  30. rules: {
  31. name: [
  32. { required: true, message: "请输入姓名", trigger: "blur" },
  33. { min: 3, max: 5, message: "长度在 3 到 5 个字符", trigger: "blur" }
  34. ],
  35. mobile: [{ required: true, message: "请输入电话", trigger: "change" }]
  36. }
  37. };
  38. },
  39. methods: {
  40. submitForm(formName) {
  41. this.$refs[formName].validate(valid => {
  42. console.log("valid", valid);
  43. if (valid) {
  44. alert("submit!");
  45. } else {
  46. console.log("error submit!!");
  47. }
  48. });
  49. },
  50. resetForm(formName) {
  51. this.$refs[formName].resetFields();
  52. }
  53. }
  54. };
  55. </script>

表单组件的封装:ZInput.vue

  • Input
    • 双向绑定:@input、:value 派发校验事件
    • 派发校验事件
<template>
  <div>
    <!-- 
      1. <z-input v-model="searchForm.name"></z-input> 等价于
      <z-input :value="searchForm.name" @input="searchForm.name = $event"></z-input>
      虽然等价,区别是什么呢?v-model在输入法组合过程中不会更新值,而@input这种是会更新的,详情参见之前的笔记
      2. v-bind="$attrs" 接收z-input上的除props接收外设置的其它属性,比如placeholder等
    -->
    <input :value="value" @input="oninput" v-bind="$attrs" />
  </div>
</template>

<script>
export default {
  props: {
    value: {
      type: String,
      required: true
    }
  },
  methods: {
    oninput(e) {
      this.$emit("input", e.target.value); // 双向绑定
      this.$parent.$emit("validate"); // 触发父组件的校验
    }
  }
};
</script>
  • FormItem ZFormItem.vue
    • 给Input预留插槽
    • slot 能够展示label和校验信息
    • 能够进行校验
<template>
  <div class="z-form-item">
    <div class="label">{{ label }}:</div>
    <div class="input"><slot></slot></div>
    <div class="error" v-if="errMsg">{{ errMsg }}</div>
  </div>
</template>

<script>
import Schema from "async-validator";
export default {
  inject: ["form"], // 从祖先组件接收searchForm传参
  props: {
    label: {
      // 对应的标签名
      type: String,
      required: true,
      default: ""
    },
    prop: {
      // 对应的字段名
      type: String
    }
  },

  data() {
    return {
      errMsg: "" // 错误信息
    };
  },

  mounted() {
    // 当前组件监听validate事件,子组件通过$parent.$emit触发
    this.$on("validate", () => {
      this.validate();
    });
  },

  methods: {
    // 返回promise, 注意 asyc-validator的版本,需要是新的
    validate() {
      let value = this.form.model[this.prop];
      let rules = this.form.rules[this.prop];
      console.log(this.prop, value, rules);

      let desc = { [this.prop]: rules };
      let schema = new Schema(desc);
      return schema.validate({ [this.prop]: value }, errors => {
        if (errors) {
          this.errMsg = errors[0].message;
        } else {
          console.log("验证成功");
          this.errMsg = "";
        }
      });
    },
    //  rules: {
    //     name: [
    //       { required: true, message: "请输入姓名", trigger: "blur" },
    //       { min: 3, max: 5, message: "长度在 3 到 5 个字符", trigger: "blur" }
    //     ],
    //     mobile: [{ required: true, message: "请输入电话", trigger: "change" }]
    //   }
    resetFields() {
      this.form.model[this.prop] = ""; // 重置值
      this.errMsg = ""; // 重置错误消息
    }
  }
};
</script>
  • Form ZForm.vue
    • 给FormItem留插槽
    • 设置数据和校验规则
    • 全局校验
<template>
  <!-- el-form 模拟-->
  <div>
    <slot></slot>
  </div>
</template>

<script>
export default {
  // 将z-form元素上的model以及rules属性的值传递到z-form-item,用于校验,显示错误信息
  provide() {
    return {
      form: {
        model: this.value,
        rules: this.rules
      }
    };
  },
  props: {
    value: {
      type: Object
    },
    rules: {
      type: Object
    }
  },
  methods: {
    // submit时的校验
    async validate(cb) {
      // this.$children 所有form-item vue实例 获取实例的this.prop属性,有值则校验
      let tasks = this.$children
        .filter(item => item.prop)
        .map(item => item.validate());

      console.log("tasks", tasks);
      // 执行他们的校验方法,如果大家的Promise全部都resolve,校验通过
      // 如果其中有reject,catch()中可以处理错误提示信息
      try {
        await Promise.all(tasks);
        cb(true);
      } catch (e) {
        cb(false);
      }
    },

    resetFields() {
      // form,这样做可能只是清空了值,但没有清楚form-item的错误提示信息
      // Object.keys(this.value).forEach(key => {
      //   this.value[key] = "";
      // });
      this.$children
        .filter(item => item.prop)
        .forEach(item => item.resetFields());
    }
  }
};
</script>

这里的实现和element-ui的实现有什么区别呢?可以参考element源码,我这里简单说几个区别

  1. element使用的表单校验 async-validator是 1.x.x 版本,而上面的示例需要使用的版本 3.x.x版本
  2. Form组件里provide,上面只为form绑定了rules和props,element中指定绑定了当前this
  3. 事件的监听和触发,这里使用的是$parent来处理,element里面通过 this.dispatch事件来触发
  4. element form支持很多参数,这里只是简单的模拟,element里面会复杂很多。

完整demo参见 element form实现 - fedemo | github

2020/05/27 周三

tabs标签页组件的坑

当使用tabs组件,特别是同一个组件可能会打开多个tab时。需要注意

  1. 组件打开一次后,created已执行,再打开一个tab时,不会触发created或mounted,需要用watch监听prop传值的改变进行一些请求接口的初始化操作,如果组件还有子组件,也需要这样做,防止数据不刷新的问题
  2. 点击一个tab后,如果请求比较慢,再点击另一个tab,数据可能会乱,注意tab切换时,取消发出的请求
  3. 仔细检查同组件不同tab切换时的数据、操作相关的影响,需要做到互相独立,互不影响

pinyin中文转拼音npm包在前端使用时的坑

在很早之前node项目中就使用过这个npm包。这次由于Element table组件排序时,无法按照首字母排序,就引入了这个包。由于是单页面应用,import进来是没问题的,chrome里面正常。

import pinyin from "pinyin";

console.log(
  pinyin("测试", {
    style: pinyin.STYLE_NORMAL, // 设置拼音风格
    heteronym: true
  }).join("") 
);
// ceshi

后面在IE11里出现了一个bug,就是页面路由不能正常加载,调了好久。最开始以为是路由层级的问题,调到怀疑人生,最后发现是 pinyin 这个包的问题,他在IE下无法正常加载,偶尔报错 “函数错误”,导致整个页面执行失败,路由无法加载。所以在遇到难调试的问题时,先把error的报错全部解决再调, 已经遇到好几次这种情况了

IE下出现异常,console里是无法看到是哪个文件报错的,需要在F12里点击断点位置,选择遇到错误时停止,这样出现问题就会自动跳转到对应的位置

ie_jserror.png

2020/05/20 周三

sessionStorage新打开一个tab页就失效的问题

首页我们要知道3点:

  1. sessionStorage在浏览器的两个tab页之前是无法共享的,一个tab页中sessionStorage修改后,不能触发其他tab页的storage事件
  2. 当前tab页的localStorage修改是无法触发当前页的storage事件的,他会触发其他tab页的storage事件
  3. localStorage的共享,只发生在同源的地址里。非同源无法共享localStorage

怎么将就页面的sessionStorage传递到新开的tab页呢?

由于sessionStorage打开新tab页默认会丢失。那新开tab页的sessionStorage就是空的。我们可以判断,如果sessionStorage.length值为0,那么就是新开的页面。这时我们通过设置一个localStorage字段的值,触发之前打开页面的Storage事件,在这个事件里我们将当前页面的sessionStorage通过localStorage设置值,来触发新页面的Storage事件,把sessionStorage传递到新的页面

下面是部分核心代码,详细demo参见 github demo地址

<script>
import NewTabSessionShare from "./newTabSessionShare";
export default {
  data() {
    return {
      alreadyCheck: false
    };
  },
  created() {
    this.alreadyCheck = sessionStorage.getItem("TEST_alreadyCheck") === "true";
    NewTabSessionShare.init(() => {
      this.alreadyCheck =
        sessionStorage.getItem("TEST_alreadyCheck") === "true";
    });
  }
}

newTabSessionShare.js

class NewTabSessionShare {
  constructor() {}

  static init(cb) {
    let tempFields = "TEST_tempEmit";

    window.addEventListener("storage", event => {
      console.log(event);
      // 由于每个页面都会触发该事件,我们需要判断当前页是新开的tab页,还是旧的
      // 如果是新开的tab页,负责接收localStorage.getItem('sessionStorage') 并删除
      // 如果是旧的tab页,负责写入localStorage.setItem('sessionStorage')

      // 旧的tab页接收到事件时,key会是tempFields
      if (event.key === tempFields) {
        console.log("接收到新tab页打开时触发的消息");
        // 触发新tab页的storage事件,传递当前页的sessioinStorage事件
        localStorage.setItem("sessionStorage", JSON.stringify(sessionStorage));
        // 清除localStorage
        localStorage.removeItem("sessionStorage");
        // 这里会触发两次新tab页的storage事件
        // 1. newValue: "{"TEST_alreadyCheck":"true"}"  oldValue: null
        // 2. newValue: null oldValue: "{"TEST_alreadyCheck":"true"}"
      } else if (event.key === "sessionStorage") {
        console.log(
          "新tab页接收到老tab页,设置的localStorage,接收并删除",
          localStorage.getItem("sessionStorage")
        );
        // 新打开窗口如果newValue的值不为null,那就是旧tab页将其sessionStorage传递到了当前页
        // 然后,将传过来的数据原封不动的设置到当前页
        if (event.newValue !== null) {
          let data = JSON.parse(event.newValue);
          for (let key in data) {
            sessionStorage.setItem(key, data[key]);
          }
          typeof cb === "function" && cb();
        }
      }
    });

    // 如果是新开的tab页,那么sessoinStorage为空
    if (!sessionStorage.length) {
      // 通过触发其他页面的storage事件,来读取之前页面的sessionStorage并传递到当前页
      localStorage.setItem(tempFields, Date.now());
    }
  }
}

export default NewTabSessionShare;

参考

H5拖放(Drag and Drop)的坑

  • event.dataTransfer.effectAllowed只能设置鼠标样式,不能设置拖动元素行为,drag后之前的元素会消失,想要保留需要使用cloneNode来操作

    // 放置后,删除原来的图片
    // ev.target.appendChild(document.querySelector(`#${reciveData}`))
    // 放置后,保留原图片
    ev.target.appendChild(document.querySelector(`#${reciveData}`).cloneNode(true))
    
  • 在Chrome中,放置区域的ondrop事件不触发,需要在onenter和onover事件里阻止默认行为(火狐不需要这样处理)

  • 在Firefox(火狐)浏览器里drop图片后,会新在新的tab也打开图片,不仅要在drop里阻止默认行为,还要阻止事件冒泡

demo如下,demo 在线体验地址,demo github源码

<!-- 图片默认的 draggable="true" 而想 h1这种默认为false不可拖动 -->
<img id="img" src="test.png" >
<!-- 放置区域a -->
<div class="wrap" id="targetA"></div>

<script>
// 被拖动元素的事件监听
let img = document.getElementById('img')
// 当元素开始拖动时触发,仅触发一次
img.addEventListener('dragstart', (ev) => {
  // 设置值,在放置区域触发drop事件时,可以通过ev.dataTransfer.getData获取这里的值
  ev.dataTransfer.setData("text", ev.target.id)
  // link 会音响拖动到放置区域的鼠标样式,只是样式,并不决定行为 
  ev.dataTransfer.effectAllowed = 'copy'; 
})

// 放置区域A的事件监听
let targetA = document.getElementById('targetA')
// 当有拖动元素(放到)落到放置区域时触发,一次
targetA.addEventListener('drop', (ev) => {
  ev.stopPropagation(); // 必要,阻止冒泡,防止火狐浏览器放置图片后打开新的窗口
  ev.preventDefault(); // 必要,阻止默认行为 防止火狐浏览器放置后直接打开图片

  // 放置落下时,接收被拖拽的元素在 dragstart时用ev.dataTransfer.setData设置的值
  // 这里传的id备用
  let reciveData = ev.dataTransfer.getData("text")
  console.log('drop, recive data', reciveData, ev.dataTransfer.dropEffect)

  // 必要,设置拖动后放置的效果,移动还是copy
  // 放置后,删除原来的图片
  // ev.target.appendChild(document.querySelector(`#${reciveData}`))
  // 放置后,保留原图片
  ev.target.appendChild(document.querySelector(`#${reciveData}`).cloneNode(true))

  ev.target.classList.remove('active') // 必要,放置在区域里后,还原样式
})

// 当拖动元素移动到放置区域时触发,触发多次
targetA.addEventListener('dragover', (ev) => {
  ev.preventDefault() // 必要,chrome drop兼容必须
})
// 当拖动元素进入放置区域时触发,一次
targetA.addEventListener('dragenter', (ev) => {
  ev.preventDefault() // 必要,chrome drop兼容必须
  ev.target.classList.add('active') // 必要,设置进入时的样式
})
  // 当拖动元素离开放置区域时触发,一次
targetA.addEventListener('dragleave', (ev) => {
  ev.target.classList.remove('active') // 必要,设置离开后的样式
})
</script>

参考:

扩展:

a + 1 === a + 2 为true的情况

注意这里是全等,不是宽松相等时,隐式转换的问题。我现在了解的有两种情况:

// 1. Infinity
var a = Infinity // Infinity是这个神奇的数,我试了下除了 * 0等于NAN外,其他情况基本都等于他自己
a + 1 === a + 2 // true

// 2. Math.pow(2, 53) - 1 最大的安全整数
Number.MAX_SAFE_INTEGER === Math.pow(2, 53) -1 // true
a = Number.MAX_SAFE_INTEGER
a + 1 === a + 2 // true

以上,当大于2的53次方-1时,就不安全了,结果会超出常规,ES2020引入了bigint来处理大于2的53次方-1的数据

// bigint类型的数与n结尾
a = BigInt(Number.MAX_SAFE_INTEGER) // 9007199254740991n
a + 1n // 9007199254740992n
a + 2n // 9007199254740993n

参考:ES2020 bigint数据类型,为什么要新增这个数据类型?

2020/05/14 周四

less使用mixin抽取公共代码,减少重复代码

由于没有系统的学习less,之前只用到less的嵌套写法,很少用变量,基本没用mixin模块化封装,这次尝试了下,发现还是不错的,下面来用封装一个基础的布局组件

<!-- pageA -->
<template>
  <div class="container">
    <div class="top"></div>
    <div class="main">
      <div class="left"></div>
      <div class="right"></div>
    </div>
  </div>
</template>

<script>
export default {}
</script>

<style lang="less" scoped>
@import (reference) url('./common/base.less');
.container-mixin(); /* 调用base.less里面定义的mixin方法 */
</style>

common/base.less

.container-mixin() {
  .container {
    @headerHeight: 100px; /* 变量,顶部高度 */
    .top {
      height: @headerHeight;
      background: #999;
    }
    .main {
      display: flex;
      height: calc(100vh - @headerHeight);
      background-color: rgba(255, 0, 0, 0.2);
      .left {
        width: 20%;
        background-color: greenyellow;
      }
      .right {
        width: 80%;
        background-color: turquoise;
      }
    }
  }
}

公共方法封装的好处在于,下次如果相同的页面,就不需要再写一遍了,虽然用class也可以,但less的mixin会更加强大,灵活,他还可以传参数,我们在页面B引入时,可以对默认样式进行修改

<template>
  <div class="container">
    <div class="top"></div>
    <div class="main">
      <div class="left"></div>
      <div class="right"></div>
    </div>
  </div>
</template>

<script>
export default {}
</script>

<style lang="less" scoped>
@import (reference) url('./common/base.less');
.container-mixin();
// 引入公共样式后,再修改部分公共的样式
.container {
  .top {
    background: red;
  }
}
</style>

上面的例子中使用 (reference) 是为了防止在不同的组件中引入导致公共代码多次打包问题

2020/05/13 周三

vue为什么建议永远不要把 v-if 和 v-for 同时用在同一个元素上

当Vue处理指令时,v-for 比 v-if 优先级高,来看个例子

<ul>
  <li
    v-for="user in users"
    v-if="user.isActive"
    :key="user.id"
  >
    {{ user.name }}
  </li>
</ul>

将进行如下计算,其实显示是正常的,但v-for会遍历所有的元素,哪怕我们只想通过v-if渲染出少部分元素,每次重新渲染的时候都会遍历整个列表

this.users.map(function (user) {
  if (user.isActive) {
    return user.name
  }
})

这种情况,建议使用 computed属性过滤需要显示的数组

computed: {
  activeUsers: function () {
    return this.users.filter(function (user) {
      return user.isActive
    })
  }
}

参考:避免-v-if-和-v-for-用在一起必要 | Vue.js

vue项目文件以及文件夹命名规范问题

单文件组件文件名 要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)。

vue官方风格指南没有建议文件夹的命名,找了个网上我比较认同的一种,文件或文件夹的命名遵循以下原则:

  • index.js 或者 index.vue,统一使用小写字母开头的(kebab-case)命名规范
  • 属于组件或类的,统一使用大写字母开头的(PascalCase)命名规范
  • 其他非组件或类的,统一使用小写字母开头的(kebab-case)命名规范

参考

vue里简单的总线(bus)发布订阅模式实现

先来看看怎么调用

// main.js
import Bus from 'Bus.js'
Vue.prototype.$bus = new Bus()

// child1
this.$bus.$on('foo', handle)
// child2
this.$bus.$emit('foo')

来写Bus.js

class Bus {
  constructor() {
    this.callbacks = {}
  }
  $on(name, fn) {
    // 如果之前没有监听,就创建一个新的数组
    !this.callbacks[name] && (this.callbacks[name] = [])
    typeof fn === 'function' && this.callbacks[name].push(fn)
  }
  $emit(name, args) {
    if (this.callbacks[name]) {
      this.callbacks[name].forEach(cb => cb(args))
    }
  }
  $off(name, fn) {
    if (this.callbacks[name]) {
      // 如果没传fn, 移除所有,如果传了移除对应的函数,这里只做移除素有的
      this.callbacks[name] = undefined // 讲思路
    }
  } 
} 

export default Bus

vue组件之间通信方式总结

父组件 => 子组件

  • props ```javascript // child props: { msg: string }

// parent


- 引用refs
```javascript
// parent
<hellow-word ref="hw" />

this.$refs.hw.xx

子组件 => 父组件

// child
this.$emit('add', 'val')

// parent
<hello-word @add="cartAdd($event)" />

兄弟组件:通过共同的祖辈组件($parent或$root) 利用vue内置的发布订阅模式功能

// brother1
this.$parent.$on('foo', handle)
// brother2
this.$parent.$emit('foo')

祖先和后代之间

  • provide / inject 祖先给后代传值 ```javascript // 祖先组件 provide() { return { foo: ‘foo’} // 要像data一样,用函数包裹 }

// 后代组件 inject: [‘foo’]


- dispatch:后代给祖先传值
```javascript
function dispatch(eventName, data) {
  let parent = this.$parent
  // 只要还存在父元素就继续往上找
  while (parent) {
    // 父元素用$emit触发
    parent.$emit(eventName, data)
    // 继续传给上一层父元素
    parent = parent.$parent
  }
}

任意两个组件之间:事件总线(Bus)或vuex

// vue组件自身实现了发布订阅模式
// Bus.js
export default new Vue()

// A组件
import Bus from 'Bus'
Bus.$on('foo', handle)
// B组件
import Bus from 'Bus'
Bus.$emit('foo', 'val')

2020/05/12 周二

vue为什么要将插槽slot=”aaa”的写法变更为v-slot:aaa

在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除

具名插槽 主要用于当有多个插槽时,通过名字对不同的插槽进行区分

由于在父组件里使用子组件,会写上对应的插槽,这时插槽的作用域为当前的父组件,如果想在这里获取子组件的作用域,就需要作用域插槽

来看代码

<!-- base-layout组件实现 -->
<div>
  <header>
    <slot name="slotA" v-bind:user="user"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
</div>

<!-- base-layout组件调用 新的写法 -->
<base-layout>
  <!-- 
    通过template(不能是其他元素) 指定其子元素的内容放到name为slotA的插槽里
    可以通过slotProps拿到slot位置bind的所有属性 
  -->
  <template v-slot:slotA="slotProps">
    <h1>Here might be a page title {{slotProps.user}}</h1>
  </template>

  <p>A paragraph for the main content.</p>
</base-layout>

<!-- base-layout组件调用 旧的写法 -->
<base-layout>
  <!-- 
    通过普通元素和template 指定其子元素的内容放到name为slotA的插槽里
    可以通过slotProps拿到slot位置bind的所有属性 
  -->
  <div slot="slotA" slot-scope="slotProps">
    <h1>Here might be a page title {{slotProps.user}}</h1>
  </div>

  <p>A paragraph for the main content.</p>
</base-layout>

v-slot将slot和slot-scope简写为一个属性,且v-slot更符合vue的语法规则

参考:

vue中$attrs$listeners 的使用场景

之前的笔记有提过,如果A组件包含B组件,B组件包含C组件。C组件想要触发A组件的方法,可以在B组件上加 v-on="$listeners" 来实现。那他做了哪些操作呢?

我们知道template里面 {{}} 或者v-bind、v-on等于的 "" 里,直接会省去this,v-on="$listeners" 里面的值在.vue文件的 script 中,可以使用 this.$listeners 来获取

$listeners 它是一个对象,包含了作用在当前组件上的所有监听器

{
  focus: function (event) { /* ... */ }
  input: function (value) { /* ... */ },
}

$attr$listeners有什么用呢?他们在对input等表单元素的二次封装时非常有用

比如我要封装一个 zuo-input 组件,来对原生的input元素进行功能性增强,来看看zuo-input可能的使用场景

<zuo-input value="xx" placeholder="请输入" @focus.native="focus" @input="xxx" @change="xx"/>

我们需要把 zuo-input 上的属性、方法直接绑定到内部的input元素上,你可以用props来传,但是如果有很多个属性呢?一个属性写一个props就太麻烦了,这时我们可以使用 $attrs

$attrs 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。

v-bind="$attrs" 他类似属性展开运算符,将父组件调用子组件时传入的属性展开(不包含props已接收的)、v-bind到当前的元素上。

v-on="$listeners" 也类似上面的行为,他会将父组件传递给子组件的事件 v-on 到当前的元素上

在 zuo-input 组件内部可以通过下面的方法直接绑定prop及事件

<input v-bind="$attrs" v-on="$listeners">

参考:

v-model与.sync的区别

一般父组件给子组件传值是单向的,对于非引用类型,子组件怎么修改父组件传给子组件prop对应的值呢?除了通过 $parent、$root、Bus(发布,订阅)、状态管理(vuex)、额外定义一个方法外,还有两种方法:使用 v-model,或者为加.sync,来看下对比

先来看v-model

<my-div v-model="someValue" />
<!-- 等价于 -->
<my-div :value="someValue" @input="someValue = $event">

<script>
  // this.$emit('input', '修改somevalue的值为这里的值')
</script>

再来看.sync

<my-div :someValue.sync="someValue" />
<!-- 等价于 -->
<my-div :someValue="someValue" v-on:update:title="someValue = $event" />


<script>
  // this.$emit('update:someValue', '修改somevalue的值为这里的值')
</script>

两者的区别

  • v-model主要用于表单输入的双向绑定,注重值的改变,.sync主要用于状态的切换
  • v-model事件及prop的名称,子组件接收时是可以通过model自定义的,.sync子组件接收到的值是固定的

参考:

vue在自定义组件上使用v-model指令

在自定义组件上,使用v-model指令,默认会向子组件传递一个字段名为 value 的 prop 属性,以及绑定一个名为 input 的事件。在子组件里,可以用props来接收value字段,可以用 this.$emit(‘input’) 来对父组件里value的值进行修改。

<my-div v-model="someValue"></my-div>
<!-- 等价于 -->
<my-div :value="someValue" @input="someValue = $event">

<script>
export default {
  props: {
    value: {
      type: String,
      required: true
    }
  },
  data() {
    return {}
  },
  methods: {
    modifyParentCompsValue() {
      this.$emit('input', '要设置的值')
    }
  }
}
</script>

怎么修改v-model的默认行为呢?

model选项,允许一个自定义组件在使用 v-model 时定制 prop 和 event。默认情况下,一个组件上的 v-model 会把 value 用作 prop 且把 input 用作 event,但是一些输入类型比如单选框和复选框按钮可能想使用 value prop 来达到不同的目的。使用 model 选项可以回避这些情况产生的冲突。

export default {
  model: {
    prop: 'show',
    event: 'close'
  }
  props: {
    show: {
      type: String,
      required: true
    }
  },
  data() {
    return {}
  },
  methods: {
    modifyParentCompsValue() {
      this.$emit('close', '要设置的值')
    }
  }
}

参考:

v-infinite-scroll 放到slot里或者用v-if控制时首次无法触发loadMore的问题

最近项目结构调整,发现一个问题,把 v-infinite-scroll 对应的元素放到 slot 里,首次无法触发loadMore, 不放到slot里面又是正常的,来看代码

<template>
  <div>
    <-- 用 sub-comps-middle 组件嵌套,写在slot时,无法加载loadMore,去掉sub-comps-middle 就是正常的-->
    <sub-comps-middle>
      <div v-infinite-scroll="loadMore" infinite-scroll-distance="10">
        测试
      </div>
    </sub-comps-middle>
  </div>
</template>

带着这个问题,我看了下 v-infinite-scroll 的源码,在关键位置写了几个console,找到了其中的原因,来看看产生问题的地方

// InfiniteScroll 部分源码
// github地址:https://github.com/ElemeFE/vue-infinite-scroll/blob/master/src/directive.js
var InfiniteScroll = {
  bind: function bind(el, binding, vnode) {
    el[ctx] = {
      el: el,
      vm: vnode.context,
      expression: binding.value
    };
    var args = arguments;
    console.log('bindfunc before mouted', el[ctx].vm, el[ctx])
    el[ctx].vm.$on('hook:mounted', function () {
      console.log('hook:mounted')
      el[ctx].vm.$nextTick(function () {
        if (isAttached(el)) {
          doBind.call(el[ctx], args);
        }

正常情况下,页面一加载,InfiniteScroll 会开始初始化,执行其bind函数。bind函数里加了一个监听,当接收到当前组件的 hook:mounted 事件,也就是mounted事件时,开始做真正的绑定,执行doBind方法。

那么问题来了,正常情况下,在组件mounted之前,InfiniteScroll会完成初始化,这样就可以接收到页面的mounted消息,然后执行真正的相关事件绑定。

假如我们把 v-infinite-scroll 写在slot里,当前页面组件mounted过后,InfiniteScroll才执行初始化,初始化时监听mounted再执行doBind,而页面已经mounted过了,所以会无法触发loadMore,同理,v-if 控制时,如果为false,可能会有InfiniteScroll没初始化之前,页面就已经mounted的情况。

怎么解决这个问题呢?记住 v-infinite-scroll 必须放在一个单独的单文件组件里,不要放到某个组件的slot里。且不要用v-if控制,使用v-show,这样就不会有问题了。

测试demo,参见: v-infinite-scroll 测试demo

2020/05/08 周五

动态组件怎么动态绑定一个或多个v-bind属性

最近有封装一个tabs标签页组件,引入组件,可以将页面进行tab化。

原先的页面作为子组件放到tabs组件里,由于标签页跳转页面时有需要打开新的标签页。所以tabs组件里会包含多个页面组件。

为了避免像el-tabs那样,每次引入tabs组件都需要自己写v-if的逻辑来切换tab显示。我把这一步封装到了自定义tabs组件内部,内部使用动态组件component、is来切换组件显示。

为了页面tab化时最好不要改动,我需要根据不同的组件,动态v-bind不同的组件名。但问题是动态v-bind属性局限性很强,由于动态属性还包含修饰符,所以只能是单个的变量,不能是 tabs[curTabIndex].prop 这种写法,且这种方法只能传入一个参数,如果tab页组件需要传入多个参数,那怎么办?我暂时直接用options传入一个对象,在需要tab化的组件里转换一下才行。

如果需要更好的处理,可能需要写render函数了。

<component is="comsMap[tabs[curTabIndex]]" :[tabs[curTabIndex].prop]="tabs[curTabIndex].value"></component>

另外如果在动态组件里加了 keep-alive 也是有坑的,因为假如可以打开多个详情标签页,那多个标签页是同一个组件,只是不同的值在切换,如果加了keep-alive那每次打开的都是同一个详情页,我们可以使用watch监听下options值的变动,值改动时,触发页面数据跟着改变,也就是tabs页对于需要打开多个相同组件,不同内容的tab,是做不到keep-alive的,除非自己写缓存逻辑,-_-

组件封装的目的很简单,就是封装变化、减少代码量。易用性、可扩展、可维护性之间要寻找一个平衡。看哪些是必须要提供的功能,在这个前提下尽量精简,精简到不能继续封装为止。另外我们在使用这个组件时,需要做的工作尽可能少,代码尽量优雅。核心问题还是提高效率,增加代码结构化。

在大话设计模式的书里,有讲到,产品可能会频繁的变动、新增功能。我们要考虑到页面可能发生的各种变化,尽量在发生变动时,不用怎么改代码,这也是设计模式的核心理念:封装变化。这样才能写出更健壮的代码。

多层级组件,父组件怎么将事件传递给孙组件?

来看一个例子,假设A组件包含组件B,B组件又包含组件C,我们知道,在B组件里 this.$emit(‘open-tab’) 会执行其父组件A里面对应的方法,但如果B的子组件C,也想触发A组件的事件,那要怎么做呢?

<comp-a :detail="detail" @open-tab="openTab"></comp-a>
<comp-b></comp-b>
<comp-c></comp-c>

这就要用到 v-on=”$listeners”,在B组件上加上这个属性,可以将A组件上v-on绑定的事件(不含 .native 修饰器的)传递到其子组件,对创建高层级组件非常有用。

<comp-b v-on="$listeners"></comp-b>

同理怎么将A组件的props值传递到C组件?可以通过加 v-bind=”$attrs” 来实现

vm.$listeners API — Vue.js

2020/05/03 周日

uni动态修改导航栏按钮文案

先来看导航栏按钮配置,导航栏右侧有一个按钮 “编辑”

{
  "path": "pages/cart/cart",
  "style": {
    "navigationBarTitleText": "标题",
    "app-plus": {
      "autoBackButton": false,
      "titleNView": {
        // 这里没有用搜索栏
        // "searchInput": {
        //     "align": "center",
        //     "backgroundColor": "#eee",
        //     "borderRadius": "5px", // 只能用px作单位
        //     "placeholder": "请输入内容",
        //     "placeholderColor": "#ccc"
        // },
        "buttons": [{
            "color": "#222222",
            "colorPressed": "#eee",
            "float": "right",
            "fontSize": "14px",
            "width": "45px",
            "text": "编辑" // 字体图标\u 开头,加上字体图标unicode后面四位
        }]
      }
    }
  }
}

对应的js

export default {
  // 导航栏右侧按钮  编辑 => 完成
  // 点击编辑或完成,会触发该函数
  onNavigationBarButtonTap(e) {
    let isApp = !!this.$mp.page.$getAppWebview
    if (isApp) {
      // 如果是app场景
      this.changeNavButtonText()
    } else {
      // 如果是H5
      let btnEle = document.querySelectorAll('.uni-page-head-btn i')[1]
      let curText = btnEle.textContent
      btnEle.textContent = curText === '完成' ? '编辑' : '完成'
      this.isEdit = curText === '编辑'
    }
  },
  methods: {
     // 修改导航栏标题
    changeNavButtonText(text) {
      let webview = this.$mp.page.$getAppWebview()
      let tn = webview.getStyle().titleNView;
      let curText = webview.getStyle().titleNView.buttons[0].text

      webview.setTitleNViewButtonStyle(0, {  
        text: curText === '完成' ? '编辑' : '完成'
      }); 
      this.isEdit = curText === '编辑' 
      // 用于真机调试时 log
      // uni.showToast({
      //   title: curText + '/' + this.isEdit + '/' + uni.getSystemInfoSync().platform,
      //   icon: 'none',
      //   image: '',
      //   duration: 1500,
      //   mask: false,
      // })
    }
  }
}

uni复制功能只支持app、小程序,怎么兼容H5

当H5时引导用户自己选择后copy,如果是app调用uni的api

copy() {
  // #ifdef H5
  prompt('复制失败。请选中下列微信号,手动复制', this.copyInfo)
  // #endif

  // #ifdef APP-PLUS
  uni.setClipboardData({
    data: this.contact,
    success: function () {
      uni.showToast({
        title: '复制成功',
        icon: 'none',
        image: '',
        duration: 1500,
        mask: false,
      })
    }
  });
  // #endif
}

2020/05/01 周五

怎么看chrome浏览器更新记录及内容

最近发现办公电脑的chrome浏览器console里不支持 ?? 运算符,而我自己的电脑就可以,对比了下版本,我的是最新的81版本,而办公电脑还是71的版本,于是我就想看看chrome每次版本的更新记录,这个貌似要翻墙,我用了一个开源的chrome访问助手,找到了对应的位置

Web Updates (2020) | Google Developers”

这里有介绍每次chrome的更新记录,按月份来,以4月的 Chrome 81来讲 New in Chrome 81 介绍了对应的改动,比如

  • I’ve got an update on the adjusted Chrome release schedule.
  • App Icon Badging graduates from its origin trial.
  • Hit testing for augmented reality is now available in the browser.(WebXR hit testing)
  • Web NFC starts its origin trial.
  • And more.

感觉发现了新大陆,web还可以操作NFC… 对于了解一些新的技术,是很有必要看看这些的,顺便练习下英语

为什么要写单元测试

昨天04/30日发版,持续到到今天凌晨2点左右,测试发现有个bug:时间区间组件DatePicker前面一个时间没有显示,而这里应该显示最近一周的时间区间,现在只显示了后面一个时间。

但测试环境测试、UAT测试都是过了的,怎么突然就有问题了呢?于是看同事的代码定位问题,发现根据当前时间计算最近一周的日期逻辑有问题,只是简单粗暴的把 day 减了 6 天,之前一直是4月中下旬,大于6,所以没有出问题,这次正好是5月1号, 1 - 6 就是 -5 了, 时间拼接为 2020-05-0-5,这就导致有bug了。还好今天是5月1号,不然测不出这个bug就会导致后面生成环境的bug了。

从发现问题到定位问提、解决问题,大概用了5-10分钟左右。最后用当前时间戳 - 6 24 3600 * 1000 来解决。

我们一般在写程序时,很难发现自己逻辑上的bug,假设我们这里有写单元测试,考虑了很多种情况,那就可以避免这个问题。但现实是,我们目前大部分人都没有这个习惯,只要测试过了,就基本以为没问题了。但对于那种测试都测试不出来的在特定时间才会触发的bug,真的很可能造成线上bug,完全依赖于个人写代码时的逻辑严谨性。

怎么让自己写的代码更严谨,出问题的几率更小呢?那就是写单元测试。这样我们会考虑更多的特殊场景、而不是靠人肉测试。

WebSocket的使用场景

WebSocket是HTML5开始提供的一种在单个TCP连接上进行全双工通讯的协议。之前在工作中基本没用到过,今天偶然看到一个网站,他里面列出了WebSocket的几种使用场景,如下:

  • 在线多人点菜
  • 远程画版同步
  • 在线选座
  • 游戏 (只要涉及到多人对战、协同的就需要用到)
  • 扫码登录/支付
  • IM 聊天

而且还可以在线体验,还不错,体验地址:https://www.goeasy.io/cn/demos/demos.html