button组件
亮点:prop接收参数
<template>
<button :class="'i-button-size' + size" :disabled="disabled">
</button>
</template>
<script>
// 判断参数是否是其中之⼀
function oneOf (value, validList) {
for (let i = 0; i < validList.length; i++) {
if (value === validList[i]) { return true; }
}
return false;
}
export default {
props: {
size: {
validator (value)
{ return oneOf(value, ['small', 'large', 'default']); },
default: 'default' },
disabled: {
type: Boolean, default: false
} } }
</script>
使⽤组件:
<i-button size="large">
</i-button>
<i-button disabled></i-button>
mixins
如果你的项⽬⾜够复杂,或需要多⼈协同开发时,在 app.vue ⾥会 写⾮常多的代码,多到结构复杂难以维护。这时可以使⽤ Vue.js 的 混合 mixins,将不同的逻辑分开到不同的 js ⽂件⾥。
user.js
export default {
data () { return { userInfo: null } },
methods: {
getUserInfo(){
$.ajax('/user/info', (data) => { this.userInfo = data; }); } },
mounted () {
this.getUserInfo();
} }
然后在 app.vue 中混合: app.vue:
<script>
import mixins_user from '../mixins/user.js'
export default {
mixins: [mixins_user],
data() {
return {}
},
}
</script>
跟⽤户信息相关的逻辑,都可以在 user.js ⾥维护
$on 与 $emit
$emit 会在当前组件实例上触发⾃定义事件,并传递⼀些参数给监 听器的回调,⼀般来说,都是在⽗级调⽤这个组件时,使⽤ @on 的 ⽅式来监听⾃定义事件的,⽐如在⼦组件中触发事件:$on 监听了⾃⼰触发的⾃定义事件 test,因为有时不确定何时会触 发事件,⼀般会在 mounted 或 created 钩⼦中来监听。
子组件
methods: {
handleEmitEvent() {
this.$emit('test', 'Hello Vue.js')
},
},
父组件
mounted() {
this.$on('test', (text) => {
window.alert(text)
})
},
⾃⾏实现 dispatch 和 broadcast ⽅法
*思路:
在⼦组件调⽤ dispatch ⽅法,向上级指定的组件实例(最近 的)上触发⾃定义事件,并传递数据,且该上级组件已预先通 过 $on 监听了这个事件;
相反,在⽗组件调⽤ broadcast ⽅法,向下级指定的组件实例 (最近的)上触发⾃定义事件,并传递数据,且该下级组件已 预先通过 $on 监听了这个事件。*
该⽅法可能在很多组件中都会使⽤,复⽤起⻅,我们封装在混合(mixins)⾥。那它的使⽤样例可能是这样的:
有 A.vue 和 B.vue 两个组件,其中 B 是 A 的⼦组件,中间可能跨多级,在 A 中向 B 通信:
<!-- A.vue -->
<template><button @click="handleClick">触发事件</button></template>
<script>
import Emitter from '../mixins/emitter.js'
export default {
name: 'componentA',
mixins: [Emitter],
methods: {
handleClick() {
this.broadcast('componentB', 'on- message', 'Hello Vue.js')
},
},
}
</script>
// B.vue
export default {
name: 'componentB',
created () {
this.$on('on-message', this.showMessage);
},
methods: {
showMessage (text) { window.alert(text);
} } }
在独⽴组件 (库)⾥,每个组件的 name 值应当是唯⼀的,name 主要⽤于递归 组件
emitter.js
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
const name = child.$options.name;
if (name ===componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
} })
}
export default {
methods: {
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.name;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent; if (parent) {
name = parent.$options.name;
} }
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
} },
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
} };
同理,如果是 B 向 A 通信,在 B 中调⽤ dispatch ⽅法,在 A 中使 ⽤ $on 监听事件即可。
因为是⽤作 mixins 导⼊,所以在 methods ⾥定义的 dispatch 和 broadcast ⽅法会被混合到组件⾥,⾃然就可以⽤ this.dispatch 和 this.broadcast 来使⽤。
这两个⽅法都接收了三个参数,第⼀个是组件的 name 值,⽤于向上 或向下递归遍历来寻找对应的组件,第⼆个和第三个就是上⽂分析的 ⾃定义事件名称和要传递的数据。 可以看到,在 dispatch ⾥,通过 while 语句,不断向上遍历更新当 前组件(即上下⽂为当前调⽤该⽅法的组件)的⽗组件实例(变量 parent 即为⽗组件实例),直到匹配到定义的 componentName 与 某个上级组件的 name 选项⼀致时,结束循环,并在找到的组件实例 上,调⽤ $emit ⽅法来触发⾃定义事件 eventName。broadcast ⽅法与之类似,只不过是向下遍历寻找。
*相⽐ Vue.js 1.x,有以下不同:
需要额外传⼊组件的 name 作为第⼀个参数;
⽆冒泡机制;
第三个参数传递的数据,只能是⼀个(较多时可以传⼊⼀个对 象),⽽ Vue.js 1.x 可以传⼊多个参数,当然,你对 emitter.js 稍作修改,也能⽀持传⼊多个参数,只是⼀般场景 传⼊⼀个对象⾜以*
组件的通信找到任意组件实例 ——findComponents 系列⽅法
是组件通信的终极⽅案。通过递归、遍历,找到指定组件的 name 选项 匹配的组件实例并返回。 findComponents 系列⽅法最终都是返回组件的实例,进⽽可以读 取或调⽤该组件的数据和⽅法
*它适⽤于以下场景:
由⼀个组件,向上找到最近的指定组件;
由⼀个组件,向上找到所有的指定组件;
由⼀个组件,向下找到最近的指定组件;
由⼀个组件,向下找到所有指定的组件;
由⼀个组件,找到指定组件的兄弟组件。*
utils/assits.js
// 由⼀个组件,向上找到最近的指定组件
//context 上下文
//componentName 组件名称
function findComponentUpward (context, componentName) {
let parent = context.$parent;
let name = parent.$options.name;
while (parent && (!name || [componentName].indexOf(name) < 0)) {
parent = parent.$parent;
if (parent) name = parent.$options.name;
}
return parent;
}
// 由⼀个组件,向上找到所有的指定组件
function findComponentsUpward(context, componentName) {
let parents = []
const parent = context.$parent
if (parent) {
if (parent.$options.name === componentName) parents.push(parent)
return parents.concat(findComponentsUpward(parent, componentName))
} else {
return []
}
},
// 由⼀个组件,向下找到最近的指定组件
function findComponentDownward(context, componentName) {
//context.$children 得到的是当前组件的全部⼦组件
const childrens = context.$children
let children = null
if (childrens.length) {
for (const child of childrens) {
const name = child.$options.name
if (name === componentName) {
children = child
break
} else {
children = findComponentDownward(child, componentName)
if (children) break
}
}
}
return children
},
// 由⼀个组件,向下找到所有指定的组件
//使⽤ reduce 做累加器,为数组中的每一个元素依次执行回调函数,并 ⽤递归将找到的组件合并为⼀个数组并返回,
function findComponentsDownward(context, componentName) {
return context.$children.reduce((components, child) => {
if (child.$options.name === componentName) components.push(child)
const foundChilds = findComponentsDownward(child, componentName)
return components.concat(foundChilds)
}, [])
},
// 由⼀个组件,找到指定组件的兄弟组件
function findBrothersComponents(context, componentName, exceptMe = true) {
//⽗组件的 全部⼦组件,这⾥⾯当前包含了本身
//exceptMe true是不包括自己
let res = context.$parent.$children.filter((item) => {
return item.$options.name === componentName
})
// Vue.js 在渲染组件时,都会给每个组件加⼀个内置的属 性 _uid,这个 _uid 是不会重复的,借此我们可以从⼀系列兄弟组 件中把⾃⼰排除掉
let index = res.findIndex((item) => item._uid === context._uid)
if (exceptMe) res.splice(index, 1)
return res
},
export { findComponentUpward,findComponentsUpward,findComponentDownward,findComponentsDownward,findBrothersComponents };
使用(A 是 B 的⽗组件)
<!-- component-a.vue -->
<template>
<div>组件 A <component-b></component-b></div>
</template>
<script>
import componentB from './component-b.vue'
import { findComponentDownward } from '../utils/assist.js';
export default {
name: 'componentA',
components: { componentB },
data() {
return { name: 'Aresn' }
},
methods: {
sayHello() {
console.log('Hello, Vue.js')
},
},
mounted(){
const comB = findComponentDownward(this, 'componentB');
if (comB) { console.log(comB.name);
}
}
</script>
<!-- component-b.vue -->
<template>
<div>组件 B</div>
</template>
<script>
import { findComponentUpward,findBrothersComponents } from '../utils/assist.js'
export default {
name: 'componentB',
mounted() {
const comA = findComponentUpward(this, 'componentA')
if (comA) {
console.log(comA.name) // Aresn
comA.sayHello() // Hello, Vue.js
}
const comsB = findBrothersComponents(this, 'componentB');
console.log(comsB); // ① [],空数组
//如果在 A 中再写⼀个 B:这时就会打印出 [VueComponent],有⼀个组件了
},
}
</script>
组合多选框组件—— CheckboxGroup & Checkbox
checkbox.vue
updateModel⽅法在 Checkbox ⾥的 mounted 初始化时调⽤。这 个⽅法的作⽤就是在 CheckboxGroup ⾥通过 findComponentsDownward ⽅法找到所有的 Checkbox,然后把 CheckboxGroup 的 value,赋值给 Checkbox 的 model,并根 据 Checkbox 的 label,设置⼀次当前 Checkbox 的选中状态。 这样⽆论是由内⽽外选择,或由外向内修改数据,都是双向绑定的, ⽽且⽀持动态增加 Checkbox 的数量。
<!-- checkbox.vue -->
<template>
<label>
<span>
<input
v-if="group"
type="checkbox"
:disabled="disabled"
:value="label"
v-model="model"
@change="change"
/>
<input
v-else
type="checkbox"
:disabled="disabled"
:checked="currentValue"
@change="change"
/>
</span>
<slot></slot>
</label>
</template>
<script>
import { findComponentUpward } from '../../utils/assist.js'
export default {
name: 'iCheckbox',
props: {
disabled: { type: Boolean, default: false },
value: { type: [String, Number, Boolean], default: false },
trueValue: { type: [String, Number, Boolean], default: true },
falseValue: { type: [String, Number, Boolean], default: false },
label: { type: [String, Number, Boolean] },
},
data() {
return {
currentValue: this.value,
model: [],
group: false,
parent: null,
}
},
methods: {
change(event) {
if (this.disabled) {
return false
}
const checked = event.target.checked
this.currentValue = checked
const value = checked ? this.trueValue : this.falseValue
this.$emit('input', value)
if (this.group) {
this.parent.change(this.model)
} else {
this.$emit('on-change', value)
this.dispatch('iFormItem', 'on-form-change', value)
}
},
updateModel() {
this.currentValue = this.value === this.trueValue
},
},
watch: {
value(val) {
if (val === this.trueValue || val === this.falseValue) {
this.updateModel()
} else {
throw 'Value should be trueValue or falseValue.'
}
},
},
mounted() {
this.parent = findComponentUpward(this, 'iCheckboxGroup')
if (this.parent) {
this.group = true
}
if (this.group) {
this.parent.updateModel(true)
} else {
this.updateModel()
}
},
}
</script>
checkbox-group.vue
<!-- checkbox-group.vue -->
<template>
<div>
<slot></slot>
</div>
</template>
<script>
import { findComponentsDownward } from '../../utils/assist.js'
import Emitter from '../../mixins/emitter.js'
export default {
name: 'iCheckboxGroup',
mixins: [Emitter],
props: {
value: {
type: Array,
default() {
return []
},
},
},
data() {
return { currentValue: this.value, childrens: [] }
},
methods: {
updateModel(update) {
this.childrens = findComponentsDownward(this, 'iCheckbox')
if (this.childrens) {
const { value } = this
this.childrens.forEach((child) => {
child.model = value
if (update) {
child.currentValue = value.indexOf(child.label) >= 0
child.group = true
}
})
}
},
change(data) {
this.currentValue = data
this.$emit('input', data)
this.$emit('on-change', data)
this.dispatch('iFormItem', 'on-form-change', data)
},
},
mounted() {
this.updateModel(true)
},
watch: {
value() {
this.updateModel(true)
},
},
}
</script>
Vue的构造器extend 与⼿ 动挂载$mount
Vue 的$mount()为手动挂载,在项目中可用于延时挂载(例如在挂载之前要进行一些其他操作、判断等),之后要手动挂载上。new Vue时,el和$mount并没有本质上的不同。
创建⼀个 Vue 实例时,都会有⼀个选项 el,来指定 实例的根节点,如果不写 el 选项,那组件就处于未挂载状 态。Vue.extend 的作⽤,就是基于 Vue 构造器,创建⼀个“⼦ 类”,它的参数跟 new Vue 的基本⼀样,但 data 要跟组件⼀样, 是个函数,再配合 $mount ,就可以让组件渲染,并且挂载到任意 指定的节点上,⽐如 body。
import Vue from 'vue'
//创建了⼀个构造器,这个过程就可以解决异步获取 template 模板的问题
const AlertComponent = Vue.extend({
template: '<div>{{ message }}</div>',
data() {
return { message: 'Hello, Aresn' }
},
})
⼿动渲染组件,并把它挂载到 body 下:
const component = new AlertComponent().$mount();
//$mount ⽅法对组件进⾏了⼿动渲染,但它仅 仅是被渲染好了,并没有挂载到节点上,也就显示不了组件。此时的 component 已经是⼀个标准的 Vue 组件实例,因此它的 $el 属性 也可以被访问:
document.body.appendChild(component.$el);
$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 中移除。
动态渲染 .vue ⽂件的组件—— Display
⼀个常规的 .vue ⽂件⼀般都会包含 3个部分:
<template>:组件的模板
<script>:组件的选项,不包含 el;
<style>:CSS 样式。
思路
1.⽗级传递 code 后,将其分割,并保存在 data 的 html、js、css
2.使⽤正则,基于 <> 和 </> 的特性进⾏分割:
utils/random_str.js
// ⽣成随机字符串
// 是从指定的 a-zA-Z0-9 中随机⽣成 32 位的字 符串。
export default function (len = 32) {
const $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV WXYZ1234567890';
const maxPos = $chars.length; let str = '';
for (let i = 0; i < len; i++) {
// Math.floor(Math.random() * maxPos) 0到32的整数
// charAt(int index) 方法用于返回指定索引处的字符
str += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return str;
},
组件display.vue
<!-- display.vue -->
<template>
<div ref="display"></div>
</template>
<script>
import Vue from 'vue'
import randomStr from '../../utils/random_str.js'
export default {
props: { code: { type: String, default: '' } },
data() {
return {
html: '',
js: '',
css: '',
component: null,
id: randomStr()
}
},
// 当 this.code 更新时,整个过程要重新来⼀次,所以要对 code 进 ⾏ watch 监听:
watch: {
code() {
this.destroyCode()
this.renderCode()
},
},
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}>`)
)
},
// getSource ⽅法接收两个参数: source:.vue ⽂件代码,即 props: code;
// type:分割的部分,也就是 template、script、style。
// 分割后,返回的内容不再包含 <template> 等标签,直接是对应的 内容,在 splitCode ⽅法中,把分割好的代码分别赋值给 data 中声 明的 html、js、css。
// 有两个细节需要注意: 1. .vue 的 <script> 部分⼀般都是以 export default 开始 的,可以看到在 splitCode ⽅法中将它替换为了 return,这 个在后⽂会做解释,当前只要注意,我们分割完的代码,仍然 是字符串; 2. 在分割的 <template> 外层套了⼀个 <div id="app">,这 是为了容错,有时使⽤者传递的 code 可能会忘记在外层包⼀ 个节点,没有根节点的组件,是会报错的。
splitCode() {
const script = this.getSource(this.code, 'script').replace(
/export default/,
'return '
)
const style = this.getSource(this.code, 'style')
const template =
'<div id="app">' + this.getSource(this.code, 'template') + '</div>'
this.js = script
this.css = style
this.html = template
},
renderCode() {
this.splitCode()
if (this.html !== '' && this.js !== '') {
// new Function ([arg1[, arg2[, ...argN]],] functionBody)
// arg1, arg2, ... argN 是被函数使⽤的参数名称,functionBody 是 ⼀个含有包括函数定义的 JavaScript 语句的字符串。也就是说,示 例中的字符串 return a + b 被当做语句执⾏了。
// 当前的 this.js 是字符串, ⽽ extend 接收的选项可不是字符串,⽽是⼀个对象类型,那就要先 把 this.js 转为⼀个对象
// const sum = new Function('a', 'b', 'return a + b'); console.log(sum(2, 6)); // 8
// new Function eval 函数也可以使⽤
// this.js 中是将 export default 替换为 return 的,如 果将 this.js 传⼊ new Function ⾥,那么 this.js 就执⾏了,这时 因为有 return,返回的就是⼀个对象类型的 this.js 了
const parseStrToFunc = new Function(this.js)()
parseStrToFunc.template = this.html
const Component = Vue.extend(parseStrToFunc)
this.component = new Component().$mount()
// extend 构造的实例通过 $mount 渲染后,挂载到了组件唯⼀的⼀ 个节点 <div ref="display"> 上。
this.$refs.display.appendChild(this.component.$el)
if (this.css !== '') {
const style = document.createElement('style')
style.type = 'text/css'
// 创建⼀个 <style> 标签,然后把 css 写进去,再插⼊到⻚ ⾯的 <head> 中,这样 css 就被浏览器解析了。为了便于后⾯在 this.code 变化或组件销毁时移除动态创建的 <style> 标签,我 们给每个 style 标签加⼀个随机 id ⽤于标识。
style.id = this.id
style.innerHTML = this.css
document.getElementsByTagName('head')[0].appendChild(style)
}
}
},
// 当 Display 组件销毁时,也要⼿动销毁 extend 创建的实例以及上 ⾯的 css:
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
}
},
},
mounted() {
this.renderCode()
},
beforeDestroy() {
this.destroyCode()
},
}
</script>
使⽤
新建⼀条路由,并在 src/views 下新建⻚⾯ display.vue 来使 ⽤ Display 组件
src/views/display.vue
<!-- src/views/display.vue -->
<template>
<div>
<h3>动态渲染 .vue ⽂件的组件—— Display</h3>
<i-display :code="code"></i-display>
</div>
</template>
<script>
import iDisplay from '../components/display/display.vue'
import defaultCode from './default-code.js'
export default {
components: { iDisplay },
data() {
return { code: defaultCode }
},
}
</script>
// src/views/default-code.js
const code = `<template>
<div>
<input v-model="message"> {{ message }}
</div>
</template>
<script> export default { data () { return { message: '' } } }
</script>`;
export default code;
如果使⽤的是 Vue CLI 3 默认的配置,直接运⾏时,会抛出下⾯的 错误:
*这涉及到另⼀个知识点,就是 Vue.js 的版本。
在使⽤ Vue.js 2 时,有独⽴构建(standalone)和运⾏时构建(runtime-only)
两 种版本可供选择,
Vue CLI 3 默认使⽤了 vue.runtime.js,
它不允许编译 template 模板,
因为我们在 Vue.extend 构造实例时,
⽤了 template 选 项,所以会报错。
解决⽅案有两种,⼀是⼿动将 template 改写为 Render 函数,但这成本太⾼;
另⼀种是对 Vue CLI 3 创建的⼯程做 简单的配置。我们使⽤后者。*
在项⽬根⽬录,新建⽂件 vue.config.js:
module.exports = { runtimeCompiler: true };
*它的作⽤是,是否使⽤包含运⾏时编译器的 Vue 构建版本。设置为 true 后就可以在 Vue 组件中使⽤ template 选项了,
这个⼩⼩的 Display 组件,能做的事还有很多,⽐如要写⼀套 Vue 组件库的⽂档,传统⽅法是在开发环境写⼀个个的 .vue ⽂件,然后 编译打包、上传资源、上线,如果要修改,哪怕⼀个标点符号,都要 重新编译打包、上传资源、上线。有了 Display 组件,只需要提供 ⼀个服务来在线修改⽂档的 .vue,就能实时更新,不⽤打包、上 传、上线*,
全局提示组件—— $Alert
this.$Alert 可以在任何位置调⽤,⽆需单独引⼊。
该⽅法接收两 个参数:
content:提示内容;
duration:持续时间,单位秒,默认 1.5 秒,到时间⾃动消 失
Alert 组件不同于常规的组件使⽤⽅式,它最终是通过 JS 来调⽤ 的,因此组件不⽤预留 props 和 events 接⼝
在 src/component 下新建 alert ⽬录,并创建⽂件 alert.vue
<template>
<div class="alert">
<div class="alert-main" v-for="item in notices" :key="item.name">
<div class="alert-content">{{ item.content }}</div>
</div>
</div>
</template>
<script>
let seed = 0
function getUuid() {
return 'alert_' + seed++
}
// JS 调⽤ Alert 的⼀个⽅法 add,并 将 content 和 duration 传⼊进来:
export default {
data() {
// 通知可以是多个,我们⽤⼀个数组 notices 来管理每条通知
return { notices: [] }
},
// 在 add ⽅法中,给每⼀条传进来的提示数据,加了⼀个不重复的 name 字段来标识,并通过 setTimeout 创建了⼀个计时器,当到 达指定的 duration 持续时间后,调⽤ remove ⽅法,将对应 name 的那条提示信息找到,并从数组中移除。 由这个思路,Alert 组件就可以⽆限扩展,只要在 add ⽅法中传递 更多的参数,就能⽀持更复杂的组件,⽐如是否显示⼿动关闭按钮、 确定 / 取消按钮,甚⾄传⼊⼀个 Render 函数都可以,完成本例 后,
methods: {
add(notice) {
const name = getUuid()
let _notice = Object.assign({ name: name }, notice)
this.notices.push(_notice)
// 定时移除,单位:秒
const duration = notice.duration
setTimeout(() => {
this.remove(name)
}, duration * 1000)
},
remove(name) {
const notices = this.notices
for (let i = 0; i < notices.length; i++) {
if (notices[i].name === name) {
this.notices.splice(i, 1)
break
}
}
},
},
}
</script>
<style>
.alert {
position: fixed;
width: 100%;
top: 16px;
left: 0;
text-align: center;
pointer-events: none;
}
.alert-content {
display: inline-block;
padding: 8px 16px;
background: #fff;
border-radius: 3px;
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.2);
margin-bottom: 8px;
}
</style>
对 Alert 组件进⼀步封装,让它能够实例化,⽽不是常 规的组件使⽤⽅法
使⽤ Vue.extend 或 new Vue,然后⽤ $mount 挂载到 body 节点下。
notification.js 并不是最终的⽂件,它只是对 alert.vue 添加了⼀个 ⽅法 newInstance。虽然 alert.vue 包含了 template、script、 style 三个标签,并不是⼀个 JS 对象,那怎么能够给它扩展⼀个⽅法 newInstance 呢?事实上,alert.vue 会被 Webpack 的 vue- loader 编译,把 template 编译为 Render 函数,最终就会成为⼀ 个 JS 对象,⾃然可以对它进⾏扩展。
Alert 组件没有任何 props,这⾥在 Render Alert 组件时,还是给 它加了 props,当然,这⾥的 props 是空对象 {},⽽且即使传了内 容,也不起作⽤。这样做的⽬的还是为了扩展性,如果要在 Alert 上 添加 props 来⽀持更多特性,是要在这⾥传⼊的。不过话说回来, 因为能拿到 Alert 实例,⽤ data 或 props 都是可以的。
notification.js
import Alert from './alert.vue'
import Vue from 'vue'
Alert.newInstance = (properties) => {
const props = properties || {}
const Instance = new Vue({
data: props,
render(h) {
return h(Alert, { props: props })
},
})
const component = Instance.$mount()
document.body.appendChild(component.$el)
const alert = Instance.$children[0]
return {
add(noticeProps) {
alert.add(noticeProps)
},
remove(name) {
alert.remove(name)
},
}
}
export default Alert
⼊⼝
最后要做的,就是调⽤ notification.js 创建实例,并通过 add 把数 据传递过去,这是组件开发的最后⼀步,也是最终的⼊⼝。
在 src/component/alert 下创建⽂件
alert.js:
// alert.js
import Notification from './notification.js'
let messageInstance
// getMessageInstance 函数⽤来获取实例,它不会重复创建,如 果 messageInstance 已经存在,就直接返回了,只在第⼀次调⽤ Notification 的 newInstance 时来创建实例。
function getMessageInstance() {
messageInstance = messageInstance || Notification.newInstance()
return messageInstance
}
function notice({ duration = 1.5, content = '' }) {
let instance = getMessageInstance()
instance.add({ content: content, duration: duration })
}
export default {
info(options) {
return notice(options)
},
}
// alert.js 对外提供了⼀个⽅法 info,如果需要各种显示效果,⽐如 成功的、失败的、警告的,可以在 info 下⾯提供更多的⽅法,⽐如 success、fail、warning 等,并传递不同参数让 Alert.vue 知道显 示哪种状态的图标。本例因为只有⼀个 info,事实上也可以省略 掉,直接导出⼀个默认的函数,这样在调⽤时,就不⽤ this.$Alert.info() 了,直接 this.$Alert()。
把 alert.js 作为插件注册到 Vue ⾥就⾏,在⼊⼝⽂件 src/main.js中,通过 prototype 给 Vue 添加⼀个实例⽅法:
src/main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import Alert from '../src/components/alert/alert.js'
Vue.config.productionTip = false
Vue.prototype.$Alert = Alert
new Vue({ router, render: h => h(App) }).$mount('#app')
这样在项⽬任何地⽅,都可以通过 this.$Alert 来调⽤ Alert 组 件了
src/views/alert.vue
<template>
<div>
<button @click="handleOpen1">打开提示 1</button>
<button @click="handleOpen2">打开提示 2</button>
</div>
</template>
<script>
export default {
methods: {
handleOpen1() {
this.$Alert.info({ content: '我是提示信息 1' })
},
handleOpen2() {
this.$Alert.info({ content: '我是提示信息 2', duration: 3 })
},
},
}
</script>
是同类组件中值得注意的:
1. Alert.vue 的最外层是有⼀个 .alert 节点的,它会在第⼀次调 ⽤ $Alert 时,在 body 下创建,因为不在 <router-view> 内,
它不受路由的影响,也就是说⼀经创建,除⾮刷新⻚⾯,
这个节点是不会消失的,所以在 alert.vue 的设计中,并没有 主动销毁这个组件,
⽽是维护了⼀个⼦节点数组 notices。
2. .alert 节点是 position: fixed 固定的,因此要合理设计它 的 z-index,否则可能被其它节点遮挡。
3. notification.js 和 alert.vue 是可以复⽤的,如果还要开发其 它同类的组件,⽐如⼆次确认组件 $Confirm,
只需要再写⼀ 个⼊⼝ confirm.js,并将 alert.vue 进⼀步封装,将 notices 数组的循环体写为⼀个新的组件,
通过配置来决定是 渲染 Alert 还是 Confirm,这在可维护性上是友好的。
4. 在 notification.js 的 new Vue 时,使⽤了 Render 函数来渲 染 alert.vue,
这是因为使⽤ template 在 runtime 的 Vue.js 版本下是会报错的。
5. 本例的 content 只能是字符串,如果要显示⾃定义的内容,除 了⽤ v-html 指令,也能⽤ Functional Render。
结语Vue.js 的精髓是组件,组件的精髓是 JavaScript。将 JavaScript 开 发中的技巧结合 Vue.js 组件,就能玩出不⼀样的东⻄。
更灵活的组件:Render 函数与 Functional
Render Render 函数 返回的是⼀个 JS
对象,没有传统 DOM 的层级关系,配合上 if、 else、for 等语句,将节点拆分成不同JS对象再组装。
template 和 Render 写法的对照:
<template>
<div id="main" class="container" style="color: red">
<p v-if="show">内容 1</p>
<p v-else>内容 2</p>
</div>
</template>
<script>
export default {
data() {
return { show: false }
},
}
</script>
Render
export default {
data() {
return { show: false }
},
render: (h) => {
let childNode
if (this.show) {
childNode = h('p', '内容 1')
} else {
childNode = h('p', '内容 2')
}
return h(
'div',
{
attrs: { id: 'main' },
class: { container: true },
style: { color: 'red' },
},
[childNode]
)
},
}
这⾥的 h,即 createElement,是 Render 函数的核⼼。
可以看 到,template 中的 v-if / v-else 等指令,都被 JS 的 if / else 替 代了,
那 v-for ⾃然也会被 for 语句替代。
h 有 3 个参数,分别是:
- 要渲染的元素或组件,可以是⼀个 html 标签、组件选项或⼀ 个函数(不常⽤),该参数为必填项。示例:
// 1. html 标签
h('div');
// 2. 组件选项
import DatePicker from '../component/date- picker.vue'; h(DatePicker);
- 对应属性的数据对象,⽐如组件的 props、元素的 class、绑 定的事件、slot、⾃定义指令等,
该参数是可选的,上⽂所说 的 Render 配置项多,指的就是这个参数。
该参数的完整配置 和示例,可以到 Vue.js 的⽂档查看,没必要全部记住,
⽤到 时查阅就好:createElement 参数 (https://cn.vuejs.org/v2/guide... function.html#createElement-参数)。
- ⼦节点,可选,String 或 Array,它同样是⼀个 h。示例:
[
'内容', h('p', '内容'),
h(Component, { props: { someProp: 'foo' } })
]
所有的组件树中,如果 vNode 是组件或含有组件的 slot,那么 vNode 必须唯⼀
重复渲染多个组件或元素,可以通过⼀个循环和⼯⼚函数来解决:
const Child = {
render: (h) => {
return h('p', 'text')
},
}
export default {
render: (h) => {
const children = Array.apply(null, { length: 5 }).map(() => {
return h(Child)
})
return h('div', children)
},
}
对于含有组件的 slot,复⽤⽐较复杂,需要将 slot 的每个⼦节点都 克隆⼀份,例如:
{
render: (h) => {
function cloneVNode(vnode) {
//递归遍历所有⼦节点,并克隆
const clonedChildren =
vnode.children && vnode.children.map((vnode) => cloneVNode(vnode))
const cloned = h(vnode.tag, vnode.data, clonedChildren)
cloned.text = vnode.text
cloned.isComment = vnode.isComment
cloned.componentOptions = vnode.componentOptions
cloned.elm = vnode.elm
cloned.context = vnode.context
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
return cloned
}
const vNodes =
this.$slots.default === undefined ? [] : this.$slots.default
const clonedVNodes =
this.$slots.default === undefined
? []
: vNodes.map((vnode) => cloneVNode(vnode))
return h('div', [vNodes, clonedVNodes])
}
}
- 在 Render 函数⾥创建了⼀个 cloneVNode 的⼯⼚函数,通过递归 将 slot 所有⼦节点都克隆了⼀份,并对 VNode 的关键属性也进⾏ 了复制
深度克隆 slot 并⾮ Vue.js 内置⽅法,
在⼀些特殊的场景才会使⽤到,正常业务⼏乎是⽤不到的。⽐如 iView 组件库的穿梭框组件 Transfer,就⽤到了这种⽅法:
slot 是⼀个 Refresh 按钮,使⽤者只写了⼀遍,但在 Transfer 组件中,是通过克隆 VNode 的⽅法,显示了两遍。
如果 不这样做,就要声明两个具名 slot,但是左右两个的逻辑可能是完 全⼀样的,使⽤者就要写两个⼀模⼀样的 slot,这是不友好的*
Render 函数的基本⽤法还有很多,⽐如 v-model 的⽤ 法、事件和修饰符、slot 等,读者可以到 Vue.js ⽂档阅 读。Vue.js 渲染函数 (https://cn.vuejs.org/v2/guide... function.html)
Render 函数使⽤场景
⼀般情况下是不推荐直接使⽤ Render 函数的,使⽤ template ⾜以,在 Vue.js 中,使⽤ Render 函数的场景,主要有 以下 4 点
- 使⽤两个相同 slot。在 template 中,Vue.js 不允许使⽤两个 相同的 slot,⽐如下⾯的示例是错误的:
<template>
<div>
<slot></slot>
<slot></slot>
</div>
</template>
解决⽅案就是上⽂中讲到的约束,使⽤⼀个深度克隆 VNode 节点的⽅法
- 在 SSR 环境(服务端渲染),如果不是常规的 template 写 法,⽐如通过 Vue.extend 和 new Vue 构造来⽣成的组件实 例,是编译不过的,使⽤ Render 函 数来渲染
- 在 runtime 版本的 Vue.js 中,如果使⽤ Vue.extend ⼿动构 造⼀个实例,使⽤ template 选项是会报错的,解决⽅案也很简单,把 template 改写为 Render 就可以了。需要注意的是,在开发独⽴组件时,可以通过配置 Vue.js 版本来使 template 选项可⽤,但这是在⾃⼰的环境, ⽆法保证使⽤者的 Vue.js 版本,所以对于提供给他⼈⽤的组 件,是需要考虑兼容 runtime 版本和 SSR 环境的。
- 这可能是使⽤ Render 函数最重要的⼀点。⼀个 Vue.js 组 件,有⼀部分内容需要从⽗级传递来显示,如果是⽂本之类 的,直接通过 props 就可以,如果这个内容带有样式或复杂 ⼀点的 html 结构,可以使⽤ v-html 指令来渲染,⽗级传递 的仍然是⼀个 HTML Element 字符串,不过它仅仅是能解析 正常的 html 节点且有 XSS ⻛险。当需要最⼤化程度⾃定义显 示内容时,就需要 Render 函数,它可以渲染⼀个完整的 Vue.js 组件。你可能会说,⽤ slot 不就好了?的确,slot 的 作⽤就是做内容分发的,但在⼀些特殊组件中,可能 slot 也不 ⾏。⽐如⼀个表格组件 Table,它只接收两个 props:列配置 columns 和⾏数据 data,不过某⼀列的单元格,不是只将数 据显示出来那么简单,可能带有⼀些复杂的操作,这种场景只 ⽤ slot 是不⾏的,没办法确定是那⼀列的 slot。这种场景有两 种解决⽅案,其⼀就是 Render 函数,另⼀种是⽤作⽤域 slot(slot- scope)
Functional Render
*Vue.js 提供了⼀个 functional 的布尔值选项,设置为 true 可以 使组件⽆状态和⽆实例,也就是没有 data 和 this 上下⽂。这样⽤ Render 函数返回虚拟节点可以更容易渲染,因为函数化组件 (Functional Render)只是⼀个函数,渲染开销要⼩很多。
使⽤函数化组件,Render 函数提供了第⼆个参数 context 来提供临 时上下⽂。组件需要的 data、props、slots、children、parent 都 是通过这个上下⽂来传递的,⽐如 this.level 要改写为 context.props.level,this.$slots.default 改写为 context.children,您可以阅读 Vue.js ⽂档—函数式组件 (https://cn.vuejs.org/v2/guide... function.html#函数式组件) 来查看示例。
函数化组件在业务中并不是很常⽤,⽽且也有类似的⽅法来实现,⽐ 如某些场景可以⽤ is 特性来动态挂载组件。函数化组件主要适⽤于 以下两个场景*
1.程序化地在多个组件中选择⼀个;
2.在将 children、props、data 传递给⼦组件之前操作它们。
某个组件需要使⽤ Render 函数来⾃定义,⽽不是 通过传递普通⽂本或 v-html 指令,这时就可以⽤ Functional Render,来看下⾯的示例:
⾸先创建⼀个函数化组件 render.js:
// 它只定义了⼀个 props:render,格式为 Function,因为是 functional,所以在 render ⾥使⽤了第⼆个参数 ctx 来获取 props。这是⼀个中间⽂件,并且可以复⽤,其它组件需要这 个功能时,都可以引⼊它
// render.js
export default {
functional: true,
props: { render: Function },
render: (h, ctx) => {
return ctx.props.render(h)
},
}
创建组件:
<!-- my-component.vue -->
<template>
<div><Render :render="render"></Render></div>
</template>
<script>
import Render from './render.js'
export default {
components: { Render },
props: { render: Function }
}
</script>
使⽤上⾯的 my-compoennt 组件:
<template>
<div>
<my-component :render="render"></my-component>
</div>
</template>
<script>
import myComponent from '../components/my-component.vue'
export default {
components: { myComponent },
data() {
return {
render: (h) => {
return h('div', { style: { color: 'red' } }, '⾃定义内容')
},
}
},
}
</script>
这⾥的 render.js 因为只是把 demo.vue 中的 Render 内容过继, 并⽆其它⽤处,所以⽤了 Functional Render。 就此例来说,完全可以⽤ slot 取代 Functional Render,那是因为 只有 render 这⼀个 prop。如果示例中的 <Render> 是⽤ v-for ⽣成的,也就是多个时,⽤ ⼀个 slot 是实现不了的,那时⽤Render 函数就很⽅便了.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。