在 vue 项目开发中,很容易产生一些问题,比如代码重复、繁杂等,其实 Vue 项目开发中有很多技巧可以使用,本文将列出一些简单且很好用的几个技巧,帮助我们写出漂亮的代码。用到的技术栈是 Vue2.0 + TypeScript + vue-property-decorator + ElementUI。将用到以下几个技巧:
使用 $attrs 和 $listeners 进行多层级的数据和事件传递
实现数据的双向绑定,方便维护代码
使用 provide / inject 快速获得祖先组件数据
利用 Mixins 提高代码的可维护性
使用动态组件去懒加载组件
在组件作用域内的 css 中使用 ::v-deep 修改组件样式
使用装饰器优化代码
利用 require.context 去获取项目目录信息
先聊聊如何传递 Prop,可以分为静态和动态的 Prop:
<!-- 静态的prop -->
<blog-post title="My journey with Vue"></blog-post>
<!-- 动态的prop -->
<blog-post v-bind:title="post.title"></blog-post>
<!-- 动态的prop传递可以简写成 -->
<blog-post :title="post.title"></blog-post>
<!-- 需要传递多个prop的时候,可以一起写在v-bind上 -->
<blog-post v-bind="{ editable, title: post.title}"></blog-post>
了解了 Props 的传递方式,在看看官方文档是怎么定义 $attrs 的, 在尤大大的文档中这样介绍了 $attrs:
$attrs: 包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件
$attrs 包含了传入到父作用域中没有在 props 声明的其他 props,因此我们可以用 $attrs 去代替那些父组件中不需要的而子组件需要的 props, 通过 v-bind="$attrs" 统一传递给后代。这样就避免了一个个声明然后再一个个传递。
<blog-post v-bind="$attrs"></blog-post>
上面这一行代码就通过 v-bind="$attrs" 的方式将本作用域中不作为 prop 的其他属性传递给了 blog-post 组件。
父组件通过 $attrs 传递给后代组件后,后代组件如果想通过触发事件来更新父组件状态该如何处理?如果一级一级地往上 emit 事件,会不会弄得代码太繁琐复杂了?在 Vue 中可以通过 $listeners 解决这个问题,先看看官方文档关于 $listeners 的说明:
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。
文档中说了 $listeners 包含了父作用域中的事件监听器。意思就是 $listeners 表示了父组件中的事件监听器集合,只要是触发父组件的事件,而不是自己的,就可以用一个 v-on="$listeners"表示。
<!-- 父组件(第一层组件) -->
<componentA @on-change="handleChange" v-bind="{ editable, title: post.title}" />
<!-- 中间层的组件 -->
<Child v-bind="$attrs" v-on="$listeners"/>
<!-- 数据传递的目标组件,事件触发的组件 -->
<div @click="handleClick">{{ title }} </div>
<script>
export default {
props: {
title: String
}
handleClick() {
this.$emit('on-change', 'New Title');
}
}
</script>
上面的代码示例中,中间层的组件内通过 v-bind="$attrs" 将其余的 Prop 传递给了 Child 组件,再通过 v-on="$listeners" 绑定父作用域中的事件监听器,一旦 emit 就会传给了父组件。
有很多这样的场景,父组件需要传递数据给子组件,且在子组件触发数据更新的时候,马上反馈给父组件,父组件数据更新,单向数据流向子组件,最后子组件更新。通常情况用 props + $emit 的方式去更新状态,但是这种处理方式有点笨拙,且不易维护,所以可以通过实现数据的“双向绑定”来提高代码的可维护性。可以通过这以下方式去实现:
在 v-bind prop的时候添加 .sync 修饰符,赋新值的时候用 this.$emit('update:propName', newValue)
<!-- .sync是 v-on:update这种模式的一种缩写 -->
<Child v-on:update:title="title" />
<!-- 相当于 -->
<Child :title.sync="title" />
如果要更新上述代码中的 title 值,只需要 this.$emit('update:title', '新标题'),完成数据更新。
model 是2.2.0+ 新增的选项,一个组件上的 v-model 默认会利用名为 value 的 Prop 和名为 input 的事件, 而 model 选项可以规定 Prop 名称和事件名称来实现 v-model,好处是在实现 v-model 的同时也避免了 Prop 和事件名的冲突。
<!-- 父组件 -->
<Model v-model="checked"/>
<!-- Model组件 -->
<div @click="handleClick">
<p>自定义组件的 v-model</p>
checked {{checked}}
</div>
<script lang="ts">
export default {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
methods: {
handleClick() {
this.$emit('change', !this.checked);
}
}
在上述代码中,只需要在 model 选项中添加 prop 和 event,就可以实现了 v-model。而在 vue-property-decorator 中提供了 Model 的装饰器,需要这么写:
@Model('change', { type: Boolean }) readonly checked!: boolean
handleClick() {
this.$emit('change', !this.checked);
}
只需要通过 .sync 和 model 就可以实现数据的“双向绑定”,这样书写代码可以一定程度上减少我们的代码,而且另代码变得更优雅且可维护。
在项目实战中,经常也遇到这种情况,就是组件层级很深,子孙组件和祖先组件需要数据的交流和通信。一旦逻辑比较复杂,数据就很难维护,而 Vue 提供了的 Provide / Inject 选项可以处理这种问题,在官方文档中是这样描述 Provide / Inject 的:
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。
从描述中可以得知,只要祖先组件使用 provide,其子孙组件都可以通过 inject 去得到。
// 祖先组件 provide
provide() {
return {
foo: 'bar',
};
},
// 后代只需要 inject 就可以获得此数据
inject: ['foo']
这样就能轻松地获取祖先组件数据,而在 Ts 项目中是使用 vue-property-decorator 提供的 Provide 和 Inject 、Providereactive 和 InjectReactive 装饰器。
实际上 vue-property-decorator 提供的 Provide / Inject 也是不具备响应式的,他们的区别如下面的表格所示:
\ | 单向数据流 | 响应式 |
---|---|---|
Provide / Inject | × | × |
ProvideReactive / InjectReactive | √ | × |
代码中的使用如下:
// 祖先组件
@Provide()
parentName = 'parent';
@ProvideReactive()
parentNumber = 1;
handleChangeParentName() {
this.parentName = 'parent2';
this.parentNumber++;
}
// 子孙组件
@Inject() parentName: string;
@InjectReactive() parentNumber: number;
使用 Provide / Inject ,在数据更新的时候,并不会让后代组件 inject 的数据也同步更新,而 ProvideReactive / InjectReactive 是可以的,这是两者的区别。提到数据传递又想到了响应式,响应式的数据作用太大了。那么如何将 Provide / Inject 传输的数据也做成响应式的?而 Vue 提供了这样的方法:
提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
从提示可知,只需要 provide 一个可监听对象,那么通过 Provide / Inject 传递的数据也具备响应式。
// 祖先组件: 用Provide 提供了一个响应式对象 obj
@Provide()
obj = {
currentName: '1',
initData: this.initData,
};
initData() {
console.log(this.obj.currentName);
}
在后代组件,就可以通过 inject 获取这个 obj,而这个对象是响应式的,直接改变 obj,会改变祖先组件的 obj。这样就可以更新祖先组件的数据,或者执行祖先组件的函数。
@Inject() obj:any;
handleChangeParentName() {
console.log(this.obj.currentName); // 1
this.obj.currentName = '2';
this.obj.initData(); // 2
}
Provide / Inject 能非常方便地处理层级深的组件间数据的传递和共享,如果层级越深,数据越不好维护,Provide / Inject 可以帮我们解决这个问题,但是 Vue 不建议在业务中使用,所以尽量在组件中使用。
相信大部分使用Vue的人都对 Mixins 不陌生,Mixins 作用很多,可以利用它去抽取公共代码加强代码复用,也可以抽取功能代码提高可维护性,比如在表格页面中经常重复使用分页的方法,我们应当抽取出来,这里讲讲如何在 TS 项目中使用。
首先写一个公共的 mixin 文件, 把高复用的状态和函数写进去。
export default class CommonMixins extends Vue{
public paginations = {
pageSize: 20,
total: 0,
currentPage: 1,
}
handleChangePageSize (pageSize: number, cb: Function) {
this.paginations.pageSize = pageSize;
cb();
}
handleChangePageNum (currentPage: number, cb: Function) {
this.paginations.currentPage = currentPage;
cb();
}
}
vue-property-decorator 提供了 Mixins 的装饰器,在业务页面中引入 Mixin 只需要往里 Mixins 传入 , 可以传多个,表示混入多个 Mixin.
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator';
import CommonMixins from "./common-mixin";
import PermissionMixins from "./permission-mixin";
@Component({})
export default class Parent extends Mixins(CommonMixins, PermissionMixins) {
}
</script>
如果只需要一个的话,也可以直接继承
<script lang="ts">
import { Component, Mixins } from 'vue-property-decorator';
import CommonMixins from "./common-mixin";
@Component({})
export default class Parent extends CommonMixins {
}
</script>
我们可以利用 Mixin 抽离代码,想办法将项目瘦身,提高代码可维护性。
组件在加载都是同步的,但当页面内容很多,有些组件并不需要一开始就加载出来的比如弹窗类的组件,这些就可以用动态组件,当用户执行了某些操作后再加载出来,这样可以提高主模块加载的性能, 在 Vue 中可以使用 component 动态组件, 依 is 的值,来决定哪个组件被渲染。
<template>
<div>
主页面 <br/>
<button @click="handleClick1">点击记载组件1</button><br/>
<button @click="handleClick2">点击记载组件2</button><br/>
<component :is="child1"></component>
<component :is="child2"></component>
</div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component({})
export default class AsyncComponent extends Vue {
public child1:Component = null;
public child2:Component = null;
handleClick1() {
this.child1 = require('./child1').default;
}
handleClick2() {
this.child2 = require('./child2').default;
}
}
</script>
示例代码中,只有当点击的时候才会去加载组件。component还可以配合v-show去控制显示和隐藏,这样这个component只会mounted一次,优化性能。
有很多场景想更改ui组件样式,然后怕影响别人的使用,加上 scoped 后又不能生效,可以使用 ::v-deep 深度作用选择器去修改组件作用域内的 CSS 的样式。在 CSS 中我们可以使用 >>> 操作符,但在预处理器中的写法就要用 /deep/ 或 ::v-deep。
<style scoped>
>>> .ivu-tabs-tabpane {
background: #f1f1f1;
}
</style>
<style lang="scss" scoped>
/deep/ .ivu-tabs-tabpane {
background: #f1f1f1;
}
</style>
<style lang="scss" scoped>
::v-deep .ivu-tabs-tabpane {
background: #f1f1f1;
}
</style>
::v-deep 和 /deep/ 作用是一样的,但不推荐使用 /deep/, 在 Vue3.0 中将不支持 /deep/ 这种写法。
装饰器增加了代码的可读性,清晰地表达了意图,而且提供一种方便的手段,增加或修改类的功能,比如给类其中的方法提供防抖的功能。
import debounce from 'lodash.debounce';
export function Debounce(delay: number, config: object = {}) {
return (target: any, prop: string) => {
return {
value: debounce(target[prop], delay, config),
};
};
}
这样的好处是使用起来非常方便,另外增加了代码的可读性。
@Debounce(300)
onIdChange(val: string) {
this.$emit('idchange', val);
}
关于 require.context,webpack 文档是这么描述的:
可以给这个函数传入三个参数:一个要搜索的目录,一个标记表示是否还搜索其子目录, 以及一个匹配文件的正则表达式。
webpack 会在构建中解析代码中的 require.context() 。如果想引入一个文件夹下面的所有文件,或者引入能匹配一个正则表达式的所有文件,这个功能就会很有帮助
根据这个提示,我们可以引用到一个文件夹下面的所有文件,由此可以利用获取的文件信息去做一些操作,比如在注册组件的时候,原本我们注册组件的时候需要一个个引入并且一个个注册,而且后面想加新的,又要再写上,现在可以通过 require.context 去优化这一段代码。
// import WmsTable from './wms-table/table/index';
import Table from './table/index.vue';
import CustomHooks from './custom-hooks/custom-hooks-actions/index';
import SFilter from './s-filter/filter-form';
import WButton from './button/index';
import CreateForm from './createForm/create-form/CreateForm.vue';
import Action from './table/action-table-column.vue';
import DetailItem from './detail-item.vue';
Vue.component('w-filter', SFilter);
Vue.component('w-button', WButton);
Vue.component('custom-hooks', CustomHooks);
Vue.component('create-form', CreateForm);
Vue.component('w-table', Table);
Vue.component('w-table-action', Action);
Vue.component('zonetime-date-picker', ZonetimeDatePicker);
Vue.component('detail', DetailItem);
注册全局组件的时候,不需要一个一个import,和一个个去注册,使用 require.context 可以自动导入模块,这样的好处在于,当我们新建一个组件,不用自己再去手写注册,而在一开始就帮我们自动完成。
const contexts = require.context('./', true, /\.(vue|ts)$/);
export default {
install (vm) {
contexts.keys().forEach(component => {
const componentEntity = contexts(component).default;
if (componentEntity.name) {
vm.component(componentEntity.name, componentEntity);
}
});
}
};
本文介绍了在 Vue 实战中经常用到的一些技巧,这些技巧的目的都是为了提高代码的可维护性、可读性。总结了 Vue 中可以方便我们在实际开发中可以运用到技巧,帮助我们写出高可用,可读性高的漂亮代码。
一直以来进行了比较多的微信小程序开发... 总会接触到一些和官方组件或 api 相关或其无法解决的需求,于是决定在这里小小的整理一下微信小程序开发的一些技巧
小程序提供onShareAppMessage 函数,此函数只支持分享给我微信朋友,小程序如何分享到朋友圈呢?使用canvas绘制一张图片,并用wx.previewImage预览图片,然后长按图片保存图片到手机。
前端新手程序员不知道的 20个小技巧:作为前端开发者,使用双显示器能大幅提高开发效率、学编程最好的语言不是PHP,是English、东西交付之前偷偷测试一遍、问别人之前最好先自己百度,google一下、把觉得不靠谱的需求放到最后做,很可能到时候需求就变了...
本地的 IP 地址是分配给你计算机上的内部硬件或虚拟网卡的本地/私有 IP 地址。根据你的 LAN 配置,上述 IP 地址可能是静态或动态的。公共的 IP 地址是你的 Internet 服务提供商(ISP)为你分配的公共/外部 IP 地址。
使用 :not() 在菜单上应用/取消应用边框;给body添加行高;所有一切都垂直居中;逗号分隔的列表;使用负的 nth-child 选择项目;对图标使用SVG;优化显示文本;对纯CSS滑块使用 max-height;继承 box-sizing
禁用右键点击;禁用搜索文本框;新窗口打开链接;检测浏览器;预加载图片;样式筛选;列高度相同;字体大小调整;返回页面顶部;获取鼠标的xy坐标;验证元素是否为空;替换元素
为你网站的用户留下良好的第一印象是非常必要的。随着商业领域的竞争,拥有一个吸引人的网站可以帮助你脱颖而出。研究表明,如果加载时间超过3秒,会有 40% 的用户放弃访问你的网站
清除浮动主要用于子元素浮动(float)之后,父元素无法撑起高度和宽度。文字少时居中,多时靠左因为div嵌套着p,所以p的尺寸并不会超过div。但是要注意,当p的内容为英文单词组成的时候
这次我们主要来分享11个在日常教程中不常被提及的JavaScript小技巧,他们往往在我们的日常工作中经常出现,但是我们又很容易忽略。Set类型是在ES6中新增的,它类似于数组,但是成员的值都是唯一的
为什么要在JavaScript里写CSS?避免命名全局污染,条件和动态样式(比如选择主题色之类的),在框架层面进行限制或补充(比如补全供应商前缀),避免业务人员使用奇技淫巧
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!