{{ time | dateServer }}
目录
目录
1. 多图表resize事件去中心化
1.1 一般情况
1.2 优化
1.3 再次优化
2. 全局过滤器注册
2.1 一般情况
2.2 优化
3. 全局组件注册
3.1 一般情况
3.2 优化
4. 不同路由的组件复用
4.1 场景还原
4.2 优化
5. 组件事件属性穿透
5.1 一般情况
5.2 优化
6. 路由根据开发状态懒加载
6.1 一般情况
6.2 优化
7 vue-loader小技巧
7.1 用 preserveWhitespace 减少文件体积
7.2 使用 transformToRequire 再也不用把图片写成变量了
8. render 函数
8.1 动态标签
8.2 动态组件
9.将一个 prop 限制在一个类型的列表中
10. 使用引号来监听嵌套属性
11.知道何时使用 v-if
12.单个作用域插槽的简写
13.将局部和全局的 style 混合在一起
14.重写子组件的样式
15.如何在 Vue 之外创建一个具有响应性的变量
16. v-for 中的解构
17.在指定范围内循环
18.窃取 prop 类型
19.检测元素外部(或内部)的单击
20.从组件外部调用一个方法
21.监听数组和对象
22.template 标签的另一个用途
23.处理错误(和警告)的更好方法
24.默认内容和扩展点
25. 有条件地渲染插槽
26.如何监听一个插槽的变化
27.用Vue Router进行深度链接
❤️ 谢谢支持
持续记录中...
1. 多图表resize事件去中心化
有时候我们会遇到这样的场景,一个组件中有几个图表,在浏览器resize的时候我们希望图表也进行resize,因此我们会在 父容器组件中写:
mounted() {
setTimeout(() => window.onresize = () => {
this.$refs.chart1.chartWrapperDom.resize()
this.$refs.chart2.chartWrapperDom.resize()
// ...
}, 200)
}
destroyed() { window.onresize = null }
这样子图表组件如果跟父容器组件不在一个页面,子组件的状态就被放到父组件进行管理,为了维护方便,我们自然希望子组件的事件和状态由自己来维护,这样在添加删除组件的时候就不需要去父组件挨个修改
这里使用了lodash的节流throttle函数,也可以自己实现,这篇文章也有节流的实现可以参考一下。以Echarts为例,在每个图表组件中:
computed: {
/* 图表DOM */
chartWrapperDom() {
const dom = document.getElementById('consume-analy-chart-wrapper')
return dom && Echarts.init(dom)
},
/* 图表resize节流,这里使用了lodash,也可以自己使用setTimout实现节流 */
chartResize() {
return _.throttle(() => this.chartWrapperDom && this.chartWrapperDom.resize(), 400)
}
},
mounted() {
window.addEventListener('resize', this.chartResize)
},
destroyed() {
window.removeEventListener('resize', this.chartResize)
}
感谢 @JserWang 的提醒,这里因为多个 chart 实例都使用同一套初始化逻辑,可以使用 extends 来考虑复用,因此我想到了 Vue 提供的 Mixins,所以我在这里做了点优化,可以让每个同类型的 chart 组件更优雅一点:新建一个 mixin.js 文件:
import Echarts from 'echarts'
import _ from 'lodash'
export default {
computed: {
/* 图表DOM */
$_chartMixin_chartWrapperDom() {
const dom = document.getElementById(this.thisDomId)
return dom && Echarts.init(dom)
},
/** 图表resize节流,这里使用了lodash,也可以自己使用setTimout实现节流 */
$_chartMixin_ chartResize() {
return _.throttle(() => this. $_chartMixin_ chartWrapperDom.resize(), 400)
}
},
methods: {
/* 图表初始化 */
$_chartMixin_ initChart() {
this. $_chartMixin_ chartWrapperDom.setOption({ /* options */ }
},
mounted() {
this. $_chartMixin_ initChart()
window.addEventListener('resize', this.$_chartMixin_chartResize)
},
destroyed() {
window.removeEventListener('resize', this. $_chartMixin_ chartResize)
}
}
然后在每个 chart 组件中:
这样就可以在每个图表组件中混入之前在 mixin.js
中定义的 resize 事件逻辑,且自动初始化,并在 destroyed 的时候自动销毁事件~
当然可以进一步优化一下,比如一个页面有多个图表的话,上面的实现就力有不逮了,这里需要重构一下,具体代码可以参照 chartInitMixin - Github 的实现~
2. 全局过滤器注册
官方注册过滤器的方式:
export default {
data () { return {} },
filters:{
orderBy (){
// doSomething
},
uppercase () {
// doSomething
}
}
}
但是我们做项目来说,大部分的过滤器是要全局使用的,不会每每用到就在组件里面去写,抽成全局的会更好些。官方注册全局的方式:
// 注册
Vue.filter('my-filter', function (value) {
// 返回处理后的值
})
// getter,返回已注册的过滤器
var myFilter = Vue.filter('my-filter')
但是分散写的话不美观,因此可以抽出成单独文件。
我们可以抽出到独立文件,然后使用Object.keys在main.js入口统一注册
/src/common/filters.js
let dateServer = value => value.replace(/(\d{4})(\d{2})(\d{2})/g, '$1-$2-$3')
export { dateServer }
/src/main.js
import * as custom from './common/filters/custom'
Object.keys(custom).forEach(key => Vue.filter(key, custom[key]))
然后在其他的.vue 文件中就可愉快地使用这些我们定义好的全局过滤器了
{{ time | dateServer }}
3. 全局组件注册
需要使用组件的场景:
我们写了一堆基础UI组件,然后每次我们需要使用这些组件的时候,都得先import,然后声明components,很繁琐,这里可以使用统一注册的形式
我们需要借助一下神器webpack,使用 require.context()
方法来创建自己的模块上下文,从而实现自动动态require组件。这个方法需要3个参数:要搜索的文件夹目录、是否还应该搜索它的子目录、以及一个匹配文件的正则表达式。我们在components文件夹添加一个叫 componentRegister .js的文件,在这个文件里借助webpack动态将需要的基础组件统统打包进来。
/src/components/ componentRegister .js
import Vue from 'vue'
/**
* 首字母大写
* @param str 字符串
* @example heheHaha
* @return {string} HeheHaha
*/
function capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
/**
* 对符合'xx/xx.vue'组件格式的组件取组件名
* @param str fileName
* @example abc/bcd/def/basicTable.vue
* @return {string} BasicTable
*/
function validateFileName(str) {
return /^\S+\.vue$/.test(str) &&
str.replace(/^\S+\/(\w+)\.vue$/, (rs, $1) => capitalizeFirstLetter($1))
}
const requireComponent = require.context('./', true, /\.vue$/)
// 找到组件文件夹下以.vue命名的文件,如果文件名为index,那么取组件中的name作为注册的组件名
requireComponent.keys().forEach(filePath => {
const componentConfig = requireComponent(filePath)
const fileName = validateFileName(filePath)
const componentName = fileName.toLowerCase() === 'index'
? capitalizeFirstLetter(componentConfig.default.name)
: fileName
Vue.component(componentName, componentConfig.default || componentConfig)
})
这里文件夹结构:
components
│ componentRegister.js
---BasicTable
│ BasicTable.vue
---MultiCondition
│ index.vue
这里对组件名做了判断,如果是index的话就取组件中的name属性处理后作为注册组件名, 所以最后注册的组件为:multi-condition
、 basic-table
最后我们在main.js中import 'components/ componentRegister .js',然后我们就可以随时随地使用这些基础组件,无需手动引入了~
4. 不同路由的组件复用
当某个场景中vue-router从/post-page/a,跳转到/post-page/b。然后我们惊人的发现,页面跳转后数据竟然没更新?!原因是vue-router"智能地"发现这是同一个组件,然后它就决定要复用这个组件,所以你在created函数里写的方法压根就没执行。通常的解决方案是监听$route的变化来初始化数据,如下:
data() {
return {
loading: false,
error: null,
post: null
}
},
watch: {
'$route': { // 使用watch来监控是否是同一个路由
handler: 'resetData',
immediate: true
}
},
methods: {
resetData() {
this.loading = false
this.error = null
this.post = null
this.getPost(this.$route.params.id)
},
getPost(id){ }
}
为了实现这样的效果可以给 router-view
添加一个不同的key,这样即使是公用组件,只要url变化了,就一定会重新创建这个组件。
还可以在其后加 ++newDate()
时间戳,保证独一无二
感谢网友 @rolitter 的提醒,如果组件被放在
中的话,可以把获取新数据的方法放在activated钩子,代替原来在created、mounted钩子中获取数据的任务。
5. 组件事件属性穿透
// 父组件
// 子组件
vue的组件实例中的 $props
、 $attrs
给我们提供了很大的便利,特别是父子组件传值的时候。1、 每一个从父组件传到子组件的props,我们都得在子组件的Props中显式的声明才能使用。这样一来,我们的子组件每次都需要申明一大堆props,这里我们知道 v-bind 是可以传对象 的,可以在 vm.$props
中拿到所有父组件props的值 v-bind="$props"
2、 类似placeholer这种dom原生的property可以使用 $attrs
直接从父传到子,无需声明。方法如下:
$attrs
包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind="$attrs"
传入内部组件。
3、 注意到子组件的 @focus="$emit('focus', $event)"
其实什么都没做,只是把event传回给父组件而已,那其实和上面类似,完全没必要显式地申明:
$listeners
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners"
传入内部组件——在创建更高层次的组件时非常有用。
需要注意的是,由于我们input并不是BaseInput这个组件的根节点,而默认情况下父作用域的不被认作 props
的特性绑定将会“回退”且作为普通的 HTML 特性应用在子组件的根元素上。所以我们需要设置 inheritAttrs:false
,这些默认行为将会被去掉,上面优化才能成功。
6. 路由根据开发状态懒加载
一般我们在路由中加载组件的时候:
import Login from '@/views/login.vue'
export default new Router({
routes: [{ path: '/login', name: '登陆', component: Login} ]
})
当你需要懒加载 lazy-loading 的时候,需要一个个把routes的component改为 ()=>import('@/views/login.vue')
,甚为麻烦。
当你的项目页面越来越多之后,在开发环境之中使用 lazy-loading 会变得不太合适,每次更改代码触发热更新都会变得非常的慢。所以建议只在生成环境之中使用路由懒加载功能。
根据Vue的异步组件和Webpack的代码分割功能可以轻松实现组件的懒加载,如:
const Foo = () => import('./Foo.vue')
在区分开发环境与生产环境时,可以在路由文件夹下分别新建两个文件:
_import_production.js
module.exports = file => () => import('@/views/' + file + '.vue')
_import_development.js
(这种写法 vue-loader
版本至少v13.0.0以上)
module.exports = file => require('@/views/' + file + '.vue').default
而在设置路由的 router/index.js
文件中:
const _import = require('./_import_' + process.env.NODE_ENV)
export default new Router({
routes: [{ path: '/login', name: '登陆', component: _import('login/index') } ]
})
这样组件在开发环境下就是非懒加载,生产环境下就是懒加载的了
7 vue-loader小技巧
vue-loader 是处理 *.vue 文件的 webpack loader。它本身提供了丰富的 API,有些 API 很实用但很少被人熟知。例如接下来要介绍的 preserveWhitespace
和 transformToRequire
preserveWhitespace
减少文件体积有些时候我们在写模板时不想让元素和元素之间有空格,可能会写成这样:
- 1111
- 2222
- 333
当然还有其他方式,比如设置字体的 font-size:0
,然后给需要的内容单独设置字体大小,目的是为了去掉元素间的空格。其实我们完全可以通过配置 vue-loader 实现这一需求。
{
vue: {
preserveWhitespace: false
}
}
它的作用是阻止元素间生成空白内容,在 Vue 模板编译后使用 _v(" ")
表示。如果项目中模板内容多的话,它们还是会占用一些文件体积的。例如 Element 配置该属性后,未压缩情况下文件体积减少了近 30Kb。
transformToRequire
再也不用把图片写成变量了以前在写 Vue 的时候经常会写到这样的代码:把图片提前 require 传给一个变量再传给组件。
其实通过配置 transformToRequire
后,就可以直接配置,这样vue-loader会把对应的属性自动 require 之后传给组件
{
vue: {
transformToRequire: {
avatar: ['default-src']
}
}
}
于是我们代码就可以简化不少
在 vue-cli 的 webpack 模板下,默认配置是:
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
可以举一反三进行一下类似的配置
vue-loader 还有很多实用的 API 例如最近加入的 自定义块,感兴趣的各位可以去文档里找找看。
8. render 函数
在某些场景下你可能需要 render 渲染函数带来的完全编程能力来解决不太容易解决的问题,特别是要动态选择生成标签和组件类型的场景。
1. 一般情况
比如根据props来生成标签的场景
其中level是data中的变量,可以看到这里有大量重复代码,如果逻辑复杂点,加上一些绑定和判断就更复杂了,这里可以利用 render 函数来对要生成的标签加以判断。
2. 优化
使用 render 方法根据参数来生成对应标签可以避免上面的情况。
Hello world!
示例可以查看 CodePen
当然render函数还有很多用法,比如要使用动态组件,除了使用 :is
之外也可以使用render函数
9.将一个 prop 限制在一个类型的列表中
我们在使用 prop 时,可能会有时候需要判断该 prop 是否在我们规定的范围内(或者说规定的值内),此时可以使用 prop 定义中的 validator 选项,将一个 prop 类型限制在一组特定的值中。
export default {
name: 'Test',
props: {
name: {
type: String,
},
size: {
type: String,
validator: s => ['small', 'middle'].includes(s)
}
}
};
这个验证函数接受一个 prop,如果 prop 有效或无效,则返回 true 或 false。当单单传入的 true 或 false 来控制某些条件不能满足需求时,我通常使用这个方法来做。
10. 使用引号来监听嵌套属性
你可能使用过 watch 来监听过某个属性,但是你可能不知道这一点,我们可以通过使用引号轻松地直接监听嵌套值
watch {
'obj.message.name'() {
// ...
}
}
11.知道何时使用 v-if
有时候使用 v-if,往往使用 v-show 来代替,会有更高的性能。
当 v-if 被打开或关闭时,它将创建并完全销毁该元素。相反,v-show 将创建该元素并将其留在那里,通过设置其样式为 display: none 来隐藏它,如果你要切换的组件的渲染成本很高,那么这样做会更有效率,特别是对于结构复杂的组件或页面来说,更推荐使用 v-show。
反过来说,如果不需要立即执行昂贵的组件,可以使用 v-if,这样它就会跳过渲染它,而使页面的加载速度更快一些。
12.单个作用域插槽的简写
限定范围的插槽非常有趣,但为了使用它们,您还必须使用许多模板标记。幸运的是,有一个简写可以让我们摆脱它,但只有在我们使用单个作用域槽的情况下。
你或许这样使用过
不使用 template
13.将局部和全局的 style 混合在一起
14.重写子组件的样式
Scoped CSS 在保持内容整洁方面非常棒,而且不会将样式引入应用的其他组件中。但有时我们需要覆盖一个子组件的样式,并跳出这个作用域。
Vue 有一个 deep 选择器可以完成以上功能
注意:如果你使用像 LESS 这样的 CSS 预处理器,你可能需要使用 /deep/ 来代替
15.如何在 Vue 之外创建一个具有响应性的变量
如果你从 Vue 之外得到一个变量,让它具有反应性是很好的。这样,我们就可以在 computed props、watch 和其他任何地方使用它,它的工作方式就像 Vue 中的任何其他状态一样。
如果我们使用的选项 API,需要的只是将其放在组件的数据部分中
const externalVariable = getValue();
export default {
data() {
return {
reactiveVariable: externalVariable,
};
}
};
使用observable 代替
import Vue from 'vue'
// 可以完全在 Vue 组件之外完成
const externalVariable = getValue();
const anotherReactiveVariable = Vue.observable(externalVariable);
16. v-for 中的解构
// users: [{name:'leo',id:'1'},{name:'lion',id:'2'}]
{{ name }}
通过使用这样的元组从 v-for 中取出索引
{{ index + 1 }} => {{ movie }}
当使用一个对象时,可以这样使用 key
{{ key }}: {{ value }}
也可以将这两种方法结合起来,获取 key 以及属性的 index
{{ index + 1 }} => {{ key }}: {{ value }}
17.在指定范围内循环
v-for 允许我们遍历数组,也允许我们遍历一个范围
- Item {{ num }}
这里有个注意点,当我们使用 v-for 遍历一个范围时,它将从 1 开始,以我们指定的数字结束。
18.窃取 prop 类型
我们从一个子组件中复制 prop 类型,只是为了在一个父组件中使用它们。但是往往偷取这些 prop 类型要比仅仅复制它们好得多,什么意思?
举个例子,我们在某个组件中使用了一个 List 组件
{{ title }}
为了让它正常工作,我们需要添加正确的 prop 类型,从 List 组件复制
import List from './List';
export default {
components: { List },
props: {
listType: {
type: String,
required: true,
},
listSize: {
type: String,
default: 'medium',
validator: size => [
'small',
'medium',
'large',
'x-large'
].includes(size),
},
listName: {
type: String,
default: 'list',
},
title: {
type: String,
required: true,
},
},
};
看下来,有没有觉得很麻烦?
当 List 组件的 prop 类型被更新时,我们肯定会忘记返回这个组件并更新它们。随着时间的推移,当该组件的 prop 类型开始偏离 List 组件中的 prop 类型时,就会引入错误。
因此,这就是为什么我们要窃取组件的 prop 类型,具体如下
import List from './List';
export default {
components: { List },
props: {
...List.props, // 窃取List的prop,不需要再重复累赘地一一列出
title: {
type: String,
required: true,
},
},
};
这样是不是简便了很多?
除了在上面的例子中,我们把 List 加在每个 prop 名称的开头。所以我们必须做一些额外的工作来实现这一点
import List from './List';
const listProps = {};
Object.entries(List.props).forEach((key, val) => {
listProps[`list${key.toUpperCase()}`] = val;
});
export default {
components: { List },
props: {
...listProps,
title: {
type: String,
required: true,
},
},
};
现在,如果 List 组件中的 prop 类型被修改,我们的组件将保持最新状态。但是,如果一个 prop 类型从 List 组件中被添加或删除了呢?为了应对这些情况,我们可以使用 v-bind 和一个计算的 prop 来保持动态。
19.检测元素外部(或内部)的单击
当我们需要检测一个点击是发生在一个特定元素 el 的内部还是外部,通常使用的方法
window.addEventListener('click', e => {
// 获取被点击的元素
const currtentEl = e.target;
// 检测在el元素的内部还是外部
if (el.contains(currtentEl)) {
// 在el里面点击了
} else {
// 在el外面点击了
}
});
20.从组件外部调用一个方法
我们可以从一个组件的外部通过给它一个 ref 用来调用一个方法
// 在该组件调用子组件的方法
this.$refs.child.method();
通常情况下,我们使用 props 和 events 在组件之间进行交流。props 被下发到子组件中,而 events 被上发到父组件中
// Child.vue
export default {
props: ['trigger'],
watch: {
shouldCallMethod(newVal) {
if (newVal) {
// Call the method when the trigger is set to `true`
this.method();
}
}
}
}
这可以正常工作,但只能在第一次调用时使用。如果您需要多次触发此操作,则必须进行清理并重置状态。逻辑如下
相反,如果我们在子组件上设置一个 ref,我们可以直接调用该方法(如最开始的方式),我们打破了 “props down, events up” 的规则,我们打破了封装,但是这样做更清晰,更容易理解。
21.监听数组和对象
有时候使用 watcher 不能正确触发,很多情况下是因为我们试图监听数组或对象,但没有将 deep 设置为 true
export default {
name: 'namesChange',
props: {
names: {
type: Array,
required: true,
},
},
watch: {
names: {
// 这将让 Vue 知道要在数组内部寻找,如不添加,则不能进行深度监听
deep: true,
handler(newVal,oldVal)
console.log('The list of names has changed!');
}
}
}
}
22.template 标签的另一个用途
template 标签可以在模板中的任何地方使用,以更好地组织代码。有时它来简化 v-if 逻辑,有时也用 v-for。如下,我们有几个元素都使用同一个 v-if 条件
{{ title }}
{{ subheading }}
仔细观察上面的代码,我们会发现一些元素的显示与隐藏的条件是一样的,但是这样写并不友好,当在一个更大、更复杂的组件上,有可能是一个糟糕的情况。
我们可以使用 template 标签来分组这些元素(v-if 条件一致的元素),并将 v-if 提升到模板 template 本身,达到优化的目的
{{ title }}
{{ subheading }}
23.处理错误(和警告)的更好方法
我们可以为 Vue 中的错误和警告提供一个全局的自定义处理程序
// Vue 2
Vue.config.errorHandler = (err) => {
alert(err);
};
当然你也可以用它们来更优雅地处理错误,以获得更好的用户体验。例如,如果一个错误未被处理,应用程序不会直接崩溃,而是显示一个完整的错误屏幕,提示用户刷新或尝试其他操作。
24.默认内容和扩展点
Vue中的槽可以有默认的内容,这使我们可以制作出更容易使用的组件。
我们可以取组件的任何部分,将其封装在一个插槽中,在外面我们可以用想要的任何内容覆盖组件的该部分。默认情况下,它仍然会按照原来的方式工作,但这样做会有了更多的选项
现在我们可以用许多不同的方式使用这个组件。简单的、默认的方式,或者自定义的方式。
Do something a little different here
25. 有条件地渲染插槽
每个Vue组件都有一个特殊的$slots对象,里面有你所有的插槽。默认槽的键是default,任何被命名的槽都使用其名称作为键。
const $slots = {
default: ,
icon: ,
button:
但这个$slots对象只有适用于该组件的插槽,而不是每一个定义的插槽。
拿这个定义了几个插槽的组件来说,包括几个命名的插槽。
Here are some slots
如果我们只对组件应用一个插槽,那么只有那个插槽会显示在我们的$slots对象中。
This will be applied to the second slot.
$slots = { second: }
我们可以在我们的组件中使用这一点来检测哪些插槽已经被应用到组件中,例如,通过隐藏插槽的包装元素。
A wrapped slot
现在,应用样式的包装器div只有在我们用某些东西填充这个插槽时才会被渲染。
如果不使用v-if,那么如果没有插槽,就会得到一个空的不必要的div。根据div的样式,这可能会打乱我们的布局,让界面看起来很奇怪。
那么,为什么我们希望能够有条件地渲染插槽呢?
使用条件插槽的主要原因有三个:
当使用封装的div来添加默认样式时
插槽是空的
如果我们将默认内容与嵌套槽相结合
例如,当我们在添加默认样式时,我们在插槽周围添加一个div:
This is a pretty great component, amirite?
然而,如果父组件没有将内容应用到该插槽中,我们最终会在页面上渲染出一个空的div。
This is a pretty great component, amirite?
26.如何监听一个插槽的变化
有时我们需要知道插槽内的内容何时发生了变化。
不幸的是,Vue没有内置的方法让我们检测这一点。
然而,我的朋友Austin想出了一个非常干净的方法,使用MutationObserver来做这件事。
MutationObserver接口提供了监视对DOM树所做更改的能力。它被设计为旧的Mutation Events功能的替代品,该功能是DOM3 Events规范的一部分。
export default {
mounted() {
// 当有变化时调用`update`
const observer = new MutationObserver(this.update);
// 监听此组件的变化
observer.observe(this.$el, {
childList: true,
subtree: true
});
}
};
27.用Vue Router进行深度链接
我们可以在URL中存储(一点)状态,允许我们直接跳到页面上的一个特定状态。
例如,你加载一个已经选择了日期范围过滤器的页面:
someurl.com/edit?date-range=last-week
这对于应用中用户可能共享大量链接的部分来说是非常棒的,对于服务器渲染的应用,或者在两个独立的应用之间通信的信息比普通链接通常提供的更多。
我们可以存储过滤器、搜索值、模态框是打开还是关闭,或者在列表的哪个位置滚动以完美地实现无限分页。
使用 vue-router 获取查询参数是这样工作的(这也适用于大多数Vue框架,如Nuxt和Vuepress):
const dateRange = this.$route.query.dateRange;
为了改变它,我们使用 RouterLink 组件并更新 query。
❤️ 谢谢支持
以上便是本次分享的全部内容,希望对你有所帮助^_^
喜欢的话别忘了 分享、点赞、收藏 三连哦~。