首页
关于
Search
1
唤端-LaunchApp
211 阅读
2
前端风格指南
172 阅读
3
前端CI/CD简述
141 阅读
4
H5兼容性问题(持续更新...)
130 阅读
5
IOS风格指南
127 阅读
默认分类
前端
javascript
CSS
基础
移动APP
后端
flutter
登录
Search
yu.zhai
累计撰写
6
篇文章
累计收到
1
条评论
首页
栏目
默认分类
前端
javascript
CSS
基础
移动APP
后端
flutter
页面
关于
搜索到
6
篇与
的结果
2022-12-04
H5兼容性问题(持续更新...)
1、ios端兼容input光标高度问题详情描述:input输入框光标,在安卓手机上显示没有问题,但是在苹果手机上当点击输入的时候,光标的高度和父盒子的高度一样。例如下图,左图是正常所期待的输入框光标,右边是ios的input光标。出现原因分析:通常我们习惯用height属性设置行间的高度和line-height属性设置行间的距离(行高),当点击输入的时候,光标的高度就自动和父盒子的高度一样了。(谷歌浏览器的设计原则,还有一种可能就是当没有内容的时候光标的高度等于input的line-height的值,当有内容时,光标从input的顶端到文字的底部解决办法:高度height和行高line-height内容用padding撑开例如: .content{ float: left; box-sizing: border-box; height: 88px; width: calc(100% - 240px); .content-input{ display: block; box-sizing: border-box; width: 100%; color: #333333; font-size: 28px; //line-height: 88px; padding-top: 20px; padding-bottom: 20px; } } 2、ios端微信h5页面上下滑动时卡顿、页面缺失问题详情描述:在ios端,上下滑动页面时,如果页面高度超出了一屏,就会出现明显的卡顿,页面有部分内容显示不全的情况,例如下图,右图是正常页面,边是ios上下滑动后,卡顿导致如左图下面部分丢失。图片描述出现原因分析:笼统说微信浏览器的内核,Android上面是使用自带的WebKit内核,iOS里面由于苹果的原因,使用了自带的Safari内核,Safari对于overflow-scrolling用了原生控件来实现。对于有-webkit-overflow-scrolling的网页,会创建一个UIScrollView,提供子layer给渲染模块使用。【有待考证】解决办法:只需要在公共样式加入下面这行代码*{-webkit-overflow-scrolling: touch;}But,这个属性是有bug的,比如如果你的页面中有设置了绝对定位的节点,那么该节点的显示会错乱,当然还有会有其他的一些bug。拓展知识:-webkit-overflow-scrolling:touch是什么?MDN上是这样定义的:-webkit-overflow-scrolling 属性控制元素在移动设备上是否使用滚动回弹效果. auto: 使用普通滚动, 当手指从触摸屏上移开,滚动会立即停止。touch: 使用具有回弹效果的滚动,当手指从触摸屏上移开,内容会继续保持一段时间的滚动效果。继续滚动的速度和持续的时间和滚动手势的强烈程度成正比。同时也会创建一个新的堆栈上下文。3、ios键盘唤起,键盘收起以后页面不归位问题详情描述:输入内容,软键盘弹出,页面内容整体上移,但是键盘收起,页面内容不下滑出现原因分析:固定定位的元素 在元素内 input 框聚焦的时候 弹出的软键盘占位 失去焦点的时候软键盘消失 但是还是占位的 导致input框不能再次输入 在失去焦点的时候给一个事件解决办法:<div class="list-warp"> <div class="title"><span>投·被保险人姓名</span></div> <div class="content"> <input class="content-input" placeholder="请输入姓名" v-model="peopleList.name" @focus="changefocus()" @blur.prevent="changeBlur()"/> </div> </div> changeBlur(){ let u = navigator.userAgent, app = navigator.appVersion; let isIOS = !!u.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); if(isIOS){ setTimeout(() => { const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop || 0 window.scrollTo(0, Math.max(scrollHeight - 1, 0)) }, 200) } } 拓展知识:position: fixed的元素在ios里,收起键盘的时候会被顶上去,特别是第三方键盘4、安卓弹出的键盘遮盖文本框问题详情描述:安卓微信H5弹出软键盘后挡住input输入框,如下左图是期待唤起键盘的时候样子,右边是实际唤起键盘的样子图片描述出现原因分析:待补充解决办法:给input和textarea标签添加focus事件,如下,先判断是不是安卓手机下的操作,当然,可以不用判断机型,Document 对象属性和方法,setTimeout延时0.5秒,因为调用安卓键盘有一点迟钝,导致如果不延时处理的话,滚动就失效了changefocus(){ let u = navigator.userAgent, app = navigator.appVersion; let isAndroid = u.indexOf('Android') > -1 || u.indexOf('Linux') > -1; if(isAndroid){ setTimeout(function() { document.activeElement.scrollIntoViewIfNeeded(); document.activeElement.scrollIntoView(); }, 500); } }, 拓展知识:Element.scrollIntoView()方法让当前的元素滚动到浏览器窗口的可视区域内。而Element.scrollIntoViewIfNeeded()方法也是用来将不在浏览器窗口的可见区域内的元素滚动到浏览器窗口的可见区域。但如果该元素已经在浏览器窗口的可见区域内,则不会发生滚动5、Vue中路由使用hash模式,开发微信H5页面分享时在安卓上设置分享成功,但是ios的分享异常问题详情描述:ios当前页面分享给好友,点击进来是正常,如果二次分享,则跳转到首页;使用vue router跳转到第二个页面后在分享时,分享设置失败;以上安卓分享都是正常图片描述出现原因分析:jssdk是后端进行签署,前端校验,但是有时跨域,ios是分享以后会自动带上 from=singlemessage&isappinstalled=0 以及其他参数,分享朋友圈参数还不一样,貌似系统不一样参数也不一样,但是每次获取url并不能获取后面这些参数解决办法:(1)可以使用改页面this.$router.push跳转,为window.location.href去跳转,而不使用路由跳转,这样可以使地址栏的地址与当前页的地址一样,可以分享成功(适合分享的页面不多的情况下,作为一个单单页运用,这样刷新页面跳转,还是..)(2)把入口地址保存在本地,等需要获取签名的时候 取出来,注意:sessionStorage.setItem(‘href’,href); 只在刚进入单应用的时候保存!【该方法未验证】
2022年12月04日
130 阅读
0 评论
0 点赞
2022-12-04
浅谈代码设计原则
设计原则25个问题,你会几个?如何理解单一职责原则?如何判断职责是否足够单一?职责是否设计得越单一越好?什么是开闭原则?修改代码就一定意味着违反开闭原则吗?怎样的代码改动才被定义为扩展或者说是修改?如何做到对扩展开放、修改关闭?如何在项目中灵活运用开闭原则?什么是依赖反转(倒置)原则 ?高层模块和低层模块是啥意识?如何理解反转两个字?什么依赖被反转了?什么是控制反转 IOC ( Inversion Of Control )?什么是依赖注入 DI ( Dependency Injection )?IOC 和 DI 有什么区别?代码行数越少就越简单吗?代码逻辑复杂就违背 KISS 原则吗?如何写出满足 KISS 原则的代码?如何判断是否满足 KISS 原则?重复的代码就一定违背 DRY 吗?如何提高代码的复用性?什么是迪米特法则?高内聚、松耦合是什么意识?如何理解高内聚和松耦合?如何用好迪米特法则?越下游,越自由大家有没有这种感觉,没有没关系,我举几个例子,大家就明白了。例子如下所示:产品经理写产品策划文档,给开发测试看,起码要有点人样交互设计写交互设计文档,给开发产品看,起码要有点人样ui 设计师产出设计稿,给开发产品看,起码要有点人样看完上面例子,再说我们:当骄傲的前端工程师写完代码,然后 two days later 。惊喜的发现,自己写的代码已经不认识了,这就非常尴尬了。然心中窃喜,毕竟我们处于最下游,不存在把代码给谁阅读之说,最多也就是走下 code review 。这种感觉是非常危险的,当我们处于非常下游的地方,也意味着我们非常自由,它有很多负面影响。所以,我们通过什么来约束这种自由呢?这个答案就是本文想详细阐述的:通过设计原则来约束这种过火的自由。设计原则有哪些呢大家请看下图:18.png图中设计原则一栏,涵盖了所有重要的设计原则,如 SOLID 、 KISS 、 YANGI 、 DRY、LOD 。活不多说,下面大家跟着我,一步步掌握设计原则吧!用好设计原则的目的上面说了为什么要学习设计原则,那大家再想一下,我们用好设计原则的目的是什么?目的如下:让代码或者项目具备:可读性可扩展性复用性可维护性总结一句话就是:降低软件开发的复杂度,让迭代的难度保持在合理区间内。OK , 说完目的,我们开始逐一介绍这些设计原则,小伙伴们请往下阅读。注意:下文所说的类,也代指模块,这样我就不再单独写一遍模块了。SOLID这是第一个介绍,也是最重要的。重要的事说三遍:请记住:设计原则中, SOLID 是重点, 而 SO 是重点中的重点。S名称SRP: Single Responsibility Principle中文:单一职责原则QUESTION如何理解单一职责原则?一个类只负责完成一个职责或者功能,不要设计大而全的类,要设计粒度小、功能单一的类,简单点说,就是要小而美。单一职责原则的目的是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。如何判断职责是否足够单一?这里有 5 个技巧:类中的私有方法过多比较难给类起一个合适的名字类中的代码行数、函数或者属性过多类中大量的方法都是集中操作类中的某几个属性类依赖的其他类过多,或者依赖类的其他类过多职责是否设计得越单一越好?单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。同时,类的职责单一,其依赖的和被依赖的其他类也会变少,从而实现代码的高内聚、松耦合。注意: 如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。生活中例子社会分工: 写代码的,不会同时去写策划文档。code 展示例子1:符合单一职责useFuncA() useFuncB()可以把上面的函数看成是 hooks , 一个函数( hooks )完成一个功能。例子2:不同业务层面,可以符合单一职责,也可以不符合单一职责const userInfo = { userId: '', username: '', email: '', telephone: '', createTime: '', lastLoginTime: '', avatarUrl: '', provinceOfAddress: '', cityOfAddress: '', regionOfAddress: '', detailedAddress: '' }从用户业务层面看,满足单一职责原则。从用户展示信息、地址信息、登录认证信息这些更细粒度的业务层面来看,就不满足单一职责原则。例子3:符合单一职责1.png从上图可以看出,一个功能只由一个模块目录完成。例子4:不符合单一职责function bindEvent(elem, type, selector, fn) { if (fn == null) { fn = selector selector = null } } bindEvent(elem, 'click', '#div', fn) bindEvent(elem, 'click', fn)我们发现,bindEvent 函数可以传很多参数,不符合单一职责原则,它是外观模式思想的体现。PS: 外观模式如下图所示:SRP 总结上面的问题回答,进行了总结,这里再补充句:SRP 要结合业务场景去看待,角度不同,结果不同。OOCP: Open Closed Principle中文:开闭原则QUESTION什么是开闭原则?软件实体(类、模块、函数)都应当对扩展具有开放性,但对于修改具有封闭性。也就是说:添加一个新功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码。修改代码就一定意味着违反开闭原则吗?不一定,这个我们要灵活看待:第一:开闭原则并不是说完全杜绝修改,而是以最小修改代码的代价来完成新功能的开发第二:同样的代码改动,在粗代码粒度下,可能被认定为修改,在细代码粒度下,可能又被认定为扩展第三:尽量让最核心、最复杂的那部分逻辑代码满足开闭原则怎样的代码改动才被定义为扩展或者说是修改?通常情况下,只要它没有破坏原有代码的正常运行,没有破坏原有的单元测试,我们就可以认为它是符合开闭原则的。如果破坏了,那我们就可以认为它不符合开闭原则。如何做到对扩展开放、修改关闭?保持函数、类和模块当前本身的状态,或是近似于他们一般情况下的状态(即不可修改性)使用组合的方式(避免使用继承方式)来扩展现有的类、函数或模块,以使它们可能以不同的名称来暴露新的特性或功能如何在项目中灵活运用开闭原则?时刻具备扩展意识、抽象意识、封装意识。生活中例子高考试卷: 比如明天就要高考了,但是老师发现没法区分高分学生和低分学生,必须得在试卷里面增加两个难度比较大的题,但是明天就高考了,如果现在去修改高考中的试卷,显然是不合理的。经过思考,最好的办法就是给高考的试卷加一个附加题【你可以加附加题,但是你不能修改原来的卷子,这就是对扩展开放,对修改关闭】。code例子1:中间件app.use(A).use(B).use(C)例子2:function optionalfn(f1(),f2(),f3())例子3:插件Vue.use(PluginA) Vue.use(PluginB)例子4:装饰器@get('/hello') async hello() { // ... }总结有以下两点:开闭原则是最重要的设计原则,很多设计模式都是以开闭原则为指导原则的它的核心是为了提高代码的扩展性LLSP: Liskov Substitution Principle中文:里氏替换原则额,这缩写怎么有点搞笑,嗯?image.pngQUESTION什么是里氏替换原则?子类在设计的时候,要遵守父类的行为约定(或者叫协议)。父类定义了函数的行为约定,子类可以改变函数的内部实现逻辑,但不能改变函数原有的行为约定。这里的行为约定包括:函数声明要实现的功能、对输入、输出、异常的约定、甚至包括注释中所罗列的任何特殊说明。如何判断是否满足里氏替换原则?拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类的设计实现没有完全遵守父类的约定,子类有可能违背了里氏替换原则。生活中例子盗版光盘: 原来人家的光盘是正版的,但现在你弄了一个盗版的光盘,我们有两张光盘,放到 DVD 里面,都可以单独运行【盗版光盘把正版光盘全部 copy 过来,子类父类行为预期一致】code例子1:代码执行重复,不符合 LSP 原则class People { constructor(name, age) { this.name = name this.age = age } eat() { // ... } } class Student extends People { constructor(name, age) { super(name, age) } eat() { // ... } } const kuangXiangLu = new Student('KuangXiangLu', 10) kuangXiangLu.eat()为什么不符合 LSP 呢?有以下两个原因:第一:Student 类继承了 People 类,同时修改了 People 类的 eat 方法,这时就违背了 LSP 原则第二:没有遵循父类的设计,修改了输出总结里氏替换原则的核心是用来指导,继承关系中子类该如何设计的一个原则,也就是 design by contract (按照协议来设计)。IISP: Interface Segregation Principle中文:接口隔离原则QUESTION什么是接口隔离原则 ?接口的调用者或者使用者,不应该强迫依赖它不需要的接口。接口隔离原则中的接口是指什么?接口可以理解为下列三种东西:一组 API 接口集合、单个 API 接口或函数、OOP 中的接口概念生活中例子汽车 USB 插口: 汽车上有很多插口,但是你想插 usb 接口,你想让它有 usb 功能,又想让它有三线插头的功能,这就是不科学的事情【每一个接口都应该有自己的一种角色,只负责自己的角色】。code代码1:const obj = { login() { // 用户登录 }, delete() { // 删除用户信息 } }delete 是不常用且危险的操作,如果和 login 放在一起,就存在被不需要调 delete 的业务误调的可能,违背了 ISP 原则。代码2:function main() { // 处理加法 // 处理减法 // 处理乘法 // 处理... }一个函数里面处理了很多逻辑,也违背了 ISP 原则。总结兄弟们, 细细品,重在隔离。DDIP: Dependency Inversion Principle中文:依赖反转(倒置)原则QUESTION什么是依赖反转(倒置)原则 ?高层模块( high-level modules )不要依赖低层模块( low-level )。高层模块和低层模块应该通过抽象( abstractions )来互相依赖。除此之外,抽象( abstractions )不要依赖具体实现细节( details ),具体实现细节 ( details )依赖抽象( abstractions )。高层模块和低层模块是啥意识?在调用链上,调用者属于高层,被调用者属于低层。如何理解反转两个字?反转指的是在没有使用框架之前,程序员自己控制整个程序的执行。在使用框架之后,整个程序的执行流程可以通过框架来控制。流程的控制权从程序员反转到了框架。什么依赖被反转了?高层模块被反转了。什么是控制反转 IOC (Inversion Of Control) ?控制反转,控制是指对程序执行流程的控制,在没有反转前,控制权在程序员手里,经过反转后,控制权到了框架手里。控制反转并不是一种具体的实现(编码)技巧,而是一个比较笼统的设计思想,一般用来指导框架层面的设计。什么是依赖注入 DI (Dependency Injection) ?不通过 new() 的方式在类内部创建 依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递 (或注入)给类使用。依赖注入是一种具体的(实现)编码技巧。IOC 和 DI 有什么区别?IOC 是设计思想, DI 是具体(实现)编码技巧。生活中例子三个和尚打水: 正常操作是直接用桶从井里面打水,但是现在非要加一个环节,先用桶把井里的水打到大桶里,然后再从大桶里面打水【不需要中间操作环节,直接用底层操作】。CPU 内存: 硬盘都是针对接口设计的,如果针对实现来设计,内存就要对应到具体的某个品牌的主板,明显不合理。code代码和解读如下图所示:总结DIP 是一个抽象难懂的设计原则,从 IOC 和 DI 的中文命名就可以看出来。大家在运用 DIP 的时候,要理解透彻 反转 一词。记住将流程交给框架控制,然后再实现它。SOLID 总结下表是对 SOLID 在不同维度的比较,大家可以看看,然后结合上面阐述的内容,细细品味下。原则耦合度内聚度扩展性冗余度维护性测试性适应性一致性SRP-+oo++ooOCPoo+-+o+oLSP-ooo+oo+ISP-+o-oo+oDIP-oo-o++o+代表增加,-代表降低,o代表持平。KISS英文:Keep It Simple and Stupid中文:保持简单愚蠢俗解:保持代码简单QUESTION代码行数越少就越简单吗?不一定,如一些较长的正则表达式,三位运算符,这些都是违背了 KISS 原则。代码逻辑复杂就违背 KISS 原则吗?不一定,如果是复杂的问题,用复杂的方法解决,并不违反 KISS 原则。如何写出满足 KISS 原则的代码?不要使用同事可能不懂的技术来实现代码不要重复造轮子,要善于使用已经有的工具类库不要过度优化如何判断是否满足 KISS 原则?KISS 是一个主观的评判,可以通过 code review 来做,如果大多数同事对你的代码有很多疑问,基本就说明不够 KISS 。code如下代码所示:不符合 KISS 原则let a = b ? c : d ? e : f总结关注如何做我们在做开发的时候,一定不要过度设计,不要觉得简单的东西就没有技术含量。实际上,越是能用简单的方法解决复杂的问题,越能体现一个人的能力。YANGI英文:You Ain’t Gonna Need It中文:你不会需要它俗解:不要做过度设计生活中例子双11剁手: 卧槽,好便宜啊,下单下单下单,然后... 自行想象。总结永远不要因为:预计你会用到某个功能就去写一段代码去实现而是:真的需要这个功能时才去实现它DRY英文:Don’t Repeat Yourself中文:不要重复你自己俗解:不要写重复的代码QUESTION重复的代码就一定违背 DRY 吗?重复的代码不一定违背 DRY 原则,代码重复有三种典型情况,分别是:实现逻辑重复功能语义重复代码执行重复如何提高代码的复用性?减少代码耦合、满足单一职责原则、模块化、业务与非业务逻辑分离、通用代码抽离、抽象和封装、使用设计模式。code例子1:实现逻辑重复代码 1 :function isValidUserName() { // 内容一样 } function isValidPassword() { // 内容一样 } function main() { isValidUserName() isValidPassword() }代码2:function isValidUserNameOrPassword() { // 内容一样 } function main() { isValidUserNameOrPassword() }大家看, 代码 1 中的两个函数代码都是一样的,所以我们通过去除重复,变成一个函数,变成了代码 2 。这里问大家一个问题,你们觉得这样做是否违背了 DRY 呢?结果:代码 1 不违背 DRY ,代码 2 违背了 DRY 的初衷。原因:虽然实现逻辑重复,但是语义不重复。从功能上看,他们是做的是两件完全不同的事情。合并后,一个函数做了两件事情,违反了 SRP 和 ISP 。改善:抽象出更细粒度函数例子 2 :功能语义重复function sayHello() { // } function speakHello() { // }都是表达 hello 的意识,虽然代码没重复,但是语义重复了,违背 DRY 。例子 3 :代码执行重复function isLogin() { // } function main() { if (xxx) { isLogin() } // 代码省略... if (yyy) { isLogin() } } 在一个函数中,多次执行同一个函数,违背了 DRY 。总结不重复并不代表可复用,要辩证思考和灵活应用。LOD英文:Law of Demeter 中文:迪米特法则 俗解:高内聚、松耦合QUESTION什么是迪米特法则?不该有直接依赖关系的类之间,不要有依赖。有依赖关系的类之间,尽量只依赖必要的接口高内聚、松耦合是什么意识?高内聚是指:相近的功能应该放到同一个类中,不相近的功能不要放到同一类中。因为相近的功能往往会被同时修改,放到同一个类中,修改会比较集中。松耦合是指:在代码中,类与类之间的依赖关系简单清晰。即使两个类有依赖关系,一个类的代码改动也不会或者很少导致依赖类的代码改动。如何理解高内聚和松耦合?结合下图来理解:它是一个通用的设计思想,可以用来指导不同粒度代码的设计与开发,比如系统、模块、类,甚至是函数,也可以应用到不同的开发场景中,比如微服务、框架、组件、类库等。如何用好迪米特法则?减少代码耦合、满足单一职责原则、模块化。生活中例子现实中的对象:你对你的对象肯定了解的很多,但是你要是对别人的对象也了解很多,那就出大事了【一个对象应该对其他对象有尽可能少的了解】。image.pngcodeimage.png如上图所示:我们用 lerna 去开发一个框架,将框架的不同功能放到不同的 package 中进行维护迭代,符合 LOD 。总结高内聚、松耦合是一个非常重要的设计思想,它能够有效地提高代码的可读性和可维护性,缩小功能改动导致的代码改动范围。设计原则总结好啦,看到这,设计原则基本就阐述完了,我对主要的设计原则进行了阐述,大家读会发现,一些设计原则虽然起名不同,但是其目标都是类似和相同的。学习和掌握主要的设计原则,可以帮助我们更好的进行软件设计、开发和迭代。也是为我们学习和掌握设计模式打下坚实的基础。转自: 杨不败https://juejin.cn/post/6948235657978314783- EOF -推荐阅读 点击标题可跳转1、JavaScript 的 API 设计原则2、纯前端实现 App Store 卡片展开效果3、设计灵感:12组优秀的App引导页设计
2022年12月04日
119 阅读
0 评论
0 点赞
2022-12-04
前端CI/CD简述
前端CI/CD定义持续集成(CI)是一种软件开发实践,它基于将代码频繁集成到共享代码仓中。 然后通过自动构建(automated build)验证每个签入(Check-In)。持续集成(CI)的主要目标是更早,更容易地识别开发过程中可能出现的问题。 如果定期集成 - 在查找错误时检查的次数要少得多。 这样可以减少调试时间,增加功能时间。 还有一个选项可以设置代码样式的检查,代码复杂度(低复杂性使测试过程更简单)和其他检查。 这有助于最大限度地减少负责代码审查的人员的工作量,节省时间并提高代码质量工作流程开发人员在其本地计算机上检查代码完成后 - 他们将代码变更提交到代码仓代码仓向CI系统发送请求(webhook)CI服务器运行任务(测试,覆盖率,检查语法等)CI服务器发布已保存的工件(artifacts)以进行测试如果构建或测试失败,CI服务器会向团队发出警报该团队解决了这个问题方案比对circle CIFeaturesCircleCI是一个基于云的系统 - 不需要专用服务器,您无需管理它。 但是,它还提供了一个本地解决方案,允许您在私有云或数据中心中运行它。即使是商业帐户,它也有免费计划Rest API - 您可以访问项目,构建和工件(artifacts)。构建的结果将是工件或工件组。 工件可以是已编译的应用程序或可执行文件(例如,android APK)或元数据(例如,关于测试`成功的信息)CircleCI 缓存必要的安装(requirements installation)。 它会检查第三方依赖项,而不是持续安装所需的环境您可以触发SSH模式访问容器并进行自己的调查(如果出现任何问题)这是一个完整的开箱即用解决方案,需要最少的配置\调整支持语言Python,Node.js,Ruby,Java,Go等Ubuntu(12.04,14.04),Mac OS X(付费账户)Github,BitbucketAWS,Azure,Heroku,Docker,专用服务器Jira,HipChat,Slack优缺点优快速启动CircleCI有一个免费的企业项目计划这很容易,也很快开始轻量级,易读的YAML配置您不需要任何专用服务器来运行CircleCI缺CircleCI仅支持2个版本的Ubuntu免费(12.04和14.04)和MacOS作为付费部分尽管CircleCI可以使用并运行所有语言,但tt仅支持“开箱即用”的以下编程语言:Go(Golang),Haskell,Java,PHP,Python,Ruby / Rails,Scala如果您想进行自定义,可能会出现一些问题:您可能需要一些第三方软件来进行这些调整此外,虽然作为基于云的系统是一方的优势,它也可以停止支持任何软件,你将无法阻止Travi Clfeatures:...Circle CI的功能可选择在Linux和Mac OS X上同时运行测试开箱即用支持更多语言:Android,C,C#,C ++,Clojure,Crystal,D,Dart,Erlang,Elixir,F#,Go,Groovy,Haskell,Haxe,Java,JavaScript(带Node.js),Julia,Objective-C,Perl,Perl6, PHP,Python,R,Ruby,Rust,Scala,Smalltalk,Visual Basic支持构建矩阵构建矩阵是一种工具,可以使用不同版本的语言和包运行测试。 您可以以不同的方式自定义它。 例如,某些环境的失败可以触发通知但不会使所有构建失败(这对包的开发版本有帮助)TOXTox是一种通用的virtualenv管理和测试命令行工具。 您可以使用pip install tox或easy_install tox命令安装它。支持语言Android,C,C#,C ++,Clojure,Crystal,D,Dart,Erlang,Elixir,F#,Go,Groovy,Haskell,Haxe,Java,JavaScript(带Node.js),Julia,Objective-C,Perl,Perl6, PHP,Python,R,Ruby,Rust,Scala,Smalltalk,Visual Basic优缺点优开箱即用构建矩阵快速启动轻量级YAML配置开源项目的免费计划无需专用服务器缺与CircleCI相比,价格更高,没有免费的企业计划定制(对于某些你需要第三方的东西)Jenkinsfeature:Jenkins是一个独立的基于Java的程序,随时可以运行,包含Windows,Mac OS X和其他类Unix操作系统的软件包凭借更新中心中的数百个插件,Jenkins几乎集成了持续集成和持续交付工具链中的所有工具Jenkins可以通过其插件架构进行扩展,为Jenkins提供了几乎无限的可能性各种工作模式:自由式项目(Freestyle project),管道(Pipeline),外部作业(External Job),多配置项目,文件夹,GitHub组织,多分支管道Jenkins管道。这是一套插件,支持在Jenkins中实现和集成连续交付管道。 Pipeline提供了一组可扩展的工具,用于通过Pipeline DSL“作为代码”对简单到复杂的交付管道进行建模允许您启动具有各种条件的构建。您可以使用Libvirt,Kubernetes,Docker等运行Jenkins。Rest API - 可以访问控制您获取的数据量,获取/更新config.xml,删除作业,检索所有构建,获取/更新作业说明,执行构建,禁用/启用作业支持语言ALL优缺点优价格(免费)定制插件系统完全控制系统缺需要专用服务器(或多个服务器)。这导致额外的费用。对于服务器本身,DevOps等...配置/定制所需的时间总结CircleCI建议用于小型项目,其主要目标是尽快开始集成。从事开源项目时,建议使用Travis CI,这些项目应在不同环境中进行测试。Jenkins被推荐用于大型项目,在这些项目中,您需要进行大量自定义,这些自定义可以通过使用各种插件来完成。 您可以在这里更改几乎所有内容,但此过程可能需要一段时间。如果您计划使用CI系统最快的开始,Jenkins可能不是您的选择。
2022年12月04日
141 阅读
0 评论
0 点赞
IOS风格指南
iOS Coding Style Guide命名规范我们尽可能遵守 Apple 的命名约定, 其推荐使用长的,描述性强的方法和变量名,使其阅读起来更加清晰易懂。不能随意使用缩写,导致其他人员阅读代码困难。Coding Guidelines for Cocoa前缀项目名称、类名、文件名都应该保持一致的前缀名,NAP iOS项目使用YN作为前缀。类名大驼峰式命名:每个单词的首字母都采用大写字母。@interface YNHomeViewController : YNBaseViewController @endproperty变量小驼峰式命名:第一个单词以小写字母开始,后面的单词的首字母全部大写。属性的关键字推荐按照 原子性,内存管理,读写的顺序排列。Block、NSString属性应该使用copy关键字。代理使用weak关键字防止循环引用。@property (nonatomic, strong, readonly) UILabel *subjectLabel; @property (nonatomic, copy) NSString *statusName; @property (nonatomic, weak) id <YNLogisticsProductsCellDelegate>delegate; @property (nonatomic, copy) void(^actionBlock)(__nullable id);枚举Enum中枚举内容的命名需要以该Enum类型名称开头。NS_ENUM定义通用枚举,NS_OPTIONS定义位移枚举。typedef NS_OPTIONS(NSUInteger, YNLogLevel){ YNLogLevelError = 1 << 0, YNLogLevelWarning = 1 << 1, YNLogLevelInfo = 1 << 2, YNLogLevelDebug = 1 << 3, YNLogLevelVerbose = 1 << 4 }; typedef NS_ENUM(NSUInteger, YNContentAlignment) { YNContentAlignmentLeft = 0, YNContentAlignmentCenter = 1, YNContentAlignmentRight = 2 };宏和常量对于宏定义的常量define 预处理定义的常量全部大写,单词间用 _ 分隔,宏定义中如果包含表达式或变量,表达式或变量必须用小括号括起来。对于类型常量对于局限于某编译单元(实现文件)的常量,以字符k开头,例如kAnimationDuration,且需要以static const修饰。对于定义于类头文件的常量,外部可见,则以定义该常量所在类的类名开头,例如EOCViewClassAnimationDuration, 仿照苹果风格,在头文件中进行extern声明,在实现文件中定义其值。//宏定义的常量 #define ANIMATION_DURATION 0.3 #define MY_MIN(A, B) ((A)>(B)?(B):(A)) //局部类型常量 static const NSTimeInterval kAnimationDuration = 0.3; //外部可见类型常量 //YNMagazinePresenter.h extern NSString * const YNMagazineChannelTypeFemaleNewest; //YNMagazinePresenter.m NSString * const YNMagazineChannelTypeFemaleNewest = @"1";方法方法名用小驼峰式命名。方法名不要使用new作为前缀。不要使用and来连接属性参数,如果方法描述两种独立的行为,使用and来串接它们。方法实现时,如果参数过长,则令每个参数占用一行,以冒号对齐。一般方法不使用前缀命名,私有方法可以使用统一的前缀来分组和辨识。方法名要与对应的参数名保持高度一致。//不要使用 and 来连接属性参数 - (int)runModalForDirectory:(NSString *)path file:(NSString *)name types:(NSArray *)fileTypes; //推荐 - (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes; //反对 //表示对象行为的方法、执行性的方法 - (void)insertModel:(id)model atIndex:(NSUInteger)atIndex; - (void)selectTabViewItem:(NSTableViewItem *)tableViewItem //返回性的方法 - (instancetype)arrayWithArray:(NSArray *)array; //参数过长的情况 - (void)longMethodWith:(NSString *)theFoo rect:(CGRect)theRect interval:(CGFloat)theInterval; //不要加get - (NSSize)cellSize; //推荐 - (NSSize)getCellSize; //反对 方法参数不要在参数名中使用 pointer 或 ptr,让参数的类型来说明它是指针避免使用 one, two,...,作为参数名,更不要用1,2,...避免为节省几个字符而缩写采用以下参数标签与参数组合的惯例...action:(SEL)aSelector ...alignment:(int)mode ...atIndex:(int)index ...content:(NSRect)aRect ...doubleValue:(double)aDouble ...floatValue:(float)aFloat ...font:(NSFont *)fontObj ...frame:(NSRect)frameRect ...intValue:(int)anInt ...keyEquivalent:(NSString *0charCode ...length:(int)numBytes ...point:(NSPoint)aPoint ...stringValue:(NSString *)aString ...tag:(int)anInt ...target:(id)anObject ...title:(NSString *)aString代码注释规范优秀的代码大部分是可以自描述的,我们完全可以用代码本身来表达它到底在干什么,而不需要注释的辅助。但并不是说一定不能写注释,有以下三种情况比较适合写注释:公共接口(注释要告诉阅读代码的人,当前类能实现什么功能)。涉及到比较深层专业知识的代码(注释要体现出实现原理和思想)。容易产生歧义的代码(但是严格来说,容易让人产生歧义的代码是不允许存在的)。除了上述这三种情况,如果别人只能依靠注释才能读懂你的代码的时候,就要反思代码出现了什么问题。属性注释写在属性上方,三斜线开头斜线后面跟一个空格后面跟注释内容;可使用Xcode自带模板快捷添加(默认快捷键:⌥+⌘+/)。/// 文章ID,用于下面的更多精彩内容去重 @property (nonatomic, copy) NSArray <NSString *> *articleIds;方法声明注释公开接口,重要的方法,分类,以及协议,都应该伴随文档(注释):三斜线开头,斜线后跟一个空格;第一行为方法描述,第二行开始为方法参数以@param标记(默认快捷键:⌥+⌘+/)。/// 快速生成渐变图层 /// @param frame 图层位置大小 /// @param beginColor 起始颜色 /// @param endColor 结束颜色 + (CAGradientLayer *)gradientLayerWithFrame:(CGRect )frame beginColor:(UIColor *)beginColor endColor:(UIColor *) endColor;TODO使用// TODO: 说明 标记一些未完成的或完成的需要优化的地方。// TODO: restore scrollView state // This is temporary solution. Have to implement the save and restore scrollView state UIScrollView *superscrollView = strongLastScrollView;FIXME使用// FIXME: 说明 标记一些有bug的或以临时方案解决的地方。(使用// FIXME: 标记在检索器会出现提醒图)// FIXME: 等待接口字段 // FIXME: someday check the return codes on these binds.代码格式规范指针*位置跟在类型后面用空格隔开紧跟着变量名。NSURL *url; - (id)initWithChildren:(NSArray *)children;方法声明和实现+、-后面跟一个空格,方法名和第一个参数之间不留空格;方法体的第一个大括号跟在方法名末尾用空格隔开。// 方法声明 - (id)initWithChildren:(NSArray *)children; + (instancetype)actionWithTitle:(NSString *)title style:(UIPreviewActionStyle)style handler:(void (^)(UIPreviewAction *action, UIViewController *previewViewController))handler; // 方法实现 - (id)initWithChildren:(NSArray *)children { self = [super initWithChildren:children]; return self; } + (instancetype)actionWithTitle:(NSString *)title style:(UIPreviewActionStyle)style handler:(void (^)(UIPreviewAction *action, UIViewController *previewViewController))handler { UIPreviewAction *action = [[UIPreviewAction alloc] init]; action.title = title; ... return action; }对implementation方法进行分组#pragma mark - Life cycle - (void)viewDidLoad { [super viewDidLoad]; } #pragma mark - Private methods #pragma mark - Delegates #pragma mark - Event response #pragma mark - Setter #pragma mark - Getter编码规范if语句尽量列出所有分支(穷举所有的情况),而且每个分支都须给出明确的结果。不要使用过多的分支,要善于使用return来提前返回错误的情况,把最正确的情况放到最后返回。条件过多,过长的时候应该换行;条件表达式如果很长,则需要将他们提取出来赋给一个BOOL值,或者抽取出一个方法。NSString *hintStr; if (count < 3) { hintStr = @"Good"; } else { hintStr = @""; }if (!user.userName) return NO; if (!user.password) return NO; if (!user.email) return NO; return YES;if (condition1 && condition2 && condition3) { // Do something } BOOL finalCondition = condition1 && condition2 && condition3 && condition4 if (finalCondition) { // Do something } if ([self canDelete]){ // Do something } - (BOOL)canDelete { BOOL finalCondition1 = condition1 && condition2 BOOL finalCondition2 = condition3 && condition4 return condition1 && condition2; }for循环避免在for循环内修改循环变量,防止for循环失去控制。如果必须修改,可以创建一个临时变量进行遍历或者使用NSArray的enumerateObjectsUsingBlock:方法进行遍历。NSArray *arr = [NSArray arrayWithArray:mArr]; for (NSMutableDictionary *dic in arr) { if ([dic[@"a"] isEqualToString:@"3"]) { [mArr removeObject:dic]; } } [mArr enumerateObjectsUsingBlock:^( id obj, NSUInteger idx, BOOL *stop) { if ([[obj objectForKey: @"a"] isEqualToString:@"3"]) { *stop = YES ; [mArr removeObject:obj]; } }];方法单一性原则,每个函数的职责都应该划分的很明确(就像类一样)。对于有返回值的方法,每一个分支都必须有返回值。对输入参数的正确性和有效性进行检查。如果在不同的方法内部有相同的功能,应该把相同的功能抽取出来单独作为另一个方法。将方法内部比较复杂的逻辑提取出来作为单独的方法。// 推荐写法 - (void)viewDidLoad { [super vidwDidLoad]; [self setupUI]; [self prepareData]; } - (void)setupUI { } - (void)prepareData { } // 不推荐 - (void)viewDidLoad { [super vidwDidLoad]; [self build]; } - (void)build { // setupUI ... // prepareData ... }// 推荐写法 - (int)function { if(condition1){ return count1 }else if(condition2){ return count2 }else{ return defaultCount } } // 不推荐 - (int)function { if(condition1){ return count1 }else if(condition2){ return count2 } }- (void)functionWithParam:(id)param { if([param isUnavailable]){ return; } //Do some right thing }属性和变量数据成员保持最小公开原则在不需要改变一个属性时,添加readonly最好使用auto-synthesis。当使用属性,实例变量时应该使用self.来访问,这意味着所有的属性将很容易区分,因为它们都使用 self. 开头nil / Nil / NULL / NSNullObjective-C中默认所有的指针指向nil,nil最显著的行为是可以有消息发送给它,结果返回0if (name != nil && [name isEqualToString:@"Steve"]) { ... } // …可以被简化为: if ([name isEqualToString:@"steve"]) { ... }SymbolValueMeaningNULL(void *)0literal null value for C pointersnil(id)0literal null value for Objective-C objectsNil(Class)0literal null value for Objective-C classesNSNull[NSNull null]singleton object used to represent nullBooleansObjective-C用BOOL来编码真值。它是signed char的typedef,并且用宏YES和NO来相应的表示真和假;在Objective-C中,当遇到处理真值的参数,属性和实例变量时,使用类型BOOL。当分配字面值时,使用宏YES和NO;如果BOOL属性的名称表示为一个形容词,该属性可以省略“is”字头,但需要为get方法按照常规指定名称。@property (assign, getter=isEditable) BOOL editable;判断真假时不要与字面值比较// 推荐 if (someObject) { } if (![anotherObject boolValue]) { } // 不推荐 if (someObject == nil) {} if ([anotherObject boolValue] == NO) {} if (isAwesome == YES) {} // Never do this. if (isAwesome == true) {} // Never do this.NameTypedefHeaderTrue ValueFalse ValueBOOLsigned charobjc.hYESNObool_Bool (int)stdbool.htruefalsebooleanunsigned charMacTypes.hTRUEFALSENSNumber__NSCFBooleanFoundation.h@(YES)@(NO)CFBooleanRefstructCoreFoundation.hkCFBooleanTruekCFBooleanFalse团队约定字典类型参数使用YNParameters进行包装,可以避免空数据带来的crash。YNParameters *parameter = [YNParameters new]; [parameter addParameter:@"order_id" value:order_id]; [parameter addParameter:@"current_page" value:current_page]; [parameter addParameter:@"order_type" value:orderType]; [YNSensorsAnalytic trackEvent:YNSensorsAnalytic_OrderPageOrderDelete properties:parameter.parameterDict];数组取值使用YYKit的objectOrNilAtIndex:方法,避免数组越界。__kindof UIViewController *viewController = [self.viewControllers objectOrNilAtIndex:index];字符串文案使用yn_safeString:方法进行转换,避免显示异常。NSString *title = [NSString yn_safeString:model.title];使用枚举定义有限类型而非简单数字判断。typedef NS_ENUM(NSUInteger, YNContentAlignment) { YNContentAlignmentLeft = 0, YNContentAlignmentCenter = 1, YNContentAlignmentRight = 2, }; if (aligment == YNContentAlignmentCenter) { ... } else { ... } switch (aligment) { case YNContentAlignmentCenter:{ ... break; } case YNContentAlignmentRight:{ ... break; } default: { ... break; } } // 不推荐 if (aligment == 2) { ... } else { ... } switch (aligment) { case 1:{ ... break; } case 2:{ ... break; } default: { ... break; } }所有测试代码使用DEBUG宏包起来,不将测试代码上传到远程仓库。#if DEBUG - (void)mock { self.title = @"PDP"; self.brand = @"NET-A-PORTER"; } #endif所有接口地址统一在YNNetInterface进行维护。
2022年12月04日
127 阅读
0 评论
0 点赞
2022-12-04
唤端-LaunchApp
复杂场景下唤起App实践(转转)在上一篇文章《【第2059期】唤起 App 在转转的实践》中,我们介绍了唤端技术的实现原理和一些实践。这里先回顾一下上一篇主要内容:1. 什么是唤端?一张图了解一下什么是唤端。2. 唤端功能唤端功能架构图如下。功能3. 唤端技术唤端技术架构图如下。面临的新问题虽然,当前方案已经支持了基本的唤端能力,可以说是唤端技术在转转的从 0 到 1。然而,随着时间的迁移,支持兼容的业务逻辑越来越多,项目内部结构已混乱不堪、维护艰难,并且满足不了新的业务诉求,因此,决定对其进行一次重大重构。经过业务反馈和调研,主要问题如下:集团内App尚未完全覆盖,平台兼容性尚待完善。未与行业方案对齐,满足不了业务提出的更高要求。周边生态有待完善,业务使用体验有待提高。对以上问题进行了梳理、评估,最终定下了本次重构目标:整体架构升级,支持唤起多App,提升唤起兼容性。对标业界方案,完善现有能力。唤起App周边生态完善,提升业务体验。项目重构整体架构1. 旧的方案与架构首先,看一下原来的目录结构(其实一个好的基础库项目只看目录结构就能大概推测出架构的轮廓)。架构图如下:旧架构这种以运行时平台基础的策略模式+适配器模式的设计架构,随着业务复杂度的提高,主要存在以下弊端:运行平台多变莫测,如果新增运行时环境则必须新增一个 caller 类支持,不符合开闭原则。在进行业务扩展和处理交叉逻辑时,代码量会激增,严重影响基础库代码体积。功能职责划分不清晰不明确,每一个 caller 类,都需要单独进行处理,项目整体维护成本较大并会增加心智负担。在支持有限个运行时平台,唤起单一App时,上述架构设计完全满足需求,上述问题也不会凸显,但是随着业务复杂度和要求越来越高,缺点会越来越突出。2. 业界方案对社区的一些唤端方案和建设进行了一些调研并进行了对比。主要内容如下:项目特点缺点star开源-web-launch-app功能职责划分明确,兼容平台多参数设计较复杂,内部封装不够简练,不支持回调678开源-callapp-lib功能职责划分明确,代码组织友好,兼容平台多只提供浏览器端唤起单一App方案1.8K文章-唤端背后的技术介绍了一些前沿方案缺少细节信息量,不开源-可以看出,社区开源的方案在设计各有差异,但是可以看出都采用了单一职责原则和功能模块化的设计思想。在架构设计上第二个更清晰更简洁,相对较有参考价值,所以我们选择基于此进行二次开发。其实,在 JS 架构设计中这种思想在各大开源框架都在使用,比如社区的 Vue 框架,其源码内部的功能划分、职责分离和模块化做的很完美。因此,这种编程思想(架构思想)很值得去借鉴。3. 升级后的转转方案转转的业务场景相对于业界方案较复杂,需要一行代码支持集团所有App唤端。这种用户极其方便和无感的用法,意味着所有的业务逻辑和配置都需要在基础库内部进行处理,复杂度也随着上升,对架构的设计也有较高的要求。// 转转唤端基础库的使用方法,一行代码,支持集团所有app// path 为内部统跳平台生成的统跳协议地址newCallApp().start({path: ''})结合了自身的业务场景的特点,和业界的优秀方案,最终沉淀出以下架构,如图所示。设计的原则和思想是,以功能职责划分,抽象出各个功能模块,然后再将各个模块有机的组成一个整体。把一个整体(完成人类生存的所有工作)切分成不同的部分(分工),由不同角色来完成这些分工,并通过建立不同部分相互沟通的机制,使得这些部分能够有机的结合为一个整体。重构中的技术思考1. 运行环境判断策略优化在对运行环境进行判断时,需要处理巨大型 if else。优化的思路(手段):合理的数据结构 + 策略模式思想。先来看看优化前的样子。if(isIos) {if(isWechat && isLow7WX) {//...} elseif(isLow9Ios) {// ...} elseif(!supportUniversal && isBaidu) {// ...} elseif(!supportUniversal && (isWeibo || isWechat)) {// ...} elseif(!supportUniversal || isQQ || isQQBrowser || isQzone) {// ...} elseif(isQuark) {// ...} else{// ...}} if(isAndroid) {// 省略...}一般情况下,在 JS 中最普通的策略模式实现代码实现如下:// 定义策略const strategy = {'case1': handler1,'case1': handler1};// 匹配策略并运行if(case) { strategy[case]();}然而,这种用对象 Object 定义的策略模式实现,并不能满足该场景的需求,原因如下:key 不能为引用类型。Object key无序性。提到 有序,key 为引用,脑子里第一个想到的肯定是 Map,采用 Map 是可行的。但是在该场景中也存在以下问题:key 为函数,需要进行多次 set 操作。需要按顺序遍历执行后才能得知 key 是否命中,不是 O(1),发挥不出 Map 的优势。const map = newMap()map.set(fn1, handler1)map.set(fn2, handler2)// ....// map.set(fnN, handlerN)// 业务流程中进行使用(算法),最坏O(n)for(let[key, val] of map) {if(key()) return val()}最后,综合了业务使用场景考虑,采用数组 + 对象的数据结构,既能保证执行顺序,又可提供判断条件为函数形式,并且还能赋予更多属性。// 定义一组有序的运行时环境策略 以单一职责和开闭原则为基准// 默认策略exportlet tempIosPlatRegList = null; exportconst getDefaultIosPlatRegList = (ctx) => [{ name: 'wxSub', platReg: () => (isWechat && isLow7WX), handler: (instance) => { //... }},{ name: 'low9', platReg: () => isLow9Ios, handler: (instance) => { // ... }},{ name: 'bd', platReg: () => !ctx.supportUniversal && isBaidu, handler: (instance) => { // ... }}// ...]// 对外提供 获取方法exportconst getIosPlatRegList = (ctx) => tempIosPlatRegList || (tempIosPlatRegList = getDefaultIosPlatRegList(ctx)) // 对外提供 扩展方法exportconst addIosPlatReg(ctx, item) {if(validPlatRegItem(item)) {const list = getDefaultIosPlatRegList(ctx) list.splice(-1, 0, item) tempIosPlatRegList = [...list]}return tempIosPlatRegList} // 在唤端业务流程中使用// 匹配运行时平台 并运行匹配到的功能函数if(isIos) {for(let item of iosPlatRegList) {try{if(item && item.regPlat()) { item.handler(ctx)break;}} catch(error) { logError(item, error)}}}if(isAndroid) {for(let item of androidPlatRegList) {try{if(item && item.regPlat()) { item.handler(ctx)break;}} catch(error) { logError(item, error)}}}是不是代码看着代码清晰简洁了很多,并且对外提供了扩展能力,方便使用者对其进行扩展,这也很好的遵循了开闭原则。感悟:其实,JS中策略模式实现并不一定非得采用用对象 Object 或 class 的数据结构去实现,只要定义好一组 case 和 handler, 然后让算法按照指定的 case 去运行对应的逻辑即可,与具体哪种方式无关。参照策略模式的设计准则:定义一族算法(业务规则);封装每个算法;这族算法可互换代替。2. 引入 hooksAOP 思想应该大家都有听说,在前端框架中常常会用到比如:钩子函数或者生命周期函数,其实也是AOP思想的一种实践。引入了钩子函数可以很方便让使用者在相应的位置插入一些自定义逻辑。比如埋点,检查或魔改配置等等。PS: 在唤端处理流程中,由于没有 mixin 之类的用法,没必要兼容 hooks 队列的处理方式,所以并没有引入发布订阅(on、emit)的方式进行hooks实现,只采用最简单函数调用方式即可满足。// 引入 hooks ,方便逻辑插入,埋点等callApp.start({ path: '',// 开始唤起钩子, 暴露出来配置方便进行检查或魔改 callStart(opts) {},// 唤起成功钩子 callSuccess() {},// 唤起失败钩子 callFailed() {},// 开始下载钩子 callDownload() {}})3. 利用位操作进行优化众所周知,在计算机的世界中其有 0 和 1,在对高级语言的处理时,对位运算的处理效率也是最高的,并且在多重判断时候还可以结合二进制 与/或/非 运算特性来简化操作。在判断 currentApp 与 targetApp 时,引入了位运算符,部分代码如下:// AppFlags 标记exportconstenumAppFlags{ ZZ = 1,ZZSeller= 1<< 1,ZZHunter= 1<< 2,ZZSeeker= 1<< 3,WXMini= 1<< 4,NoZZ= (1<< 1) | (1<< 2) | (1<< 3) | (1<< 4),} // targetAppFlag 为唤起目标app flagif(targetAppFlag & AppFlags.ZZ) {// ...} elseif(targetAppFlag & AppFlags.NoZZ) {// ...} else{} // 引入 flag 之前的写法- if(targetApp === 'zz') {- //- } elseif(- targetApp === 'zzSeeKer'||- targetApp === 'zzHunter'||- targetApp === 'zzSeller'||- targetApp === 'wxMini'- ) {- //- } else{}更多实践下载后还原还原方案主要分为两种:1.存储到剪切板在转转IOS端进行初次下载还原活动页面功能,主要采用的是复制到剪切板的方案。唤端失败会触发失败 hook 或者点击下载触发下载 hook 时,会把约定好的协议内容copy 到系统剪切板, 下载完成后 App 启动会自动读取剪切板内容,如果内容的协议格式匹配成功,则跳转到指定页面进而实现页面还原。在前端的工作主要就是 把协议内容复制到剪切板即可。代码实现如下:const copyToClipboard = str => {// 创建 textarea 元素const el = document.createElement('textarea'); el.value = str; el.setAttribute('readonly', ''); el.style.position = 'absolute'; el.style.left = '-9999px';// 添加到页面 document.body.appendChild(el);// 获取原有 selected rangeconst selected = document.getSelection().rangeCount > 0? document.getSelection().getRangeAt(0): false;// 选中并复制 el.select(); document.execCommand('copy');// 移除 textarea document.body.removeChild(el);// 还原 selected rangeif(selected) { document.getSelection().removeAllRanges(); document.getSelection().addRange(selected);}};页面还原流程页面还原效果如下:2. 上传到服务端在安卓侧是通过绑定 deeplinkid 来进行实现,首先讲活动页地址在服务端生成一个唯一的 depplinkid,然后在触发下载时,请求指定的链接(url要带上deeplinkid),服务端会返回带有 deeplinkid信息的下载包链接,触发进行下载安装即可还原活动页。// 拼接 deeplinkid 到下载 api url 即可const zzDownloadLink = `${downloadApi}?applinkId=${}&channelId=${}` // 触发下载evokeDownload(zzDownloadLink)整体流程如下图:还原流程短信短链接唤起如果用户可以在短信中直接唤起 App(毕竟短信也是普遍流行的通信工具之一) 那将是一件很美好的事情,唤端体验丝滑。由于有 IOS 的 universal link 协议和 Android 的 app links 协议的支持(基于 http scheme),让投放的短信短链接唤起 App 也成为了可能。经过调研和实践发现实现起来并不难,只需要把短链接服务的域名支持上相对应的协议并在客户端稍作配置即可。下面是 IOS 短信短链接唤起 App 的实现流程。短链接唤起流程短链接唤起效果如下:唤端数据统计为了方便唤端数据的统计(唤端 PV 以及唤端成功率/失败率/下载率等), 可以结合 hooks 和 乐高埋点(转转内部埋点系统)进行唤端数据上报和后续分析。hooks + lego埋点代码如下:import lego from'./lego' newCallApp({ path: '', callStart() { lego.send({ actiontype: 'START', pagetype: 'ZZDOWNLOADH5', backup: { channelId }, }) }, callSuccess() { lego.send({ actiontype: 'SUCCESS', pagetype: 'ZZDOWNLOADH5', backup: { channelId }, }) }, callFailed() { lego.send({ actiontype: 'FAILED', pagetype: 'ZZDOWNLOADH5', backup: { channelId }, }) }, callDownload() { lego.send({ actiontype: 'DOWNLOAD', pagetype: 'ZZDOWNLOADH5', backup: { channelId }, }) }, })通过埋点数据我们可以做进一步的分析统计,比如唤端次数,系统信息 UA 等等,进而可针对性的对系统进行完善。下面举个例子:下图是在lego平台查看的某时段的埋点数据:唤端触发唤端成功唤端失败678359238通过数据发现一个问题,数据中触发 > 成功 + 失败, 那么说明什么原因造成的呢?可以推测唤端触发时按钮没有加防抖,那么就可以加以对其进行修复完善,这样就可以形成一个数据驱动反馈的闭环。周边生态建设工具平台前端工具平台可一键生成唤起App长链接、短链接,短信短链接等,便于业务使用。工具平台统跳平台统跳平台便于多个App跳转目标URL进行统一管理。URL可以是一个页面地址,也可以是一个行为的描述,比如弹窗。中间落地页统一的中间页,让使用者可以接入URL页面的形式来接入唤起功能,而无需修改项目代码引入唤起库,唤端中间页支持多App。开源建设项目已经在 Github 开源,欢迎吐槽。Github 地址:https://github.com/zhuanzhuanfe/call-app总结与展望经过大量的实践,在唤端方面积累沉淀了一些经验,但是还有很长的路要走。比如现在的唤端成功率只是一个很粗略的统计。如果用户触发了 universal link 唤端,唤起失败就会跳转到 universal link 绑定的域名,这种成功/失败是统计不到的(页面进行了跳转,页面栈消失),需要结合客户端侧共同做一套全链路的上报系统。另外,现在端内分享出去的页面,和唤端回流形不成闭环,因此这种分享链路以及用户之间的隐藏关系就无法统计,无法更好的进行算法推荐。以上就是本次分享的全部内容了,感谢阅读。最后,附上唤端兼容性一览表:H5 唤端兼容性【都是心血】
2022年12月04日
211 阅读
0 评论
0 点赞
1
2