优秀的编程知识分享平台

网站首页 > 技术文章 正文

Vue的一些小技巧,可以收藏用到的时候特别爽

nanyue 2024-08-08 18:38:30 技术文章 17 ℃

给 props 属性设置多个类型

这个技巧在开发组件的时候用的较多,为了更大的容错性考虑,同时代码也更加人性化:

export default {
 props: {
 width: {
 type: [String, Number],
 default: '100px'
 }
 // 或者这样
 // width: [String, Number]
 }
}

比如一个 <my-button> 上暴露了一个 width 属性,我们既可以传 100px,也可以传 100 :

<!-- my-button.vue -->
<template>
 <button :style="computedWidth">{{ computedWidth }}</button>
</template>
<script>
 export default {
 props: {
 width: [String, Number]
 },
 computed: {
 computedWidth () {
 let o = {}
 if (typeof this.width === 'string') o.width = this.width
 if (typeof this.width === 'number') o.width = this.width + 'px'
 return o
 }
 }
 }
</script>

使用:

<!-- 在其他组件中使用 -->
<template>
 <my-button width="100px"></my-button>
 <!-- or -->
 <my-button :width="100"></my-button>
</template>

阻止 <el-form> 默认提交行为

有时候我们在用饿了么组件 <el-form> 在文本框中键入 enter 快捷键的时候会默认触发页面刷新。我们可以加入如下代码解决其默认行为:

<el-form @submit.native.prevent>
 ...
</el-form>

使用 <el-scrollbar> 组件

Element 官方是没有明确提供这个组件的,但是其源码 中确实存在。关于如何使用我写了一个Demo,你可以狠狠的戳这里查看示例,本文不再赘述。

根据业务合并 el-table 中的行和列

最近在做项目的时候遇到一个需求:同一个账号ID下有多个被分配的角色时并列显示角色信息。于是就想到了 el-table 提供的合并方法 :span-method。但它对后台数据格式是有要求的:

  • 如果后台返回的数据是数组里面嵌套数组的话,你就需要把里面的数组按顺序也拿到外面了
// 假如后台返回的数据是下面这样的
{
 data: [
 { id: 1, appkey: 1, name: 'a', list: [{ id: 11, appkey: 1, name: 'a-1'}, {id: 12, appkey: 1, name: 'a-2'}] }
 { id: 2, appkey: 2, name: 'b', list: [{ id: 21, appkey: 2, name: 'b-1'}, {id: 22, appkey: 2, name: 'b-2'}] }
 ]
}
// 整理过后的格式应该是这样的
{
 data: [
 { id: 1, appkey: 1, name: 'a' },
 { id: 11, appkey: 1, name: 'a-1' },
 { id: 12, appkey: 1, name: 'a-2' },
 { id: 2, appkey: 2, name: 'b' },
 { id: 21, appkey: 2, name: 'b-1' },
 { id: 22, appkey: 2, name: 'b-2' }
 ]
}

下面是具体的处理流程:

<template>
 <el-table 
 :data="formattedList" 
 :span-method="handleColspanMethod"
 >
 ...
 </el-table>
</template>
<script>
import Api from '@/api/assign'
export default {
 data() {
 return {
 list: [], // 后台返回的数据
 formattedList:[], // 格式化后的数据
 spanArr: [], // 保存要合并的行列数据
 }
 },
 created() {
 this.getList()
 },
 methods: {
 getList() {
 Api.fetchList().then(response => {
 this.list = response.data.data
 // 格式化数据
 this.formattedList = this.formatArray(this.list)
 // 获取合并位置
 this.getSpanArr()
 })
 },
 /**
 * 格式化数组
 * {Array} sources 源数组
 * {Array} arrayed 格式化后的数组
 * return 返回格式化后的数组
 */
 formatArray: function f(sources, arrayed) {
 if (!sources) return []
 
 const arr = arrayed || []
 const len = sources.length
 for (let i = 0; i < len; i++) {
 const childs = sources[i].list || []
 arr.push(sources[i])
 if (childs.length > 0) {
 f(sources[i].list, arr)
 }
 }
 return arr
 },
 /**
 * 获取合并位置信息
 */
 getSpanArr() {
 // 重置 spanArr,因为翻页的时候数据就变了
 // 之前的数据如果不清空,其他页也会受影响
 this.spanArr = []
 const data = this.formattedList
 if (!data || data.length <= 0) return
 // 遍历
 for (let i = 0; i < data.length; i++) {
 if (i === 0) {
 this.spanArr.push(1)
 // 其实就是行索引
 this.pos = 0
 } else {
 // 如果当前对象和上一个对象的 appkey 相等
 // 说明需要合并
 if (data[i].appkey === data[i - 1].appkey) {
 this.spanArr[this.pos] += 1
 this.spanArr.push(0)
 } else {
 this.spanArr.push(1)
 this.pos = i
 }
 }
 }
 },
 /**
 * 处理跨行列合并
 */
 handleColspanMethod({ row, column, rowIndex, columnIndex}) {
 if (columnIndex < 2) {
 const _spa = this.spanArr[rowIndex]
 const _row = _spa ? _spa : 0
 const _col = _row > 0 ? 1 : 0
 return {
 rowspan: _row,
 colspan: _col
 }
 }
 }
 }
}
</script>

单文件样式提取

如果 a.vue 和 b.vue 都用到了下面的样式:

.btn {
 color: silver
}
// 省略很长的样式
...

可以考虑把样式提取到 styleName.scss/css 中,例如:

./components/common.scss

.btn {
 color: silver
}
// 省略很长的样式

然后再在两文件中引入,即:

<template>...</template>
<script>...</script>
<style lang="scss" src="./components/common.scss" scoped/>

是不是省了很多代码呢?快去试试吧!

解决 vue-template-admin 单文件中背景图片生产打包后路径404问题

  • 找到 build/utils.js
  • 然后找到 generateLoaders 方法
  • 在 if(options.extract){...} 中修改,即:
if (options.extract) {
 // 解决其打包背景图片路径问题
 loaders.push({
 loader: MiniCssExtractPlugin.loader,
 options: {
 publicPath: '../../'
 }
 })
} else {
 ...
}

记住,千万别再你的 css 中这样写:

background: url("/new/static/images/assets/loginbackground.png");
复制代码

因为 new 文件夹是后台同学帮你配置的,随时都有可能被修改,一个地方还好,多个地方改起来就蛋疼了。

data 初始化

因为 props 要比 data 先完成初始化,所以我们可以利用这一点给 data 初始化一些数据进去,看代码:

export default {
 data () {
 return {
 buttonSize: this.size
 }
 },
 props: {
 size: String
 }
}

除了以上,子组件的 data 函数也可以有参数,且该参数是当前实例对象。所有我们可以利用这一点做一些自己的判断。如,改写上面的代码:

export default {
 data (vm) {
 return {
 buttonSize: vm.size
 }
 },
 props: {
 size: String
 }
}

template

我们在做 v-if 判断的时候,可以把判断条件放在 template 组件上,最终的渲染结果将不包含 <template> 元素。

<template>
 <div class="box">
 <template v-if="isVal">
 <h2>...</h2>
 </template>
 <template v-else>
 <h2>...</h2>
 </template>
 </div>
</template>

v-for 也同样适用。

Lifecycle hook

生命周期钩子可以是一个数组类型,且数组中的函数会依次执行。

export default {
 ...
 created: [
 function one () {
 console.log(1)
 },
 function two () {
 console.log(2)
 }
 ]
 ...
}

没什么用,知道就行了。事实上生命周期钩子还可以作用于 DOM 元素上,利用这一点,我们可以用父组件中的方法来初始化子组件的生命周期钩子:

<!-- Child.vue -->
<template>
 <h3>I'm child!</h3>
</template>
<!-- Parent.vue -->
<template>
 <child @hook:created="handleChildCreated"></child>
</template>
<script>
 import Child from './child.vue'
 export default {
 components: [ Child ],
 methods: {
 handleChildCreated () {
 console.log('handle child created...')
 }
 }
 }
</script>

其他钩子雷同,不再赘述。

v-for

在用 v-for 遍历数组的时候,我们一般都会错误的这样去做,举个栗子:

v-for 和 v-if 放在同一个元素上使用:

<template>
 <ul class="items">
 <!-- 只有激活的用户才可以显示 -->
 <li 
 v-for="(user, index) in users" 
 v-if="user.isActive" 
 :key="user.id">
 {{ user.name }}
 </li>
 </ul>
</template>

由于 v-for 和 v-if 放在同一个元素上使用会带来一些性能上的影响,官方给出的建议是在计算属性上过滤之后再进行遍历。所以平时开发不推荐一起使用,知道有这回事即可,不至于面试时不知道。 关于为什么不推荐放在一起使用,参见 避免-v-if-和-v-for-用在一起。

混合

如果好多组件都共用到一些像 props、data、methods 等,可以单独抽出来放到 mixins 混合器中。比如,在用户管理列表中使用。

分页混合器:

// paging-mixin.vue
export default {
 props: {
 pageSize: 1,
 pageLength: 10,
 currentPage: 1
 total: 20
 },
 methods: {
 /**
 * 上一页
 */
 prevPage (page) {
 ...
 },
 /**
 * 下一页
 */
 nextPage (page) {
 ...
 }
 /**
 * 跳转到当前页
 */
 currentPage (page) {
 ...
 }
 }
}

Users.vue:

<template>
 <div class="user-model">
 <my-table :data="users"></my-table>
 <my-paging
 :page-length="pageLength"
 :page-size="pageSize"
 :current-page="currentPage"
 :total="total">
 </my-paging>
 </div>
</template>
<script>
 import PagingMixin from '../mixins/paging-mixin.vue'
 export default {
 mixins: [PagingMixin],
 data () {
 return {
 users: [],
 pageLength: 10,
 pageSize: 1,
 currentPage: 1,
 total: 20
 }
 }
 }
</script>

不用每个页面都写一遍 props 和 methods 了。

render 函数

下面是一段简单的 template 模板代码:

<template>
 <div class="box">
 <h2>title</h2>
 this is content
 </div>
</template>

我们用渲染函数来重写上面的代码:

export default {
 render (h) {
 let _c = h
 return _c('div', 
 { class: 'box'}, 
 [_c('h2', {}, 'title'), 'this is content'])
 }
}

事实上,Vue 会把模板(template)编译成渲染函数(render),你可以通过一个在线工具 实时查看编译结果。上面的 template 模板会被编译成如下渲染函数:

let render = function () {
 return _c('div',
 {staticClass:"box"},
 [_c('h2', [_v("title")]), _v("this is content")])
}

是不是很像? 正如官方说的,渲染函数比 template 更接近编译器。如果用一个流程图来解释的话,大概是这个样子:

template
 ↓
预编译工具(vue-loader + vue-template-compile)
 ↓
 render
 ↓
resolve vnode

具体参见 Vue声明周期图示。

渲染函数用处:

  • 开发组件库,Element 源码用的都是 render
  • 封装一些高阶组件。组件里面嵌套组件就是高阶组件,前提是要满足组件三要素:props、event、slot
  • 用于处理一些复杂的逻辑判断。如果我们一个组件里面有很多 v-if 判断的话,用模板就显得不合适了,这个时候可以用渲染函数来轻松处理

errorCaptured

捕获一个来自子孙组件的错误时被调用。有时候当我们想收集错误日志,却不想把错误暴露到浏览器控制台的时候,这很有用。下面是个例子:

Child.vue

<template>
 <!-- 省略一些无关代码 -->
</template>
<script>
 export default {
 mounted () {
 // 故意把 console 写错
 consol.log('这里会报错!')
 }
 }
</script>

Parent.vue

<template>
 <child></child>
</template>
<script>
 import Child from './Child.vue'
 export default {
 components: [ Child ],
 /**
 * 收到三个参数:
 * 错误对象、发生错误的组件实例
 * 以及一个包含错误来源信息的字符串。
 * 此钩子可以返回 false 以阻止该错误继续向上传播。
 */
 errorCaptured (err, vm, info) {
 console.log(err)
 // -> ReferenceError: consle is not defined ...
 console.log(vm)
 // -> {_uid: 1, _isVue: true, $options: {…}, _renderProxy: o, _self: o,…}
 console.log(info)
 // -> `mounted hook`
 // 告诉我们这个错误是在 vm 组件中的 mounted 钩子中发生的
 
 // 阻止该错误继续向上传播
 return false
 }
 }
</script>

关于 errorCaptured 更多说明,请移步官网-> 。

v-once

通过 v-once 创建低开销的静态组件。渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once 特性以确保这些内容只计算一次然后缓存起来,就像这样:

<template>
 <div class="box" v-once>
 <h2> 用户协议 </h2>
 ... a lot of static content ...
 </div>
</template>

只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。关于 v-once 更多介绍,请移步官网->。

slot-scope

作用域插槽。vue@2.5.0 版本以前叫 scope,之后的版本用 slot-scope 将其代替。除了 scope 只可以用于 <template> 元素,其它和 slot-scope 都相同。

用过 Element 组件的同学都知道,当我们在使用 <el-table> 的时候会看到如下代码:

Element@1.4.x 的版本:

<el-table-column label="操作">
 <template scope="scope">
 <el-button
 size="small"
 @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
 <el-button
 size="small"
 type="danger"
 @click="handleDelete(scope.$index, scope.row)">删除</el-button>
 </template>
</el-table-column>

但在 2.0 之后的版本替换成了 slot-scope。

Element@2.0.11:

<el-table-column label="操作">
 <template slot-scope="scope">
 <el-button
 size="mini"
 @click="handleEdit(scope.$index, scope.row)">编辑</el-button>
 <el-button
 size="mini"
 type="danger"
 @click="handleDelete(scope.$index, scope.row)">删除</el-button>
 </template>
</el-table-column>

说白了,slot-scope 相当于函数的回调,我把结果给你,你想怎么处理就怎么处理,一切随你:

function getUserById (url, data, callback) {
 $.ajax({
 url,
 data,
 success: function (result) {
 callback(result)
 }
 })
}
// 使用
getUserById('/users', { id: 1 }, function (response) {
 // 拿到数据并开始处理自己的页面逻辑
})

下面我们来简单模拟下 <el-table> 组件内部是怎么使用 slot-scope 的,看代码:

模拟的 <el-table> 组件:

// 定义模板
let template = `
 <ul class="table">
 <li v-for="(item, index) in data" :key="index">
 <!-- 我希望数据由调用者自己处理 -->
 <!-- 'row' 相当于变量名,随便定义,比如 aaa,bbb 啥的 -->
 <slot :row="item">
 <!-- 当使用者什么都没写的时候,默认值才会显示-->
 {{ item.name }}
 </slot>
 </li>
 </ul>
`
Vue.component('el-table', {
 template,
 props: {
 data: Array,
 default: []
 }
})

在你需要的地方使用 <el-table> 组件:

HTML:

<div id="app">
 <el-table :data="userData">
 <!-- 使用的时候可以用 template -->
 <!-- `scope` 也是个变量名,随便命名不是固定的,比如 foo, bar -->
 <template slot-scope="scope">
 <!-- 其中 `scope.row` 中的 row 就是我们上边定义的变量啦-->
 <!-- `scope.row`返回的是 `item` 对象 -->
 <template v-if="scope.row.isActived">
 <span class="red">{{ scope.row.name }}</span>
 </template>
 <template v-else>
 {{ scope.row.name }}
 </template>
 </template>
 </el-table>
</div>

JavaScript:

new Vue({
 el: '#app',
 data: {
 userData: [
 {id: 1, name: '张三', isActived: false},
 {id: 2, name: '李四', isActived: false},
 {id: 1, name: '王五', isActived: true},
 {id: 1, name: '赵六', isActived: false},
 ]
 }
})

CSS:

.red {
 color: red
}
复制代码

你可以狠狠的戳这里查看上面的效果!最后,我们再使用 render 函数来重构上面的代码:

JavaScript:

// `<el-table>` 组件
Vue.component('el-table', {
 name: 'ElTable',
 render: function (h) {
 return h('div', { 
 class: 'el-table'
 }, this.$slots.default)
 },
 props: {
 data: Array
 }
})
// `<el-table-column>`
Vue.component('el-table-column', {
 name: 'ElTableColumn',
 render: function (h) {
 // 定义一个存放 li 元素的数组
 let lis = [], 
 // 获取父组件中的 data 数组
 data = this.$parent.data
 // 遍历数组,也就是上面的 `v-for`,生成 `<li>` 标签
 // `this.$scopedSlots.default` 获取的就是上面 slot-scope 作用于插槽的部分,
 // 并把 `{ row: item }` 传给上面的 `scope` 变量
 data.forEach((item, index) => {
 let liEl = h('li', {
 key: item.id
 }, [ this.$scopedSlots.default({ row: item }) ])
 // 把生成的 li 标签存到数组
 lis.push(liEl)
 })
 return h('ul', {
 class: 'el-table-column'
 }, lis)
 }
})

在你的页面这样来使用:

HTMl:

<div id="app">
 <el-table :data="list">
 <el-table-column>
 <template slot-scope="scope">
 <span class="red" v-if="scope.row.actived">{{ scope.row.name }}</span>
 <span v-else>{{ scope.row.name }}</span>
 </template>
 </el-table-column>
 </el-table>
</div>

JavaScript:

new Vue({
 el: '#app',
 data: {
 list: [
 { id: 1, name: '张三', actived: false },
 { id: 1, name: '李四', actived: false },
 { id: 1, name: '王五', actived: true },
 { id: 1, name: '赵六', actived: false },
 ]
 }
})
最近发表
标签列表