Vue项目开发中一些常用实用的小技巧:
场景 1:开发vue项目的时候我们写了一堆基础UI组件,然后每次我们需要使用这些组件的时候,都得先import,然后声明components注册这些组件,这样的话如果我们封装了50个组件,那么就意味我们要写50个相同的组件引入语句,50个注册语句,大量的重复代码:
main.ts
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import store from './store/index';
import AppHeader from '@/components/app-header/app-header.vue';
import AppButton from '@/components/app-button/app-button.vue';
import AppModal from '@/components/app-modal/app-modal.vue';
import AppInput from '@/components/app-input/app-input.vue';
Vue.component('app-header', AppHeader);
Vue.component('app-button', AppButton);
Vue.component('app-modal', AppModal);
Vue.component('app-input', AppInput);
【优化解析】:如果遇到从一个文件夹引入很多模块的情况,我们可以借助一下神器webpack的一个api→ require.context() 方法来创建自己的(模块)上下文,它会遍历文件夹中的指定文件,然后自动导入,从而实现自动动态require组件。
语法:
/**
* directory {String} -需要检索的文件目录
* useSubdirectories {Boolean} -是否遍历文件的子目录
* regExp {RegExp} - 匹配文件的正则
**/
require.context(directory, useSubdirectories = false, regExp = /^.//);
【使用】
(1)我们在components文件夹添加一个叫global.ts的文件,在这个文件里借助webpack动态将需要的基础组件统统打包进来。
import Vue from 'vue'
export function capitalizeFirstLetter(val: string) {
return val.charAt(0).toUpperCase() + val.slice(1)
}
//找到components文件夹下以.vue命名的文件
export const files = require.context('@/components', true, /\.vue$/);
this.getComponents();
// 针对3中不同的组件设计模式提供不同的组件注册策略
export function getComponents() {
this.files.keys().forEach(path => {
const type = this.getType(path, this.files(path));
switch (type) {
case 'component':
this.installComonent(Vue, this.files(path));
break;
case 'componentGroup':
this.instalGroupComponent(Vue, this.files(path));
break;
case 'serveApi':
this.installServeApi(Vue, this.files(path));
break;
}
});
}
getType(path, file) {
if (path.lastIndexOf('.ts') === -1) {
return 'component';
} else {
if (file.default.install) {
return 'serveApi';
} else {
return 'componentGroup';
}
}
}
instalGroupComponent(Vue, file) {
const components = file.default;
Object.keys(components).forEach(name => {
Vue.component('Nb' + name, components[name]);
});
}
installComonent(Vue, file) {
const options = file.default;
const name = options.name;
Vue.component('Nb' + name, options);
}
installServeApi(Vue, file) {
Vue.use(file.default);
}
// 最简单的使用
this.files.keys().forEach(fileName => {
const componentConfig = this.files(fileName)
const componentName = capitalizeFirstLetter(
fileName.replace(/^\.\//, '').replace(/\.\w+$/, '')
//因为得到的filename格式是: './baseButton.vue', 所以这里我们去掉头和尾,只保留真正的文件名
)
Vue.component(componentName, componentConfig.default || componentConfig)
})
(2)最后我们在main.ts中import 'components/global.js',然后我们就可以随时随地使用这些基础组件,无需手动引入了。
场景 2:组件在创建的时候我们会通过接口获取一次列表,同时监听input框,每当发生变化的时候重新获取一次列表。
【解析】可以使用@Watch() 中的immediate属性 立即执行
@Watch
接受第一个参数为要监听的属性名 第二个属性为可选对象。@Watch
所装饰的函数即监听到属性变化之后的操作.
有时候我们可能需要监听整个对象,例如我们针对一个对象中的某个属性进行监听,但是这个对象中有很多属性,任何一个属性的改变我们都需要重新发起请求,那么我们可以利用watch的deep属性进行深度监听,也就是监听复杂的数据类型。
immediate:true表示创建组件时立马执行一次。
import {Vue, Component, Watch} from 'vue-property-decorator';
可以直接利用 watch 的immediate和handler属性简写
@Watch('nameInput', { immediate: true, deep: true })
getData(newVal: Person, oldVal: Person){
// todo...
}
场景 3:在使用Vue
进行开发时,我们经常看到有些组件有些重复的 js 逻辑,这部分逻辑我们可以使用mixins(混合)来实现,有两种mixins
的方法。
(1)是vue-class-component
提供的
//定义要混合的类 mixins.ts
import Vue from 'vue';
import Component from 'vue-class-component';
@Component // 一定要用Component修饰
export default class myMixins extends Vue {
value: string = "Hello"
}
___________________________________________________________
// 引入
import Component {mixins} from 'vue-class-component';
import myMixins from 'mixins.ts';
@Component
export class myComponent extends mixins(myMixins) {
// 直接extends myMinxins 也可以正常运行
created(){
console.log(this.value) // => Hello
}
}
(2)是在@Component
中混入
// 定义 mixins.ts
import { Vue, Component } from 'vue-property-decorator';
declare module 'vue/types/vue' {
interface Vue {
value: string;
}
}
@Component
export default class myMixins extends Vue {
value: string = 'Hello'
}
_________________________________________________________________
//引入mixin
import { Vue, Component, Prop } from 'vue-property-decorator';
import myMixins from '@static/js/mixins';
@Component({
mixins: [myMixins]
})
export default class myComponent extends Vue{
created(){
console.log(this.value) // => Hello
}
}
场景 3: 多个过滤器全局使用
// 多个过滤器全局注册
//定义filter.ts //src/common/filters.ts
let dateFormat = value => value.replace(/(\d{4})(\d{2})(\d{2})/g, '$1-$2-$3')
export { dateFormat }
//使用 /src/main.ts
import * as custom from './common/filters/custom'
Object.keys(custom).forEach(key => Vue.filter(key, custom[key]))
场景 4: 在使用Vue开发项目中我们经常用scoped属性表示它的样式只作用于当前模块,是样式私有化。
scoped渲染的规则/原理:
给HTML的DOM节点添加一个 不重复的data属性 来表示 唯一性;
在对应的 CSS选择器 末尾添加一个当前组件的 data属性选择器来私有化样式。
【问题】:使用了scoped属性后,我们组件内部的样式无法在外部被控制,(怎么都改不了样式)
【解决方案】:deep属性
.result-tab /deep/ {
.nav-bar {
a {
flex: initial;
height: 50px;
font-size: 36px;
margin-left: 40px;
line-height: 100px;
height: auto;
}
}
.result-content {
padding: 0 40px;
}
}
场景 5:Modal弹框大体结构固定,只是中间内容有所变化,这种情况下就可以用到插槽。
通过slot可以向组件内部指定位置插入内容,通过slot可以实现父子传参
【使用】:
子组件:
// 父组件
// 父组件使用子组件传过来的user属性, slotProps可以为任意名
{
{ slotProps.user.name }}
{
{ slotProps.user.jobTitle }}
{
{ 'my.item.logout.msg' | translate}}
{
{ 'common.confirm' | translate }}
场景 6:比如一个需求是从/post-page/a,跳转到/post-page/b。然后我们惊人的发现,页面跳转后数据竟然没更新?!
【原因】vue-router”智能地”发现这是同一个组件,然后它就决定要复用这个组件,所以你在created函数里写的方法压根就没执行。通常的解决方案是监听$route的变化来初始化数据。
【优化】可以给router-view添加一个unique的key,这样即使是公用组件,只要url变化了,就一定会重新创建这个组件。(虽然损失了一丢丢性能,但避免了无限的bug)。同时,注意我将key直接设置为路由的完整路径,一举两得。
场景 7:实现动态换肤(比如 默认情况css会传递一个默认主题色给js, 当将用户选择某个主题色时再传给css)
js将变量值传给scss, 可以使用 css var()
实现
:root {
--main-bg-color: coral;
}
#div1 {
background-color: var(--main-bg-color);
}
#div2 {
background-color: var(--main-bg-color);
}
scss变量传给js(可以通过 css-modules :export
来实现)
// variable.scss
$theme: blue;
:export {
theme: $theme;
}
// test.ts
import variable from '@/styles/variable.scss'
console.log(variable.theme) // blue
场景 8:平时写业务的时候,免不了会对第三方组件进行二次封装,比如我们基于el-input组件进行封装,这样的话需要传入很多个@Prop 或者传多个$Emit
【优化】我们可以使用v-bind="$attrs"
:传递所有属性;v-on="$listeners"
传递所有方法
场景 9:当需要在子组件中修改父组件值的时候可以用(.sync)
等同于以下:
itemData = val">
this.$emit('update:data', newVal)
场景 10: 避免一些全局操作:
(1)在组件内选择节点的时候一定要切记避免使用 document.querySelector()等一系列的全局选择器。
应该使用this.$el或者this.refs.xxx.$el的方式来选择 DOM。
这样就能将你的操作局限在当前的组件内,能避免很多问题。
(2)经常会不可避免的需要注册一些全局性的事件,
比如监听页面窗口的变化 window.addEventListener('resize', this.__resizeHandler),
但再声明了之后一定要在 beforeDestroy或者destroyed生命周期注销它。
window.removeEventListener('resize', this.__resizeHandler)避免造成不必要的消耗。
(3)避免过多的全局状态,不是所有的状态都需要存在 vuex 中的,应该根据业务进行合理的进行取舍。
如果不可避免有很多的值需要存在 vuex 中,建议使用动态注册的方式。
只是部分业务需要的状态处理,建议使用 Event Bus或者使用 简单的 store 模式。
(4)css 也应该尽量避免写太多的全局性的样式。除了一些全局公用的样式外,所以针对业务的或者组件的
样式都应该使用命名空间的方式或者直接使用 vue-loader 提供的 scoped写法,避免一些全局冲突。
场景 11:组件中注册一些全局事件的时候,都需要在mounted
中声明,在destroyed
中销毁,但由于这个是写在两个生命周期内的,很容易忘记,如果你在组件摧毁阶段忘记移除的话,会造成内存的泄漏,而且都不太容易发现。
【解决】使用Hook
mounted() {
window.addEventListener('resize', this.handleResize())
}
beforeDestory() {
window.removeEventListener('resize', this.handleResize())
}
修改后:
mounted() {
window.addEventListener('resize', this.handleResize());
this.$on('hook:destroyed', () => {
window.removeEventListener('resize', this.handleResize())
})
}