在渲染时,会被替换为组件的内容。 常见的限制元素还有
、
、
。
提示: 如果使用的是字符串模板,是不受限制的,比如后面章节介绍的.vue
单文件用法等。
除了template
选项外,组件还可以像Vue实例那样使用其他的选项,比如data
、computed
、method
等。 但是在使用data
时,和实例稍有区别,data
必须是函数,然后将数据return
出去。 例如:
1 < div id ="app" >
2 < my-component > my-component >
3 div >
4
5 < script >
6 Vue.component( " my-component " , {
7 template: " {{message}}
" ,
8 data: function () {
9 return {
10 message: " 组件内容 "
11 };
12 }
13 });
14
15 var app = new Vue({
16 el: " #app "
17 });
18 script >
JavaScript对象是引用关系,所以如果return
出的对象引用了外部的一个对象,那这个对象就是共享的,任何一方修改都会同步。 比如下面的示例:
1 < div id ="app" >
2 < my-component > my-component >
3 < my-component > my-component >
4 < my-component > my-component >
5 div >
6
7 < script >
8 var data = {
9 counter: 0
10 };
11
12 Vue.component( " my-component " , {
13 template: " {{counter}} " ,
14 data: function () {
15 return data;
16 }
17 });
18
19 var app = new Vue({
20 el: " #app "
21 });
22 script >
组件使用了3次,但是点击任意一个
,3个按钮的数字都会加1。 那是因为组件的data
引用的是外部的对象,这肯定不是我们期望的效果。 所以给组件返回一个新的data
对象来独立,示例代码如下:
1 < div id ="app" >
2 < my-component > my-component >
3 < my-component > my-component >
4 < my-component > my-component >
5 div >
6
7 < script >
8 Vue.component( " my-component " , {
9 template: " {{counter}} " ,
10 data: function () {
11 return {
12 counter: 0
13 };
14 }
15 });
16
17 var app = new Vue({
18 el: " #app "
19 });
20 script >
这样,点击3个按钮就互不影响了,完全达到复用的目的。
7.2 使用props传递数据
7.2.1 基本用法
组件不仅仅是要把模板的内容进行复用,更重要的是组件间要进行通信。 通常父组件的模板中包含子组件,父组件要正向地向子组件传递数据或参数,子组件接收到后根据参数的不同来渲染不同的内容或执行操作。 这个正向传递数据的过程就是通过props
来实现的。
在组件中,使用选项props
来声明需要从父级接收的数据。props
的值可以是两种,一种是字符串数组,一种是对象,本小节先介绍数组的用法。 比如我们构造一个数组,接收一个来自父级的数据message
,并把它在组件模板中渲染,示例代码如下:
1 < div id ="app" >
2 < my-component message ="来自父组件的数据" > my-component >
3 div >
4
5 < script >
6 Vue.component( " my-component " , {
7 props: [ " message " ],
8 template: " {{message}}
"
9 });
10
11 var app = new Vue({
12 el: " #app "
13 });
14 script >
渲染后的结果为:
1 < div id ="app" >
2 < div > 来自父组件的数据div >
3 div >
props
中声明的数据与组件data
函数return
的数据主要区别就是props
的来自父级,而data
中的是组件自己的数据,作用域是组件本身,这两种数据都可以在模板template
及计算属性computed
和方法methods
中使用。 上例的数据message
就是通过props
从父级传递过来的,在组件的自定义标签上直接写该props
的名称,如果要传递多个数据,在props
数组中添加项即可。
由于HTML特性不区分大小写,当使用DOM模板时,驼峰命名(camelCase)的props
名称要转为短横分隔命名(kebab-case)。例如:
1 < div id ="app" >
2 < my-component warning-text ="提示信息" > my-component >
3 div >
4
5 < script >
6 Vue.component( " my-component " , {
7 props: [ " warningText " ],
8 template: " {{warningText}}
"
9 });
10
11 var app = new Vue({
12 el: " #app "
13 });
14 script >
提示: 如果使用的是字符串模板,仍然可以忽略这些限制。
有时候,传递的数据并不是直接写死的,而是来自父级的动态数据,这是可以使用指令v-bind
来动态绑定props
的值,当父组件的数据变化时,也会传递给子组件。示例代码如下:
1 < div id ="app" >
2 < input type ="text" v-model ="parentMessage" >
3 < my-component :message ="parentMessage" > my-component >
4 div >
5
6 < script >
7 Vue.component( " my-component " , {
8 props: [ " message " ],
9 template: " {{message}}
"
10 });
11
12 var app = new Vue({
13 el: " #app " ,
14 data: {
15 parentMessage: ""
16 }
17 });
18 script >
这里用v-model
绑定了父级的数据parentMessage
。 当通过输入框任意输入时,子组件接收的props
(message
)也会实时响应,并更新组件模板。
提示: 注意,如果你要直接传递数字、布尔值、数组、对象,而且不使用v-bind
,传递的仅仅是字符串,尝试下面的示例来对比。
1 < div id ="app" >
2 < my-component message ="[1,2,3]" > my-component >
3 < my-component :message ="[1,2,3]" > my-component >
4 div >
5 < script >
6 Vue.component( " my-component " , {
7 props: [ " message " ],
8 template: " {{message.length}}
"
9 });
10 var app = new Vue({
11 el: " #app "
12 });
13 script >
同一个组件使用了两次,区别仅仅是第二个使用的是v-bind
。
渲染后的结果:第一个是7,第二个才是数组的长度3。
7.2.2 单向数据流
Vue 2.x与Vue 1.x比较大的一个改变就是,Vue 2.x通过props
传递数据是单向的了,也就是父组件数据变化时会传递给子组件,但是反过来不行。 而在Vue 1.x里提供了.sync
修饰符来支持双向绑定。 之所以这样设计,是尽可能将父子组件解耦,避免子组件无意中修改了父组件的状态。
业务中经常用到两种需要改变prop
的情况,一种是父组件传递初始值进来,子组件将它作为初始值保存起来,在自己的作用域下可以随意使用和修改。 何种情况可以在组件data
内再声明一个数据,引用父组件的prop
,实例代码如下:
1 < div id ="app" >
2 < my-component :init-count ="1" > my-component >
3 div >
4
5 < script >
6 Vue.component( " my-component " , {
7 props: [ " initCount " ],
8 template: " {{count}}
" ,
9 data: function () {
10 return {
11 count: this .initCount
12 };
13 }
14 });
15
16 var app = new Vue({
17 el: " #app "
18 });
19 script >
组件中声明了数据count
,它在组件初始化时会获取来自父组件的initCount
,之后就与之无关了,只用维护count
,这样就可以避免直接操作initCount
。
另一种情况就是prop
作为需要转变的原始值传入。 这种情况用计算属性就可以了,示例代码如下:
1 < div id ="app" >
2 < my-component :width ="100" > my-component >
3 div >
4
5 < script >
6 Vue.component( " my-component " , {
7 props: [ " width " ],
8 template: " 组件内容
" ,
9 computed: {
10 style: function () {
11 return {
12 width: this .width + " px "
13 };
14 }
15 }
16 });
17
18 var app = new Vue({
19 el: " #app "
20 });
21 script >
因为用CSS传递宽度要带单位(px),但是每次都写太麻烦,而且数值计算一般是不带单位的,所以统一在组件内使用计算属性就可以了。
提示: 注意,在JavaScript中对象和数组是引用类型,指向同一个内存空间,所以props
是对象和数组时,在子组件内改变时会影响父组件的。
7.2.3 数据验证
我们上面所介绍的props
选项的值都是一个数组,一开始也介绍过,除了数组外,还可以是对象,当prop
需要验证时,就需要对象写法。
一般当你的组件需要提供给别人使用时,推荐都进行数据验证。 比如某个数据必须是数字类型,如果传入字符串,就会在控制台弹出警告。
一下是几个prop
的示例:
1 Vue.component("my-component", {
2 props: {
3 // 必须是数字类型
4 propA: Number,
5 // 必须是字符串或数字类型
6 propB: [String, Number],
7 // 布尔值,如果没有定义,默认值就是true
8 propC: {
9 type: Boolean,
10 default : true
11 },
12 // 数字,而且是必传
13 propD: {
14 type: Number,
15 required: true
16 },
17 // 如果是数组或对象,默认值必须是一个函数来返回
18 propE: {
19 type: Array,
20 default : function () {
21 rturn[];
22 }
23 },
24 // 自定义一个验证函数
25 propF: {
26 validator: function (value) {
27 return value > 10;
28 }
29 }
30 }
31 });
验证的type
类型可以是:
String
Number
Boolean
Object
Array
Function
type
也可以是一个自定义构造器,使用instanceof
检测。 当prop
验证失败时,在开发版本下会在控制台抛出一条警告。
7.3 组件通信
我们已经知道,从父组件向子组件通信,通过props
传递数据就可以了。 但Vue组件通信的场景不止有这一种,归纳起来,组件之间通信可以用图7-2表示。 
组件关系可分为父子组件通信、兄弟组件通信、跨级组件通信。 本节将介绍组中组件之间通信的方法。
7.3.1 自定义事件
当子组件需要向父组件传递数据时,就要用到自定义事件。 我们在介绍指令 v-on 时有提到,v-on
除了监昕DOM事件外,还可以用于组件之间的自定义事件。
如果你了解过JavaScript的设计模式一一观察者模式,一定知道dispatchEvent
和addEventListener
这两个方法。 Vue组件也有与之类似的一套模式,子组件用$emit()
来触发事件,父组件用$on()
来监昕子组件的事件。
父组件也可以直接在子组件的自定义标签上使用v-on
来监昕子组件触发的自定义事件,示例代码如下:
1 < div id ="app" >
2 < p > 总数:{{total}}p >
3 < my-component @increase ="handleGetTotal" @reduce ="handleGetTotal" > my-component >
4 div >
5
6 < script >
7 Vue.component( " my-component " , {
8 template: " +1 -1
" ,
9 data: function () {
10 return {
11 counter: 0
12 }
13 },
14 methods: {
15 handleIncrease: function () {
16 this .counter ++ ;
17 this .$emit( " increase " , this .counter);
18 },
19 handleReduce: function () {
20 this .counter -- ;
21 this .$emit( " reduce " , this .counter);
22 }
23 }
24 });
25
26 var app = new Vue({
27 el: " #app " ,
28 data: {
29 total: 0
30 },
31 methods: {
32 handleGetTotal: function (total) {
33 this .total = total;
34 }
35 }
36 });
37 script >
上面示例中,子组件有两个按钮,分别实现加1和减1的效果,在改变组件的data
(counter
)后,通过$emit()
再把它传递给父组件,父组件用v-on:increase
和v-on:reduce
(示例使用的是语法糖)。$emit()
方法的第一个参数是自定义事件的名称,例如示例的increase
和reduce
后面的参数都是要传递的数据,可以不填或填写多个。
除了用v-on
在组件上监听自定义事件外,也可以监听DOM事件,这时可以使用.native
修饰符表示监听的是一个原生事件,监听的是该组件的根元素,实例代码如下:
1 < my-component v-on:click.native ="handleClick" > my-component >
7.3.2 使用v-model
Vue 2.x可以在自定义组件上使用v-model
指令,我们先来看一个示例:
1 < div id ="app" >
2 < p > 总数:{{total}}p >
3 < my-component v-model ="total" > my-component >
4 div >
5
6 < script >
7 Vue.component( " my-component " , {
8 template: " +1 " ,
9 data: function () {
10 return {
11 counter: 0
12 }
13 },
14 methods: {
15 handleClick: function () {
16 this .counter ++ ;
17 this .$emit( " input " , this .counter);
18 }
19 }
20 });
21 var app = new Vue({
22 el: " #app " ,
23 data: {
24 total: 0
25 }
26 });
27 script >
仍然是点击按钮加1的效果,不过这次组件$emit()
的事件名是特殊的input
,在使用组件的父级,并没有在
上使用@input='handler'
,而是直接用了v-model
绑定的一个数据total
。 这也可以称作是一个语法糖,因为上面的示例可以间接地用自定义事件来实现:
1 < div id ="app" >
2 < p > 总数:{{total}}p >
3 < my-component @input ="handleGetTotal" > my-component >
4 div >
5
6 < script >
7 Vue.component( " my-component " , {
8 template: " +1 " ,
9 data: function () {
10 return {
11 counter: 0
12 }
13 },
14 methods: {
15 handleClick: function () {
16 this .counter ++ ;
17 this .$emit( " input " , this .counter);
18 }
19 }
20 });
21
22 var app = new Vue({
23 el: " #app " ,
24 data: {
25 total: 0
26 },
27 methods: {
28 handleGetTotal: function (total) {
29 this .total = total;
30 }
31 }
32 });
33 script >
v-model
还可以用来创建自定义的表单输入组件,进行数据双向绑定,例如:
实现这样一个具有双向绑定的v-model
组件要满足下面两个要求:
接收一个value
属性。
在有新的value
时触发input
事件。
7.3.3 非父子组件通信
在实际业务中,除了父子组件通信外,还有很多非父子组件通信的场景,非父子组件一般有两种:兄弟组件和跨多级组件。 为了更加彻底地了解Vue.js 2.x中的通信方法,我们先来看一下在Vue.js 1.x中是如何实现的,这样便于我们了解Vue.js的设计思想。
在Vue.js 1.x中,除了$emit()
方法外,还提供了$dispatch()
和$broadcast()
这两个方法。$dispatch()
用于向上级派发事件,只要是它的父级(一般或多级以上),都可以在Vue实例的events
选项内接收,示例代码如下:
1
2 < div id ="app" >
3 {{message}}
4 < my-component > my-component >
5 div >
6
7 < script >
8 Vue.component( " my-component " , {
9 template: " 源发事件 " ,
10 methods: {
11 handleDispatch: function () {
12 this .$dispatch( " on-message " , " 来自内部组件的数据 " );
13 }
14 }
15 });
16
17 var app = new Vue({
18 el: " #app " ,
19 data: {
20 message: ""
21 },
22 methods: {
23 " on-message " : function (msg) {
24 this .message = msg;
25 }
26 }
27 });
28 script >
同理,$broadcast()
是由上级向下级广播事件的,用法完全一致,只是方向相反。
这两种方法一旦发出事件后,任何组件都是可以接收到的,就近原则,而且会在第一次接收到后停止冒泡,除非返回true
。
这两个方法虽然看起来很好用,但是在Vue.js 2.x中都废弃了,因为基于组件树结构的事件流方式让人难以理解,并且在组件结构扩展的过程中会变得越来越脆弱,并且不能解决兄弟组件通信的问题。
在Vue.js 2.x中,推荐使用一个空的Vue实例作为中央事件总线(bus),也就是一个中介。 为了更形象地了解它,我们举一个生活中的例子。
比如你需要租房子,你可能会找房产中介来等级你的需求,然后中介把你的信息发给满足要求的出租者,出租者再把报价和看房时间告诉中介,由中介再转达给你。 整个过程中,买家和卖家并没有任何交流,都是通过中间人来传话的。
或者你最近可能要换房了,你会找房产中介登记你的信息,订阅与你找房需求相关的资讯。 一旦有符合你的房子出现时,中介会通知你,并传达你房子的具体信息。
这两个例子中,你和出租者担任的就是两个跨级的组件,而房产中介就是这个中央事件总线(bus)。 比如下面的示例代码:
1 < div id ="app" >
2 {{message}}
3 < my-component-a > my-component-a >
4 div >
5
6 < script >
7 var bus = new Vue();
8
9 Vue.component( " my-component-a " , {
10 template: " 传递事件 " ,
11 methods: {
12 handleEvent: function () {
13 bus.$emit( " on-message " , " 来自组件 component-a 的内容 " );
14 }
15 }
16 });
17
18 var app = new Vue({
19 el: " #app " ,
20 data: {
21 message: ""
22 },
23 mounted: function () {
24 var _this = this ;
25 // 在实例初始化时,监听来自bus示例的事件
26 bus.$on( " on-message " , function (msg) {
27 _this.message = msg;
28 });
29 }
30 });
31 script >
首先创建了一个名为bus
的空Vue实例,里面没有任何内容;然后全局定义了组件component-a
;最后创建Vue实例app
。 在app
初始化时,也就是在生命周期mounted
钩子函数里监听了来自bus
的事件on-message
, 而在组件component-a
中,点击按钮会通过bus
把事件on-message
发出去, 此时app
就会接受到来自bus
的事件,进而在回调里完成自己的业务逻辑。
这种方法巧妙而清凉地实现了任何组件间的通信,包括父子、兄弟、跨级,而且Vue 1.x和Vue 2.x都适用。 如果深入使用,可以扩展bus
实例,给它添加data
、methods
、computed
等选项,这些都是可以公用的。 在业务中,尤其是协同开发时非常有用,因为经常需要共享一些通用的信息。 比如用户登录的昵称、性别、邮箱等,还有用户的授权token等,只需在初始化时让bus
获取一次,任何时间、任何组件就可以从中直接使用了,在单页面富应用(SPA)中会很实用,我们会在进阶篇中逐步介绍这些内容。
当你的项目比较大,有更多的小伙伴参与开发时,也可以你选择更好的状态管理解决方案vuex,在进阶篇里会详细介绍关于它的用法。
除了中央事件总线bus
外,还有两种方法可以实现组件间通信:父链和子组件索引。
父链
在子组件中,使用this.$parent
可以直接访问该组件的父实例或组件,父组件也可以通过this.$children
访问它所有的子组件,而且可以递归向上或向下无限访问,直到根实例或最内层的组件。 示例代码如下:
1 < div id ="app" >
2 {{message}}
3 < my-component-a > my-component-a >
4 div >
5
6 < script >
7 Vue.component( " my-component-a " , {
8 template: " 通过父链直接修改数据 " ,
9 methods: {
10 handleEvent: function () {
11 // 访问到父链后,可以做任何操作,比如直接修改数据
12 this .$parent.message = " 来自组件 component-a 的内容 " ;
13 }
14 }
15 });
16
17 var app = new Vue({
18 el: " #app " ,
19 data: {
20 message: ""
21 }
22 });
23 script >
尽管Vue允许这样操作,但在业务中,子组件应该尽可能地避免依赖父组件的数据,更不应该去主动修改它的数据,因为这样使得父子组件紧耦合,只看父组件,很难理解父组件的状态,因为它可能被任意组件修改,理想情况下,只有组件自己能修改它的状态。 父子组件最好还是通过props
和$emit()
来通信。
子组件索引
当子组件较多时,通过this.$children
来一一遍历我们需要的一个组件实例是比较困难的,尤其是组件动态渲染时,它们的序列是不固定的。Vue提供了子组件索引的方法,用特殊的属性ref
来为子组件指定一个索引名称。 示例代码如下:
1 < div id ="app" >
2 < button @click ="handleRef" > 通过ref获取子组件实例button >
3 < component-a ref ="comA" > component-a >
4 div >
5
6 < script >
7 Vue.component( " component-a " , {
8 template: " 子组件
" ,
9 data: function () {
10 return {
11 message: " 子组件内容 "
12 };
13 }
14 });
15
16 var app = new Vue({
17 el: " #app " ,
18 methods: {
19 handleRef: function () {
20 // 通过$refs来访问指定的实例
21 var msg = this .$refs.comA.message;
22 console.log(msg);
23 }
24 }
25 });
26 script >
在父组件模板中,子组件标签上使用ref
指定一个名称,并在父组件内通过this.$refs
来访问指定名称的子组件。
提示: \(refs只在组件渲染完成后才填充,并且它是非响应式的。 它仅仅作为一个直接访问子组件的应急方案,应当避免在模板或计算属性中使用\)refs。
与Vue 1.x不同的是,Vue 2.x将v-el
和v-ref
合并为了ref
,Vue会自动去判断是普通标签还是组件。 可以尝试补全下面的代码,分别打印出两个ref
看看都是什么:
1 < div id ="app" >
2 < p ref ="p" > 内容p >
3 < child-component ref ="child" > child-component >
4 div >
7.4 使用slot分发内容
7.4.1 什么是slot
我们先看一个比较常规的网站布局,如图7-3所示。 
这个网站由一级导航、二级导航、左侧列表、正文以及底部版权信息5个模块组成。 如果要将它们都组件化,这个结构可能会是:
1 < app >
2 < menu-main > menu-main >
3 < menu-sub > menu-sub >
4 < div class ="container" >
5 < menu-left > menu-left >
6 < container > container >
7 div >
8 < app-footer > app-footer >
9 app >
当需要让组件组合使用,混合父组件的内容与子组件的模板时,就会用到slot
,这个过程叫做内容分发(transclusion)。 以
为例,它有两个特点:
组件不知道它的挂载点会有什么内容。挂载点的内容是由
的父组件决定的。
组件很可能有它自己的模板。
props
传递数据、events
触发事件和slot
内容分发就构成了Vue组件的3个API来源,再复杂的组件也是由这3部分构成的。
7.4.2 作用域
正式介绍slot
前,需要先知道一个概念:编译的作用域。 比如父组件有如下模板:
1 < child-component >
2 {{message}}
3 child-component >
这里的message
就是一个slot
。 但是它绑定的是父组件的数据,而不是组件
的数据。
父组件模板的内容是在父组件作用域内编译,子组件模板的内容是在子组件作用域内编译。 例如下面的代码示例:
1 < div id ="app" >
2 < child-component v-show ="showChild" > child-component >
3 div >
4
5 < script >
6 Vue.component( " child-component " , {
7 template: " 子组件
"
8 });
9 var app = new Vue({
10 el: " #app " ,
11 data: {
12 showChild: true
13 }
14 })
15 script >
这里的状态showChild
绑定的是父组件的数据,如果想在子组件上绑定,那应该是:
1 < div id ="app" >
2 < child-component > child-component >
3 div >
4
5 < script >
6 Vue.component( " child-component " , {
7 template: " 子组件
" ,
8 data: function () {
9 return {
10 showChild: true
11 };
12 }
13 });
14
15 var app = new Vue({
16 el: " #app "
17 })
18 script >
因此,slot
分发的内容,作用域是在父组件上的。
7.4.3 solt用法
单个Slot
在子组件内使用特殊的
元素就可以为这个子组件开启一个slot(插槽),在父组件模板里,插入子组件标签内的所有内容将替代子组件的
标签及它的内容。 示例代码如下:
1 < div id ="app" >
2 < child-component >
3 < p > 分发的内容p >
4 < p > 更多分发的内容p >
5 child-component >
6 div >
7
8 < template id ="template" >
9 < div >
10 < slot >
11 < p > 如果父组件没有插入内容,我将作为默认出现p >
12 slot >
13 div >
14 template >
15
16 < script >
17 Vue.component( " child-component " , {
18 template: " #template "
19 });
20 var app = new Vue({
21 el: " #app "
22 })
23 script >
子组件child-component
的模板内定义了一个
元素,并且用一个
作为默认的内容,在父组件没有使用slot
时,会渲染这段默认的文本;如果写入了slot
,那就回替换整个
。 所以上例渲染后的结果为:
1 < div id ="app" >
2 < div >
3 < p > 分发的内容p >
4 < p > 更多分发的内容p >
5 div >
6 div >
提示: 注意,子组件
内的备用内容,它的作用域是子组件本身。
具名Slot
给
元素指定一个name
后可以分发多个内容,具名Slot可以与单个Slot共存。 例如下面的示例:
1 < div id ="app" >
2 < child-component >
3 < h2 slot ="header" > 标题h2 >
4 < p > 正文内容p >
5 < p > 更多的正文内容p >
6 < div slot ="footer" > 底部信息div >
7 child-component >
8 div >
9
10 < template id ="template" >
11 < div class ="container" >
12 < div class ="header" >
13 < slot name ="header" > slot >
14 div >
15 < div class ="main" >
16 < slot > slot >
17 div >
18 < div class ="footer" >
19 < slot name ="footer" > slot >
20 div >
21 div >
22 template >
23
24 < script >
25 Vue.component( " child-component " , {
26 template: " #template "
27 });
28 var app = new Vue({
29 el: " #app "
30 })
31 script >
子组件内声明了3个
元素,其中在内的
没有使用
name
特性,它将作为默认slot出现,父组件没有使用slot特性的元素与内容都将会出现在这里。
如果没有指定默认的匿名slot,父组件内多余的内容片断都将被抛弃。
上例最终渲染后的结果为:
1 < div id ="app" >
2 < div class ="container" >
3 < div class ="header" >
4 < h2 > 标题h2 >
5 div >
6 < div class ="main" >
7 < p > 正文内容p >
8 < p > 更多的正文内容p >
9 div >
10 < div class ="footer" >
11 < div > 底部信息div >
12 div >
13 div >
14 div >
在组合使用组件时,内容分发API至关重要。
7.4.4 作用域插槽
作用域插槽是一种特殊的slot,使用一个可以复用的模板替换已渲染元素。 概念比较难理解,我们先看一个简单的示例来了解它的基本用法。 示例代码如下:
1 < div id ="app" >
2 < child-component >
3 < template scope ="props" >
4 < p > 来自父组件的内容p >
5 < p > {{props.msg}}p >
6 template >
7 child-component >
8 div >
9
10 < template id ="template" >
11 < div class ="container" >
12 < slot msg ="来自子组件的内容" > slot >
13 div >
14 template >
15
16 < script >
17 Vue.component( " child-component " , {
18 template: " #template "
19 });
20 var app = new Vue({
21 el: " #app "
22 })
23 script >
观察子组件的模板,在
元素上有一个类似props
传递数据给组件的写法msg="xxx"
,数据传到了插槽。 父组件中使用了
元素,而且拥有一个scope="props"
的特性,这里的props
只是一个临时变量,就像v-for="item in items"
里面的item
一样。template
内可以通过临时变量props
访问来自子组件插槽的数据msg
。
将上面的示例渲染后的最终结果为:
1 < div id ="app" >
2 < div class ="container" >
3 < p > 来自父组件的内容p >
4 < p > 来自子组件的内容p >
5 div >
6 div >
作用域插槽根据代表性的用力是列表组件,允许组件自定义应该如何渲染列表每一项。 示例代码如下:
1 < div id ="app" >
2 < my-list v-bind:books ="books" >
3 < template slot ="book" scope ="props" >
4 < li > {{props.bookName}}li >
5 template >
6 my-list >
7 div >
8
9 < template id ="template" >
10 < ul >
11 < slot name ="book" v-for ="book in books" v-bind:book-name ="book.name" >
12
13 slot >
14 ul >
15 template >
16
17 < script >
18 Vue.component( " my-list " , {
19 props: {
20 books: {
21 type: Array,
22 default : function () {
23 return {};
24 }
25 }
26 },
27 template: " #template "
28 });
29
30 var app = new Vue({
31 el: " #app " ,
32 data: {
33 books: [
34 {name: " 《Vue.js实战》 " },
35 {name: " 《JavaScript语言精粹》 " },
36 {name: " 《JavaScript高级程序设计》 " }
37 ]
38 }
39 });
40 script >
子组件my-list
接收一个来自父级的prop
数组books
,并且将它在name
为book
的slot
上使用v-for
指令循环,同时暴露一个变量bookName
。
如果你仔细揣摩上面的用法,你可能会产生这样的疑问:我直接在父组件用v-for
不就好了吗?为什么还要绕一步,在子组件里面循环呢? 的确,如果只是针对上面的示例,这样写是多此一举的。此例的用意主要是介绍作用域插槽的用法,并没有加入使用场景,而作用域插槽的适用场景就是既可以复用子组件的slot,又可以使slot内容不一致。 如果上例还在其他组件内使用,
的内容渲染权是由使用者掌握的,而数据却可以通过临时变量(比如props
)子组件内获取。
7.4.5 访问slot
在Vue.js 1.x中,想要获取某个slot是比较麻烦的,需要用v-el
间接获取。 而Vue.js 2.x提供了用来访问被slot分发的内容的方法$slots
,请看下面的示例:
1 < div id ="app" >
2 < child-component >
3 < h2 slot ="header" > 标题h2 >
4 < p > 正文内容p >
5 < p > 更多的正文内容p >
6 < div slot ="footer" > 底部信息div >
7 child-component >
8 div >
9
10 < template id ="template" >
11 < div class ="container" >
12 < div class ="header" >
13 < slot name ="header" > slot >
14 div >
15 < div class ="main" >
16 < slot > slot >
17 div >
18 < div class ="footer" >
19 < slot name ="footer" > slot >
20 div >
21 div >
22 template >
23
24 < script >
25 Vue.component( " child-component " , {
26 template: " #template " ,
27 mounted: function () {
28 var header = this .$slots.header;
29 var main = this .$slots.main;
30 var footer = this .$slots.footer;
31 console.log(footer);
32 console.log(footer[ 0 ].elm.innerHTML);
33 }
34 });
35
36 var app = new Vue({
37 el: " #app "
38 });
39 script >
通过$slots
可以访问某个具名slot,this.$slots.default
包括了所有没有被包含在具名slot中的节点。 尝试编写代码,查看两个console打印的内容。
$slots
在业务中几乎用不到,在用render
函数(进阶篇中将介绍)创建组件时会比较有用,但主要还是用于独立组件开发中。
7.5 组件高级用法
本节会介绍组件的一些高级用法,这些用法在实际业务中不是很常用,但会独立组件开发时可能会用到。 如果你感觉以上内容已经足骨完成你的业务开发了,可以跳过本节;如果你想继续探索Vue组件的奥秘,读完本节会对你有很大的启发。
7.5.1 递归组件
组件在它的模板内可以递归地调用自己,只要给组件设置name
的选项就可以了。 示例代码如下:
1 < div id ="app" >
2 < child-component v-bind:count ="1" > child-component >
3 div >
4
5 < template id ="template" >
6 < div class ="child" >
7 < child-component v-bind:count ="count+1" v-if ="count<3" > child-component >
8 div >
9 template >
10
11 < script >
12 Vue.component( " child-component " , {
13 name: " child-component " ,
14 props: {
15 count: {
16 type: Number,
17 default : 1
18 }
19 },
20 template: " #template "
21 });
22
23 var app = new Vue({
24 el: " #app "
25 });
26 script >
设置name
后,在组件模板内就可以递归使用了。 不过需要注意的是,必须给一个条件来限制递归数量,否则会抛出错误:max stack size exceeded
。
组件递归使用可以用来开发一些具有未知层级关系的阻力组件,比如级联选择器和树形控件等。 如图7-4和图7-5所示: 

在实战篇里,我们会详细介绍级联选择器的实现。
7.5.2 内联模板
组件的模板一般都是在template
选项内自定义的,Vue提供了一个内联模板的功能,在使用组件时,给组件标签使用inline-template
特性,组件就会把它的内容当做模板,而不是把它当内容分发,这让模板更灵活。 示例代码如下:
1 < div id ="app" >
2 < child-component inline-template :data ="message" >
3 < div >
4 < h2 > 在父组件中定义了组件的模板h2 >
5 < p > {{data}}p >
6 < p > {{msg}}p >
7 div >
8 child-component >
9 div >
10
11 < script >
12 Vue.component( " child-component " , {
13 props: [ " data " ],
14 data: function () {
15 return {
16 msg: " 在子组件声明的数据 "
17 };
18 }
19 });
20
21 var app = new Vue({
22 el: " #app " ,
23 data: {
24 message: " 在父组件声明的数据 "
25 }
26 });
27 script >
渲染后的结果为:
1 < div id ="app" >
2 < div >
3 < h2 > 在父组件中定义了组件的模板h2 >
4 < p > 在父组件声明的数据p >
5 < p > 在子组件声明的数据p >
6 div >
7 div >
7.5.3 动态组件
Vue.js提供了一个特殊的元素
用来动态地挂载不同的组件,使用is
特性来选择要挂载的组件。 示例代码如下:
1 < div id ="app" >
2 < input type ="text" placeholder ="请输入组件名称: A/B/C" >
3 < button @click ="handleChangeView" > 确定切换button >
4 < component v-bind:is ="currentView" > component >
5 div >
6
7 < script >
8 var app = new Vue({
9 el: " #app " ,
10 components: {
11 comA: {
12 template: " 组件A
"
13 },
14 comB: {
15 template: " 组件B
"
16 },
17 comC: {
18 template: " 组件C
"
19 }
20 },
21 data: {
22 currentView: " comA "
23 },
24 methods: {
25 handleChangeView: function () {
26 var value = document.querySelector( " input " ).value;
27 this .currentView = " com " + value;
28 }
29 }
30 });
31 script >
动态地改变currentView
的值就可以动态挂载组件了。 也可以直接绑定在组件对象上:
1 < div id ="app" >
2 < component v-bind:is ="currentView" > component >
3 div >
4
5 < script >
6 var Home = {
7 template: " Welcome home!
"
8 };
9 var app = new Vue({
10 el: " #app " ,
11 data: {
12 currentView: Home
13 }
14 });
15 script >
7.5.4 异步组件
当你的工程足够大,使用的组件足够多时,是时候考虑下性能的问题了,因为一开始把所有的组件都加载是没必要的一笔开销。 好在Vue.js允许将组件定义为一个工厂函数,动态地解析组件。 Vue.js值在组件需要渲染时触发工厂函数,并且把结果缓存起来,用于后面的再次渲染。 例如下面的示例:
1 < div id ="app" >
2 < child-component > child-component >
3 div >
4
5 < script >
6 Vue.component( " child-component " , function (resolve, reject) {
7 window.setTimeout( function () {
8 resolve({
9 template: " 我是异步渲染的
"
10 });
11 }, 2000 );
12 });
13 var app = new Vue({
14 el: " #app "
15 });
16 script >
工厂函数接收一个resolve
回调,在收到从服务器下载的组件定义时调用。 也可以调用reject(reason)
指示加载失败。 这里setTimeout
只是为了演示异步,具体的下载逻辑可以自己决定,比如把组件配置写成一个对象配置,通过Ajax来请求,然后调用resolve
传入配置选项。
在进阶篇里,我们还会介绍主流的打包编译工具webpack和.vue单文件的用法,更优雅地实现异步组件(路由)。
7.6 其他
7.6.1 $nextTick
我们先来看这样一个场景: 有一个div,默认用v-if
将它隐藏,点击一个按钮后,改变v-if
的值,让它显示出来,同时拿到这个div的文本内容。 如果v-if
的值是false
,直接去获取div的内容是获取不到的,因为此时div还没有被创建出来。 那么应该在点击按钮后,改变v-if
的值为true
, div才会被创建,此时再去获取。 示例代码如下:
1 < div id ="app" >
2 < div id ="div" v-if ="showDiv" > 这是一段文本div >
3 < button v-on:click ="getText" > 获取div内容button >
4 div >
5
6 < script >
7 var app = new Vue({
8 el: " #app " ,
9 data: {
10 showDiv: false
11 },
12 methods: {
13 getText: function () {
14 this .showDiv = true ;
15 var text = document.getElementById( " div " ).innerHTML;
16 console.log(text);
17 }
18 }
19 });
20 script >
这段代码并不难理解,但是运行后在控制台会抛出一个错误:Cannot read property 'innerHTML' of null
。意思就是获取不到div元素。这里就涉及Vue一个重要的概念:异步更新队列。
Vue在观察到数据变化时并不是直接更新DOM,从而避免不必要的计算和DOM操作。 然后,在下一个事件循环tick中,Vue刷新队列并执行实际(已去重的)工作。 所以如果你用一个for循环来动态改变数据100次,其实它只会应用最后一次改变,如果没有这种机制,DOM就要重绘100次,这固然是一个很大的开销。
Vue会根据当前浏览器环境优先使用原生的Promise.then
和MutationObserver
,如果都不支持,就会采用setTimeout
代替。
知道了Vue异步更新DOM的原理,上面示例的报错也就不难理解了。 事实上,在执行this.showDiv=true
时,div仍然还是没有被创建出来,直到下一个Vue事件循环时,才开始创建。$nextTick
就是用来知道什么时候DOM更新完成的,所以上面的示例代码需要修改为:
1 < div id ="app" >
2 < div id ="div" v-if ="showDiv" > 这是一段文本div >
3 < button v-on:click ="getText" > 获取div内容button >
4 div >
5
6 < script >
7 var app = new Vue({
8 el: " #app " ,
9 data: {
10 showDiv: false
11 },
12 methods: {
13 getText: function () {
14 this .showDiv = true ;
15 this .$nextTick( function () {
16 var text = document.getElementById( " div " ).innerHTML;
17 console.log(text);
18 });
19 }
20 }
21 });
22 script >
这时再点击按钮,控制台就打印出div的内容“这时一段文本了”。
理论上,我们应该不用去主动操作DOM,因为Vue的核心思想就是数据驱动DOM,但在很多业务里,我们避免不了会使用一些第三方库,比如popper.js、swiper等,这些基于原生JavaScript的库都有创建和更新及销毁的完整生命周期,与Vue配合使用时,就要利用好$nextTick
。
7.6.2 X-Templates
如果你没有使用webpack、gulp等工具,试想一下你的组件template
的内容很冗长、复杂,如果都在JavaScript里拼接字符串,效率是很低的,因为不能像写HTML那样舒服。 Vue提供了另一种定义模板的方式,在
标签里使用text/x-template
类型,并且指定一个id
,将这个id
赋给template
。示例代码如下:
1 < div id ="app" >
2 < my-component > my-component >
3 < script type ="text/x-template" id ="my-component" >
4 < div > 这是组件的内容 < / div>
5 script >
6 div >
7
8 < script >
9 Vue.component( " my-component " , {
10 template: " #my-component "
11 });
12 var app = new Vue({
13 el: " #app "
14 });
15 script >
在
标签里,你可以愉快地编写HTML代码,不用考虑执行等问题。
很多刚接触Vue开发的新手会非常喜欢这个功能,因为用它,再加上组件知识,就可以很轻松地完成交互相对复杂的页面和应用了。如果再配合一些构建工具(gulp)组织好代码结构,开发一些中小型产品是没有问题的。 不过,Vue的初衷并不是滥用它,因为它将模板和组件的其他定义隔离了。 在进阶篇里,我们会介绍如何使用webpack来编译.vue的单文件,从而优雅地解决HTML书写的问题。
7.6.3 手动挂载实例
我们现在所创建的实例都是通过new Vue()
的形式创建出来的。 在一些非常特殊的情况下,我们需要动态地去创建Vue实例,Vue提供了Vue.extend
和$mount
两个方法来手动挂载一个实例。
Vue.extend
是基础Vue构造器,创建一个“子类”,参数是一个包含组件选项的对象。
如果Vue实例在实例化时没有收到el
选项,它就处于“未挂载”状态,没有关联的DOM元素。 可以使用$mount()
手动地挂载一个未挂载的实例。 这个方法返回实例自身,因而可以链式调用其他实例方法。 示例代码如下:
1 < div id ="mount-div" > div >
2
3 < script >
4 var MyComponent = Vue.extend({
5 template: " Hello: {{name}}
" ,
6 data: function () {
7 return {
8 name: " Jack "
9 };
10 }
11 });
12
13 new MyComponent().$mount( " #mount-div " );
14 script >
运行后,id
为mount-div
的div元素会被替换为组件MyComponent
的template
的内容:
除了这种写法外,以下两种写法也是可以的:
1 new MyComponent().$mount("#mount-div");
2 // 同上
3 new MyComponent({
4 el: "#mount-div"
5 });
6 // 或者,在文档之外渲染并且随后挂载
7 var component = new MyComponent().$mount();
8 document.getElementById("mount-div").appendChild(component.$el);
手动挂载实例(组件)是一种比较极端的高级用法,在业务中几乎用不到,只是开发一些复杂的独立组件时可能会使用,所以只做了解就好。
7.7 实战:两个常用组件的开发
本节以组件知识为基础,整合指令、事件等前面两章的内容,开发两个业务中常用的组件,即数字输入框和标签页。
7.7.1 开发一个数字输入框组件
数字输入框时对普通输入框的扩展,用来快捷输入一个标准的数字。 如图7-6所示: 
数字输入框只能输入数字,而且有两个快捷按钮,可以直接减1或加1. 除此之外,还可以设置初始值、最大值、最小值,在数值改变时,触发一个自定义事件来通知父组件。
了解了基本需求后,我们先定义目录文件:
index.html 入口页
input-number.js 数字输入框组件
index.js 根实例
因为该示例是以交互功能为主,所以就不写CSS美化样式了。
首先写入基本的结构代码,初始化项目。
index.html:
1 DOCTYPE html >
2 < html lang ="zh" >
3 < head >
4 < meta charset ="UTF-8" >
5 < meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
6 < meta http-equiv ="X-UA-Compatible" content ="ie=edge" >
7 < title > 数字输入框组件title >
8 head >
9 < body >
10 < div id ="app" > div >
11
12 < script src ="vue.js" > script >
13 < script src ="input-number.js" > script >
14 < script src ="index.js" > script >
15 body >
16 html >
index.js:
1 var app = new Vue({
2 el: "#app"
3 });
input-number.js:
1 Vue.component("input-number", {
2 template: "\
3 \
4 \
5
",
6 props: {
7 max: {
8 type: Number,
9 default : Infinity
10 },
11 min: {
12 type: Number,
13 default : -Infinity
14 },
15 value: {
16 type: Number,
17 default : 0
18 }
19 }
20 });
该示例的主角是input-number.js
,所有的组件配置都在这里面定义。 现在template
里面定义了组件的根节点,因为是独立组件,所以应该对每个prop进行校验。 这里面根据需求有最大值、最小值、默认值(也就是绑定值)3个prop,max
和min
都是数字类型,默认值是正无限大和负无限大;value
也是数字类型,默认值是0。
接下来,我们先在父组件引入input-number
组件,并给它一个默认值5,最大值10,最小值0。
index.js:
1 var app = new Vue({
2 el: "#app",
3 data: {
4 value: 5
5 }
6 });
index.html:
1 < div id ="app" >
2 < input-number v-model ="value" :max ="10" :min ="0" > input-number >
3 div >
value
是一个关键的绑定至,所以用了v-model
,这样既优雅地实现了双向绑定,也让API看起来很合理。 大多数的表单类组件都应该有一个v-model
,比如输入框、单选框、多选框、下拉选择器等。
剩余的代码量就都聚焦到了input-number.js
上。
我们之前介绍过,Vue组件是单向数据流,所以无法从组件内部直接修改prop:value
的值。 解决办法也介绍过,就是给组件声明一个data
,默认引用value
的值,然后在组件内部维护这个data
:
1 Vue.component("input-number", {
2 // ...
3 data: function () {
4 return {
5 currentValue: this .value
6 };
7 }
8 });
这样只解决了初始化时引用父组件value
的问题。 但是如果从父组件修改了value
,input-number组件的currentValue
也要一起更新。 为了实现这个功能,我们需要用到一个新的概念,监听(watch)。
watch
选项用来监听某个prop或data的改变,当他们发生变化时,就会触发watch
配置的函数,从而完成我们的业务逻辑。 在本例中,我们要监听两个量:value
和currentValue
。 监听value
是要知晓从父组件修改了value
,监听currentValue
是为了当currentValue
改变时,更新value
。 相关代码如下:
1 Vue.component("input-number", {
2 // ...
3 data: function () {
4 return {
5 currentValue: this .value
6 };
7 },
8 watch: {
9 currentValue: function (val) {
10 this .$emit("input", val);
11 this .$emit("on-change", val);
12 },
13 value: function (val) {
14 this .updateValue(val);
15 }
16 },
17 methods: {
18 updateValue: function (val) {
19 if (val > this .max) {
20 val = this .max;
21 }
22
23 if (val < this .min) {
24 val = this .min;
25 }
26
27 this .currentValue = val;
28 }
29 },
30 mounted: function () {
31 this .updateValue(this .value);
32 }
33 });
从父组件传递过来的value
有可能是不符合当前条件的(大于max或小于min),所以在选项methods
里写了一个方法updateValue
,用来过滤出一个正确的currentValue
。
watch
监听的数据的回调函数有2个参数可用,第一个是新的智,第二个是旧的值,这里没有太复杂的逻辑,就只用了第一个参数。 再回调函数里,this
是指向当前组件实例的,所以可以直接调用this.updateValue()
,因为Vue代理了props
、data
、computed
及methods
。
监听currentValue
的回调里:
this.$emit("input", val)
是在使用v-model
时改变value
的;
this.$emit("on-change", val)
是触发自定义事件on-change
,用于告知父组件数字输入框的值有所改变(示例中没有使用该事件〉。
在生命周期mounted
钩子里也调用了updateValue()
方法,是因为第一次初始化时,也对value
进行了过滤。 这里也有另一种写法,在data
选项返回对象前进行过滤:
1 Vue.component("input-number", {
2 // ...
3 data: function () {
4 var val = this .value;
5
6 if (val > this .max) {
7 val = this .max;
8 }
9
10 if (val < this .min) {
11 val = this .min;
12 }
13
14 return {
15 currentValue: val
16 };
17 }
18 });
实现的效果是一样的。
最后剩余的就是补全模板template
,内容是一个输入框和两个按钮。 相关代码如下:
input绑定了数据currentValue
和原生的change事件,在句柄handleChange
函数中,判断了当前输入的是否是数字。 注意,这里绑定的currentValue
也是单向数据流,并没有用v-model
,所以在输入时,currentValue
的值并没有实时改变。 如果输入的不是数字(比如英文和汉字等〉,就将输入的内容重置为之前的currentValue。 如果输入的是符合要求的数字,就把输入的值赋给
currentValue`。
数字输入框组件的核心逻辑就是这些。 回顾一下我们设计一个通用组件的思路,首先,在写代码前一定要明确需求,然后规划好API。 一个Vue组件的API只来自props
、events
和slots
,确定好这3部分的命名、规则,剩下的逻辑即使第一版没有做好,后续也可以迭代完善。但是API如果没有设计好,后续再改对使用者成本就很大了。
完整的示例代码如下:
index.html:
1 DOCTYPE html >
2 < html lang ="zh" >
3 < head >
4 < meta charset ="UTF-8" >
5 < meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
6 < meta http-equiv ="X-UA-Compatible" content ="ie=edge" >
7 < title > 数字输入框组件title >
8 head >
9 < body >
10 < div id ="app" >
11 < input-number v-model ="value" :max ="10" :min ="0" > input-number >
12 div >
13
14 < script src ="vue.js" > script >
15 < script src ="input-number.js" > script >
16 < script src ="index.js" > script >
17 body >
18 html >
index.js:
1 var app = new Vue({
2 el: "#app",
3 data: {
4 value: 5
5 }
6 });
input-number.js:
1 function isValueNumber(value) {
2 return (/(^-?[0-9]+\.(1)\d+$)|(^-?[1-9][0-9]*$)|(^-?0{1})/.test(value + ""));
3 }
4
5 Vue.component("input-number", {
6 template: "\
7 \
8 \
9 - \
10 + \
11
",
12 props: {
13 max: {
14 type: Number,
15 default : Infinity
16 },
17 min: {
18 type: Number,
19 default : -Infinity
20 },
21 value: {
22 type: Number,
23 default : 0
24 }
25 },
26 data: function () {
27 return {
28 currentValue: this .value
29 };
30 },
31 watch: {
32 currentValue: function (val) {
33 this .$emit("input", val);
34 this .$emit("on-change", val);
35 },
36 value: function (val) {
37 this .updateValue(val);
38 }
39 },
40 methods: {
41 handleDown: function () {
42 if (this .currentValue <= this .min) {
43 return ;
44 }
45 this .currentValue -= 1;
46 },
47 handleUp: function () {
48 if (this .currentValue >= this .max) {
49 return ;
50 }
51 this .currentValue += 1;
52 },
53 handleChange: function (event) {
54 var val = event.target.value.trim();
55 var max = this .max;
56 var min = this .min;
57
58 if (isValueNumber(val)) {
59 val = Number(val);
60 this .currentValue = val;
61 if (val > max) {
62 this .currentValue = max;
63 } else if (val < min) {
64 this .currentValue = min;
65 }
66 } else {
67 event.target.value = this .currentValue;
68 }
69 },
70 updateValue: function (val) {
71 if (val > this .max) {
72 val = this .max;
73 }
74
75 if (val < this .min) {
76 val = this .min;
77 }
78
79 this .currentValue = val;
80 }
81 },
82 mounted: function () {
83 this .updateValue(this .value);
84 }
85 });
练习1: 在输入框聚焦时,增加对键盘上下键的支持,相当于加1和减1;
练习2: 增加一个控制步伐的prop:step,比如设置为10,点击加号按钮,一次增加10。
7.7.2 开发一个标签组件
本小节将开发一个比较有挑战的组件:标签页组件。 标签页(即选项卡切换组件)是网页和布局中经常用到的元素,常用于平级区域大块内容的收纳和展现。 如图7-7所示: 
根据上个示例的经验,我们先分析业务需求,制定出API,这样不至于一上来就无从下手。
每个标签页的主体内容肯定是由使用组件的父级控制的,所以这部分是一个slot,而且slot的数量决定了标签切换按钮的数量。 假设我们有3个标签页,点击每个标签按钮时,另外两个标签对应的slot应该被隐藏。 一般这个时候,比较容易想到的解决办法是,在slot里写3个div,在接收到切换通知时,显示和隐藏相关div。 这样设计没有问题,只不过提现不出组件的价值来,因为我们还是一些了一些与业务无关的业务逻辑,而这部分逻辑最好组件本身帮忙处理了,我们只用聚焦在slot内容本身,这才是我们业务最相关的。 这种情况下,我们在定义一个子组件panel,嵌套在标签页组件tabs里,我们的业务代码都放在panel的slot内,而3个panel组件作为整体成为tabs的slot。
由于tabs和panel两个组件是分离的,但是tabs组件上的标题应该由panel组件来定义,因为slot是卸载panel里,因此在组件初始化(及标签标题动态改变)时,tabs要从panel里获取标题,并保存起来,自己使用。
确定好了结构,我们先创建所需的文件:
index.html 入口页
style.css 样式表
tabs.js 标签页外层的组件 tabs
panel.js 标签页嵌套的组件 panel
先初始化各个文件:
index.html:
1 DOCTYPE html >
2 < html lang ="zh" >
3 < head >
4 < meta charset ="UTF-8" >
5 < meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
6 < meta http-equiv ="X-UA-Compatible" content ="ie=edge" >
7 < title > 标签页组件title >
8 < link rel ="stylesheet" type ="text/css" href ="style.css" >
9 head >
10 < body >
11 < div id ="app" > div >
12
13 < script src ="vue.js" > script >
14 < script src ="panel.js" > script >
15 < script src ="tabs.js" > script >
16 < script >
17 var app = new Vue({
18 el: " #app "
19 });
20 script >
21 body >
22 html >
tabs.js:
1 Vue.component("tabs", {
2 template: "\
3 \
4 \
5 \
6
\
7 \
8 \
9 \
10
\
11 "
12 });
panel.js
1 Vue.component("panel", {
2 name: "panel",
3 template: "\
4 \
5 \
6
"
7 });
panel需要控制标签页内容的显示与隐藏。 设置一个data:show
,并且用v-show
指令来控制元素:
1 Vue.component("panel", {
2 name: "panel",
3 template: "\
4 \
5 \
6
",
7 data: function () {
8 return {
9 show: true
10 };
11 }
12 });
当点击到这个panel对应的标签页标题按钮时,此panel的show
值设置为true
,否则应该是false
。 这步操作是在tabs组件完成的,我们稍后再介绍。
既然要单击对应的标签页标题按钮,那应该有一个唯一的值来标识这个panel,我们可以设置一个prop:name
让用户来设置,但它不是必须的,如果使用者不设置,可以默认从0开始自动设置,这不操作仍然是tabs执行的,因为panel本身并不知道自己是第几个。 除了name
,还需要标签页标题的prop:label
,tabs组件需要将它显示在标签页标题里。 这部分代码如下:
1 props: {
2 name: {
3 type: String
4 },
5 label: {
6 type: String,
7 default : ""
8 }
9 }
上面的prop:label
用户是可以动态调整的,所以在panel初始化以及label
更新时,都要通知父组件也更新,因为是独立组件,所以不能依赖像bus.js或vuex这样的状态管理办法,我们可以直接通过this.$parent
访问tabs组件的实例来调用它的方法更新标题,该方法暂定为updateNav
。 注意,在业务中尽可能不要使用$parent
来操作父链,这种方法适合于标签页这样的独立组件。 这部分代码如下:
1 methods: {
2 updateNav() {
3 this .$parent.updateNav();
4 }
5 },
6 watch: {
7 label() {
8 this .updateNav();
9 }
10 },
11 mounted() {
12 this .updateNav();
13 }
在生命周期mounted
,也就是panel初始化时,调用一遍tabs的updateNav
方法。 同时监听了prop:label
,在label
更新时,同样调用。
剩余任务就是完成tabs.js组件。
首先需要把panel组件设置的标题动态渲染出来,也就是当panel触发tabs的updateNav
方法时,更新标题内容。 我们先看一下这部分的代码:
1 Vue.component("tabs", {
2 // ...
3 data: function () {
4 return {
5 // 用于渲染tabs的标题
6 navList: []
7 };
8 },
9 methods: {
10 getTabs() {
11 // 通过遍历子组件,得到所有的panel组件
12 return this .$children.filter(function (item) {
13 return item.$options.name === "panel";
14 });
15 },
16 updateNav() {
17 this .navList = [];
18 // 设置对this的引用,在function回调里,this指向的并不是Vue实例
19 var _this = this ;
20 this .getTabs().forEach(function (panel, index) {
21 _this.navList.push({
22 label: panel.label,
23 name: panel.name || index
24 });
25 // 如果没有给panel设置name,默认设置它的索引
26 if (!panel.name) {
27 panel.name = index;
28 }
29 // 设置当前选中的tab的索引,在后面介绍
30 if (index === 0) {
31 if (!_this.currentValue) {
32 _this.currentValue = panel.name || index;
33 }
34 }
35 });
36 },
37 updateStatus() {
38 var tabs = this .getTabs();
39 var _this = this ;
40 // 显示当前选中的tab对应的panel组件,隐藏没有选中的
41 tabs.forEach(function (tab) {
42 return tab.show = tab.name === _this.currentValue;
43 });
44 }
45 }
46 });
getTabs
是一个公用的方法,使用this.$children
来拿到所有的panel组件实例。
需要注意的是,在methods
里使用了有function回调的方法时(例如遍历数组的方法forEach
),在回调内的this
不再执行当前的Vue实例,也就是tabs组件本身,所以要在外层设置一个_this=this
的局部变量来间接使用this
。 如果你熟悉ES2015,也可以直接使用箭头函数=>
,我们会在实战篇里介绍相关的用法。
遍历了每一个panel组件后,把它的label
和name
提取出来,构成一个Object并添加到数据navList
数组里,后面我们会在template
里用到它。
设置完navList
数组后,我们调用了updateStatus
方法,又将panel组件遍历了以便,不过这时是为了将当前选中的tab对应的panel组件内容显示出来,把没有选中的隐藏掉。 因为在上一步操作里,我们有可能需要设置currentValue
来标识当前选中项的name
(在用户没有设置value
时,才会自动设置),所以必须要遍历2次才可以。
拿到navList
后,就需要对它用v-for
指令把tab的标题渲染出来,并且判断每个tab当前的状态。 这部分代码如下:
1 Vue.component("tabs", {
2 template: "\
3 ",
11 props: {
12 // 这里的value是为了可以使用v-model
13 value: {
14 type: [String, Number]
15 }
16 },
17 data: function () {
18 return {
19 // 因为不能修改value,所以复制一份自己维护
20 currentValue: this .value,
21 navList: []
22 };
23 },
24 methods: {
25 tabCls: function (item) {
26 return [
27 "tabs-tab",
28 {
29 // 给当前选中的tab加一个class
30 "tabs-tab-active": item.name === this .currentValue
31 }
32 ];
33 },
34 // 点击tab标题时触发
35 handleChange: function (index) {
36 var nav = this .navList[index];
37 var name = nav.name;
38 // 改变当前选中的tab,并触发下面的watch
39 this .currentValue = name;
40 // 更新value
41 this .$emit("input", name);
42 // 触发一个自定义事件,供父级使用
43 this .$emit("on-click", name);
44 }
45 },
46 watch: {
47 value: function (val) {
48 this .currentValue = val;
49 },
50 currentValue: function () {
51 // 在当前选中的tab发生变化时,更新panel的显示状态
52 this .updateStatus();
53 }
54 }
55 });
在使用v-for
指令循环显示tab标题时,使用v-bind:class
指向了一个名为tabCls
的methods
来动态设置class
名称。 因为计算属性不能接收参数,无法知道当前tab是否是选中的,所以这里我们才用到methods。 不过要知道,methods是不缓存的,可以回顾关于计算属性的章节。
点击每个tab标题时,会触发handleChange
方法来改变当前选中tab的索引,也就是panel组件的name。 在watch选项里,我们监听了currentValue
,当其发生变化时,触发updateStatus
方法来更新panel组件的显示状态。
以上就是标签页组件的核心代码分解。 总结一下该示例的技术难点:
使用了组件嵌套的方式,将一系列panel组件作为tabs组件的slot;
tabs组件和panel组件通信上,使用了$parent
和$children
的方法访问父链和子链;
定义了prop:value
和data:currentValue
,使用$emit("input")
来实现v-model
的用法。
以下是标签页组件的完整代码:
index.html:
1 DOCTYPE html >
2 < html lang ="zh" >
3 < head >
4 < meta charset ="UTF-8" >
5 < meta name ="viewport" content ="width=device-width, initial-scale=1.0" >
6 < meta http-equiv ="X-UA-Compatible" content ="ie=edge" >
7 < title > 标签页组件title >
8 < link rel ="stylesheet" type ="text/css" href ="style.css" >
9 head >
10 < body >
11 < div id ="app" >
12 < tabs v-model ="activeKey" >
13 < panel label ="标签一" name ="1" > 标签一的内容panel >
14 < panel label ="标签二" name ="2" > 标签二的内容panel >
15 < panel label ="标签三" name ="3" > 标签三的内容panel >
16 tabs >
17 div >
18
19 < script src ="vue.js" > script >
20 < script src ="panel.js" > script >
21 < script src ="tabs.js" > script >
22 < script >
23 var app = new Vue({
24 el: " #app " ,
25 data: {
26 activeKey: " 1 "
27 }
28 });
29 script >
30 body >
31 html >
panel.js:
1 Vue.component("panel", {
2 name: "panel",
3 template: "\
4 \
5 \
6
",
7 props: {
8 name: {
9 type: String
10 },
11 label: {
12 type: String,
13 default : ""
14 }
15 },
16 data: function () {
17 return {
18 show: true
19 };
20 },
21 methods: {
22 updateNav() {
23 this .$parent.updateNav();
24 }
25 },
26 watch: {
27 label() {
28 this .updateNav();
29 }
30 },
31 mounted() {
32 this .updateNav();
33 }
34 });
tabs.js:
1 Vue.component("tabs", {
2 template: "\
3 ",
11 props: {
12 value: {
13 type: [String, Number]
14 }
15 },
16 data: function () {
17 return {
18 currentValue: this .value,
19 navList: []
20 };
21 },
22 methods: {
23 tabCls: function (item) {
24 return [
25 "tabs-tab",
26 {
27 "tabs-tab-active": item.name === this .currentValue
28 }
29 ];
30 },
31 // 点击tab标题时触发
32 handleChange: function (index) {
33 var nav = this .navList[index];
34 var name = nav.name;
35 // 改变当前选中的tab,并触发下面的watch
36 this .currentValue = name;
37 // 更新value
38 this .$emit("input", name);
39 // 触发一个自定义事件,供父级使用
40 this .$emit("on-click", name);
41 },
42 getTabs() {
43 // 通过遍历子组件,得到所有的panel组件
44 return this .$children.filter(function (item) {
45 return item.$options.name === "panel";
46 });
47 },
48 updateNav() {
49 this .navList = [];
50 // 设置对this的引用,在function回调里,this指向的并不是Vue实例
51 var _this = this ;
52 this .getTabs().forEach(function (panel, index) {
53 _this.navList.push({
54 label: panel.label,
55 name: panel.name || index
56 });
57 // 如果没有给panel设置name,默认设置它的索引
58 if (!panel.name) {
59 panel.name = index;
60 }
61 // 设置当前选中的tab的索引,在后面介绍
62 if (index === 0) {
63 if (!_this.currentValue) {
64 _this.currentValue = panel.name || index;
65 }
66 }
67 });
68 },
69 updateStatus() {
70 var tabs = this .getTabs();
71 var _this = this ;
72 // 显示当前选中的tab对应的panel组件,隐藏没有选中的
73 tabs.forEach(function (tab) {
74 return tab.show = tab.name === _this.currentValue;
75 });
76 }
77 },
78 watch: {
79 value: function (val) {
80 this .currentValue = val;
81 },
82 currentValue: function () {
83 // 在当前选中的tab发生变化时,更新panel的显示状态
84 this .updateStatus();
85 }
86 }
87 });
style.css:
1 [v-cloak] {display :none ;}
2
3 .tabs {font-size :14px ; color :#657180 ;}
4 .tabs-bar:after {
5 content :"" ; display :block ;
6 width :100% ; height :1px ;
7 background :#D7DDE4 ; margin-top :-1px ;
8 }
9 .tabs-tab {
10 display :inline-block ; cursor :pointer ; position :relative ;
11 padding :4px 16px ; margin-right :6px ;
12 background :#FFF ; border :1px solid #D7DDE4 ;
13 }
14 .tabs-tab-active {
15 cursor :#3399FF ;
16 border-top :1px solid #3399FF ; border-bottom :1px solid #FFF ;
17 }
18 .tabs-tab-active:before {
19 content :"" ; display :block ; height :1px ; background :#3399FF ;
20 position :absolute ; top :0 ; left :0 ; right :0 ;
21 }
22 .tabs-content {padding :8px 0 ;}
练习1: 给panel组件新增一个prop:closable
的布尔值,来支持是否可以关闭这个panel,如果开启,在tabs的标签标题上会有一个关闭的按钮;
提示: 在初始化panel时,我们是在mounted里通知的。 关闭时,你会用到beforeDestroy。
练习2: 尝试在切换panel的显示与隐藏时,使用滑动的动画。提示:可以使用CSS3的transform:translateX
。
转载于:https://www.cnblogs.com/geeksss/p/10810698.html
你可能感兴趣的:(javascript,webpack,设计模式,ViewUI)
【iOS】MVC设计模式
Magnetic_h
ios mvc 设计模式 objective-c 学习 ui
MVC前言如何设计一个程序的结构,这是一门专门的学问,叫做"架构模式"(architecturalpattern),属于编程的方法论。MVC模式就是架构模式的一种。它是Apple官方推荐的App开发架构,也是一般开发者最先遇到、最经典的架构。MVC各层controller层Controller/ViewController/VC(控制器)负责协调Model和View,处理大部分逻辑它将数据从Mod
Long类型前后端数据不一致
igotyback
前端
响应给前端的数据浏览器控制台中response中看到的Long类型的数据是正常的到前端数据不一致前后端数据类型不匹配是一个常见问题,尤其是当后端使用Java的Long类型(64位)与前端JavaScript的Number类型(最大安全整数为2^53-1,即16位)进行数据交互时,很容易出现精度丢失的问题。这是因为JavaScript中的Number类型无法安全地表示超过16位的整数。为了解决这个问
【一起学Rust | 设计模式】习惯语法——使用借用类型作为参数、格式化拼接字符串、构造函数
广龙宇
一起学Rust # Rust设计模式 rust 设计模式 开发语言
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档文章目录前言一、使用借用类型作为参数二、格式化拼接字符串三、使用构造函数总结前言Rust不是传统的面向对象编程语言,它的所有特性,使其独一无二。因此,学习特定于Rust的设计模式是必要的。本系列文章为作者学习《Rust设计模式》的学习笔记以及自己的见解。因此,本系列文章的结构也与此书的结构相同(后续可能会调成结构),基本上分为三个部分
DIV+CSS+JavaScript技术制作网页(旅游主题网页设计与制作)云南大理
STU学生网页设计
网页设计 期末网页作业 html静态网页 html5期末大作业 网页设计 web大作业
️精彩专栏推荐作者主页:【进入主页—获取更多源码】web前端期末大作业:【HTML5网页期末作业(1000套)】程序员有趣的告白方式:【HTML七夕情人节表白网页制作(110套)】文章目录二、网站介绍三、网站效果▶️1.视频演示2.图片演示四、网站代码HTML结构代码CSS样式代码五、更多源码二、网站介绍网站布局方面:计划采用目前主流的、能兼容各大主流浏览器、显示效果稳定的浮动网页布局结构。网站程
关于城市旅游的HTML网页设计——(旅游风景云南 5页)HTML+CSS+JavaScript
二挡起步
web前端期末大作业 javascript html css 旅游 风景
⛵源码获取文末联系✈Web前端开发技术描述网页设计题材,DIV+CSS布局制作,HTML+CSS网页设计期末课程大作业|游景点介绍|旅游风景区|家乡介绍|等网站的设计与制作|HTML期末大学生网页设计作业,Web大学生网页HTML:结构CSS:样式在操作方面上运用了html5和css3,采用了div+css结构、表单、超链接、浮动、绝对定位、相对定位、字体样式、引用视频等基础知识JavaScrip
HTML网页设计制作大作业(div+css) 云南我的家乡旅游景点 带文字滚动
二挡起步
web前端期末大作业 web设计网页规划与设计 html css javascript dreamweaver 前端
Web前端开发技术描述网页设计题材,DIV+CSS布局制作,HTML+CSS网页设计期末课程大作业游景点介绍|旅游风景区|家乡介绍|等网站的设计与制作HTML期末大学生网页设计作业HTML:结构CSS:样式在操作方面上运用了html5和css3,采用了div+css结构、表单、超链接、浮动、绝对定位、相对定位、字体样式、引用视频等基础知识JavaScript:做与用户的交互行为文章目录前端学习路线
webpack图片等资源的处理
dmengmeng
需要的loaderfile-loader(让我们可以引入这些资源文件)url-loader(其实是file-loader的二次封装)img-loader(处理图片所需要的)在没有使用任何处理图片的loader之前,比如说css中用到了背景图片,那么最后打包会报错的,因为他没办法处理图片。其实你只想能够使用图片的话。只加一个file-loader就可以,打开网页能准确看到图片。{test:/\.(p
node.js学习
小猿L
node.js node.js 学习 vim
node.js学习实操及笔记温故node.js,node.js学习实操过程及笔记~node.js学习视频node.js官网node.js中文网实操笔记githubcsdn笔记为什么学node.js可以让别人访问我们编写的网页为后续的框架学习打下基础,三大框架vuereactangular离不开node.jsnode.js是什么官网:node.js是一个开源的、跨平台的运行JavaScript的运行
JavaScript 中,深拷贝(Deep Copy)和浅拷贝(Shallow Copy)
跳房子的前端
前端面试 javascript 开发语言 ecmascript
在JavaScript中,深拷贝(DeepCopy)和浅拷贝(ShallowCopy)是用于复制对象或数组的两种不同方法。了解它们的区别和应用场景对于避免潜在的bugs和高效地处理数据非常重要。以下是对深拷贝和浅拷贝的详细解释,包括它们的概念、用途、优缺点以及实现方式。1.浅拷贝(ShallowCopy)概念定义:浅拷贝是指创建一个新的对象或数组,其中包含了原对象或数组的基本数据类型的值和对引用数
JAVA学习笔记之23种设计模式学习
victorfreedom
Java技术 设计模式 android java 常用设计模式
博主最近买了《设计模式》这本书来学习,无奈这本书是以C++语言为基础进行说明,整个学习流程下来效率不是很高,虽然有的设计模式通俗易懂,但感觉还是没有充分的掌握了所有的设计模式。于是博主百度了一番,发现有大神写过了这方面的问题,于是博主迅速拿来学习。一、设计模式的分类总体来说设计模式分为三大类:创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。结构型模式,共七种:适配器
JavaScript `Map` 和 `WeakMap`详细解释
跳房子的前端
JavaScript 原生方法 javascript 前端 开发语言
在JavaScript中,Map和WeakMap都是用于存储键值对的数据结构,但它们有一些关键的不同之处。MapMap是一种可以存储任意类型的键值对的集合。它保持了键值对的插入顺序,并且可以通过键快速查找对应的值。Map提供了一些非常有用的方法和属性来操作这些数据对:set(key,value):将一个键值对添加到Map中。如果键已经存在,则更新其对应的值。get(key):获取指定键的值。如果键
切换淘宝最新npm镜像源是
hai40587
npm 前端 node.js
切换淘宝最新npm镜像源是一个相对简单的过程,但首先需要明确当前淘宝npm镜像源的状态和最新的镜像地址。由于网络环境和服务更新,镜像源的具体地址可能会发生变化,因此,我将基于当前可获取的信息,提供一个通用的切换步骤,并附上最新的镜像地址(截至回答时)。一、了解npm镜像源npm(NodePackageManager)是JavaScript的包管理器,用于安装、更新和管理项目依赖。由于npm官方仓库
设计模式之建造者模式(通俗易懂--代码辅助理解【Java版】)
ok!ko
设计模式 设计模式 建造者模式 java
文章目录设计模式概述1、建造者模式2、建造者模式使用场景3、优点4、缺点5、主要角色6、代码示例:1)实现要求2)UML图3)实现步骤:1)创建一个表示食物条目和食物包装的接口2)创建实现Packing接口的实体类3)创建实现Item接口的抽象类,该类提供了默认的功能4)创建扩展了Burger和ColdDrink的实体类5)创建一个Meal类,带有上面定义的Item对象6)创建一个MealBuil
详解“c:/work/src/components/a/b.vue“‘ has no default export报错原因
hw_happy
开发语言 前端 vue.js javascript
前情提要在一个vue文件中需要引入定义的b.vue文件,但是提示b文件没有默认导出,对于vue2文件来说有exportdefault,在中,所有定义的变量、函数和组件都会自动被视为默认导出的组件内容。因此,不需要显式地使用exportdefault来导出组件。但是在我引用这个文件的时候还是提示了这个错误,原来是我的项目使用了ts和vite\webpack,因为TypeScript和Vue的默认导出
高性能javascript--算法和流程控制
海淀萌狗
-for,while和do-while性能相当-避免使用for-in循环,==除非遍历一个属性量未知的对象==es5:for-in遍历的对象便不局限于数组,还可以遍历对象。原因:for-in每次迭代操作会同时搜索实例或者原型属性,for-in循环的每次迭代都会产生更多开销,因此要比其他循环类型慢,一般速度为其他类型循环的1/7。因此,除非明确需要迭代一个属性数量未知的对象,否则应避免使用for-i
360前端星计划-动画可以这么玩
马小蜗
动画的基本原理定时器改变对象的属性根据新的属性重新渲染动画functionupdate(context){//更新属性}constticker=newTicker();ticker.tick(update,context);动画的种类1、JavaScript动画操作DOMCanvas2、CSS动画transitionanimation3、SVG动画SMILJS动画的优缺点优点:灵活度、可控性、性能
JavaScript中秋快乐!
Q_w7742
javascript 开发语言 ecmascript
我们来实现一个简单的祝福网页~主要的难度在于使用canvas绘图当点击canvas时候,跳出“中秋节快乐”字样,需要注册鼠标单击事件和计时器。首先定义主要函数:初始化当点击canvas之后转到onCanvasClick函数,绘图生成灯笼。functiononCanvasClick(){//事件处理函数context.clearRect(0,0,canvas1.width,canvas1.heigh
Nginx从入门到实践(三)
听你讲故事啊
动静分离动静分离是将网站静态资源(JavaScript,CSS,img等文件)与后台应用分开部署,提高用户访问静态代码的速度,降低对后台应用访问。动静分离的一种做法是将静态资源部署在nginx上,后台项目部署到应用服务器上,根据一定规则静态资源的请求全部请求nginx服务器,达到动静分离的目标。rewrite规则Rewrite规则常见正则表达式Rewrite主要的功能就是实现URL的重写,Ngin
设计模式 23 访问者模式
WineMonk
# 设计模式 设计模式 访问者模式
设计模式23创建型模式(5):工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式结构型模式(7):适配器模式、桥接模式、组合模式、装饰者模式、外观模式、享元模式、代理模式行为型模式(11):责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式、访问者模式文章目录设计模式23访问者模式(VisitorPattern)1定义2结构3
设计模式】Listener模式和Visitor模式的区别
不爱洗脚的小滕
设计模式 访问者模式 java golang
文章目录前言一、介绍Listener模式Visitor模式二、代码实现2.1Listener模式的Java实现2.2Listener模式的Go实现2.3Visitor模式的Java实现2.4Visitor模式的Go实现三、总结前言在软件设计中,设计模式是解决特定问题的通用解决方案。Listener模式和Visitor模式是两种常见的行为设计模式,它们在不同的场景下提供了解决问题的有效方法。本文将详
Nginx的使用场景:构建高效、可扩展的Web架构
张某布响丸辣
nginx 前端 架构
Nginx,作为当今最流行的Web服务器和反向代理软件之一,凭借其高性能、稳定性和灵活性,在众多Web项目中扮演着核心角色。无论是个人博客、中小型网站,还是大型企业级应用,Nginx都能提供强大的支持。本文将探讨Nginx的几个主要使用场景,帮助读者理解如何在实际项目中充分利用Nginx的优势。1.静态文件服务对于包含大量静态文件(如HTML、CSS、JavaScript、图片等)的网站,Ngin
前端知识点
ZhangTao_zata
前端 javascript css
下面是一个最基本的html代码body{font-family:Arial,sans-serif;margin:20px;}//JavaScriptfunctionthatdisplaysanalertwhencalledfunctionshowMessage(){alert("Hello!Youclickedthebutton.");}MyFirstHTMLPageWelcometoMyPage
开发游戏的学习规划
杰克逊的日记
游戏 学习
第一阶段:●C#语言快速系统地学习一遍(基础的语法、面向对象、基础的数据结构、基础的设计模式)●Unity的2D和3D部分及UI、动画、物理系统●阶段性测验:需要去用前面所学的这些基础知识来完成一个简单的2d或者3d的案例,将通过一个自制的《Flappybird》游戏案例讲解游戏开发的思想及方法,并将《Flappybird》这个游戏进一步改造成一个横版射击类游戏《Crazybird》以巩固并且升华
【JS】前端文件读取FileReader操作总结
程序员-张师傅
前端 前端 javascript 开发语言
前端文件读取FileReader操作总结FileReader是JavaScript中的一个WebAPI,它允许web应用程序异步读取用户计算机上的文件(或原始数据缓冲区)的内容,例如读取文件以获取其内容,并在不将文件发送到服务器的情况下在客户端使用它。这对于处理图片、文本文件等非常有用,尤其是当你想要在用户界面中即时显示文件内容或进行文件预览时。创建FileReader对象首先,你需要创建一个Fi
从单体到微服务:FastAPI ‘挂载’子应用程序的转变
黑金IT
fastapi 微服务 fastapi 架构
在现代Web应用开发中,模块化架构是一种常见的设计模式,它有助于将大型应用程序分解为更小、更易于管理的部分。FastAPI,作为一个高性能的PythonWeb框架,提供了强大的支持来实现这种模块化设计。通过“挂载”子应用程序,我们可以为不同的功能区域(如前端接口、管理员接口和用户中心)创建独立的应用程序,并将它们整合到一个主应用程序中。本文将详细介绍如何在FastAPI中使用“挂载”子应用程序的方
webstorm报错TypeError: this.cliEngine is not a constructor
Blue_Color
点击Details在控制台会显示报错的位置TypeError:this.cliEngineisnotaconstructoratESLintPlugin.invokeESLint(/Applications/RubyMine.app/Contents/plugins/JavaScriptLanguage/languageService/eslint/bin/eslint-plugin.js:97:
创建一个完整的购物商城系统是一个复杂的项目,涉及前端(用户界面)、后端(服务器逻辑)、数据库等多个部分。由于篇幅限制,我无法在这里提供一个完整的系统代码,但我可以分别给出一些关键部分的示例代码,涵盖几
uthRaman
前端 ui 服务器
前端(HTML/CSS/JavaScript)grsyzp.cnHTML页面结构(index.html)html购物商城欢迎来到购物商城JavaScript(Ajax请求商品数据,app.js)javascriptdocument.addEventListener('DOMContentLoaded',function(){fetch('/api/products').then(response=
了解 UNPKG:前端开发者的包管理利器
小于负无穷
前端 javascript typescript css html5 node.js
在现代前端开发中,JavaScript包管理和模块化是至关重要的,而npm则是最流行的JavaScript包管理器之一。不过,随着前端项目复杂性的增加,有时候我们希望快速引入外部依赖,而无需本地安装和构建。此时,CDN(内容分发网络)成为了一种方便快捷的解决方案,而UNPKG就是这种方式中的佼佼者。什么是UNPKG?UNPKG是一个基于npm的内容分发网络(CDN),它允许开发者直接通过URL从n
[面试高频问题]关于多线程的单例模式
朱玥玥要每天学习
java 单例模式 开发语言
单例模式什么是设计模式?设计模式可以看做为框架或者是围棋中的”棋谱”,红方当头炮,黑方马来跳.根据一些固定的套路下,能保证局势不会吃亏.在日常的程序设计中,往往有许多业务场景,根据这些场景,大佬们总结出了一些固定的套路.按照这个套路来实现代码,也不会吃亏.什么是单例模式,保证某类在程序中只有一个实例,而不会创建多份实例.单例模式具体的实现方式:可分为”懒汉模式”,”饿汉模式”.饿汉模式类加载的同时
从简单到复杂:三种工厂模式的对比与应用
技术拾光者
设计模式 java 设计模式 简单工厂模式 抽象工厂模式 工厂方法模式
在软件设计中,创建型设计模式用于处理对象创建的复杂性。本文将对比三种常见的创建型设计模式:简单工厂模式、工厂方法模式和抽象工厂模式。一,简单工厂模式定义:简单工厂模式(SimpleFactoryPattern)定义了一个工厂类,该类可以根据传入的参数决定创建哪一种产品实例。结构:产品(Product):定义产品的接口。具体产品(ConcreteProduct):实现具体产品。工厂(Factory)
apache 安装linux windows
墙头上一根草
apache inux windows
linux安装Apache 有两种方式一种是手动安装通过二进制的文件进行安装,另外一种就是通过yum 安装,此中安装方式,需要物理机联网。以下分别介绍两种的安装方式
通过二进制文件安装Apache需要的软件有apr,apr-util,pcre
1,安装 apr 下载地址:htt
fill_parent、wrap_content和match_parent的区别
Cb123456
match_parent fill_parent
fill_parent、wrap_content和match_parent的区别:
1)fill_parent
设置一个构件的布局为fill_parent将强制性地使构件扩展,以填充布局单元内尽可能多的空间。这跟Windows控件的dockstyle属性大体一致。设置一个顶部布局或控件为fill_parent将强制性让它布满整个屏幕。
2) wrap_conte
网页自适应设计
天子之骄
html css 响应式设计 页面自适应
网页自适应设计
网页对浏览器窗口的自适应支持变得越来越重要了。自适应响应设计更是异常火爆。再加上移动端的崛起,更是如日中天。以前为了适应不同屏幕分布率和浏览器窗口的扩大和缩小,需要设计几套css样式,用js脚本判断窗口大小,选择加载。结构臃肿,加载负担较大。现笔者经过一定时间的学习,有所心得,故分享于此,加强交流,共同进步。同时希望对大家有所
[sql server] 分组取最大最小常用sql
一炮送你回车库
SQL Server
--分组取最大最小常用sql--测试环境if OBJECT_ID('tb') is not null drop table tb;gocreate table tb( col1 int, col2 int, Fcount int)insert into tbselect 11,20,1 union allselect 11,22,1 union allselect 1
ImageIO写图片输出到硬盘
3213213333332132
java image
package awt;
import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imagei
自己的String动态数组
宝剑锋梅花香
java 动态数组 数组
数组还是好说,学过一两门编程语言的就知道,需要注意的是数组声明时需要把大小给它定下来,比如声明一个字符串类型的数组:String str[]=new String[10]; 但是问题就来了,每次都是大小确定的数组,我需要数组大小不固定随时变化怎么办呢? 动态数组就这样应运而生,龙哥给我们讲的是自己用代码写动态数组,并非用的ArrayList 看看字符
pinyin4j工具类
darkranger
.net
pinyin4j工具类Java工具类 2010-04-24 00:47:00 阅读69 评论0 字号:大中小
引入pinyin4j-2.5.0.jar包:
pinyin4j是一个功能强悍的汉语拼音工具包,主要是从汉语获取各种格式和需求的拼音,功能强悍,下面看看如何使用pinyin4j。
本人以前用AscII编码提取工具,效果不理想,现在用pinyin4j简单实现了一个。功能还不是很完美,
StarUML学习笔记----基本概念
aijuans
UML建模
介绍StarUML的基本概念,这些都是有效运用StarUML?所需要的。包括对模型、视图、图、项目、单元、方法、框架、模型块及其差异以及UML轮廓。
模型、视与图(Model, View and Diagram)
&
Activiti最终总结
avords
Activiti id 工作流
1、流程定义ID:ProcessDefinitionId,当定义一个流程就会产生。
2、流程实例ID:ProcessInstanceId,当开始一个具体的流程时就会产生,也就是不同的流程实例ID可能有相同的流程定义ID。
3、TaskId,每一个userTask都会有一个Id这个是存在于流程实例上的。
4、TaskDefinitionKey和(ActivityImpl activityId
从省市区多重级联想到的,react和jquery的差别
bee1314
jquery UI react
在我们的前端项目里经常会用到级联的select,比如省市区这样。通常这种级联大多是动态的。比如先加载了省,点击省加载市,点击市加载区。然后数据通常ajax返回。如果没有数据则说明到了叶子节点。 针对这种场景,如果我们使用jquery来实现,要考虑很多的问题,数据部分,以及大量的dom操作。比如这个页面上显示了某个区,这时候我切换省,要把市重新初始化数据,然后区域的部分要从页面
Eclipse快捷键大全
bijian1013
java eclipse 快捷键
Ctrl+1 快速修复(最经典的快捷键,就不用多说了)Ctrl+D: 删除当前行 Ctrl+Alt+↓ 复制当前行到下一行(复制增加)Ctrl+Alt+↑ 复制当前行到上一行(复制增加)Alt+↓ 当前行和下面一行交互位置(特别实用,可以省去先剪切,再粘贴了)Alt+↑ 当前行和上面一行交互位置(同上)Alt+← 前一个编辑的页面Alt+→ 下一个编辑的页面(当然是针对上面那条来说了)Alt+En
js 笔记 函数
征客丶
JavaScript
一、函数的使用
1.1、定义函数变量
var vName = funcation(params){
}
1.2、函数的调用
函数变量的调用: vName(params);
函数定义时自发调用:(function(params){})(params);
1.3、函数中变量赋值
var a = 'a';
var ff
【Scala四】分析Spark源代码总结的Scala语法二
bit1129
scala
1. Some操作
在下面的代码中,使用了Some操作:if (self.partitioner == Some(partitioner)),那么Some(partitioner)表示什么含义?首先partitioner是方法combineByKey传入的变量,
Some的文档说明:
/** Class `Some[A]` represents existin
java 匿名内部类
BlueSkator
java匿名内部类
组合优先于继承
Java的匿名类,就是提供了一个快捷方便的手段,令继承关系可以方便地变成组合关系
继承只有一个时候才能用,当你要求子类的实例可以替代父类实例的位置时才可以用继承。
在Java中内部类主要分为成员内部类、局部内部类、匿名内部类、静态内部类。
内部类不是很好理解,但说白了其实也就是一个类中还包含着另外一个类如同一个人是由大脑、肢体、器官等身体结果组成,而内部类相
盗版win装在MAC有害发热,苹果的东西不值得买,win应该不用
ljy325
游戏 apple windows XP OS
Mac mini 型号: MC270CH-A RMB:5,688
Apple 对windows的产品支持不好,有以下问题:
1.装完了xp,发现机身很热虽然没有运行任何程序!貌似显卡跑游戏发热一样,按照那样的发热量,那部机子损耗很大,使用寿命受到严重的影响!
2.反观安装了Mac os的展示机,发热量很小,运行了1天温度也没有那么高
&nbs
读《研磨设计模式》-代码笔记-生成器模式-Builder
bylijinnan
java 设计模式
声明: 本文只为方便我个人查阅和理解,详细的分析以及源代码请移步 原作者的博客http://chjavach.iteye.com/
/**
* 生成器模式的意图在于将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示(GoF)
* 个人理解:
* 构建一个复杂的对象,对于创建者(Builder)来说,一是要有数据来源(rawData),二是要返回构
JIRA与SVN插件安装
chenyu19891124
SVN jira
JIRA安装好后提交代码并要显示在JIRA上,这得需要用SVN的插件才能看见开发人员提交的代码。
1.下载svn与jira插件安装包,解压后在安装包(atlassian-jira-subversion-plugin-0.10.1)
2.解压出来的包里下的lib文件夹下的jar拷贝到(C:\Program Files\Atlassian\JIRA 4.3.4\atlassian-jira\WEB
常用数学思想方法
comsci
工作
对于搞工程和技术的朋友来讲,在工作中常常遇到一些实际问题,而采用常规的思维方式无法很好的解决这些问题,那么这个时候我们就需要用数学语言和数学工具,而使用数学工具的前提却是用数学思想的方法来描述问题。。下面转帖几种常用的数学思想方法,仅供学习和参考
函数思想
把某一数学问题用函数表示出来,并且利用函数探究这个问题的一般规律。这是最基本、最常用的数学方法
pl/sql集合类型
daizj
oracle 集合 type pl/sql
--集合类型
/*
单行单列的数据,使用标量变量
单行多列数据,使用记录
单列多行数据,使用集合(。。。)
*集合:类似于数组也就是。pl/sql集合类型包括索引表(pl/sql table)、嵌套表(Nested Table)、变长数组(VARRAY)等
*/
/*
--集合方法
&n
[Ofbiz]ofbiz初用
dinguangx
电商 ofbiz
从github下载最新的ofbiz(截止2015-7-13),从源码进行ofbiz的试用
1. 加载测试库
ofbiz内置derby,通过下面的命令初始化测试库
./ant load-demo (与load-seed有一些区别)
2. 启动内置tomcat
./ant start
或
./startofbiz.sh
或
java -jar ofbiz.jar
&
结构体中最后一个元素是长度为0的数组
dcj3sjt126com
c gcc
在Linux源代码中,有很多的结构体最后都定义了一个元素个数为0个的数组,如/usr/include/linux/if_pppox.h中有这样一个结构体: struct pppoe_tag { __u16 tag_type; __u16 tag_len; &n
Linux cp 实现强行覆盖
dcj3sjt126com
linux
发现在Fedora 10 /ubutun 里面用cp -fr src dest,即使加了-f也是不能强行覆盖的,这时怎么回事的呢?一两个文件还好说,就输几个yes吧,但是要是n多文件怎么办,那还不输死人呢?下面提供三种解决办法。 方法一
我们输入alias命令,看看系统给cp起了一个什么别名。
[root@localhost ~]# aliasalias cp=’cp -i’a
Memcached(一)、HelloWorld
frank1234
memcached
一、简介
高性能的架构离不开缓存,分布式缓存中的佼佼者当属memcached,它通过客户端将不同的key hash到不同的memcached服务器中,而获取的时候也到相同的服务器中获取,由于不需要做集群同步,也就省去了集群间同步的开销和延迟,所以它相对于ehcache等缓存来说能更好的支持分布式应用,具有更强的横向伸缩能力。
二、客户端
选择一个memcached客户端,我这里用的是memc
Search in Rotated Sorted Array II
hcx2013
search
Follow up for "Search in Rotated Sorted Array":What if duplicates are allowed?
Would this affect the run-time complexity? How and why?
Write a function to determine if a given ta
Spring4新特性——更好的Java泛型操作API
jinnianshilongnian
spring4 generic type
Spring4新特性——泛型限定式依赖注入
Spring4新特性——核心容器的其他改进
Spring4新特性——Web开发的增强
Spring4新特性——集成Bean Validation 1.1(JSR-349)到SpringMVC
Spring4新特性——Groovy Bean定义DSL
Spring4新特性——更好的Java泛型操作API
Spring4新
CentOS安装JDK
liuxingguome
centos
1、行卸载原来的:
[root@localhost opt]# rpm -qa | grep java
tzdata-java-2014g-1.el6.noarch
java-1.7.0-openjdk-1.7.0.65-2.5.1.2.el6_5.x86_64
java-1.6.0-openjdk-1.6.0.0-11.1.13.4.el6.x86_64
[root@localhost
二分搜索专题2-在有序二维数组中搜索一个元素
OpenMind
二维数组 算法 二分搜索
1,设二维数组p的每行每列都按照下标递增的顺序递增。
用数学语言描述如下:p满足
(1),对任意的x1,x2,y,如果x1<x2,则p(x1,y)<p(x2,y);
(2),对任意的x,y1,y2, 如果y1<y2,则p(x,y1)<p(x,y2);
2,问题:
给定满足1的数组p和一个整数k,求是否存在x0,y0使得p(x0,y0)=k?
3,算法分析:
(
java 随机数 Math与Random
SaraWon
java Math Random
今天需要在程序中产生随机数,知道有两种方法可以使用,但是使用Math和Random的区别还不是特别清楚,看到一篇文章是关于的,觉得写的还挺不错的,原文地址是
http://www.oschina.net/question/157182_45274?sort=default&p=1#answers
产生1到10之间的随机数的两种实现方式:
//Math
Math.roun
oracle创建表空间
tugn
oracle
create temporary tablespace TXSJ_TEMP
tempfile 'E:\Oracle\oradata\TXSJ_TEMP.dbf'
size 32m
autoextend on
next 32m maxsize 2048m
extent m
使用Java8实现自己的个性化搜索引擎
yangshangchuan
java superword 搜索引擎 java8 全文检索
需要对249本软件著作实现句子级别全文检索,这些著作均为PDF文件,不使用现有的框架如lucene,自己实现的方法如下:
1、从PDF文件中提取文本,这里的重点是如何最大可能地还原文本。提取之后的文本,一个句子一行保存为文本文件。
2、将所有文本文件合并为一个单一的文本文件,这样,每一个句子就有一个唯一行号。
3、对每一行文本进行分词,建立倒排表,倒排表的格式为:词=包含该词的总行数N=行号