组件定义
SFC 单文件组件
<template>
<p class="demo">
<button class="btn-primary" @click.prevent="handleClick">
<slot></slot>(clicked - {{ count }})
</button>
</p>
</template>
<script>
export default {
data() {
return {
count: 0
};
},
methods: {
handleClick() {
this.count++;
console.log("clicked", this.count);
}
}
};
</script>
<style scoped>
.btn-primary {
display: inline-block;
font-size: 1.2rem;
color: #fff;
background-color: #3eaf7c;
padding: 0.8rem 1.6rem;
border-radius: 4px;
transition: background-color 0.1s ease;
box-sizing: border-box;
border-bottom: 1px solid #389d70;
}
</style>
字符串模板或 ES6 模板字面量
Vue.component('my-btn', { template: `
<button class="btn-primary" @click.prevent="handleClick">
<slot></slot>(clicked - {{ count }})
</button>
`, data() { return { count: 0, }; }, methods: { handleClick() { this.count++;
console.log('clicked', this.count); }, }, });
渲染函数
Vue.component('my-btn', { data() { return { count: 0, }; }, methods: {
handleClick() { this.count++; console.log('clicked', this.count); }, },
render(h) { return h( 'button', { attrs: { class: 'btn-primary', }, on: { click:
this.handleClick, }, }, this.$slots.default ); }, });
JSX 语法糖
Vue.component('my-btn', { data() { return { text: 'Click me', }; }, methods: {
handleClick() { console.log('clicked'); }, }, render() { return (
<button class="btn-primary" @click.prevent="handleClick">
{this.$slots.default}(clicked - {{ count }})
</button>
); }, });
Vue 类组件装饰器
<template>
<button class="btn-primary" @click.prevent="handleClick">
<slot></slot>(clicked - {{ count }})
</button>
</template>
<script>
import Vue from 'vue';
import Component from 'vue-class-component';
@Component
export default MyBtn extends Vue {
count = 0;
handleClick() {
this.count++;
console.log('clicked', this.count);
}
}
</script>
<style scoped>
.btn-primary {
background-color: blue;
}
</style>
参考
- https://vuejs.org/v2/guide/single-file-components.html
- https://vuejs.org/v2/guide/render-function.html
- https://medium.com/js-dojo/7-ways-to-define-a-component-template-in-vuejs-c04e0c72900d
- https://codewithhugo.com/writing-multiple-vue-components-in-a-single-file/
组件通信
Prop属性和事件
vue组件基本上遵循单向数据流原则,即数据通过props向下传递子组件,子组件向上触发事件。
props数据只读,因此无法子组件无法改变props。 当props变化时,子组件会自动重新渲染。(props是响应式数据源)
子组件只能向父组件触发事件,父组件接收事件,改变映射至子组件的props数据。
<template>
<button @click="$emit('click');">{{ text }}</button>
</template>
<script>
export default {
name: 'v-btn',
props: {
text: String,
},
};
</script>
<template>
<v-btn :text="buttonText" @click="handleClick"></v-btn>
</template>
<script>
export default {
data() {
return {
clickCount: 0,
buttonText: 'initial button text',
},
};
methods: {
handleClick() {
this.buttonText = `Button clicked ${++this.clickCount}`;
console.log('clicked', this.buttonText);
},
},
};
</script>
参考
https://vuejs.org/v2/guide/components-props.html
https://alligator.io/vuejs/component-communication/
https://www.smashingmagazine.com/2017/08/creating-custom-inputs-vue-js/
https://vegibit.com/vue-sibling-component-communication/
https://medium.com/fullstackio/managing-state-in-vue-js-23a0352b1c87
https://gambardella.info/2017/09/13/vue-js-communication-part-2-parent-child-components/
组件事件处理
参考
- https://vuejs.org/v2/guide/components-custom-events.html
- https://itnext.io/leveraging-vue-events-to-reduce-prop-declarations-e38f5dce2aaf
- https://alligator.io/vuejs/component-event-hooks/
- https://alligator.io/vuejs/global-event-bus/
- https://medium.com/@jesusgalvan/vue-js-event-bus-promises-f83e73a81d72
组件条件渲染
指令
v-if
<h1 v-if="true">Render only if v-if condition is true</h1>
v-if
和 v-else
<h1 v-if="true">Render only if v-if condition is true</h1>
<h1 v-else>Render only if v-if condition is false</h1>
v-else-if
<div v-if="type === 'A'">Render only if `type` is equal to `A`</div>
<div v-else-if="type === 'B'">Render only if `type` is equal to `B`</div>
<div v-else-if="type === 'C'">Render only if `type` is equal to `C`</div>
<div v-else>Render if `type` is not `A` or `B` or `C`</div>
v-show
<h1 v-show="true">
Always rendered, but it should be visible only if `v-show` conditions is true
</h1>
如果想条件渲染多个元素,可以在模板template
元素上使用条件指令。
注意模板元素仅仅是隐形包装,不会渲染成DOM。
<template v-if="true">
<h1>All the elements</h1>
<p>will be rendered into DOM</p>
<p>except `template` element</p>
</template>
渲染函数或JSX语法糖
如果你在vue应用中使用渲染函数或jsx
, 可以直接使用 if-else
和switch-case
语句以及三元和逻辑运算符。
if-else
语句
export default {
data() {
return {
isTruthy: true,
};
},
render(h) {
if (this.isTruthy) {
return <h1>Render value is true</h1>;
} else {
return <h1>Render value is false</h1>;
}
},
};
switch case
语句
import Info from './Info';
import Warning from './Warning';
import Error from './Error';
import Success from './Success';
export default {
data() {
return {
type: 'error',
};
},
render(h) {
switch (this.type) {
case 'info':
return <Info text={text} />;
case 'warning':
return <Warning text={text} />;
case 'error':
return <Error text={text} />;
default:
return <Success text={text} />;
}
},
};
或者你可以使用map
对象简化switch-case
import Info from './Info';
import Warning from './Warning';
import Error from './Error';
import Success from './Success';
const COMPONENT_MAP = {
info: Info,
warning: Warning,
error: Error,
success: Success,
};
export default {
data() {
return {
type: 'error',
};
},
render(h) {
const Comp = COMPONENT_MAP[this.type || 'success'];
return <Comp />;
},
};
三元
运算符
export default {
data() {
return {
isTruthy: true,
};
},
render(h) {
return (
<div>
{this.isTruthy ? (
<h1>Render value is true</h1>
) : (
<h1>Render value is false</h1>
)}
</div>
);
},
};
逻辑运算符
export default {
data() {
return {
isLoading: true,
};
},
render(h) {
return <div>{this.isLoading && <h1>Loading ...</h1>}</div>;
},
};
参考
https://vuejs.org/v2/guide/conditional.html
https://dzone.com/articles/difference-between-v-if-and-v-show-with-a-video
动态组件
带有is属性组件
<component :is="currentTabComponent"></component>
上例中,如果有不同组件渲染进入component
组件,渲染过的组件将会销毁。
如果你想要component
标签内部实例不被销毁,可以在外面再包一层keep-alive
:
<keep-alive> <component :is="currentTabComponent"></component> </keep-alive>
参考
https://vuejs.org/v2/guide/components.html#Dynamic-Components
https://vuejs.org/v2/guide/components-dynamic-async.html
https://medium.com/scrumpy/dynamic-component-templates-with-vue-js-d9236ab183bb
组件组合
第三方库
基础组合
<template>
<div class="component-b"><component-a></component-a></div>
</template>
<script>
import ComponentA from './ComponentA';
export default {
components: {
ComponentA,
},
};
</script>
参考
https://vuejs.org/v2/guide/#Composing-with-Components
扩展
当你想扩展单文件组件时可以使用扩展:
<template>
<button class="button-primary" @click.prevent="handleClick">
{{ buttonText }}
</button>
</template>
<script>
import BaseButton from './BaseButton';
export default {
extends: BaseButton,
props: ['buttonText'],
};
</script>
参考
https://vuejs.org/v2/api/#extends
https://medium.com/js-dojo/extending-vuejs-components-42fefefc688b
混入
// closableMixin.js
export default {
props: {
isOpen: {
default: true,
},
},
data: function() {
return {
shown: this.isOpen,
};
},
methods: {
hide: function() {
this.shown = false;
},
show: function() {
this.shown = true;
},
toggle: function() {
this.shown = !this.shown;
},
},
};
<template>
<div
v-if="shown"
class="alert alert-success"
:class="'alert-' + type"
role="alert"
>
{{ text }}
<i class="pull-right glyphicon glyphicon-remove" @click="hide"></i>
</div>
</template>
<script>
import closableMixin from './mixins/closableMixin';
export default {
mixins: [closableMixin],
props: ['text'],
};
</script>
参考
https://vuejs.org/v2/guide/mixins.html
http://www.qcode.in/practical-use-of-components-and-mixins-in-vue-js/
默认插槽
/* VBtn */
<template>
<button class="btn btn-primary"><slot></slot></button>
</template>
<script>
export default {
name: 'VBtn',
};
</script>
<template>
<v-btn> <span class="fa fa-user"></span> Login </v-btn>
</template>
<script>
import VBtn from './VBtn';
export default {
components: {
VBtn,
},
};
</script>
参考
https://vuejs.org/v2/guide/components-slots.html#Slot-Content
https://alligator.io/vuejs/component-slots/
https://alligator.io/web-components/composing-slots-named-slots/
https://alligator.io/vuejs/vue-abstract-components/
命名插槽
基础布局文件
/* base-layout */
<div class="container">
<header><slot name="header"></slot></header>
<main><slot></slot></main>
<footer><slot name="footer"></slot></footer>
</div>
应用文件
<base-layout>
<template slot="header">
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template slot="footer">
<p>Here's some contact info</p>
</template>
</base-layout>
参考
https://vuejs.org/v2/guide/components-slots.html#Named-Slots
https://medium.com/@fdietz/vue-js-component-composition-with-slots-eda311579218
域插槽
<template>
<ul>
<li v-for="todo in todos" v-bind:key="todo.id">
<!-- We have a slot for each todo, passing it the -->
<!-- `todo` object as a slot prop. -->
<slot v-bind:todo="todo"> {{ todo.text }} </slot>
</li>
</ul>
</template>
<script>
export default {
name: 'TodoList',
props: {
todos: {
type: Array,
default: () => [],
},
},
};
</script>
<template>
<todo-list v-bind:todos="todos">
<template slot-scope="{ todo }">
<span v-if="todo.isComplete">✓</span> {{ todo.text }}
</template>
</todo-list>
</template>
<script>
import TodoList from './TodoList';
export default {
components: {
TodoList,
},
data() {
return {
todos: [
{ todo: 'todo 1', isComplete: true },
{ todo: 'todo 2', isComplete: false },
{ todo: 'todo 3', isComplete: false },
{ todo: 'todo 4', isComplete: true },
];
};
},
};
</script>
参考
https://vuejs.org/v2/guide/components-slots.html#Scoped-Slots
https://medium.com/js-dojo/getting-your-head-around-vue-js-scoped-slots-281bf82a1e4e
https://medium.com/corebuild-software/understanding-scoped-slots-in-vue-js-db5315a42391
https://alligator.io/vuejs/scoped-component-slots/
https://adamwathan.me/the-trick-to-understanding-scoped-slots-in-vuejs/
https://pineco.de/power-scoped-slots-vue/
https://medium.com/@tkwebdev/building-a-list-keyboard-control-component-with-vue-js-and-scoped-slots-c74db4fcf84f
渲染属性
大部分场景可以用域插槽替代渲染属性,但有些场景渲染属性更有用。
使用SFC
<template>
<div id="app"><Mouse :render="__render" /></div>
</template>
<script>
import Mouse from './Mouse.js';
export default {
name: 'app',
components: {
Mouse,
},
methods: {
__render({ x, y }) {
return (
<h1>
The mouse position is ({x}, {y})
</h1>
);
},
},
};
</script>
<style>
* {
margin: 0;
height: 100%;
width: 100%;
}
</style>
使用JSX
const Mouse = {
name: 'Mouse',
props: {
render: {
type: Function,
required: true,
},
},
data() {
return {
x: 0,
y: 0,
};
},
methods: {
handleMouseMove(event) {
this.x = event.clientX;
this.y = event.clientY;
},
},
render(h) {
return (
<div style={{ height: '100%' }} onMousemove={this.handleMouseMove}>
{this.$props.render(this)}
</div>
);
},
};
export default Mouse;
参考
https://vuejs.org/v2/guide/render-function.html
https://medium.com/@dillonchanis/leveraging-render-props-in-vue-7eb9a19c262d
https://medium.com/js-dojo/use-a-vue-js-render-prop-98880bc44e05
https://medium.com/js-dojo/using-react-style-callback-props-with-vue-pros-and-cons-e0ee7455695b
传递 Props 和监听器
有时需要向子组件传递属性和监听器,但不需要声明全部属性。
你可以在子组件中绑定$attrs
和$listeners
,设置inheritAttrs
为false
,
外部div和子组件都会接收到设置的attributes。
传递Props
<template>
<div>
<h1>{{title}}</h1>
<passing-props-child v-bind="$attrs" v-on="$listeners"></passing-props-child>
</div>
</template>
<script>
import PassingPropsChild from './PassingPropsChild';
export default {
components: {
PassingPropsChild,
},
inheritAttrs: false,
props: {
title: {
type: String,
default: 'Hello, Vue!',
},
},
};
</script>
父组件
<template>
<p class="demo">
<passing-props
title="This is from <passing-props />"
childPropA="This is from <passing-props-child />"
@click="handleClickPassingPropsChildComponent"
>
</passing-props>
</p>
</template>
<script>
import PassingProps from './PassingProps';
export default {
components: {
PassingProps,
},
methods: {
handleClickPassingPropsChildComponent() {
console.log('This event comes from `<passing-props-child />`');
alert('This event comes from `<passing-props-child />`');
},
},
};
</script>
参考
https://zendev.com/2018/05/31/transparent-wrapper-components-in-vue.html
高阶组件
参考
- https://medium.com/bethink-pl/higher-order-components-in-vue-js-a79951ac9176
- https://medium.com/bethink-pl/do-we-need-higher-order-components-in-vue-js-87c0aa608f48
- https://medium.com/tldr-tech/higher-order-components-in-vue-js-38b500c6d49f
依赖注入
Vue支持 Provide和Inject机制为所有子代组件提供对象。
不管组件层次有多深,只要都在共同父组件下,机制都可以发挥作用。
注意provide
和inject
绑定都不是响应式的,除非传递可观察对象。
<parent-component>
<child-component>
<grand-child-component></grand-child-component>
</child-component>
</parent-component>
根据上例中组件层次,为了从父组件中获取数据,你应该将数据对象作为props
传递给
子组件和孙组件。但是如果父组件提供
数据对象,孙组件仅定义注入
父组件提供的数据对象可以直接获取数据。
参考
- https://vuejs.org/v2/api/#provide-inject
- https://vuejs.org/v2/guide/components-edge-cases.html#Dependency-Injection
- https://alligator.io/vuejs/component-communication/#provide--inject
- https://blog.kloud.com.au/2017/03/22/dependency-injection-in-vuejs-app-with-typescript/
Provide和Inject API
提示: 可以使用vue属性装饰器的
@Provide
和@Inject
。
主题提供者
<script>
export default {
provide: {
theme: {
primaryColor: '#3eaf7c',
secondaryColor: '#1FA2FF'
},
},
render(h) {
return this.$slots.default[0];
},
};
</script>
带主题按钮
<template>
<button class="btn" :style="{ color: '#fff', backgroundColor: (primary && theme.primaryColor) || (secondary && theme.secondaryColor) }">
<slot></slot>
</button>
</template>
<script>
export default {
inject: {
theme: {
default: {},
},
},
props: {
primary: {
type: Boolean,
default: false,
},
secondary: {
type: Boolean,
default: false,
},
},
};
</script>
<theme-provider>
<p class="demo">
<button class="btn">Normal Button</button>
<theme-button secondary>Themed Button</theme-button>
</p>
</theme-provider>
错误处理
errorCaptured 钩子函数
错误边界
<script>
export default {
name: 'ErrorBoundary',
data() {
return {
error: false,
errorMessage: '',
};
},
errorCaptured(err, vm, info) {
this.error = true;
this.errorMessage = `Sorry, error occured in ${info}`;
return false;
},
render(h) {
if (this.error) {
return h('p', { class: 'demo bg-danger' }, this.errorMessage);
}
return this.$slots.default[0];
},
};
</script>
抛出错误
<template>
<p class="demo">
<button class="btn btn-danger" @click.prevent="throwError()">Error Thrown Button ({{count}})</button>
</p>
</template>
<script>
export default {
data() {
return {
count: 0,
};
},
watch: {
count() {
throw new Error('error');
},
},
methods: {
throwError() {
this.count++;
},
},
};
</script>
<error-boundary> <throw-error></throw-error> </error-boundary>
参考
- https://medium.com/@dillonchanis/handling-errors-in-vue-with-error-boundaries-91f6ead0093b
- https://jsfiddle.net/Linusborg/z84wspcg/
效率建议
创建时执行观察
// don't
created() {
this.fetchUserList();
},
watch: {
searchText: 'fetchUserList',
}
// do
watch: {
searchText: {
handler: 'fetchUserList',
immediate: true,
}
}
网友评论