温故知新- vue 用指令封装函数式组件

250 阅读4分钟

背景

最近公司业务涉及到大量表单,每个表单上都有日志。于是封装了一个日志弹窗组件,然后在每个页面去引用, 并且写这么大一坨。高大上的程序员 webCoder 瞬间秒变码农。

-- template

<el-button type="text" @click="handlViewLog(scope.row)">{{ $t('日志') }}</el-button>

<LogTableModal v-model="logModalVisible" :ref-id="currentDetail.id" biz-log-key="TMS_PORT" />

-- js
import LogTableModal from '@/components/LogTableModal'

{
    component: {
        LogTableModal
    },
    data(){
        return {
            logModalVisible: false
        }
    },
    methods: {
        handlViewLog(row){
            // todo...
        }
    }
}

我开始思考,是否有一种方式来简化这种操作,咱们能不能也用一个指令来替代这些冗余重复的工作。例如elment-ui的 v-loading,。

说干就干,干完之后,我们的代码变成了这样, 一行代码解决 Perfect~

<el-button type="text" v-log="{bizLogKey: 'TMS_PORT', refId: scope.row.id}">{{ $t('日志') }}</el-button>

实现

直接看看是怎么实现的。

实现思路主要参考Elment-ui, 涉及到知识点 Directive, Vue.extend, Vue.$mount, 函数式组件。

import Vue from 'vue'
import LogTableModal from '@/components/LogTableModal'

let instance

const showDialog = function(el, binding) {
  return function() {
    instance = new Loigc({
      propsData: {
        init: true,
        bizLogKey: binding.value.bizLogKey,
        refId: binding.value.refId
      }
    })

    instance.$on('input', function(v) {
      instance.value = v
      if (!v) {
        instance.$el.parentNode.removeChild(instance.$el)
        instance = null
      }
    })

    instance.value = true
    instance.$mount()

    document.body.appendChild(instance.$el)
    instance.initEditData()
  }
}

const Loigc = Vue.extend(LogTableModal)
export default {
  inserted(el, binding) {
    el.addEventListener('click', showDialog(el, binding))
  },

  unbind(el) {
    el.removeEventListener('click', showDialog)
  }
}

函数式组件

三无产品- 函数式组件

  • 无状态 (没有 响应式数据)
  • 无生命周期 (没有 created mounted update)
  • 没有实例 (没有 this 上下文)

有状态组件和无状态组件指的是组件是否有自己的数据(state)

React里面的状态跟Vue的data一个概念, 并且Vue函数式组件里面没有响应式数据,所以这里理解为无状态既无响应式数据, 如果理解有错误,欢迎评论区指正。

参考React无状态

有状态组件

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      name: '张三'
    }
  }
  render() {
    return (
      <div>
        <h1>{this.state.name}</h1>
      </div>
    )
  }
}

注:类组件继承React.Component组件,会从父类中继承一个state属性,通过这个属性可以定义自己的状态

无状态组件

function App() {
  return (
    <div>
      <h1>hello</h1>
    </div>
  )
}

注:函数式组件没有继承React.Component组件,没有state属性,没有自己的状态 (使用HOOK可以给函数式组件添加状态)

函数式无状态组件特点
● 它是为了创建纯展示组件,这种组件只负责根据传入的props来展示,不涉及到state状态的操作。
● 组件不能访问this对象
● 不能访问生命周期方法 函数式组件的性能优化 减少重新 render 的次数。因为在 React 里最重(花时间最长)的一块就是 reconction(简单的可以理解为 diff),如果不 render,就不会 reconction。 减少计算的量。主要是减少重复计算,对于函数式组件来说,每次 render 都会重新从头开始执行函数调用

Vue 官方描述

之前创建的锚点标题组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。

无状态的组件用函数式组件 对于一些纯展示,没有响应式数据,没有状态管理,也不用生命周期钩子函数的组件, 我们就可以设置成函数式组件,提高渲染性能,因为会把它当成一个函数来处理, 所以开销很低

原理是在 patch 过程中对于函数式组件的 render 生成的虚拟 DOM,不会有递归 子组件初始化的过程,所以渲染开销会低很多

它可以接受 props,但是由于不会创建实例,所以内部不能使用 this.xx 获取组 件属性,写法如下


 
<template functional>
 <div>
   <div class="content">{{ value }}</div>
 </div>
</template>
<script>
export default {
 props: ['value']
}
</script>
 
// 或者
Vue.component('my-component', {
 functional: true, // 表示该组件为函数式组件
 props: { ... }, // 可选
// 为了弥补缺少的实例  
// 提供第二个参数作为上下文
 render: function (createElement, context) {
   // ...
}
})

Vue.extend

使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

data 选项是特例,需要注意 - 在 Vue.extend() 中它必须是函数

extend 创建的是 Vue 构造器,而不是我们平时常写的组件实例,所以不可以通过 new Vue({ components: Loigc }) 来直接使用需要通过 new Loigc().$mount({el: '#app'}) 来挂载到指定的元素上。

$mount

如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用 vm.$mount() 手动地挂载一个未挂载的实例。

如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素,并且你必须使用原生 DOM API 把它插入文档中。

这个方法返回实例自身,因而可以链式调用其它实例方法。

var MyComponent = Vue.extend({  
template: '<div>Hello!</div>'  
})  
  
// 创建并挂载到 #app (会替换 #app)  
new MyComponent().$mount('#app')  
  
// 同上  
new MyComponent({ el: '#app' })  
  
// 或者,在文档之外渲染并且随后挂载  
var component = new MyComponent().$mount()  
document.getElementById('app').appendChild(component.$el)

踩坑 i18n

i18n报错

做的过程中发现控制台有报错,打开控制台看了下是i18n的问题 image.png

<template>
    <div>{{ $t('commen.close') }}</div>
<template>

咋一看没有毛病啊,为啥换了种用法就出现问题了呢。 回顾一下上文中提到的 函数式组件 是三无组件,无状态,无生命周期,无this. 在注册i18n的时候,t是挂在到main.js实例上,所以我们在调用子组件的时候可以通过this.t是挂在到 main.js 实例上,所以我们在调用子组件的时候可以通过this.t() 调用到i18n, 但是当它变成函数式组件后, 变成了三无产品,此时我们是无法再通过这种方法去取到Vue实例上的$t,此时的this 指向的是当前函数实例。

所以我们只需要把i18n挂在到这个实例对象上就可以解决问题了

-- i18n.js

export const $t = (localeKey, options) => {
  // 使用i18n的 te 方法来检查是否能够匹配到对应键值
  const hasKey = i18n.te(localeKey, store.state.common?.lang)
  const translatedStr = i18n.t(localeKey, options)
  if (hasKey) {
    return translatedStr
  }
  return localeKey
}

-- component.vue
<template>
    <div>{{ t('commen.close') }}</div>
<template>

import { $t } from '@/plugins/i18n'

{
    methods: {
        t(...args) {
          return $t.apply(this, args)
        },
    }
}