关于组件设计:如何开发跨框架的组件

第 61 篇原创好文~本文首发于政采云前端团队博客:如何开发跨框架的组件背景题主所在的业务中台团队,须要提供业务组件给不同的下层业务方应用,但因为一些历史遗留问题,不同业务线应用的框架不对立,包含 jQuery、React 、Vue 。为了满足不同业务方的需要,往往须要依据业务方应用的框架,开发对应框架的组件。 这样做就会产生一些痛点: 每种选型都须要开发一次,费时劳力组件降级,须要业务方同步发版降级,沟通老本高、迭代效率低现实中的组件跨框架:Write once, run everywhere少降级:组件降级,业务方少降级不降级(留神:组件降级后业务线回归还是必要的)实现计划如何设计一个合乎下面跨框架、少降级冀望的通用计划呢? 很容易想到用原生 JS 来实现,防止跨框架的问题 原生实现用原生 JS 实现,蕴含页面里用到的 UI 组件,不依赖任何框架。 长处: 跨框架:不依赖于框架实现轻量:能够不依赖其余 UI 组件,体积较小毛病: 投入产出比低:实现一套常用工具办法和 UI 组件,投入工夫长踩坑:兼容性问题的坑要走一遍,危险大很难满足简单业务场景的需要实用场景: 不须要简单交互的场景,如前台吊顶、后盾菜单侧边栏可采纳这种形式。 但在理论的业务场景中,业务组件中有比拟多简单的交互场景, 下面的计划不太能满足要求,所以咱们在下面的计划之上进行迭代: 原生容器组件 + iframe 加载业务逻辑组件咱们将业务组件拆分为两局部 一、容器组件: 用原生 JS 实现中间层容器组件,解决跨框架的加载问题,容器组件次要负责: 收集组件须要的参数注册全局回调组件挂载加载 iframe二、业务逻辑组件 依据 iframe 人造的沙箱个性,业务逻辑用 iframe 页面加载,就保障了业务组件的实现不受框架的限度,能够完满解决问题。业务逻辑组件次要负责: 与容器组件通信运行环境隔离,能够应用任意框架实现业务逻辑毛病: 动静加载动态资源,iframe 加载略慢,理论体验在承受范畴内跨域通信问题此计划容器组件作为中间层,封装不变的逻辑,将多变的业务逻辑隔离进去,从而保障合作方尽量少降级或不降级。业务定制性可依据接口配置,返回不同的 iframe 地址,加载不同的业务逻辑组件,一次开发任意应用。 如何实现上面是整个组件的逻辑图 应用方通过容器组件初始化参数、并注册相应的回调: 容器组件 初始化: 设置 document.domain,让内部组件和 iframe 能够通信// 获取主域名function getTopLevelDomain(host) {    let data = host || window.location.host;    return data.split('.').slice(-2).join('.');}// 设置主域名function setDomainToTopLevelDomain() {  try {    window.document.domain = getTopLevelDomain();  } catch (error) {    console.error("设置domain失败")  }}render: ...

July 20, 2020 · 3 min · jiezi

Web-Components-系列教程

Web Components 开始不添加任何依赖来构建自己的定制组件带有样式,拥有交互功能并且在各自文件中优雅组织的 HTML 标签 https://developer.mozilla.org... Web Components是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的web应用中使用它们。 示例 https://github.com/mdn/web-co... polyfill https://www.webcomponents.org... https://github.com/webcompone... https://unpkg.com/browse/@web... npm install @webcomponents/webcomponentsjs <!-- load webcomponents bundle, which includes all the necessary polyfills --><script src="node_modules/@webcomponents/webcomponentsjs/webcomponents-bundle.js"></script><!-- load the element --><script type="module" src="my-element.js"></script><!-- use the element --><my-element></my-element>Web Component 是一系列 web 平台的 API,它们可以允许你创建全新可定制、可重用并且封装的 HTML 标签定制的组件基于 Web Component 标准构建,可以在现在浏览器上使用,也可以和任意与 HTML 交互的 JavaScript 库和框架配合使用。 它赋予了仅仅使用纯粹的JS/HTML/CSS就可以创建可重用组件的能力。如果 HTML 不能满足需求,我们可以创建一个可以满足需求的 Web Component。 举个例子,你的用户数据和一个 ID 有关,你希望有一个可以填入用户 ID 并且可以获取相应数据的组件。HTML 可能是下面这个样子: <user-card user-id="1"></user-card>Web Component 的四个核心概念HTML 和 DOM 标准定义了四种新的标准来帮助定义 Web Component。这些标准如下:定制元素(Custom Elements):web 开发者可以通过定制元素创建新的 HTML 标签、增强已有的 HTML 标签或是二次开发其它开发者已经完成的组件。这个 API 是 Web Component 的基石。HTML 模板(HTML Templates):HTML 模板定义了新的元素,描述一个基于 DOM 标准用于客户端模板的途径。模板允许你声明标记片段,它们可以被解析为 HTML。这些片段在页面开始加载时不会被用到,之后运行时会被实例化。Shadow DOM:Shadow DOM 被设计为构建基于组件的应用的一个工具。它可以解决 web 开发的一些常见问题,比如允许你把组件的 DOM 和作用域隔离开,并且简化 CSS 等等。HTML 引用(HTML Imports):HTML 模板(HTML Templates)允许你创建新的模板,同样的,HTML 引用(HTML imports)允许你从不同的文件中引入这些模板。通过独立的HTML文件管理组件,可以帮助你更好的组织代码。组件的命名定制元素的名称必须包含一个短横线。所以 <my-tabs> 和 <my-amazing-website> 是合法的名称, 而 <foo> 和 <foo_bar> 不行。在 HTML 添加新标签时需要确保向前兼容,不能重复注册同一个标签。 ...

August 21, 2019 · 11 min · jiezi

antd源码分析之折叠面板collapse

官方文档 https://ant.design/components... 目录一、antd中的collapse 代码目录 1、组件结构图(♦♦♦重要) 2、源码节选:antd/components/collapse/collapse.tsx 3、源码节选:antd/components/collapse/CollapsePanel.tsx 二、RcCollapse 代码目录 1、组件内部属性结构及方法调用关系图(♦♦♦重要) 2、组件应用的设计模式(♦♦♦重要) 3、源码节选:rc-collapse/Collapse.jsx 4、源码节选:rc-collapse/panel.jsx 一、antd中的collapseantd组件中有些使用了React 底层基础组件(查看具体列表点这里),collapse就是这种类型的组件 antd中collapse主要源码及组成结构如下,其中红色标注的Rc开头的组件是React底层基础组件 代码目录 1、组件结构图: 2、antd/components/collapse/collapse.tsxexport default class Collapse extends React.Component<CollapseProps, any> { static Panel = CollapsePanel; static defaultProps = { prefixCls: 'ant-collapse', bordered: true, openAnimation: { ...animation, appear() { } }, }; renderExpandIcon = () => { return ( <Icon type="right" className={`arrow`} /> ); } render() { const { prefixCls, className = '', bordered } = this.props; const collapseClassName = classNames({ [`${prefixCls}-borderless`]: !bordered, }, className); return ( <RcCollapse {...this.props} className={collapseClassName} expandIcon={this.renderExpandIcon} /> ); }}3、antd/components/collapse/CollapsePanel.tsxexport default class CollapsePanel extends React.Component<CollapsePanelProps, {}> { render() { const { prefixCls, className = '', showArrow = true } = this.props; const collapsePanelClassName = classNames({ [`${prefixCls}-no-arrow`]: !showArrow, }, className); return <RcCollapse.Panel {...this.props} className={collapsePanelClassName} />; }}二、RcCollapse由上述Collapse源码不难看出,折叠面板组件的实现逻辑主要在RcCollapse中,下面是核心代码、组件内部属性结构及方法调用关系图 ...

August 20, 2019 · 3 min · jiezi

????揭秘vuesfccli-组件研发利器

前言本文将揭示vue单文件组件的工具 vue-sfc-cli 的内涵,说明它是如何在整个组件研发流程中提升效率的。 本文可以看成是 ????vue组件发布npm最佳实践 的成长篇,是 ????打造自动化的Github Workflow 的姐妹篇,是团队最佳实践的落地产物,涉及的背景知识有点多,需要花点时间消化???? 使用教程快速开始npx vue-sfc-cli# 接下来会有一串的提示,请务必填写# 推荐kebab-case风格,小写字母,多个单词用-(dash)分隔,如my-component# 填充完提示后cd my-component# 使用git初始化,这样可以使用commit hookgit init# 安装依赖yarn# 开始开发yarn dev# 打包yarn build# 可以发布了!yarn publish参数选项-u, --upgrade根据 template目录下模板,生成新的文件,更新到当前组件中。使用的是覆盖策略,默认覆盖的文件定义在 update-files.js。常用于使用最新版本vue-sfc-cli对旧组件的配置进行升级 # cd my-componentnpx vue-sfc-cli -u--files如果想更新额外的文件,可以传此选项,后接文件名,多个文件使用 , 分隔 npx vue-sfc-cli -u --files package.json,.babelrc.js--test生成一个测试的组件模板,常用于ci环境测试。 npx vue-sfc-cli --test示例文档在docs目录下,新建 md 文件,建议命名同样是kebab-case 以上传组件upload-to-ali的 docs/draggable.md 文档为例  yarn dev 时会转这个markdown文件就会换成demo,可以看到实际代码,还可以实时修改代码,让demo刷新 API文档在vue文件里,编写注释,即可生成API文档。 props在props里使用多行注释 slot在slot上一行,使用  @slot 开头的注释 event在emit事件上方,使用多行注释 methods在要公开显示的方法上方,使用多行注释,并添加 @public 效果预览 引入第三方库以Element-UI为例 yarn add element-ui新增一个文件:styleguide/element.js import Vue from 'vue'import Element from 'element-ui'import 'element-ui/lib/theme-chalk/index.css'Vue.use(Element)修改配置文件:styleguide.config.js 环境变量如果需要使用环境变量,推荐使用 dotenv  yarn add dotenv --dev ...

July 7, 2019 · 2 min · jiezi

支持嵌套对象多级数组的Vue动态多级表单组件-vuedynamicformcomponent

方便不想看完全篇文章的童鞋,简单总结一下,这是篇软广,主要是推广自己在业务中沉淀的一个开源组件 vue-dynamic-form-component 。基于 element-ui 实现的 vue组件,只需编写类似 async-validator 的规则,自动生成对应的表单,支持常见输入类型的同时,支持嵌套对象、hashmap、多维数组等复杂类型。有需要的童鞋欢迎使用和贡献代码,顺便给个star(我也不知道为什么字体自动加黑了,不关我事) 前言几个月前,我在github开源了一个前端解析手机应用安装包(IPA 或 APK 文件)信息的工具 app-info-parser ,算是第一次正儿八经的做开源这件事,之后就有了半夜三四点回复issue、修bug的体验,说实话,上完班还要处理issue是挺累的,但也是乐在其中。正所谓开源一时爽,一直开源一直爽。 对于程序员而言,最不喜欢的事情,除了和产品经理 吵架 (交流,是交流,不是吵架,要peace)外,估计就是一直做重复的事情了。在程序界,有相当一部分开源工具都是为了把人从重复的事情中解放出来,去做更有趣、更能体现个人价值的事情。比如 AI智能回复老婆消息 (请勿随意尝试,老婆没了我不负责)。 之前开源的工具 app-info-parser 是减少重复工作,提高生产效率,接下来的主角 vue-dynamic-form-component 也是如此。 先贴一个展示大概功能的 gif ,动图有点大,如果加载不出来的话可以到 组件首页 查看。 左边是你需要编写的主要代码,右边是对应生成的表单。 背景(Why)为什么要做这个组件?其实在前言中已经提到:因为不想一直做重复、没有技术含量的事情。 对于本篇文章而言,这件重复、没有技术含量的事情就是:简单的表单代码 我所在的小组主要负责公司的公共服务系统搭建及维护,随之而来的便是一套接一套的CURD系统,目前业界已经存在很多优秀的UI库,比如为 Vue 而生的 element-ui, iView,基于 react 的 ant-design 等,已经在很大程度上提高了PC管理系统的开发效率,减少了很多重复工作。 但是对于表单功能,UI库出于通用性的考虑,实际使用中,对于简单的数据对象,我们仍然需要编写大量的表单代码来实现,因为出现了很多优秀的动态表单组件,比如 vue-form-generator,vue-form-making 那么为什么我还要再造一个类似的轮子?这其实要结合组内的技术栈来说: 由于组内的人员配置问题:前端1人(没错,就是孤独的我)、后端8人+,在技术栈上,选用了后端同事相对容易上手的 Vue ,基于 element-ui 开发管理系统。而目前已有的动态表单组件存在以下不适用的问题: vue-form-generator:设计思想很好,但是组件样式比较old school,同时对多级对象、多维数组等复杂数据支持不是很好,需要自己实现 field 组件,使用成本较高vue-form-making:也是基于element-ui, 样式统一,但是基于组件类型生成表单的方式不够灵活,只能利用已存在的输入组件,因此不支持多级对象等复杂类型其他的组件相对而言存在更多的问题,就不一一列举了。 以上就是为什么我会想要再造一个Vue的动态表单轮子,其实里面就已经包含了接下来我们要讲的: vue-dynamic-form-component 有什么作用? 功能(What)动态生成表单基于 async-validator 的规则来生成表单,只需要编写简单的声明配置,即可自动生成表单,只需要关注数据类型,无需关注数据类型对应何种输入组件 <template> <dynamic-form v-model="data" :descriptors="descriptors"> </dynamic-form></template>export default { data () { return { data: {}, descriptors: { date: { type: 'date', label: 'date \'s label', required: false }, number: { type: 'number', label: 'number \'s label', required: true, placeholder: 'please input the number' }, string: { type: 'string', label: 'string \'s label', required: true, pattern: /^test$/g }, url: { type: 'url', label: 'url \'s label', required: true, placeholder: 'please input the url' }, email: { type: 'email', label: 'email \'s label', required: false }, enum: { type: 'enum', label: 'enum\'s label', enum: ['value-1', 'value-2'] } } } }} ...

June 24, 2019 · 2 min · jiezi

Vue $mount实战--实现消息弹窗组件

之前的项目一直在使用Element-UI框架,element中的Notification、Message组件使用时不需要在html写标签,而是使用js调用。那时就很疑惑,为什么element ui使用this.$notify、this.$message就可以实现这样的功能?1、实现消息弹窗组件的几个问题如何在任何组件中使用this.$message就可以显示消息?如何将消息的dom节点插入到body中?同时出现多个消息弹窗时,消息弹窗的z-index如何控制?2、效果预览3、代码实现PMessage.vue<template> <transition name=“message-fade”> <div class=“p-message” :class="[type, extraClass]" v-show=“show” @mouseenter=“clearTimer” @mouseleave=“startTimer”> <div class=“p-message-container”> <i class=“p-message-icon” :class="p-message-icon-${type}"></i> <div class=“p-message-content”> <slot class=“p-message-content”> <div v-html=“message”></div> </slot> </div> </div> </div> </transition></template><script> // 绑定事件 function _addEvent(el, eventName, fn){ if(document.addEventListener){ el.addEventListener(eventName, fn, false); }else if(window.attachEvent){ el.attactEvent(‘on’ + eventName, fn); } }; // 解绑事件 function _offEvent(el, eventName, fn){ if(document.removeEventListener){ el.removeEventListener(eventName, fn, false); }else if(window.detachEvent){ el.detachEvent(‘on’ + eventName, fn); } }; export default { name: “PMessage”, data(){ return { type: ‘success’, duration: 3000, extraClass: ‘’, message: ‘’, timer: null, closed: false, show: false } }, methods: { startTimer(){ if(this.duration > 0){ this.timer = setTimeout(() => { if(!this.closed){ this.close(); } }, this.duration); } }, clearTimer(){ clearTimeout(this.timer); }, close(){ this.closed = true; if(typeof this.onClose === ‘function’){ // 调用onClose方法,以从p-message.js中的instances数组中移除当前组件,不移除的话就占空间了 this.onClose(); } }, // 销毁组件 destroyElement(){ _offEvent(this.$el, ’transitionend’, this.destroyElement); // 手动销毁组件 this.$destroy(true); this.$el.parentNode.removeChild(this.$el); }, }, watch: { // 监听closed,如果它为true,则销毁message组件 closed(newVal){ if(newVal){ this.show = false; // message过渡完成后再去销毁message组件及移除元素 addEvent(this.$el, ’transitionend’, this.destroyElement); } } }, mounted() { this.startTimer(); } }</script><style lang=“stylus”>@import “p-message.styl”</style>p-message.jsimport Vue from ‘vue’;import PMessage from ‘./PMessage.vue’;import {popupManager} from “../../common/js/popup-manager”;let PMessageControl = Vue.extend(PMessage);let count = 0;// 存储message组件实例,如需有关闭所有message的功能就需要将每个message组件都存储起来let instances = [];const isVNode = function (node) { return node !== null && typeof node === ‘object’ && Object.prototype.hasOwnProperty.call(node, ‘componentOptions’);};const Message = function (options) { options = options || {}; if(typeof options === ‘string’){ options = { message: options }; } let id = ‘message’ + ++count; let userOnClose = options.onClose; // PMsesage.vue销毁时会调用传递进去的onClose,而onClose的处理就是将指定id的message组件从instances中移除 options.onClose = function (){ Message._close(id, userOnClose); }; /* 这里传递给PMessageControl的data不会覆盖PMessage.vue中原有的data,而是与PMessage.vue中原有的data进行合并,类似 * 与mixin,包括传递methods、生命周期函数也是一样 / let instance = new PMessageControl({ data: options }); // 传递vNode if(isVNode(instance.message)){ instance.$slots.default = [instance.message]; instance.message = null; } instance.id = id; // 渲染元素,随后使用原生appendChild将dom插入到页面中 instance.$mount(); let $el = instance.$el; // message弹窗的z-index由popupManager来提供 $el.style.zIndex = popupManager.getNextZIndex(); document.body.appendChild($el); // 将message显示出来 instance.show = true; console.log(instance) instances.push(instance); return instance;};// message简化操作[‘success’,’error’].forEach(function (item) { Message[item] = options => { if(typeof options === ‘string’){ options = { message: options } } options.type = item; return Message(options); }});/* * 从instances删除指定message,内部使用 * @param id * @param userOnClose * @private /Message._close = function (id, userOnClose) { for(var i = 0, len = instances.length; i < len; i++){ if(instances[i].id === id){ if(typeof userOnClose === ‘function’){ userOnClose(instances[i]); } instances.splice(i, 1); break; } }};// 关闭所有messageMessage.closeAll = function () { for(var i = instances.length - 1; i >= 0; i–){ instances.close(); }};export default Message;popup-manager.jslet zIndex = 1000;let hasZIndexInited = false;const popupManager = { // 获取索引 getNextZIndex(){ if(!hasZIndexInited){ hasZIndexInited = true; return zIndex; } return zIndex++; }};export {popupManager};p-index.jsimport pMessage from ‘./p-message.js’;export default pMessage;p-message.styl.p-message{ position: fixed; top: 20px; left: 50%; padding: 8px 15px; border-radius: 4px; background-color: #fff; color: #000; transform: translateX(-50%); transition: opacity .3s, transform .4s; &.message-fade-enter, &.message-fade-leave-to{ opacity: 0; transform: translateX(-50%) translateY(-30px); } &.message-fade-enter-to, &.message-fade-leave{ opacity: 1; transform: translateX(-50%) translateY(0); } &.error{ color: #ff3737; } .p-message-icon{ / 使图标与内容能够垂直居中 / display: table-cell; vertical-align: middle; width: 64px; height: 45px; &.p-message-icon-success{ background: url("../../assets/images/icons/message-icon/icon_success.png") no-repeat 0 0; } &.p-message-icon-error{ background: url("../../assets/images/icons/message-icon/icon_error.png") no-repeat 0 0; } } .p-message-content{ / 使图标与内容能够垂直居中 */ display: table-cell; vertical-align: middle; padding-left: 15px; }}main.js// 引入pMessage组件import pMessage from ‘./components/p-message/p-index.js’;// 将pMessage绑定到Vue.prototype中。这样在组件中就可以通过this.$pMessage()的形式来使用了Vue.prototype.$pMessage = pMessage;3、参考参考了 Element-UI 的message代码封装Vue组件的一些技巧 ...

April 19, 2019 · 3 min · jiezi

基于react的高度过渡组件

开发中,最麻烦的过渡效果就是高度过渡。因为过渡效果需要设置一个起始状态和结束状态,但是对于web中的元素来说,容器高度大部分是根据内部元素自动撑起来的,这时很难去设置一个稳定的过渡高度。所以我写了一个基于react的组件来自动完成高度过渡这一实现。使用方法:首先npm install auto-height-transition -S具体使用:import { TransitionWhenHeightChange, TransitionWhenToggle} from ‘auto-height-transition’;// 在元素显示隐藏时进行过渡<TransitionWhenToggle duration={300} timeFunction={’ease’}> { this.state.show && ( <div> placeholder<br/> placeholder<br/> placeholder<br/> placeholder<br/> </div> ) }</TransitionWhenToggle>// 设置一个最小高度,然后切换进行过渡。通常用于 查看更多 显示详情<TransitionWhenHeightChange height={this.state.shouldCollapsed ? 20 : null} duration={300} timeFunction={’ease’}> <div> placeholder<br/> placeholder<br/> placeholder<br/> placeholder<br/> </div></TransitionWhenHeightChange>github地址:auto-height-transition

December 23, 2018 · 1 min · jiezi

Vue 中的受控与非受控组件

Vue 中的受控与非受控组件熟悉 React 的开发者应该对“受控组件”的概念并不陌生,实际上对于任何组件化开发框架而言,都可以实现所谓的受控与非受控,Vue 当然也不例外。并且理解受控与非受控对应的需求场景,可以让我们在设计一些基础组件时思路更加清晰,暴露出来的组件 API 也更加合理、统一。需求许多 UI 组件都是有状态(stateful)的,而这个状态是由组件外部控制还是组件内部维护,也就对应了受控与非受控两种模式。例如 Tabs 组件是很常见的一种 UI 组件,它的核心状态就是记录当前 active 的 Tab,并且允许用户切换。很多时候我们只希望 Tabs 可以正确的展示 active 的内容、并在用户操作时正常切换,不需要进行任何干预,那么就希望 只需要传入所有的 Tab 内容,不需要再做额外的配置。但有的时候我们又希望对 Tabs 的状态有很强的控制能力,例如多个关联的 Tabs,子级 Tabs 的内容需要根据父级 Tabs 的 active Tab 动态切换,这时候就会希望 Tabs 组件可以暴露足够充分的 API,来实现业务的需求。因此我们可以用一种通用的模式,来让任意组件的任意状态同时兼容受控与非受控两种模式,让不同需求场景下都可以使用最合理的 API。简化示例我们用一个简单的 Tabs 实现来演示这种通用的组件 API 设计模式,简化的部分包括:用 index 来作为 Tab 的唯一标识Tab content 只支持字符串可以打开 online DEMO 配合阅读API 设计对于 Vue 组件而言,API 设计主要指的是内部的 data, computed, methods 以及对外的 props, events。在这个示例中,我们会用 activeIdx 作为核心状态,所有的 API 也都会围绕这个状态命名。非受控模式如上文所说,非受控模式指的是使用者不需要关心控制组件的状体,完全交由组件内部维护。因此我们的 API 会包括:{ props: { defaultActiveIdx: { type: Number, default: 0 } }, data() { return { localActiveIdx: this.defaultActiveIdx } }, methods: { handleActiveIdxChange(idx) { this.localActiveIdx = idx; this.$emit(“active-idx-change”, idx); } }}localActiveIdx 是我们用来存放 active index 的组件内 data,对于非受控模式而言,虽然不希望在外部维护状态,但是仍有可能希望在外部决定初始状态,所以我们用 defaultActiveIdx 这个 props 决定 localActiveIdx 的初始值。之后当我们用 v-for="(tab, idx) in tabs" 指令生成所有的 Tab 时,就可以通过 idx === localActiveIdx 的方式判断当前 Tab 是否 active,再通过 @click=“handleActiveIdxChange(idx)” 就可以实现对 localActiveIdx 的更新。同样的,我们也可以通过 {{ tabs[localActiveIdx].content }} 展示 active Tab 的内容。需要注意的是在 handleActiveIdxChange 的事件处理中,我们也 emit 了 active-idx-change 这一事件,这样可以方便外部在不需要管理组件状态的同时也可以与组件状态保持同步。例如我们希望将 active Tab 反映在 URL 中,就可以在外部监听 active-idx-change 这一事件,并将当前 index 同步到路由中,在将路由中获取到的 index 作为 defaultActiveIdx 传入,就可以实现 URL 和 Tabs 的同步。受控模式对于受控模式来说,我们可以理解为 active index 是外部传入的 props,由外部自行维护其状态。因此我们只需要添加如下 props:props: { activeIdx: Number}由于我们已经有对外 emit 的事件 active-idx-change,所以外部用以下方式就可以用一个 data 属性 externalActiveIdx 维护对应状态:<tabs :tabs=“tabs” :activeIdx=“externalActiveIdx” @active-idx-change=“this.externalActiveIdx = $event”/>当然由于在这种模式下外部对状态有完全的控制权,所以在 active-idx-change 的事件处理中也可以做更为复杂的判断,例如是否允许激活目标 Tab 之类的校验。而在 Tabs 组件内部,我们还需要做一些小的修改。在受控模式中,我们所有状态相关的处理都是直接使用 localActiveIdx,而现在我们的逻辑应该变为“如果存在 activeIdx props,则使用,否则使用 localActiveIdx”。为了保证以上逻辑不会让我们的组件内部实现变得复杂、易错,我们引入一个 computed 属性:computed: { _activeIdx() { return this.activeIdx || this.localActiveIdx; }}这样我们就可以把状态相关的判断改为通过 idx === _activeIdx 判断一个 Tab 是否为激活状态,也通过 {{ tabs[_activeIdx].content }} 展示 active Tab 的内容。同样,我们在 handleActiveIdxChange 的方法内部也可以增加一个判断,如果存在 props aciveIdx 则不更新 localActiveIdx:handleActiveIdxChange(idx) { if (this.activeIdx === undefined) { this.localActiveIdx = idx; } this.$emit(“active-idx-change”, idx);}在一些更复杂的组件中,可能会频繁判断是否为受控模式并做不同的处理,这时候通过 this.activeIdx 这样的核心状态 props 是否传入来判断是否为受控模式是一个不错的实践。总结最终我们为 active index 设计的完整 API 如下:{ props: { activeIdx: Number, defaultActiveIdx: { type: Number, default: 0 } }, data() { return { localActiveIdx: this.defaultActiveIdx }; }, computed: { _activeIdx() { return this.activeIdx || this.localActiveIdx; } }, methods: { handleActiveIdxChange(idx) { if (this.activeIdx === undefined) { this.localActiveIdx = idx; } this.$emit(“active-idx-change”, idx); } }}通过这种 API 设计方式,可以让我们设计的基础组件使用方式更一致,拓展性更强,不论是开发还是使用时思路也会更加简洁清晰。 ...

December 17, 2018 · 2 min · jiezi