02020 / vite-kit

0 stars 0 forks source link

vue-hoc 探究 #2

Open 02020 opened 3 years ago

02020 commented 3 years ago

vue-hoc hoc-promise-compose

// 高阶组件基本构成
function WithConsole(WrappedComponent) {
  return {
    mounted() {
      console.log('I have already mounted');
    },
    props: WrappedComponent.props,
    render(h) {
      const slots = Object.keys(this.$slots)
        .reduce((arr, key) => arr.concat(this.$slots[key]), [])
        .map((vnode) => {
          vnode.context = this._self;
          return vnode;
        });

      const attribute = {
        on: this.$listeners,
        props: this.$props,
        // 透传 scopedSlots
        scopedSlots: this.$scopedSlots,
        attrs: this.$attrs,
      };
      // console.log('attribute', attribute);
      return h(WrappedComponent, attribute, slots);
    },
  };
}
02020 commented 3 years ago

vue-src

_c slot 渲染代码

function renderSlot(name, fallback, props, bindObject) {
  var scopedSlotFn = this.$scopedSlots[name];
  var nodes;
  if (scopedSlotFn) {
    // scoped slot
    props = props || {};
    if (bindObject) {
      if (process.env.NODE_ENV !== 'production' && !isObject(bindObject)) {
        warn('slot v-bind without argument expects an Object', this);
      }
      props = extend(extend({}, bindObject), props);
    }
    nodes = scopedSlotFn(props) || fallback;
  } else {
    nodes = this.$slots[name] || fallback;
  }

  var target = props && props.slot;
  if (target) {
    return this.$createElement('template', { slot: target }, nodes);
  } else {
    return nodes;
  }
}

简化版(剔除多余传参)

function renderSlot(name) {
  var scopedSlotFn = this.$scopedSlots[name];
  return scopedSlotFn ? scopedSlotFn({}) : this.$slots[name];
}

export { renderSlot };
02020 commented 3 years ago

render-demo

封装组件

export default {
  name: 'e-btns',
  props: {
    value: {}, // model
    config: {
      type: Array,
      required: true,
    },
    size: String,
    shape: String,
    vertical: Boolean,
  },

  methods: {
    onCommand(...args) {
      this.$emit('cmd', ...args);
    },
  },

  render(h) {
    const child = ({ label, key, name, type, icon, disabled, cmd }) =>
      h(
        'Button',
        {
          props: { type, icon, disabled },
          on: {
            click: cmd || (() => this.onCommand(key || name)),
          },
        },
        label
      );

    return h(
      'ButtonGroup',
      {
        props: {
          size: this.size,
          shape: this.shape,
          vertical: this.vertical,
        },
      },
      this.config.map((x) => child(x))
    );
  },
};

应用

配置

const config = [
  { key: '1', type: 'primary', icon: 'md-cloud-upload', disabled: true },
  { key: '2', type: 'error', icon: 'ios-create', disabled: false },
  { key: '3', type: 'warning', icon: 'md-cloud-download' },
];

使用

export const renderTest = {
  name: 'renderTest',
  components: { btns },
  props: {
    msg: String,
  },
  data() {
    return { config };
  },
  methods: {
    onCMD(...args) {
      console.log(this.msg, ...args);
    },
  },
  render(h) {
    return h('btns', {
      props: {
        config: this.config,
      },
      on: {
        cmd: this.onCMD,
      },
    });
  },
};
02020 commented 3 years ago

vue-hoc

createRenderFnc 的使用方法


import { createRenderFnc } from 'vue-hoc';

// 定义 组件A的渲染传参
const renderOptions = {
  props: {
    config() {
      // this = 父组件组件
      console.log('config 被过滤了');
      return this.config.filter((x) => x.disabled);
    },
  },
  listeners: {
    cmd(arg) {
      console.log('cmd', arg);
      this.$emit('cmd', arg);
    },
  },
  scopedSlots: {
    default: (me) => {
      console.log('入参', me);
      console.log('此上下文: 空', this);
      return '我是默认';
    },
    named_slot: function (me) {
      console.log('此上下文: 父组件组件(调用处)', this);
      return 'named_slot';
    },
    other_slot: (me) => 'sss',
  },
};

// 定义组件A
const Component = {
  props: {
    config: {
      type: Array,
    },
  },
  render(h) {
    const slots = this.$scopedSlots;
    return h('div', [
      ...this.config.map((x) => h('div', [JSON.stringify(x)])),
      h('div', ['----以下是子组件----']),
      slots['default'](this),
      h(
        'div',
        {
          staticClass: 'named',
          on: {
            click: () => this.$emit('cmd', '组件named点击了'),
          },
        },
        [slots['named_slot'](this)]
      ),
      h('div', { staticClass: 'other' }, [slots['other_slot'](this)]),
    ]);
  },
};

// 在组件A的基础上构建组件B  即 .vue 文件
const enhanced = {
  props: {
    config: {
      type: Array,
      required: true,
    },
  },
  name: 'HOC',
  render: createRenderFnc(renderOptions)(Component),
};

export default enhanced;
02020 commented 3 years ago

render-builder

collapse-builder v1.0

针对 iviewui

export default {
  name: 'collapse-builder',
  props: {
    value: {}, // model
    config: {
      type: Array,
      required: true,
    },
    accordion: Boolean,
    simple: Boolean,
  },

  methods: {
    onCommand(...args) {
      this.$emit('cmd', ...args);
    },
    __input(v) {
      this.$emit('input', v);
    },
  },

  render(h) {
    this.config.forEach((group, index) => {
      group.name = group.name || `panel_${index}`;
    });

    const panel = ({ label, name, content }) =>
      h(
        'Panel',
        {
          props: { name },
          scopedSlots: {
            content: () => this.$scopedSlots.default(content),
          },
        },
        label //  增加按钮及图标
      );

    return h(
      'Collapse',
      {
        on: {
          'on-change': this.onCommand,
        },
        model: {
          value: this.value,
          callback: this.__input,
          expression: 'value',
        },
        props: {
          accordion: this.accordion,
          simple: this.simple,
        },
      },
      this.config.map(panel)
    );
  },
};

数据格式

const data = config.map((x) => {
  return {
    label: x.label,
    name: x.value,
    content: x.children.map((x) => {
      const { label, value } = x;
      return { label, value };
    }),
  };
});

demo

  <builder :config="config" @cmd="onCommand" v-model="value">
    <template v-slot="data">
      <template v-for="(item, index) in data">
        <Button @click="onClick(index)" :key="index" type="text">{{
          item.label
        }}</Button>
      </template>
    </template>
  </builder>
02020 commented 3 years ago

hoc-composed 高阶组件的函数组合

<div id="app">
  <hoc msg="msg" @change="onChange">
    <template>
      <div>I am slot</div>
    </template>
    <template v-slot:named>
      <div>I am named slot</div>
    </template>
  </hoc>
</div>

渲染

new Vue({
  el: '#app',
  components: { hoc },
  methods: { onChange() {} },
});

组件 A

var view = {
  props: ['result'],
  data() {
    return {
      requestParams: {
        name: 'ssh',
      },
    };
  },
  methods: {
    reload() {
      this.requestParams = {
        name: 'changed!!',
      };
    },
  },
  template: `
          <span>
            <span>{{result?.name}}</span>
            <slot></slot>
            <slot name="named"></slot>
            <button @click="reload">重新加载数据</button>
          </span>
        `,
};

组件 B

const withPromise = (promiseFn) => (wrapped) => {
  return {
    data() {
      return {
        loading: false,
        error: false,
        result: null,
      };
    },
    methods: {
      async request() {
        this.loading = true;
        // 从子组件实例里拿到数据
        const { requestParams } = this.$refs.wrapped;
        // 传递给请求函数
        const result = await promiseFn(requestParams).finally(() => {
          this.loading = false;
        });
        this.result = result;
      },
    },
    async mounted() {
      // 立刻发送请求,并且监听参数变化重新请求
      this.$refs.wrapped.$watch('requestParams', this.request.bind(this), {
        immediate: true,
      });
    },
    render(h) {
      const args = {
        props: {
          // 混入 $attrs
          ...this.$attrs,
          result: this.result,
          loading: this.loading,
        },

        // 传递事件
        on: this.$listeners,

        // 传递 $scopedSlots
        scopedSlots: this.$scopedSlots,
        ref: 'wrapped',
      };

      const wrapper = h('div', [
        this.loading ? h('span', ['加载中……']) : null,
        this.error ? h('span', ['加载错误']) : null,
        h(wrapped, args),
      ]);

      return wrapper;
    },
  };
};

组件 C

const withLog = (wrapped) => {
  return {
    mounted() {
      console.log('I am mounted!');
    },
    render(h) {
      return h(wrapped, normalizeProps(this));
    },
  };
};

组装

const request = (data) => {
  return new Promise((r) => {
    setTimeout(() => {
      r(data);
    }, 1000);
  });
};

const composed = compose(withLog, withPromise(request));

var hoc = composed(view);

// 通用函数
function compose(...funcs) {
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
}

function normalizeProps(vm) {
  return {
    on: vm.$listeners,
    attr: vm.$attrs,
    // 传递 $scopedSlots
    scopedSlots: vm.$scopedSlots,
  };
}