编写可复用性的模块
在生活中,重复的机械劳动会消耗我们的时间和精力,提高生产成本,降低工作效率。同样,在代码世界中,编写重复的代码会导致代码的冗余,页面性能的下降以及后期维护成本的增加。由此可见将重复的事情复用起来是提高生产效率、降低维护成本的不二之选。
在 Vue 项目中,每一个页面都可以看作是由大大小小的模块构成的,即便是一行代码、一个函数、一个组件都可以看作是一个个自由的模块。那么提高代码的复用性的关键便在于编写可复用的模块,也就是编写可复用的代码、函数和组件等。
一个简单的例子
let person = [];
for (let i = 0; i < data.obj.items.length; i++) {
person.push({
name: data.obj.items[i].name,
age: data.obj.items[i].age
});
}
不知道上方代码给你的第一印象是什么?总之给我的印象是糟糕的,因为出现了重复性的代码片段 data.obj.items
,可能这样的代码在我们团队开发中随处可见,这也说明了重复编码现象其实无处不在。
面对自己编写的代码,我们应该保持一颗去重的心,发现重复的地方就相当于找到了可以复用的模块。在不复用的情况下,上述代码一旦需要修改变量 items
为 lists
,那么我们就得修改 3 处地方,不知不觉就增加了维护成本。而到时候往往修改你代码的人并不是你自己,所以对自己好点,对他人也会好点。复用后的代码如下:
let person = [];
let values = data.obj.items;
for (let i = 0; i < values.length; i++) {
person.push({
name: values[i].name,
age: values[i].age
});
}
我们通过将 data.obj.items 的值赋值给变量 values 来实现了复用,此时修改 items
为 lists
的话我们只需修改一处地方即可,不管是维护成本还是代码可读性上,复用的优势都显而易见。
封装成一个函数
除了使用变量的赋值缓存使用来解决数据的重复读取外,我们在开发过程中重复性更多的也许是功能点的重复,比如:
<tempalte>
<div>
<input type="text" v-model="str1">
<input type="text" v-model="str2">
<div>{{ str1.slice(1).toUpperCase() }}</div>
<div>{{ str2.slice(1).toUpperCase() }}</div>
</div>
</template>
上述代码的重复功能点在于截取输入框中第二个字符开始到最后的值并把它们转化成大写字母,像这样很简单的操作虽然重复使用也不会出现太大的问题,但是如果是代码量较多的操作呢?重复书写相同功能的代码是一种不经过大脑思考的行为,我们需要对其进行优化,这里我们可以把功能点封装成一个函数:
export default {
methods: {
sliceUpperCase(val) {
return val.slice(1).toUpperCase()
}
}
}
如此我们只要在用到该方法的地方调用即可,将值传入其中并返回新值。
当然像在双花括号插值和 v-bind 表达式中重复的功能点我们可以封装成过滤器:
仅做了解,在 Vue 3 中,过滤器被移除了,使用方法或计算属性可以代替它。
// 单文件组件注册过滤器
filters: {
sliceUpperCase(val) {
return val.slice(1).toUpperCase()
}
}
// 全局注册过滤器
Vue.filter('sliceUpperCase', function (val) {
return val.slice(1).toUpperCase()
})
然后在 html 中使用“管道”符进行过滤:
<div>{{ str1 | sliceUpperCase }}</div>
<div>{{ str2 | sliceUpperCase }}</div>
封装成一个组件
相比较于函数的封装,规模更大一点的便是组件的封装,组件包含了模板、脚本以及样式的代码,在实际开发中组件的使用频率也是非常大的,我们项目中的每一个页面其实都可以看作是一个父组件,其可以包含很多子组件,子组件通过接收父组件的值来渲染页面,父组件通过响应子组件的回调来触发事件。
封装一个组件主要包含两种方式,一种是最常见的整体封装,用户通过改变数据源来呈现不同的页面状态,代码结构不可定制化。例如:
<div>
<my-component data="我是父组件传入子组件的数据"></my-component>
</div>
另一种便是自定义封装,也就是插槽(slot),我们可以开放一部分槽位给父组件,使其能够进行一定程度的定制化,例如:
<div>
<my-component data="我是父组件传入子组件的数据">
<template slot="customize">
<span>这是定制化的数据</span>
</template>
</my-component>
</div>
在 myComponent 组件中我们便可以接收对应的 slot:
<div class="container">
<span>{{ data }}</span>
<slot name="customize"></slot>
<div>
这里我们通过定义 slot 标签的 name 值为 customize 来接收父组件在使用该组件时在 template 标签上定义的 slot="customize" 中的代码,不同父组件可以定制不同的 slot 代码来实现差异化的插槽。最终渲染出来的代码如下:
<div>
<div class="container">
<span>我是父组件传入子组件的数据</span>
<span>这是定制化的数据</span>
</div>
</div>
这样我们就完成了一个小型组件的封装,将共用代码封装到组件中去,页面需要引入的时候直接使用 import 并进行相应注册即可,当然你也可以进行全局的引入:
import myComponent from '../myComponent.vue'
// 全局
Vue.component('my-component', myComponent)
写了一个完整的例题,方便大家理解
我们将创建一个名为 BaseButton.vue
的按钮组件,该组件支持自定义文本、颜色和点击事件。
在 Vue 项目中,创建一个新的组件文件 BaseButton.vue
:
<template>
<button
:class="buttonClass"
@click="handleClick"
:style="{ backgroundColor: bgColor, color: textColor }"
>
<slot>{{ text }}</slot>
</button>
</template>
<script>
export default {
name: 'BaseButton',
props: {
text: {
type: String,
default: 'Button',
},
bgColor: {
type: String,
default: '#42b983',
},
textColor: {
type: String,
default: '#ffffff',
},
},
computed: {
buttonClass() {
return {
'base-button': true,
'base-button--primary': this.bgColor === '#42b983',
'base-button--secondary': this.bgColor === '#35495e',
};
},
},
methods: {
handleClick(event) {
this.$emit('click', event);
},
},
};
</script>
<style scoped>
.base-button {
padding: 10px 20px;
border: none;
border-radius: 5px;
font-size: 16px;
cursor: pointer;
transition: background-color 0.3s;
}
.base-button:hover {
opacity: 0.9;
}
</style>
解释一下buttonClass
计算属性的作用
-
'base-button': true
:- 这个条目保证了所有按钮都将始终拥有
base-button
类。
- 这个条目保证了所有按钮都将始终拥有
-
'base-button--primary': this.bgColor === '#42b983'
:- 当
bgColor
的值为#42b983
时,计算属性返回true
,因此将base-button--primary
类添加到按钮的类列表中,下面的同理。
- 当
Vue 会将 buttonClass
的返回值解析为按钮的 CSS 类。这样,根据 bgColor
的变化,按钮的外观可以动态调整,以便于根据不同的背景颜色应用不同的样式。这种方法使得组件更加灵活、可维护且计算属性会自动缓存,只有在依赖的 bgColor
属性发生变化时才会重新计算,从而提高性能。
在你的主组件或其他组件中使用 BaseButton
组件。假设我们在 App.vue
中使用它
<template>
<div id="app">
<BaseButton
text="Primary Button"
bgColor="#42b983"
textColor="#ffffff"
@click="onButtonClick"
/>
<BaseButton
text="Secondary Button"
bgColor="#35495e"
textColor="#ffffff"
@click="onButtonClick"
>
Custom Slot Button
</BaseButton>
</div>
</template>
<script>
import BaseButton from './components/BaseButton.vue';
export default {
name: 'App',
components: {
BaseButton,
},
methods: {
onButtonClick(event) {
console.log('Button clicked!', event);
},
},
};
</script>
<style>
#app {
text-align: center;
margin: 20px;
}
</style>
-
属性:
BaseButton
组件接收三个 props:text
:按钮上的文本,默认为"Button"。bgColor
:按钮的背景颜色,默认为#42b983
。textColor
:按钮的文本颜色,默认为#ffffff
。
封装成一个插件
在某些情况下,我们封装的内容可能不需要使用者对其内部代码结构进行了解,其只需要熟悉我们提供出来的相应方法和 api 即可,这需要我们更系统性的将公用部分逻辑封装成插件,来为项目添加全局功能,比如常见的 loading 功能、弹框功能等。
Vue 提供给了我们一个 install 方法来编写插件,使用该方法中的第一个 Vue 构造器参数可以为项目添加全局方法、资源、选项等。比如我们可以给组件添加一个简单的全局调用方法来实现插件的编写:
/* toast.js */
import ToastComponent from './toast.vue' // 引入组件
let $vm
export default {
install(Vue, options) {
// 判断实例是否存在
if (!$vm) {
const ToastPlugin = Vue.extend(ToastComponent); // 创建一个“扩展实例构造器”
// 创建 $vm 实例
$vm = new ToastPlugin({
el: document.createElement('div') // 声明挂载元素
});
document.body.appendChild($vm.$el); // 把 toast 组件的 DOM 添加到 body 里
}
// 给 toast 设置自定义文案和时间
let toast = (text, duration = options.duration || 3000) => {
$vm.text = text;
$vm.duration = duration;
// 在指定 duration 之后让 toast 消失
setTimeout(() => {
$vm.isShow = false;
}, $vm.duration);
}
// 判断 Vue.$toast 是否存在
if (!Vue.$toast) {
Vue.$toast = toast;
}
Vue.prototype.$toast = Vue.$toast; // 全局添加 $toast 事件
}
}
成功编写完插件的 JS 脚本后,我们在入口文件中需要通过 Vue.use() 来注册一下该插件:
import Toast from '@/widgets/toast/toast.js'
Vue.use(Toast); // 注册 Toast
最后我们在需要调用它的地方直接传入配置项使用即可,比如:
this.$toast('Hello World', 2000);
当然你也可以不使用 install 方法来编写插件,直接采用导出一个封装好的实例方法并将其挂载到 Vue 的原型链上来实现相同的功能。
拓展
1.在 Vue 中如何添加全局自定义指令?
1. 创建全局自定义指令
全局自定义指令可以通过 Vue 实例的 directive
方法来注册。下面是一个示例,展示如何创建一个简单的自定义指令,该指令用于改变元素的背景颜色。
示例:创建一个 v-color
指令
// main.js
import Vue from 'vue';
import App from './App.vue';
// 注册全局自定义指令
Vue.directive('color', {
// 当被绑定的元素插入到 DOM 中时调用
inserted(el, binding) {
el.style.backgroundColor = binding.value; // 设置背景颜色
}
});
new Vue({
render: h => h(App),
}).$mount('#app');
在上面的代码中,我们创建了一个名为 v-color
的指令。当这个指令被绑定到一个元素时,它会将该元素的背景颜色设置为传入的值。
2. 在组件中使用自定义指令
一旦我们定义了全局指令,就可以在任何 Vue 组件中使用它。以下是如何在一个组件中使用 v-color
指令的示例:
<template>
<div>
<h1 v-color="'lightblue'">这是一个标题</h1>
<p v-color="'lightgreen'">这是一个段落</p>
</div>
</template>
<script>
export default {
name: 'MyComponent'
}
</script>
<style>
/* 样式可以在这里定义 */
</style>
3. 自定义指令的钩子函数
全局自定义指令有以下几个钩子函数,可以用于不同的生命周期阶段:
bind(el, binding, vnode)
: 指令第一次绑定到元素时调用,只调用一次。inserted(el, binding, vnode)
: 被绑定元素插入父节点时调用。update(el, binding, vnode, oldVnode)
: 被绑定元素所在的 VNode 更新时调用。componentUpdated(el, binding, vnode, oldVnode)
: 指令所在组件的 VNode 及其子 VNode 更新时调用。unbind(el, binding, vnode)
: 只调用一次,指令与元素解绑时调用。
示例:使用更多的钩子函数
下面是一个更复杂的自定义指令示例,演示了如何使用多个钩子:
// main.js
Vue.directive('highlight', {
// 绑定时
bind(el) {
el.style.transition = 'background-color 0.5s';
},
// 插入时
inserted(el) {
el.style.backgroundColor = 'yellow';
},
// 更新时
update(el, binding) {
el.style.backgroundColor = binding.value;
},
// 解绑时
unbind(el) {
el.style.backgroundColor = '';
}
});
在这个例子中,v-highlight
指令会在绑定时设置过渡效果,并在元素插入时将背景色设置为黄色。它还会在更新时根据传入值改变背景色,并在解除绑定时清除样式。
4. 使用指令修饰符
指令也可以使用修饰符(如 .stop
, .prevent
, .self
等),以提供更灵活的功能。例如,如果你希望在点击元素时阻止事件冒泡,可以使用 v-on:click.stop
。
2.在 vue 路由切换时如何全局隐藏某个插件?
在路由切换时,你可以在 beforeEach
或 beforeResolve
钩子中调用隐藏 Toast 的方法。
下面是一个示例,展示如何在 Vue Router 中实现这一功能。
1. 创建 Toast 插件
这里是一个简单的示例:
// toast.js
let $vm;
const ToastPlugin = {
install(Vue) {
if (!$vm) {
const ToastComponent = Vue.extend({
data() {
return {
isShow: false,
text: '',
duration: 3000
};
},
template: `<div v-if="isShow" class="toast">{{ text }}</div>`,
methods: {
show(text, duration = this.duration) {
this.text = text;
this.isShow = true;
setTimeout(() => {
this.isShow = false;
}, duration);
},
hide() {
this.isShow = false;
}
}
});
$vm = new ToastComponent({ el: document.createElement('div') });
document.body.appendChild($vm.$el);
}
Vue.prototype.$toast = {
show(text, duration) {
$vm.show(text, duration);
},
hide() {
$vm.hide();
}
};
}
};
export default ToastPlugin;
2. 在 Vue Router 中使用导航守卫
接下来,设置 Vue Router 并在路由切换时调用 Toast 的 hide
方法:
// router.js
import Vue from 'vue';
import Router from 'vue-router';
import ToastPlugin from './toast'; // 导入 Toast 插件
Vue.use(Router);
Vue.use(ToastPlugin); // 使用 Toast 插件
const routes = [
// 定义你的路由
];
const router = new Router({
routes
});
// 在路由切换前隐藏 Toast
router.beforeEach((to, from, next) => {
// 调用 Toast 的 hide 方法以隐藏 Toast
Vue.prototype.$toast.hide();
next(); // 继续路由导航
});
export default router;
3. 在主入口文件中整合
将路由和 Toast 插件整合到主入口文件中:
// main.js
import Vue from 'vue';
import App from './App.vue';
import router from './router'; // 导入路由
Vue.config.productionTip = false;
new Vue({
router,
render: h => h(App),
}).$mount('#app');
4. 使用 Toast 插件
在你的组件中使用 Toast 插件:
<template>
<div>
<button @click="showToast">显示 Toast</button>
</div>
</template>
<script>
export default {
methods: {
showToast() {
this.$toast.show('这是一个 Toast 提示!', 2000);
}
}
}
</script>
5. 完整效果
通过以上步骤,当你在应用中的任意位置使用 $toast.show
方法时,Toast 会显示。在每次路由切换时,beforeEach
钩子会调用 $toast.hide
方法,从而确保在新路由加载时 Toast 被隐藏。
3.如何实现一个表单验证插件?需要运用到哪些知识?
需要掌握以下知识点:
- Vue 的插件机制:理解如何创建和使用 Vue 插件。
- Vue 的混入(Mixin):使用混入将验证逻辑添加到组件中。
- Vue 的数据响应性:使用
this.$set
来确保响应式地更新错误信息。 - 事件处理:处理表单输入和提交事件。
- 条件渲染:使用
v-if
条件渲染错误信息。
1. 创建基本的插件结构
目录结构示例:
/form-validator
├── index.js // 插件入口
├── validator.js // 验证逻辑
└── styles.css // 样式(可选)
2. 编写验证逻辑
创建一个 validator.js
文件,定义你的验证规则和逻辑。
// validator.js
const validators = {
required(value) {
return !!value || '该字段是必填的';
},
email(value) {
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return re.test(value) || '请输入有效的邮箱地址';
},
minLength(min) {
return value => {
return value.length >= min || `至少需要 ${min} 个字符`;
};
},
maxLength(max) {
return value => {
return value.length <= max || `最多 ${max} 个字符`;
};
},
// 可以添加更多验证规则
};
export default validators;
3. 创建插件入口文件
在 index.js
中,定义插件的 API 和如何与 Vue 组件进行交互。
// index.js
import validators from './validator';
const FormValidator = {
install(Vue) {
Vue.mixin({
data() {
return {
formErrors: {}, // 存储表单错误信息
};
},
methods: {
validateField(fieldName, value, rules) {
const errors = [];
rules.forEach(rule => {
const validate = typeof rule === 'function' ? rule : validators[rule];
const result = validate(value);
if (typeof result === 'string') {
errors.push(result);
}
});
this.$set(this.formErrors, fieldName, errors);
},
validateForm(fields) {
this.formErrors = {};
let isValid = true;
for (const field in fields) {
const { value, rules } = fields[field];
this.validateField(field, value, rules);
if (this.formErrors[field].length) {
isValid = false;
}
}
return isValid;
},
},
});
},
};
export default FormValidator;
4. 使用插件
在你的 Vue 组件中使用这个插件。确保在你的主入口文件中安装该插件。
// main.js
import Vue from 'vue';
import App from './App.vue';
import FormValidator from './form-validator';
Vue.use(FormValidator);
new Vue({
render: h => h(App),
}).$mount('#app');
5. 示例组件
创建一个示例组件,展示如何使用这个表单验证插件。
<template>
<form @submit.prevent="handleSubmit">
<div>
<label for="email">邮箱:</label>
<input type="text" v-model="email" @blur="validateField('email', email, ['required', 'email'])" />
<span v-if="formErrors.email.length">{{ formErrors.email[0] }}</span>
</div>
<div>
<label for="password">密码:</label>
<input type="password" v-model="password" @blur="validateField('password', password, [minLength(6)])" />
<span v-if="formErrors.password.length">{{ formErrors.password[0] }}</span>
</div>
<button type="submit">提交</button>
</form>
</template>
<script>
export default {
data() {
return {
email: '',
password: '',
};
},
methods: {
handleSubmit() {
const isValid = this.validateForm({
email: { value: this.email, rules: ['required', 'email'] },
password: { value: this.password, rules: [this.minLength(6)] },
});
if (isValid) {
// 表单有效,执行提交逻辑
alert('表单提交成功!');
}
},
},
};
</script>
<style>
/* 添加一些样式 */
</style>