关于组件化:前端工程化中重要概念之组件化开发框架

一、组件化开发框架的基本概念组件化开发框架是一种将 UI 和性能逻辑封装为独立的可复用组件的开发方式。这些组件能够在不同的页面和我的项目中重复使用,从而实现代码的共享和可维护性的晋升。常见的组件化开发框架有 React、Vue、Angular 等。 二、组件化开发框架的原理组件化思维 组件化思维是指将 UI 和性能逻辑拆分为独立的模块,每个模块都能够独立地渲染和操作,从而进步代码的可维护性和重用性。在组件化开发框架中,每个组件都是一个独立的模块,能够被其余组件援用和调用。 残缺内容请点击下方链接查看: 前端工程化中重要概念之组件化开发框架 版权申明:本文内容由阿里云实名注册用户自发奉献,版权归原作者所有,阿里云开发者社区不领有其著作权,亦不承当相应法律责任。具体规定请查看《阿里云开发者社区用户服务协定》和《阿里云开发者社区知识产权爱护指引》。如果您发现本社区中有涉嫌剽窃的内容,填写侵权投诉表单进行举报,一经查实,本社区将立即删除涉嫌侵权内容。

April 20, 2023 · 1 min · jiezi

React中类组件和函数式组件

1. 函数式组件和类组件--初识1.1 函数式定义的无状态组件定义组件的最简单方法是编写JavaScript函数 function Welcome(props) { return <h1>Hello, {props.name}</h1>;}此函数是一个有效的React组件,因为它接受单个“props”(代表属性)对象参数与数据并返回一个React元素。我们称这些组件为“函数组件”,因为它们实际上是JavaScript函数。 1.2 es6形式的extends React.Component定义的组件class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; }}从React的角度来看,上述两个组件是等效的。 以上内容来自于react官方文档 1.3 es5原生方式React.createClass定义的组件class Welcome extends React.CreatClass { render() { return <h1>Hello, {this.props.name}</h1>; }}2. React创建组件的三种方式及其区别2.1 React创建组件的三种方式纯函数式定义的无状态组件React.createClass 定义的组件Extends React.Component 定义的组件2.2 纯函数式(无状态)组件的特点无状态组件式从React 0.14 版本开始的。为了创建纯展示组件,这种组件只负责根据传入的props来展示,不涉及到要state状态的操作。 其官方指出:在大部分React代码中,大多数组件被写成无状态的组件,通过简单组合可以构建成其他的组件等;这种通过多个简单然后合并成一个大应用的设计模式被提倡。 目前Reart已发展到16.9,引入了Hook,推荐使用无状态组件。 无状态函数式组件形式上表现为一个只带有一个render方法的组件类,通过函数形式或者ES6 arrow function的形式在创建,并且该组件是无state状态的。具体的创建形式如下:第一个参数是 props,第二个是 context function Welcome(props, context) { return <h1>Hello, {props.name}</h1>;}ReactDOM.render(<Welcome name="whongliang" />, mountNode) 无状态组件的创建形式使代码的可读性更好,并且减少了大量冗余的代码,精简至只有一个render方法,大大的增强了编写一个组件的便利,除此之外无状态组件还有以下几个显著的特点: 组件不会被实例化,整体渲染性能得到提升因为组件被精简成一个render方法的函数来实现的,由于是无状态组件,所以无状态组件就不会在有组件实例化的过程,无实例化过程也就不需要分配多余的内存,从而性能得到一定的提升。 组件不能访问this对象无状态组件由于没有实例化过程,所以无法访问组件this中的对象,例如:this.ref、this.state等均不能访问。若想访问就不能使用这种形式来创建组件 组件无法访问生命周期的方法因为无状态组件是不需要组件生命周期管理和状态管理,所以底层实现这种形式的组件时是不会实现组件的生命周期方法。所以无状态组件是不能参与组件的各个生命周期管理的。 无状态组件只能访问输入的props,同样的props会得到同样的渲染结果,不会有副作用无状态组件被鼓励在大型项目中尽可能以简单的写法来分割原本庞大的组件,未来React也会这种面向无状态组件在譬如无意义的检查和内存分配领域进行一系列优化,所以只要有可能,尽量使用无状态组件。 补充无状态组件内部其实是可以使用ref功能的,虽然不能通过this.refs访问到,但是可以通过将ref内容保存到无状态组件内部的一个本地变量中获取到。例如下面这段代码可以使用ref来获取组件挂载到dom中后所指向的dom元素: function TestComp(props){ let ref; return (<div> <div ref={(node) => ref = node}> ... </div> </div>)}2.3 React.createClassd的特点React.createClass是React刚开始推荐的创建组件的方式,这是ES5的原生的JavaScript来实现的React组件,其形式如下: ...

September 11, 2019 · 2 min · jiezi

深度理解slot

序言:slot是创建组件复用的强大工具,尽管不是很容易理解,我们来看一看slot是什么以及如何在应用中使用它。1、什么是slot?slot是vue组件的一种机制,允许你以严格父子关系以外的方式组合组件。slot为你提供了将内容放置在新位置或使组件更加通用。理解它们最好的办法就是通过实践,我们以一个最简单的例子开始: <template> <div> <component-b>hello world</component-b> </div></template><script> import vue from 'vue' const ComponentB = vue.component('component-b', { template:` <div><slot></slot></div> ` }); export default { data() { return { } }, components: { ComponentB } }</script>运行结果; 我们可以看到<component-b>标签里面的内容将会替换slot标签,这就是所谓的内容分发,slot标签起到的就是这个作用。 2、后备内容后备内容是指当你没有指定分发内容时,可以给slot指定一个内容。 <template> <div> <component-b>指定的分发内容</component-b> <component-b></component-b> </div></template><script> import vue from 'vue' const ComponentB = vue.component('component-b', { template:` <div><slot>我是一个默认值</slot></div> ` }); export default { data() { return { } }, components: { ComponentB } }</script>运行结果: ...

August 28, 2019 · 2 min · jiezi

Flutter网格型布局-GridView篇

1. 前言Flutter作为时下最流行的技术之一,凭借其出色的性能以及抹平多端的差异优势,早已引起大批技术爱好者的关注,甚至一些闲鱼,美团,腾讯等大公司均已投入生产使用。虽然目前其生态还没有完全成熟,但身靠背后的Google加持,其发展速度已经足够惊人,可以预见将来对Flutter开发人员的需求也会随之增长。 无论是为了现在的技术尝鲜还是将来的潮流趋势,都9102年了,作为一个前端开发者,似乎没有理由不去尝试它。正是带着这样的心理,笔者也开始学习Flutter,同时建了一个用于练习的仓库,后续所有代码都会托管在上面,欢迎star,一起学习。 经过上一篇对ListView组件的学习,我们已经对滚动型组件的使用有了初步认识,这对今天要学习的GridView组件十分有帮助。因为两者都继承自BoxScrollView,所以两者的属性有80%以上是相同的,用法非常相似。 而且如下图所示可见,GridView网格布局在app中的使用频率其实非常高,所以接下来就让我们来看看在Flutter中如何使用吧~ 2. 初识GridView今天我们的主角GridView一共有5个构造函数:GridView,GridView.builder,GridView.count,GridView.extent和GridView.custom。但是不用慌,因为可以说其实掌握其默认构造函数就都会了~ 来看下GridView构造函数(已省略不常用属性): GridView({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, @required this.gridDelegate, double cacheExtent, List<Widget> children = const <Widget>[],})虽然又是一大堆属性,但是大部分都很熟悉,老朋友嘛~除了一个必填参数gridDelegate外,全和ListView默认构造函数的参数一样,这也是文章开头为什么说掌握了ListView再学GridView非常容易的原因。 那么接下来,就让我们来重点关注下gridDelegate这个参数,它其实是GridView组件如何控制排列子元素的一个委托。跟踪源码我们可以在scroll_view.dart中看到,gridDelegate的类型是SliverGridDelegate,进一步跟踪进sliver_grid.dart可以看到SliverGridDelegate其实是一个抽象类,而且一共有两个实现类: SliverGridDelegateWithFixedCrossAxisCount:用于固定列数的场景;SliverGridDelegateWithMaxCrossAxisExtent:用于子元素有最大宽度限制的场景;2.1 SliverGridDelegateWithFixedCrossAxisCount我们先来看下SliverGridDelegateWithFixedCrossAxisCount,根据类名我们也能大概猜它是干什么用的:如果你的布局中每一行的列数是固定的,那你就应该用它。 来看下其构造函数: SliverGridDelegateWithFixedCrossAxisCount({ @required this.crossAxisCount, this.mainAxisSpacing = 0.0, this.crossAxisSpacing = 0.0, this.childAspectRatio = 1.0,})crossAxisCount:列数,即一行有几个子元素;mainAxisSpacing:主轴方向上的空隙间距;crossAxisSpacing:次轴方向上的空隙间距;childAspectRatio:子元素的宽高比例。 想必看到上面的示例图,你就秒懂其中各个参数的含义了。不过,这里有一点需要特别注意:如果你的子元素宽高比例不为1,那么你一定要设置childAspectRatio属性。 2.2 SliverGridDelegateWithMaxCrossAxisExtentSliverGridDelegateWithMaxCrossAxisExtent在实际应用中可能会比较少,来看下其构造函数: SliverGridDelegateWithMaxCrossAxisExtent({ @required this.maxCrossAxisExtent, this.mainAxisSpacing = 0.0, this.crossAxisSpacing = 0.0, this.childAspectRatio = 1.0,})可以看到除了maxCrossAxisExtent外,其他参数和SliverGridDelegateWithFixedCrossAxisCount都是一样的。那么maxCrossAxisExtent是干什么的呢?我们来看个例子: ...

July 15, 2019 · 1 min · jiezi

打怪升级小程序评论回复和发贴功能实战一

在学习成长的过程中,常常会遇到一些自己从未接触的事物,这就好比是打怪升级,每次打倒一只怪,都会获得经验,让自己进步强大。特别是我们这些做技术的,逆水行舟不进则退。下面分享下小程序开发中的打怪升级经历~ 先来看下实际效果图,小程序开发中有时会要做一些的功能复杂的组件,比如评论回复和发帖功能等,这次主要讲的是关于评论模块的一些思路和实战中的经验,希望能抛砖引玉,给大家一些启发,一同成长~ >>(最下面有实战demo的地址,可以直接浏览器打开添加至IDE工具中) << 根据这个demo.gif,本人做了一个简单的流程图,帮助大家理解。下面罗列一些开发中需要“打的怪”:1、组件目录结构├─components ---小程序自定义组件│ ├─plugins --- (重点)可独立运行的大型模块,可以打包成plugins│ │ ├─comment ---评论模块│ │ │ │ index.js│ │ │ │ index.json│ │ │ │ index.wxml│ │ │ │ index.wxss│ │ │ │ services.js ---(重点)用来处理和清洗数据的service.js,配套模板和插件 │ └─submit ---评论模块子模块:提交评论 index.js index.json index.wxml index.wxss为什么要单独做个评论页面页面(submit)?因为如果是当前页面最下面input输入的形式,会出现一些兼容问题,比如: 不同手机的虚拟键盘高度不同,不好绝对定位和完全适配弹窗输入框过小输入不方便,如果是大的textare时,容易误触下面评论的交。注:目录结构,仅供参考。 2、NODE端API接口返回结构和页面结构//node:API接口返回{ "data": { "commentTotal": 40, "comments": [ { "contentText": "喜欢就关注我", //评论内容 "createTime": 1560158823647, //评论时间 "displayName": "智酷方程式", //用户名 "headPortrait": "https://blz.nosdn.127.net/1/weixin/zxts.jpg", //用户头像 "id": "46e0fb0066666666", //评论ID 用于回复和举报 "likeTotal": 2, //点赞数 "replyContents": [ //回复评论 { "contentText": "@智酷方程式 喜欢就回复我", //回复评论内容 "createTime": 1560158986524, //回复时间 "displayName": "神秘的前端开发", //回复的用户名 "headPortrait": "https://blz.nosdn.127.net/1/2018cosplay/fourth/tesss.jpg", //回复的用户头像 "id": "46e0fb00111111111", //回复评论的ID "likeTotal": 2, //回复评论的点赞数 "replyContents": [], //回复的回复 盖楼 "replyId": "46e0fb001ec222222222", //回复评论的独立ID,用于统计 }, { "contentText": "@智酷方程式: 威武,学习学习", "createTime": 1560407232814, "displayName": "神秘的前端开发", "headPortrait": "https://blz.nosdn.127.net/1/2018cosplay/fourth/tesss.jpg", "id": "46e0fb00111111111", "likeTotal": 0, "replyContents": [], "replyId": "46e0fb001ec222222222", } ], "replyId": "", "topicId": "46e0fb001ec3333333", } ], "curPage": 1, //当前页面 //通过ID 判断 当前用户点赞了 哪些评论 "likes": [ "46e0fb00111111111", "46e0fb001ec222222222", "46e0fb0066666666", ], "nextPage": null, //下一页 "pageSize": 20, //一页总共多少评论 "total": 7, //总共多少页面 }, "msg": "success", "status": "success"}<!-- HTML 部分 --><block wx:if="{{commentList.length>0}}"> <!-- 评论模块 --> <block wx:for="{{commentList}}" wx:for-item="item" wx:for-index="index" wx:key="idx"> <view class="commentItem" catchtap="_goToReply" data-contentid="{{item.id}}" data-replyid="{{item.id}}" data-battle-tag="{{item.displayName}}"> <view class="titleWrap"> <image class="logo" src="{{item.headPortrait||'默认图'}}"></image> <view class="authorWrap"> <view class="author">{{item.displayName}}</view> <view class="time">{{item.createTime}}</view> </view> <view class="starWrap" catchtap="_clickLike" data-index="{{index}}" data-like="{{item.like}}" data-contentid="{{item.id}}" data-topicid="{{item.topicId}}"> <text class="count">{{item.likeTotal||""}}</text> <view class="workSprite icon {{item.like?'starIconHasClick':'starIcon'}}"></view> </view> </view> <view class="text"> {{item.contentText}} </view> </view> <!-- 评论的评论 --> <block wx:for="{{item.replyContents}}" wx:for-item="itemReply" wx:for-index="indexReply" wx:key="idxReply"> <view class="commentItem commentItemReply" catchtap="_goToReply" data-contentid="{{itemReply.id}}" data-replyid="{{item.id}}" data-battle-tag="{{itemReply.displayName}}"> ... 和上面类似 </view> </block> </block> <!-- 加载更多loading --> <block wx:if="{{isOver}}"> <view class="more">评论加载完成</view> </block></block>通过node提供一个API接口,通过用户的openId来判断是否点赞,这里提供一个参考的JSON结构。JSON尽量做成array循环的结构方便渲染,根据ID来BAN人和管理。底部加上加载更多的效果,同时,记得做一些兼容,比如默认头像等。 ...

July 11, 2019 · 3 min · jiezi

react之如何写一个管理自有状态的自定义组件

一、函数组件函数组件类似一个纯函数,接受外部传入的参数,生成并返回一个React元素(伪DOM)。例如,如下,Greeting作为一个组件,接受传入的参数name,并返回一个内容已填充的p标签。 function Greeting (props) { return ( <p> {props.name},how are you? </p> )}const element = <Greeting name="Agnes" />ReactDOM.render( element, document.getElementById('root'))二、class组件react中class组件的书写方式跟es6中类的书写方式非常接近,可以通过React.Compnent进行创建。与函数组件不同的是,该组件可以进行复杂逻辑的处理,既可以接受外部参数,也可以拥有自己的state,用于组件内的通信。 class HighGreeting extends React.Component { constructor(props){ super(props); this.state={ inputValue: this.props.name } this.handleInputChange = this.handleInputChange.bind(this); } render () { return ( <input type="text" onChange="handleInputChange"/> <p>{this.state.inputValue},how are you?</p> ) } handleInputChange(e){ let value = e.target.value; this.setState({ inputValue: value }) }} const element = <HighGreeting name="Agnes" />ReactDOM.render( element, document.getElementById('root'))上面的组件,接收props参数作为初始值,当用户输入时,会实时更新。 每次定义子类的构造函数时,都需要调用super方法,因此所有含有构造函数的React组件中,构造函数必须以super开头。调用super相当于初始化this.props。class组件内部可以定义state,相当于vue组件内的data,更改时需要调用this.setState,每次调用该方法时,都会执行render方法,自动更新组件。如上图,监听input的onchange事件,实时更改inputValue的值并展示。需要注意的是,props不仅可以传递值,还可以传递函数,甚至传递另一个组件。(这一点跟vue不一样,后面的文章会再细讲。)

July 10, 2019 · 1 min · jiezi

GrapeCity-Documents-for-Excel-与-Apache-POI-功能对比

GrapeCity Documents for Excel 是什么?GrapeCity Documents for Excel (简称为:GcExcel)是葡萄城推出的一款文档API组件,同时适用于 Java 和所有支持 .NET Standard 2.0 及以上标准的平台,以编码的方式,无需依赖任何 Microsoft Excel 组件,即可快速批量操作 Excel 文件,轻松满足您关于 Excel 电子表格的一切需求。 超快速、低占用率、更轻量,使用 GrapeCity Documents 可极大节省应用程序在生成、加载、编辑和保存大型文档时所占用的内存和时间,帮助企业以更高效的方式处理各种文档,实现更多定制化选项。 下载试用GrapeCity Documents for Excel (Java平台)下载试用GrapeCity Documents for Excel (.NET平台)Apache POI是什么?Apache POI 是由Java编写的一款免费开源的跨平台Java API,主要用于实现对Microsoft Office文档进行读、写的功能。POI为“Poor Obfuscation Implementation”首字母的缩写,意为“简单的模糊实现”。 GrapeCity Documents for Excel相对于Apache POI的主要优势1.公式数量支持GcExcel支持452种Excel公式,而在Apache POI中,支持的公式数量很少(虽然Apache POI网站罗列了280多种可评估的公式,但在API中仅显示为157种)。 2.导出PDFGcExcel支持导出为PDF格式,以及控制页面设置选项。 Apache POI不支持导出为PDF。 3.条件格式GcExcel支持更多条件格式规则,如自定义图标集、高于平均值(AboveAverage)、发生日期、Top 10和重复项,且这些条件格式规则与VSTO保持一致。但在Apache POI中,使用高级API支持条件格式会受到限制,例如,需要使用标记为内部用途的低级类来处理Top10、高于平均值(AboveAverage)等格式化。 4.图表类型支持GcExcel的图表界面与VSTO一致,支持约53种图表类型。 Apache POI对图表的支持非常有限,仅支持Line、Bar、Column、Scatter和Radar图表类型。 5.迷你图GcExcel完全支持添加和配置迷你图(Sparklines)。 Apache POI目前不支持Sparklines。 6.剪切、复制、粘贴形状GcExcel支持剪切、复制、粘贴形状,Apache POI不支持。 7.过滤器数据类型GcExcel广泛支持文本、数字、日期、颜色和图标等过滤器。 Apache POI仅支持基本的AutoFilter,需要使用低级类来实现应用过滤或创建任何其他高级过滤器。 ...

July 9, 2019 · 1 min · jiezi

Vue中jsx不完全应用指南

前言:文章不介绍任务背景知识,没有原理说明,偏向于实践的总结和经验分享。文章所有的代码是基于Vue CLI 3.x版本,不会涉及到一步步通过Webpack来配置JSX所需要的知识点。 在使用Vue开发项目时绝大多数情况下都是使用模板来写HTML,但是有些时候页面复杂又存在各种条件判断来显示/隐藏和拼凑页面内容,或者页面中很多部分存在部分DOM结构一样的时候就略显捉襟见肘,会写大量重复的代码,会出现单个.vue文件过长的情况,这个时候我们就需要更多的代码控制,这时候可以使用渲染函数。 渲染函数想必平时几乎没有人去写,因为写起来很痛苦(本人也没有写过)。更多的是在Vue中使用JSX语法。写法上和在React中差不多,但是功能上还是没有React中那么完善。 在写JSX的过程中不得考虑一个样式的问题,虽然可以直接在.vue文件中不写<tempate>部分,只写<script>和<style>部分,而不用担心样式作用域问题。但是更多的时候还是推荐直接使用.js的方式来写组件,这个时候就涉及到样式作用域的问题了。 在React的生态中,有很多CSS-IN-JS的解决方案,比如styled-jsx、emotion、styled-components等,目前最活跃和用户量最多的是styled-components,目前已经拥有良好的生态圈子。如果需要在样式中作一些像Sass/Less中的颜色计算,可以使用polished来实现,当然不止这么简单的功能。但是在Vue中可使用的方案就太少了,因为Vue使用模板来写HTML本身是开箱即用的样式scoped,在使用JSX写组件的时候就面临着样式问题,一种方案是在组件包裹<div>中取一个特殊的名字,然后样式都嵌套写在这个class下面,但是难免会遇到命名冲突的情况,而且每次还得变着花样取名称。此外,就是引入CSS-IN-JS在Vue对应的实现,但目前来看Styled-components官方提供了一个Vue版本的叫vue-styled-components和emotion的vue-emotion,但是用的人实在太少。像styled-components进行了重大更新和变化,但是Vue版本的还是最初的版本,而且有时候还出现样式不生效的情况。 接下来进入正题,从简单语法到经验分享(大牛请绕行) 基本用法首先需要约定一下,使用JSX组件命名采用首字母大写的驼峰命名方式,样式可以少的可以直接基于vue-styled-components写在同一个文件中,复杂的建议放在单独的_Styles.js_文件中,当然也可以不采用CSS-IN-JS的方式,使用Less/Sass来写,然后在文件中import进来。 下面是一个通用的骨架: import styled from 'vue-styled-components'const Container = styled.div` heigth: 100%;`const Dashboard = { name: 'Dashboard', render() { return ( <Container>内容</Container> ) }}export default Dashboard插值在JSX中使用单个括号来绑定文本插值 <span>Message: {this.messsage}</span><!-- 类似于v-html --><div domPropsInnerHTML={this.dangerHtml}/><!-- v-model --><el-input v-model={this.vm.name} />在jsx中不需要把v-model分成事件绑定和赋值二部分分开来写,因为有相应的babel插件来专门处理。 样式在JSX中可以直接使用class="xx"来指定样式类,内联样式可以直接写成style="xxx" <div class="btn btn-default" style="font-size: 12px;">Button</div><!-- 动态指定 --><div class={`btn btn-${this.isDefault ? 'default' : ''}`}></div><div class={{'btn-default': this.isDefault, 'btn-primary': this.isPrimary}}></div><div style={{color: 'red', fontSize: '14px'}}></div>遍历在JSX中没有v-for和v-if等指令的存在,这些全部需要采用Js的方式来实现 {/* 类似于v-if */}{this.withTitle && <Title />}{/* 类似于v-if 加 v-else */}{this.isSubTitle ? <SubTitle /> : <Title />}{/* 类似于v-for */}{this.options.map(option => { <div>{option.title}</div>})}事件绑定事件绑定需要在事件名称前端加上on前缀,原生事件添加nativeOn ...

July 4, 2019 · 5 min · jiezi

用-Vue-编写抽象组件

看过 Vue 源码的同学可以知道,<keep-alive>、<transition>、<transition-group>等组件组件的实现是一个对象,注意它有一个属性 abstract 为 true,表明是它一个抽象组件。 Vue 的文档没有提这个概念,在抽象组件的生命周期过程中,我们可以对包裹的子组件监听的事件进行拦截,也可以对子组件进行 Dom 操作,从而可以对我们需要的功能进行封装,而不需要关心子组件的具体实现。 <!-- more --> 下面实现一个 debounce 组件,对子组件的 click 事件进行拦截 核心代码如下: <script>import {get, debounce, set} from 'loadsh';export default { name: 'debounce', abstract: true, //标记为抽象组件 render() { let vnode = this.$slots.default[0]; // 子组件的vnode if (vnode) { let event = get(vnode, `data.on.click`); // 子组件绑定的click事件 if (typeof event === 'function') { set(vnode, `data.on.click`, debounce(event, 1000)); } } return vnode; }};</script>使用 <debounce> <button @click="clickHandler">测试</button></debounce>可以看到,按钮的 click 事件已经加上了去抖(debounce)操作。 ...

May 6, 2019 · 1 min · jiezi

Angualr-8-事件绑定

一般格式(event)="模板语句"例如: (click)="onClick()"(click)="hidden=false"两种写法都是合法的 $event 对象$event 对象为 DOM 事件对象,一般经常使用到 event.target.value 获取当前元素的值。 $event 包含大量的信息,而其实绝大多数情况下,我们仅仅需要使用 event.target.value,因此,应该尽量避免使用 $event 传递值。 当你使用 $event 对象时需要注意, $event 对象总是有一个对应的类型,所以并不推荐到处使用 any 类型来偷懒,如果不知道类型所对应的名称是什么,可以尝试打印 typeof event 查看。 使用 $event 的小例子: <input (keyup)="onKey($event)">模板引用变量 #var我们在 Angular 组件 中已经使用过了 模板引用变量。 模板引用变量的感觉比较像 DOM 元素变量化。 <input #box (keyup)="onKey(box.value)">如此就可以将 box 作为 DOM 元素本身来使用了,相对于 $event ,代码更加 “可读”。 绑定 “enter 事件”<input #box (keyup.enter)="onEnter(box.value)">自定义组件事件.html <input #textbox type="text" (keyup)="onKeyUp(textbox.value)">.ts @Output("onKeyUp") keyUp: EventEmitter<string> = new EventEmitter();public onKeyUp(v: string): void { console.log(v);}使用 ...

May 6, 2019 · 1 min · jiezi

用vuereact写一个全局提示弹框

vue的实现方法1、写一个Toast组件Toast.vue <template> <div class="toast" v-if="show"> <div class="box"> <div class="title">{{title}}</div> <div class="content">{{content}}</div> <div class="btn" @click="callback()">{{btn}}</div> </div> </div></template><script>export default { name: "Toast", data() { return { show: true }; }};</script><style scoped>.toast { position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: 99; font-size: 14px;}.box { height: 130px; width: 240px; border: 1px solid #ccc; border-radius: 4px; position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);}.title,.content { line-height: 30px; padding: 0 10px;}.title { color: #666; border-bottom: 1px solid #ccc;}.btn { display: inline-block; padding: 4px 10px; color: gray; border: 1px solid #ccc; border-radius: 2px; position: absolute; bottom: 10px; left: 50%; transform: translate(-50%); cursor: pointer;}</style>组件中除了拥有是否展现自身的show属性以外其他属性都没有被定义,这些属性将在下面的toast.js中通过Vue.extend出来的实例构造器的实例化对象传入。 ...

May 5, 2019 · 2 min · jiezi

手把手教你写一个微信小程序日历组件

今天我们一起写一个微信小程序日历组件 微信小程序日历组件https://github.com/749264345/wx-calendar好,我们先看一下要实现的模样,如下图由以上截图我们可以看到 1.日历可以通过按钮【切换展示效果】改变日历的呈现效果,上图是平铺模式,下图是收起滚动模式。2.通过点击具体的日期可以在页面上显示当前选中的具体日期。3.点击【今天】快速回到当日视图。4.点击【◀】和【▶】切换月份。上面的四点也是基本的交互需求,我们马上开始。首先,我们先结构后样式,做出最基本的界面结构这边我们把整体结构分成上中下,操作显示区,星期显示区,日期显示区。<view class='calendar'> <!--显示当前年月日--> <view class='calendar-title'> <view class='item ctrl' bindtap='lastMonth'>{{lastMonth}}</view> <view class='item title'>{{title}}</view> <view class='item ctrl' bindtap='nextMonth'>{{nextMonth}}</view> <view class='item ctrl today' bindtap='today'>今天</view> </view> <!--星期--> <view class='calendar-week'> <view class='item'>{{item}}</view> </view> <!--日期--> <view class='calendar-container'> <!--上个月占位格子--> <view class='grid gray'>{{item}}</view> <!--当月格子--> <view class='grid'> <view class="wrap">{{item.date}}</view> </view> <!--下个月占位格子--> <view class='grid gray'>{{item}}</view> </view></view>这是我们基本的日历结构,机智的小伙伴已经从布局中知道我们实现的大致逻辑了,是的,我们先获取当月有多少天,上月和下月有多少天,这样我们的日历就出来了。好,慢慢来,下面我们详细说,我们先写上基本的样式。 .calendar { width: 100%; text-align: center; font-size: 30rpx; box-sizing: border-box;}/* 标题 */.calendar-title { line-height: 70rpx; font-size: 30rpx; text-align: left; padding: 0 20rpx; box-sizing: border-box;}.calendar-title .ctrl { display: inline-block; padding: 0 20rpx; background: #f5f5f5; border-radius: 10rpx;}.calendar-title .item { display: inline-block; vertical-align: middle; line-height: 50rpx;}.calendar-title .title { min-width: 300rpx; text-align: center;}.calendar-title .today { float: right; margin-top: 10rpx;}/* 星期 */.calendar-week { display: flex; text-align: center; padding: 20rpx 10rpx; box-sizing: border-box; border-top: 1rpx solid #e0e0e0; border-bottom: 1rpx solid #e0e0e0; background: #f5f5f5;}.calendar-week .item { flex: 1;}/* 日期 */.calendar-container { display: flex; flex-wrap: wrap; padding: 20rpx 10rpx; box-sizing: border-box;}.calendar-container .grid { display: inline-block; width: 14.28571428571429%; line-height: 70rpx; position: relative; z-index: 1;}.calendar-container .grid.gray { color: #ccc;}.calendar-container .grid .wrap.select { background: rgb(49, 120, 228); border-radius: 10rpx; color: #fff; width: 80%; margin: 0 auto;}以上我们基本试下了日历的界面,下面我们来实现星期和日期的展示。好,我们先显示星期,我们先在组件中定义一个数组,用来遍历显示星期的标题; ...

May 2, 2019 · 3 min · jiezi

前端性能优化02vue性能优化

一、template语义化标签,避免乱嵌套,合理命名属性等等标准推荐的东西就不谈了。 模板部分帮助我们展示结构化数据,vue 通过数据驱动视图,主要注意一下几点 v-show,v-if 用哪个?在我来看要分两个维度去思考问题,第一个维度是权限问题,只要涉及到权限相关的展示无疑要用 v-if ,第二个维度在没有权限限制下根据用户点击的频次选择,频繁切换的使用 v-show ,不频繁切换的使用 v-if ,这里要说的优化点在于减少页面中 dom 总数,我比较倾向于使用 v-if ,因为减少了 dom 数量,加快首屏渲染,至于性能方面我感觉肉眼看不出来切换的渲染过程,也不会影响用户的体验。不要在模板里面写过多的表达式与判断 v-if="isShow && isAdmin && (a || b)" ,这种表达式虽说可以识别,但是不是长久之计,当看着不舒服时,适当的写到 methods 和 computed 里面封装成一个方法,这样的好处是方便我们在多处判断相同的表达式,其他权限相同的元素再判断展示的时候调用同一个方法即可。循环调用子组件时添加 key。 key 可以唯一标识一个循环个体,可以使用例如 item.id 作为 key,假如数组数据是这样的 ['a' , 'b', 'c', 'a'],使用 :key="item" 显然没有意义,更好的办法就是在循环的时候 (item, index) in arr ,然后 :key="index" 来确保 key 的唯一性。在列表数据进行遍历渲染时,给每一项item设置唯一key值,会方便vuejs内部机制精准找到该条列表数据。当state更新时,新的状态值和旧的状态值对比,较快地定位到diff。二、style将样式文件放在 vue 文件内还是外?讨论起来没有意义,重点是按模块划分,我的习惯是放在 vue 文件内部,方便写代码是在同一个文件里跳转上下对照,无论内外建议加上 <style scoped> 将样式文件锁住,目的很简单,再好用的标准也避免不了多人开发的麻烦,约定命名规则也可能会冲突,锁定区域后尽量采用简短的命名规则,不需要 .header-title__text 之类的 class,直接 .title 搞定。为了和上一条作区分,说下全局的样式文件,全局的样式文件,尽量抽象化,既然不在每一个组件里重复写,就尽量通用,这部分抽象做的越好说明你的样式文件体积越小,复用率越高。建议将复写组件库如 Element 样式的代码也放到全局中去。不使用 float 布局,之前看到很多人封装了 .fl -- float: left 到全局文件里去,然后又要 .clear,现在的浏览器还不至于弱到非要用 float 去兼容,完全可以 flex,grid 兼容性一般,功能其实 flex 布局都可以实现,float 会带来布局上的麻烦,用过的都知道。至于其他通用的规范这里不赘述。三、script这部分也是最难优化的点,说下个人意见吧。 ...

April 30, 2019 · 2 min · jiezi

组件化实践

最近想了解一些组件化的知识,去看了Casa写的iOS应用架构谈 组件化方案这篇文章,Casa在文中针对蘑菇街的组件化方案提出了一些不同的观点,陈述了自己的组件化方案。 大神们讨论具体的实施方案,是对理论的描述,在架构层面来分析利弊,我看过之后感觉还是有点晦涩,具体的方案异同之处我们先不说,今天我们先从应用着手,在自己当前的工程实施组件化。 当然了,我们选择使用的方案是Casa的CTMediator。 准备首先我们得先了解组件化这个概念,其实通俗的讲,就是把我们的项目拆解成一个一个的小组件分别管理。我们平时使用cocoapods继承的三方的库,可以理解成是一个公有的组件。我们项目中,也可以把一些模块拆解出来,使用cocoapods来集成。这样拆解成一个个的组件的好处有很多,比如说业务模块之间解耦,复用模块,节省编译时间等等。 所以我们要先学会创建cocoapods私有库。 这里多说一句,Casa的组件化方案在实施的时候,每独立出来一个组件,就会相应的创建一个Category工程,作为中间的调度,所以说,我们每做一个组件,就要创建两个私有的pod工程。 我们结合Casa这篇在现有工程中实施基于CTMediator的组件化方案,来做一下补充或者说是注解吧,本文中的流程取自于上文。 创建私有Pod工程1. 先去开一个repo,这个repo就是我们私有Pod源仓库2. pod repo add [私有Pod源仓库名字] [私有Pod源的repo地址]3. 创立一个文件夹,例如Project。把我们的主工程文件夹放到Project下:~/Project/MainProject4. 在~/Project下clone快速配置私有源的脚本repo:git clone git@github.com:casatwy/ConfigPrivatePod.git5. 将ConfigPrivatePod的template文件夹下Podfile中source 'https://github.com/ModulizationDemo/PrivatePods.git'改成第一步里面你自己的私有Pod源仓库的repo地址6. 将ConfigPrivatePod的template文件夹下upload.sh中PrivatePods改成第二步里面你自己的私有Pod源仓库的名字首先我们先创建一个名为Project的文件,然后把我们项目的主程序,我们叫做MainProject放到Project路径下,然后在Project路径下clone出我们需要的脚本(Casa提供) 在~/Project下clone快速配置私有源的脚本:git clone git@github.com:casatwy/ConfigPrivatePod.git现在我们的文件目录结构是这样的。 Project├── ConfigPrivatePod(脚本文件)└── MainProject在Project路径下创建我们的组件工程(一个普通的iOS工程),我们把这个工程名字叫PayComponents (模拟抽取项目中的支付模块)。 当前目录结构 Project├── ConfigPrivatePod├── MainProject└── PayComponents有了本地的工程之后,我们现在需要创建一个repo,作为我们的私有pod源仓库。也就是在github,或者gitee(码云)上面创建一个项目,放我们的项目代码,命名PayComponents。 然后呢,我们还需要创建一个东西,就是私有Pod源仓库名字。 pod repo add [私有Pod源仓库名字] [私有Pod源的repo地址]落实到我们这个项目中,我们应该这样写。 pod repo add PayComponents https://gitee.com/LittleBin/PayComponents.git那这到底代表着我们创建了什么? 我们打开finder->前往->前往文件夹,然后输入~/.cocoapods/repos 可以看到目录是这样子的 repos路径下面有一个master,一个payComponents。这两个文件夹我们可以粗略的认为他和pod search还有install有关。 打个比方就拿search来说,我们查询一个库的时候会用下面这个命令 pod search AFNetworking然后会从master路径下找到AFNetworking,然后列出来他有哪些版本什么的。我们有的时候会发现一个库其实已经跟新到2.x.x版本,但是我们search出来只有1.x.x,这也可能是我们的cocoapods没有更新,我们的master路径下没有新的版本。 这个PayComponents文件夹,就代表我们本地有一个私有的pod库。我们search的时候,也会查这些本地的私有库。 下面把远程的这个repo和我们本地创建的项目关联到一起,这个工作Casa给我们提供的脚本就可以完成,顺便还会帮我们生成.podspec的文件,具体这个文件的作用我们后面再说,还会初始化podfile。 我们进到ConfigPrivatePod文件中,执行config.sh脚本,然后在终端根据提示输入就行了。 [localhost:ConfigPrivatePod sunxiaobin$ ./config.sh Enter Project Name: PayComponentsEnter HTTPS Repo URL: https://gitee.com/LittleBin/PayComponents.gitEnter SSH Repo URL: git@gitee.com:LittleBin/PayComponents.gitEnter Home Page URL: https://gitee.com/LittleBin/PayComponents================================================ Project Name : PayComponents HTTPS Repo : https://gitee.com/LittleBin/PayComponents.git SSH Repo : git@gitee.com:LittleBin/PayComponents.git Home Page URL : https://gitee.com/LittleBin/PayComponents================================================confirm? (y/n):ycopy to ../PayComponents/FILE_LICENSEcopy to ../PayComponents/.gitignorecopy to ../PayComponents/PayComponents.podspeccopy to ../PayComponents/readme.mdcopy to ../PayComponents/upload.shcopy to ../PayComponents/Podfileediting...edit finishedcleaning...Initialized empty Git repository in /Users/fmb/Documents/LEARN/Project_test/PayComponents/.git/clean finishedfinishedlocalhost:ConfigPrivatePod sunxiaobin$ Enter Project Name:的时候,后面的名字一定要跟我们创建的PayComponents工程一样,要不然脚本找不到文件,就配置不了。 ...

April 28, 2019 · 2 min · jiezi

低复杂度多选框设计与实现

引言还是性能的问题,数据量大的时候,特别得卡。上算法课,也没找到一种性能很优的算法,最终使用Map重新设计了一下,并使用原生的checkbox,性能有极大地提升,用户感觉不出任何卡顿。优化实践原组件性能分析<nz-checkbox-wrapper style=“width: 100%;” (nzOnChange)=“change($event)"> <div nz-row> <div nz-col nzSpan=“4” ngFor=“let host of hostListValues”> <label nz-checkbox [nzValue]=“host.value” [(ngModel)]=“host.checked”>{{host.label}}</label> </div> </div></nz-checkbox-wrapper>this.hostService.getAllHosts().subscribe((hosts) => { this.hostListValues = []; // 获取主机数量 const length = hosts.length; // 使用主机信息构造多选框绑定数据 for (let index = 0; index < length; index++) { this.hostListValues.push({ label: hosts[index].name, value: hosts[index], checked: HostCheckboxComponent.existIn(hosts[index], this._hostList) }); }});根据计算机列表构造符合规范的数组(每次循环都需要判断是否选中)。html中ngFor。每点击一次,就输出一次nzOnChange事件,因为该事件的参数是当前选中的计算机列表,所以应该也进行循环了。使用原生checkbox使用原生的checkbox,我们就不需要再去循环构建符合ng zorro要求格式的数据了,直接就把计算机列表传给页面。<nz-row> <nz-col ngFor=“let host of hosts” [nzSpan]=“4”> <label> <input type=“checkbox”> {{ host.name }} </label> </nz-col></nz-row>默认选中的设计原来的默认选中复杂度太高。假设2000台计算机,100个默认选中的,最终执行次数就是2000 * 100。for 计算机列表 for 默认选中计算机列表怎样降低复杂度呢?最终想到了使用Map,毕竟Map查询的复杂度是要比自己for循环低的多的。设计一个checkedMap,该Map中存储了所有被选中的主机的id。/ * 计算机选中的Map存储 /public checkedMap: Map<number, boolean>;<nz-row> <nz-col ngFor=“let host of hosts” [nzSpan]=“4”> <label> <input type=“checkbox” (change)=“syncHostCheckedMap(host.id)” [checked]=“checkedMap.get(host.id)"> {{ host.name }} </label> </nz-col></nz-row>再分析一下,2000台计算机,100台默认选中的,因为Native Map的高性能,应该能提升部分性能。组件输出@Output()hostCheck = new EventEmitter<Array<Host>>();因为多选框写成了组件,组件其实并不知道用我的页面什么时候要我的数据,所以使用事件输出的,当用户选中的内容有变化时,我就for循环一次所有的计算机,然后把选中的输出出去。现在则是写一个public方法,供外部调用。/ * 获取所有选中的计算机列表 * 供外部调用 /public getAllCheckedHostList(): Array<Host> { // 初始化计算机列表 const hostList: Array<Host> = new Array<Host>(); // 遍历选中的Map this.checkedMap.forEach((value, key) => { // 遍历计算机列表 this.hosts.forEach((host) => { // 如果符号位,添加到列表中 if (host.id === key) { hostList.push(host); } }); }); // 返回 return hostList;}怎么调的呢?以下是示例代码:<app-host-checkbox #hostCheckbox [primaryHostList]=“primaryHostList”></app-host-checkbox>/* * 获取HostCheckboxComponent组件 /@ViewChild(‘hostCheckbox’)private hostCheckbox: HostCheckboxComponent;/* * 计算机组更新方法 * @param hostGroup 计算机组 */public update(hostGroup: HostGroup): void { // 从组件中拿去选中的计算机的列表 hostGroup.hostList = this.hostCheckbox.getAllCheckedHostList(); // 请求后台更新计算机组}ViewChild - Angular.ioAngular官网说ViewChild用于视图查询,我理解就是把用到的组件注进来,和组件的交互不仅限于输入输出,还可以调用组件对外暴露的方法。总结当编码已经不成问题的时候,我们真正可以当语言为工具,通过我们的思考,构造一个又一个实用的设计。 ...

March 15, 2019 · 1 min · jiezi

Vue动态组件和异步组件

动态组件如果我们打算在一个地方根据不同的状态引用不同的组件的话,比如tab页,那么Vue给我们提供动态组件。基本使用Parent.vue<template><div> <el-button-group> <el-button v-for=’(btn, index) in btnGroup’ :key=“index” :class="{active:btn.disabled}" @click=‘change(index)’>{{btn.name}} </el-button> </el-button-group> <div> <component :is=‘currentCom’></component> </div></div></template><script>import Childs1 from ‘./Childs1’import Childs2 from ‘./Childs2’import Childs3 from ‘./Childs3’import Childs4 from ‘./Childs4’export default { name:‘Parent’, components:{ Childs1, Childs2, Childs3, Childs4 }, data() { return { btnGroup: [ {name: ‘Childs1’, disabled: true}, {name: ‘Childs2’, disabled: false}, {name: ‘Childs3’, disabled: false}, {name: ‘Childs4’, disabled: false}, ], currentCom:‘Childs1’ } }, methods: { change(index){ let pre = Number(this.currentCom[this.currentCom.length -1]); this.btnGroup[pre -1].disabled = false; this.btnGroup[index].disabled = true; this.currentCom = ‘Childs’ + (index + 1); } }}</script><style scoped>.active{ background-color: red;}</style>运行结果如下图:当我们点击不同的按钮时,下面会切换不同的组件。实现动态组件的加载。is 的值可以是一个已经注册的组件的名字或者一个组件的选对象。当我们点击按钮时,这个按钮的 disabled 为 true 然后我们将给这个按钮一个active 的css类,同时改变 currentCom 的值keep-alive:动态组件的缓存如果我们需要频繁的切换页面,每次都是在组件的创建和销毁的状态间切换,这无疑增大了性能的开销。那么我们要怎么优化呢? Vue提供了动态组件的 缓存。keep-alive 会在切换组件的时候缓存当前组件的状态,等到再次进入这个组件,不需要重新创建组件,只需要从前面的缓存中读取并渲染。Parent.vue(其余地方代码和上面一样)<template><div> <el-button-group class=‘btn-group’> <el-button v-for=’(btn, index) in btnGroup’ :key=“index” :class="{active:btn.disabled}" @click=‘change(index)’> {{btn.name}} </el-button> </el-button-group> <div style=‘padding-top:100px;’> <keep-alive> <component :is=‘currentCom’></component> </keep-alive> </div></div></template><style scoped>.btn-group{ position:fixed;}.active{ background-color: red;}</style>Childs1.vue<template> <div> {{title}} <button @click=‘change’>点我+1</button> </div></template><script>export default { name:‘Childs1’, data(){ return{ title: 1 } }, methods:{ change(){ this.title += 1; } }, mounted(){ console.log(‘child1 mounted’); }}</script>Childs2.vue<template> <div> Childs2 </div></template><script>export default { name:‘Childs2’, mounted(){ console.log(‘child2 mounted’); }}</script>运行结果如下图: ‘对比:如果我们将<keep-alive></keep-alive>去掉,运行结果如下图:前一组图片在切换组件的时候,title从1加到3,然后等下次再切换回来的时候,title还是停留在3,从控制台可以看出,Childs1.vue这个组件的mounted的钩子函数只有一次。后一组图片,title一开始加到3,下一次进入这个组件的时候title又从1开始,控制台图片也显示这个组件经历个了多次钩子函数,说明组件是销毁重建的。 tips:因为缓存的组件只需要建立一次,所以如果我们要在每次进入组件的钩子函数里面做相应的操作的时候,会出现问题,所以请明确我们使用的场景,避免出现bug异步组件异步组件存在的意义在于加载一个体量很大的页面时,如果我们不设置加载的优先级的话,那么可能页面在加载视频等信息的时候会非常占用时间,然后主要信息就会阻塞在后面在加载。这对用户来说无疑不是一个很差的体验。但是如果我们设置加载的顺序,那么我们可以优先那些最重要的信息优先显示,优化了整个项目。一般来说我们是将加载组件和 路由 (vue-router)配合在一起使用,所以这里我就不细讲了,具体学习可以参考官网来进行学习。 ...

January 25, 2019 · 1 min · jiezi

Vue父子组件通信的三两事(prop、emit)

组件是Vue核心功能之一,合理的组件化,可以减少我们代码的冗余,提高项目的可维护性。下面,我将由浅入深的讲Vue的组件在讲之前,首先我们先了解一下组件的命名。 HTML是对特征名不敏感的语言,他会将所有的字符全部转换成小写。我们命名了一个组件的名称为 nameTest ,然后再其他组件里面引用 <nameTest> </nameTest> ,那么我们将找不到这个组件,因为这个组件一已经将名字转换为nametestprops : 父组件 向 子组件 传参基本使用Parent.vue<template><div> parent:下面是我的子组件 <childSon :userName=‘name’></childSon></div></template><script>import childSon from ‘./Childs’export default { name:‘Parent’, components:{ childSon }, data(){ return{ name:‘啊哈’ } }}</script>Childs.vue<template><div> child:这是父组件给我传的数据——{{userName}}</div></template><script>export default { name:‘Childs’, props:[‘userName’], data(){ return{ } }}</script>我们在 Parent.vue 组件里面引用子组件 Childs.vue 然后传入 userName 参数给子组件,Childs 在props里面接收父组件传传来的数据。上面的例子我们传入的是一个字符串,其实,props可以传入String、Number、Object、Boolen、Array等数据类型。那么我们在接受参数的时候就会有一个问题,我们怎么知道接收的应该是字符串'12’还是数字12呢?所以 Vue有一个 Prop验证 的功能。Prop 验证子组件在接受数据的时候,可以指定接收具体类型的数据、是否不能为空,是否有默认值等。 Parent.vue<template><div> parent:下面是我的子组件 <childSon :name=‘name’ :firstName=‘firstName’ :age=‘18’ ></childSon></div></template><script>import childSon from ‘./Childs’export default { name:‘Parent’, components:{ childSon }, data(){ return{ name:‘大卫’, firstName:‘大华’ } }}</script>Child.vue<template><div> child:这是父组件给我传的数据——name:{{name}}——firstName:{{firstName}}——lastName:{{lastName}}——age:{{age}}</template><script>export default { name:‘Childs’, props:{ name: String, firstName: { type: String,//规定值的类型 required: true //必须传值,否则报错 }, lastName: { type: String, default: ’lastNameDefault’ //如果不传值,则为default的值 }, age: { type: [String,Number], //类型可以是多种 validator: function(value) { //自定义验证 let num = parseInt(value) if (num > 0 && num <100) { return true; } else { return false; } } } }, data(){ return{ } }}</script>运行结果如下图:如果我们将条件改变的时候,name 传入一个数组,firstName 不传值,age 传入一个不能转换为数字的值。 <childSon :name=[11] age=‘ss’ ></childSon>运行结果如下图:根据我们的验证规则,name 必须为一个String 类型,所以控制台报错:希望得到一个String,得到了一个数组;firstName 为一个必填的值,但是我们没有传值,所以报错;age要为一个可以转换成数字的值,但是我们穿了"ss",会经过我们自定义的验证,然后抛错。Prop传入对象如果我们要将一个 对象 的所有属性全部传给子组件,我们不需要将属性一个个的作为Prop传递,只需要将整个对象传递过去就可以。 Parent.vuetemplate><div> parent:下面是我的子组件 <childSon v-bind=‘obj’ ></childSon></div></template><script>import childSon from ‘./Childs’export default { name:‘Parent’, components:{ childSon }, data(){ return{ obj: { name: ’lily’, age: ‘16’ } } }}</script>Childs.vue<template><div> child:这是父组件给我传的数据——name:{{name}}——age:{{age}}</div></template><script>export default { name:‘Childs’, props:{ name: String, age: { type: [String,Number], //类型可以是多种 validator: function(value) { //自定义验证 let num = parseInt(value) if (num > 0 && num <100) { return true; } else { return false; } } } }}</script>运行结果如下图:我们传入一个 obj 对象,然后在子组件里面可以拿到对象的所有属性。Prop的单向数据传递直接作为一个本地变量Parent.vue<template><div> parent:<input type=‘text’ v-model=“content”>下面是我的子组件 <childSon :content=‘content’ ></childSon></div></template><script>import childSon from ‘./Childs’export default { name:‘Parent’, components:{ childSon }, data() { return { content:’er’ }; },}</script>Childs.vue<template><div> child:这是父组件给我传的数据——{{con}}</div></template><script>export default { name:‘Childs’, props:[‘content’], data(){ return{ con:this.content } }}</script>运行结果如下图:emit :子组件 向 父组件 传递数据基本使用子组件向父组件传递数据,不能像上面一样实时的传递数据,必须通过 事件 触发。我们通过 $emit 方法来向父子间传递数据,第一个参数为事件的 名称 ,第二个为传递的 数据 ,是一个可选的参数。父组件必须监听同样的事件名称才能监听到我们的这个事件,事件抛出的值必须通过 $event或者通过一个方法来访问。**Parent.vue<template><div> parent:这是我的子组件传给我的值:{{num}} <childSon :content=‘content’ @getNum=‘getMsg’></childSon></div></template><script>import childSon from ‘./Childs’export default { name:‘Parent’, components:{ childSon }, data() { return { content:’er’, num:’’ } }, methods: { getMsg(num){ this.num = num; } }}</scriptChilds.vue<template><div> child:这是父组件给我传的数据——{{content}} <br /> <button @click=“sendMsgtoParent”>点击我可以向父子间传递参数哦</button></div></template><script>export default { name:‘Childs’, props:[‘content’], data(){ return{ num: 0 } }, methods: { sendMsgtoParent(){ this.$emit(‘getNum’,this.num ++ ); } }}</script>运行结果如下图:子组件定义了一个num变量,然后点击按钮触发method,通过 $emit向父组件发送事件的名称(getNum)和一个参数(this.num),然后 父组件 监听事件getNum,然后将传递值赋值给父组件的一个属性上,这样就可以是实现子组件点击一次按钮,就向父组件发送一次数据。更多实例可以参考官网。组件间的数据双向绑定我们知道我们可以使用v-model来实现数据的双向绑定。但是如果这个数据是跨组件的话,我们要怎样实现绑定吗? 首先我们先要明白v-model的原理。v-model其实是分为两个方面,一方面数据层的改变引起视图层的变化,我们可以使用v-bind来实现,另一方面视图层的变化引起数据层的变化我们可以监听事件来实现。所以我们想要双向绑定一个数据,只需要这两步操作。具体实现参考官网。弹框嵌套表格组件化使用(待续…) ...

January 24, 2019 · 2 min · jiezi

Vue基本语法和父子组件传参(prop、emit)

Vue基本语法和组件传参基本语法Vue是一个 MVVM 的框架,数据驱动和 组件化是Vue的核心思想。简单的讲MVVM框架就是:我们只需要在数据层做数据操作,显示层会检测到我们每次的数据变化,然后做出相应的改变,监测数据这个工作就是中间的ViewModel。通过这种模式,我们就可以不用再直接操作DOM节点来进行数据的改变。一、插值{{data}} 在模板里可以实现data数据的展示,如果data数据改变,展示的数据也会响应式的改变。响应式的改变意味着我们不需要强制刷新页面就可以实现数据的变化。这种语法为请输入代码Mustache语法<template> <div class=“main”> <h3>这里是title的值:{{title}}</h3> </div></template>export default { name:‘phonerisk’, data(){ return{ title:’testTitle’ } }}网页上就会显示出来data里面title的值。二、v-htmlv-html可以将一段HTML的代码字符串输出成HTML片段而不是普通的文本。<template> <div class=“main”> <p >这里是<span v-html=‘html’></span></p> </div></template>export default { name:‘phonerisk’, data(){ return{ html:’<span style=“color:blue;font-size:23px;">v-if</span>’ } }}网页上将html字符串渲染成颜色为蓝色的普通文本。v-bindMustache·语法不能用于HTML上,所以我们需要绑定一些属之类的需要使用v-bind。v-bind就是将data里面的数据绑定到HTML上面,从而实现属性的变化。<template> <div class=“main”> <img :src=“imgUrl” /> </div></template>export default { name:‘phonerisk’, data(){ return{ imgUrl:”../../static/img/KFC.e66b2f8e.png" } }}v-bind 可以简写成 :三、v-modelv-model是用于表单输入的数据双向绑定。所谓双向绑定就是视图层的数据变化会引起数据层数据的改变,相反的,数据层的变化也会导致视图层展示数据的变化。<template> <div class=“main”> <input type=“text” v-model=“name”> {{name}} <button @click=‘changeName’>改变名字</button> </div></template>export default { name:‘phonerisk’, data(){ return{ name:‘小明’ } }, methods:{ changeName(){ this.name = “小花”; } }}input输入框绑定name,输入框初始显示‘小明’,在输入框里更改信息的时候,name同时发生改变,点击按钮改变name数值的时候,输入框里面的数据同时发生改变。四、v-onv-on 用于监听DOM事件,如按钮的点击事件、双击事件等。v-on 的简写为 @,如下面的 @click 就等价为 v-on:click。template> <div class=“main”> <button @click=‘yes’>你敢点我吗?</button> </div></template> methods:{ yes(){ alert(“我有啥不敢的!!!”); } }这个案例是监听按钮的点击事件,点击之后调用 yes 函数,然后弹出警告框。 在平时的开发过程中我们可能会使用到 event.preventDefault() 或者 event.stopPropagation() 來阻止事件的继续传播,但是这个是直接操作DOM节点,不符合Vue的思想。所以Vue采用修饰符来进行相关的操作。下面我例举几个常用的,如了解更过,可以查看Vue的官网进行更详细的学习。.stop没有加修饰符<div class=“main” @click=“div”> <button @click.stop=‘yes’>你敢点我吗?</button> </div> methods:{ yes(){ alert(“我有啥不敢的!!!”); }, div(){ alert(“我会DIV”); } }效果如下图:加了 .stop 修饰符之后的效果<button @click.stop=‘yes’>你敢点我吗?</button>效果如下图:.stop 修饰符阻止了事件的继续传播,所以子节点的 click事件没有冒泡到父节点,所以div的点击监听没有监听到内容。.prevent没有加修饰符 <form @submit=“onSubmit”> <button @click=“onSubmit”>提交</button> </form>运行结果如下图:加了 .prevent 修饰符之后的效果<form @submit.prevent =“onSubmit”> <button @click=“onSubmit”>提交</button></form> .prevent 提交表单之后页面不在重新渲染。可以看到没加修饰符的时候页面重新加载,但是在加修饰符之后,页面不在重新加载。.keyup<input v-on:keyup.13=“submit”><input @keyup.enter=“submit”>绑定到输入框里,可以直接按enter键就出发提交的方法,和点击提交按钮一样的效果,官网还提供了其他按键的别名五、v-ifv-if用于做条件化的渲染,当组件的判断条件发生改变,这个组件会被销毁并重建。<template> <div class=“main”> <span v-if=“display”>我叫001</span> <span v-if="!display">我叫002</span> <button @click=“changeShow”>切换</button> </div></template>javascript data(){ return{ display:true } }, methods:{ changeShow(){ this.display = !this.display; }, }运行结果如下图:v-if绑定的变量为 true 的时候,其所在的元素会被渲染出来,反之亦然。六、v-else、v-else-ifv-else 和C语言中的else一样的语法效果。如果条件变量不满足 v-if ,则执行 <v-else> 的内容 <div class=“main”> <span v-if=“display”>我叫001</span> <span v-else>我叫002</span> <button @click=“changeShow”>切换</button> </div>运行效果和上图一致。 v-else-if 是Vue2.1.0版本新增的一个属性。v-else-if 必须用在 v-if 和 v-else 之间才有效果。<template> <div class=“main”> <span v-if=“display === 1”>我叫001</span> <span v-else-if=“display === 2”>我叫002</span> <span v-else>我叫003</span> <button @click=“changeShow”>切换</button> </div></template> data(){ return{ display:1 } }, methods:{ changeShow(){ this.display = (this.display + 1)%3; }, }运行结果如下图:七、v-showv-show 是切换元素的 display 属性的。<template> <div class=“main”> <span v-show=“display”>我叫001</span> <span v-show="!display “>我叫002</span> <button @click=“changeShow”>切换</button> </div></template> data(){ return{ display:true } }, methods:{ changeShow(){ this.display = !this.display; }, }运行效果如下图:八、v-forv-for 用于多次渲染同一模板或者元素。<div class=“main”> <ul v-for="(name, index) in names” :key=“index”> <li>{{index}}、我的名字叫{{name}}</li> </ul> </div> data(){ return{ names:[‘jack’,‘joe’,‘Nancy’, ’lily’] } },运行结果如下图:v-for 多次渲染了li 里面的内容,循环遍历了names 数组,并将结放入 {{name}} 里面。九、v-once v-once 只渲染元素和组件一次,如果内容改变,也会将元素、组件视为静态元素跳过。<div class=“main”> <!– 单个元素 –> <span v-once>This will never change:{{msg}}</span> <!– 有子元素 –> <div v-once> <span>comment:::</span> <span>{{msg}}</span> </div> <!– 循环 –> <ul> <li v-for=“i in names” v-once :key=“i”>{{i}}</li> </ul> <hr> <span>This will change:</span> <!– 单个元素 –> <span>This will never change:{{msg}}</span> <!– 有子元素 –> <div > <span>comment:::</span> <span>{{msg}}</span> </div> <!– 循环 –> <ul> <li v-for=“i in names” :key=“i”>{{i}}</li> </ul> <button @click=“changValue”>testChange</button> </div>data() { return { msg: 111, names: [“jack”, “joe”, “Nancy”, “lily”] }; }, methods: { changValue() { this.msg += 111; this.names[2] = “web”; } }运行效果如下图: 在点击按钮后, msg 会增加,但是上面有v-once指令的元素等则不会重新渲染。十、v-if和v-show的区别前面讲了v-if 和 v-show,两个指令都是用在元素之间的显示和隐藏的切换,那么,这两个指令究竟有什么不同呢?接下来我将用一个示例来讲解他们之间的差异。<template> <div class=“main”> <h3>v-if</h3> <div class=“content” v-if=“display”>1</div> <div class=“content” v-else>2</div> <h3>v-show</h3> <div class=“content” v-show=“display”>1</div> <div class=“content” v-show="!display">2</div> <h3>对比</h3> <div class=“content”>1</div> <div class=“content”>2</div> <button @click=“changeValue”>点击我啦</button> </div></template>data() { return { display: true }; }, methods: { changeValue() { this.display = !this.display; } }.content { display: inline-block; width: 100px; height: 100px; border: solid 1px black; background-color: red;}.content + .content { margin-left: 20px;}运行效果如下图:从上图我们可以看出当display 为 false 的时候,v-if 和v-show显示位置不一样。首先我们解读一下这个代码的css:content类设置了div的长宽和背景色,dispaly属性为 inline-block, .content + .content 设置了如果有两个content 类在一起的时候,后面一个的左边距为20个像素。在dispaly 为 true 的时候,两个div都靠左显示。在 display 为 false 的时候,上面的div在原来的位置重新渲染,但是下面的div却有一个20像素的左边距。所以v-if渲染的时候,不会将不符合条件的元素加载到DOM树里面,而v-show则会将所有的节点都加载到DOM树,然后根据条件,更改节点的display 属性,生成不同的渲染树。 一般来说, v-if需要更高的开销,每次都会改变DOM树,但是v-show 只需要改变元素的 display ,开销更低。十一、v-for和v-if优先级问题当v-for 和v-if 在同一个蒜素里的时候,前者比后者有更高的优先级,所以我们在开发中一定要注意优先级的问题。如果我们是想有条件的跳过循环中的某些项的时候:<template> <div class=“main”> <ul > <li v-for="(count, index) in counts" :key=“index” v-if=“count >30”> {{index + 1}}、我花费了{{count}}元 </li>> {{index + 1}}、我花费了{{count}}元 </li> </ul> </div></template>data() { return { counts:[11,22,33,44,55,66] };运行结果如下图:跳过了count 小于 30 的项 2.如果我们是打算有条件的跳过循环的话那么我们应该将判断写在循环的外面,先判断条件。<template> <div class=“main”> <ul v-if=“counts.length >3”> <li v-for="(count, index) in counts" :key=“index”> {{index + 1}}、我花费了{{count}}元 </li> </ul> </div></template>运行结果如下图:至此,讲完了Vue 的基本语法……撒花✿✿ヽ(°▽°)ノ✿ ...

January 18, 2019 · 3 min · jiezi

vue延迟加载、懒加载(比如加载vueCropper)

服务端渲染的时候,有些组件加载时候,浏览器会报错window is not defined 或者 document is not defined比如我们在使用前端的裁剪插件 vue-cropper时,刷新时或者使用nuxt框架服务端渲染时会报这样的错!这时候我们考虑到使用懒加载,就是延迟加载:1.引入组件const vueCropper = resolve => resolve(require(‘vue-cropper’))2.使用这样刷新操作,或者使用nuxt等服务端渲染框架就没有问题了!

January 15, 2019 · 1 min · jiezi

微信小程序之scroll-view的flex布局问题

关于微信小程序的scroll-view组件,第一次写的时候是直接在scroll-view中用了一层容器包裹子元素,然后用了flex布局,并且是用了组件来实现的横向滚动,后面有提出改进,但是不记录下,就发现过了几天,就有点懵了1.效果图2.在scroll-view里加一层容器,使用flex布局实现这里用flex布局实现的话,就要用组件的形式wxss文件.scrollView{ padding: 0 20rpx; white-space: nowrap; box-sizing: border-box;}.item{ display: inline-block; margin-right: 20rpx; width: calc(100% / 3); height: 100rpx; background: #ff00ff;}.scrollView1{ display: flex; margin-top: 40rpx; padding: 0 20rpx; width: 100%; flex-wrap: nowrap; box-sizing: border-box;}.item1{ margin-right: 20rpx; width: calc(100% / 3); height: 100rpx; background: #ff00ff;}.scrollView2{ margin-top: 40rpx; padding: 0 20rpx; width: 100%; box-sizing: border-box;}.itemContainer{ display: flex; width: 100%; flex-wrap: nowrap;}.scrollItem{ margin-right: 20rpx;}.scrollView3{ margin-top: 40rpx; padding: 0 20rpx; width: 100%; box-sizing: border-box;}.item3{ margin-right: 20rpx; /* width: calc(100% / 3); */ width: 240rpx; height: 100rpx; background: #aa22dd;}wxml文件<!– 要想使用flex布局实现横向滚动,就要在scroll-view里加一层容器包裹,并且使用子组件才会出现滚动效果 –><scroll-view scroll-x class=“scrollView2”> <view class=“itemContainer”> <block wx:for="{{4}}" wx:key="{{index}}"> <view-item class=“scrollItem” /> </block> </view></scroll-view>子组件里就一个view标签,可以自己直接写3.直接使用display:inline-blockwxml文件<scroll-view scroll-x class=“scrollView”> <block wx:for="{{4}}" wx:key="{{index}}"> <view class=“item”></view> </block></scroll-view>4.自己的理解scroll-view不可以直接使用flex布局,使用flex布局会使得他不会按照预想的那样横向排列、滚动要使用flex布局则要麻烦一点如果直接使用flex布局,不用子组件的话,则会被挤成一排正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断iOS和Android及PC端纯css实现瀑布流(multi-column多列及flex布局)实现单行及多行文字超出后加省略号微信小程序之购物车和父子组件传值及calc的注意事项 ...

January 15, 2019 · 1 min · jiezi

Android组件化方案及组件消息总线modular-event实战

背景组件化作为Android客户端技术的一个重要分支,近年来一直是业界积极探索和实践的方向。美团内部各个Android开发团队也在尝试和实践不同的组件化方案,并且在组件化通信框架上也有很多高质量的产出。最近,我们团队对美团零售收银和美团轻收银两款Android App进行了组件化改造。本文主要介绍我们的组件化方案,希望对从事Android组件化开发的同学能有所启发。为什么要组件化近年来,为什么这么多团队要进行组件化实践呢?组件化究竟能给我们的工程、代码带来什么好处?我们认为组件化能够带来两个最大的好处:提高组件复用性可能有些人会觉得,提高复用性很简单,直接把需要复用的代码做成Android Module,打包AAR并上传代码仓库,那么这部分功能就能被方便地引入和使用。但是我们觉得仅仅这样是不够的,上传仓库的AAR库是否方便被复用,需要组件化的规则来约束,这样才能提高复用的便捷性。降低组件间的耦合我们需要通过组件化的规则把代码拆分成不同的模块,模块要做到高内聚、低耦合。模块间也不能直接调用,这需要组件化通信框架的支持。降低了组件间的耦合性可以带来两点直接的好处:第一,代码更便于维护;第二,降低了模块的Bug率。组件化之前的状态我们的目标是要对团队的两款App(美团零售收银、美团轻收银)进行组件化重构,那么这里先简单地介绍一下这两款应用的架构。总的来说,这两款应用的构架比较相似,主工程Module依赖Business Module,Business Module是各种业务功能的集合,Business Module依赖Service Module,Service Module依赖Platform Module,Service Module和Platform Module都对上层提供服务,有所不同的是Platform Module提供的服务更为基础,主要包括一些工具Utils和界面Widget,而Service Module提供各种功能服务,如KNB、位置服务、网络接口调用等。这样的话,Business Module就变得非常臃肿和繁杂,各种业务模块相互调用,耦合性很强,改业务代码时容易“牵一发而动全身”,即使改一小块业务代码,可能要连带修改很多相关的地方,不仅在代码层面不利于进行维护,而且对一个业务的修改很容易造成其他业务产生Bug。组件化方案调研为了得到最适合我们业态和构架的组件化方案,我们调研了业界开源的一些组件化方案和公司内部其他团队的组件化方案,在此做个总结。开源组件化方案调研我们调研了业界一些主流的开源组件化方案。CC号称业界首个支持渐进式组件化改造的Android组件化开源框架。无论页面跳转还是组件间调用,都采用CC统一的组件调用方式完成。DDComponentForAndroid得到的方案采用路由 + 接口下沉的方式,所有接口下沉到base中,组件中实现接口并在IApplicationLike中添加代码注册到Router中。ModularizationArchitecture组件间调用需指定同步实现还是异步实现,调用组件时统一拿到RouterResponse作为返回值,同步调用的时候用RouterResponse.getData()来获取结果,异步调用获取时需要自己维护线程。ARouter阿里推出的路由引擎,是一个路由框架,并不是完整的组件化方案,可作为组件化架构的通信引擎。聚美Router聚美的路由引擎,在此基础上也有聚美的组件化实践方案,基本思想是采用路由 + 接口下沉的方式实现组件化。美团其他团队组件化方案调研美团收银ComponentCenter美团收银的组件化方案支持接口调用和消息总线两种方式,接口调用的方式需要构建CCPData,然后调用ComponentCenter.call,最后在统一的Callback中进行处理。消息总线方式也需要构建CCPData,最后调用ComponentCenter.sendEvent发送。美团收银的业务组件都打包成AAR上传至仓库,组件间存在相互依赖,这样导致mainapp引用这些组件时需要小心地exclude一些重复依赖。在我们的组件化方案中,我们采用了一种巧妙的方法来解决这个问题。美团App ServiceLoader美团App的组件化方案采用ServiceLoader的形式,这是一种典型的接口调用组件通信方式。用注解定义服务,获取服务时取得一个接口的List,判断这个List是否为空,如果不为空,则获取其中一个接口调用。WMRouter美团外卖团队开发的一款Android路由框架,基于组件化的设计思路。主要提供路由、ServiceLoader两大功能。之前美团技术博客也发表过一篇WMRouter的介绍:《WMRouter:美团外卖Android开源路由框架》。WMRouter提供了实现组件化的两大基础设施框架:路由和组件间接口调用。支持和文档也很充分,可以考虑作为我们团队实现组件化的基础设施。组件化方案组件化基础框架在前期的调研工作中,我们发现外卖团队的WMRouter是一个不错的选择。首先,WMRouter提供了路由+ServiceLoader两大组件间通信功能,其次,WMRouter架构清晰,扩展性比较好,并且文档和支持也比较完备。所以我们决定了使用WMRouter作为组件化基础设施框架之一。然而,直接使用WMRouter有两个问题:我们的项目已经在使用一个路由框架,如果使用WMRouter,需要把之前使用的路由框架改成WMRouter路由框架。WMRouter没有消息总线框架,我们调研的其他项目也没有适合我们项目的消息总线框架,因此我们需要开发一个能够满足我们需求的消息总线框架,这部分会在后面详细描述。组件化分层结构在参考了不同的组件化方案之后,我们采用了如下分层结构:App壳工程:负责管理各个业务组件和打包APK,没有具体的业务功能。业务组件层:根据不同的业务构成独立的业务组件,其中每个业务组件包含一个Export Module和Implement Module。功能组件层:对上层提供基础功能服务,如登录服务、打印服务、日志服务等。组件基础设施:包括WMRouter,提供页面路由服务和ServiceLoader接口调用服务,以及后面会介绍的组件消息总线框架:modular-event。整体架构如下图所示:业务组件拆分我们调研其他组件化方案的时候,发现很多组件方案都是把一个业务模块拆分成一个独立的业务组件,也就是拆分成一个独立的Module。而在我们的方案中,每个业务组件都拆分成了一个Export Module和Implement Module,为什么要这样做呢?避免循环依赖如果采用一个业务组件一个Module的方式,如果Module A需要调用Module B提供的接口,那么Module A就需要依赖Module。同时,如果Module B需要调用Module A的接口,那么Module B就需要依赖Module A。此时就会形成一个循环依赖,这是不允许的。也许有些读者会说,这个好解决:可以把Module A和Module B要依赖的接口放到另一个Module中去,然后让Module A和Module B都去依赖这个Module就可以了。这确实是一个解决办法,并且有些项目组在使用这种把接口下沉的方法。但是我们希望一个组件的接口,是由这个组件自己提供,而不是放在一个更加下沉的接口里面,所以我们采用了把每个业务组件都拆分成了一个Export Module和Implement Module。这样的话,如果Module A需要调用Module B提供的接口,同时Module B需要调用Module A的接口,只需要Module A依赖Module B Export,Module B依赖Module A Export就可以了。业务组件完全平等在使用单Module方案的组件化方案中,这些业务组件其实不是完全平等,有些被依赖的组件在层级上要更下沉一些。但是采用Export Module+Implement Module的方案,所有业务组件在层级上完全平等。功能划分更加清晰每个业务组件都划分成了Export Module+Implement Module的模式,这个时候每个Module的功能划分也更加清晰。Export Module主要定义组件需要对外暴露的部分,主要包含:对外暴露的接口,这些接口用WMRouter的ServiceLoader进行调用。对外暴露的事件,这些事件利用消息总线框架modular-event进行订阅和分发。组件的Router Path,组件化之前的工程虽然也使用了Router框架,但是所有Router Path都是定义在了一个下沉Module的公有Class中。这样导致的问题是,无论哪个模块添加/删除页面,或是修改路由,都需要去修改这个公有的Class。设想如果组件化拆分之后,某个组件新增了页面,还要去一个外部的Java文件中新增路由,这显然难以接受,也不符合组件化内聚的目标。因此,我们把每个组件的Router Path放在组件的Export Module中,既可以暴露给其他组件,也可以做到每个组件管理自己的Router Path,不会出现所有组件去修改一个Java文件的窘境。Implement Module是组件实现的部分,主要包含:页面相关的Activity、Fragment,并且用WMRouter的注解定义路由。Export Module中对外暴露的接口的实现。其他的业务逻辑。组件化消息总线框架modular-event前文提到的实现组件化基础设施框架中,我们用外卖团队的WMRouter实现页面路由和组件间接口调用,但是却没有消息总线的基础框架,因此,我们自己开发了一个组件化消息总线框架modular-event。为什么需要消息总线框架之前,我们开发过一个基于LiveData的消息总线框架:LiveDataBus,也在美团技术博客上发表过一篇文章来介绍这个框架:《Android消息总线的演进之路:用LiveDataBus替代RxBus、EventBus》。关于消息总线的使用,总是伴随着很多争论。有些人觉得消息总线很好用,有些人觉得消息总线容易被滥用。既然已经有了ServiceLoader这种组件间接口调用的框架,为什么还需要消息总线这种方式呢?主要有两个理由:更进一步的解耦基于接口调用的ServiceLoader框架的确实现了解耦,但是消息总线能够实现更彻底的解耦。接口调用的方式调用方需要依赖这个接口并且知道哪个组件实现了这个接口。消息总线方式发送者只需要发送一个消息,根本不用关心是否有人订阅这个消息,这样发送者根本不需要了解其他组件的情况,和其他组件的耦合也就越少。多对多的通信基于接口的方式只能进行一对一的调用,基于消息总线的方式能够提供多对多的通信。消息总线的优点和缺点总的来说,消息总线最大的优点就是解耦,因此很适合组件化这种需要对组件间进行彻底解耦的场景。然而,消息总线被很多人诟病的重要原因,也确实是因为消息总线容易被滥用。消息总线容易被滥用一般体现在几个场景:消息难以溯源有时候我们在阅读代码的过程中,找到一个订阅消息的地方,想要看看是谁发送了这个消息,这个时候往往只能通过查找消息的方式去“溯源”。导致我们在阅读代码,梳理逻辑的过程不太连贯,有种被割裂的感觉。消息发送比较随意,没有强制的约束消息总线在发送消息的时候一般没有强制的约束。无论是EventBus、RxBus或是LiveDataBus,在发送消息的时候既没有对消息进行检查,也没有对发送调用进行约束。这种不规范性在特定的时刻,甚至会带来灾难性的后果。比如订阅方订阅了一个名为login_success的消息,编写发送消息的是一个比较随意的程序员,没有把这个消息定义成全局变量,而是定义了一个临时变量String发送这个消息。不幸的是,他把消息名称login_success拼写成了login_seccess。这样的话,订阅方永远接收不到登录成功的消息,而且这个错误也很难被发现。组件化消息总线的设计目标消息由组件自己定义以前我们在使用消息总线时,喜欢把所有的消息都定义到一个公共的Java文件里面。但是组件化如果也采用这种方案的话,一旦某个组件的消息发生变动,都会去修改这个Java文件。所以我们希望由组件自己来定义和维护消息定义文件。区分不同组件定义的同名消息如果消息由组件定义和维护,那么有可能不同组件定义了重名的消息,消息总线框架需要能够区分这种消息。解决前文提到的消息总线的缺点解决消息总线消息难以溯源和消息发送没有约束的问题。基于LiveData的消息总线之前的博文《Android消息总线的演进之路:用LiveDataBus替代RxBus、EventBus》详细阐述了如何基于LiveData构建消息总线。组件化消息总线框架modular-event同样会基于LiveData构建。使用LiveData构建消息总线有很多优点:使用LiveData构建消息总线具有生命周期感知能力,使用者不需要调用反注册,相比EventBus和RxBus使用更为方便,并且没有内存泄漏风险。使用普通消息总线,如果回调的时候Activity处于Stop状态,这个时候进行弹Dialog一类的操作就会引起崩溃。使用LiveData构建消息总线完全没有这个风险。组件消息总线modular-event的实现解决不同组件定义了重名消息的问题其实这个问题还是比较好解决的,实现的方式就是采用两级HashMap的方式解决。第一级HashMap的构建以ModuleName作为Key,第二级HashMap作为Value;第二级HashMap以消息名称EventName作为Key,LiveData作为Value。查找的时候先用组件名称ModuleName在第一级HashMap中查找,如果找到则用消息名EventName在第二级HashName中查找。整个结构如下图所示:对消息总线的约束我们希望消息总线框架有以下约束:只能订阅和发送在组件中预定义的消息。换句话说,使用者不能发送和订阅临时消息。消息的类型需要在定义的时候指定。定义消息的时候需要指定属于哪个组件。如何实现这些约束在消息定义文件上使用注解,定义消息的类型和消息所属Module。定义注解处理器,在编译期间收集消息的相关信息。在编译器根据消息的信息生成调用时需要的interface,用接口约束消息发送和订阅。运行时构建基于两级HashMap的LiveData存储结构。运行时采用interface+动态代理的方式实现真正的消息订阅和发送。整个流程如下图所示:消息总线modular-event的结构modular-event-base:定义Anotation及其他基本类型modular-event-core:modular-event核心实现modular-event-compiler:注解处理器modular-event-plugin:Gradle PluginAnotation@ModuleEvents:消息定义@Retention(RetentionPolicy.SOURCE)@Target(ElementType.TYPE)public @interface ModuleEvents { String module() default “”;}@EventType:消息类型@Retention(RetentionPolicy.SOURCE)@Target(ElementType.FIELD)public @interface EventType { Class value();}消息定义通过@ModuleEvents注解一个定义消息的Java类,如果@ModuleEvents指定了属性module,那么这个module的值就是这个消息所属的Module,如果没有指定属性module,则会把定义消息的Java类所在的包的包名作为消息所属的Module。在这个消息定义java类中定义的消息都是public static final String类型。可以通过@EventType指定消息的类型,@EventType支持java原生类型或自定义类型,如果没有用@EventType指定消息类型,那么消息的类型默认为Object,下面是一个消息定义的示例://可以指定module,若不指定,则使用包名作为module名@ModuleEvents()public class DemoEvents { //不指定消息类型,那么消息的类型默认为Object public static final String EVENT1 = “event1”; //指定消息类型为自定义Bean @EventType(TestEventBean.class) public static final String EVENT2 = “event2”; //指定消息类型为java原生类型 @EventType(String.class) public static final String EVENT3 = “event3”;}interface自动生成我们会在modular-event-compiler中处理这些注解,一个定义消息的Java类会生成一个接口,这个接口的命名是EventsDefineOf+消息定义类名,例如消息定义类的类名为DemoEvents,自动生成的接口就是EventsDefineOfDemoEvents。消息定义类中定义的每一个消息,都会转化成接口中的一个方法。使用者只能通过这些自动生成的接口使用消息总线。我们用这种巧妙的方式实现了对消息总线的约束。前文提到的那个消息定义示例DemoEvents.java会生成一个如下的接口类:package com.sankuai.erp.modularevent.generated.com.meituan.jeremy.module_b_export;public interface EventsDefineOfDemoEvents extends com.sankuai.erp.modularevent.base.IEventsDefine { com.sankuai.erp.modularevent.Observable<java.lang.Object> EVENT1(); com.sankuai.erp.modularevent.Observable<com.meituan.jeremy.module_b_export.TestEventBean> EVENT2( ); com.sankuai.erp.modularevent.Observable<java.lang.String> EVENT3();}关于接口类的自动生成,我们采用了square/javapoet来实现,网上介绍JavaPoet的文章很多,这里就不再累述。使用动态代理实现运行时调用有了自动生成的接口,就相当于有了一个壳,然而壳下面的所有逻辑,我们通过动态代理来实现,简单介绍一下代理模式和动态代理:代理模式:给某个对象提供一个代理对象,并由代理对象控制对于原对象的访问,即客户不直接操控原对象,而是通过代理对象间接地操控原对象。动态代理:代理类是在运行时生成的。也就是说Java编译完之后并没有实际的class文件,而是在运行时动态生成的类字节码,并加载到JVM中。在动态代理的InvocationHandler中实现查找逻辑:根据interface的typename得到ModuleName。调用的方法的methodname即为消息名。根据ModuleName和消息名找到相应的LiveData。完成后续订阅消息或者发送消息的流程。消息的订阅和发送可以用链式调用的方式编码:订阅消息ModularEventBus .get() .of(EventsDefineOfModuleBEvents.class) .EVENT1() .observe(this, new Observer<TestEventBean>() { @Override public void onChanged(@Nullable TestEventBean testEventBean) { Toast.makeText(MainActivity.this, “MainActivity收到自定义消息: " + testEventBean.getMsg(), Toast.LENGTH_SHORT).show(); } });发送消息ModularEventBus .get() .of(EventsDefineOfModuleBEvents.class) .EVENT1() .setValue(new TestEventBean(“aa”));订阅和发送的模式订阅消息的模式observe:生命周期感知,onDestroy的时候自动取消订阅。observeSticky:生命周期感知,onDestroy的时候自动取消订阅,Sticky模式。observeForever:需要手动取消订阅。observeStickyForever:需要手动取消订阅,Sticky模式。发送消息的模式setValue:主线程调用。postValue:后台线程调用。组件化总结本文介绍了美团行业收银研发组Android团队的组件化实践,以及业界首创强约束组件消息总线modular-event的原理和使用。我们团队很早之前就在探索组件化改造,前期有些方案在落地的时候遇到很多困难。我们也研究了很多开源的组件化方案,以及公司内部其他团队(美团App、美团外卖、美团收银等)的组件化方案,学习和借鉴了很多优秀的设计思想,当然也踩过不少的坑。我们逐渐意识到:任何一种组件化方案都有其适用场景,我们的组件化架构选择,应该更加面向业务,而不仅仅是面向技术本身。后期工作展望我们的组件化改造工作远远没有结束,未来可能会在以下几个方向继续进行深入的研究:组件管理:组件化改造之后,每个组件是个独立的工程,组件也会迭代开发,如何对这些组件进行版本化管理。组件重用:现在看起来对这些组件的重用是很方便的,只需要引入组件的库即可,但是如果一个新的项目到来,需求有些变化,我们应该怎样最大限度的重用这些组件。CI集成:如何更好的与CI集成。集成到脚手架:集成到脚手架,让新的项目从一开始就以组件化的模式进行开发。参考资料Android消息总线的演进之路:用LiveDataBus替代RxBus、EventBusWMRouter:美团外卖Android开源路由框架美团外卖Android平台化架构演进实践作者简介海亮,美团高级工程师,2017年加入美团,目前主要负责美团轻收银、美团收银零售版等App的相关业务及模块开发工作。招聘美团餐饮生态诚招Android高级/资深工程师和技术专家,Base北京、成都,欢迎有兴趣的同学投递简历到chenyuxiang@meituan.com。 ...

December 24, 2018 · 1 min · jiezi

编写React组件项目实践分析

通过实例给大家分享了编写React组件项目实践的全过程,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。开始前:我们使用ES6、ES7语法如果你不是很清楚展示组件和容器组件的区别,建议您从阅读这篇文章开始如果您有任何的建议、疑问都清在评论里留言 基于类的组件现在开发React组件一般都用的是基于类的组件。下面我们就来一行一样的编写我们的组件:import React, { Component } from ‘react’;import { observer } from ‘mobx-react’; import ExpandableForm from ‘./ExpandableForm’;import ‘./styles/ProfileContainer.css’;其实我很喜欢css in javascript。但是,这个写样式的方法还是太新了。所以我们在每个组件里引入css文件。而且本地引入的import和全局的import会用一个空行来分割。初始化Stateimport React, { Component } from ‘react’import { observer } from ‘mobx-react’ import ExpandableForm from ‘./ExpandableForm’import ‘./styles/ProfileContainer.css’ //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860export default class ProfileContainer extends Component { state = { expanded: false }可以使用了老方法在constructor里初始化state。更多相关可以看这里。但是我们选择更加清晰的方法。同时,我们确保在类前面加上了export default。(译者注:虽然这个在使用了redux的时候不一定对)。propTypes and defaultPropsimport React, { Component } from ‘react’import { observer } from ‘mobx-react’import { string, object } from ‘prop-types’ import ExpandableForm from ‘./ExpandableForm’import ‘./styles/ProfileContainer.css’ export default class ProfileContainer extends Component { state = { expanded: false } static propTypes = { model: object.isRequired, title: string } static defaultProps = { model: { id: 0 }, title: ‘Your Name’ } // …}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860propTypes和defaultProps是静态属性。尽可能在组件类的的前面定义,让其他的开发人员读代码的时候可以立刻注意到。他们可以起到文档的作用。如果你使用了React 15.3.0或者更高的版本,那么需要另外引入prop-types包,而不是使用React.PropTypes。更多内容移步这里。你所有的组件都应该有prop types。方法import React, { Component } from ‘react’import { observer } from ‘mobx-react’import { string, object } from ‘prop-types’ import ExpandableForm from ‘./ExpandableForm’import ‘./styles/ProfileContainer.css’ export default class ProfileContainer extends Component { state = { expanded: false } static propTypes = { model: object.isRequired, title: string } static defaultProps = { model: { id: 0 }, title: ‘Your Name’ } handleSubmit = (e) => { e.preventDefault() this.props.model.save() } handleNameChange = (e) => { this.props.model.changeName(e.target.value) } handleExpand = (e) => { e.preventDefault() this.setState({ expanded: !this.state.expanded }) } // … }//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860在类组件里,当你把方法传递给子组件的时候,需要确保他们被调用的时候使用的是正确的this。一般都会在传给子组件的时候这么做:this.handleSubmit.bind(this)。使用ES6的箭头方法就简单多了。它会自动维护正确的上下文(this)。给setState传入一个方法在上面的例子里有这么一行:this.setState({ expanded: !this.state.expanded });setState其实是异步的!React为了提高性能,会把多次调用的setState放在一起调用。所以,调用了setState之后state不一定会立刻就发生改变。所以,调用setState的时候,你不能依赖于当前的state值。因为i根本不知道它是值会是神马。解决方法:给setState传入一个方法,把调用前的state值作为参数传入这个方法。看看例子:this.setState(prevState => ({ expanded: !prevState.expanded }))拆解组件import React, { Component } from ‘react’import { observer } from ‘mobx-react’ import { string, object } from ‘prop-types’import ExpandableForm from ‘./ExpandableForm’import ‘./styles/ProfileContainer.css’ export default class ProfileContainer extends Component { state = { expanded: false } //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 static propTypes = { model: object.isRequired, title: string } static defaultProps = { model: { id: 0 }, title: ‘Your Name’ } handleSubmit = (e) => { e.preventDefault() this.props.model.save() } handleNameChange = (e) => { this.props.model.changeName(e.target.value) } handleExpand = (e) => { e.preventDefault() this.setState(prevState => ({ expanded: !prevState.expanded })) } //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 render() { const { model, title } = this.props return ( <ExpandableForm onSubmit={this.handleSubmit} expanded={this.state.expanded} onExpand={this.handleExpand}> <div> <h1>{title}</h1> <input type=“text” value={model.name} onChange={this.handleNameChange} placeholder=“Your Name”/> </div> </ExpandableForm> ) }}有多行的props的,每一个prop都应该单独占一行。就如上例一样。要达到这个目标最好的方法是使用一套工具:Prettier。装饰器(Decorator)@observerexport default class ProfileContainer extends Component {如果你了解某些库,比如mobx,你就可以使用上例的方式来修饰类组件。装饰器就是把类组件作为一个参数传入了一个方法。装饰器可以编写更灵活、更有可读性的组件。如果你不想用装饰器,你可以这样:class ProfileContainer extends Component { // Component code}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860export default observer(ProfileContainer)闭包尽量避免在子组件中传入闭包,如:<input type=“text” value={model.name} // onChange={(e) => { model.name = e.target.value }} // ^ Not this. Use the below: onChange={this.handleChange} placeholder=“Your Name”/>注意:如果input是一个React组件的话,这样自动触发它的重绘,不管其他的props是否发生了改变。一致性检验是React最消耗资源的部分。不要把额外的工作加到这里。处理上例中的问题最好的方法是传入一个类方法,这样还会更加易读,更容易调试。如:import React, { Component } from ‘react’import { observer } from ‘mobx-react’import { string, object } from ‘prop-types’// Separate local imports from dependenciesimport ExpandableForm from ‘./ExpandableForm’import ‘./styles/ProfileContainer.css’//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 // Use decorators if needed@observerexport default class ProfileContainer extends Component { state = { expanded: false } // Initialize state here (ES7) or in a constructor method (ES6) // Declare propTypes as static properties as early as possible static propTypes = { model: object.isRequired, title: string } // Default props below propTypes static defaultProps = { model: { id: 0 }, title: ‘Your Name’ } //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 // Use fat arrow functions for methods to preserve context (this will thus be the component instance) handleSubmit = (e) => { e.preventDefault() this.props.model.save() } handleNameChange = (e) => { this.props.model.name = e.target.value } handleExpand = (e) => { e.preventDefault() this.setState(prevState => ({ expanded: !prevState.expanded })) } render() { // Destructure props for readability const { model, title } = this.props return ( <ExpandableForm onSubmit={this.handleSubmit} expanded={this.state.expanded} onExpand={this.handleExpand}> // Newline props if there are more than two <div> <h1>{title}</h1> <input type=“text” value={model.name} // onChange={(e) => { model.name = e.target.value }} // Avoid creating new closures in the render method- use methods like below onChange={this.handleNameChange} placeholder=“Your Name”/> </div>//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 </ExpandableForm> ) }}方法组件这类组件没有state没有props,也没有方法。它们是纯组件,包含了最少的引起变化的内容。经常使用它们。propTypesimport React from ‘react’import { observer } from ‘mobx-react’import { func, bool } from ‘prop-types’import ‘./styles/Form.css’ExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860// Component declaration我们在组件的声明之前就定义了propTypes。分解Props和defaultPropsimport React from ‘react’import { observer } from ‘mobx-react’import { func, bool } from ‘prop-types’import ‘./styles/Form.css’ ExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool, onExpand: func.isRequired}//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 function ExpandableForm(props) { const formStyle = props.expanded ? {height: ‘auto’} : {height: 0} return ( <form style={formStyle} onSubmit={props.onSubmit}> {props.children} <button onClick={props.onExpand}>Expand</button> </form> )}我们的组件是一个方法。它的参数就是props。我们可以这样扩展这个组件:import React from ‘react’import { observer } from ‘mobx-react’import { func, bool } from ‘prop-types’import ‘./styles/Form.css’ //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860ExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool, onExpand: func.isRequired} function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) { const formStyle = expanded ? {height: ‘auto’} : {height: 0} return (//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 <form style={formStyle} onSubmit={onSubmit}> {children} <button onClick={onExpand}>Expand</button> </form> )}现在我们也可以使用默认参数来扮演默认props的角色,这样有很好的可读性。如果expanded没有定义,那么我们就把它设置为false。但是,尽量避免使用如下的例子:const ExpandableForm = ({ onExpand, expanded, children }) => {看起来很现代,但是这个方法是未命名的。如果你的Babel配置正确,未命名的方法并不会是什么大问题。但是,如果Babel有问题的话,那么这个组件里的任何错误都显示为发生在 <>里的,这调试起来就非常麻烦了。匿名方法也会引起Jest其他的问题。由于会引起各种难以理解的问题,而且也没有什么实际的好处。我们推荐使用function,少使用const。装饰方法组件由于方法组件没法使用装饰器,只能把它作为参数传入别的方法里。import React from ‘react’import { observer } from ‘mobx-react’import { func, bool } from ‘prop-types’import ‘./styles/Form.css’ //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860ExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool, onExpand: func.isRequired} function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) { const formStyle = expanded ? {height: ‘auto’} : {height: 0} return ( <form style={formStyle} onSubmit={onSubmit}> {children} <button onClick={onExpand}>Expand</button> </form>//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 )}export default observer(ExpandableForm)只能这样处理:export default observer(ExpandableForm)。这就是组件的全部代码:import React from ‘react’import { observer } from ‘mobx-react’import { func, bool } from ‘prop-types’// Separate local imports from dependenciesimport ‘./styles/Form.css’ //欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860// Declare propTypes here, before the component (taking advantage of JS function hoisting)// You want these to be as visible as possibleExpandableForm.propTypes = { onSubmit: func.isRequired, expanded: bool, onExpand: func.isRequired} // Destructure props like so, and use default arguments as a way of setting defaultPropsfunction ExpandableForm({ onExpand, expanded = false, children, onSubmit }) { const formStyle = expanded ? { height: ‘auto’ } : { height: 0 } return ( <form style={formStyle} onSubmit={onSubmit}> {children} <button onClick={onExpand}>Expand</button> </form> )} // Wrap the component instead of decorating itexport default observer(ExpandableForm)条件判断某些情况下,你会做很多的条件判断:<div id=“lb-footer”> {props.downloadMode && currentImage && !currentImage.video && currentImage.blogText ? !currentImage.submitted && !currentImage.posted ? <p>Please contact us for content usage</p> : currentImage && currentImage.selected ? <button onClick={props.onSelectImage} className=“btn btn-selected”>Deselect</button> : currentImage && currentImage.submitted ? <button className=“btn btn-submitted” disabled>Submitted</button> : currentImage && currentImage.posted ? <button className=“btn btn-posted” disabled>Posted</button> : <button onClick={props.onSelectImage} className=“btn btn-unselected”>Select post</button> }//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860</div>这么多层的条件判断可不是什么好现象。有第三方库JSX-Control Statements可以解决这个问题。但是与其增加一个依赖,还不如这样来解决:<div id=“lb-footer”> {//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 (() => { if(downloadMode && !videoSrc) { if(isApproved && isPosted) { return <p>Right click image and select “Save Image As..” to download</p> } else { return <p>Please contact us for content usage</p> } } // … })() }</div>使用大括号包起来的IIFE,然后把你的if表达式都放进去。返回你要返回的组件。结语感谢您的观看,如有不足之处,欢迎批评指正。 ...

December 20, 2018 · 5 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

深入解析React中的元素、组件、实例和节点

React 深入系列,深入讲解了React中的重点概念、特性和模式等,旨在帮助大家加深对React的理解,以及在项目中更加灵活地使用React。React 中的元素、组件、实例和节点,是React中关系密切的4个概念,也是很容易让React 初学者迷惑的4个概念。现在,我就来详细地介绍这4个概念,以及它们之间的联系和区别,满足喜欢咬文嚼字、刨根问底的同学的好奇心。元素 (Element)React 元素其实就是一个简单JavaScript对象,一个React 元素和界面上的一部分DOM对应,描述了这部分DOM的结构及渲染效果。一般我们通过JSX语法创建React 元素,例如:const element = <h1 className=‘greeting’>Hello, world</h1>;element是一个React 元素。在编译环节,JSX 语法会被编译成对React.createElement()的调用,从这个函数名上也可以看出,JSX语法返回的是一个React 元素。上面的例子编译后的结果为:const element = React.createElement( ‘h1’, {className: ‘greeting’}, ‘Hello, world!’);最终,element的值是类似下面的一个简单JavaScript对象:const element = { type: ‘h1’, props: { className: ‘greeting’, children: ‘Hello, world’ }//欢迎加入前端全栈开发交流圈一起学习交流:864305860}React 元素可以分为两类:DOM类型的元素和组件类型的元素。DOM类型的元素使用像h1、div、p等DOM节点创建React 元素,前面的例子就是一个DOM类型的元素;组件类型的元素使用React 组件创建React 元素,例如:const buttonElement = <Button color=‘red’>OK</Button>;buttonElement就是一个组件类型的元素,它的值是:const buttonElement = { type: ‘Button’, props: { color: ‘red’, children: ‘OK’ }//欢迎加入前端全栈开发交流圈一起学习交流:864305860}对于DOM类型的元素,因为和页面的DOM节点直接对应,所以React知道如何进行渲染。但是对于组件类型的元素,如buttonElement,React是无法直接知道应该把buttonElement渲染成哪种结构的页面DOM,这时就需要组件自身提供React能够识别的DOM节点信息,具体实现方式在介绍组件时会详细介绍。有了React 元素,我们应该如何使用它呢?其实,绝大多数情况下,我们都不会直接使用React 元素,React 内部会自动根据React 元素,渲染出最终的页面DOM。更确切地说,React元素描述的是React虚拟DOM的结构,React会根据虚拟DOM渲染出页面的真实DOM。组件 (Component)React 组件,应该是大家最熟悉的React中的概念。React通过组件的思想,将界面拆分成一个个可以复用的模块,每一个模块就是一个React 组件。一个React 应用由若干组件组合而成,一个复杂组件也可以由若干简单组件组合而成。React组件和React元素关系密切,React组件最核心的作用是返回React元素。这里你也许会有疑问:React元素不应该是由React.createElement() 返回的吗?但React.createElement()的调用本身也是需要有“人”负责的,React组件正是这个“责任人”。React组件负责调用React.createElement(),返回React元素,供React内部将其渲染成最终的页面DOM。既然组件的核心作用是返回React元素,那么最简单的组件就是一个返回React元素的函数:function Welcome(props) { return <h1>Hello, {props.name}</h1>;}Welcome是一个用函数定义的组件。如果使用类(class)定义组件,返回React元素的工作具体就由组件的render方法承担,例如:class Welcome extends React.Component { render() { return <h1>Hello, {this.props.name}</h1>; }//欢迎加入前端全栈开发交流圈一起学习交流:864305860}其实,使用类定义的组件,render方法是唯一必需的方法,其他组件的生命周期方法都只不过是为render服务而已,都不是必需的。现在来考虑下面这个例子:class Home extends React.Component { render() { return ( <div> <Welcome name=‘前端攻城老湿’ /> <p>Anything you like</p> </div> )//欢迎加入前端全栈开发交流圈一起学习交流:864305860 }}Home 组件使用了Welcome组件,返回的React元素为:{ type: ‘div’, props: { children: [ { type: ‘Welcome’, props: { name: ‘前端攻城老湿’ } }, { type: ‘p’, props: { children: ‘Anything you like’ }//欢迎加入前端全栈开发交流圈一起学习交流:864305860 }, ] }}对于这个结构,React 知道如何渲染type = ‘div’ 和 type = ‘p’ 的节点,但不知道如何渲染type=‘Welcome’的节点,当React 发现Welcome 是一个React 组件时(判断依据是Welcome首字母为大写),会根据Welcome组件返回的React 元素决定如何渲染Welcome节点。Welcome组件返回的React 元素为:{ type: ‘h1’, props: { children: ‘Hello, 前端攻城小牛’ }//欢迎加入前端全栈开发交流圈一起学习交流:864305860}这个结构中只包含DOM节点,React是知道如何渲染的。如果这个结构中还包含其他组件节点,React 会重复上面的过程,继续解析对应组件返回的React 元素,直到返回的React 元素中只包含DOM节点为止。这样的递归过程,让React 获取到页面的完整DOM结构信息,渲染的工作自然就水到渠成了。另外,如果仔细思考的话,可以发现,React 组件的复用,本质上是为了复用这个组件返回的React 元素,React 元素是React 应用的最基础组成单位。实例 (Instance)这里的实例特指React组件的实例。React 组件是一个函数或类,实际工作时,发挥作用的是React 组件的实例对象。只有组件实例化后,每一个组件实例才有了自己的props和state,才持有对它的DOM节点和子组件实例的引用。在传统的面向对象的开发方式中,实例化的工作是由开发者自己手动完成的,但在React中,组件的实例化工作是由React自动完成的,组件实例也是直接由React管理的。换句话说,开发者完全不必关心组件实例的创建、更新和销毁。节点 (Node)在使用PropTypes校验组件属性时,有这样一种类型:MyComponent.propTypes = { optionalNode: PropTypes.node,}PropTypes.node又是什么类型呢?这表明optionalNode是一个React 节点。React 节点是指可以被React渲染的数据类型,包括数字、字符串、React 元素,或者是一个包含这些类型数据的数组。例如:// 数字类型的节点function MyComponent(props) { return 1;} // 字符串类型的节点function MyComponent(props) { return ‘MyComponent’;}//欢迎加入前端全栈开发交流圈一起学习交流:864305860 // React元素类型的节点function MyComponent(props) { return <div>React Element</div>;} // 数组类型的节点,数组的元素只能是其他合法的React节点function MyComponent(props) { const element = <div>React Element</div>; const arr = [1, ‘MyComponent’, element]; return arr;}//欢迎加入前端全栈开发交流圈一起学习交流:864305860 // 错误,不是合法的React节点function MyComponent(props) { const obj = { a : 1} return obj;}总结一下,React 元素和组件的概念最重要,也最容易混淆;React 组件实例的概念大家了解即可,几乎使用不到;React 节点有一定使用场景,但看过本文后应该也就不存在理解问题了。结语感谢您的观看,如有不足之处,欢迎批评指正。 ...

December 14, 2018 · 2 min · jiezi

如何用vue封装一个防用户删除的平铺页面的水印组件

需求为了防止截图等安全问题,在web项目页面中生成一个平铺全屏的水印要求水印内容为用户名,水印节点用户不能通过开发者工具等删除效果如上图在body节点下插入水印DOM节点,水印节点覆盖在页面最上层但不影响页面正常操作在通过js或者用户通过开发者工具删除或修改水印节点时自动复原原理通过canvas画出节点需生成水印的文字生成base64图片生成该水印背景图的div节点插入到body下,通过jsMutationObserver方法监听节点变化,再自动重新生成生成水印DOM节点// 生成水印DOM节点 init () { let canvas = document.createElement(‘canvas’) canvas.id = ‘canvas’ canvas.width = ‘200’ // 单个水印的宽高 canvas.height = ‘130’ this.maskDiv = document.createElement(‘div’) let ctx = canvas.getContext(‘2d’) ctx.font = ’normal 18px Microsoft Yahei’ // 设置样式 ctx.fillStyle = ‘rgba(112, 113, 114, 0.1)’ // 水印字体颜色 ctx.rotate(30 * Math.PI / 180) // 水印偏转角度 ctx.fillText(this.inputText, 30, 20) let src = canvas.toDataURL(‘image/png’) this.maskDiv.style.position = ‘fixed’ this.maskDiv.style.zIndex = ‘9999’ this.maskDiv.id = ‘_waterMark’ this.maskDiv.style.top = ‘30px’ this.maskDiv.style.left = ‘0’ this.maskDiv.style.height = ‘100%’ this.maskDiv.style.width = ‘100%’ this.maskDiv.style.pointerEvents = ’none’ this.maskDiv.style.backgroundImage = ‘URL(’ + src + ‘)’ // 水印节点插到body下 document.body.appendChild(this.maskDiv) },监听DOM更改// 监听更改,更改后执行callback回调函数,会得到一个相关信息的参数对象 Monitor () { let body = document.getElementsByTagName(‘body’)[0] let options = { childList: true, attributes: true, characterData: true, subtree: true, attributeOldValue: true, characterDataOldValue: true } let observer = new MutationObserver(this.callback) observer.observe(body, options) // 监听body节点 },使用直接引入项目任何组件中使用即可组件prop接收三个参数 props: { // 显示的水印文本 inputText: { type: String, default: ‘waterMark水印’ }, // 是否允许通过js或开发者工具等途径修改水印DOM节点(水印的id,attribute属性,节点的删除) // true为允许,默认不允许 inputAllowDele: { type: Boolean, default: false }, // 是否在组件销毁时去除水印节点(前提是允许用户修改DOM,否则去除后会再次自动生成) // true会,默认不会 inputDestroy: { type: Boolean, default: false } }inputText(String):需要生成的水印文本,默认为’waterMark水印’inputAllowDele(Boolean):是否需要允许用户删除水印DOM节点,true为允许,默认不允许inputDestroy(Boolean):是否在组件销毁时去除水印节点,true会,默认不会,(只有在inputAllowDele为ftrue时才能生效)如果需要修改水印大小,文字,颜色等样式,可直接进入组件中按注释修改小结工作写了个相关组件,复用率挺高就封装了下,没有经过严格测试,可当做参考使用有需要的朋友欢迎下载源码使用相关GitHub代码 ...

December 7, 2018 · 1 min · jiezi

WeGeek | WePY 开源框架

今天前来专栏分享的极客,是腾讯微信支付团队。小程序公测一个月时,微信支付团队开源了小程序上的组件化开发框架 WePY,在 Github 上一经发布便受到了众多开发者的追捧,网上搜索「微信小程序 WePY 开源框架资源汇总」尽是网友们自发分享的相关干货。尽管 WePY 开源框架如今倍受推崇,回忆起开源的初衷,来自微信支付团队的 Gcaufy 还是表示:「WePY 开源框架的对外开源并不是要去分享一个很成功的解决方案,而是我认为这套方案能够解决在小程序开发中遇到的一些实际问题,并且希望能借助外界的力量去帮助一起完善这套方案。」接下来,让我们一起看看 WePY 开源框架背后的开发故事吧。如果你有一定的开发经验,会明显感受到小程序的开发十分容易上手——小程序本身提供一些特性如:模块化,模板,数据绑定等,极大地方便了习惯使用 MVVM 框架的用户。但同时,由于运行环境有限,小程序尚不能使用市面上的流行框架。在几个月的开发历程里,作者 Gcaufy 便一直希望可以设计出一套方案,更大可能地让小程序开发贴近于当下开发习惯,WePY 开源框架应运而生。WePY 开源框架的原理很简单:通过 WePY 开源框架开发的代码在经过编译后,能生成一份完美运行在小程序端的代码,使得小程序开发更贴近于传统 H5 框架开发,可以像开发 H5 一样支持引入 npm 包,并且支持组件化开发以及支持 JS 新特性等,实现类 Vue 的开发体验。WePY 开源框架实现小程序的预加载我们知道,传统的 H5 可以通过预加载来提升用户体验,那么小程序能够实现吗?答案是肯定的。在小程序中使用预加载,比在 H5 中实现起来更为简单方便,但也更容易被开发者忽视。传统 H5 在启动时,page1.html 只会加载 page1.html 的页面与逻辑代码,当 page1.html 跳转至 page2.html 时,page1 所有的 Javascript 数据将会从内存中消失。在 page1 与 page2 之间的数据通信只能通过 URL 参数传递或者浏览器的 cookie,localStorge 存储处理。而小程序在启动时,会直接加载所有页面逻辑代码进内存。即便 page2 可能都不会被使用,在 page1 跳转至 page2 时,page1 的逻辑代码 Javascript 数据也不会从内存中消失。page2 甚至可以直接访问 page1 中的数据。小程序的这种机制差异正好可以更好的实现预加载。通常情况下,我们习惯将数据拉取写在 onLoad 事件中。但是小程序的 page1 跳转到 page2,到 page2 的 onLoad 存在一个 300ms ~ 400ms 的延时。如下图:因为小程序的特性,完全可以在 page1 中预先拿取数据,然后在 page2 中直接使用,这样就可以避开 redirecting 的 300ms ~ 400ms了。如下图:对于上述问题,WePY 开源框架中封装了两种概念去解决:预加载数据用于小程序中 page1 主动传递数据给 page2,比如 page2 需要加载一份耗时很长的数据。我可以在 page1 闲时先加载好,进入 page2 时直接就可以使用。预查询数据用于避免于 redirecting 延时,在跳转时调用 page2 预查询。WePY 开源框架添加了 onPrefetch 事件,会在 redirect 之时被主动调用,这一改进扩展了生命周期;同时 onLoad 事件也添加了一个参数,用于接收预加载或者是预查询的数据:// params// data.from: 来源页面,page1// data.prefetch: 预查询数据// data.preload: 预加载数据onLoad (params, data) {}WePY 开源框架实现小程序的数据优化可能有开发者还不了解,其实小程序的视图层与逻辑层是完全分离的,这二者之间的通信全都依赖于 WeixinJSBridge 实现。如:开发者工具中是基于 window.postMessageiOS 中基于 window.webkit.messageHandlers.invokeHandler.postMessageAndroid 中基于 WeixinJSCore.invokeHandler数据绑定方法 this.setData() 亦然,于是很容易想到,频繁的数据绑定会不会导致通信的成本大大增加呢?为了验证 setData() 存在性能问题,微信支付团队做了一个相关测试:动态绑定 1000 条数据的列表进行性能测试,主要针对以下三种情况:最优绑定: 在内存中添加完毕后最后执行 setData() 操作。最差绑定: 在添加一条记录执行一次 setData() 操作。最智能绑定:不管中间进行了什么操作,在运行结束时执行一次脏检查,对需要设置的数据进行 setData() 操作。经过十次刷新运行测试后得出了以下结果:从测试结果可以明显看出,实现同样的逻辑,性能数据却相差 40 倍左右。通过分析测试结果,我们可以得知,在开发过程中,应当尽量避免同一流程内多次 setData() 操作。那么有什么优化方式呢?采取人工维护肯定能够实现,但当页面逻辑负责起来之后,即便花很大的精力去维护也不一定能保证每个流程只存在一次 setData(),可维护性也不高。因此,WePY 开源框架选择了使用脏检查去做数据绑定优化。虽然 WePY 开源框架在语法上借鉴了 Vue,原理则是完全不同的。比如 WePY 开源框架使用的是 ng 的脏检查设计,而不是使用的 Vue 的 getter,setter 等。用户不用再担心在流程里,数据被修改了多少次,只用在流程最后做一次脏检查,并且按需执行 setData() 即可。使用 WePY 开源框架在开发效率上的提升除了上述基于性能上做出的优化以外,WePY 开源框架也作出了一系列开发效率上的优化。支持丰富的编译器js 可以选择用 Babel 或者 TypeScript 编译。wxml 可以选择使用 Pug(原Jade)。wxss 可以选择使用 Less、Sass、Styus。支持丰富的插件处理可以通过配置插件对生成的js进行压缩混淆,压缩图片,压缩 wxml 和 json 已节省空间等等。支持 ESLint 语法检查添加一行配置就可以支持 ESLint 语法检查,可以避免低级语法错误以及统一项目代码的风格。生命周期优化WePY 开源框架添加了 onRoute 的生命周期。用于页面跳转后触发。 这一优化项是因为小程序中并不存在一个页面跳转事件。onShow 事件可以用作页面跳转事件,但同时也存在负作用,比如按 HOME 键后切回来,或者拉起支付后取消,拉起分享后取消都会触发 onShow 事件。优化事件传参原有的传参写法:<view data-alpha-beta=“1” data-alphaBeta=“2” bindtap=“bindViewTap”> DataSet Test </view>Page({ bindViewTap:function(event){ event.target.dataset.alphaBeta === 1 // - 会转为驼峰写法 event.target.dataset.alphabeta === 2 // 大写会转为小写 }})优化后:<view @tap=“bindViewTap(“1”, “2”)"> DataSet Test </view>methods: { bindViewTap(p1, p2, event) { p1 === “1”; p2 === “2”; }}更多详情可以参看 WePY 开源框架文档:https://tencent.github.io/wep…。WePY 开源框架2.0 计划目前 WePY 开源框架主要由微信支付团队内相关人员利用业余时间,与几个外部贡献者一起来维护。技术社区中有不少热心的贡献者,不仅自己参与,也会带来了一些新的贡献者力量,不时还会提供一些比较核心的功能。当被问及「WePY 开源框架 2.0 进度」问题时,微信支付团队表示现已将进入了 WePY 2.0 内测阶段,相信不久后就将与大家正式见面。「希望 2.0 是一个全新的,对得起开发者的版本。」了解更多小程序开发相关内容,欢迎微信扫描下方二维码关注微信极客WeGeek公众号,共筑微信生态。 ...

November 13, 2018 · 2 min · jiezi

Category 特性在 iOS 组件化中的应用与管控

背景iOS Category功能简介Category 是 Objective-C 2.0之后添加的语言特性。Category 就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。在 Objective-C(iOS 的开发语言,下文用 OC 代替)中的具体体现为:实例(类)方法、属性和协议。除了引用中提到的添加方法,Category 还有很多优势,比如将一个类的实现拆分开放在不同的文件内,以及可以声明私有方法,甚至可以模拟多继承等操作,具体可参考官方文档Category。若 Category 添加的方法是基类已经存在的,则会覆盖基类的同名方法。本文将要提到的组件间通信都是基于这个特性实现的,在本文的最后则会提到对覆盖风险的管控。组件通信的背景随着移动互联网的快速发展,不断迭代的移动端工程往往面临着耦合严重、维护效率低、开发不够敏捷等常见问题,因此越来越多的公司开始推行“组件化”,通过解耦重组组件来提高并行开发效率。但是大多数团队口中的“组件化”就是把代码分库,主工程使用 CocoaPods 工具把各个子库的版本号聚合起来。但能合理的把组件分层,并且有一整套工具链支撑发版与集成的公司较少,导致开发效率很难有明显地提升。处理好各个组件之间的通信与解耦一直都是组件化的难点。诸如组件之间的 Podfile 相互显式依赖,以及各种联合发版等问题,若处理不当可能会引发“灾难”性的后果。目前做到 ViewController (指iOS中的页面,下文用VC代替)级别解耦的团队较多,维护一套 mapping 关系并使用 scheme 进行跳转,但是目前仍然无法做到更细粒度的解耦通信,依然满足不了部分业务的需求。实际业务案例例1:外卖的首页的商家列表(WMPageKit),在进入一个商家(WMRestaurantKit)选择5件商品返回到首页的时候,对应的商家cell需要显示已选商品“5”。例2:搜索结果(WMSearchKit)跳转到商超的容器页(WMSupermarketKit),需要传递一个通用Domain(也有的说法叫模型、Model、Entity、Object等等,下文统一用Domain表示)。例3:做一键下单需求(WMPageKit),需要调用下单功能的一个方法(WMOrderKit)入参是一个订单相关 Domain 和一个 VC,不需要返回值。这几种场景基本涵盖了组件通信所需的的基本功能,那么怎样才可以实现最优雅的解决方案?组件通信的探索模型分析对于上文的实际业务案例,很容易想到的应对方案有三种,第一是拷贝共同依赖代码,第二是直接依赖,第三是下沉公共依赖。对于方案一,会维护多份冗余代码,逻辑更新后代码不同步,显然是不可取的。对于方案二,对于调用方来说,会引入较多无用依赖,且可能造成组件间的循环依赖问题,导致组件无法发布。对于方案三,其实是可行解,但是开发成本较大。对于下沉出来的组件来说,其实很难找到一个明确的定位,最终沦为多个组件的“大杂烩”依赖,从而导致严重的维护性问题。那如何解决这个问题呢?根据面向对象设计的五大原则之一的“依赖倒置原则”(Dependency Inversion Principle),高层次的模块不应该依赖于低层次的模块,两者(的实现)都应该依赖于抽象接口。推广到组件间的关系处理,对于组件间的调用和被调用方,从本质上来说,我们也需要尽量避免它们的直接依赖,而希望它们依赖一个公共的抽象层,通过架构工具来管理和使用这个抽象层。这样我们就可以在解除组件间在构建时不必要的依赖,从而优雅地实现组件间的通讯。业界现有方案的几大方向实践依赖倒置原则的方案有很多,在 iOS 侧,OC 语言和 Foundation 库给我们提供了数个可用于抽象的语言工具。在这一节我们将对其中部分实践进行分析。1.使用依赖注入代表作品有 Objection 和 Typhoon,两者都是 OC 中的依赖注入框架,前者轻量级,后者较重并支持 Swift。比较具有通用性的方法是使用「协议」 <-> 「类」绑定的方式,对于要注入的对象会有对应的 Protocol 进行约束,会经常看到一些RegisterClass:ForProtocol:和classFromProtocol的代码。在需要使用注入对象时,用框架提供的接口以协议作为入参从容器中获得初始化后的所需对象。也可以在 Register 的时候直接注册一段 Block-Code,这个代码块用来初始化自己,作为id类型的返回值返回,可以支持一些编译检查来确保对应代码被编译。美团内推行将一些运行时加载的操作前移至编译时,比如将各项注册从 +load 改为在编译期使用__attribute((used,section("__DATA,key"))) 写入 mach-O 文件 Data 的 Segment 中来减少冷启动的时间消耗。因此,该方案的局限性在于:代码块存取的性能消耗较大,并且协议与类的绑定关系的维护需要花费更多的时间成本。2.基于SPI机制全称是 Service Provider Interfaces,代表作品是 ServiceLoader。实现过程大致是:A库与B库之间无依赖,但都依赖于P平台。把B库内的一个接口I下沉到平台层(“平台层”也叫做“通用能力层”,下文统一用平台层表示),入参和返回值的类型需要平台层包含,接口I的实现放在B库里(因为实现在B库,所以实现里可以正常引用B库的元素)。然后A库通过P平台的这个接口I来实现功能。A可以调用的到接口I,但是在B的库中进行实现。在A库需要通过一个接口I实例化出一个对象,使用ServiceLoader.load(接口,key),通过注册过的key使用反射找到这个接口imp的文件路径然后得到这个实例对象调用对应接口。这个操作在安卓中使用较为广泛,大致相当于用反射操作来替代一次了 import 这样的耦合引用。但实际上iOS中若使用反射来实现功能则完全不必这么麻烦。关于反射,Java可以实现类似于ClassFromString的功能,但是无法直接使用 MethodFromString的功能。并且ClassFromString也是通过字符串map到这个类的文件路径,类似于 com.waimai.home.searchImp,从而可以获得类型然后实例化,而OC的反射是通过消息机制实现。3.基于通知中心之前和一个做读书类App的同学交流,发现行业内有些公司的团队在使用 NotificationCenter 进行一些解耦的通信,因为通知中心本身支持传递对象,并且通知中心的功能也原生支持同步执行,所以也可以达到目的。通知中心在iOS 9之后有一次比较大的升级,将通知支持了 request 和 response 的处理逻辑,并支持获取到通知的发送者。比以往的通知群发但不感知发送者和是否收到,进步了很多。字符串的约定也可以理解为一个简化的协议,可设置成宏或常量放在平台层进行统一的维护。比较明显的缺陷是开发的统一范式难以约束,风格迥异,且字符串相较于接口而言还是难以管理。4.使用objc_msgSend这是iOS原生消息机制中最万能的方法,编写时会有一些硬编码。核心代码如下:id s = ((id(*)(id, SEL))objc_msgSend)(ClassName,@selector(methodName)); 这种方法的特点是即插即用,在开发者能100%确定整条调用链没问题的时候,可以快速实现功能。此方案的缺陷在于编写十分随意,检查和校验的逻辑还不够,满屏的强转。对于 int、Integer、NSNumber 这样的很容易发生类型转换错误,结果虽然不报错,但数字会有错误。方案对比接下来,我们对这几个大方向进行一些性能对比。考虑到在公司内的实际用法与限制,可能比常规方法增加了若干步骤,结果也可能会与常规裸测存在一定的偏差。例如依赖注入常用做法是存在单例(内存)里,但是我们为了优化冷启动时间都写入 mach-O 文件 Data 的 Segment 里了,所以在我们的统计口径下存取时间会相对较长。// 为了不暴露类名将业务属性用“some”代替,并隐藏初始化、循环100W次、差值计算等代码,关键操作代码如下// 存取注入对象xxConfig = [[WMSomeGlueCore sharedInstance] createObjectForProtocol:@protocol(WMSomeProtocol)];// 通知发送[[NSNotificationCenter defaultCenter]postNotificationName:@“nixx” object:nil];// 原生接口调用a = [WMSomeClass class];// 反射调用b = objc_getClass(“WMSomeClass”);运行结果显示如下:可以看出原生的接口调用明显是最高效的用法,反射的时长比原生要多一个数量级,不过100W次也就是多了几十毫秒,还在可以接受的范围之内。通知发送相比之下性能就很低了,存取注入对象更低。当然除了性能消耗外,还有很多不好量化的维度,包括规范约束、功能性、代码量、可读性等,笔者按照实际场景客观评价给出对比的分值。下面,我们用五种维度的能力值图来对比每一种方案优缺点:各维度的的评分考虑到了一定的实际场景,可能和常规结果稍有偏差。已经做了转化,看图面积越大越优。可读性的维度越长代表可读性越高,代码量的维度越长代表代码成本越少。如图2所示,可以看出上图的四种方式或多或少都存在一些缺点:依赖注入是因为美团的实际场景问题,所以在性能消耗上存在明显的短板,并且代码量和可读性都不突出,规范约束这里是亮点。SPI机制的范围图很大,但使用了反射,并且代码开发成本较高,实践上来看,对协议管理有一定要求。通知中心看上去挺方便,但发送与接收大多成对出现,还附带绑定方法或者Block,代码量并不少。而msgsend功能强大,代码量也少,但是在规范约束和可读性上几乎为零。综合看来 SPI 和 objc_msgSend 两者的特点比较明显,很有潜力,如果针对这两种方案分别进行一定程度的完善,应该可以实现一个综合评分更高的方案。从现有方案中完善或衍生出的方案5.使用Category+NSInvocation此方案从 objc_msgSend 演化而来。NSInvocation 的调用方式的底层还是会使用到 objc_msgSend,但是通过一些方法签名和返回值类型校验,可以解决很多类型规范相关的问题,并且这种方式没有繁琐的注册步骤,任何一次新接口的添加,都可以直接在低层的库中进行完成。为了更进一步限制调用者能够调用的接口,创建一些 Category 来提供接口,内部包装下层接口,把返回值和入参都限制实际的类型。业界比较接近的例子有 casatwy 的 CTMediator。6.原生CategoryCoverOrigin方式此方案从 SPI 方式演化而来。两个的共同点是都在平台层提供接口供业务方调用,不同点是此方式完全规避了各种硬编码。而且 CategoryCoverOrigin 是一个思想,没有任何框架代码,可以说 OC 的 Runtime 就是这个方案的框架支撑。此方案的核心操作是在基类里汇总所有业务接口,在上层的业务库中创建基类的 Category 中对声明的接口进行覆盖。整个过程没有任何硬编码与反射。演化出的这两种方案能力评估如下(绿色部分),图中也贴了和演化前方案(桔色部分)的对比:上文对这两种方案描述的非常概括,可能有同学会对能力评估存在质疑。接下来会分别进行详解的介绍,并描述在实际操作值得注意的细节。这两种方案组合成了外卖内部的组件通信框架 WMScheduler。WMScheduler组件通信外卖的 WMScheduler 主要是通过对 Category 特性的运用来实现组件间通信,实际操作中有两种的应用方案:Category+NSInvocation 和 Category CoverOrigin。1.Category+NSInvocation方案方案简介:这个方案将其对 NSInvocation 功能容错封装、参数判断、类型转换的代码写在下层,提供简易万能的接口。并在上层创建通信调度器类提供常用接口,在调度器的的 Category 里扩展特定业务的专用接口。所有的上层接口均有规范约束,这些规范接口的内部会调用下层的简易万能接口即可通过NSInvocation 相关的硬编码操作调用任何方法。UML图:如图3-1所示,代码的核心在 WMSchedulerCore 类,其包含了基于 NSInvocation 对 target 与 method 的操作、对参数的处理(包括对象,基本数据类型,NULL类型)、对异常的处理等等,最终开放了简洁的万能接口,接口参数有 target、method、parameters等等,然后内部帮我们完成调用。但这个接口并不是让上层业务直接进行调用,而是需要创建一个 WMSchedule r的 Category,在这个 Category 中编写规范的接口(前缀、入参类型、返回值类型都是确定的)。值得一提的是,提供业务专用接口的 Category 没有以 WMSchedulerCore 为基类,而是以 WMScheduler 为基类。看似多此一举,实际上是为了做权限的隔离。上层业务只能访问到 WMScheduler.h 及其 Category 的规范接口。并不能访问到 WMSchedulerCore.h 提供的“万能但不规范”接口。例如:在UML图中可以看到 外界只可以调用到wms_getOrderCountWithPoiid(规范接口),并不能使用wm_excuteInstance Method(万能接口)。为了更好地理解实际使用,笔者贴一个组件调用周期的完整代码:如图3-2,在这种方案下,“B库调用A库方法”的需求只需要改两个仓库的代码,需要改动的文件标了下划线,请仔细看下示例代码。示例代码:平台(通用功能)库三个文件:①// WMScheduler+AKit.h#import “WMScheduler.h”@interface WMScheduler(AKit)/** * 通过商家id查到当前购物车已选e的小红点数量 * @param poiid 商家id * @return 实际的小红点数量 */+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber )poiID;@end②// WMScheduler+AKit.m#import “WMSchedulerCore.h”#import “WMScheduler+AKit.h”#import “NSObject+WMScheduler.h”@implementation WMScheduler (AKit)+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber )poiID{ if (nil == poiid) { return 0; }#pragma clang diagnostic push#pragma clang diagnostic ignored “-Wundeclared-selector” id singleton = [wm_scheduler_getClass(“WMXXXSingleton”) wm_executeMethod:@selector(sharedInstance)]; NSNumber orderFoodCount = [singleton wm_executeMethod:@selector(calculateOrderedFoodCountWithPoiID:) params:@[poiID]]; return orderFoodCount == nil ? 0 : [orderFoodCount integerValue];#pragma clang diagnostic pop}@end③// WMSchedulerInterfaceList.h#ifndef WMSchedulerInterfaceList_h#define WMSchedulerInterfaceList_h// 这个文件会被加到上层业务的pch里,所以下文不用import本文件#import “WMScheduler.h”#import “WMScheduler+AKit.h”#endif / WMSchedulerInterfaceList_h /BKit (调用方)一个文件:// WMHomeVC.m@interface WMHomeVC () <UITableViewDataSource, UITableViewDelegate>@end@implementation WMHomeVC… NSUInteger foodCount = [WMScheduler wms_getOrderedFoodCountWithPoiID:currentPoi.poiID]; NSLog(@"%ld",foodCount);…@end代码分析:上文四个文件完成了一次跨组件的调用,在 WMScheduler+AKit.m 中的第30、31行,调用的都是AKit(提供方)的现有方法,因为 WMSchedulerCore 提供了 NSInvocation 的调用方式,所以可以直接向上调用。WMScheduler+AKit 中提供的接口就是上文说的“规范接口”,这个接口在WMHomeVC(调用方)调用时和调用本仓库内的OC方法,并没有区别。延伸思考:上文的例子中入参和返回值都是基本数据类型,Domain 也是支持的,前提是这个 Domain 是放在平台库的。我们可以将工程中的 Domain 分为BO(Business Object)、VO(View Object)与TO(Transfer Object),VO 经常出现在 view 和 cell,BO一般仅在各业务子库内部使用,这个TO则是需要放在平台库是用于各个组件间的通信的通用模型。例如:通用 PoiDomain,通用 OrderDomain,通用 AddressDomain 等等。这些称为 TO 的 Domain 可以作为规范接口的入参类型或返回值类型。在实际业务场景中,跳转页面时传递 Domain 的需求也是一个老生常谈的问题,大多数页面级跳转框架仅支持传递基本数据类型(也有 trick 的方式传 Domain 内存地址但很不优雅)。在有了上文支持的能力,我们可以在规范接口内通过万能接口获取目标页面的VC,并调用其某个属性的 set 方法将我们想传递的Domain赋值过去,然后将这个 VC 对象作为返回值返回。调用方获得这个 VC 后在当前的导航栈内push即可。上文代码中我们用 WMScheduler 调用了 Akit 的一个名为calculateOrderedFoodCount WithPoiID:的方法。那么有个争议点:在组件通信需要调用某方法时,是允许直接调用现有方法,还是复制一份加上前缀标注此方法专门用于提供组件通信? 前者的问题点在于现有方法可能会被修改,扩充参数会直接导致调用方找不到方法,Method 字符串的不会编译报错(上文平台代码 WMScheduler+AKit.m 中第31行)。后者的问题在于大大增加了开发成本。权衡后我们还是使用了前者,加了些特殊处理,若现有方法被修改了,则会在isReponseForSelector这里检查出来,并走到 else 的断言及时发现。阶段总结:Category+NSInvocation 方案的优点是便捷,因为 Category 的专用接口放在平台库,以后有除了 BKit 以外的其他调用方也可以直接调用,还有更多强大的功能。但是,不优雅的地方我们也列举一下:当这个跨组件方法内部的代码行数比较多时,会写很多硬编码。硬编码method字符串,在现有方法被修改时,编译检测不报错(只能靠断言约束)。下层库向上调用的设计会被诟病。接下来介绍的 CategoryCoverOrigin 的方案,可以解决这三个问题。2.CategoryCoverOrigin方案方案简介:首先说明下这个方案和 NSInvocation 没有任何关系,此方案与上一方案也是完全不同的两个概念,不要将上一个方案的思维带到这里。此方案的思路是在平台层的 WMScheduler.h 提供接口方法,接口的实现只写空实现或者兜底实现(兜底实现中可根据业务场景在 Debug 环境下增加 toast 提示或断言),上层库的提供方实现接口方法并通过 Category 的特性,在运行时进行对基类同名方法的替换。调用方则正常调用平台层提供的接口。在 CategoryCoverOrigin 的方案中 WMScheduler 的 Category 在提供方仓库内部,因此业务逻辑的依赖可以在仓库内部使用常规的OC调用。UML图:从图4-1可以看出,WMScheduler 的 Category 被移到了业务仓库,并且 WMScheduler 中有所有接口的全集。为了更好地理解 CategoryCover 实际应用,笔者再贴一个此方案下的完整完整代码:如图4-2,在这种方案下,“B库调用A库方法”的需求需要修改三个仓库的代码,但除了这四个编辑的文件,没有其他任何的依赖了,请仔细看下代码示例。示例代码:平台(通用功能库)两个文件①// WMScheduler.h@interface WMScheduler : NSObject// 这个文件是所有组件通信方法的汇总#pragma mark - AKit / * 通过商家id查到当前购物车已选e的小红点数量 * @param poiid 商家id * @return 实际的小红点数量 */+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID;#pragma mark - CKit// …#pragma mark - DKit// …@end②// WMScheduler.m#import “WMScheduler.h”@implementation WMScheduler#pragma mark - Akit+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID{ return 0; // 这个.m里只要求一个空实现 作为兜底方案。}#pragma mark - Ckit// …#pragma mark - Dkit// …@endAKit(提供方)一个 Category 文件:// WMScheduler+AKit.m#import “WMScheduler.h”#import “WMAKitBusinessManager.h”#import “WMXXXSingleton.h” // 直接导入了很多AKit相关的业务文件,因为本身就在AKit仓库内@implementation WMScheduler (AKit)// 这个宏可以屏蔽分类覆盖基类方法的警告#pragma clang diagnostic push#pragma clang diagnostic ignored “-Wobjc-protocol-method-implementation”// 在平台层写过的方法,这边是是自动补全的+ (NSUInteger)wms_getOrderedFoodCountWithPoiID:(NSNumber *)poiID{ if (nil == poiid) { return 0; } // 所有AKIT相关的类都能直接接口调用,不需要任何硬编码,可以和之前的写法对比下。 WMXXXSingleton *singleton = [WMXXXSingleton sharedInstance]; NSNumber *orderFoodCount = [singleton calculateOrderedFoodCountWithPoiID:poiID]; return orderFoodCount == nil ? 0 : [orderFoodCount integerValue];}#pragma clang diagnostic pop@endBKit(调用方) 一个文件写法不变:// WMHomeVC.m@interface WMHomeVC () <UITableViewDataSource, UITableViewDelegate>@end@implementation WMHomeVC… NSUInteger *foodCount = [WMScheduler wms_getOrderedFoodCountWithPoiID:currentPoi.poiID]; NSLog(@"%ld",foodCount);…@end代码分析:CategoryCoverOrigin 的方式,平台库用 WMScheduler.h 文件存放所有的组件通信接口的汇总,各个仓库用注释隔开,并在.m文件中编写了空实现。功能代码编写在服务提供方仓库的 WMScheduler+AKit.m,看这个文件的17、18行业务逻辑是使用常规 OC 接口调用。在运行时此Category的方法会覆盖 WMScheduler.h 基类中的同名方法,从而达到目的。CategoryCoverOrigin 方式不需要其他功能类的支撑。延伸思考:如果业务库很多,方法很多,会不会出现 WMScheduler.h 爆炸? 目前我们的工程跨组件调用的实际场景不是很多,所以汇总在一个文件了,如果满屏都是跨组件调用的工程,则需要思考业务架构与模块划分是否合理这一问题。当然,如果真出现 WMScheduler.h 爆炸的情况,完全可以将各个业务的接口移至自己Category 的.h文件中,然后创建一个 WMSchedulerInterfaceList 文件统一 import 这些 Category。两种方案的选择刚才我们对于 Category+NSInvocation 和 CategoryCoverOrigin 两种方式都做了详细的介绍,我们再整理一下两者的优缺点对比:Category+NSInvocationCategoryCover优点只改两个仓库,流程上的时间成本更少可以实现url调用方法(scheme://target/method:?para=x)无任何硬编码,常规OC接口调用除了接口声明、分类覆盖、调用,没有其他多余代码不存在下层调用上层的场景缺点功能复杂时硬编码写法成本较大下层调上层,上层业务改变时会影响平台接口不能使用url调用方法新增接口时需改动三个仓库,稍有麻烦。(当接口已存在时,两种方式都只需修改一处)笔者更建议使用 CategoryCoverOrigin 的无硬编码的方案,当然具体也要看项目的实际场景,从而做出最优的选择。更多建议关于组件对外提供的接口,我们更倾向于借鉴 SPI 的思想,作为一个 Kit 哪些功能是需要对外公开的?提供哪些服务给其他方解耦调用?建议主动开放核心方法,尽量减少“用到才补”的场景。例如全局购物车就需要“提供获取小红点数量的方法”,商家中心就需要提供“根据字符串 id 得到整个 Poi 的 Domain”的接口服务。需要考虑到抽象能力,提供更有泛用性的接口。比如“获取到了最低满减价格后拼接成一个文案返回字符串” 这个方法,就没有“获取到了最低满减价格” 这个方法具备泛用性。Category 风险管控先举两个发生过的案例1. 2017年10月 一个关于NSDate重复覆盖的问题当时美团平台有 NSDate+MTAddition 类,在外卖侧有 NSDate+WMAddition 类。前者 NSDate+MTAddition 之前就有方法 getCurrentTimestamp,返回的时间戳是秒。后者 NSDate+WMAddition 在一次需求中也增加了 getCurrentTimestamp 方法,但是为了和其他平台统一口径返回值使用了毫秒。在正常的加载顺序中外卖类比平台类要晚,因此在外卖的测试中没有发现问题。但集成到 imeituan 主项目之后,原先其他业务方调用这个返回“秒”的方法,就被外卖测的返回“毫秒”的同名方法给覆盖了,出现接口错误和UI错乱等问题。2. 2018年3月 一个WMScheduler组件通信遇到的问题在外卖侧有订单组件和商家容器组件,这两个组件的联系是十分紧密的,有的功能放在两个仓库任意一个中都说的通。因此出现了了两个仓库写了同名方法的场景。在 WMScheduler+Restaurant 和 WMScheduler+Order 两个仓库都添加了方法 -(void)wms_enterGlobalCartPageFromPage:,在运行中这两处有一处被覆盖。在有一次 Bug 解决中,给其中一处增加了异常处理的代码,恰巧增加的这处先加载,就被后加载的同名方法覆盖了,这就导致了异常处理代码不生效的问题。那么使用 CategoryCover 的方式是不是很不安全? NO!只要弄清其中的规律,风险点都是完全可以管控的,接下来,我们来分析 Category 的覆盖原理。Category 方法覆盖原理1) Category 的方法没有“完全替换掉”原来类已经有的方法,也就是说如果 Category 和原来类都有methodA,那么 Category 附加完成之后,类的方法列表里会有两个 methodA。2) Category 方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的 Category 的方法会“覆盖”掉原来类的同名方法,这是因为运行过程中,我们在查找方法的时候会顺着方法列表的顺序去查找,它只要一找到对应名字的方法,就会罢休^_^,殊不知后面可能还有一样名字的方法。Category 在运行期进行决议,而基类的类是在编译期进行决议,因此分类中,方法的加载顺序一定在基类之后。美团曾经有一篇技术博客深入分析了 Category,并且从编译器和源码的角度对分类覆盖操作进行详细解析:深入理解Objective-C:Category根据方法覆盖的原理,我们可以分析出哪些操作比较安全,哪些存在风险,并针对性地进行管理。接下来,我们就介绍美团 Category 管理相关的一些工作。Category 方法管理由于历史原因,不管是什么样的管理规则,都无法直接“一刀切”。所以针对现状,我们将整个管理环节先拆分为“数据”、“场景”、 “策略”三部分。其中数据层负责发现异常数据,所有策略公用一个数据层。针对 Category 方法的数据获取,我们有如下几种方式:根据优缺点的分析,再考虑到美团已经彻底实现了“组件化”的工程,所以对 Category 的管控最好放在集成阶段以后进行。我们最终选择了使用 linkmap 进行数据获取,具体方法我们将在下文进行介绍。策略部分则针对不同的场景异常进行控制,主要的开发工作位于我们的组件化 CI 系统上,即之前介绍过的 Hyperloop 系统。Hyperloop 本身即提供了包括白名单,发布集成流程管理等一系列策略功能,我们只需要将工具进行关联开发即可。我们开发的数据层作为一个独立组件,最终也是运行在 Hyperloop 上。根据场景细分的策略如下表所示(需要注意的是,表中有的场景实际不存在,只是为了思考的严谨列出):我们在前文描述的 CategoryCoverOrigin 的组件通信方案的管控体现在第2点。风险管控中提到的两个案例的管控主要体现在第4点。Category 数据获取原理上一章节,我们提到了采用 linkmap 分析的方式进行 Category 数据获取。在这一章节内,我们详细介绍下做法。启用 linkmap首先,linkmap 生成功能是默认关闭的,我们需要在 build settings 内手动打开开关并配置存储路径。对于美团工程和美团外卖工程来说,每次正式构建后产生的 linkmap,我们还会通过内部的美团云存储工具进行持久化的存储,保证后续的可追溯。linkmap 组成若要解析 linkmap,首先需要了解 linkmap 的组成。如名称所示,linkmap 文件生成于代码链接之后,主要由4个部分组成:基本信息、Object files 表、Sections 表和 Symbols 表。前两行是基本信息,包括链接完成的二进制路径和架构。如果一个工程内有多个最终产物(如 Watch App 或 Extension),则经过配置后,每一个产物的每一种架构都会生成一份 linkmap。# Path: /var/folders/tk/xmlx38_x605127f0fhhp_n1r0000gn/T/d20180828-59923-v4pjhg/output-sandbox/DerivedData/Build/Intermediates.noindex/ArchiveIntermediates/imeituan/InstallationBuildProductsLocation/Applications/imeituan.app/imeituan# Arch: arm64第二部分的 Object files,列举了链接所用到的所有的目标文件,包括代码编译出来的,静态链接库内的和动态链接库(如系统库),并且给每一个目标文件分配了一个 file id。# Object files:[ 0] linker synthesized[ 1] dtrace[ 2] /var/folders/tk/xmlx38_x605127f0fhhp_n1r0000gn/T/d20180828-59923-v4pjhg/output-sandbox/DerivedData/Build/Intermediates.noindex/ArchiveIntermediates/imeituan/IntermediateBuildFilesPath/imeituan.build/DailyBuild-iphoneos/imeituan.build/Objects-normal/arm64/main.o……[ 26] /private/var/folders/tk/xmlx38_x605127f0fhhp_n1r0000gn/T/d20180828-59923-v4pjhg/repo-sandbox/imeituan/Pods/AFNetworking/bin/libAFNetworking.a(AFHTTPRequestOperation.o)……[25919] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.3.sdk/usr/lib/libobjc.tbd[25920] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS11.3.sdk/usr/lib/libSystem.tbd第三部分的 Sections,记录了所有的 Section,以及它们所属的 Segment 和大小等信息。# Sections:# Address Size Segment Section0x100004450 0x07A8A8D0 __TEXT __text……0x109EA52C0 0x002580A0 __DATA __objc_data0x10A0FD360 0x001D8570 __DATA __data0x10A2D58D0 0x0000B960 __DATA __objc_k_kylin……0x10BFE4E5D 0x004CBE63 __RODATA __objc_methname0x10C4B0CC0 0x000D560B __RODATA __objc_classname第四部分的 Symbols 是重头戏,列举了所有符号的信息,包括所属的 object file、大小等。符号除了我们关注的 OC 的方法、类名、协议名等,也包含 block、literal string 等,可以供其他需求分析进行使用。# Symbols:# Address Size File Name0x1000045B8 0x00000060 [ 2] ___llvm_gcov_writeout0x100004618 0x00000028 [ 2] ___llvm_gcov_flush0x100004640 0x00000014 [ 2] ___llvm_gcov_init0x100004654 0x00000014 [ 2] ___llvm_gcov_init.40x100004668 0x00000014 [ 2] ___llvm_gcov_init.60x10000467C 0x0000015C [ 3] _main……0x10002F56C 0x00000028 [ 38] -[UIButton(_AFNetworking) af_imageRequestOperationForState:]0x10002F594 0x0000002C [ 38] -[UIButton(_AFNetworking) af_setImageRequestOperation:forState:]0x10002F5C0 0x00000028 [ 38] -[UIButton(_AFNetworking) af_backgroundImageRequestOperationForState:]0x10002F5E8 0x0000002C [ 38] -[UIButton(_AFNetworking) af_setBackgroundImageRequestOperation:forState:]0x10002F614 0x0000006C [ 38] +[UIButton(AFNetworking) sharedImageCache]0x10002F680 0x00000010 [ 38] +[UIButton(AFNetworking) setSharedImageCache:]0x10002F690 0x00000084 [ 38] -[UIButton(AFNetworking) imageResponseSerializer]……linkmap 数据化根据上文的分析,在理解了 linkmap 的格式后,通过简单的文本分析即可提取数据。由于美团内部 iOS 开发工具链统一采用 Ruby,所以 linkmap 分析也采用 Ruby 开发,整个解析器被封装成一个 Ruby Gem。具体实施上,处于通用性考虑,我们的 linkmap 解析工具分为解析、模型、解析器三层,每一层都可以单独进行扩展。对于 Category 分析器来说,link map parser 解析指定 linkmap,生成通用模型的实例。从实例中获取 symbol 类,将名字中有“()”的符号过滤出来,即为 Category 方法。接下来只要按照方法名聚合,如果超过1个则肯定有 Category 方法冲突的情况。按照上一节中分析的场景,分析其具体冲突类型,提供结论输出给 Hyperloop。具体对外接口可以直接参考我们的工具测试用例。最后该 Gem 会直接被 Hyperloop 使用。 it ‘should return a map with keys for method name and classify’ do @parser = LinkmapParser::Parser.new @file_path = ‘spec/fixtures/imeituan-LinkMap-normal-arm64.txt’ @analyze_result_with_classification = @parser.parse @file_path expect(@analyze_result_with_classification.class).to eq(Hash) # Category 方法互相冲突 symbol = @analyze_result_with_classification["-[NSDate isEqualToDateDay:]"] expect(symbol.class).to eq(Hash) expect(symbol[:type]).to eq([LinkmapParser::CategoryConflictType::CONFLICT]) expect(symbol[:detail].class).to eq(Array) expect(symbol[:detail].count).to eq(3) # Category 方法覆盖原方法 symbol = @analyze_result_with_classification["-[UGCReviewManager setCommonConfig:]"] expect(symbol.class).to eq(Hash) expect(symbol[:type]).to eq([LinkmapParser::CategoryConflictType::REPLACE]) expect(symbol[:detail].class).to eq(Array) expect(symbol[:detail].count).to eq(2) endCategory 方法管理总结1. 风险管理对于任何语法工具,都是有利有弊的。所以除了发掘它们在实际场景中的应用,也要时刻对它们可能带来的风险保持警惕,并选择合适的工具和时机来管理风险。而 Xcode 本身提供了不少的工具和时机,可以供我们分析构建过程和产物。若是在日常工作中遇到一些坑,不妨从构建期工具的角度去考虑管理。比如本文内提到的 linkmap,不仅可以用于 Category 分析,还可以用于二进制大小分析、组件信息管理等。投入一定资源在相关工具开发上,往往可以获得事半功倍的效果。2. 代码规范回到 Category 的使用,除了工具上的管控,我们也有相应的代码规范,从源头管理风险。如我们在规范中要求所有的 Category 方法都使用前缀,降低无意冲突的可能。并且我们也计划把“使用前缀”做成管控之一。3. 后续规划1.覆盖系统方法检查 由于目前在管控体系内暂时没有引入系统符号表,所以无法对覆盖系统方法的行为进行分析和拦截。我们计划后续和 Crash 分析系统打通符号表体系,提早发现对系统库的不当覆盖。2.工具复用 当前的管控系统仅针对美团外卖和美团 App,未来计划推广到其他 App。由于有 Hyperloop,事情在技术上并没有太大的难度。 从工具本身的角度看,我们有计划在合适的时机对数据层代码进行开源,希望能对更多的开发有所帮助。总结在这篇文章中,我们从具体的业务场景入手,总结了组件间调用的通用模型,并对常用的解耦方案进行了分析对比,最终选择了目前最适合我们业务场景的方案。即通过 Category 覆盖的方式实现了依赖倒置,将构建时依赖延后到了运行时,达到我们预期的解耦目标。同时针对该方案潜在的问题,通过 linkmap 工具管控的方式进行规避。另外,我们在模型设计时也提到,组件间解耦其实在 iOS 侧有多种方案选择。对于其他的方案实践,我们也会陆续和大家分享。希望我们的工作能对大家的 iOS 开发组件间解耦工作有所启发。作者简介尚先,美团资深工程师。2015年加入美团,目前作为美团外卖 iOS 端平台化虚拟小组组长,主要负责业务架构、持续集成和工程化相关工作。同时也是移动端领域新技术的爱好者,负责多项新技术在外卖业务落地中的难点攻关,目前个人拥有七项国家发明专利。泽响,美团技术专家,2014年加入美团,先后负责过公司 iOS 持续集成体系建设,美团 iOS 端平台业务,美团 iOS 端基础业务等工作。目前作为美团移动平台架构平台组 Team Leader,主要负责美团 App 平台架构、组件化、研发流程优化和部分基础设施建设,致力于提升平台上全业务的研发效率与质量。招聘信息美团外卖长期招聘 iOS、Android、FE 高级/资深工程师和技术专家,Base 北京、上海、成都,欢迎有兴趣的同学投递简历到 chenhang03@meituan.com。 ...

November 9, 2018 · 4 min · jiezi

高级 Vue 组件模式 (9)

09 使用 Functional 组件目标到此为止,我们的 toggle 组件已经足够强大以及好用了,因此这篇文章不会再为它增加新的特性。如果你是从第一篇文章一直读到这里的读者,你一定会发现,整篇文章中,我几乎没有对 toggle-on 和 toggle-off 做出任何更改和重构,因此这篇文章着重来重构一下它们。之前一直没有对它们进行任何更改,一个很重要的原因是因为它们足够简单,组件内部没有任何 data 状态,仅靠外部传入的 prop 属性 on 来决定内部渲染逻辑。这听起来似乎有些耳熟啊,没错,它们就是在上一篇文章中所提及的木偶组件(Dump Component)。在 Vue 中,这种类型的组件也可以叫做函数式组件(Functional Component)。仔细观察 app 组件的模板代码,会发现存在一定的冗余性的,比如:<toggle-on :on=“status.on”>{{firstTimes}}</toggle-on><toggle-off :on=“status.on”>{{firstTimes}}</toggle-off>这里两行代码的逻辑几乎一模一样,但我们却要写两次。同时你还会发现一个问题,由于其内部的渲染逻辑是通过 v-if 来描述的,实际上在 Vue 渲染完成后,会渲染两个 dom 节点,在切换时的状态从 devtool 中观察的效果大概是这样子的:未显示的节点是一个注释节点,而显示的节点是一个 div 节点。这篇文章将着重解决这两个问题:将 toggle-on 和 toggle-off 合二为一,减少代码冗余性重构以 v-if 实现的渲染逻辑,改为更好的动态渲染逻辑(仅使用一个 dom 节点)实现转化为函数式组件首先,先将已经存在的 toggle-on 和 toggle-off 组件转化为函数式组件,很简单,只需保留 template 代码块即可,同时在左边的标签上声明 functional 属性,或者在 script 代码块中声明 functional: true 也是可以的。唯一要注意的是,由于函数式组件没有 data 也没有 this,因此所有模板中用到的与 prop 相关的渲染逻辑,都要作出相应更改,比如原先的 on 要改为 props.on的形式,由于这里我们要移除 v-if 的渲染逻辑,因此直接移除即可,详细代码如下:// ToggleOn.vue<template functional> <div class=“toggle-on”><slot></slot></div></template><style>.toggle-on { color: #86d993;}</style>// ToggleOff.vue<template functional> <div class=“toggle-off”><slot></slot></div></template><style>.toggle-off { color: red;}</style>除此之外,还可以发现,我为两个组件增加不同的颜色样式以便于区分当前的开关状态。实现 ToggleStatus 组件接下来实现今天的主角,ToggleStatus 组件,由于我们的目标是将原先的二个函数式组件合二为一,因此这个组件本身应当也是一个函数式组件,不过我们需要使用另外一种写法,如下:<script>import ToggleOn from ‘./ToggleOn’import ToggleOff from ‘./ToggleOff’export default { functional: true, render(createElement, {props, data, children}) { let Comp = ToggleOff if(props.on) Comp = ToggleOn return createElement(Comp, data, children) }}</script>关于这种写法中,render 和 createElement 方法的参数就不赘述了,不熟悉的读者请参考官方文档。可以发现,这里将 toggle-on 和 toggle-off 以模块的形式导入,之后由 props.on 的值进行判定,从而决定哪一个被作为 createElement 方法的第一个参数进行渲染。诸如 data 和 children 参数我们原封不动的传入 createElement 即可,因为这里对于 toggle-status 组件的定位是一个代理组件,对于其他参数以及子元素应当原封不动的传递给被代理的组件中。之后在 app 组件中更改响应的渲染逻辑即可,如下:// controlled toggle<toggle-status :on=“status.on”>{{firstTimes}}</toggle-status>// uncontrolled toggle<toggle-status :on=“status.on”>{{secondTimes}}</toggle-status>成果一切如原先一样,只不过这次我们可以少写一行冗余的代码了。同时打开 devtool 可以发现,两种状态的组件会复用同一个 dom 节点,如下:你可以通过下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-9总结关于函数式组件,我是在 React 中第一次接触,其形式和它的名字一样,就是一个函数。这种组件和普通组件相比的优势主要在于,它是无状态的,这意味着它的可测试性和可读性更好,同时一些情况下,渲染开销也更小。我们在日常工作中,可能会经常遇到动态渲染的需求,一般情况下,我们均会通过 v-if 来解决,在比较简单的情况下,v-if 确实一种很简单且高效的方式,但是随着组件复杂度的上升,很可能会出现面条式的代码,可读性和可测试性都大打折扣,这是不妨换一个角度从渲染机制本身将组件重构为更小的颗粒,并用一个函数式组件动态的代理它们,可能会得到更好的效果,举一个比较常见的例子,比如表单系统中的表单项,一般都具有多种渲染状态,如编辑状态、浏览状态、禁用状态等等,这时利用该模式来抽离不同状态的渲染逻辑就非常不错。目录github gist ...

October 29, 2018 · 1 min · jiezi

高级 Vue 组件模式 (8)

08 使用 Control Props目标在第七篇文章中,我们对 toggle 组件进行了重构,使父组件能够传入开关状态的初始值,同时还可以传入自定义的状态重置逻辑。虽然父组件拥有了改变 toggle 组件内部状态的途径,但是如果进一步思考的话,父组件并没有绝对的控制权。在一些业务场景,我们期望父组件对于子组件的状态,拥有绝对的控制权。熟悉 React 的读者一定不会对智能组件(Smart Component)和木偶组件(Dump Component)感到陌生。对于后者,其父组件一定对其拥有绝对控制权,因为它内部没有状态,渲染逻辑完全取决于父组件所传 props 的值。而对于前者则相反,由于组件内部会有自己的状态,它内部的渲染逻辑由父组件所传 props 与其内部状态共同决定。这篇文章将着重解决这个问题,如果能够使一个智能组件的状态变得可控,即:toggle 组件的开关状态应该完全由 prop 属性 on 的值决定当没有 on 属性时,toggle 组件的开关状态降级为内部管理额外地,我们还将实现一个小需求,toggle 组件的开关状态至多切换四次,如果超过四次,则需点击重置后,才能够重新对开关切换状态进行切换。实现判定组件是否受控由于 toggle 组件为一个智能组件,我们需要提供一个判定它是否受控的方式。很简单,由目标中的第一点可知,当父组件传入了 on 属性后,toggle 处于被控制的状态,否则则没有,于是可以利用 Vue 组件的 computed 特性,声明一个 isOnControlled 计算属性,如下:computed: { isOnControlled() { return this.on !== undefined; }}其内部逻辑很简单,就是判定 prop 属性 on 的值是否为 undefined,如果是,则未被父组件控制,反之,则被父组件控制。更改 on 的声明方式由于要满足目标中提及的第二点,关于 prop 属性 on 的声明,我们要做出一些调整,如下:on: { type: Boolean, default: undefined},就是简单地将默认值,由 false 改为了 undefined,这么做的原因是因为,按照之前的写法,如果 on 未由父组件传入,则默认值为 false,那么 toggle 组件会认为父组件实际传入了一个值为 false 的 on 属性,因此会将其内部的开关状态控制为关,而非降级为内部管理开关状态。实现状态解析逻辑之前的实现中,通过 scope-slot 注入插槽的状态完全取决于组件内部 status 的值,我们需要改变状态的注入逻辑。当组件受控时,其开关状态应该与 prop 属性保持一致,反之,则和原来一样。因此编写一个叫做 controlledStatus 的计算属性:controlledStatus() { return this.isOnControlled ? { on: this.on } : this.status;}这里利用了之前声明的 isOnControlled 属性来判断当前组件是否处于受控状态。之后相应地把模板中开关状态的注入逻辑也进行更改:<slot :status=“controlledStatus” :toggle=“toggle” :reset=“reset”></slot>相应地,除了开关状态的注入逻辑,toggle 方法和 reset 方法的注入逻辑也需要更改,至于为什么,就交由读者自行思考得出答案吧,这里简单罗列实现代码,以供参考:// toggle 方法toggle() { if (this.isOnControlled) { this.$emit(“toggle”, !this.on); } else { this.status.on = !this.status.on; this.$emit(“toggle”, this.status.on); }}// reset 方法reset() { if (this.isOnControlled) { Promise.resolve(this.onReset(!this.on)).then(on => { this.$emit(“reset”, on); }); } else { Promise.resolve(this.onReset(this.status.on)).then(on => { this.status.on = on || false; this.$emit(“reset”, this.status.on); }); }}总体上的思路是,如果组件受控,则传入回调方法中的开关状态参数,是在触发相应事件后,由 prop 属性 on 得出的组件在下一时刻,应当处于的状态。这么说可能有点绕,换句话说就是,当组件状态发生更改时,如果当前的 on 属性为 true(开关状态为开),则组件本该处于关的状态,但由于组件受控,则它内部不能直接将开关状态更改为关,而是依旧保持为开,但是它会将 false(开关状态为关)作为参数传入触发事件,这将告知父组件,当前组件的下一个状态为关,至于父组件是否同意将其状态更改为关则有父组件决定。如果组件不受控,开关状态由组件内部自行管理,那和之前的实现逻辑是一模一样的,保留之前的代码即可。成果当 toggle 组件被改造后,实现这个需求就很容易了。关于实现的代码,这里就不进行罗列了,有兴趣可以通过在线代码链接进行查看,十分简单,这里仅简单附上一个最终的动态效果图:你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-8总结关于 Controlled Component 和 Uncontrolled Component 的概念,我第一次是在 React 中关于表单的介绍中接触到的。实际工作中,大部分对于状态可控的需求也都存在于表单组件中,之所以存在这样的需求,是因为表单系统往往是复杂的,将其实现为智能组件,往往内部状态过于复杂,而如果实现为木偶组件,代码结构或者实现逻辑又过于繁琐,这时如果可以借鉴这种模式的话,往往可以达到事半功倍的效果。目录github gist ...

October 29, 2018 · 1 min · jiezi

高级 Vue 组件模式 (4)

04 使用 slot 替换 mixin目标在第三篇文章中,我们使用 mixin 来抽离了注入 toggle 依赖项的公共逻辑。在 react 中,类似的需求是通过 HOC 的方式来解决的,但是仔细想想的话,react 在早些的版本也是支持 mixin 特性的,只不过后来将它标注为了 deprecated。mixin 虽然作为分发可复用功能的常用手段,但是它是一把双刃剑,除了它所带来的便利性之外,它还有以下缺点:混入的 mixin 可能包含隐式的依赖项,这在某些情况下可能不是调用者所期望的多个 mixin 可能会造成命名冲突问题,且混入结果取决于混入顺序使用不当容易使项目的复杂度呈现滚雪球式的增长所以是否有除了 mixin 以外的替代方案呢?答案当时也是有的,那就是使用 vue 中提供的作用域插槽特性。实现这里关于作用域插槽的知识同样不赘述了,不熟悉的读者可以去官方文档了解。我们可以在 toggle 组件模板中的 slot 标签上将所有与其上下文相关的方法及属性传递给它,如下:<div class=“toggle”> <slot :status=“status” :toggle=“toggle”></slot></div>这样,我们可以通过 slot-scope 特性将这些方法和属性取出来,如下:<template slot-scope="{status, toggle}"> <custom-button :on=“status.on” :toggle=“toggle”></custom-button> <custom-status-indicator :on=“status.on”></custom-status-indicator></template>当然,相比上一篇文章,我们需要对 custom-button 和 custom-status-indicator 组件做一些简单的更改,只需要将混入 mixin 的逻辑去掉,并分别声明相应的 props 属性即可。成果通过作用域插槽,我们有效地避免了第三方组件由于混入 toggleMixin 而可能造成的命名冲突以及隐式依赖等问题。你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-4总结mixin 虽好,但是一定不要滥用,作为组件开发者,可以享受它带来的便利性,但是它对于组件调用者来说,可能会造成一些不可预料的问题,通过作用域插槽,我们可以将这种问题发生的程度降到最小,同时解决 mixin 需要解决的问题。目录github gist

October 22, 2018 · 1 min · jiezi

高级 Vue 组件模式 (5)

05 使用 $refs 访问子组件引用目标在之前的文章中,详细阐述了子组件获取父组件所提供属性及方法的一些解决方案,如果我们想在父组件之中访问子组件的一些方法和属性怎么办呢?设想以下一个场景:当前的 custom-button 组件中,有一个 input 元素我们期望当 toggle 的开关状态为开时,显示 input 元素并自动获得焦点这里要想完成目标,需要获取某个组件或者每个元素的引用,在不同的 mvvm 框架中,都提供了相关特性来完成这一点:angularjs: 可以使用依赖注入的 $element 服务Angular: 可以使用 ViewChild、ContentChild 或者 template ref 来获取引用react: 使用 ref 属性声明获取引用的逻辑在 vue 中,获取引用的方法与 react 类似,通过声明 ref 属性来完成。实现首先,在 custom-button 组件中增加一个 input 元素,如下:<input v-if=“on” ref=“input” type=“text” placeholder=“addtional messages”>注意这里的 ref=“input”,这样在组件内部,可以通过 this.$refs.input 获得该元素的引用,为了实现目标中提及的需求,再添加一个新的方法 focus 来使 input 元素获取焦点,如下:focus() { this.$nextTick(function() { this.$refs.input.focus(); });},注意这里的 this.$nextTick,正常情况下,直接调用 input 的 focus 方法是没有问题的,然而却不行。因为 input 的渲染逻辑取决于 prop 属性 on 的状态,如果直接调用 focus 方法,这时 input 元素的渲染工作很可能还未结束,这时 this.$refs.input 所指向的引用值为 undefined,继续调用方法则会抛出异常,因此我们利用 this.$nextTick 方法,将调用的逻辑延迟至下次 DOM 更新循环之后执行。同理,在 app 组件中,为 custom-button 添加一个 ref 属性,如下:<custom-button ref=“customButton” :on=“status.on” :toggle=“toggle”></custom-button>之后修改 onToggle 方法中的逻辑以满足目标中的需求,当 toggle 组件状态为开时,调用 custom-button 组件的 focus 方法,如下:onToggle(on) { if (on) this.$refs.customButton.focus(); console.log(“toggle”, on);}成果点击按钮会发现,每当开关为开时,input 元素都会显示,并会自动获得焦点。你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-5总结当期望获得子元素或者子组件的引用时,切记使用 ref 和 $refs 来解决问题。文章中所举例子的交互,在实际场景中很常见,比如:当通过一个 icon 触发搜索框时,期望自动获得焦点当表单校验失败时,期望自动获得发生错误的表单项的焦点当复杂列表的筛选器展开时,期望第一个筛选单元获得焦点这几种情况下,都可以使用该模式来高效地解决问题,而不是通过使用 DOM 中的 api 或者引入 jquery 获取相关元素再进行操作。目录github gist ...

October 22, 2018 · 1 min · jiezi

高级 Vue 组件模式 (6)

06 通过 directive 增强组件内容目标之前的五篇文章中,switch 组件一直是被视为内部组件存在的,细心的读者应该会发现,这个组件除了帮我们提供开关的交互以外,还会根据当前 toggle 的开关状态,为 button 元素增加 aria-expanded 属性,以 aira 开头的属性叫作内容增强属性,它用于描述当前元素的某种特殊状态,帮助残障人士更好地浏览网站内容。但是,作为组件调用者,未必会对使用这种相关属性对网站内容进行增强,那么如何更好地解决这个问题呢?答案就是使用 directive。我们期望能够显示地声明当前的元素是一个 toggler 职能的组件或者元素,这个组件或者元素,可以根据当前 toggle 组件的开关状态,动态地更新它本身的 aria-expanded 属性,以便针对无障碍访问提供适配。实现简单实现首先创建一个 toggler 指令函数,如下:export default function(el, binding, vnode) { const on = binding.value if (on) { el.setAttribute(aria-expanded, true); } else { el.removeAttribute(aria-expanded, false); }}这个指令函数很简单,就是通过传入指令的表达式的值来判定,是否在当前元素上增加一个 aria-expanded 属性。之后再 app 引入该指令,如下:directives: { toggler}之后就可以在 app 组件的模板中使用该指令了,比如:<custom-button v-toggler=“status.on” ref=“customButton” :on=“status.on” :toggle=“toggle”></custom-button>一切都将按预期中运行,当 toggle 组件的状态为开时,custom-button 组件的根元素会增加一个 aria-expanded=“true” 的内容增强属性。Note: 这里关于指令的引入,使用的函数简写的方式,会在指令的 bind 和 update 钩子函数中触发相同的逻辑,vue 中的指令包含 5 个不同的钩子函数,这里就不赘述了,不熟悉的读者可以通过阅读官方文档来了解。注入当前组件实例上文中的指令会通过 binding.value 来获取 toggle 组件的开关状态,这样虽然可行,但在使用该指令时,custom-button 本身的 prop 属性 on 已经代表了当前的开关状态,能否直接在指令中获取当前所绑定的组件实例呢?答案是可以的。指令函数的第三个参数即为当前所绑定组件的虚拟 dom 节点实例,其 componentInstance 属性指向当前组件实例,所以可以将之前的指令改版如下:export default function(el, binding, vnode) { const comp = vnode.componentInstance; const on = binding.value || comp.on; if (on) { el.setAttribute(aria-expanded, true); } else { el.removeAttribute(aria-expanded, false); }}这样,即使不向指令传入表达式,它也可以自动去注入当前修饰组件所拥有的 prop 属性 on 的值,如下:<custom-button v-toggler ref=“customButton” :on=“status.on” :toggle=“toggle”></custom-button>提供更多灵活性指令函数的第二个参数除了可以获取传入指令内部的表达式的值以外,还有其他若干属性,比如 name、arg、modifiers等,详细说明可以去参考官方文档。为了尽可能地使指令保证灵活性,我们期望可以自定义无障碍属性 aria 的后缀名称,比如叫做 aria-on,这里我们可以通过 arg 这个参数轻松实现,改版如下:export default function(el, binding, vnode) { const comp = vnode.componentInstance; const suffix = binding.arg || “expanded”; const on = binding.value || comp.on; if (on) { el.setAttribute(aria-${suffix}, true); } else { el.removeAttribute(aria-${suffix}, false); }}可以发现,这里通过 binding.arg 来获取无障碍属性的后缀名称,并当没有传递该参数时,降级至 expanded。这里仅仅是为了演示,读者有兴趣的话,还可以利用 binding 对象的其他属性提供更多的灵活性。成果最终的运行结果就不用语言描述了,直接截了一个图,是 toggle 组件开关状态为开时的截图:你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-6总结关于指令的概念,我自身还是在 angularjs(v1.2以下版本) 中第一次接触,当时其实不兴组件化开发这个概念,指令本身的设计理念也是基于增强这个概念的,即增强某个 html 标签。到后来兴起了组件化开发的开发思想,指令似乎是随着 angularjs 的没落而消失了踪影。但仔细想想的话,web 开发流程中,并不是所有的场景都可以拿组件来抽象和描述的,比如说,你想提供一个类似高亮边框的公用功能,到底如何来按组件化的思想抽象它呢?这时候使用指令往往是一个很好的切入点。因此,当你面临解决的问题,颗粒度小于组件化抽象的粒度,同时又具备复用性,那就大胆的使用指令来解决它吧。目录github gist ...

October 22, 2018 · 1 min · jiezi

高级 Vue 组件模式 (1)

写在前头去年,曾经阅读过一系列关于高级 react 组件模式的文章,今年上半年,又抽空陆陆续续地翻译了一系列关于高级 angular 组件模式的文章,碰巧最近接手了一个公司项目,前端这块的技术栈是 vue。我对于 vue 本身还是比较熟悉的,不过大多都是一些很简单的个人项目,在构建相对比较复杂的应用中缺乏实践经验,就想着也搜搜类似题材的文章,涨涨知识。结果似乎没有找到(其实也是有一些的,只不过不是和 react 和 angular 对比来写的),不如就按照 react 和 angular 这两个系列文章的思路,使用 vue 来亲自实现一次吧。由于三个框架的设计思想、语法都有比较大的区别,所以在实现过程中,均使用更符合 vue 风格的方式去解决问题,同时也提供一些对比,供读者参考,如果观点有误,还望指正。01 实现一个 toggle 组件这个系列的文章的第一篇,都会从实现一个最简单的 toggle 组件开始。在 Vue 中,我们通过 data 来声明一个 checked 属性,这个属性所控制的状态代表组件本身的开关状态,这个状态会传递给负责渲染开关变换逻辑的 switch 组件中,关于 switch 组件,这里不做过多介绍,你把它当作一个私有组件即可,其内部实现与该篇文章没有太大的关联。同时这个组件还拥有一个 on 属性,用来初始化 checked 的状态值。通过在 switch 组件注册原生 click 事件,toggle 组件还会触发一个 toggled 事件,在 App 组件中,我们会监听这个事件,并将其回传的值打印到控制台中。你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-1总结toggle组件的实现是一个很典型的利用单向数据流作为数据源的简单组件:on 是单向数据源,checked 代表组件内部的开关状态通过触发 toggle 事件,将 checked 状态的变化传递给父组件目录github gist

October 21, 2018 · 1 min · jiezi

高级 Vue 组件模式 (3)

03 使用 mixin 来增强 Vue 组件目标之前一篇文章中,我们虽然将 toggle 组件划分为了 toggle-button、toggle-on 和 toggle-off 三个子组件,且一切运行良好,但是这里面其实是存在一些问题的:toggle 组件的内部状态和方法只能和这三个子组件共享,我们期望第三方的组件也可以共享这些状态和方法inject 的注入逻辑我们重复编写了三次,如果可以的话,我们更希望只声明一次(DRY原则)inject 的注入逻辑当前为硬编码,某些情况下,我们可能期望进行动态地配置如果熟悉 react 的读者这里可能马上就会想到 HOC(高阶组件) 的概念,而且这也是 react 中一个很常见的模式,该模式能够提高 react 组件的复用程度和灵活性。在 vue 中,我们是否也有一些手段或特性来提高组件的复用程度和灵活性呢?答案当然是有的,那就是 mixin。实现关于 mixin 本身的知识,这里就不做过多赘述了,不熟悉的读者可以去官方文档了解。我们通过声明一个叫作 toggleMixin 的 mixin 来抽离公共的注入逻辑,如下:export const withToggleMixin = { inject: { toggleComp: “toggleComp” }};之后,每当需要注入 toggle 组件提供的依赖项时,就混入当前 mixin,如下:mixins: [withToggleMixin]如果关于注入的逻辑,我们增加一些灵活性,比如期望自由地声明注入依赖项的 key 时,我们可以借由 HOC 的概念,声明一个高阶 mixin(可以简称 HOM ?? 皮一下,很开心),如下:export function withToggle(parentCompName = “toggleComp”) { return { inject: { [parentCompName]: “toggleComp” } };}这个 HOC mixin 可以按如下的方式使用:mixins: [withToggle(“toggle”)]这样在当前的组件中,调用 toggle 组件相关状态和方法时,就不再是 this.toggleComp,而是 this.toggle。成果通过实现 toggleMixin,我们成功将注入的逻辑抽离了出来,这样每次需要共享 toggle 组件的状态和方法时,混入该 mixin 即可。这样就解决了第三方组件无法共享其状态和方法的问题,在在线实例代码中,我实现了两个第三方组件,分别是 custom-button 和 custom-status-indicator,前者是自定义开关,使用 withToggleMixin 来混入注入逻辑,后者是自定义的状态指示器,使用 withToggle 高阶函数来混入注入逻辑。你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-3总结mixin 作为一种分发 Vue 组件中可复用功能的非常灵活的方式,可以在很多场景下大展身手,尤其在一些处理公共逻辑的组件,比如通知、表单错误提示等,使用这种模式尤其有用。目录github gist ...

October 21, 2018 · 1 min · jiezi

高级 Vue 组件模式 (2)

02 编写复合组件目标我们需要实现的需求是能够使使用者通过 <toggle> 组件动态地改变包含在它内部的内容。熟悉 vue 的童鞋可能马上会想到不同的解决方案,比如使用 slot 并配合 v-if,我们这里采用另外一种方法,利用 vue 提供的 provide/inject 属性按照复合组件的思想来实现。这里简单介绍下 provide/inject 的功能,它允许某个父组件向子组件注入一个依赖项(这里的父子关系可以跨域多个层级,也就是祖先与后代),如果我们在其他 mvvm 框架对比来看的话,你可以发现其他框架也具有相同的特性,比如:angularjs: directive 中的 require 属性来声明注入逻辑Angular: 依赖注入中组件级别的注入器React: context 上下文对象想进一步了解的话,可以参考官方文档实现在 vue 中,这里我们会分别实现三个组件,依次为:toggle-button: 代表开关,用来渲染父组件的开关状态toggle-on: 根据父组件 toggle 的开关状态,渲染当状态为开时的内容toggle-off: 根据父组件 toggle 的开关状态,渲染当状态为关时的内容在上一篇文章中,我们已经实现了 toggle 组件,这里我们要做一些更改。首先,需要使用 provide 属性增加一个提供依赖的逻辑,如下:provide() { return { toggleComp: { status: this.status, toggle: this.toggle } }}这里的 status 是该组件 data 中的声明的一个可监听对象,这个对象包含一个 on 属性来代表组件的开关状态,而 toggle 则是 methods 中的一个组件方法。关于为什么这里不直接使用 on 属性来代表开关状态,而使用一个可监听对象,是因为 provide 和 inject 绑定并不是可响应的,同时官方文档也指出,这是刻意而为,所以为了享受到 vue 响应性带来的便利性,我们这里传入 status 这个可监听对象。对于其他三个组件,其内部实现逻辑十分简单,相信读者通过参考在线代码实例马上就能看懂,这里只提一下关于 inject 声明注入依赖的逻辑,如下:inject: { toggleComp: “toggleComp” }这里的 “toggleComp” 与之前的 provide 对象中声明的 key 值所对应,而 inject 对象的 key 值当前组件注入依赖项的变量名称,之后,子组件即可以通过 this.toggleComp 来访问父组件的属性与方法。成果通过复合组件的方式,我们将 toggle 组件划分为了三个更小的、职责更加单一的子组件。同时由于 toggle-on 和 toggle-off 都使用 slot 来动态地注入组件调用者在其内部包含的自定义渲染逻辑,其灵活性得到了进一步的提升,只要这三个组件是作为 toggle 组件的子组件来调用,一切都将正常运行。你可以下面的链接来看看这个组件的实现代码以及演示:sandbox: 在线演示github: part-2总结通常情况下,在设计和实现职能分明的组件时,可以使用这种模式,比如 tabs 与 tab 组件,tabs 只负责 tab 的滚动、导航等逻辑,而 tab 本身仅负责内容的渲染,就如同这里的 toggle 和 toggle-button、`toggle-on、toggle-off 一样。目录github gist ...

October 21, 2018 · 1 min · jiezi

高级 Angular 组件模式 (7)

07 使用 Content Directives原文: Use Content Directives因为父组件会提供所有相关的 UI 元素(比如这里的 button),所以 toggle 组件的开发者可能无法满足组件使用者的一些附加需求,比如,在一个自定义的开关控制元素上增加 aria 属性。如果 toggle 组件能够提供一些 hooks 方法或指令给组件使用者,这些 hooks 方法或指令能够在自定义的开关元素上设置一些合理的默认值,那将是极好的。目标提供一些 hooks 方法或指令给组件使用者,使其可以与所提供的 UI 元素交互并修改它们。实现我们通过实现一个 [toggler] 指令来负责向组件使用者提供的自定义元素增加 role=“switch” 和 aria-pressed 属性。这个 [toggler] 指令拥有一个 [on] input 属性(并与 <switch> 组件共享),该属性将决定 aria-pressed 属性的值是 true 还是 false。成果stackblitz演示地址译者注到这里已经是第七篇了,也许你已经发现,Angular 中很多开发模式或者理念,都和 Directive 脱不了干系。Angular 中其本身推崇组件化开发,即把一切 UI 概念当做 Component 来看待,但仔细思考的话,这其实是有前提的,即这个 UI 概念一般是由一个或多个 html 元素组成的,比如一个按钮、一个表格等。但是在前端开发中,小于元素这个颗粒度的概念也是存在的,比如上文提及的 aira 属性便是其中之一,如果也为将这些 UI 概念抽象化为一个组件,就未免杀鸡用牛刀了,因此这里使用 Directive 才是最佳实践,其官方文章本身也有描述,Directive 即为没有模板的 Component。从组件开发者的角度来看的话,Directive 也会作为一种相对 Component 更加轻量的解决方案,因为与其提供封装良好、配置灵活、功能完备(这三点其实很难同时满足)的 Component,不如提供功能简单的 Directive,而将部分其他工作交付组件使用者来完成。比如文章中所提及的,作为组件开发者,无法预先得知组件使用者会怎样管理开关元素以及它的样式,因此提供一些 hooks 是很有必要的,而 hooks 这个概念,一般情况下,都会是相对简单的,比如生命周期 hook、调用过程 hook、自定义属性 hook 等,在这里,我们通过 Directive 为自定义开关元素增加 aria 属性来达到提供自定义属性 hook 的目标。 ...

October 9, 2018 · 1 min · jiezi

简陋至极:微信小程序日历组件(思路)

最近在做微信小程序项目,其中涉及到日历。一直以来,遇到日历,就是网上随便找个插件,这次心血来潮,想着自己去实现一下。这次不是封装功能强大,健硕完美的组件,只是记录一下,主体思路。更多功能还得根据项目需要,自己去挖掘、实现。(大佬轻喷)思路分析首先最主要的一点,就是要计算出某年某月有多少天,其中涉及到大小月,闰、平年二月。其次,弄清楚每月一号对应的是周几。然后,有时为填充完整,还需显示上月残余天数以及下月开始几天,这些又该如何展示。最后,根据自己项目需求实现其它细枝末节。计算每月天数按照一般思路,[1,3,5,7,8,10,12]这几个月是31天,[2,3,6,9,11]这几个月是30天,闰年2月29天,平年2月28天。每次需要计算天数时,都得如此判断一番。方案可行,而且也是大多数人的做法。但是,这个方法,我却觉得有些繁琐。其实换一种思路,也未尝不可。时间戳就是一个很好的载体。当前月一号零时的时间戳,与下月一号零时的时间戳之差,不就是当前月天数的毫秒数嘛。// 获取某年某月总共多少天 getDateLen(year, month) { let actualMonth = month - 1; let timeDistance = +new Date(year, month) - +new Date(year, actualMonth); return timeDistance / (1000 * 60 * 60 * 24); },看到上述代码,你可能会问,是不是还缺少当月为12月时的特殊判断,毕竟涉及到跨年问题。当然,你无需担心,根据MDN中关于Date的表述,js已经为我们考虑好了这一点当Date作为构造函数调用并传入多个参数时,如果数值大于合理范围时(如月份为13或者分钟数为70),相邻的数值会被调整。比如 new Date(2013, 13, 1)等于new Date(2014, 1, 1),它们都表示日期2014-02-01(注意月份是从0开始的)。其他数值也是类似,new Date(2013, 2, 1, 0, 70)等于new Date(2013, 2, 1, 1, 10),都表示时间2013-03-01T01:10:00。计算每月一号是周几呃,这个就不需要说了吧,getDay()你值得拥有// 获取某月1号是周几 getFirstDateWeek(year, month) { return new Date(year, month - 1, 1).getDay() },每个月的数据如何展示如果只是简单展示当月数据,那还是很简单的,获取当月天数,依次遍历,就可以拿到当月所有数据。// 获取当月数据,返回数组 getCurrentArr(){ let currentMonthDateLen = this.getDateLen(this.data.currentYear, this.data.currentMonth) // 获取当月天数 let currentMonthDateArr = [] // 定义空数组 if (currentMonthDateLen > 0) { for (let i = 1; i <= currentMonthDateLen; i++) { currentMonthDateArr.push({ month: ‘current’, // 只是为了增加标识,区分上下月 date: i }) } } this.setData({ currentMonthDateLen }) return currentMonthDateArr },很多时候,为了显示完整,需要显示上下月的残余数据。一般来说,日历展示时,最大是7 X 6 = 42位,为啥是42位,呃,自己去想想吧。当月天数已知,上月残余天数,我们可以用当月1号是周几来推断出来,下月残余天数,正好用42 - 当月天数 -上月残余。// 上月 年、月 preMonth(year, month) { if (month == 1) { return { year: –year, month: 12 } } else { return { year: year, month: –month } } },// 获取当月中,上月多余数据,返回数组 getPreArr(){ let preMonthDateLen = this.getFirstDateWeek(this.data.currentYear, this.data.currentMonth) // 当月1号是周几 == 上月残余天数) let preMonthDateArr = [] // 定义空数组 if (preMonthDateLen > 0) { let { year, month } = this.preMonth(this.data.currentYear, this.data.currentMonth) // 获取上月 年、月 let date = this.getDateLen(year, month) // 获取上月天数 for (let i = 0; i < preMonthDateLen; i++) { preMonthDateArr.unshift({ // 尾部追加 month: ‘pre’, // 只是为了增加标识,区分当、下月 date: date }) date– } } this.setData({ preMonthDateLen }) return preMonthDateArr },// 下月 年、月 nextMonth(year, month) { if (month == 12) { return { year: ++year, month: 1 } } else { return { year: year, month: ++month } } },// 获取当月中,下月多余数据,返回数组 getNextArr() { let nextMonthDateLen = 42 - this.data.preMonthDateLen - this.data.currentMonthDateLen // 下月多余天数 let nextMonthDateArr = [] // 定义空数组 if (nextMonthDateLen > 0) { for (let i = 1; i <= nextMonthDateLen; i++) { nextMonthDateArr.push({ month: ’next’,// 只是为了增加标识,区分当、上月 date: i }) } } return nextMonthDateArr },整合三组数据,就得到了完整的当月数据,格式如下[ {month: “pre”, date: 30}, {month: “pre”, date: 31}, {month: “current”, date: 1}, {month: “current”, date: 2}, … {month: “current”, date: 31}, {month: “next”, date: 1}, {month: “next”, date: 2}]至于上下月切换,选择某年某月等功能,无非就是参数变化而已,自己琢磨琢磨即可。骨架都有了,你想创造什么样的功能还不是手到擒来。完整代码 GitHub ...

September 3, 2018 · 2 min · jiezi