yang

yang 查看完整档案

南京编辑华中科技大学  |  计算机应用技术 编辑南京  |  前端工程师 编辑填写个人主网站
编辑

镜花水月何从影,云散缘由不是风。

个人动态

yang 收藏了文章 · 10月27日

vue组件略佳操作

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 个参数,分别是:

  1. 要渲染的元素或组件,可以是⼀个 html 标签、组件选项或⼀ 个函数(不常⽤),该参数为必填项。示例:
 // 1. html 标签 

 h('div'); 

 // 2. 组件选项

 import DatePicker from '../component/date- picker.vue'; h(DatePicker);
  1. 对应属性的数据对象,⽐如组件的 props、元素的 class、绑 定的事件、slot、⾃定义指令等,

该参数是可选的,上⽂所说 的 Render 配置项多,指的就是这个参数。

该参数的完整配置 和示例,可以到 Vue.js 的⽂档查看,没必要全部记住,

⽤到 时查阅就好:createElement 参数 (https://cn.vuejs.org/v2/guide... function.html#createElement-参数)。

  1. ⼦节点,可选,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 点

  1. 使⽤两个相同 slot。在 template 中,Vue.js 不允许使⽤两个 相同的 slot,⽐如下⾯的示例是错误的:
<template> 

 <div> 

 <slot></slot> 

 <slot></slot>

 </div> 

 </template>

解决⽅案就是上⽂中讲到的约束,使⽤⼀个深度克隆 VNode 节点的⽅法

  1. 在 SSR 环境(服务端渲染),如果不是常规的 template 写 法,⽐如通过 Vue.extend 和 new Vue 构造来⽣成的组件实 例,是编译不过的,使⽤ Render 函 数来渲染
  2. 在 runtime 版本的 Vue.js 中,如果使⽤ Vue.extend ⼿动构 造⼀个实例,使⽤ template 选项是会报错的,解决⽅案也很简单,把 template 改写为 Render 就可以了。需要注意的是,在开发独⽴组件时,可以通过配置 Vue.js 版本来使 template 选项可⽤,但这是在⾃⼰的环境, ⽆法保证使⽤者的 Vue.js 版本,所以对于提供给他⼈⽤的组 件,是需要考虑兼容 runtime 版本和 SSR 环境的。
  3. 这可能是使⽤ 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 函数就很⽅便了.

实战 5:可⽤ Render ⾃定义列 的表格组件——Table

*正规的表格,是由 <table>、<thead>、<tbody>、<tr>、<th>、<td> 这些标签 组成,⼀般分为表头 columns 和数据 data。
⽀持使⽤ Render 函数来⾃定义某⼀列*。
写⼀个个的 table 系列标签是很麻烦并且重复的,⽽组件的好处就是省去这些基础的⼯作,我们直接给 Table 组件传递列的配置columns 和⾏数据 data,其余的都交给 Table 组件做了。
**columns:列配置,格式为数组,其中每⼀列 column 是⼀个
对象,⽤来描述这⼀列的信息,它的具体说明如下:**

title:列头显示⽂字;
key:对应列内容的字段名;
render:⾃定义渲染列,使⽤ Vue 的 Render 函数,不
定义则直接显示为⽂本。

*column 定义的 key 值,与 data 是⼀⼀对应的,这是⼀种常⻅的
数据接⼝定义规则,*
**data:显示的结构化数据,格式为数组,其中每⼀个对象,就
是⼀⾏的数据,**

先来完成⼀个基础的表格组件,之后再接⼊ Render 来配置⾃定
义列。
在 src/components ⽬录下新建 table-render ⽬录,并创建
table.vue ⽂件:

<template>
   <table>
     <thead>
       <tr>
         <th v-for="(col,i) in columns" :key="i">
           {{col.title}}
         </th>
       </tr>
     </thead>
     <tbody>
       <tr v-for="(row,index) in data" :key="index">
         <td v-for="(col,inx) in columns" :key="inx">
           <template v-if="'render' in col">
             <Render :row="row" :column="col" :index="rowIndex" :render="col.render">
             </Render>
           </template>
           <template v-else>
              {{row[col.key]}}             
           </template>
         </td>
       </tr>
     </tbody>
   </table>
</template>

<script>

import Render from './render.js'
  export default {
    props:{
      columns:{
        type:Array,
        default(){
          return []
        }
      },
      data:{
        type:Array,
        default(){
          return[]
        }
      }
    },
    components:{
      Render
    }
  }
</script>

<style lang="scss" scoped>
  table{
    width:100%;
    border-collapse:collapse;
    border-spacing:0;
    empty-cells:show;//隐藏表格中空单元格上的边框和背景:
    border:1px solid #e9e9e9;
  }
  table th{
    background: #f7f7f7;
    color:#5c6b77;
    font-weight:600;
    white-space:nowrap;//段落中的文本不进行换行
  }
  table td, table th{
    padding:8px 16px;
    border: 1px solid #e9e9e9;
    text-align:center;
  }
</style>

**新建路由 table-render,并在 src/views/ ⽬录下新建⻚⾯
table-render.vue**

*如果 columns 中的某⼀列配置了 render 字段,那就通过
render.js 完成⾃定义模板,否则以字符串形式渲染。⽐如对出⽣⽇
期这列显示为标准的⽇期格式,可以这样定义 column:*

<template>
  <div>
    <table-render :columns="columns" :data="data"></table-render>
  </div>
</template>

<script>
// 整⾏数据编辑的功能。
// 操作这⼀列,默认是⼀个修改按钮,点击后,变为保存和取消两个按
// 钮,同时本⾏其它各列都变为了输⼊框,并且初始值就是刚才单元格
// 的数据。变为输⼊框后,可以任意修改单元格数据,点击保存按钮保
// 存整⾏数据,点击取消按钮,还原⾄修改前的数据。
// 当进⼊编辑状态时,每⼀列的输⼊框都要有⼀个临时的数据使⽤ vmodel 双向绑定来响应修改,所以在 data ⾥再声明四个数据:

import TableRender from './receive'
export default {
  components: {
    TableRender,
  },
  data() {
    return {
      editName: '', // 第⼀列输⼊框
      editAge: '', // 第⼆列输⼊框
      editBirthday: '', // 第三列输⼊框
      editAddress: '', // 第四列输⼊框
      // editIndex 默认给了 -1,也就是⼀个不存在的⾏号,当点击修改 按钮时,再将它置为正确的⾏号。
      editIndex: -1, // 当前聚焦的输⼊框的⾏数
      columns: [
//         除编辑列,其它各数据列都有两种状态:
// 1. 当 editIndex 等于当前⾏号 index 时,呈现输⼊框状态;
// 2. 当 editIndex 不等于当前⾏号 index 时,呈现默认数据。

// edit 根据 editIndex 呈现不同的节点,还是先看 else,直接
// 显示了对应字段的数据。在聚焦时(this.editIndex ===
// index),渲染⼀个 input 输⼊框,初始值 value 通过 render 的
// domProps 绑定了 row.name(这⾥也可绑定 editName),并监
// 听了 input 事件,将输⼊的内容,实时缓存在数据 editName 中,
// 供保存时使⽤。事实上,这⾥绑定的 value 和事件 input 就是语法
// 糖 v-model 在 Render 函数中的写法,在 template 中,经常写作
// <input v-model="editName">。

        {
          title: '姓名',
          key: 'name',
          render:(h,{row,index})=>{
            let edit;
            // 当前行为聚焦行时
            if(this.editIndex == index){
              edit = [h('input',{
                domProps:{
                  value:row.name
                },
                on:{
                  input:(event)=>{
                    this.editName = event.target.value
                  }
                }
              })]
            }else{
              edit = row.name
            }
            return h('div',[edit])
          }
        },
        {
          title: '年龄',
          key: 'age',
          render:(h,{row,index})=>{
            let editAge;
            if(this.editIndex == index){
              editAge = [h('input',{
                domProps:{
                  value: row.age
                },
                on:{
                  input:(event)=>{
                    this.editAge = event.target.value
                  }
                }
              })]
            }else{
              editAge = row.age
            }
            return h('div',[editAge])
          }
        },
        {
          title: '出⽣⽇期',
          key: 'birthday',
          render: (h, { row, column, index }) => {
            let editBirth;
            if(this.editIndex == index){
              editBirth = [h('input',{
                domProps:{
                  value:row.birthday
                },
                on:{
                  input:(event)=>{
                    this.editBirthday = event.target.value
                  }
                }
              })]
            }else{
            const date = new Date(parseInt(row.birthday))
            const year = date.getFullYear()
            const month = date.getMonth() + 1
            const day = date.getDate()
            editBirth = `${year}-${month}-${day}`
            }
            
            return h('div', [editBirth])
          },
        },
        {
          title: '地址',
          key: 'address',
          render:(h,{row,index})=>{
            let editAddress;
            if(this.editIndex ==index){
              editAddress=[h('input',{
                domProps:{
                  value:row.address
                },
                on:{
                  input:(event)=>{
                    this.editAdress = event.target.value
                  }
                }
              })]
            }else{
                editAddress = row.address 
            }
            return h('div',[editAddress])
          }
        },
        // 先定义操作列的 render:
//         为默认是⾮
// 编辑状态,
// 也就是说 editIndex 还是 -1。当点击修改按钮时,把 render 中
// 第⼆个参数 { row } 中的各列数据赋值给了之前在 data 中声明的
// 4 个数据,这样做是因为之后点击取消按钮时,editName 等值已经
// 修改了,还没有还原,所以在开启编辑状态的同时,初始化各输⼊框
// 的值(当然也可以在取消时重置)。最后再把 editIndex 置为了对
// 应的⾏序号 { index },此时 render 的 if 条件
// this.editIndex === index 为真,编辑列变成了两个按钮:保
// 存和取消。点击保存,直接修改表格源数据 data 中对应的各字段
// 值,并将 editIndex 置为 -1,退出编辑状态;点击取消,不保存源
// 数据,直接退出编辑状态。
        {
          title: '操作',
          render:(h,{row,index})=>{
            //如果当前行是编辑状态,则渲染两个按钮
            if(this.editIndex === index){
              return [
                h('button',{
                  on:{
                    click:()=>{
                      this.data[index].name = this.editName
                      this.data[index].age = this.editAge
                      this.data[index].address = this.editAddress
                      this.data[index].birthday = this.editBirthday
                      this.editIndex = -1
                    }
                  }
                },'保存'),
                h('button',{
                  style:{
                    marginLeft:'6px'
                  },
                  on:{
                    click:()=>{
                      this.editIndex = -1
                    }
                  }
                },'取消')
              ]
            }else{
              // 当前行是默认状态,渲染为一个按钮
              return h('button',{
                on:{
                  click:()=>{
                    this.editName = row.name
                    this.editAge = row.age
                    this.editAddress = row.address
                    this.editBirthday = row.birthday
                    this.editIndex = index
                  }
                }
              },'修改')
            }
          }
        },
      ],
      data: [
        {
          name: '王⼩明',
          age: 18,
          birthday: '919526400000',
          address: '北京市朝阳区芍药居',
        },
        {
          name: '张⼩刚',
          age: 25,
          birthday: '696096000000',
          address: '北京市海淀区⻄⼆旗',
        },
        {
          name: '李⼩红',
          age: 30,
          birthday: '563472000000',
          address: '上海市浦东新区世纪⼤道',
        },
        {
          name: '周⼩伟',
          age: 26,
          birthday: '687024000000',
          address: '深圳市南⼭区深南⼤道',
        },
      ],
    }
  },
}
</script>

<style lang="scss" scoped></style>

*columns ⾥定义的 render,是有两个参数的,第⼀ 个是 createElement(即 h),

  第⼆个是从 render.js 传过来的对象,

它包含了当前⾏数据(row)、当前列配置(column)、当前
是第⼏⾏(index),使⽤者可以基于这 3 个参数得到任意想要的
结果。由于是⾃定义列了,显示什么都是使⽤者决定的,因此在使⽤
了 render 的 column ⾥可以不⽤写字段 key 。*
*columns ⾥定义的 render 字段,它
仅仅是名字叫 render 的⼀个普通函数,并⾮ Vue.js 实例的 render
选项,只是我们恰巧把它叫做 render ⽽已,如果愿意,也可以改为
其它名字,⽐如 renderRow。真正的 Render 函数只有⼀个地⽅,
那就是 render.js 中的 render 选项,只是它代理了 column 中的
render*。

*显示正常的⽇期 写⼀个计算属性(computed) 但对于操作这⼀列就不可取了,因为它带有业
务逻辑,点击编辑按钮,是可以对当前⾏数据进⾏修改的。这时就要 ⽤到 Render 函数。*
先在 src/components/table-render ⽬录下新建 render.js ⽂件
*使⽤ Render ⾃定义列模板
函数式组件 Functional Render 的⽤法,它 没有状态和上下⽂,主要⽤于中转⼀个组件*

// render.js 定义了 4 个 props:
// row:当前⾏的数据;
// column:当前列的数据;
// index:当前是第⼏⾏;
// render:具体的 render 函数内容。
// 这⾥的 render 选项并没有渲染任何节点,⽽是直接返回 props 中
// 定义的 render,并将 h 和当前的⾏、列、序号作为参数传递出去。
// 然后在 table.vue ⾥就可以使⽤ render.js 组件:
export default {
    functional: true,
    props:{
        row:Object,
        column:Object,
        index:Number,
        render:Function
    },
    render:(h,ctx)=>{
        const params = {
            row:ctx.props.row,
            column:ctx.props.column,
            index:ctx.props.index
        };
        return ctx.props.render(h,params)
    }
}

*⼀个完整的 Table 组件功能要复杂的多,⽐如排序、筛选、列
固定、表头固定、表头嵌套等*
**很多 Vue.js 的开发难题,都可以⽤ Render 函数来解决,
它⽐ template 模板更灵活,可以完全发挥 JavaScript 的编程能
⼒,因此很多 JS 的开发思想都可以借鉴。**

实战 6:可⽤ slot-scope ⾃定 义列的表格组件——Table

Render 函数能够完全发挥 JavaScript 的编程能⼒,实
现⼏乎所有的⾃定义⼯作,但本质上,使⽤者写的是⼀个庞⼤的 JS
对象,它不具备 DOM 结构,可读性和可维护性都⽐较差。实现⼀种达到同样渲染效果,但对使⽤者更友好的 slot-scope
写法。
常规的 slot ⽆法实现对组件循环体的每⼀项进⾏不同的内容分发,
这就要⽤到 slot-scope,它本质上跟 slot ⼀样,只不过可以传递参
数。

<ul>
      <li v-for="book in books" :key="book.id">
        <slot :book="book">
          {{book.name}}
        </slot>
      </li>
    </ul>

*在 slot 上,传递了⼀个⾃定义的参数 book,它的值绑定的是当前
循环项的数据 book,这样在⽗级使⽤时,就可以在 slot 中访问它
了:*

 <table-list>
      <template slot-scope="slotProps">
      <span v-if="slotProps.book.scale">限时优惠</span>
      {{slotProps.book.name}}
      </template>
    </table-list>

*除了可以传递参数,其它⽤法跟 slot 是⼀样的,⽐如也可以“具
名”:使⽤ ES6 的解构,能让参数使⽤起来更⽅便*

<slot :book="book" name="book">
      {{ book.name }}
</slot>
    
    
   // ES6 的解构
    <template slot-scope="{ book }" slot="book">
      <span v-if="book.sale">限时优惠</span>
      {{ book.name }}
    </template>

*这就是作⽤域 slot(slot-scope),能够在组件的循环体中做内容
分发,有了它,Table 组件的⾃定义列模板就不⽤写⼀⻓串的
Render 函数了,slot-scope作用域插槽实现了父组件调用子组件内部的数据,子组件的数据通过slot-scope属性传递到了父组件*
⽤ 3 种⽅法来改写 Table,实现 slot-scope ⾃定义列模板
第⼀种⽅案,
⽤最简单的 slot-scope 实现,同时也兼容 Render 函
数的旧⽤法。拷⻉上⼀节的 Table 组件⽬录,更名为 tableslot,同时也拷⻉路由,更名为 table-slot.vue。为了兼容旧
的 Render 函数⽤法,在 columns 的列配置 column 中,新增⼀个
字段 slot 来指定 slot-scope 的名称:

<!-- src/components/table-slot/table.vue -->
<template>
   <table>
     <thead>
       <tr>
         <th v-for="(col,i) in columns" :key="i">
           {{col.title}}
         </th>
       </tr>
     </thead>
     <tbody>
       <tr v-for="(row,index) in data" :key="index">
         <td v-for="(col,inx) in columns" :key="inx">
           <template v-if="'render' in col">
             <Render :row="row" :column="col" :index="rowIndex" :render="col.render">
             </Render>
           </template>
            <template v-else-if="'slot' in col">
             <slot :index="rowIndex" :name="row.slot" :column="col">  
             </slot>
           </template>
           <template v-else>
              {{row[col.key]}}             
           </template>
         </td>
       </tr>
     </tbody>
   </table>
</template>

<script>

import Render from './render.js'
  export default {
    props:{
      columns:{
        type:Array,
        default(){
          return []
        }
      },
      data:{
        type:Array,
        default(){
          return[]
        }
      }
    },
    components:{
      Render
    }
  }
</script>

<style lang="scss" scoped>
  table{
    width:100%;
    border-collapse:collapse;
    border-spacing:0;
    empty-cells:show;//隐藏表格中空单元格上的边框和背景:
    border:1px solid #e9e9e9;
  }
  table th{
    background: #f7f7f7;
    color:#5c6b77;
    font-weight:600;
    white-space:nowrap;//段落中的文本不进行换行
  }
  table td, table th{
    padding:8px 16px;
    border: 1px solid #e9e9e9;
    text-align:center;
  }
</style>

*相⽐原先的⽂件,只在 'render' in col 的条件下新加了⼀个
template 的标签,如果使⽤者的 column 配置了 render 字段,
就优先以 Render 函数渲染,然后再判断是否⽤ slot-scope 渲染。
在定义的作⽤域 slot 中,将⾏数据 row、列数据 column 和第⼏⾏
index 作为 slot 的参数,并根据 column 中指定的 slot 字段值,
动态设置了具名 name。使⽤者在配置 columns 时,只要指定了某
⼀列的 slot,那就可以在 Table 组件中使⽤ slot-scope。我们以上
⼀节的可编辑整⾏数据为例,⽤ slot-scope 的写法实现完全⼀样的
效果:*

<!-- src/views/table-slot.vue -->
<template>
  <div>
    <table-slot :columns="columns" :data="data">
      <template slot-scope="{ row, index }" slot="name">
        <input type="text" v-model="editName" v-if="editIndex === index" />
        <span v-else>{{ row.name }}</span>
      </template>
      <template slot-scope="{ row, index }" slot="age">
        <input type="text" v-model="editAge" v-if="editIndex === index" />
        <span v-else>{{ row.age }}</span>
      </template>
      <template slot-scope="{ row, index }" slot="birthday">
        <input type="text" v-model="editBirthday" v-if="editIndex === index" />
        <span v-else>{{ getBirthday(row.birthday) }}</span>
      </template>
      <tempalte slot-scope="{ row, index }" slot="address">
        <input type="text" v-model="editAddress" v-if="editIndex === index" />
        <span v-else>{{ row.address }}</span>
      </tempalte>
      <template slot-scope="{ row, index }" slot="action">
        <div v-if="editIndex === index">
          <button @click="handleSave(index)">保存</button>
          <button @click="editIndex = -1">取消</button>
        </div>
        <div v-else>
          <button @click="handleEdit(row, index)">操作</button>
        </div>
      </template>
    </table-slot>
  </div>
</template>

<script>
import TableSlot from './table.vue'
export default {
  components: {
    TableSlot,
  },
  data() {
    return {
      editName: '', // 第⼀列输⼊框
      editAge: '', // 第⼆列输⼊框
      editBirthday: '', // 第三列输⼊框
      editAddress: '', // 第四列输⼊框
      editIndex: -1, // 当前聚焦的输⼊框的⾏数
      columns: [
        {
          title: '姓名',
          slot: 'name',
        },
        {
          title: '年龄',
          slot: 'age',
        },
        {
          title: '出⽣⽇期',
          slot: 'birthday',
        },
        {
          title: '地址',
          slot: 'address',
        },
        {
          title: '操作',
          slot: 'action',
        },
      ],
      data: [
        {
          name: '王⼩明',
          age: 18,
          birthday: '919526400000',
          address: '北京市朝阳区芍药居',
        },
        {
          name: '张⼩刚',
          age: 25,
          birthday: '696096000000',
          address: '北京市海淀区⻄⼆旗',
        },
        {
          name: '李⼩红',
          age: 30,
          birthday: '563472000000',
          address: '上海市浦东新区世纪⼤道',
        },
        {
          name: '周⼩伟',
          age: 26,
          birthday: '687024000000',
          address: '深圳市南⼭区深南⼤道',
        },
      ],
    }
  },
  methods: {
      handleEdit(row,index) {
          this.editName = row.name
          this.editAge = row.age
          this.editAddress = row.address
          this.editBirthday = row.birthday
          this.editIndex = index
      },
      handleSave(index){
          this.data[index].name = this.editBirthday
          this.data[index].age = this.editAge
          this.data[index].address = this.editAddress
          this.data[index].birtyhday = this.editBirthday
          this.editIndex = -1
      },
      getBirthday(birthday){
          const date = new Date(parseInt(birthday))
          const year = date.getFullYear()
          const month = date.getMonth()+1
          const day = date.getDate()
          return `${year}-${month}-${day}`
      }
  },
}
</script>

<style lang="scss" scoped></style>

*<table-slot> 内的每⼀个 <template> 就对应某⼀列
的 slot-scope 模板,通过配置的 slot 字段,指定具名的 slotscope。可以看到,基本是把 Render 函数还原成了 html 的写法,
这样看起来直接多了,渲染效果是完全⼀样的。在 slot-scope 中,
平时怎么写组件,这⾥就怎么写,Vue.js 所有的 API 都是可以直接
使⽤的。*

第⼆种⽅案
不需要修改原先的 Table 组件代码,只是在使⽤层⾯
修改即可。先来看具体的使⽤代码,然后再做分析。注意,这⾥使⽤
的 Table 组件,仍然是上⼀节 src/components/table-render
的组件,它只有 Render 函数,没有定义 slot-scope:

<!-- src/views/table-render.vue -->
<template>
  <div>
    <table-render ref="table" :columns="columns" :data="data">
      <template slot-scope="{ row, index }" slot="name">
        <input type="text" v-model="editName" v-if="editIndex === index" />
        <span v-else>{{ row.name }}</span>
      </template>
      <template slot-scope="{ row, index }" slot="age">
        <input type="text" v-model="editAge" v-if="editIndex === index" />
        <span v-else>{{ row.age }}</span>
      </template>
      <template slot-scope="{ row, index }" slot="birthday">
        <input type="text" v-model="editBirthday" v-if="editIndex === index" />
        <span v-else>{{ getBirthday(row.birthday) }}</span>
      </template>
      <tempalte slot-scope="{ row, index }" slot="address">
        <input type="text" v-model="editAddress" v-if="editIndex === index" />
        <span v-else>{{ row.address }}</span>
      </tempalte>
      <template slot-scope="{ row, index }" slot="action">
        <div v-if="editIndex === index">
          <button @click="handleSave(index)">保存</button>
          <button @click="editIndex = -1">取消</button>
        </div>
        <div v-else>
          <button @click="handleEdit(row, index)">操作</button>
        </div>
      </template>
    </table-render>
  </div>
</template>

<script>
import TableRender from './table.vue'
export default {
  components: {
    TableRender,
  },
  data() {
    return {
      editName: '', // 第⼀列输⼊框
      editAge: '', // 第⼆列输⼊框
      editBirthday: '', // 第三列输⼊框
      editAddress: '', // 第四列输⼊框
      editIndex: -1, // 当前聚焦的输⼊框的⾏数
      columns: [
        {
          title: '姓名',
          render:(h,{row,column,index})=>{
            //   $scopedSlots获取到父组件中创建的插槽 向插槽内传递参数   name插槽传递
              return h('div',this.$refs.table.$scopedSlots.name({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '年龄',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots.age({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '出⽣⽇期',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots.birthday({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '地址',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots.address({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '操作',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
      ],
      data: [
        {
          name: '王⼩明',
          age: 18,
          birthday: '919526400000',
          address: '北京市朝阳区芍药居',
        },
        {
          name: '张⼩刚',
          age: 25,
          birthday: '696096000000',
          address: '北京市海淀区⻄⼆旗',
        },
        {
          name: '李⼩红',
          age: 30,
          birthday: '563472000000',
          address: '上海市浦东新区世纪⼤道',
        },
        {
          name: '周⼩伟',
          age: 26,
          birthday: '687024000000',
          address: '深圳市南⼭区深南⼤道',
        },
      ],
    }
  },
  methods: {
      handleEdit(row,index) {
          this.editName = row.name
          this.editAge = row.age
          this.editAddress = row.address
          this.editBirthday = row.birthday
          this.editIndex = index
      },
      handleSave(index){
          this.data[index].name = this.editBirthday
          this.data[index].age = this.editAge
          this.data[index].address = this.editAddress
          this.data[index].birtyhday = this.editBirthday
          this.editIndex = -1
      },
      getBirthday(birthday){
          const date = new Date(parseInt(birthday))
          const year = date.getFullYear()
          const month = date.getMonth()+1
          const day = date.getDate()
          return `${year}-${month}-${day}`
      }
  },
}
</script>

<style lang="scss" scoped></style>

在 slot-scope 的使⽤上(即 template 的内容),与⽅案⼀是完全
⼀致的,可以看到,在 column 的定义上,仍然使⽤了 render 字
段,只不过每个 render 都渲染了⼀个 div 节点,⽽这个 div 的内
容,是指定来在 <table-render> 中定义的 slot-scope:

<!-- src/views/table-render.vue -->
<template>
  <div>
    <table-render ref="table" :columns="columns" :data="data">
      <template slot-scope="{ row, index }" slot="name">
        <input type="text" v-model="editName" v-if="editIndex === index" />
        <span v-else>{{ row.name }}</span>
      </template>
      <template slot-scope="{ row, index }" slot="age">
        <input type="text" v-model="editAge" v-if="editIndex === index" />
        <span v-else>{{ row.age }}</span>
      </template>
      <template slot-scope="{ row, index }" slot="birthday">
        <input type="text" v-model="editBirthday" v-if="editIndex === index" />
        <span v-else>{{ getBirthday(row.birthday) }}</span>
      </template>
      <tempalte slot-scope="{ row, index }" slot="address">
        <input type="text" v-model="editAddress" v-if="editIndex === index" />
        <span v-else>{{ row.address }}</span>
      </tempalte>
      <template slot-scope="{ row, index }" slot="action">
        <div v-if="editIndex === index">
          <button @click="handleSave(index)">保存</button>
          <button @click="editIndex = -1">取消</button>
        </div>
        <div v-else>
          <button @click="handleEdit(row, index)">操作</button>
        </div>
      </template>
    </table-render>
  </div>
</template>

<script>
import TableRender from './table.vue'
export default {
  components: {
    TableRender,
  },
  data() {
    return {
      editName: '', // 第⼀列输⼊框
      editAge: '', // 第⼆列输⼊框
      editBirthday: '', // 第三列输⼊框
      editAddress: '', // 第四列输⼊框
      editIndex: -1, // 当前聚焦的输⼊框的⾏数
      columns: [
        {
          title: '姓名',
          render:(h,{row,column,index})=>{
            //   $scopedSlots获取到父组件中创建的插槽 插槽内容放在div标签中,向插槽内传递参数   name插槽传递
              return h('div',this.$refs.table.$scopedSlots.name({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '年龄',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots.age({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '出⽣⽇期',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots.birthday({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '地址',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots.address({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '操作',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
      ],
      data: [
        {
          name: '王⼩明',
          age: 18,
          birthday: '919526400000',
          address: '北京市朝阳区芍药居',
        },
        {
          name: '张⼩刚',
          age: 25,
          birthday: '696096000000',
          address: '北京市海淀区⻄⼆旗',
        },
        {
          name: '李⼩红',
          age: 30,
          birthday: '563472000000',
          address: '上海市浦东新区世纪⼤道',
        },
        {
          name: '周⼩伟',
          age: 26,
          birthday: '687024000000',
          address: '深圳市南⼭区深南⼤道',
        },
      ],
    }
  },
  methods: {
      handleEdit(row,index) {
          this.editName = row.name
          this.editAge = row.age
          this.editAddress = row.address
          this.editBirthday = row.birthday
          this.editIndex = index
      },
      handleSave(index){
          this.data[index].name = this.editBirthday
          this.data[index].age = this.editAge
          this.data[index].address = this.editAddress
          this.data[index].birtyhday = this.editBirthday
          this.editIndex = -1
      },
      getBirthday(birthday){
          const date = new Date(parseInt(birthday))
          const year = date.getFullYear()
          const month = date.getMonth()+1
          const day = date.getDate()
          return `${year}-${month}-${day}`
      }
  },
}
</script>

<style lang="scss" scoped></style>

在 slot-scope 的使⽤上(即 template 的内容),与⽅案⼀是完全
⼀致的,可以看到,在 column 的定义上,仍然使⽤了 render 字
段,只不过每个 render 都渲染了⼀个 div 节点,⽽这个 div 的内
容,是指定来在 <table-render> 中定义的 slot-scope:

 {
          title: '姓名',
          render:(h,{row,column,index})=>{
            //   $scopedSlots获取到父组件中创建的插槽 插槽内容放在div标签中,向插槽内传递参数   name插槽传递
              return h('div',this.$refs.table.$scopedSlots.name({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },

*name 这⼀列仍然是使⽤ Functional Render,只不过 Render 的是⼀个预先定义好的slot-scope 模板
有⼀点需要注意的是,示例中的 data 默认是空数组,⽽在
mounted ⾥才赋值的,是因为这样定义的 slot-scope,初始时读
取 this.$refs.table.$scopedSlots 是读不到的,会报错,当
没有数据时,也就不会去渲染,也就避免了报错。
这种⽅案虽然可⾏,但归根到底是⼀种 hack,不是⾮常推荐*

⽅案三
第 3 中⽅案的思路和第 2 种是⼀样的,它介于⽅案 1 与⽅案 2 之
间。这种⽅案要修改 Table 组件代码,但是⽤例与⽅案 1 完全⼀
致。
在⽅案 2 中,我们是通过修改⽤例使⽤ slot-scope 的,也就是说
Table 组件本身没有⽀持 slot-scope,是我们“强加”上去的,如果
把强加的部分,集成到 Table 内,那对使⽤者就很友好了,同时也
避免了初始化报错,不得不把 data 写在 mounted 的问题。
保持⽅案 1 的⽤例不变,修改 src/components/table-render
中的代码。为了同时兼容 Render 与 slot-scope,我们在 tablerender 下新建⼀个 slot.js 的⽂件:

export default {
    functional:true,
    inject:['tableRoot'],
    props:{
        row:Object,
        column:Object,
        index:Number
    },
    render:(h,ctx)=>{
        return h('div',ctx.inject.tableRoot.$scopedSlots[ctx.props.column.slot]{
            row:ctx.props.row,
            column:ctx.props.column,
            index:ctx.props.index
        })
    }
}

它仍然是⼀个 Functional Render,使⽤ inject 注⼊了⽗级组件
table.vue(下⽂改写) 中提供的实例 tableRoot。在 render
⾥,也是通过⽅案 2 中使⽤ $scopedSlots 定义的 slot,不过这
是在组件级别定义,对⽤户来说是透明的,只要按⽅案 1 的⽤例来
写就可以了。
table.vue 也要做⼀点修改:

<template>
   <table>
     <thead>
       <tr>
         <th v-for="(col,i) in columns" :key="i">
           {{col.title}}
         </th>
       </tr>
     </thead>
     <tbody>
       <tr v-for="(row,rowIndex) in data" :key="rowIndex">
         <td v-for="(col,inx) in columns" :key="inx">
           <template v-if="'render' in col">
             <Render :row="row" :column="col" :index="rowIndex" :render="col.render">
             </Render>
           </template>
            <template v-else-if="'slot' in col">
             <slot-scope :index="rowIndex" :name="row.slot" :column="col">  
             </slot-scope>
           </template>
           <template v-else>
              {{row[col.key]}}             
           </template>
         </td>
       </tr>
     </tbody>
   </table>
</template>

<script>

import Render from './render.js'
import SlotScope from './slot.js'
  export default {
    provide(){
      return {
        tableRoot:this
      }
    },
    props:{
      columns:{
        type:Array,
        default(){
          return []
        }
      },
      data:{
        type:Array,
        default(){
          return[]
        }
      }
    },
    components:{
      Render,
      SlotScope
    }
  }
</script>

<style lang="scss" scoped>
  table{
    width:100%;
    border-collapse:collapse;
    border-spacing:0;
    empty-cells:show;//隐藏表格中空单元格上的边框和背景:
    border:1px solid #e9e9e9;
  }
  table th{
    background: #f7f7f7;
    color:#5c6b77;
    font-weight:600;
    white-space:nowrap;//段落中的文本不进行换行
  }
  table td, table th{
    padding:8px 16px;
    border: 1px solid #e9e9e9;
    text-align:center;
  }
</style>

*因为 slot-scope 模板是写在 table.vue 中的(对使⽤者来说,相当
于写在组件 <table-slot></table-slot> 之间),所以在
table.vue 中使⽤ provide 向下提供了 Table 的实例,这样在
slot.js 中就可以通过 inject 访问到它,继⽽通过 $scopedSlots
获取到 slot。需要注意的是,在 Functional Render 是没有 this 上
下⽂的,都是通过 h 的第⼆个参数临时上下⽂ ctx 来访问 prop、
inject 等的。
⽅案 3 也是推荐使⽤的,当 Table 的功能⾜够复杂,层级会嵌套的
⽐较深,那时⽅案 1 的 slot 就不会定义在第⼀级组件中,中间可能
会隔许多组件,slot 就要⼀层层中转,相⽐在任何地⽅都能直接使
⽤的 Render 就要麻烦了。所以,如果你的组件层级简单,推荐⽤第
⼀种⽅案;如果你的组件已经成型(某 API 基于 Render 函数),
但⼀时间不⽅便⽀持 slot-scope,⽽使⽤者⼜想⽤,那就选⽅案
2;如果你的组件已经成型(某 API 基于 Render 函数),但组件层
级复杂,要按⽅案 1 那样⽀持 slot-scope 可能改动较⼤,还有可
能带来新的 bug,那就⽤⽅案 3,它不会破坏原有的任何内容,但
会额外⽀持 slot-scope ⽤法,关键是改动简单*。

理论上,绝⼤多数能⽤ Render 的地⽅,都可以⽤ slot-scope

动态组件

*根据⼀些条件,动态地切换某个组件,或动态地选择渲染某个组件
使⽤ Render 或 Functional Render 可以解决动
态切换组件的需求,不过那是基于⼀个 JS 对象(Render 函数),
⽽ Vue.js 提供了另外⼀个内置的组件 <component> 和 is 特性,
可以更好地实现动态组件*
先来看⼀个 <component> 和 is 的基本示例,⾸先定义三个普通组
件:

<!--
 * @Author: yang
 * @Date: 2020-10-28 17:45:13
 * @LastEditors: yang
 * @LastEditTime: 2020-10-28 17:53:43
 * @FilePath: \gloud-h5\src\component\index\a.vue
-->
<template>
    <div>
        a组件
    </div>
</template>

<script>
    export default {
        beforeDestroy () {
            console.log('组件销毁');
        },
        mounted () {
            console.log('组件创建');
        },
    }
</script>

<style lang="scss" scoped>

</style>
<!--
 * @Author: yang
 * @Date: 2020-10-28 17:45:17
 * @LastEditors: yang
 * @LastEditTime: 2020-10-28 17:45:46
 * @FilePath: \gloud-h5\src\component\index\b.vue
-->
<template>
    <div>
        b组件
    </div>
</template>

<script>
    export default {
        
    }
</script>

<style lang="scss" scoped>

</style>
<!--
 * @Author: yang
 * @Date: 2020-10-28 17:45:22
 * @LastEditors: yang
 * @LastEditTime: 2020-10-28 17:45:54
 * @FilePath: \gloud-h5\src\component\index\c.vue
-->
<template>
    <div>
        c组件
    </div>
</template>

<script>
    export default {
        
    }
</script>

<style lang="scss" scoped>

</style>

然后在⽗组件中导⼊这 3 个组件,并动态切换:
只要切换到 A 组件,mounted 就会触发⼀次,切换到其它组
件,beforeDestroy 也会触发⼀次,说明组件再重新渲染,这样
有可能导致性能问题。为了避免组件的重复渲染,可以在
<component> 外层套⼀个 Vue.js 内置的 <keep-alive> 组件,
这样,组件就会被缓存起来:
*keep-alive 还有⼀些额外的 props 可以配置:
include:字符串或正则表达式。只有名称匹配的组件会被缓
存。
exclude:字符串或正则表达式。任何名称匹配的组件都不会
被缓存。
max:数字。最多可以缓存多少组件实例。*

<template>
    <div>
        <button @click="handleChange('a')">显示组件a</button>
        <button @click="handleChange('b')">显示组件b</button>
        <button @click="handleChange('c')">显示组件c</button>
        <keep-alive>
        <component :is="component"></component>
        </keep-alive>
    </div>
</template>

<script>
import a from './a.vue';
import b from './b.vue';
import c from './c.vue';
    export default {
        data() {
            return {
                component: a
            }
        },
        methods: {
            handleChange(item) {
                switch(item){
                    case 'a':
                        this.component = a
                        break;
                     case 'b':
                        this.component = b
                        break;
                         case 'c':
                        this.component = c
                        break;
                }
            }
        },
    }
</script>

<style lang="scss" scoped>

</style

这⾥的 is 动态绑定的是⼀个组件对象(Object),它直接指向 a /
b / c 三个组件中的⼀个

动态渲染标签

除了直接绑定⼀个 Object,还可以是⼀ 个 String,⽐如标签名、组件名。下⾯的这个组件,将原⽣的按钮 button 进⾏了封装,如果传⼊了 prop: to,那它会渲染为⼀个 标签,⽤于打开这个链接地址,如果没有传⼊ to,就当作普通 button 使⽤。来看下⾯的示例:

button.vue

<template>
    <component :is="tagName" v-bind="tagProps">
        <slot></slot>
    </component>
</template>

<script>
    export default {
        props:{
            to:{
                type:String,
                default:""
            },
            target:{
                type:String,
                default:'_self'
            }
        },
        computed: {
            // 动态渲染不同的标签
            tagName() {
                return this.to === ''?'button':'a'
            },
            // 如果是链接,把这些属性绑定到component上
            tagProps(){
                let props = {}
                if(this.to){
                    props={
                        target:this.target,
                        href:this.to
                    }
                }
                return props
            }
        },
    }
</script>

<style lang="scss" scoped>

</style>

使用

<template>
    <div>
        <i-button>普通按钮</i-button>
        <i-button to="http://www.baidu.com">链接按钮</i-button>
        <i-button target="_bank" to="http://www.baidu.com">新窗口打开链接</i-button>

    </div>
</template>

<script>
import iButton from './button.vue';
    export default {
        components: {
            iButton,
        },
    }
</script>

<style lang="scss" scoped>

</style>

i-button 组件中的 <component> is 绑定的就是⼀个标签名称
button / a,并且通过 v-bind 将⼀些额外的属性全部绑定到了
<component> 上。

查看原文

yang 发布了文章 · 10月27日

vue组件略佳操作

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 个参数,分别是:

  1. 要渲染的元素或组件,可以是⼀个 html 标签、组件选项或⼀ 个函数(不常⽤),该参数为必填项。示例:
 // 1. html 标签 

 h('div'); 

 // 2. 组件选项

 import DatePicker from '../component/date- picker.vue'; h(DatePicker);
  1. 对应属性的数据对象,⽐如组件的 props、元素的 class、绑 定的事件、slot、⾃定义指令等,

该参数是可选的,上⽂所说 的 Render 配置项多,指的就是这个参数。

该参数的完整配置 和示例,可以到 Vue.js 的⽂档查看,没必要全部记住,

⽤到 时查阅就好:createElement 参数 (https://cn.vuejs.org/v2/guide... function.html#createElement-参数)。

  1. ⼦节点,可选,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 点

  1. 使⽤两个相同 slot。在 template 中,Vue.js 不允许使⽤两个 相同的 slot,⽐如下⾯的示例是错误的:
<template> 

 <div> 

 <slot></slot> 

 <slot></slot>

 </div> 

 </template>

解决⽅案就是上⽂中讲到的约束,使⽤⼀个深度克隆 VNode 节点的⽅法

  1. 在 SSR 环境(服务端渲染),如果不是常规的 template 写 法,⽐如通过 Vue.extend 和 new Vue 构造来⽣成的组件实 例,是编译不过的,使⽤ Render 函 数来渲染
  2. 在 runtime 版本的 Vue.js 中,如果使⽤ Vue.extend ⼿动构 造⼀个实例,使⽤ template 选项是会报错的,解决⽅案也很简单,把 template 改写为 Render 就可以了。需要注意的是,在开发独⽴组件时,可以通过配置 Vue.js 版本来使 template 选项可⽤,但这是在⾃⼰的环境, ⽆法保证使⽤者的 Vue.js 版本,所以对于提供给他⼈⽤的组 件,是需要考虑兼容 runtime 版本和 SSR 环境的。
  3. 这可能是使⽤ 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 函数就很⽅便了.

实战 5:可⽤ Render ⾃定义列 的表格组件——Table

*正规的表格,是由 <table>、<thead>、<tbody>、<tr>、<th>、<td> 这些标签 组成,⼀般分为表头 columns 和数据 data。
⽀持使⽤ Render 函数来⾃定义某⼀列*。
写⼀个个的 table 系列标签是很麻烦并且重复的,⽽组件的好处就是省去这些基础的⼯作,我们直接给 Table 组件传递列的配置columns 和⾏数据 data,其余的都交给 Table 组件做了。
**columns:列配置,格式为数组,其中每⼀列 column 是⼀个
对象,⽤来描述这⼀列的信息,它的具体说明如下:**

title:列头显示⽂字;
key:对应列内容的字段名;
render:⾃定义渲染列,使⽤ Vue 的 Render 函数,不
定义则直接显示为⽂本。

*column 定义的 key 值,与 data 是⼀⼀对应的,这是⼀种常⻅的
数据接⼝定义规则,*
**data:显示的结构化数据,格式为数组,其中每⼀个对象,就
是⼀⾏的数据,**

先来完成⼀个基础的表格组件,之后再接⼊ Render 来配置⾃定
义列。
在 src/components ⽬录下新建 table-render ⽬录,并创建
table.vue ⽂件:

<template>
   <table>
     <thead>
       <tr>
         <th v-for="(col,i) in columns" :key="i">
           {{col.title}}
         </th>
       </tr>
     </thead>
     <tbody>
       <tr v-for="(row,index) in data" :key="index">
         <td v-for="(col,inx) in columns" :key="inx">
           <template v-if="'render' in col">
             <Render :row="row" :column="col" :index="rowIndex" :render="col.render">
             </Render>
           </template>
           <template v-else>
              {{row[col.key]}}             
           </template>
         </td>
       </tr>
     </tbody>
   </table>
</template>

<script>

import Render from './render.js'
  export default {
    props:{
      columns:{
        type:Array,
        default(){
          return []
        }
      },
      data:{
        type:Array,
        default(){
          return[]
        }
      }
    },
    components:{
      Render
    }
  }
</script>

<style lang="scss" scoped>
  table{
    width:100%;
    border-collapse:collapse;
    border-spacing:0;
    empty-cells:show;//隐藏表格中空单元格上的边框和背景:
    border:1px solid #e9e9e9;
  }
  table th{
    background: #f7f7f7;
    color:#5c6b77;
    font-weight:600;
    white-space:nowrap;//段落中的文本不进行换行
  }
  table td, table th{
    padding:8px 16px;
    border: 1px solid #e9e9e9;
    text-align:center;
  }
</style>

**新建路由 table-render,并在 src/views/ ⽬录下新建⻚⾯
table-render.vue**

*如果 columns 中的某⼀列配置了 render 字段,那就通过
render.js 完成⾃定义模板,否则以字符串形式渲染。⽐如对出⽣⽇
期这列显示为标准的⽇期格式,可以这样定义 column:*

<template>
  <div>
    <table-render :columns="columns" :data="data"></table-render>
  </div>
</template>

<script>
// 整⾏数据编辑的功能。
// 操作这⼀列,默认是⼀个修改按钮,点击后,变为保存和取消两个按
// 钮,同时本⾏其它各列都变为了输⼊框,并且初始值就是刚才单元格
// 的数据。变为输⼊框后,可以任意修改单元格数据,点击保存按钮保
// 存整⾏数据,点击取消按钮,还原⾄修改前的数据。
// 当进⼊编辑状态时,每⼀列的输⼊框都要有⼀个临时的数据使⽤ vmodel 双向绑定来响应修改,所以在 data ⾥再声明四个数据:

import TableRender from './receive'
export default {
  components: {
    TableRender,
  },
  data() {
    return {
      editName: '', // 第⼀列输⼊框
      editAge: '', // 第⼆列输⼊框
      editBirthday: '', // 第三列输⼊框
      editAddress: '', // 第四列输⼊框
      // editIndex 默认给了 -1,也就是⼀个不存在的⾏号,当点击修改 按钮时,再将它置为正确的⾏号。
      editIndex: -1, // 当前聚焦的输⼊框的⾏数
      columns: [
//         除编辑列,其它各数据列都有两种状态:
// 1. 当 editIndex 等于当前⾏号 index 时,呈现输⼊框状态;
// 2. 当 editIndex 不等于当前⾏号 index 时,呈现默认数据。

// edit 根据 editIndex 呈现不同的节点,还是先看 else,直接
// 显示了对应字段的数据。在聚焦时(this.editIndex ===
// index),渲染⼀个 input 输⼊框,初始值 value 通过 render 的
// domProps 绑定了 row.name(这⾥也可绑定 editName),并监
// 听了 input 事件,将输⼊的内容,实时缓存在数据 editName 中,
// 供保存时使⽤。事实上,这⾥绑定的 value 和事件 input 就是语法
// 糖 v-model 在 Render 函数中的写法,在 template 中,经常写作
// <input v-model="editName">。

        {
          title: '姓名',
          key: 'name',
          render:(h,{row,index})=>{
            let edit;
            // 当前行为聚焦行时
            if(this.editIndex == index){
              edit = [h('input',{
                domProps:{
                  value:row.name
                },
                on:{
                  input:(event)=>{
                    this.editName = event.target.value
                  }
                }
              })]
            }else{
              edit = row.name
            }
            return h('div',[edit])
          }
        },
        {
          title: '年龄',
          key: 'age',
          render:(h,{row,index})=>{
            let editAge;
            if(this.editIndex == index){
              editAge = [h('input',{
                domProps:{
                  value: row.age
                },
                on:{
                  input:(event)=>{
                    this.editAge = event.target.value
                  }
                }
              })]
            }else{
              editAge = row.age
            }
            return h('div',[editAge])
          }
        },
        {
          title: '出⽣⽇期',
          key: 'birthday',
          render: (h, { row, column, index }) => {
            let editBirth;
            if(this.editIndex == index){
              editBirth = [h('input',{
                domProps:{
                  value:row.birthday
                },
                on:{
                  input:(event)=>{
                    this.editBirthday = event.target.value
                  }
                }
              })]
            }else{
            const date = new Date(parseInt(row.birthday))
            const year = date.getFullYear()
            const month = date.getMonth() + 1
            const day = date.getDate()
            editBirth = `${year}-${month}-${day}`
            }
            
            return h('div', [editBirth])
          },
        },
        {
          title: '地址',
          key: 'address',
          render:(h,{row,index})=>{
            let editAddress;
            if(this.editIndex ==index){
              editAddress=[h('input',{
                domProps:{
                  value:row.address
                },
                on:{
                  input:(event)=>{
                    this.editAdress = event.target.value
                  }
                }
              })]
            }else{
                editAddress = row.address 
            }
            return h('div',[editAddress])
          }
        },
        // 先定义操作列的 render:
//         为默认是⾮
// 编辑状态,
// 也就是说 editIndex 还是 -1。当点击修改按钮时,把 render 中
// 第⼆个参数 { row } 中的各列数据赋值给了之前在 data 中声明的
// 4 个数据,这样做是因为之后点击取消按钮时,editName 等值已经
// 修改了,还没有还原,所以在开启编辑状态的同时,初始化各输⼊框
// 的值(当然也可以在取消时重置)。最后再把 editIndex 置为了对
// 应的⾏序号 { index },此时 render 的 if 条件
// this.editIndex === index 为真,编辑列变成了两个按钮:保
// 存和取消。点击保存,直接修改表格源数据 data 中对应的各字段
// 值,并将 editIndex 置为 -1,退出编辑状态;点击取消,不保存源
// 数据,直接退出编辑状态。
        {
          title: '操作',
          render:(h,{row,index})=>{
            //如果当前行是编辑状态,则渲染两个按钮
            if(this.editIndex === index){
              return [
                h('button',{
                  on:{
                    click:()=>{
                      this.data[index].name = this.editName
                      this.data[index].age = this.editAge
                      this.data[index].address = this.editAddress
                      this.data[index].birthday = this.editBirthday
                      this.editIndex = -1
                    }
                  }
                },'保存'),
                h('button',{
                  style:{
                    marginLeft:'6px'
                  },
                  on:{
                    click:()=>{
                      this.editIndex = -1
                    }
                  }
                },'取消')
              ]
            }else{
              // 当前行是默认状态,渲染为一个按钮
              return h('button',{
                on:{
                  click:()=>{
                    this.editName = row.name
                    this.editAge = row.age
                    this.editAddress = row.address
                    this.editBirthday = row.birthday
                    this.editIndex = index
                  }
                }
              },'修改')
            }
          }
        },
      ],
      data: [
        {
          name: '王⼩明',
          age: 18,
          birthday: '919526400000',
          address: '北京市朝阳区芍药居',
        },
        {
          name: '张⼩刚',
          age: 25,
          birthday: '696096000000',
          address: '北京市海淀区⻄⼆旗',
        },
        {
          name: '李⼩红',
          age: 30,
          birthday: '563472000000',
          address: '上海市浦东新区世纪⼤道',
        },
        {
          name: '周⼩伟',
          age: 26,
          birthday: '687024000000',
          address: '深圳市南⼭区深南⼤道',
        },
      ],
    }
  },
}
</script>

<style lang="scss" scoped></style>

*columns ⾥定义的 render,是有两个参数的,第⼀ 个是 createElement(即 h),

  第⼆个是从 render.js 传过来的对象,

它包含了当前⾏数据(row)、当前列配置(column)、当前
是第⼏⾏(index),使⽤者可以基于这 3 个参数得到任意想要的
结果。由于是⾃定义列了,显示什么都是使⽤者决定的,因此在使⽤
了 render 的 column ⾥可以不⽤写字段 key 。*
*columns ⾥定义的 render 字段,它
仅仅是名字叫 render 的⼀个普通函数,并⾮ Vue.js 实例的 render
选项,只是我们恰巧把它叫做 render ⽽已,如果愿意,也可以改为
其它名字,⽐如 renderRow。真正的 Render 函数只有⼀个地⽅,
那就是 render.js 中的 render 选项,只是它代理了 column 中的
render*。

*显示正常的⽇期 写⼀个计算属性(computed) 但对于操作这⼀列就不可取了,因为它带有业
务逻辑,点击编辑按钮,是可以对当前⾏数据进⾏修改的。这时就要 ⽤到 Render 函数。*
先在 src/components/table-render ⽬录下新建 render.js ⽂件
*使⽤ Render ⾃定义列模板
函数式组件 Functional Render 的⽤法,它 没有状态和上下⽂,主要⽤于中转⼀个组件*

// render.js 定义了 4 个 props:
// row:当前⾏的数据;
// column:当前列的数据;
// index:当前是第⼏⾏;
// render:具体的 render 函数内容。
// 这⾥的 render 选项并没有渲染任何节点,⽽是直接返回 props 中
// 定义的 render,并将 h 和当前的⾏、列、序号作为参数传递出去。
// 然后在 table.vue ⾥就可以使⽤ render.js 组件:
export default {
    functional: true,
    props:{
        row:Object,
        column:Object,
        index:Number,
        render:Function
    },
    render:(h,ctx)=>{
        const params = {
            row:ctx.props.row,
            column:ctx.props.column,
            index:ctx.props.index
        };
        return ctx.props.render(h,params)
    }
}

*⼀个完整的 Table 组件功能要复杂的多,⽐如排序、筛选、列
固定、表头固定、表头嵌套等*
**很多 Vue.js 的开发难题,都可以⽤ Render 函数来解决,
它⽐ template 模板更灵活,可以完全发挥 JavaScript 的编程能
⼒,因此很多 JS 的开发思想都可以借鉴。**

实战 6:可⽤ slot-scope ⾃定 义列的表格组件——Table

Render 函数能够完全发挥 JavaScript 的编程能⼒,实
现⼏乎所有的⾃定义⼯作,但本质上,使⽤者写的是⼀个庞⼤的 JS
对象,它不具备 DOM 结构,可读性和可维护性都⽐较差。实现⼀种达到同样渲染效果,但对使⽤者更友好的 slot-scope
写法。
常规的 slot ⽆法实现对组件循环体的每⼀项进⾏不同的内容分发,
这就要⽤到 slot-scope,它本质上跟 slot ⼀样,只不过可以传递参
数。

<ul>
      <li v-for="book in books" :key="book.id">
        <slot :book="book">
          {{book.name}}
        </slot>
      </li>
    </ul>

*在 slot 上,传递了⼀个⾃定义的参数 book,它的值绑定的是当前
循环项的数据 book,这样在⽗级使⽤时,就可以在 slot 中访问它
了:*

 <table-list>
      <template slot-scope="slotProps">
      <span v-if="slotProps.book.scale">限时优惠</span>
      {{slotProps.book.name}}
      </template>
    </table-list>

*除了可以传递参数,其它⽤法跟 slot 是⼀样的,⽐如也可以“具
名”:使⽤ ES6 的解构,能让参数使⽤起来更⽅便*

<slot :book="book" name="book">
      {{ book.name }}
</slot>
    
    
   // ES6 的解构
    <template slot-scope="{ book }" slot="book">
      <span v-if="book.sale">限时优惠</span>
      {{ book.name }}
    </template>

*这就是作⽤域 slot(slot-scope),能够在组件的循环体中做内容
分发,有了它,Table 组件的⾃定义列模板就不⽤写⼀⻓串的
Render 函数了,slot-scope作用域插槽实现了父组件调用子组件内部的数据,子组件的数据通过slot-scope属性传递到了父组件*
⽤ 3 种⽅法来改写 Table,实现 slot-scope ⾃定义列模板
第⼀种⽅案,
⽤最简单的 slot-scope 实现,同时也兼容 Render 函
数的旧⽤法。拷⻉上⼀节的 Table 组件⽬录,更名为 tableslot,同时也拷⻉路由,更名为 table-slot.vue。为了兼容旧
的 Render 函数⽤法,在 columns 的列配置 column 中,新增⼀个
字段 slot 来指定 slot-scope 的名称:

<!-- src/components/table-slot/table.vue -->
<template>
   <table>
     <thead>
       <tr>
         <th v-for="(col,i) in columns" :key="i">
           {{col.title}}
         </th>
       </tr>
     </thead>
     <tbody>
       <tr v-for="(row,index) in data" :key="index">
         <td v-for="(col,inx) in columns" :key="inx">
           <template v-if="'render' in col">
             <Render :row="row" :column="col" :index="rowIndex" :render="col.render">
             </Render>
           </template>
            <template v-else-if="'slot' in col">
             <slot :index="rowIndex" :name="row.slot" :column="col">  
             </slot>
           </template>
           <template v-else>
              {{row[col.key]}}             
           </template>
         </td>
       </tr>
     </tbody>
   </table>
</template>

<script>

import Render from './render.js'
  export default {
    props:{
      columns:{
        type:Array,
        default(){
          return []
        }
      },
      data:{
        type:Array,
        default(){
          return[]
        }
      }
    },
    components:{
      Render
    }
  }
</script>

<style lang="scss" scoped>
  table{
    width:100%;
    border-collapse:collapse;
    border-spacing:0;
    empty-cells:show;//隐藏表格中空单元格上的边框和背景:
    border:1px solid #e9e9e9;
  }
  table th{
    background: #f7f7f7;
    color:#5c6b77;
    font-weight:600;
    white-space:nowrap;//段落中的文本不进行换行
  }
  table td, table th{
    padding:8px 16px;
    border: 1px solid #e9e9e9;
    text-align:center;
  }
</style>

*相⽐原先的⽂件,只在 'render' in col 的条件下新加了⼀个
template 的标签,如果使⽤者的 column 配置了 render 字段,
就优先以 Render 函数渲染,然后再判断是否⽤ slot-scope 渲染。
在定义的作⽤域 slot 中,将⾏数据 row、列数据 column 和第⼏⾏
index 作为 slot 的参数,并根据 column 中指定的 slot 字段值,
动态设置了具名 name。使⽤者在配置 columns 时,只要指定了某
⼀列的 slot,那就可以在 Table 组件中使⽤ slot-scope。我们以上
⼀节的可编辑整⾏数据为例,⽤ slot-scope 的写法实现完全⼀样的
效果:*

<!-- src/views/table-slot.vue -->
<template>
  <div>
    <table-slot :columns="columns" :data="data">
      <template slot-scope="{ row, index }" slot="name">
        <input type="text" v-model="editName" v-if="editIndex === index" />
        <span v-else>{{ row.name }}</span>
      </template>
      <template slot-scope="{ row, index }" slot="age">
        <input type="text" v-model="editAge" v-if="editIndex === index" />
        <span v-else>{{ row.age }}</span>
      </template>
      <template slot-scope="{ row, index }" slot="birthday">
        <input type="text" v-model="editBirthday" v-if="editIndex === index" />
        <span v-else>{{ getBirthday(row.birthday) }}</span>
      </template>
      <tempalte slot-scope="{ row, index }" slot="address">
        <input type="text" v-model="editAddress" v-if="editIndex === index" />
        <span v-else>{{ row.address }}</span>
      </tempalte>
      <template slot-scope="{ row, index }" slot="action">
        <div v-if="editIndex === index">
          <button @click="handleSave(index)">保存</button>
          <button @click="editIndex = -1">取消</button>
        </div>
        <div v-else>
          <button @click="handleEdit(row, index)">操作</button>
        </div>
      </template>
    </table-slot>
  </div>
</template>

<script>
import TableSlot from './table.vue'
export default {
  components: {
    TableSlot,
  },
  data() {
    return {
      editName: '', // 第⼀列输⼊框
      editAge: '', // 第⼆列输⼊框
      editBirthday: '', // 第三列输⼊框
      editAddress: '', // 第四列输⼊框
      editIndex: -1, // 当前聚焦的输⼊框的⾏数
      columns: [
        {
          title: '姓名',
          slot: 'name',
        },
        {
          title: '年龄',
          slot: 'age',
        },
        {
          title: '出⽣⽇期',
          slot: 'birthday',
        },
        {
          title: '地址',
          slot: 'address',
        },
        {
          title: '操作',
          slot: 'action',
        },
      ],
      data: [
        {
          name: '王⼩明',
          age: 18,
          birthday: '919526400000',
          address: '北京市朝阳区芍药居',
        },
        {
          name: '张⼩刚',
          age: 25,
          birthday: '696096000000',
          address: '北京市海淀区⻄⼆旗',
        },
        {
          name: '李⼩红',
          age: 30,
          birthday: '563472000000',
          address: '上海市浦东新区世纪⼤道',
        },
        {
          name: '周⼩伟',
          age: 26,
          birthday: '687024000000',
          address: '深圳市南⼭区深南⼤道',
        },
      ],
    }
  },
  methods: {
      handleEdit(row,index) {
          this.editName = row.name
          this.editAge = row.age
          this.editAddress = row.address
          this.editBirthday = row.birthday
          this.editIndex = index
      },
      handleSave(index){
          this.data[index].name = this.editBirthday
          this.data[index].age = this.editAge
          this.data[index].address = this.editAddress
          this.data[index].birtyhday = this.editBirthday
          this.editIndex = -1
      },
      getBirthday(birthday){
          const date = new Date(parseInt(birthday))
          const year = date.getFullYear()
          const month = date.getMonth()+1
          const day = date.getDate()
          return `${year}-${month}-${day}`
      }
  },
}
</script>

<style lang="scss" scoped></style>

*<table-slot> 内的每⼀个 <template> 就对应某⼀列
的 slot-scope 模板,通过配置的 slot 字段,指定具名的 slotscope。可以看到,基本是把 Render 函数还原成了 html 的写法,
这样看起来直接多了,渲染效果是完全⼀样的。在 slot-scope 中,
平时怎么写组件,这⾥就怎么写,Vue.js 所有的 API 都是可以直接
使⽤的。*

第⼆种⽅案
不需要修改原先的 Table 组件代码,只是在使⽤层⾯
修改即可。先来看具体的使⽤代码,然后再做分析。注意,这⾥使⽤
的 Table 组件,仍然是上⼀节 src/components/table-render
的组件,它只有 Render 函数,没有定义 slot-scope:

<!-- src/views/table-render.vue -->
<template>
  <div>
    <table-render ref="table" :columns="columns" :data="data">
      <template slot-scope="{ row, index }" slot="name">
        <input type="text" v-model="editName" v-if="editIndex === index" />
        <span v-else>{{ row.name }}</span>
      </template>
      <template slot-scope="{ row, index }" slot="age">
        <input type="text" v-model="editAge" v-if="editIndex === index" />
        <span v-else>{{ row.age }}</span>
      </template>
      <template slot-scope="{ row, index }" slot="birthday">
        <input type="text" v-model="editBirthday" v-if="editIndex === index" />
        <span v-else>{{ getBirthday(row.birthday) }}</span>
      </template>
      <tempalte slot-scope="{ row, index }" slot="address">
        <input type="text" v-model="editAddress" v-if="editIndex === index" />
        <span v-else>{{ row.address }}</span>
      </tempalte>
      <template slot-scope="{ row, index }" slot="action">
        <div v-if="editIndex === index">
          <button @click="handleSave(index)">保存</button>
          <button @click="editIndex = -1">取消</button>
        </div>
        <div v-else>
          <button @click="handleEdit(row, index)">操作</button>
        </div>
      </template>
    </table-render>
  </div>
</template>

<script>
import TableRender from './table.vue'
export default {
  components: {
    TableRender,
  },
  data() {
    return {
      editName: '', // 第⼀列输⼊框
      editAge: '', // 第⼆列输⼊框
      editBirthday: '', // 第三列输⼊框
      editAddress: '', // 第四列输⼊框
      editIndex: -1, // 当前聚焦的输⼊框的⾏数
      columns: [
        {
          title: '姓名',
          render:(h,{row,column,index})=>{
            //   $scopedSlots获取到父组件中创建的插槽 向插槽内传递参数   name插槽传递
              return h('div',this.$refs.table.$scopedSlots.name({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '年龄',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots.age({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '出⽣⽇期',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots.birthday({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '地址',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots.address({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '操作',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
      ],
      data: [
        {
          name: '王⼩明',
          age: 18,
          birthday: '919526400000',
          address: '北京市朝阳区芍药居',
        },
        {
          name: '张⼩刚',
          age: 25,
          birthday: '696096000000',
          address: '北京市海淀区⻄⼆旗',
        },
        {
          name: '李⼩红',
          age: 30,
          birthday: '563472000000',
          address: '上海市浦东新区世纪⼤道',
        },
        {
          name: '周⼩伟',
          age: 26,
          birthday: '687024000000',
          address: '深圳市南⼭区深南⼤道',
        },
      ],
    }
  },
  methods: {
      handleEdit(row,index) {
          this.editName = row.name
          this.editAge = row.age
          this.editAddress = row.address
          this.editBirthday = row.birthday
          this.editIndex = index
      },
      handleSave(index){
          this.data[index].name = this.editBirthday
          this.data[index].age = this.editAge
          this.data[index].address = this.editAddress
          this.data[index].birtyhday = this.editBirthday
          this.editIndex = -1
      },
      getBirthday(birthday){
          const date = new Date(parseInt(birthday))
          const year = date.getFullYear()
          const month = date.getMonth()+1
          const day = date.getDate()
          return `${year}-${month}-${day}`
      }
  },
}
</script>

<style lang="scss" scoped></style>

在 slot-scope 的使⽤上(即 template 的内容),与⽅案⼀是完全
⼀致的,可以看到,在 column 的定义上,仍然使⽤了 render 字
段,只不过每个 render 都渲染了⼀个 div 节点,⽽这个 div 的内
容,是指定来在 <table-render> 中定义的 slot-scope:

<!-- src/views/table-render.vue -->
<template>
  <div>
    <table-render ref="table" :columns="columns" :data="data">
      <template slot-scope="{ row, index }" slot="name">
        <input type="text" v-model="editName" v-if="editIndex === index" />
        <span v-else>{{ row.name }}</span>
      </template>
      <template slot-scope="{ row, index }" slot="age">
        <input type="text" v-model="editAge" v-if="editIndex === index" />
        <span v-else>{{ row.age }}</span>
      </template>
      <template slot-scope="{ row, index }" slot="birthday">
        <input type="text" v-model="editBirthday" v-if="editIndex === index" />
        <span v-else>{{ getBirthday(row.birthday) }}</span>
      </template>
      <tempalte slot-scope="{ row, index }" slot="address">
        <input type="text" v-model="editAddress" v-if="editIndex === index" />
        <span v-else>{{ row.address }}</span>
      </tempalte>
      <template slot-scope="{ row, index }" slot="action">
        <div v-if="editIndex === index">
          <button @click="handleSave(index)">保存</button>
          <button @click="editIndex = -1">取消</button>
        </div>
        <div v-else>
          <button @click="handleEdit(row, index)">操作</button>
        </div>
      </template>
    </table-render>
  </div>
</template>

<script>
import TableRender from './table.vue'
export default {
  components: {
    TableRender,
  },
  data() {
    return {
      editName: '', // 第⼀列输⼊框
      editAge: '', // 第⼆列输⼊框
      editBirthday: '', // 第三列输⼊框
      editAddress: '', // 第四列输⼊框
      editIndex: -1, // 当前聚焦的输⼊框的⾏数
      columns: [
        {
          title: '姓名',
          render:(h,{row,column,index})=>{
            //   $scopedSlots获取到父组件中创建的插槽 插槽内容放在div标签中,向插槽内传递参数   name插槽传递
              return h('div',this.$refs.table.$scopedSlots.name({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '年龄',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots.age({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '出⽣⽇期',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots.birthday({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '地址',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots.address({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
        {
          title: '操作',
          render:(h,{row,column,index})=>{
              return h('div',this.$refs.table.$scopedSlots({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },
      ],
      data: [
        {
          name: '王⼩明',
          age: 18,
          birthday: '919526400000',
          address: '北京市朝阳区芍药居',
        },
        {
          name: '张⼩刚',
          age: 25,
          birthday: '696096000000',
          address: '北京市海淀区⻄⼆旗',
        },
        {
          name: '李⼩红',
          age: 30,
          birthday: '563472000000',
          address: '上海市浦东新区世纪⼤道',
        },
        {
          name: '周⼩伟',
          age: 26,
          birthday: '687024000000',
          address: '深圳市南⼭区深南⼤道',
        },
      ],
    }
  },
  methods: {
      handleEdit(row,index) {
          this.editName = row.name
          this.editAge = row.age
          this.editAddress = row.address
          this.editBirthday = row.birthday
          this.editIndex = index
      },
      handleSave(index){
          this.data[index].name = this.editBirthday
          this.data[index].age = this.editAge
          this.data[index].address = this.editAddress
          this.data[index].birtyhday = this.editBirthday
          this.editIndex = -1
      },
      getBirthday(birthday){
          const date = new Date(parseInt(birthday))
          const year = date.getFullYear()
          const month = date.getMonth()+1
          const day = date.getDate()
          return `${year}-${month}-${day}`
      }
  },
}
</script>

<style lang="scss" scoped></style>

在 slot-scope 的使⽤上(即 template 的内容),与⽅案⼀是完全
⼀致的,可以看到,在 column 的定义上,仍然使⽤了 render 字
段,只不过每个 render 都渲染了⼀个 div 节点,⽽这个 div 的内
容,是指定来在 <table-render> 中定义的 slot-scope:

 {
          title: '姓名',
          render:(h,{row,column,index})=>{
            //   $scopedSlots获取到父组件中创建的插槽 插槽内容放在div标签中,向插槽内传递参数   name插槽传递
              return h('div',this.$refs.table.$scopedSlots.name({
                  row:row,
                  column:column,
                  index:index
              }))
          }
        },

*name 这⼀列仍然是使⽤ Functional Render,只不过 Render 的是⼀个预先定义好的slot-scope 模板
有⼀点需要注意的是,示例中的 data 默认是空数组,⽽在
mounted ⾥才赋值的,是因为这样定义的 slot-scope,初始时读
取 this.$refs.table.$scopedSlots 是读不到的,会报错,当
没有数据时,也就不会去渲染,也就避免了报错。
这种⽅案虽然可⾏,但归根到底是⼀种 hack,不是⾮常推荐*

⽅案三
第 3 中⽅案的思路和第 2 种是⼀样的,它介于⽅案 1 与⽅案 2 之
间。这种⽅案要修改 Table 组件代码,但是⽤例与⽅案 1 完全⼀
致。
在⽅案 2 中,我们是通过修改⽤例使⽤ slot-scope 的,也就是说
Table 组件本身没有⽀持 slot-scope,是我们“强加”上去的,如果
把强加的部分,集成到 Table 内,那对使⽤者就很友好了,同时也
避免了初始化报错,不得不把 data 写在 mounted 的问题。
保持⽅案 1 的⽤例不变,修改 src/components/table-render
中的代码。为了同时兼容 Render 与 slot-scope,我们在 tablerender 下新建⼀个 slot.js 的⽂件:

export default {
    functional:true,
    inject:['tableRoot'],
    props:{
        row:Object,
        column:Object,
        index:Number
    },
    render:(h,ctx)=>{
        return h('div',ctx.inject.tableRoot.$scopedSlots[ctx.props.column.slot]{
            row:ctx.props.row,
            column:ctx.props.column,
            index:ctx.props.index
        })
    }
}

它仍然是⼀个 Functional Render,使⽤ inject 注⼊了⽗级组件
table.vue(下⽂改写) 中提供的实例 tableRoot。在 render
⾥,也是通过⽅案 2 中使⽤ $scopedSlots 定义的 slot,不过这
是在组件级别定义,对⽤户来说是透明的,只要按⽅案 1 的⽤例来
写就可以了。
table.vue 也要做⼀点修改:

<template>
   <table>
     <thead>
       <tr>
         <th v-for="(col,i) in columns" :key="i">
           {{col.title}}
         </th>
       </tr>
     </thead>
     <tbody>
       <tr v-for="(row,rowIndex) in data" :key="rowIndex">
         <td v-for="(col,inx) in columns" :key="inx">
           <template v-if="'render' in col">
             <Render :row="row" :column="col" :index="rowIndex" :render="col.render">
             </Render>
           </template>
            <template v-else-if="'slot' in col">
             <slot-scope :index="rowIndex" :name="row.slot" :column="col">  
             </slot-scope>
           </template>
           <template v-else>
              {{row[col.key]}}             
           </template>
         </td>
       </tr>
     </tbody>
   </table>
</template>

<script>

import Render from './render.js'
import SlotScope from './slot.js'
  export default {
    provide(){
      return {
        tableRoot:this
      }
    },
    props:{
      columns:{
        type:Array,
        default(){
          return []
        }
      },
      data:{
        type:Array,
        default(){
          return[]
        }
      }
    },
    components:{
      Render,
      SlotScope
    }
  }
</script>

<style lang="scss" scoped>
  table{
    width:100%;
    border-collapse:collapse;
    border-spacing:0;
    empty-cells:show;//隐藏表格中空单元格上的边框和背景:
    border:1px solid #e9e9e9;
  }
  table th{
    background: #f7f7f7;
    color:#5c6b77;
    font-weight:600;
    white-space:nowrap;//段落中的文本不进行换行
  }
  table td, table th{
    padding:8px 16px;
    border: 1px solid #e9e9e9;
    text-align:center;
  }
</style>

*因为 slot-scope 模板是写在 table.vue 中的(对使⽤者来说,相当
于写在组件 <table-slot></table-slot> 之间),所以在
table.vue 中使⽤ provide 向下提供了 Table 的实例,这样在
slot.js 中就可以通过 inject 访问到它,继⽽通过 $scopedSlots
获取到 slot。需要注意的是,在 Functional Render 是没有 this 上
下⽂的,都是通过 h 的第⼆个参数临时上下⽂ ctx 来访问 prop、
inject 等的。
⽅案 3 也是推荐使⽤的,当 Table 的功能⾜够复杂,层级会嵌套的
⽐较深,那时⽅案 1 的 slot 就不会定义在第⼀级组件中,中间可能
会隔许多组件,slot 就要⼀层层中转,相⽐在任何地⽅都能直接使
⽤的 Render 就要麻烦了。所以,如果你的组件层级简单,推荐⽤第
⼀种⽅案;如果你的组件已经成型(某 API 基于 Render 函数),
但⼀时间不⽅便⽀持 slot-scope,⽽使⽤者⼜想⽤,那就选⽅案
2;如果你的组件已经成型(某 API 基于 Render 函数),但组件层
级复杂,要按⽅案 1 那样⽀持 slot-scope 可能改动较⼤,还有可
能带来新的 bug,那就⽤⽅案 3,它不会破坏原有的任何内容,但
会额外⽀持 slot-scope ⽤法,关键是改动简单*。

理论上,绝⼤多数能⽤ Render 的地⽅,都可以⽤ slot-scope

动态组件

*根据⼀些条件,动态地切换某个组件,或动态地选择渲染某个组件
使⽤ Render 或 Functional Render 可以解决动
态切换组件的需求,不过那是基于⼀个 JS 对象(Render 函数),
⽽ Vue.js 提供了另外⼀个内置的组件 <component> 和 is 特性,
可以更好地实现动态组件*
先来看⼀个 <component> 和 is 的基本示例,⾸先定义三个普通组
件:

<!--
 * @Author: yang
 * @Date: 2020-10-28 17:45:13
 * @LastEditors: yang
 * @LastEditTime: 2020-10-28 17:53:43
 * @FilePath: \gloud-h5\src\component\index\a.vue
-->
<template>
    <div>
        a组件
    </div>
</template>

<script>
    export default {
        beforeDestroy () {
            console.log('组件销毁');
        },
        mounted () {
            console.log('组件创建');
        },
    }
</script>

<style lang="scss" scoped>

</style>
<!--
 * @Author: yang
 * @Date: 2020-10-28 17:45:17
 * @LastEditors: yang
 * @LastEditTime: 2020-10-28 17:45:46
 * @FilePath: \gloud-h5\src\component\index\b.vue
-->
<template>
    <div>
        b组件
    </div>
</template>

<script>
    export default {
        
    }
</script>

<style lang="scss" scoped>

</style>
<!--
 * @Author: yang
 * @Date: 2020-10-28 17:45:22
 * @LastEditors: yang
 * @LastEditTime: 2020-10-28 17:45:54
 * @FilePath: \gloud-h5\src\component\index\c.vue
-->
<template>
    <div>
        c组件
    </div>
</template>

<script>
    export default {
        
    }
</script>

<style lang="scss" scoped>

</style>

然后在⽗组件中导⼊这 3 个组件,并动态切换:
只要切换到 A 组件,mounted 就会触发⼀次,切换到其它组
件,beforeDestroy 也会触发⼀次,说明组件再重新渲染,这样
有可能导致性能问题。为了避免组件的重复渲染,可以在
<component> 外层套⼀个 Vue.js 内置的 <keep-alive> 组件,
这样,组件就会被缓存起来:
*keep-alive 还有⼀些额外的 props 可以配置:
include:字符串或正则表达式。只有名称匹配的组件会被缓
存。
exclude:字符串或正则表达式。任何名称匹配的组件都不会
被缓存。
max:数字。最多可以缓存多少组件实例。*

<template>
    <div>
        <button @click="handleChange('a')">显示组件a</button>
        <button @click="handleChange('b')">显示组件b</button>
        <button @click="handleChange('c')">显示组件c</button>
        <keep-alive>
        <component :is="component"></component>
        </keep-alive>
    </div>
</template>

<script>
import a from './a.vue';
import b from './b.vue';
import c from './c.vue';
    export default {
        data() {
            return {
                component: a
            }
        },
        methods: {
            handleChange(item) {
                switch(item){
                    case 'a':
                        this.component = a
                        break;
                     case 'b':
                        this.component = b
                        break;
                         case 'c':
                        this.component = c
                        break;
                }
            }
        },
    }
</script>

<style lang="scss" scoped>

</style

这⾥的 is 动态绑定的是⼀个组件对象(Object),它直接指向 a /
b / c 三个组件中的⼀个

动态渲染标签

除了直接绑定⼀个 Object,还可以是⼀ 个 String,⽐如标签名、组件名。下⾯的这个组件,将原⽣的按钮 button 进⾏了封装,如果传⼊了 prop: to,那它会渲染为⼀个 标签,⽤于打开这个链接地址,如果没有传⼊ to,就当作普通 button 使⽤。来看下⾯的示例:

button.vue

<template>
    <component :is="tagName" v-bind="tagProps">
        <slot></slot>
    </component>
</template>

<script>
    export default {
        props:{
            to:{
                type:String,
                default:""
            },
            target:{
                type:String,
                default:'_self'
            }
        },
        computed: {
            // 动态渲染不同的标签
            tagName() {
                return this.to === ''?'button':'a'
            },
            // 如果是链接,把这些属性绑定到component上
            tagProps(){
                let props = {}
                if(this.to){
                    props={
                        target:this.target,
                        href:this.to
                    }
                }
                return props
            }
        },
    }
</script>

<style lang="scss" scoped>

</style>

使用

<template>
    <div>
        <i-button>普通按钮</i-button>
        <i-button to="http://www.baidu.com">链接按钮</i-button>
        <i-button target="_bank" to="http://www.baidu.com">新窗口打开链接</i-button>

    </div>
</template>

<script>
import iButton from './button.vue';
    export default {
        components: {
            iButton,
        },
    }
</script>

<style lang="scss" scoped>

</style>

i-button 组件中的 <component> is 绑定的就是⼀个标签名称
button / a,并且通过 v-bind 将⼀些额外的属性全部绑定到了
<component> 上。

查看原文

赞 1 收藏 1 评论 0

yang 收藏了文章 · 10月26日

全局gif组件

gif-loading.js


(function(root, factory) {

    if (typeof define === 'function' && define.amd) {
        define(factory);
    } else if (typeof exports === 'object') {
        module.exports = factory();
    } else {
        root.GIFLoading = factory();
    }

})(this, function() {
    var GIFLoading = {};

    GIFLoading.version = '0.2.0';
    
   //配置图片的地方
    var loadingDefault = require('../images/loading.gif')
    var Settings = GIFLoading.settings = {
        minimum: 0.08,
        easing: 'linear',
        positionUsing: '',
        speed: 200,
        trickle: true,
        trickleSpeed: 200,
        showSpinner: true,
        barSelector: '[role="bar"]',
        spinnerSelector: '[role="spinner"]',
        parent: 'body',
        template:`<div class="gif-loading"><div class="gif-loading-bg"></div><div class="gif-loading-main"><img data-original="${loadingDefault}" alt=""></div></div>`
        // template: '<div class="bar" role="bar"><div class="peg"></div></div><div class="spinner" role="spinner"><div class="spinner-icon"></div></div>'
    };

    /**
     * Updates configuration.
     *
     *     GIFLoading.configure({
   *       minimum: 0.1
   *     });
     */
    GIFLoading.configure = function(options) {
        var key, value;
        for (key in options) {
            value = options[key];
            if (value !== undefined && options.hasOwnProperty(key)) Settings[key] = value;
        }

        return this;
    };

    /**
     * Last number.
     */

    GIFLoading.status = null;

    /**
     * Sets the progress bar status, where `n` is a number from `0.0` to `1.0`.
     *
     *     GIFLoading.set(0.4);
     *     GIFLoading.set(1.0);
     */

    GIFLoading.set = function(n) {
        var started = GIFLoading.isStarted();

        n = clamp(n, Settings.minimum, 1);
        GIFLoading.status = (n === 1 ? null : n);

        var progress = GIFLoading.render(!started),
            bar      = progress.querySelector(Settings.barSelector),
            speed    = Settings.speed,
            ease     = Settings.easing;

        progress.offsetWidth; /* Repaint */

        queue(function(next) {
            // Set positionUsing if it hasn't already been set
            if (Settings.positionUsing === '') Settings.positionUsing = GIFLoading.getPositioningCSS();

            // Add transition
            // css(bar, barPositionCSS(n, speed, ease));

            if (n === 1) {
                // Fade out
                // css(progress, {
                //     transition: 'none',
                //     opacity: 1
                // });
                progress.offsetWidth; /* Repaint */

                setTimeout(function() {
                    // css(progress, {
                    //     transition: 'all ' + speed + 'ms linear',
                    //     opacity: 0
                    // });
                    setTimeout(function() {
                        GIFLoading.remove();
                        next();
                    }, speed);
                }, speed);
            } else {
                setTimeout(next, speed);
            }
        });

        return this;
    };

    GIFLoading.isStarted = function() {
        return typeof GIFLoading.status === 'number';
    };

    /**
     * Shows the progress bar.
     * This is the same as setting the status to 0%, except that it doesn't go backwards.
     *
     *     GIFLoading.start();
     *
     */
    GIFLoading.start = function() {
        if (!GIFLoading.status) GIFLoading.set(0);

        var work = function() {
            setTimeout(function() {
                if (!GIFLoading.status) return;
                GIFLoading.trickle();
                work();
            }, Settings.trickleSpeed);
        };

        if (Settings.trickle) work();

        return this;
    };

    /**
     * Hides the progress bar.
     * This is the *sort of* the same as setting the status to 100%, with the
     * difference being `done()` makes some placebo effect of some realistic motion.
     *
     *     GIFLoading.done();
     *
     * If `true` is passed, it will show the progress bar even if its hidden.
     *
     *     GIFLoading.done(true);
     */

    GIFLoading.done = function(force) {
        if (!force && !GIFLoading.status) return this;

        return GIFLoading.inc(0.3 + 0.5 * Math.random()).set(1);
    };

    /**
     * Increments by a random amount.
     */

    GIFLoading.inc = function(amount) {
        var n = GIFLoading.status;

        if (!n) {
            return GIFLoading.start();
        } else if(n > 1) {
            return;
        } else {
            if (typeof amount !== 'number') {
                if (n >= 0 && n < 0.2) { amount = 0.1; }
                else if (n >= 0.2 && n < 0.5) { amount = 0.04; }
                else if (n >= 0.5 && n < 0.8) { amount = 0.02; }
                else if (n >= 0.8 && n < 0.99) { amount = 0.005; }
                else { amount = 0; }
            }

            n = clamp(n + amount, 0, 0.994);
            return GIFLoading.set(n);
        }
    };

    GIFLoading.trickle = function() {
        return GIFLoading.inc();
    };

    /**
     * Waits for all supplied jQuery promises and
     * increases the progress as the promises resolve.
     *
     * @param $promise jQUery Promise
     */
    (function() {
        var initial = 0, current = 0;

        GIFLoading.promise = function($promise) {
            if (!$promise || $promise.state() === "resolved") {
                return this;
            }

            if (current === 0) {
                GIFLoading.start();
            }

            initial++;
            current++;

            $promise.always(function() {
                current--;
                if (current === 0) {
                    initial = 0;
                    GIFLoading.done();
                } else {
                    GIFLoading.set((initial - current) / initial);
                }
            });

            return this;
        };

    })();

    /**
     * (Internal) renders the progress bar markup based on the `template`
     * setting.
     */

    GIFLoading.render = function(fromStart) {
        if (GIFLoading.isRendered()) return document.getElementById('GIFLoading');

        addClass(document.documentElement, 'GIFLoading-busy');

        var progress = document.createElement('div');
        progress.id = 'GIFLoading';
        progress.innerHTML = Settings.template;

        var bar      = progress.querySelector(Settings.barSelector),
            perc     = fromStart ? '-100' : toBarPerc(GIFLoading.status || 0),
            parent   = document.querySelector(Settings.parent),
            spinner;

        // css(bar, {
        //     transition: 'all 0 linear',
        //     transform: 'translate3d(' + perc + '%,0,0)'
        // });

        if (!Settings.showSpinner) {
            spinner = progress.querySelector(Settings.spinnerSelector);
            spinner && removeElement(spinner);
        }

        if (parent != document.body) {
            addClass(parent, 'GIFLoading-custom-parent');
        }

        parent.appendChild(progress);
        return progress;
    };

    /**
     * Removes the element. Opposite of render().
     */

    GIFLoading.remove = function() {
        removeClass(document.documentElement, 'GIFLoading-busy');
        removeClass(document.querySelector(Settings.parent), 'GIFLoading-custom-parent');
        var progress = document.getElementById('GIFLoading');
        progress && removeElement(progress);
    };

    /**
     * Checks if the progress bar is rendered.
     */

    GIFLoading.isRendered = function() {
        return !!document.getElementById('GIFLoading');
    };

    /**
     * Determine which positioning CSS rule to use.
     */

    GIFLoading.getPositioningCSS = function() {
        // Sniff on document.body.style
        var bodyStyle = document.body.style;

        // Sniff prefixes
        var vendorPrefix = ('WebkitTransform' in bodyStyle) ? 'Webkit' :
            ('MozTransform' in bodyStyle) ? 'Moz' :
                ('msTransform' in bodyStyle) ? 'ms' :
                    ('OTransform' in bodyStyle) ? 'O' : '';

        if (vendorPrefix + 'Perspective' in bodyStyle) {
            // Modern browsers with 3D support, e.g. Webkit, IE10
            return 'translate3d';
        } else if (vendorPrefix + 'Transform' in bodyStyle) {
            // Browsers without 3D support, e.g. IE9
            return 'translate';
        } else {
            // Browsers without translate() support, e.g. IE7-8
            return 'margin';
        }
    };

    /**
     * Helpers
     */

    function clamp(n, min, max) {
        if (n < min) return min;
        if (n > max) return max;
        return n;
    }

    /**
     * (Internal) converts a percentage (`0..1`) to a bar translateX
     * percentage (`-100%..0%`).
     */

    function toBarPerc(n) {
        return (-1 + n) * 100;
    }


    /**
     * (Internal) returns the correct CSS for changing the bar's
     * position given an n percentage, and speed and ease from Settings
     */

    function barPositionCSS(n, speed, ease) {
        var barCSS;

        if (Settings.positionUsing === 'translate3d') {
            barCSS = { transform: 'translate3d('+toBarPerc(n)+'%,0,0)' };
        } else if (Settings.positionUsing === 'translate') {
            barCSS = { transform: 'translate('+toBarPerc(n)+'%,0)' };
        } else {
            barCSS = { 'margin-left': toBarPerc(n)+'%' };
        }

        barCSS.transition = 'all '+speed+'ms '+ease;

        return barCSS;
    }

    /**
     * (Internal) Queues a function to be executed.
     */

    var queue = (function() {
        var pending = [];

        function next() {
            var fn = pending.shift();
            if (fn) {
                fn(next);
            }
        }

        return function(fn) {
            pending.push(fn);
            if (pending.length == 1) next();
        };
    })();

    /**
     * (Internal) Applies css properties to an element, similar to the jQuery
     * css method.
     *
     * While this helper does assist with vendor prefixed property names, it
     * does not perform any manipulation of values prior to setting styles.
     */

    var css = (function() {
        var cssPrefixes = [ 'Webkit', 'O', 'Moz', 'ms' ],
            cssProps    = {};

        function camelCase(string) {
            return string.replace(/^-ms-/, 'ms-').replace(/-([\da-z])/gi, function(match, letter) {
                return letter.toUpperCase();
            });
        }

        function getVendorProp(name) {
            var style = document.body.style;
            if (name in style) return name;

            var i = cssPrefixes.length,
                capName = name.charAt(0).toUpperCase() + name.slice(1),
                vendorName;
            while (i--) {
                vendorName = cssPrefixes[i] + capName;
                if (vendorName in style) return vendorName;
            }

            return name;
        }

        function getStyleProp(name) {
            name = camelCase(name);
            return cssProps[name] || (cssProps[name] = getVendorProp(name));
        }

        function applyCss(element, prop, value) {
            prop = getStyleProp(prop);
            element.style[prop] = value;
        }

        return function(element, properties) {
            var args = arguments,
                prop,
                value;

            if (args.length == 2) {
                for (prop in properties) {
                    value = properties[prop];
                    if (value !== undefined && properties.hasOwnProperty(prop)) applyCss(element, prop, value);
                }
            } else {
                applyCss(element, args[1], args[2]);
            }
        }
    })();

    /**
     * (Internal) Determines if an element or space separated list of class names contains a class name.
     */

    function hasClass(element, name) {
        var list = typeof element == 'string' ? element : classList(element);
        return list.indexOf(' ' + name + ' ') >= 0;
    }

    /**
     * (Internal) Adds a class to an element.
     */

    function addClass(element, name) {
        var oldList = classList(element),
            newList = oldList + name;

        if (hasClass(oldList, name)) return;

        // Trim the opening space.
        element.className = newList.substring(1);
    }

    /**
     * (Internal) Removes a class from an element.
     */

    function removeClass(element, name) {
        var oldList = classList(element),
            newList;

        if (!hasClass(element, name)) return;

        // Replace the class name.
        newList = oldList.replace(' ' + name + ' ', ' ');

        // Trim the opening and closing spaces.
        element.className = newList.substring(1, newList.length - 1);
    }

    /**
     * (Internal) Gets a space separated list of the class names on the element.
     * The list is wrapped with a single space on each end to facilitate finding
     * matches within the list.
     */

    function classList(element) {
        return (' ' + (element && element.className || '') + ' ').replace(/\s+/gi, ' ');
    }

    /**
     * (Internal) Removes an element from the DOM.
     */

    function removeElement(element) {
        element && element.parentNode && element.parentNode.removeChild(element);
    }

    return GIFLoading;
});

使用

import GIFLoading from '../js/gif-loading'
// 请求拦截器

axios.interceptors.request.use(
  (config) => {
   isShowLoading && GIFLoading.start()
  })
  
  
  响应拦截器
axios.interceptors.response.use(
  (response) => {
    isShowLoading && GIFLoading.done()
  },(error) => {
    // 对响应错误做点什么
    // NProgress.done();
    GIFLoading.done()
    )
查看原文

yang 收藏了文章 · 10月26日

vue组件略佳操作

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 个参数,分别是:

  1. 要渲染的元素或组件,可以是⼀个 html 标签、组件选项或⼀ 个函数(不常⽤),该参数为必填项。示例:
 // 1. html 标签 

 h('div'); 

 // 2. 组件选项

 import DatePicker from '../component/date- picker.vue'; h(DatePicker);
  1. 对应属性的数据对象,⽐如组件的 props、元素的 class、绑 定的事件、slot、⾃定义指令等,

该参数是可选的,上⽂所说 的 Render 配置项多,指的就是这个参数。

该参数的完整配置 和示例,可以到 Vue.js 的⽂档查看,没必要全部记住,

⽤到 时查阅就好:createElement 参数 (https://cn.vuejs.org/v2/guide... function.html#createElement-参数)。

  1. ⼦节点,可选,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 点

  1. 使⽤两个相同 slot。在 template 中,Vue.js 不允许使⽤两个 相同的 slot,⽐如下⾯的示例是错误的:
<template> 

 <div> 

 <slot></slot> 

 <slot></slot>

 </div> 

 </template>

解决⽅案就是上⽂中讲到的约束,使⽤⼀个深度克隆 VNode 节点的⽅法

  1. 在 SSR 环境(服务端渲染),如果不是常规的 template 写 法,⽐如通过 Vue.extend 和 new Vue 构造来⽣成的组件实 例,是编译不过的,使⽤ Render 函 数来渲染
  2. 在 runtime 版本的 Vue.js 中,如果使⽤ Vue.extend ⼿动构 造⼀个实例,使⽤ template 选项是会报错的,解决⽅案也很简单,把 template 改写为 Render 就可以了。需要注意的是,在开发独⽴组件时,可以通过配置 Vue.js 版本来使 template 选项可⽤,但这是在⾃⼰的环境, ⽆法保证使⽤者的 Vue.js 版本,所以对于提供给他⼈⽤的组 件,是需要考虑兼容 runtime 版本和 SSR 环境的。
  3. 这可能是使⽤ 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 函数就很⽅便了.

查看原文

yang 收藏了文章 · 10月26日

vue中H5的progress标签仿写一个进度条和逻辑

要求:

*载入:
0~5秒,每秒增加8%进度;
5~10秒,每秒增加4%进度;
10~20秒,每秒增加2%进度;
超过20秒,每秒增加0.2%进度,最多不超过95%*

<template>
    <progress :value="progressValue" max="100" class="pr"></progress>
</template>
<script>
data(){
    return{
    progressValue: 0,
}
}
methods:{
     // 进度条的控制
    getProgress() {
      let num = 0;
      let progress = 0.0;
      this.firstTime = setInterval(() => {
        num += 1;
        if (num <= 5) {
          progress += 8;
        } else if (num <= 10) {
          progress += 4;
        } else if (num <= 20) {
          progress += 2;
        } else if (num <= 30) {
          if (progress < 95){
            progress += 0.2;
          }
        }else{
          clearInterval(this.firstTime)
        }
        this.progressValue = progress;
        var picInstance = progress - 2;
        this.$refs.followPic.style.left = picInstance + "%";
      }, 1000);
    },
    // 接口返回的状态是成功的时候调用
    progerssValue(num,timer) {
      clearInterval(this.firstTime)
      this.secondTime = setInterval(() => {
        if (this.progressValue >= num) {
          clearInterval(this.secondTime)
        }else{
          this.progressValue += 1
          let picInstance = this.progressValue - 2
          this.$refs.followPic.style.left = picInstance + '%'
        }
      },timer)
    },
    getData(){
        getData().then((res)=>{
        if(res.code==0){
        this.progerssValue(100,5)
        }else{
        clearInterval(this.firstTime)//停止进度条
        }
        
})
    }
}
mounted() {
this.getData()
this.getProgress()
}
</script>

<style>
.progress-box {
  position: relative;
  width: 67%;
  height: 0.25rem;
  line-height: 0.25rem;
  margin: 0rem 0 0.3rem 14vw;
  .pr {
    width: 100%;
    height: 0.15rem;
    background: #ededed;
    border-radius: 15px;
  }
  progress::-webkit-progress-bar {
    background: #ededed;
    border-radius: 15px;
  }
  progress::-webkit-progress-value {
    border-radius: 15px;
    background-image: url(./img/progress.png);
  }
}
<style>
查看原文

yang 收藏了文章 · 10月26日

移动端调试面板插件vconsole

github地址

1. 安装 vconsole-webpack-plugin 插件

npm install vconsole-webpack-plugin --save-dev

2. 修改配置文件vue.config.js

const vConsolePlugin = require('vconsole-webpack-plugin')
module.exports = {
  configureWebpack: config => {
        const debug = process.env.NODE_ENV !== 'production'
        let pluginsDev = [
            new vConsolePlugin({
                filter: [],
                enable: debug
            })
        ]
        config.plugins = [...config.plugins, ...pluginsDev]
    }
}



或是
configureWebpack: config => {
    config.plugins.push(
        //手机端调试
        new vConsole({
        filter: [],     // 需要过滤的入口文件
        enable: isVConsole === true      // 生产环境不打开
        })
    )
}

3.使用

import Vue from 'vue'
import VConsole from 'vconsole'
const vConsole = new VConsole()
Vue.use(vConsole)

4.日志类型

支持 5 种不同类型的日志,会以不同的颜色输出到前端面板:

console.log('foo');   // 白底黑字
console.info('bar');  // 白底紫字
console.debug('oh');  // 白底黄字
console.warn('foo');  // 黄底黄字
console.error('bar'); // 红底红字 

支持以下 console 方法:

console.time('foo');    // 启动名为 foo 的计时器
console.timeEnd('foo'); // 停止 foo 计时器并输出经过的时间 

Object/Array 结构化展示
支持打印 Object 或 Array 变量,会以结构化 JSON 形式输出(并折叠):

var obj = {};
obj.foo = 'bar';
console.log(obj); 

多态
支持传入多个参数,会以空格隔开:

var uid = 666;
console.log('UserID:', uid); // 打印出 UserID: 233 

5:公共属性及方法

//当前 vConsole 的版本号。
vConsole.version
//显示 vConsole 主面板
vConsole.show()
//隐藏 vConsole 主面板
vConsole.hide()
//析构一个 vConsole 对象实例,并将 vConsole 面板从页面中移除。
var vConsole = new VConsole();
vConsole.destroy();
//显示 vConsole 的开关按钮。
vConsole.showSwitch()
//隐藏 vConsole 的开关按钮
vConsole.hideSwitch() 

vConsole.option配置项。

image.png

// get
vConsole.option // => {...}
// set
vConsole.setOption('maxLogNumber', 5000);
// 或者:
vConsole.setOption({maxLogNumber: 5000});

其他的调试工具

查看原文

yang 收藏了文章 · 10月26日

Vue项目的瀑布流部分代码分析

<!--
 * @Author: yang
 * @Date: 2020-10-18 15:58:57
 * @LastEditors: yang
 * @LastEditTime: 2020-10-23 17:27:59
 * @FilePath: \gl\src\component\index\receive.vue
-->
<template>
  <div class="about">
    <p>瀑布流,点击图片可删除一个</p>
    <div class="page">
      <div
        class="content"
        v-for="(item, index) in list"
        :key="item.id"
        :style="{
          width: waterfallW + 'px',
          height: item.imgH + 'px',
          left: item.left + 'px',
          top: item.top + 'px',
        }"
        ref="col"
        @click="clickMe(index)"
      >
        <img :data-original="item.image" alt="" />
      </div>
    </div>
  </div>
</template>

<script>

export default {
   data() {
     return {
      list: [
      {
      image:
        'http://img-agc.iqianggou.com/0a62fca1eeab88e894b93539c35446ec!180x180',
      imgH: 122,
      title: '标题只有1行哦长砍',
      desc: 'Bon Cake(徐家汇店)这家店不要条好吃啊',
      praiseNum: 322,
      top: 0,
      left: 0,
      itemH: 0,
    },
    {
      image:
        'http://img-agc.iqianggou.com/0a62fca1eeab88e894b93539c35446ec!180x180',
      imgH: 334,
      title: '标题只有1行哦长砍标题只有1行哦长砍标题只有1行哦长砍',
      desc:
        'Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店',
      praiseNum: 32232,
      top: 0,
      left: 0,
      itemH: 0,
    },
    {
      image:
        'http://img-agc.iqianggou.com/0a62fca1eeab88e894b93539c35446ec!180x180',
      imgH: 173,
      title: '标题只有1行哦长砍',
      desc:
        'Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店',
      praiseNum: 32,
      top: 0,
      left: 0,
      itemH: 0,
    },
    {
      image:
        'http://img-agc.iqianggou.com/0a62fca1eeab88e894b93539c35446ec!180x180',
      imgH: 225,
      title: '标题只有1行哦长砍',
      desc: 'Bon Cake(徐家汇店)这家店',
      praiseNum: 32,
      top: 0,
      left: 0,
      itemH: 0,
    },
    {
      image:
        'http://img-agc.iqianggou.com/0a62fca1eeab88e894b93539c35446ec!180x180',
      imgH: 89,
      title: '标题只有1行哦长砍',
      desc:
        'Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店Bon Cake(徐家汇店)这家店',
      praiseNum: 32,
      top: 0,
      left: 0,
      itemH: 0,
    },
    {
      image:
        'http://img-agc.iqianggou.com/0a62fca1eeab88e894b93539c35446ec!180x180',
      imgH: 112,
      title: '标题只有1行哦长砍',
      desc: 'Bon Cake(徐家汇店)这家店',
      praiseNum: 32,
      top: 0,
      left: 0,
      itemH: 0,
     }
      ],
       initLeft:'',
       waterfallW:'',
       screenWidth:document.clientWidth,//屏幕宽度
        gap:10,//图片之间的间距
      leftH : 0,//左侧高度
      rightH:0//右侧高度
     }
   },
   created () {
     this.waterfallW = (this.screenWidth-30)/2;
     this.initLeft = (this.screenWidth - this.waterfallW)/2;
   },
   mounted () {
     const nodeList = this.$refs.col;
     this.doSort(nodeList)
   },
   methods: {
    //  排序
     doSort(nodeList) {
       for(let i =0;i<nodeList.length;i++){
         nodeList[i].style.position = 'absolute';
         const domHeight = nodeList[i].clientHeight; //获取图片的高度
         let top,left,itemH;
        //  排列数据的形式高的在左侧,低矮的在右侧
         if(this.leftH>this.rightH){//如果左侧的比右侧图片高
           left = this.gap * 2 + this.waterfallW;  //右侧的left
           top = this.rightH + this.gap;//图片高度加间距
           itemH = domHeight;
           this.rightH += this.gap + domHeight;//右侧的整体高度
         }else{
           left = this.gap;
           top = this.leftH + this.gap;//左侧的top
           itemH = domHeight;//图片的高度
           this.leftH += this.gap + domHeight;//左侧的高度
         }
         this.list[i].top = top;
         this.list[i].left = left;
         this.list[i].itemH = itemH;
         this.list[i].itemW = this.waterfallW;
       }
     },
     clickMe(index){
       const renderedList = this.list.slice(0,index)//得到索引前的数据
       const afreshRenderList = this.list.slice(index+1)//得到点击索引后的数据
       if(this.list[index].left>this.gap){//就是右侧的图片
         this.rightH = this.list[index].top - this.gap //去除一个间距,被删除数据列无需重排数据的高度
         this.leftH = this.checkHeight(renderedList,'left')
       }else{
         this.rightH = this.checkHeight(renderedList,'right')
         this.leftH = this.list[index].top-this.gap//去除一个间距,被删除数据列无需重排数据的高度
       }
       const newList = this.restartSort(afreshRenderList)
       this.list = [...renderedList,...newList]
     },
     //  查找不需要重新排列的数据中非被删除列的高度
     checkHeight(list,col){
       let needHeight = 0;
       for(let i=0;i<list.length;i++){
         if(col == 'left'&& list[i].left == this.gap&&list[i].top>needHeight){
           needHeight = list[i].top+list[i].itemH
         }else if(col = 'right'&&
           list[i].left>this.gap&&
           list[i].top>needHeight){
             needHeight = list[i].top + list[i].itemH;
           }
       }
       return needHeight;
     },
     //重新排列列表中被删除数据之后的所有数据
     restartSort(list){//重排之后 长的在左边,短的在右边
       const newList = list
       newList.forEach((item)=>{
         if(this.leftH>this.rightH){
           item.left = this.gap*2+item.itemW //右侧的left
           item.top = this.rightH + this.gap //右侧的top
           this.rightH +=this.gap+item.itemH//右侧的高度
         }else{
           item.left = this.gap //左侧的left
           item.top = this.leftH + this.gap //左侧的top
           this.leftH += this.gap+item.itemH
         }
       })
       return newList;
     }
   },

}
</script>

<style lang="scss" scoped>
.page {
  position: relative;
  width: 100%;
  height: 100%;
}
.content {
  position: fixed;
  top: 100%;
  img {
    display: block;
    width: 100%;
    height: 100%;
  }
}
</style>
查看原文

yang 收藏了文章 · 10月26日

Vue 轮播图中间大两头小

使用插件版本号(决定成败,非常重要)

"swiper": "^5.3.1",
"vue-awesome-swiper": "^4.1.1",

引入

import Vue from 'vue'
import vueSwiper from 'vue-awesome-swiper'
import 'swiper/css/swiper.css'
Vue.use(vueSwiper)

模板

    <div class="top-banner">
    <swiper
        :options="swiperOption"
        ref="mySwiper"
        class="swiper"
        v-if="bannerData.length > 0"
      >
        <swiper-slide
          class="list-item"
          v-for="(item, index) in bannerData"
          :key="index"
        >
          <div class="choice-box" @click="picStartGame(item)" >
            <img :data-original="item.imgUrl" alt class="choice-pic" />
          </div>
        </swiper-slide>
      </swiper>
       </div>

swiper配置

data(){
  return{
 bannerData: [],  
  swiperOption: {
        initialSlide: 0, //设定初始化时slide的索引
        effect: 'coverflow',
        slidesPerView: 1.1,
        spaceBetween: 20,
        centeredSlides: true, //活动块会居中,而不是默认状态下的居左
        loop: true,
        speed: 1000,
        autoplay: {
          delay: 5000,
          stopOnLastSlide: false,
          disableOnInteraction: false,
        },
        observer: true, //修改swiper自己或子元素时,自动初始化swiper
        observeParents: true, //修改swiper的父元素时,自动初始化swiper
        coverflowEffect: {
          rotate: 28, // slide做3d旋转时Y轴的旋转角度。默认50。
          stretch: 3, // 每个slide之间的拉伸值,越大slide靠得越紧。
          depth: 100, // slide的位置深度。值越大z轴距离越远,看起来越小。
          modifier: 1, // depth和rotate和stretch的倍率,相当于depth*modifier、rotate*modifier、stretch*modifier,值越大这三个参数的效果越明显。默认1。
          slideShadows: false, // 开启slide阴影。默认 true。
        },
      },
      }
      }

样式

.top-banner {
    height: 3.68rem;
    // width: 6.48rem;
    width: 100%;
    text-align: center;
    margin-top: 1rem;
    img {
      width: 100%;
      height: 3.68rem;
      border-radius: 0.2rem;
    }
  }
查看原文

yang 收藏了文章 · 10月26日

vscode快捷键

1.  针对单词的光标移动

你只需按下 Option(Windows 上是 Ctrl 键)和左方向键。相反,如果要把光标移动到单词的末尾,只需要按下 Option 和右方向键就好了。可以在文档中以单词为单位不停地移动光标。

把光标移动到行首或者行末。

Fn+Home/End

代码块的光标移动

按下 Cmd + Shift + (Windows 上是 Ctrl + Shift + )

移动到文档的第一行或者最后一行

你只需按住 Cmd + 左方向键(Windows 上是 ctr +Home 键),就可以把光标移动到了这行的第一列;而如果你按住 Cmd 和右方向键(Windows 上是 ctr +End 键)

3. 删除操作

当前行中光标之前的文本全部删除,

Home + Shift (fn+shift) +delete,macOS: Cmd + Left + Shift +delete

2. 文本选择

对于基于单词、行和整个文档的光标操作,你只

需要多按一个 Shift 键,就可以在移动光标的同时选中其中的文本。 ctr+方向选取   ctr+shift+delete删除

删除单行


 Cmd + Shift + K ” (Windows 上是 “Ctrl + Shift + K”)

剪切这行代码

 Cmd + x ” (Windows 上是 “Ctrl + x”) 即可。

移动一段代码

Option + 上下方向键”(Windows中就是“Alt + 上下方向键”

复制这几行,然后粘贴到当前行的上面或者下面。

“Option + Shift + 上下方向键”(Windows中就是“Alt + shift + 上下方向键”)

添加注释


“ Cmd + / ” (Windows 上时 “Ctrl + /”)

代码格式化

Option + Shift + F” (Windows 上是 Alt + Shift + F)

调换字符的位置

Ctrl + t” (Windows 上未绑定快捷键,可以打开命令面板,搜索 ”转置游标处的字符

调整字符的大小写

命令面板里运行“转换为大写”或 “转换为小写”

合并代码行


“ Ctrl + j ” (Windows 上未绑定快捷键,可以打开命令面板,搜索 ”合并行排序“ 命令面板,然后搜索 “按升序排列行” 或者 “按降序排列行” 命令执行

查看当前文件所有的关于选中的单词

ctr + d

关闭当前文件

ctr + w

打开最近打开文件的历史记录
返回上一个开启代码篇

ctr + t  

撤销光标的移动和选择

Cmd + U”(Windows 上是 “Ctrl + U”

当前行的上面新开始一行

“Cmd + Shift + Enter” (Windows 上是 “Ctrl + Shift + Enter”)

当前行的下面新开始一行

Cmd + Enter” (Windows 上是 “Ctrl + Enter”)

多光标

使用鼠标

Option(Windows上是Alt)按住点下一个需要的位置

使用键盘


Cmd + Option + 下方向键”(Windows 上是 “Ctrl + Alt + 下方向键”)

Cmd + 右方向键”(Windows 上是 End)  移动到每一行的末尾,可对css添加pz之类的

或者

Cmd + D”(Windows 上是 Ctrl + D)实现相同文字多选多光标  进行别的操作

esc退出多选

或者

Option + Shift + i” (Windows 上是 Alt + Shift + i) 每一行的最后都会创建一个新的光标。

跳转

“Ctrl+Tab”

或者

“Cmd + P” (Windows 上是 Ctrl + P) 跳出一个最近打开文件的列表,同时在列表的顶部还有一个搜索框。

找到目标文件后,可以按下 “Cmd + Enter ” (Windows 上是 Ctrl + Enter)组合键, 这个文件在一个新的编辑器窗口中打开

行跳转

Ctrl + g”

移动到定义处

ctr + f12

跳转到某个文件的某一行

先按下 “Cmd + P”,输入文件名,然后在这之后加上 “:”和指定行号即可。

符号 (Symbols) 跳转( VS Code 提供了一套 API 给语言服务插件,它们可以分析代码,告诉 VS Code 项目或者文件里有哪些类、哪些函数或者标识符(我们把这些统称为符号)。)


Cmd + Shift + O” (Windows 上是 Ctrl + Shift + O),就能够看到当前文件里的所有符号

使用方向键,或者搜索,找到你想要的符号后,按下回车,就能够立刻跳转到那个符号的位置

“Cmd + Shift +O”后,输入框里有一个 “@”符号

输入 “:”,就可以将当前文件的所有符号,进行分类

多个文件里进行符号跳转

“Cmd + T” (Windows 上是 Ctrl + T),搜索这些文件里的符号。

跳转到函数的实现的位置。

“Cmd + F12” (Windows 上是 Ctrl + F12)

跳转到定义的代码文件返回

ctr + -

显示

放大当前页面的代码

ctr +

缩小

ctr -

侧边栏显示隐藏

ctr + B

全屏

F11

显示操作台

ctr + shift + u

引用 (Reference) 跳转


当前行的下面新开始一行

Cmd + Enter” (Windows 上是 “Ctrl + Enter”)

在一个 js 文件里 export 了一个函数,在另外一个文件里引用了它但是 shift + F12 找不到

可以在这个项目下添加一个 jsconfig.json 文件,这个文件会让 VSCode 知道,当前这个文件夹下的文件都是属于同一个项目的,从而进行索引。


{

"compilerOptions": {

"target": "ES6"

},

"exclude": [

"node_modules",

"**/node_modules/*"

]

}

webpack里面配置了路径别名,vscode就找不到定义

{

"compilerOptions": {

"baseUrl": ".",

"paths": {

"ClientApp/*": ["./ClientApp/*"]

}

}

}

鼠标操作

连续按下鼠标三次,则会选中当前代码
四次 则会选中整篇代码

悬停提示窗口

 Cmd 键(Windows 上是 Ctrl),则能够在悬停提示窗口里直接看到 `foo`的实现。
 点击可实现跳转

操作左侧的资源管理目录

ctr + shift + E

左侧跨文件搜索

ctr + shift + F

管理自己的git存储库

ctr + shift + G

同步自己的 vs code 设置

插件 Settings Sync 

启动和调试

ctr + shift + D

管理扩展

ctr + shift + X

查找并运行所有命令

ctr + shift + p

重构

修改当前页面的相同函数或者变量名

把光标放到函数或者变量名上,然后按下 F2,这样这个函数或者变量出现的地方就都会被修改。

长代码抽取出来转成一个单独的函数

我们只需选中那段代码,点击黄色的灯泡图标,然后选择对应的重构操作即可。

代码片段

1 打开命令面板,搜索“配置用户代码片段”(Configure User Snippets)并且执行
2选择 JavaScript
3 选择完语言后,我们就能看到一个 JSON 文件被打开了,这个文件里的内容,现在都是被注释掉的。我们可以选中第七行到第十四行,按下 “Cmd+ /”取消注释。
{
    "Print to console": {
        "prefix": "log",
        "body": [
            "console.log('$1');",
            "$2"
        ],
        "description": "Log output to console"
    }
}

这个代码片段的名字叫做 “Print to console”。这个代码片段对象的值,也就是花括号里的代码,必须要包含 “prefix” 前缀和 “body” 内容这两个属性。同时,这个值还可以包含 “description” 描述这个属性,但这个属性不是必须的。
“prefix” 的作用是,当我们在编辑器里打出跟 “prefix” 一样的字符时,我们就能在建议列表里看到这个代码片段的选项,然后我们按下 Tab 键,就能够将这个代码片段的 “body” 里面的内容插入到编辑器里。如果这个代码片段有 “description” 这个属性的话,那么我们还能够在建议列表的快速查看窗口里看到这段 “description”
Tab Stop

当 “body” 内容被插入到编辑器后,你会发现,内容里 的`$1`和 `$2`不见了,取而代之的是两个竖线。这`$1`和 `$2`就是 Tab Stop,意思是,当我们按下 Tab 键之后,光标移动到的位置。当这段代码片段被插入到编辑器后,编辑器会把光标移动到`$1`所在的位置,然后如果你再按一次 Tab 键,光标则会立刻移动到 `$2`的位置。
光标移动到上一个 Tab Stop 的位置的快捷键
“Shift + Tab” 

占位符
*在我们插入 Tab Stop 的时候,除了 $1$2 这样的语法,我们还可以填入 ${1:label},在这个格式下,代码片段被插入编辑器里时,$1 的位置处,会预先填入 label这个值,并且 label 会被选中。
对于这个值我们称之为占位符,顾名思义,这个值是我们在代码片段中预先设置好的。如果我们觉得这个值可以用,那就不需要修改了,直接按 Tab 键跳到下一个 Tab Stop 继续编辑。如果觉得要换成一个新的值,那么也只需直接打字就可以将其替换,因为这个占位符已经被光标选中了。
这里我们对上面的代码片段进行一点修改*:

 "Print to console": {
        "prefix": "log",
        "body": [
            "console.log(${1:i});",
            "$2"
        ],
        "description": "Log output to console"
    }

*我们将 $1 改成了 ${1:i},那么当log 这个代码片段被插入时,我们将看到console.log(i);,同时 i 被选中
直接按下 Escape 键,跳出代码片段的编辑模式,之后继续我们的其他编辑操作。*
多光标
可以用 ${1:label} 来指定 Tab Stop 和占位符
也可以在代码片段的多个位置使用同样的 Tab Stop 。
代码片段中三个不同的位置插入 $1,这样编辑器就会为这三个不同的位置,分别创建一个光标,然后当我们打字的时候,他们就会被一起修改。

   "Print to console": {
        "prefix": "log",
        "body": [
            "console.log(${1:i});",
            "console.log(${1:i} + 1); // ${1:i} + 1",
            "$2"
        ],
        "description": "Log output to console"
    }

预设变量
提前预设好一些值一些变量。

https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables

这一篇关于vscode快捷键的文章也不错

查看原文

yang 发布了文章 · 10月25日

vscode快捷键

1.  针对单词的光标移动

你只需按下 Option(Windows 上是 Ctrl 键)和左方向键。相反,如果要把光标移动到单词的末尾,只需要按下 Option 和右方向键就好了。可以在文档中以单词为单位不停地移动光标。

把光标移动到行首或者行末。

Fn+Home/End

代码块的光标移动

按下 Cmd + Shift + (Windows 上是 Ctrl + Shift + )

移动到文档的第一行或者最后一行

你只需按住 Cmd + 左方向键(Windows 上是 ctr +Home 键),就可以把光标移动到了这行的第一列;而如果你按住 Cmd 和右方向键(Windows 上是 ctr +End 键)

3. 删除操作

当前行中光标之前的文本全部删除,

Home + Shift (fn+shift) +delete,macOS: Cmd + Left + Shift +delete

2. 文本选择

对于基于单词、行和整个文档的光标操作,你只

需要多按一个 Shift 键,就可以在移动光标的同时选中其中的文本。 ctr+方向选取   ctr+shift+delete删除

删除单行


 Cmd + Shift + K ” (Windows 上是 “Ctrl + Shift + K”)

剪切这行代码

 Cmd + x ” (Windows 上是 “Ctrl + x”) 即可。

移动一段代码

Option + 上下方向键”(Windows中就是“Alt + 上下方向键”

复制这几行,然后粘贴到当前行的上面或者下面。

“Option + Shift + 上下方向键”(Windows中就是“Alt + shift + 上下方向键”)

添加注释


“ Cmd + / ” (Windows 上时 “Ctrl + /”)

代码格式化

Option + Shift + F” (Windows 上是 Alt + Shift + F)

调换字符的位置

Ctrl + t” (Windows 上未绑定快捷键,可以打开命令面板,搜索 ”转置游标处的字符

调整字符的大小写

命令面板里运行“转换为大写”或 “转换为小写”

合并代码行


“ Ctrl + j ” (Windows 上未绑定快捷键,可以打开命令面板,搜索 ”合并行排序“ 命令面板,然后搜索 “按升序排列行” 或者 “按降序排列行” 命令执行

查看当前文件所有的关于选中的单词

ctr + d

关闭当前文件

ctr + w

打开最近打开文件的历史记录
返回上一个开启代码篇

ctr + t  

撤销光标的移动和选择

Cmd + U”(Windows 上是 “Ctrl + U”

当前行的上面新开始一行

“Cmd + Shift + Enter” (Windows 上是 “Ctrl + Shift + Enter”)

当前行的下面新开始一行

Cmd + Enter” (Windows 上是 “Ctrl + Enter”)

多光标

使用鼠标

Option(Windows上是Alt)按住点下一个需要的位置

使用键盘


Cmd + Option + 下方向键”(Windows 上是 “Ctrl + Alt + 下方向键”)

Cmd + 右方向键”(Windows 上是 End)  移动到每一行的末尾,可对css添加pz之类的

或者

Cmd + D”(Windows 上是 Ctrl + D)实现相同文字多选多光标  进行别的操作

esc退出多选

或者

Option + Shift + i” (Windows 上是 Alt + Shift + i) 每一行的最后都会创建一个新的光标。

跳转

“Ctrl+Tab”

或者

“Cmd + P” (Windows 上是 Ctrl + P) 跳出一个最近打开文件的列表,同时在列表的顶部还有一个搜索框。

找到目标文件后,可以按下 “Cmd + Enter ” (Windows 上是 Ctrl + Enter)组合键, 这个文件在一个新的编辑器窗口中打开

行跳转

Ctrl + g”

移动到定义处

ctr + f12

跳转到某个文件的某一行

先按下 “Cmd + P”,输入文件名,然后在这之后加上 “:”和指定行号即可。

符号 (Symbols) 跳转( VS Code 提供了一套 API 给语言服务插件,它们可以分析代码,告诉 VS Code 项目或者文件里有哪些类、哪些函数或者标识符(我们把这些统称为符号)。)


Cmd + Shift + O” (Windows 上是 Ctrl + Shift + O),就能够看到当前文件里的所有符号

使用方向键,或者搜索,找到你想要的符号后,按下回车,就能够立刻跳转到那个符号的位置

“Cmd + Shift +O”后,输入框里有一个 “@”符号

输入 “:”,就可以将当前文件的所有符号,进行分类

多个文件里进行符号跳转

“Cmd + T” (Windows 上是 Ctrl + T),搜索这些文件里的符号。

跳转到函数的实现的位置。

“Cmd + F12” (Windows 上是 Ctrl + F12)

跳转到定义的代码文件返回

ctr + -

显示

放大当前页面的代码

ctr +

缩小

ctr -

侧边栏显示隐藏

ctr + B

全屏

F11

显示操作台

ctr + shift + u

引用 (Reference) 跳转


当前行的下面新开始一行

Cmd + Enter” (Windows 上是 “Ctrl + Enter”)

在一个 js 文件里 export 了一个函数,在另外一个文件里引用了它但是 shift + F12 找不到

可以在这个项目下添加一个 jsconfig.json 文件,这个文件会让 VSCode 知道,当前这个文件夹下的文件都是属于同一个项目的,从而进行索引。


{

"compilerOptions": {

"target": "ES6"

},

"exclude": [

"node_modules",

"**/node_modules/*"

]

}

webpack里面配置了路径别名,vscode就找不到定义

{

"compilerOptions": {

"baseUrl": ".",

"paths": {

"ClientApp/*": ["./ClientApp/*"]

}

}

}

鼠标操作

连续按下鼠标三次,则会选中当前代码
四次 则会选中整篇代码

悬停提示窗口

 Cmd 键(Windows 上是 Ctrl),则能够在悬停提示窗口里直接看到 `foo`的实现。
 点击可实现跳转

操作左侧的资源管理目录

ctr + shift + E

左侧跨文件搜索

ctr + shift + F

管理自己的git存储库

ctr + shift + G

同步自己的 vs code 设置

插件 Settings Sync 

启动和调试

ctr + shift + D

管理扩展

ctr + shift + X

查找并运行所有命令

ctr + shift + p

重构

修改当前页面的相同函数或者变量名

把光标放到函数或者变量名上,然后按下 F2,这样这个函数或者变量出现的地方就都会被修改。

长代码抽取出来转成一个单独的函数

我们只需选中那段代码,点击黄色的灯泡图标,然后选择对应的重构操作即可。

代码片段

1 打开命令面板,搜索“配置用户代码片段”(Configure User Snippets)并且执行
2选择 JavaScript
3 选择完语言后,我们就能看到一个 JSON 文件被打开了,这个文件里的内容,现在都是被注释掉的。我们可以选中第七行到第十四行,按下 “Cmd+ /”取消注释。
{
    "Print to console": {
        "prefix": "log",
        "body": [
            "console.log('$1');",
            "$2"
        ],
        "description": "Log output to console"
    }
}

这个代码片段的名字叫做 “Print to console”。这个代码片段对象的值,也就是花括号里的代码,必须要包含 “prefix” 前缀和 “body” 内容这两个属性。同时,这个值还可以包含 “description” 描述这个属性,但这个属性不是必须的。
“prefix” 的作用是,当我们在编辑器里打出跟 “prefix” 一样的字符时,我们就能在建议列表里看到这个代码片段的选项,然后我们按下 Tab 键,就能够将这个代码片段的 “body” 里面的内容插入到编辑器里。如果这个代码片段有 “description” 这个属性的话,那么我们还能够在建议列表的快速查看窗口里看到这段 “description”
Tab Stop

当 “body” 内容被插入到编辑器后,你会发现,内容里 的`$1`和 `$2`不见了,取而代之的是两个竖线。这`$1`和 `$2`就是 Tab Stop,意思是,当我们按下 Tab 键之后,光标移动到的位置。当这段代码片段被插入到编辑器后,编辑器会把光标移动到`$1`所在的位置,然后如果你再按一次 Tab 键,光标则会立刻移动到 `$2`的位置。
光标移动到上一个 Tab Stop 的位置的快捷键
“Shift + Tab” 

占位符
*在我们插入 Tab Stop 的时候,除了 $1$2 这样的语法,我们还可以填入 ${1:label},在这个格式下,代码片段被插入编辑器里时,$1 的位置处,会预先填入 label这个值,并且 label 会被选中。
对于这个值我们称之为占位符,顾名思义,这个值是我们在代码片段中预先设置好的。如果我们觉得这个值可以用,那就不需要修改了,直接按 Tab 键跳到下一个 Tab Stop 继续编辑。如果觉得要换成一个新的值,那么也只需直接打字就可以将其替换,因为这个占位符已经被光标选中了。
这里我们对上面的代码片段进行一点修改*:

 "Print to console": {
        "prefix": "log",
        "body": [
            "console.log(${1:i});",
            "$2"
        ],
        "description": "Log output to console"
    }

*我们将 $1 改成了 ${1:i},那么当log 这个代码片段被插入时,我们将看到console.log(i);,同时 i 被选中
直接按下 Escape 键,跳出代码片段的编辑模式,之后继续我们的其他编辑操作。*
多光标
可以用 ${1:label} 来指定 Tab Stop 和占位符
也可以在代码片段的多个位置使用同样的 Tab Stop 。
代码片段中三个不同的位置插入 $1,这样编辑器就会为这三个不同的位置,分别创建一个光标,然后当我们打字的时候,他们就会被一起修改。

   "Print to console": {
        "prefix": "log",
        "body": [
            "console.log(${1:i});",
            "console.log(${1:i} + 1); // ${1:i} + 1",
            "$2"
        ],
        "description": "Log output to console"
    }

预设变量
提前预设好一些值一些变量。

https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables

这一篇关于vscode快捷键的文章也不错

查看原文

赞 1 收藏 1 评论 0

认证与成就

  • 获得 143 次点赞
  • 获得 8 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-03-18
个人主页被 2.1k 人浏览