面试官:MVVM 和 MVC 的区别是什么?
自己先想一分钟。
关于上面的面试题的具体解释,请移步这里,本文不在累述。正文开始,下面列举的一些小技巧有的或许你用过,有的或许你没用过。不管有的没的,希望你看完之后有所收获吧。文笔和知识有限,不对的地方,请留言斧正!
给 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>
复制代码
Element上传如何提示后再删除?
同事在做上传删除的时候,给用户加个提示“是否删除?” 发现即便点击的是取消
操作,上传列表中还是会删除。查看文档说, 如果在 ``
禁止浏览器 Auto complete 行为
有时候我们输入账号密码登录后台系统,浏览器会弹出是否保存你的登录信息。我们一般会点击保存,因为下次再次登录的时候会自动填充增加了我们的用户体验,很好。
但有时,当我们开发某个某块(比如新增用户)时,点击新增用户按钮,显示弹框,不巧的是,在账号,密码输入框中浏览器帮我们填充上了。但这并不是我们想要的。所以,我的有效解决思路如下:
- 设置
<el-input/>
为只读模式 - 在
focus
事件中去掉只读模式
代码如下:
<el-input
type="text"
v-model="addData.loginName"
readonly
@focus="handleFocusEvent"
/>
复制代码
...
methods: {
handleFocusEvent(event) {
event.target && event.target.removeAttribute('readonly')
}
}
复制代码
然而,ElementUI自带的 auto-complete="off"
貌似并不生效。
阻止 <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
。
<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 },
]
}
})
复制代码
你可以狠狠的戳这里查看上面的效果!
疑问:我们完全可以在 <li>
中进行逻辑判断,为什么还要放到外面进行处理呢? 因为有时候我们用的不是自己开发的组件,比如上面的 <el-table>
,所以就有必要这么做了。
作者:gongph
链接:https://juejin.cn/post/6844903712205307917
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
转载于:https://blog.csdn.net/sinat_17775997/article/details/83998404