# 2020年05月技术日常
# 2020/05/28 周四
# node package.json中版本前的 ~ 与 ^ 分别代表什么
来看看element ui的package.json,其中async-validator是 ~ 开头,而其他都是 ^ 开头,有什么区别呢?
"dependencies": {
"async-validator": "~1.8.1",
"babel-helper-vue-jsx-merge-props": "^2.0.0",
"deepmerge": "^1.2.0",
"normalize-wheel": "^1.0.1",
"resize-observer-polyfill": "^1.5.0",
"throttle-debounce": "^1.0.1"
},
版本格式 1.8.1 对应 major.minor.patch
- major:表示版本有了一个大更改。
- minor:表示增加了新的功能,并且可以向后兼容。
- patch:表示修复了bug,并且可以向后兼容。
~:他会更新到当前minor version(也就是中间的那位数字)中最新的版本,也就是只变动patch到最新版本,它不会自动更新minor版本, 波浪符号是曾经npm安装时候的默认符号,现在已经变为了插入符号。
^:这个符号就显得非常的灵活了,他将会把当前库的版本更新到当前major version(也就是第一位数字)中最新的版本。也就是更新minor到最新版本,他不会自动更新major版本。
当我们使用最新的Node运行'npm instal --save xxx',的时候,会优先考虑使用插入符号(^)而不是波浪符号(~)了。
可以手动安装指定版本
# 安装最新版本
npm instlal xxx
# 默认情况下,会安装最新版本npm包,等价于
npm install xxx@latest
# 安装指定版本
npm install xxx@[指定版本号]
# 安装未来版本,未正式发布的beta版本
npm install xxx@next
参考:Node.js中package.json中库的版本号详解(^和~区别) (opens new window)
# vue element表单组件简单实现
先来写一个调用示例,把el-前缀换成z-,然后我们需要实现z-form, z-form-item, z-input组件
<template>
<!-- /elementForm -->
<div>
<z-form ref="ruleForm" v-model="form" :rules="rules">
{{ form }}
<z-form-item label="姓名" prop="name">
<z-input v-model="form.name" placeholder="请输入姓名"></z-input>
</z-form-item>
<z-form-item label="电话" prop="mobile">
<z-input v-model="form.mobile" placeholder="请输入电话"></z-input>
</z-form-item>
<button @click="submitForm('ruleForm')">提交</button>
<button @click="resetForm('ruleForm')">重置</button>
</z-form>
</div>
</template>
<script>
export default {
components: {
ZInput: () => import("./ZInput"),
ZFormItem: () => import("./ZFormItem"),
ZForm: () => import("./ZForm")
},
data() {
return {
form: {
name: "",
mobile: ""
},
rules: {
name: [
{ required: true, message: "请输入姓名", trigger: "blur" },
{ min: 3, max: 5, message: "长度在 3 到 5 个字符", trigger: "blur" }
],
mobile: [{ required: true, message: "请输入电话", trigger: "change" }]
}
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate(valid => {
console.log("valid", valid);
if (valid) {
alert("submit!");
} else {
console.log("error submit!!");
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
}
}
};
</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源码 (opens new window),我这里简单说几个区别
- element使用的表单校验 async-validator是 1.x.x 版本,而上面的示例需要使用的版本 3.x.x版本
- Form组件里provide,上面只为form绑定了rules和props,element中指定绑定了当前this
- 事件的监听和触发,这里使用的是$parent来处理,element里面通过 this.dispatch事件来触发
- element form支持很多参数,这里只是简单的模拟,element里面会复杂很多。
完整demo参见 element form实现 - fedemo | github (opens new window)
# 2020/05/27 周三
# tabs标签页组件的坑
当使用tabs组件,特别是同一个组件可能会打开多个tab时。需要注意
- 组件打开一次后,created已执行,再打开一个tab时,不会触发created或mounted,需要用watch监听prop传值的改变进行一些请求接口的初始化操作,如果组件还有子组件,也需要这样做,防止数据不刷新的问题
- 点击一个tab后,如果请求比较慢,再点击另一个tab,数据可能会乱,注意tab切换时,取消发出的请求
- 仔细检查同组件不同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里点击断点位置,选择遇到错误时停止,这样出现问题就会自动跳转到对应的位置
# 2020/05/20 周三
# sessionStorage新打开一个tab页就失效的问题
首页我们要知道3点:
- sessionStorage在浏览器的两个tab页之前是无法共享的,一个tab页中sessionStorage修改后,不能触发其他tab页的storage事件
- 当前tab页的localStorage修改是无法触发当前页的storage事件的,他会触发其他tab页的storage事件
- localStorage的共享,只发生在同源的地址里。非同源无法共享localStorage
怎么将就页面的sessionStorage传递到新开的tab页呢?
由于sessionStorage打开新tab页默认会丢失。那新开tab页的sessionStorage就是空的。我们可以判断,如果sessionStorage.length值为0,那么就是新开的页面。这时我们通过设置一个localStorage字段的值,触发之前打开页面的Storage事件,在这个事件里我们将当前页面的sessionStorage通过localStorage设置值,来触发新页面的Storage事件,把sessionStorage传递到新的页面
下面是部分核心代码,详细demo参见 github demo地址 (opens new window)
<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;
参考
- 新开一个tab页,页面sessionStorage失效的问题 (opens new window)
- storage事件 JS高程3笔记 (opens new window)
- storage demo示例,同时在两个tab页中打开该页面,console里设置localStorage试试 (opens new window)
# 2020/05/19 周二
# 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 在线体验地址 (opens new window),demo github源码 (opens new window)
<!-- 图片默认的 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>
参考:
- HTML 拖放 API - Web API 接口参考 | MDN (opens new window)
- dataTransfer.setData无效,drop不触发的问题 (opens new window)
- 火狐drop后会打开新tab的问题 (opens new window)
- js 拖动后,怎么保持原来的元素不消失,drop后拖动元素消失的问题 (opens new window)
- cloneNode | JS高程3笔记 (opens new window)
扩展:
# 2020/05/17 周日
# 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数据类型,为什么要新增这个数据类型? (opens new window)
# 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 (opens new window)
# 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
// child props: { msg: string } // parent <hello-word msg="xxxx" />
引用refs
// 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 祖先给后代传值
// 祖先组件 provide() { return { foo: 'foo'} // 要像data一样,用函数包裹 } // 后代组件 inject: ['foo']
- dispatch:后代给祖先传值
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', '要设置的值')
}
}
}
参考:
# 2020/05/11 周一
# 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 (opens new window)
# 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 (opens new window)
# 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" (opens new window)
这里有介绍每次chrome的更新记录,按月份来,以4月的 Chrome 81来讲 New in Chrome 81 (opens new window) 介绍了对应的改动,比如
- 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