组件都是由prop、event、slot三部分组成,它们构成了 Vue.js 组件的 API
prop 定义了这个组件有哪些可配置的属性,组件的核心功能也都是它来确定的。写通用组件时,props最好用对象的写法,这样可以针对每个属性设置类型、默认值或自定义校验属性的值。
比如封装一个按钮组件 :
使用组件
组件里定义的 props,都是修改定义在 data 里单向数据流,也就是只能通过父级修改,组件自己不能修改 props 的值,只能的数据,非要修改,也是通过后面介绍的自定义事件通知父级,由父级来修改。
如果要给上面的按钮组件 添加一些文字内容,就要用到组件的第二个 API:插槽 slot,它可以分发组件的内容,比如在上面的按钮组件中定义一个插槽:
子组件:
这里的 节点就是指定的一个插槽的位置,这样在组件内部就可以扩展内容了:
父组件(是调用的子组件):
按钮 1
按钮 2
再举一个例子:
//父组件
我是父组件
//子组件1
我是父组件插槽内容
//子组件slotOne1
我是slotOne1组件
显示为:
我是父组件我是slotOne1组件我是父组件插槽内容
当需要多个插槽时,会用到具名 slot,比如上面的组件我们再增加一个 slot,用于设置另一个图标组件:
按钮 3
这样,父级内定义的内容,就会出现在组件对应的 slot 里,没有写名字的,就是默认的 slot。
在组件的 里也可以写一些默认的内容,这样在父级没有写任何 slot 时,它们就会出现,比如:
提交
给组件 加一个点击事件,目前有两种写法,先看自定义事件 event(部分代码省略):
通过 $emit,就可以触发自定义的事件 on-click ,在父级通过 @on-click 来监听:
上面的 click 事件,是在组件内部的 元素上声明的,这里还有另一种方法,直接在父级声明,但为了区分原生事件和自定义事件,要用到事件修饰符 .native,所以上面的示例也可以这样写:
如果不写 .native 修饰符,那上面的 @click 就是自定义事件 click,而非原生事件 click,但我们在组件内只触发了 on-click 事件,而不是 click,所以直接写 @click 会监听不到。
组件一般有以下几种关系: A.vue–>B.vue–>C.vue ;B.vue–>D.vue A 和 B、B 和 C、B 和 D 都是父子关系,C 和 D 是兄弟关系,A 和 C 是隔代关系(可能隔多代)。组件间经常会通信,Vue.js 内置的通信手段一般有两种:
比如下面的示例中,用 ref 来访问组件(部分代码省略):
// component-aexport default {
data () {
return {
title: 'Vue.js'
}
},
methods: {
sayHello () {
window.alert('Hello');
}
}
}
$parent 和 $children 类似,也是基于当前上下文访问父组件或全部子组件的。
$children : 当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。如果你发现自己正在尝试使用 $children 来进行数据绑定,考虑使用一个数组配合 v-for 来生成子组件,并且使用 Array 作为真正的来源。
$parent: 当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。
这两种方法的弊端是,无法在跨级或兄弟间通信
provide / inject可以解决上述弊端
A.vue–>B.vue,用法:
// A.vue
export default {
provide: {
name: 'Aresn'
}
}
// B.vue
export default {
inject: ['name'],
mounted () {
console.log(this.name); // Aresn
}
}
provide / inject替代 Vuex
Vuex 做状态管理,它是一个专为 Vue.js 开发的状态管理模式,用于集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
使用 Vuex,最主要的目的是跨组件通信、全局数据维护、多人协同开发。需求比如有:用户的登录信息维护、通知信息维护等全局的状态和数据。
一般在 webpack 中使用 Vue.js,都会有一个入口文件 main.js,里面通常导入了 Vue、VueRouter、iView 等库,通常也会导入一个入口组件 app.vue 作为根组件。一个简单的 app.vue 可能只有以下代码:
使用 provide / inject 替代 Vuex,就是在这个 app.vue 文件上做文章。把app.vue 理解为一个最外层的根组件,用来存储所有需要的全局数据和状态,甚至是计算属性(computed)、方法(methods)等。因为你的项目中所有的组件(包含路由),它的父组件(或根组件)都是 app.vue,所以我们把整个 app.vue 实例通过 provide 对外提供。
把整个 app.vue 的实例 this 对外提供,命名为 app(这个名字可以自定义,推荐使用 app,使用这个名字后,子组件不能再使用它作为局部属性)。接下来,任何组件(或路由)只要通过 inject 注入 app.vue 的 app 的话,都可以直接通过 this.app.xxx 来访问 app.vue 的 data、computed、methods 等内容。
app.vue 是整个项目第一个被渲染的组件,而且只会渲染一次(即使切换路由,app.vue 也不会被再次渲染),利用这个特性,很适合做一次性全局的状态数据管理。
//app.vue
其它任何界面或组件通过inject注入app 后就可以访问 userInfo 的数据了:
{{ app.userInfo }}
在其他页面导致userInfo更新,需要重新获取时:
{{ app.userInfo }}
mixins
例如上面的例子中将用户信息放到混合中:
//user.js
export default {
data () {
return {
userInfo: null
}
},
methods: {
getUserInfo () {
// 这里通过 ajax 获取用户信息后,赋值给 this.userInfo,以下为伪代码
$.ajax('/user/info', (data) => {
this.userInfo = data;
});
}
},
mounted () {
this.getUserInfo();
}
}
在app.vue中混合:
app.vue可以直接使用user.js的数据和方法,这样比较容易维护
只要一个组件使用了 provide 向下提供数据,那其下所有的子组件都可以通过 inject 来注入,不管中间隔了多少代,而且可以注入多个来自不同父级提供的数据。需要注意的是,一旦注入了某个数据,比如上面示例中的 app,那这个组件中就不能再声明 app 这个数据了,因为它已经被父级占有。
$emit 会在当前组件实例上触发自定义事件,并传递一些参数给监听器的回调,一般来说,都是在父级调用这个组件时,使用 @on 的方式来监听自定义事件的,比如在子组件中触发事件:
// child.vue,部分代码省略
export default {
methods: {
handleEmitEvent () {
this.$emit('test', 'Hello Vue.js');
}
}
}
在父组件中监听由 child.vue 触发的自定义事件 test:
这里看似是在父组件 parent.vue 中绑定的自定义事件 test 的处理句柄,然而事件 test 并不是在父组件上触发的,而是在子组件 child.vue 里触发的,只是通过 v-on 在父组件中监听。既然是子组件自己触发的,那它自己也可以监听到,这就要使用 $on 来监听实例上的事件,换言之,组件使用 $emit 在自己实例上触发事件,并用 $on 监听它。
**$on 监听了自己触发的自定义事件 test,**因为有时不确定何时会触发事件,一般会在 mounted 或 created 钩子中来监听。
在写vue.js时不论是用 CDN 的方式还是在 Webpack 里用 npm 引入的 Vue.js,都会有一个根节点,并且创建一个根实例:
Webpack一般在入口文件 main.js 里,最后会创建一个实例:
import Vue from 'vue';
import App from './app.vue';
new Vue({
el: '#app',
render: h => h(App)
});
因为用 Webpack 基本都是前端路由的,它的 html 里一般都只有一个根节点
。组件只需注册即可使用,有一下几个特点:
常规的组件无法解决以下问题:
对于这两种场景可以使用Vue.extend 和 vm.$mount语法解决。
创建一个 Vue 实例时,都会有一个选项 el,来指定实例的根节点,如果不写 el 选项,那组件就处于未挂载状态。Vue.extend 的作用,就是基于 Vue 构造器,创建一个“子类”,它的参数跟 new Vue 的基本一样,但 data 要跟组件一样,是个函数,再配合 $mount ,就可以让组件渲染,并且挂载到任意指定的节点上,比如 body。
import Vue from 'vue';
const AlertComponent = Vue.extend({
template: '{{ message }}',
data () {
return {
message: 'Hello, Aresn'
};
},
});
这一步创建了一个构造器,这个过程就可以解决异步获取 template 模板的问题,下面要手动渲染组件,并把它挂载到 body 下:
const component = new AlertComponent().$mount();
这一步调用了 $mount 方法对组件进行了手动渲染,但它仅仅是被渲染好了,并没有挂载到节点上,也就显示不了组件。此时的 component 已经是一个标准的 Vue 组件实例,因此它的 $el 属性也可以被访问:
document.body.appendChild(component.$el);
除了 body还可以挂载到其它节点上。
$mount 也有一些快捷的挂载方式,以下两种都是可以的:
// 在 $mount 里写参数来指定挂载的节点new AlertComponent().$mount('#app');
// 不用 $mount,直接在创建实例时指定 el 选项new AlertComponent({ el: '#app' });
实现同样的效果,除了用 extend 外,也可以直接创建 Vue 实例,并且用一个 Render 函数来渲染一个 .vue 文件:
import Vue from 'vue';
import Notification from './notification.vue';
const props = {}; // 这里可以传入一些组件的 props 选项
const Instance = new Vue({
render (h) {
return h(Notification, {
props: props
});
}
});
const component = Instance.$mount();
document.body.appendChild(component.$el);
渲染后,如果想操作 Render 的 Notification 实例:
const notification = Instance.$children[0];
因为 Instance 下只 Render 了 Notification 一个子组件,所以可以用 $children[0] 访问到。
需要注意的是,采用 $mount 手动渲染的组件,如果要销毁,也要用 $destroy 来手动销毁实例,必要时,也可以用 removeChild 把节点从 DOM 中移除。
动态渲染的核心技术就是extend 和 $mount。
一个常规的 .vue 文件一般都会包含 3 个部分:
- :组件的模板;
-
父级传递 code 后,将其分割,并保存在 data 的 html、js、css 中,后续使用。
使用正则,基于 <> 和 > 的特性进行分割:
// display.vue,部分代码省略
export default {
methods: {
getSource (source, type) {
const regex = new RegExp(`<${type}[^>]*>`);
let openingTag = source.match(regex);
if (!openingTag) return '';
else openingTag = openingTag[0];
return source.slice(source.indexOf(openingTag) + openingTag.length, source.lastIndexOf(`${type}>`));
},
splitCode () {
const script = this.getSource(this.code, 'script').replace(/export default/, 'return ');
const style = this.getSource(this.code, 'style');
const template = '' + this.getSource(this.code, 'template') + '';
this.js = script;
this.css = style;
this.html = template;
},
}
}
getSource 方法接收两个参数:
分割后,返回的内容不再包含 等标签,直接是对应的内容,在 splitCode 方法中,把分割好的代码分别赋值给 data 中声明的 html、js、css。有两个细节需要注意:
部分一般都是以 export default 开始的,可以看到在 splitCode 方法中将它替换为了 return,这个在后文会做解释,当前只要注意,我们分割完的代码,仍然是字符串; 外层套了一个,这是为了容错,有时使用者传递的 code 可能会忘记在外层包一个节点,没有根节点的组件,是会报错的。准备好这些基础工作后,就可以用 extend 渲染组件了,由于当前的 this.js 是字符串,而 extend 接收的选项可不是字符串,而是一个对象类型,那就要先把 this.js 转为一个对象。 new Function语法可以做到,rg1, arg2, … argN 是被函数使用的参数名称,functionBody 是一个含有包括函数定义的 JavaScript 语句的字符串:
new Function ([arg1[, arg2[, ...argN]],] functionBody)
例如:
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6)); // 8
用 extend 渲染组件:
extend 构造的实例通过 $mount 渲染后,挂载到了组件唯一的一个节点上。 加载 css 可以创建一个 标签,然后把 css 写进去,再插入到页面的 中,这样 css 就被浏览器解析了。为了便于后面在 this.code 变化或组件销毁时移除动态创建的 标签,给每个 style 标签加一个随机 id 用于标识。在 src/utils 目录下新建 random_str.js 文件,并写入以下内容:
// 从指定的 a-zA-Z0-9 中随机生成 32 位的字符串
export default function (len = 32) {
const $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
const maxPos = $chars.length;
let str = '';
for (let i = 0; i < len; i++) {
str += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return str;
}
补全 renderCode 方法:
// display.vue,部分代码省略import randomStr from '../../utils/random_str.js';
export default {
data () {
return {
id: randomStr()
}
},
methods: {
renderCode () {
if (this.html !== '' && this.js !== '') {
// ...
if (this.css !== '') {
const style = document.createElement('style');
style.type = 'text/css';
style.id = this.id;
style.innerHTML = this.css;
document.getElementsByTagName('head')[0].appendChild(style);
}
}
}
}
}
当 Display 组件销毁时,也要手动销毁 extend 创建的实例以及上面的 css:
// display.vue,部分代码省略
export default {
methods: {
destroyCode () {
const $target = document.getElementById(this.id);
if ($target) $target.parentNode.removeChild($target);
if (this.component) {
this.$refs.display.removeChild(this.component.$el);
this.component.$destroy();
this.component = null;
}
}
},
beforeDestroy () {
this.destroyCode();
}
}
当 this.code 更新时,整个过程要重新来一次,所以要对 code 进行 watch 监听:
// display.vue,部分代码省略
export default {
watch: {
code () {
this.destroyCode();
this.renderCode();
}
}
}
新建一条路由,并在 src/views 下新建页面 display.vue 来使用 Display 组件:
动态渲染 .vue 文件的组件—— Display
// src/views/default-code.js
const code =`{{ message }}`;
export default code;