Vue - vue2 前端开发技术指南
自定义事件.sync 修饰符的使用
- 在有些情况下,我们可能需要对一个 prop 进行“双向绑定”
// 子组件:
this.$emit('update:title', newTitle)
// 父组件
<text-document
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
>
</text-document>
// 为方便期间可以采用如下简写
<text-document v-bind:title.sync="doc.title"></text-document>
自定义组件的 v-model
一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件。
输入控件可能会将 value attribute 用于不同的目的,model 选项可以用来避免这样的冲突
Vue.component('base-checkbox', {
model: {
prop: 'checked',
event: 'change'
},
props: {
checked: Boolean
},
template: `
<input
type="checkbox"
v-bind:checked="checked"
v-on:change="$emit('change', $event.target.checked)"
>
`
})
/**
* 这里的 lovingVue 的值将会传入这个名为 checked 的 prop。
* 同时当 <base-checkbox> 触发一个 change 事件并附带一个新的值的时候,这个 lovingVue 的 property 将会被更新。
**/
<base-checkbox v-model="lovingVue"></base-checkbox>
侦听器监听值的变化执行副作用
watch: {
// 如果 `question` 发生改变,这个函数就会运行
question: function (newQuestion, oldQuestion) {
this.answer = 'Waiting for you to stop typing...'
this.debouncedGetAnswer()
}
},
- 直接侦听对象属性变化
<template>
<div>
<input v-model="user.name" placeholder="输入姓名">
</div>
</template>
<script>
export default {
data() {
return {
user: {
name: '',
age: 0
}
};
},
watch: {
// 直接侦听 user 对象的 name 属性
'user.name': {
handler(newValue, oldValue) {
console.log(`姓名从 ${oldValue} 变为 ${newValue}`);
// 这里可以执行副作用操作,比如发送请求等
},
immediate: false // 是否在初始化时立即执行一次 handler
}
}
};
</script>
- 侦听所有属性变化
<template>
<div>
<input v-model="user.name" placeholder="输入姓名">
<input v-model.number="user.age" placeholder="输入年龄">
</div>
</template>
<script>
export default {
data() {
return {
user: {
name: '',
age: 0
}
};
},
watch: {
user: {
handler(newValue, oldValue) {
console.log('用户信息发生了变化');
// 执行副作用操作
},
deep: true, // 开启深度侦听
immediate: false // 是否在初始化时立即执行一次 handler
}
}
};
</script>
- 借助计算属性间接侦听
<template>
<div>
<input v-model="user.name" placeholder="输入姓名">
</div>
</template>
<script>
export default {
data() {
return {
user: {
name: '',
age: 0
}
};
},
computed: {
// 计算属性返回要侦听的属性值
userName() {
return this.user.name;
}
},
watch: {
// 侦听计算属性
userName(newValue, oldValue) {
console.log(`姓名从 ${oldValue} 变为 ${newValue}`);
// 执行副作用操作
}
}
};
</script>
使用props 传递数据给子组件
export default {
props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
}
<baseInput :initialCounter="initialCounter" />
利用事件总线,将数据传递给多个监听此事件的组件中
可能出现的问题:此方式在组件初始化时,无法利用此方式进行数据传递,因为初始化时,有可能出现,在子组件还没有建立监听,已经从父组件触发了事件。
解决方案: 可以在触发事件的时候使用this.$nextTick 方法,保证子组件已经全部加载完毕,且已经进行了事件监听。
- 创建事件总线
// event-bus.js
import Vue from 'vue';
export const eventBus = new Vue();
- 监听事件 及移除监听
import { eventBus } from '@/utils/event-bus.js';
created() {
eventBus.$on('eventName', (value) => {
this.getStaticsData(value);
});
},
beforeDestroy() {
eventBus.$off('eventName');
}
- 触发事件
import { eventBus } from '@/utils/event-bus.js';
methods: {
changeData(value){
eventBus.$emit('eventName', value);
}
}
利用provide 和 inject 进行数据传递
保持传递数据响应性的方式:
- 传递对象给后代组件
- 通过传递数据的计算属性到后代
- 祖先组件
provide() {
return {
ids: this.sendConsignorIds
};
},
- 后代组件
inject: ['ids'],
created() {
this.getStaticsData(this.ids.value);
},
vue2 异步更新队列
为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。
这样回调函数将在 DOM 更新完成后被调用。
// 使用回调
this.$nextTick(function () {
console.log(this.$el.textContent) // => '已更新'
})
// 使用async,await 语法
methods: {
updateMessage: async function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
await this.$nextTick()
console.log(this.$el.textContent) // => '已更新'
}
}
vue2 调用子组件实例方法
<base-input ref="usernameInput"></base-input>
this.$refs.usernameInput.method()
vue2 动态组件
<template>
<div class="container page-container">
<handle-tab v-model="applyType" :handles="types" @switch="searchDataList" />
<keep-alive>
<component :applyType="applyType" :key="applyType" :ref="`${currentTabComponent}Ref`"
v-bind:is="currentTabComponent"></component>
</keep-alive>
</div>
</template>
<script>
// 铝锭发运审批
import aluminumIngots from './components/aluminumIngots.vue'
// 铝液发运审批
import moltenAluminum from './components/moltenAluminum.vue'
export default {
components: [
HandleTab,
aluminumIngots,
moltenAluminum
],
data() {
return {
componentCodeMap: {
[approveType.aluminumIngots.code]: 'aluminumIngots',
[approveType.moltenAluminum.code]: 'moltenAluminum',
},
currentTabComponent: 'aluminumIngots',
applyType: approveType.aluminumIngots.code,
};
},
watch: {
// 根据tab组件返回的applyType值的变化匹配对应的组件
applyType: function (code) {
this.currentTabComponent = this.componentCodeMap[code];
}
},
created() {
this.applyType = this.$route.query.applyType || this.applyType
},
async mounted() {
this.initTypes()
},
methods: {
// 初始化页签
initTypes() {
this.types = [
{
code: approveType.aluminumIngots.code,
label: approveType.aluminumIngots.label
},
{
code: approveType.moltenAluminum.code,
label: approveType.moltenAluminum.label
}
]
},
// 初始查询
searchDataList() {
this.$nextTick(function () {
this.$refs[`${this.currentTabComponent}Ref`].getDataList();
});
},
}
</script>
vue2 混入
- 混入概念
- 混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。
- 所有混入对象的选项将被“混合”进入该组件本身的选项。
- 合并规则
- 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先
- 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
- 值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
- 在组件中使用混入
// 定义混入 mixin
export default {
data() {
return {
editing: true
}
},
computed: {},
methods: {
set(key, val) {
this.listPageStore.commit(SET, { key, val })
},
merge(key, val) {
this.listPageStore.commit(MERGE, { [key]: val })
},
validateFields(fields) {
const state = this.listPageStore.state
return validateFields(fields, state, key => {
this.focus(state.keyMap[key])
})
}
}
}
// 使用混入
import mixin from './mixin'
export default {
mixins: [mixin],
}
vue2 响应式
- vue2 响应式原理
- 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,
并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。 - 这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
- 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。
之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
- Vue2 无法检测对象property 的添加或移除
- 由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在
才能让 Vue 将它转换为响应式的 - 对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,
可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 propertythis.$set(this.someObject,'b',2)
- 有时你可能需要为已有对象赋值多个新 property,比如使用 Object.assign() 或 _.extend()。但是,这样添加到对象上的
新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })` this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
- Vue2 不能检测数组的变化
- 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
// 通过Vue.set 解决响应性问题
Vue.set(vm.items, indexOfItem, newValue)
// 通过重写的 Array.prototype.splice 解决
vm.items.splice(indexOfItem, 1, newValue)
- 当你修改数组的长度时,例如:vm.items.length = newLength
// 通过删除数组项实现
vm.items.splice(newLength)
- 声明相应式property
由于 Vue 不允许动态添加根级响应式 property,所以你必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值:
var vm = new Vue({
data: {
// 声明 message 为一个空值字符串
message: ''
},
template: '<div>{{ message }}</div>'
})
// 之后设置 `message`
vm.message = 'Hello!'
vue2 利用vue-router重载当前页面
- this.$router.go(0) 重刷当前页面
- 重置路由实例来刷新当前页面,无需刷新整个页面,体验较好
// 保存当前路由实例
const currentRoute = this.$route;
// 跳转到一个空白路由
this.$router.replace({ path: '/blank' });
this.$nextTick(function() {
// 跳转回当前路由
this.$router.replace(currentRoute);
});
vue2 的生命周期函数
-
created: vue实例已创建,实例已完成对选项的处理,数据侦听、计算属性、方法、事件/侦听器的回调函数。
然而,挂载阶段还没开始,且 $el property 目前尚不可用。 -
mounted: mounted 不会保证所有的子组件也都被挂载完成。如果你希望等到整个视图都渲染完毕再执行某些操作,
可以在 mounted 内部使用 this.$nextTick:
mounted: function () { this.$nextTick(function () { // 仅在整个视图都被渲染之后才会运行的代码 }) }
若数据获取与Dom无关,则可以在created中调用异步函数获取数据,否则就必须在mounted中调用异步接口。 -
beforeDestroy: vue组件卸载前执行清理工作,比如定时器,事件监听等。
-
activated: vue组件激活时,在使用被keep-alive 组件缓存的组件激活时,可以执行的逻辑
vue2 样式覆写elementUI
.page-container {
/deep/ .el-form-item {
.el-input.custom-date-picker {
max-width: 180px;
}
}
}
Vue2 手动生成dom元素节点
const h = this.$createElement;
h('p', null, '确定要作废此条出库单吗?'),
vue2 如何按需缓存特定路由组件
- 通过路由meta信息控制
const routes = [
{
path: '/home',
component: Home,
meta: { keepAlive: true } // 表明该组件需要被缓存
},
{
path: '/detail',
component: Detail,
meta: { keepAlive: false } // 表明该组件不需要被缓存
}
]
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
- 通过组件名称(name)来控制, 在模版中使用include或exclude属性
<keep-alive include="Home,About">
<!-- 只有组件名称是 Home 或者 About 的组件会被缓存 -->
<router-view></router-view>
</keep-alive>
<keep-alive :include="cachedComponents">
<router-view></router-view>
</keep-alive>
data() {
return {
cachedComponents: ['Home', 'About']
}
}
- 通过key强制更新组件
<keep-alive>
<router-view :key="routeKey"></router-view>
</keep-alive>
computed: {
routeKey() {
// 当这个值变化时,组件会被重新渲染
return this.$route.path
}
}
ElementUI el-select 如何设置选中的选项label,与下拉选项的label不同
通过sloct-scope进行定制化显示: el-option 中的插槽显示下拉选项,:label选项则定义显示的label
<el-option v-for="item in codeList" :key="item.id" :label="item.code" :value="item.code">
<template #default>
<span>{{ `${item.code}(${item.name})`}}</span>
</template>
</el-option>
vue2中使用加载图片
- 图片位于src/assets目录
<template>
<div>
<!-- 方法1:使用require -->
<img :src="require('@/assets/logo.png')" alt="Logo">
<!-- 方法2:在script中导入 -->
<img :src="logoUrl" alt="Logo">
</div>
</template>
<script>
export default {
data() {
return {
// ES6导入(@是src目录的别名,由webpack配置)
logoUrl: require('@/assets/logo.png')
}
}
}
</script>
- 在 CSS 中使用相对路径
<template>
<div class="container"></div>
</template>
<style scoped>
.container {
/* 假设图片与组件同级目录下的images文件夹 */
background-image: url('./images/bg.png');
}
</style>
- 图片位于public目录
- 位于public下的图片不会进行打包更改文件名称
- 代码中使用/开头的绝对路径,开发环境相对于/public/,
- 生产环境则会相对于服务根目录,如果是一个服务下面放置多个目录,使用绝对路径则会出现资源访问不到问题
<template>
<div>
<!-- 注意:路径以/开头,对应public目录 -->
<img src="/images/logo.png" alt="Logo">
</div>
</template>
vue2中引入svg icon组件
- 首先创建一个全局组件 SvgIcon.vue:
<template>
<svg :class="svgClass" aria-hidden="true" v-bind="$attrs">
<use :xlink:href="iconName" />
</svg>
</template>
<script>
export default {
name: 'SvgIcon',
props: {
iconClass: {
type: String,
required: true
},
className: {
type: String,
default: ''
}
},
computed: {
iconName() {
return `#icon-${this.iconClass}`
},
svgClass() {
if (this.className) {
return `svg-icon ${this.className}`
}
return 'svg-icon'
}
}
}
</script>
<style scoped>
.svg-icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>
- 然后创建一个 svg 目录专门存放图标,并编写自动导入脚本:
// src/icons/index.js
import Vue from 'vue'
import SvgIcon from '@/components/SvgIcon.vue' // 路径根据实际情况调整
// 注册全局组件
Vue.component('svg-icon', SvgIcon)
// 自动导入所有svg文件
/**
*创建一个上下文环境,用于批量导入文件, 此上下文环境为函数
*第一个参数 './svg' 表示要搜索的目录
*第二个参数 false 表示不搜索子目录
*第三个参数 /\.svg$/ 是一个正则表达式,匹配所有以 .svg 结尾的文件
*/
const req = require.context('./svg', false, /\.svg$/)
/**
* 定义一个函数,用于处理上下文环境
* requireContext.keys() 获取所有匹配到的文件路径数组
* 通过 map(requireContext) 导入所有这些文件
**/
const requireAll = requireContext => requireContext.keys().map(requireContext)
/** 执行导入操作,将所有 SVG 文件引入到项目中**/
requireAll(req)
- 在 main.js 中引入该脚本:
import './icons' // 引入svg图标
- 最后在项目中使用:
<svg-icon icon-class="your-icon-name" />
vuex 在vue2中的使用
- 创建store
- src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
// 引入模块
import user from './modules/user'
import permission from './modules/permission'
// 引入 getters
import getters from './getters'
Vue.use(Vuex)
export default new Vuex.Store({
modules: {
user,
permission,
},
getters
})
- src/getters.js
export default {
name: state => state.user.userName,
btnList: state => state.permission.btnList,
userInfo: state => state.user.originUserInfo
}
- src/store/modules/user
const state = {
loginAccount: '',
userName: '',
};
const mutations = {
RESET_STATE: state => {
Object.assign(state, getDefaultState())
},
SET_TOKEN: (state, token) => {
state.token = token
},
SET_USERINFO: (state, data) => {
userInfoField.forEach(key => {
state[key] = data[key] || getDefaultState()[key]
})
state.originUserInfo = data
setUserInfo(data)
},
};
const actions = {
getInfo({ commit, state }) {
},
setUserInfo({ commit, state }) {
},
};
export default {
namespaced: true,
state,
mutations,
actions
}
- 注入vue2实例
在main.js 中挂载store
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
store, // 注入所有组件
render: h => h(App)
}).$mount('#app')
- 在组件中使用
- 在模版中访问
<template>
<div>
<p>Count: {{ $store.state.count }}</p>
<p>User: {{ $store.state.user?.name }}</p>
</div>
</template>
- 在代码中访问
<script>
export default {
methods: {
increment() {
this.$store.commit('INCREMENT')
}
}
}
</script>
- 使用辅助函数简化代码
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
computed: {
// 展开运算符混入
...mapState(['count']),
...mapGetters(['doubleCount'])
},
methods: {
...mapMutations(['INCREMENT']),
...mapActions(['login']),
// 使用:
handleClick() {
this.INCREMENT() // 直接调用
}
}
}
</script>
vue2 与 ElementUI中的字段验证
- 验证规则定义
rules: {
actualPaymentAmount: [
{ required: true, message: '请输入实收预付款金额', trigger: 'blur' },
// 通过正则表达式自定义验证
{ pattern: /^\d+(\.\d+)?$/, message: '实收预付款金额必须是数字', trigger: 'blur' }
],
// 嵌套表单数组字段验证
'shippingNoteMaterialList[0].shipmentQuantity': [
{ required: true, message: '请输入发货数量', trigger: 'blur' },
{ pattern: /^\d+(\.\d+)?$/, message: '发货数量必须是数字', trigger: 'blur' }
],
// 自定义验证规则, validator:指定验证函数
username: [{ required: true, trigger: 'blur', validator: this.$rule.username }],
password: [{ required: true, trigger: 'blur', validator: validatePassword }]
}
- 自定义验证函数
- 在表单验证规则中,使用 validator 属性指定自定义验证函数
- 自定义函数需要接收三个参数:rule(规则对象)、value(当前字段值)、callback(回调函数)
- 验证通过时调用 callback(),不通过时调用 callback(new Error('错误信息'))
// 自定义验证函数 - 验证手机号
const validatePhone = (rule, value, callback) => {
const phoneReg = /^1[3-9]\d{9}$/;
if (!value) {
return callback(new Error('请输入手机号'));
} else if (!phoneReg.test(value)) {
return callback(new Error('请输入正确的手机号格式'));
}
callback(); // 验证通过
};
- 验证函数中的rule参数
在 ElementUI 的自定义验证函数中,rule 参数包含了当前验证规则的完整配置信息,
它主要用于在验证过程中获取规则定义的相关属性,帮助实现更灵活的验证逻辑。
rule 是一个对象,包含以下主要属性(根据实际配置可能有所不同):
field:当前验证的字段名(与 prop 对应)
fullField:完整的字段路径(用于嵌套表单)
type:规则类型(如'string'、'number' 等,若未指定则为'string')
required:是否为必填项(布尔值)
message:默认错误提示信息
trigger:触发验证的时机(如 'blur'、'change')
其他自定义属性:在定义规则时可添加的额外属性
rule 参数的使用场景:
- 复用验证函数:通过 rule 判断当前验证的字段,实现一个函数处理多个字段的验证
- 读取规则配置:获取规则中定义的属性(如长度限制、自定义参数等)
- 动态生成错误信息:结合 rule 中的信息自定义错误提示
data() {
// 通用验证函数:同时处理手机号和邮箱验证
const validateContact = (rule, value, callback) => {
// 通过 rule.field 判断当前验证的字段
if (rule.field === 'phone') {
// 手机号验证
const phoneReg = /^1[3-9]\d{9}$/;
if (!value) {
return callback(new Error(`请输入${rule.label || '手机号'}`));
} else if (!phoneReg.test(value)) {
return callback(new Error('请输入正确的手机号格式'));
}
} else if (rule.field === 'email') {
// 邮箱验证
const emailReg = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
if (!value) {
return callback(new Error(`请输入${rule.label || '邮箱'}`));
} else if (!emailReg.test(value)) {
return callback(new Error('请输入正确的邮箱格式'));
}
}
callback();
};
// 带自定义参数的验证函数
const validateRange = (rule, value, callback) => {
// 从 rule 中获取自定义的 min 和 max 属性
if (value < rule.min || value > rule.max) {
return callback(new Error(`${rule.label}必须在${rule.min}-${rule.max}之间`));
}
callback();
};
return {
form: {
phone: '',
email: '',
age: ''
},
rules: {
phone: [
{
field: 'phone',
label: '手机号', // 自定义属性,可通过 rule.label 获取
validator: validateContact,
trigger: 'blur'
}
],
email: [
{
field: 'email',
label: '邮箱',
validator: validateContact,
trigger: 'blur'
}
],
age: [
{
type: 'number',
label: '年龄',
min: 18, // 自定义属性,年龄最小值
max: 120, // 自定义属性,年龄最大值
validator: validateRange,
trigger: 'blur'
}
]
}
};
}
vue2 插件编写
- 编写验证函数插件
// src/utils/baseRule.js
const ruleInAll = {}
ruleInAll.install = function (Vue) {
Vue.prototype.$rule = {
// ip地址
ip: (rule, value, callback) => {
if (rule.required && !value) {
callback(new Error('IP不能为空'))
}
if (value && !/^([0,1]?\d{1,2}|2([0-4][0-9]|5[0-5]))(\.([0,1]?\d{1,2}|2([0-4][0-9]|5[0-5]))){3}$/.test(value)) {
callback(new Error('请输入有效的IP地址'))
}
callback()
},
}
}
export ruleInAll;
//main.js 引入
import Vue from 'vue'
import baseRule from '@/utils/baseRule'
Vue.use(baseRule)
// 项目表单验证规则中使用
rules: {
ipAddress: [{ required: true, validator: this.$rule.ip, trigger: 'blur' }]
},
ElementUI中el-dialog 与父组件交互最佳实践
- 父组件中引入封装好的子组件dialog
<template>
<ContractDialog ref="dialogRef" :row="currentRow" @close="closeDialog" />
</template>
<script>
import ContractDialog from '../components/contractDialog.vue'
export default {
components: {
ContractDialog
},
data () {
return {
currentRow: {},
}
},
methods: {
// 打开弹窗
openDialog(row) {
this.currentRow = row; // 存储信息,并传递给弹窗组件
// 执行弹窗的初始化,及显示操作
this.$nextTick(function() {
this.$refs.dialogRef.openDialog();
});
},
// 执行关闭弹窗后的回调操作
closeDialog() {
// 设置已读
this.readIt(this.currentRow);
},
},
}
</script>
- 弹窗子组件的设计
- 获取父组件传递的数据 props
- 定义组件本身的状态(dialogVisible)以及显示数据(contractData)
- 定义父组件调用的初始化方法(openDialog) 和 向父组件发送事件和传递数据的方法(closeDialog)
<template>
<el-dialog title="合同变更" :visible.sync="dialogVisible" width="85%" :close-on-click-modal="false"
:before-close="closeDialog">
</el-dialog>
</template>
<script>
export default {
props: ['row'],
data() {
return {
loading: false,
dialogVisible: false,
contractData: {},
}
},
methods: {
openDialog() {
// 显示弹窗
this.dialogVisible = true;
// 获取数据
this.getContractChange(this.row);
},
// 关闭弹窗
closeDialog(done) {
// 向父组件发送close事件,进行相应的处理
this.$emit('close');
// 关闭弹窗
done();
},
},
}
</script>