最近在设计笔者所在组自己的组件库。从设计上看,一个组件库是否“成功”取决于前期的设计 —— 我决定用上ITCSS模型。为这个组件的团队维护、扩展打下坚实的基础。但这还不够,组件库最重要的组件往往是非大型库开发中最容易被忽视的。
我将组件从使用场景上分为两种类型:

  • 大组件:也叫“场景组件”。功能完善,自成一片天地。取之即用,通过传参在内部做不同处理 —— 或渲染不同UI、或返回不同值。最重要的是:不侵入页面逻辑。也就是说,无论谁在什么场景下调用,都是相同的方式。拿到值以后这个组件在当前操作流中就算完成自身‘职责’了。显而易见,它有很大的缺陷:一般是针对某个场景/业务线定制的。
  • 小组件:可以处理专门的逻辑。它的使用必须和页面逻辑上下文联系起来。甚至于说组件内部和外部的数据是有一定耦合的、需要N个“小组件”嵌套/组合共同完成某个功能。并且针对不同场景有不同程度的改造需要。但优势也是明显的:一般我们说的通用组件/第三方组件基本就指它。

忘了哪一本书中说过一句我非常认可的话:“使用组合而不是继承”。用在这里似乎也很巧妙。

拿笔者遇到的场景来说。结合上面我个人的定义。这个“选择商品”就可以说是“大组件”:
微店-满减赠项目-已上线

而像弹窗、提示层、按钮这种就是“小组件”了:

微店-满包邮项目迭代

只是粗略的划分一下。上面“小组件”的示例其实并不直观。不过可以这么认为:大组件示例中跳转过去的商品选择route里每一条数据的结构(是一样的,可以拆分出来)就是一个小组件。

一般我们的组件都是这么设计的:通过props动态传参、或通过调用子组件内部暴露的方法传参1;再监听子组件传出的自定义事件并接收值。
比如这样:

//子组件
<script>
export default {
	props: {
		area: {
			type: Array,
			default: []
		}
	},
    methods: {
    	$_setData(list=[]) {
            this.list = xxx(list);
        },
        //第一种方法
        $_getData() {
        	return this.xxx;
        }
        // 第二种方法
        handleData() {
        	//一些处理
        	this.$emit("harea-change", xxx);
        }
    }
}
</script>
// 父组件
<area-map
	:area="xxx" //第一种传参
	@harea-change="handleAreaChange"
	ref="mapAreaRef"
></area-map>

//js
this.$refs.mapAreaRef.$_setData(xxx); //在某个方法中调用,第二种传参
this.$refs.mapAreaRef.$_getData(); //在某个方法中调用,第一种接收参数方法
handleAreaChange(val){
	//第二种接收参数方法
}

这两种方式的好处是写起来比较直观,尤其是在父组件/页面拿到数据后需要再进行复杂处理的情况下。

但是对于一些通用组件来说,组件内定义的数据、组件暴露出来的方法名等等都需要口口相传或者文档规定清楚。
对此,我们还有另一种方式:这里笔者以组内实际使用的「底部弹层」组件为例看一下“小组件”的嵌套使用

  1. 真实弹层组件的js文件
import Creater from "../utils/popup.js";
import Module from "./index.vue"; //真实弹层组件的本体
export default Creater(Module);

那第一行是什么?其实是组内规定的「底部弹层通用动画规范」。我们将其封装为一个单独的“小组件”。真实的组件时通过Vue.extend挂载上去的(这是在js中,如果按一般写法就是父子组件)!
这样在有其它样子的弹层时也可以直接挂载而不需要再去CV动画css:

  1. 真实弹层组件的vue文件

<template>
  <section class="mkt-popup-range">
    <div class="popup-range-line" @click="setRange(1)">全店商品</div>
    <div class="popup-range-line popup-range-tb" @click="setRange(2)">部分商品</div>
    <div class="popup-range-line pr--cancel" @click="hideLayer">取消</div>
  </section>
</template>
<style lang="less">
// css略
</style>
<script>
export default {
  props: { //调用方传的一些参数,通过父组件传进来
    options: {
      type: Object,
      default() {
        return {};
      }
    },
    close: {
      type: Function
    }
  },
  data() {
    return {
      showBg: false
    };
  },
  methods: {
    hideLayer() {
      this.close && this.close();
    },
    setRange(type) {
      this.options.callback && this.options.callback(type); //回调函数,直接在内部处理,这个在使用用时能看到效果
      this.hideLayer();
    }
  }
};
</script>
  1. 动画组件的js文件
// 弹层组件样式
import "./popup.less";

import Vue from "vue";
import Popup from "./popup.vue"; //动画组件本体

const doc = document;

class ModalMaker {
  constructor(modalComp) {
    this.instance = null;
    this.modalComp = modalComp; //参数挂载(这个参数是下面函数处理过后的父子组件嵌套:父Popup-子Module)
  }

  create(options) {
    this.instance = new this.modalComp({
      el: doc.createElement("div"),
      propsData: {
        afterLeave: () => {
          doc.body.removeChild(this.instance.$el);
          this.instance = null;
        },
        options,
      }
    });
    // 添加DOM 到body
    doc.body.appendChild(this.instance.$el);
    // 页面元素展示
    this.instance.show();
  }
  delete() {
    if (!this.instance) return false;
    this.instance.hide();
  }
}

export default function createModal(innerComponent) { //这个就是在上面真实组件js中拿到 & 使用的函数
  let modalComp = Vue.extend(Object.assign({}, Popup, {
    components: { innerComponent }
  }));

  return new ModalMaker(modalComp);
}
  1. 动画组件的vue文件

<template>
  <section class="mkt__modal">
    <transition name="modal-fade" @after-leave="afterLeave">
      <div v-if="showBg" class="mkt__modal-bg"></div>
    </transition>
    <transition name="modal-popup">
      <div v-if="showBg" class="mkt__popup-container" @click="onBGClick">
      	<!-- 这里被挂载的地方就是上面js中extend里面的components部分 -->
        <inner-component :options="options" :close="hide"></inner-component>
      </div>
    </transition>
  </section>
</template>
<style lang="less"></style>
<script>
export default {
  props: { //这里的传值是上面js文件的create函数中这个父子组件实例的propsData属性,通过调用方调用create函数传进来
    afterLeave: {
      type: Function
    },
    options: {
      type: Object,
      default() {
        return {};
      }
    }
  },
  data() {
    return {
      showBg: false
    };
  },
  methods: {
  	// 上面js中“显示元素”的this.instance.show();代码就是调用的这个函数
  	// 因为上面说了,modalComp已经是这个组件成型后的结构了,new的时候取实例可以调用其中的方法
  	// 这里也说明一件事:vue文件(编译后)实际上就是一段js模块化代码、是一个闭包、是一个构造函数!!!
    show() {
      this.showBg = true;
    },
    hide() {
      this.showBg = false;
    },
    onBGClick() {
      if (this.options.autoHide) {
        this.hide();
      }
    }
  }
};
</script>

这样以后,我们使用时就可以直接一个函数中完成:

//引入
import PopupGoodsRange from "真实弹层组件的js文件位置";

PopupGoodsRange.create({
  //其他参数
  callback: type => {
    if (type !== this.form.range) {
      this.form.range = type;
    }
  }
});

这样“小组件”之间的组合,使得其它组件使用动画也是如此方便:
微店-满包邮迭代

不怎么正的例子

前两天看到CSDN的活动页面有三个弹窗。虽然他们的功能定位不同,但是两个都是很突兀的展示而另一个是有一个过渡缓动动画,这就很厚此薄彼了吧😉:
csdn-抽奖活动页

当然,CSDN的产品或许有不同的考量,但是按照本文的思路来看,也就寥寥几行代码的事!


技术圈不怎么新鲜事

  • 微软由于“邮件过滤管理系统”使用的存储日期的格式问题导致了2022开年大bug
  • Vue3 和 vite 双加持 uni-app,期待表现/等待出丑
  • 从Chrome86开始对用户隐私表现出极大“兴趣”的Chrome从97版本开始支持Keyboard MAP API;Mozilla 则是将此 API 添加到了有害 API 列表中
  • Chrome98新增两个Header:Access-Control-Request-Private-NetworkAccess-Control-Allow-Private-Network ,意在保护用户免受针对私有网络上的路由器和其它设备的CSRF攻击。

  1. 关于这一点,笔者在前面有介绍过。不同的方式在不同场景中可能会有意想不到的效果:
    我对vue中组件通信的思考
    Vue生命周期和dom操作时机 ↩︎

Logo

前往低代码交流专区

更多推荐