-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcontent.json
1 lines (1 loc) · 289 KB
/
content.json
1
{"meta":{"title":"cmlanche","subtitle":"越努力越幸运","description":"写代码是可确定因素,其他基本都是不可确定的,至于结果怎样,有时候也靠运气,不是你能够掌控的,有句话说越努力越幸运,其实也就是说,努力不一定能够成功,但可以提高概率","author":"cmlanche","url":"http://www.cmlanche.com"},"pages":[{"title":"","date":"2019-06-08T04:43:47.000Z","updated":"2019-06-08T04:43:46.000Z","comments":true,"path":"about/index.html","permalink":"http://www.cmlanche.com/about/index.html","excerpt":"","text":"hello,大家好,我是 cmlanche,网络上但凡是搜到cmlanche都是我,因为只有我才会取这个唯一的id。是采用我的名字拼音缩写cm+lanche(《三傻大闹宝莱坞》中的男主角的名字兰彻的拼音缩写)。 2013届软件工程毕业生,工作语言主要是Java, 日常开发技术有Java、JavaScript、JavaFx客户端技术、Android开发以及移动自动化测试技术。业余爱好比较广泛,希望以后能成为一个技能全面,能力超强的全栈开发工程师,后端目前正在学习Spring Boot和Spring Cloud微服务系列,前端变化很大,目前前端js方面有学Vuejs、Angular2,另外我有Html、css、JavaScript的基本功,研读了《head First Html5 & css》,同时曾经也沉浸在Bootstrap、foundation等一批优秀的前端css框架中。 另外我建立了一个JavaFx开发社群,社区活跃,欢迎各位对java客户端技术感兴趣的同学加入:518914410 2018年06月19日(已下线)上线主机深度评测网hostreport.cn,为大家选择最靠谱的虚拟主机、云服务器、VPS、SSL数字证书,更多优惠互动提醒。 2019年02月20日(运行中)上线主机排行网HostingRanking.cn,为大家选择最靠谱的虚拟主机、云服务器、VPS、SSL数字证书,更多优惠互动提醒。 2019年2月27日(持续招募独立开发者)在v2ex上写了篇《程序员的微创业》获得了大量关注,网友请求创立了“独立开发者”微信群,我的微信号是cmlanche,有兴趣的可以加我好友,拉你进群。备注”找主机”,我加你进主机微信群,备注”独立”,我拉你进独立开发者群,备注”都来”,我都拉你。 也可以扫码关注我公众号,了解更多主机和独立开发者的故事! 2019年6月1日(已上线)完成主机排行网的重新改版,上线找主机zhaozhuji.info网站,完成从头到脚每一个字符都是自主研发,提供优惠提醒,主机相关知识的普及,以及主机大全。 做一个产品贵在坚持,不忘初心,方得始终! 金陵岂是池中物,这是我的座右铭,人活一世,不做点有意义的事情,算是白活。尤其对我们普遍大众程序猿来说,都不想普通,因为大部分人来说都不会像王思聪那样含着金钥匙出生。奋斗这个词很虚幻,怎么有效合理的奋斗才可以不伤身体又能够实现目标才是我们程序猿要想明白的事情。"},{"title":"友链+","date":"2019-05-16T13:11:12.000Z","updated":"2019-06-11T10:37:59.000Z","comments":true,"path":"youlianplus/index.html","permalink":"http://www.cmlanche.com/youlianplus/index.html","excerpt":"","text":"找主机网,主机排行网,Jason,CodeSpots,运维咖啡吧,KuoLu,包子,vps导航,Congz.club,冯言疯语,透明创业实验室t9t.io,张俊杰的微博客,XCodeBuild"},{"title":"categories","date":"2017-09-30T06:56:49.000Z","updated":"2017-09-30T06:57:33.000Z","comments":true,"path":"categories/index.html","permalink":"http://www.cmlanche.com/categories/index.html","excerpt":"","text":""},{"title":"tags","date":"2017-09-24T10:38:27.000Z","updated":"2017-09-24T10:39:25.000Z","comments":true,"path":"tags/index.html","permalink":"http://www.cmlanche.com/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"读楚汉历史有感","slug":"读楚汉历史有感","date":"2019-08-03T15:42:26.000Z","updated":"2019-08-04T02:12:24.000Z","comments":true,"path":"2019/08/03/读楚汉历史有感/","link":"","permalink":"http://www.cmlanche.com/2019/08/03/读楚汉历史有感/","excerpt":"","text":"人、关系人的感情和关系都是势,关系的变化,会引起形势的变化,并非有固定死的关系,很多事情都是建立在此前建立的关系基础之上的。 所以人应该要注重关系建立和维护,不论是父子母子兄弟至亲关系,还是普通朋友关系,越是处理得当,对自身未来的发展都可能会产生一些正面的影响,务必重视,不可小孩子气。 这应该就是对与社会相处的一种理解吧。 视频连接 有志不在年高原文:”反秦”不分先后,这位从前他只是秦朝的”泗水亭长”,什么叫”泗水亭长”,按现在的官职怎么套?也就是当地一个副股级的保安队长吧,咱就这样说吧,这个人到后来,他本来是给公家干事的,他因为就误事了,不能把羁押的犯人按时送达秦政府指定的地点,最后他把那些刑徒都放了, 然后率领一些人流窜山中, 变成了一个小头目,那么大泽乡陈胜起义之后,刘邦一看天下乱了,自己也起来了,他当时就以自己老家沛县为根据地,开始了他的姗姗来迟的传奇一生,所以呢有志不在年高,成事不在年老,只要有梦想,总有实现的那一天。(刘邦起义时48岁了都) 总之,莫慌,莫急,看清楚形势,努力便是,能有机会成事那是最好,没有机会,也安然一生足矣![自我总结] 视频链接","categories":[],"tags":[]},{"title":"学计算机的你伤不起啊!!!","slug":"学计算机的你伤不起啊!!!","date":"2019-07-30T06:39:07.000Z","updated":"2019-07-30T06:42:55.000Z","comments":true,"path":"2019/07/30/学计算机的你伤不起啊!!!/","link":"","permalink":"http://www.cmlanche.com/2019/07/30/学计算机的你伤不起啊!!!/","excerpt":"学计算机的你伤不起啊!!!!!!老子六年前开始学计算机啊!!!!!!于是踏上了尼玛不归路啊!!!!!! 谁特么跟老子讲计算机是王道专业啊!!!!!!尼玛路边乞丐都是程序员!!!!!! 会打代码的一大把啊 有木有!!!!!!!!!谁再跟老子讲计算机是王道专业 老子一个键盘盖死你啊,一个鼠标线勒死你啊!!!!","text":"学计算机的你伤不起啊!!!!!!老子六年前开始学计算机啊!!!!!!于是踏上了尼玛不归路啊!!!!!! 谁特么跟老子讲计算机是王道专业啊!!!!!!尼玛路边乞丐都是程序员!!!!!! 会打代码的一大把啊 有木有!!!!!!!!!谁再跟老子讲计算机是王道专业 老子一个键盘盖死你啊,一个鼠标线勒死你啊!!!! 尼玛一上大学就找不到女朋友!!!!!!班上男女比例八比一,八比一啊!!!!!!都塔玛建军节了!!!!!!就八比一还有学长来抢有木有!!!本科学长,研究生学长还有博士学长!!!!!!玛德学长你们是兔子啊就这么喜欢啃嫩草!!!!还草!!!!!!等熬过一年去迎新,一件行李十几个人竞争!!!!!!要打群架了有木有!!!妹子吓哭了有木有!!!!!! 学了两年还在学数学物理!!!!!!傅里叶!!!拉普拉斯!!!尼玛两个法国老头死了咋还这不安神呢!!!!!!编程作业 Code 得抄在纸上交!!!汇编啊!!!!!!随随便便就几百行啊!!!!抄次作业都要半个小时啊!!!!!!课设还要插电板!!!!!一个板子插一千多根线,一千多根线!!!!!!连起来可以绕地球三圈半啊!!!!!!谁再说计算机好学劳资吐一口血水腥死他!!!!!! 学计算机的孩纸真是命苦啊!!!!!!!!电脑坏了来找你修的有没有啊!!!!!!!!!!还有尼玛谁说学计算机就会修电脑!!!!!!就会买电脑!!!!!!劳资用 linux 不懂 windows 可不可以!!!!!!劳资用 windows 也不懂 windows 可不可以!!!!!!谁规定学计算机的就要会修电脑啊!!!!!!!!!!!!!!特么学光电的会不会发光啊!!!!!!!!!!!特么学能源的会不会发电啊!!!!!!!!!!!!特么学化学的会不会自爆啊!!!!!!!!!!!!特么学妇产的难道会怀孕啊!!!!!!!!!!!!你当你是冠希哥啊,是个男人就想修你电脑啊!!!!!!!!!!!!!重装系统也别找我!!!!!!劳资帮隔壁系妹子重装了 10 几次操作系统了啊!!!10 几次啊!!!!!! 10 几次连手都还没牵到啊!!!!!!你特么一句想重装,哥特么的要通宵啊,有木有啊!!!!!!!!!!!!!!!你们全家才是修电脑的啊!!!!!!!!!!!!!!! 问你会不由盗 QQ 的有木有啊!!!!!!还鄙视你盗个 QQ 都不会搞什么计算机的有么有啊!!!!!!!!!!Word 不会用的来问你的有没有啊!!!!!!!!!!Excel 不会使的来找你的有没有啊!!!!!!!!!电影音乐下不来找你拷的有没有啊!!!!!!!!!!!!!看毛片中毒了来找你的有没有啊!!!!!!!!!!!!老子是学计算机的,不是你妹的 F1 啊!!!!!!!!!问我也不可以!!!!!!劳资还是买块硬盘撞死算了!!!!!! 语法书一本就是一个砖头啊!!!!!!!!!!!每一种都不一样啊!!!!!!!!!!C,C++,C#,mips,java,sql,jsp,asp,php !!!!!!!!!!!每一本都可以砸死你啊!!!!!!!!!特么编译器都一坨啊!!!!!!!!!!!!!学个 C,指针搞死你啊!!!!!!!学个汇编,寄存器几十个啊,尼玛每一个都不一样啊,指令有多少你都不敢想啊!!!!!!!!!!! 你当哥是 CPU 啊,人生价值就是执行指令啊!!!!!!!!!!Sql 还没学好就叫你写一个数据库的有没有啊!!!!!!!!!Windows 还没用好就叫你编译 linux 内核的有没有啊!!!!!!!!!!MeeGo 有没有听说过啊!!!!!!!!!!!!尼玛上个学期还来学校开讲座啊!!!!!!!!!!!!特么还讲的头头是道前途无限啊!!!!!!!!!!这个学期就特么没啦!!!!!!!!!!没啦!!!!!!你特么怕不怕啊!!!!!!!!!哥早几年毕业要是干这个现在就好跳楼了,有没有啊!!!!!!!!!!!! 工作敢不敢找啊!!!!!!!!!!!!!!!!!!!面试书买了十几本有木有!!!天天研究各个公司面经有木有!!!都快面瘫了!!!!!!玛德现在每个公司都学着 Google 考算法!!!!!!算你妹!!!!!!尼玛贵公司产品里只有算法啊!!!!!!尼玛难道每个人都是搞 ACM 的啊!!!!!!还不如去 SM !!!!!!NND 面试官你要不是事先知道答案你做得出来吗!!!!!!你做得出来吗!!!!!!公司一开口就是要你各种精通啊!!!!!!!!!!!!!!!精通 C,精通 java !!!!!!!!!!!!!!!!!!!还要你妹的会人际沟通啊,怕你一个学计算机的不好相处啊!!!!!!!!我特么现在就精通修电脑啊!!!!!!!!!!!!!!!!!!!!!我特么现在就会咆哮啊!!!!!!!!!!!!!! 找到工作了也是民工!!!是码农!!!Robin 都首富了,你都还没首付!!!!!!羞愧吗!!!!!!羞愧吗!!!!!!工作了照样没有女朋友!!!研发部门的比例连八比一都没有!!!!!!新入职的 mm 上学期间都被下手了有木有!!!!!!上学期间下手的 mm 入职后都被挖了墙角有木有!!!!!!卧槽都是程序猿,相煎那么急!!!!!!尼玛是个搞计算机的最后都去搞单反,搞摄影!!!!!!尼玛搞来搞去还不就是为了搞 mm !!!!! 工作压力也超大!!!!!!有没有啊!!!!!操着卖白粉的心,挣着卖白菜的钱!!!应用上线压力大,一分钟几十万收入有木有!!!尼玛收入归老板,责任该你挡有木有!!!!!!凌晨三点跑去公司解决线上故障!!!大便便秘要带笔记本防止突发事件!!!一天收几百条报警短信!!!有木有!!!有没有!!!有木有!!!!!!万一哪天 ML 时报警短信来了,吓出病了,找谁哭去!!!!!!找谁哭去!!!!!! 特么知不知道什么叫需求啊!!!!!!!!!!!!!!!!!需求特么的就跟菊爆一样爽啊,有没有啊!!!!!!!!!!!!!!!!!动一下你就痛的要死啊!!!!!!!!!!!!!!多动几下下辈子都是折翅的天使啊!!!!!!!!!特么知不知道什么叫文档啊!!!!!!!!!!!!!!!!文档特么就跟自宫一样爽啊,有没有啊!!!!!!!!!!!!!!!还没开操写文档就写死你啊!!!!!!!!!!!!!!!!!!坑爹啊!!!!!!!!!!!!!!当老子是文艺小青年啊!!!!!!!!!!!!!!!!!!!!计算理论有没有听说过啊!!!!!!!!!!!!!!!!!有限状态自动机下推自动机图灵机啊!!!!!!!!!!!!!!!!别特么问我是什么机啊!!!!!!!!!!!!!!都特么是别人 YY 的啊,有没有啊!!!!!!!!!!!!!!!正则语言上下文无关语言递归可枚举语言啊!!!!!!!!!!!!别特么问我是什么语言啊!!!!!!!!!!!!!!!!学了半天停机问题搞不定啊!!!!!!!!!!!!!!!!!别特么问我为啥搞不定啊,特么有人证明了你搞不啊!!!!!!!!!!!!!!!!!!!!!!!!证明你搞不定啊,有没有啊!!!!!!!!!!!!!!!!! 每个你用过的 IT 产品和应用,都是背后无数程序猿的血与泪啊!!!!!!!!!!每个学计算机的上辈纸都是身怀绝迹的路边乞丐啊!!!!!!!!!!!!!! runtime error go ** yourself 啊!!!!!!!!!!有木有!!有木有!!!","categories":[],"tags":[]},{"title":"美团点评云真机平台实践","slug":"美团点评云真机平台实践","date":"2019-07-24T09:36:22.000Z","updated":"2019-07-24T09:52:23.000Z","comments":true,"path":"2019/07/24/美团点评云真机平台实践/","link":"","permalink":"http://www.cmlanche.com/2019/07/24/美团点评云真机平台实践/","excerpt":"背景随着美团点评业务越来越多,研发团队越来越庞大,对测试手机的需求显著增长。这对公司来说是一笔不小的开支,但现有测试手机资源分配不均,利用率也非常有限,导致各个团队开发、测试过程中都很难做到多机型覆盖。怎么样合理、高效利用这些测试手机资源,是摆在我们面前的一道难题。","text":"背景随着美团点评业务越来越多,研发团队越来越庞大,对测试手机的需求显著增长。这对公司来说是一笔不小的开支,但现有测试手机资源分配不均,利用率也非常有限,导致各个团队开发、测试过程中都很难做到多机型覆盖。怎么样合理、高效利用这些测试手机资源,是摆在我们面前的一道难题。 现有的方案为了解决这些问题,业内也出现了一些手机管理和在线调试使用的工具或平台,比较常见的有: OpenSTF 百度MTC的远程真机调试 Testin的云真机 腾讯WeTest的云真机 阿里MQC的远程真机租用 其中OpenSTF是开源项目,其他的平台大多也都是基于OpenSTF原理实现的。因此,我们对OpenSTF项目进行了深入研究。 遇到的问题我们首先按照OpenSTF官方的方案进行了搭建,并进行了小规模的应用,但渐渐的我们发现了它的一些问题: 模块过多而且耦合紧密,解耦难度较大,每次修改需要更新所有模块,难以快速迭代开发。 部分技术选型落后。由于OpenSTF出现的时间比较早,部分技术已经落后于目前的主流。例如OpenSTF前端选用AngularJS 1.0进行开发,在生态链方面已经落后于其他流行的框架;数据库方面选用非关系型数据库RethinkDB,在数据计算和性能方面弱于MySQL等关系型数据库,同时RethinkDB资料较少,不便于开发与维护。 OpenSTF屏幕图像传输采用图片单张传输的方式进行,而且画质不能由用户来调节,实际应用中占用带宽很高,在网络比较差的情况下会有严重的卡顿现象,体验很差。 我们的方案架构设计根据业务场景的需要,并吸取了OpenSTF结构优点,我们采用Agent/Server模型的模块化设计方案。下面分别介绍主要模块的功能: Agent模块。Agent模块与OpenSTF的provider类似,部署在服务器上或者用户的电脑上,Agent连接真实的手机,并且将手机的屏幕图像通过Websocket动态代理到Websocket服务器上,然后通过消息中心来进行消息的传递。 Server模块。Server用来集中管理和调度手机,与OpenSTF结构不同的是,我们的Server端包含Web服务器、Websocket服务器、动态代理以及消息处理服务等部分,Server将用户的访问动态代理到对应的Web服务器和Websocket服务器上,并通过消息处理模块向消息中心传递消息,实现用户与Agent端手机的交互。 数据存储模块。数据存储模块用来保存整个平台的数据,例如手机的状态、用户使用记录等。数据存储模块由MySQL数据库和一个RPC服务组成,Server不再直接读写数据库,而是通过一个RPC服务来进行数据的读写操作。 消息中心。与OpenSTF的triproxy功能类似,是连接手机和Server的枢纽,消息中心主要处理屏幕的操作以及手机的状态变更等消息。 通过模块化设计,项目结构比OpenSTF更加清晰。下面是整个系统的设计图: 架构的优势Agent模块我们直接复用了OpenSTF的provider大部分功能,包括minicap、minitouch等。在此基础上,我们也扩展了一部分OpenSTF缺失的功能,比如: 在provider的基础上进行了二次开发,使其支持画质/帧率调节(下文会有详细说明)。 加入健康检测功能,检测手机网络是否正常、是否设置了网络代理等。 加入Inspector功能,方便获取UI控件树(下文会有详细说明)。 对Agent进行了版本区分,便于Web端根据不同的Agent版本对相应的功能展示和隐藏。 在Server模块中,我们引入了动态代理的机制,并且重新开发了Web部分,采用了Vue 2.0 + iView来实现,数据库采用了MySQL。 相比OpenSTF原生架构,总结下来有以下优势: 模块耦合程度低,开发和部署更方便。OpenSTF各个功能模块不仅数量多而且代码耦合紧密,在此基础上进行二次开发和部署非常困难。而我们将整个项目分为Server、Agent、消息中心、数据存储四个模块,四个模块功能和代码都是独立的,基本上没有耦合关系,支持快速迭代开发,部署也很方便。 前端框架更主流,开发和维护成本低。OpenSTF前端是使用AngularJS 1.0实现的,AngularJS 1.0已经处于废弃阶段,各种第三方组件基本已经停止支持,AngularJS 2.0的社区和生态并未成熟,而我们采用了Vue 2.0前端框架,Vue 2.0相对已经成熟,在美团侧也已经有大量应用,能够快速开发高质量的Web功能。 数据库性能强,设计灵活、维护方便。OpenSTF使用RethikDB作为数据库,RethikDB是一个NoSQL型数据库,它有非常多的缺点,比如处理大量数据时的性能很差,资料非常匮乏,排查问题和数据库维护都非常困难。而我们采用了MySQL数据库,很好的解决了这些问题,并且实现了Server模块与数据读写的分离,这样在更新数据库表结构的时候无须同时修改Server端的代码,只要保证RPC服务的接口格式一致即可,开发和维护更加方便。 除了这些基础的功能之外,我们还开发了一些特色的功能,下面我们来详细介绍。 特色功能与客户端自动化相结合为了合理、高效利用测试手机资源,我们与客户端自动化进行了结合,主要有两个方面: 与集团内部的云测平台深度融合。在云测平台的服务器节点上部署Agent代码,为云测平台自动化任务创建者提供自动化过程展示和远程调试功能,同时将云测平台空闲的手机开放给更多人使用。 开放API。我们开放了一些API给普通用户,供用户查询手机状态、占用手机、连接adb调试等,用户可以使用脚本调用API,然后直接在平台的手机上进行自动化测试。 预约功能当一台手机处于繁忙状态时,用户必须要等待手机空闲后才能使用,由于手机空闲时间不确定,就会给用户带来很大的不便。为了解决这个问题,我们开发了手机预约的功能,用户可以预约处于繁忙状态的手机,当手机空闲时,自动帮预约用户占用15分钟,并通过即时通讯工具通知预约人。 画质调节远程调试平台的核心是实时获取屏幕图像,由于屏幕传输需要比较大的网络带宽,在网络不佳的情况下就会出现卡顿现象。因此,我们针对不同的网络做了一些流畅度的优化,下面来介绍一下其中的细节。 屏幕获取的原理是通过minicap来高速截图,每秒最高可达60张,然后将这些截图显示在Web上。因此,我们考虑从两个方面来优化网络带宽的占用,第一个是调节截图的质量,minicap本身支持调节画质(OpenSTF固定设置了80%的压缩比),关键代码如下: var rate = Number(match[6]) if (rate > 2 && rate < 100) { log.info(rate) if (rate > 30) { options.screenJpegQuality = 80 }else if (rate > 15) { options.screenJpegQuality = 50 }else { options.screenJpegQuality = 20 } frameProducer.restart() framerate = rate } 第二个是调节每秒发送的图片张数,也就是帧率,我们可以在Agent端控制发送图片的数量,关键代码如下: # 首先修改帧率发送部分:function wsFrameNotifier(frame) { if (latesenttime == 0 || Date.now()-latesenttime > 1000/framerate) { latesenttime = Date.now() return send(frame, { binary: true }) } }# 再加入调整帧率的代码:case 'rate': var rate = Number(match[6]) if (rate > 2 && rate < 100) { framerate = rate } break 那么,帧率和图片压缩比调节到多少才能满足不同网络环境的需要呢?我们先来看一组数据: 图片压缩比 图片尺寸 100% 69.82KB 80% 46.78KB 50% 41.87KB 20% 37.84KB 10% 35.84KB 表中是使用minicap做的图片压缩实验,从数据中我们可以看到当图片质量降低到80%时图片大小降低比较明显,而图片质量并没有明显的下降。继续降低图片质量,图片大小降低有限。我们再来看另外一组数据: 帧率 图片压缩比 实际流量 60 100% 4.02M/S 60 80% 2.74M/S 60 50% 2.41M/S 30 80% 1.43M/S 30 50% 1.22M/S 30 20% 1.10M/S 15 50% 0.63M/S 15 20% 0.55M/S 15 10% 0.52M/S 表中是各种帧率和压缩比组合产生的实际流量数据。 从数据中我们可以看到最高帧率和压缩比的组合下,流量达到了4M/S,而80%压缩比时流量减小到了2.7M/S,降低非常明显。考虑到实际网络情况,我们将60帧、80%压缩作为了高画质选项。 而图片质量从80%降低到50%时图片大小下降并不明显,此时降低帧率就成了很好的选择。当帧率降低到30帧时流量降低了一半,1.2M/S的流量能够满足大部分网络状况使用,30帧也能保证操作的流畅度,于是30帧、50%压缩比成为了中画质的选项。 低画质主要是为了保证在较差的网络环境能够正常使用,500K/S的流量是红线。我们将15帧、20%压缩比作为低画质选项,此时图片质量和帧率较低,但能够保证基本的使用体验。 除了通过降低图片质量和帧率来减小手机屏幕图像传输的流量外,将图像使用H264等编码压缩成视频传输也是一种有效降低流量的办法,相对于图片,图像的压缩率将会更高,用户的操作体验也会更好。但是图像压缩编码原理比较复杂,相关技术我们还在研究当中。 App Mock在做App测试过程中经常需要抓包,一般情况下,我们通过修改WiFi的代理然后用抓包工具就可以实现。但是这样做的效率比较低,多个工具切换也非常不便。借助集团的Mock平台,我们开发了一键Mock功能,能够快速完成相应App的Mock操作。带来的好处是我们可以一边操作App,一边查看App发出的请求,大大提高了测试的效率。 App InspectorApp Inspector功能可以让用户在平台上使用真机的同时查看页面控件树及页面元素,并且支持Xpath,更加方便高效的查找页面元素,给UI自动化测试提供了很大便利。 这个功能我们是借助Uiautomator实现的。基本原理是写一个Uiautomator用例,用来获取当前页面的Hierarchy,然后将用例打包成一个JAR放到Agent端。当在Web端触发获取控件树时,Agent将JAR包推送到手机上并运行,此时会在手机端生成一个XML文件。然后再使用cat命令获取XML内容并在前端解析。用例核心代码如下: public class launch extends UiAutomatorTestCase { public void testDumpHierarchy() throws UiObjectNotFoundException { File file = new File(\"/data/local/tmp/local/tmp/uidump.xml\"); UiDevice uiDevice = getUiDevice(); String filename = \"uidump.xml\"; uiDevice.dumpWindowHierarchy(filename); } } 当然,你也可以用adb命令来获取Hierarchy: adb shell uiautomator dump /data/local/tmp/uidump.xml 但这种方式不能获取动态页面,比如视频播放页面。 数据报表为了更好的了解平台的运营情况,我们做了详细的数据统计,主要从使用次数、使用时间、设备数量、使用分布等方面进行统计。目前我们管理的手机近300台,平均每个月有超过500名研发人员在使用我们的平台,每天的使用次数达到280次,每天总使用时长超过60小时。 其他小功能除了以上几个比较大的功能点,我们也做了一些贴心的小功能,比如:检测手机网络是否设置代理、检测手机已安装的应用版本及安装时间、快速安装最新版本的测试包、支持App内的Schema跳转等等。这些小功能为研发人员节省了很多时间,提升了他们的工作效率。 未来规划iOS手机支持目前,云真机平台只支持Android手机,对iOS手机的支持正在进行中。我们已经初步完成了主要功能的开发,预计很快将与大家见面。 产品优化我们计划继续扩展产品功能,比如支持Log日志展示和性能数据采集等。目前云真机平台已经在美团点评内部平稳运行超过两年,我们会继续不断迭代版本、打磨产品,提供更好的使用体验。 作者简介东初,大众点评平台质量工具组负责人。7年互联网行业测试、开发经验,2015年加入原大众点评。先后主导了云真机平台、单元测试平台、web安全实验平台等项目的开发,致力于用工具来提升研发团队的工作效率。 李帅,大众点评高级测试开发工程师。2015年加入原大众点评,主要负责云真机平台的开发以及客户端底层组件的测试。热衷于钻研测试领域的前沿技术,并推动了多项新技术落地。 原文信息(好文章值得完整复制收藏,避免原文也没了)作者:美团技术团队 链接:https://juejin.im/post/5b5556eae51d45195f0b3422 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。","categories":[{"name":"自动化技术","slug":"自动化技术","permalink":"http://www.cmlanche.com/categories/自动化技术/"}],"tags":[{"name":"云真机","slug":"云真机","permalink":"http://www.cmlanche.com/tags/云真机/"}]},{"title":"热情不减,开启转变,专访独立产品设计师 Allen","slug":"热情不减,开启转变,专访独立产品设计师 Allen","date":"2019-07-24T06:34:52.000Z","updated":"2019-07-30T06:38:47.000Z","comments":true,"path":"2019/07/24/热情不减,开启转变,专访独立产品设计师 Allen/","link":"","permalink":"http://www.cmlanche.com/2019/07/24/热情不减,开启转变,专访独立产品设计师 Allen/","excerpt":"热情不减,开启转变,专访独立产品设计师 Allen原文:https://indiehacker.im/re-qing-bu-jian-kai-qi-zhuan-bian-zhuan-fang-du-9f863d86 Allen (王老板)是我们的好朋友,Price Tag、捷径社区的视觉设计都出自他之手,这个月我们还有新的合作项目即将上线。最近他开始了自己新的事业,做一个全职的独立产品设计师。上半年他上架了 One Switch 和「出彩」两款优质的应用,其中 One Switch 更是被国外知名应用订阅服务 Setapp 收录。今天我们邀请他来和我们聊聊新作品和成为独立产品设计师的体验。 谈一下你自己现在的状态和你的作品?我是一个产品设计师,最近半年经历了前公司的创业失败,所以现在是正式全职的独立产品设计师了。以前自学 Swift 想当独立开发者,现在有点学不动了,哈哈,不过主要是因为自己感受到明显的时间流逝,如果一切自己做对我来说还是太慢了点,完全的全干类型我是挺钦佩的,但因人而异,我会更享受和优秀的人合作过程中碰撞出来的乐趣,所以现在的状态还是挺开放的合作方式,有兴趣一起创造可持续盈利产品的开发者,或是想要塑造自己业务的公司都可以来找我和我的伙伴们。","text":"热情不减,开启转变,专访独立产品设计师 Allen原文:https://indiehacker.im/re-qing-bu-jian-kai-qi-zhuan-bian-zhuan-fang-du-9f863d86 Allen (王老板)是我们的好朋友,Price Tag、捷径社区的视觉设计都出自他之手,这个月我们还有新的合作项目即将上线。最近他开始了自己新的事业,做一个全职的独立产品设计师。上半年他上架了 One Switch 和「出彩」两款优质的应用,其中 One Switch 更是被国外知名应用订阅服务 Setapp 收录。今天我们邀请他来和我们聊聊新作品和成为独立产品设计师的体验。 谈一下你自己现在的状态和你的作品?我是一个产品设计师,最近半年经历了前公司的创业失败,所以现在是正式全职的独立产品设计师了。以前自学 Swift 想当独立开发者,现在有点学不动了,哈哈,不过主要是因为自己感受到明显的时间流逝,如果一切自己做对我来说还是太慢了点,完全的全干类型我是挺钦佩的,但因人而异,我会更享受和优秀的人合作过程中碰撞出来的乐趣,所以现在的状态还是挺开放的合作方式,有兴趣一起创造可持续盈利产品的开发者,或是想要塑造自己业务的公司都可以来找我和我的伙伴们。 One Switch 这是款极简的 Mac 菜单栏效率工具,可以帮你一键切换系统各项设置,搞定日常繁琐,比如隐藏桌面,自动切换黑暗模式,让电脑不熄屏,快速暂停和播放 Spotify 和 Apple Music,连接 AirPods 以及其他蓝牙耳机等等。 出彩 App 快速从视频、LivePhotos、GIF 动图导出高清照片。 你的一天生活和工作日常是怎么样的? 08:00 - 10:00 早起时间:起床,户外跑步,做咖啡,收拾下今天的健身运动包,有时候会先在家工作一会儿处理海外用户的反馈邮件,私信,安排好今天的 To-Do 清单。 10:00 - 12:30 交通时间:公交转地铁,路上快速浏览今日科技和设计新闻,到浙大附近 Price Tag 办公室楼下吃饭,有时候在家吃完再出门。 13:00 - 20:00 工作时间:有时在各种城市咖啡店,有时在 Price Tag 办公室,今天在 WeWork 写这篇访谈,我喜欢在一个不固定的地方工作。 20:30 - 21:30 健身时间:撸铁半小时,跑步或游泳半小时。 23:30 - 00:00 睡觉时间:打开 Sleep Cycle 统计睡眠时间。 One Switch 是如何诞生的?这是一个我自身有强烈需求的产品,我本身不是 macOS 的 Power User, 所以或许有些人知道的热键我其实也记不住,包括如何快速打开屏保,在设计的时候来回切换黑暗模式,桌面乱的可以,但是之前使用的一款隐藏桌面图标的软件也无法在 Mojave 里工作,所以很丧气。然后我就开始从 Menubar 这个 macOS 宝地入手开始构思集成多个开关功能的效率产品,一开始只是用 InVision Studio 快速设计了 One Switch 的开关动画,图拉鼎看过后竟然也也很快实现了这个效果,所以后面自然就确定开始打造这款产品,今年 3 月份底上架,前后花了断断续续一个月时间,但其实真实时间应该在 2 个星期左右。 One Switch 作为少数上架 Setapp 的国内团队有什么经验和有趣的事情可以分享的?对我们两人团队来说,能上架 Setapp 其实是个特别大的惊喜,当 Setapp 的运营经理 Maria 发来私信邀请我们上架的时候,真是太激动了,因为这意味着可以跟那些优秀的知名 macOS App 登上同一个平台,同时这也是对我们产品功能和设计的一个肯定。本身由于 Apple 沙盒环境的限制,我们就无缘了 Mac App Store,而 Setapp 的出现给了我们一个渠道,这对独立分发的 macOS App 是极其宝贵的。 想要上架 Setapp 的开发者们,可以先让自己的用户(同时是 Setapp 的用户)在他们的 Trello上提出自己希望可以让哪些 App 加入 Setapp,他们看过后觉得不错符合他们的标准就会去找开发者联系邀请上架,但首先你必须是一个付费的产品。 然后分成单独的 Target,把原来独立发行时的免费试用、License 购买和验证等替换成Setapp SDK 就可以单独提交到 Setapp 供应商后台上架,审核很快,最后会确定一个上架时间配合 Setapp 运营团队开始正式推送到商店,社交媒体,以及他们的邮件订阅列表。 其实这里面最有趣的是,很多国内 Setapp 用户甚至我们的朋友,突然电脑右上角收到一个熟悉的图标的推送后纷纷截图恭喜我们,有种登上舞台开始你的表演的感觉。 后面陆续收到很多 Setapp 用户的反馈,每天早上一打开手机就是这些邮件,有前 Apple 的员工,各个国家的设计师和开发者,还有甚至在 Pixar 时期和乔布斯合作过的工程师,真的都是满满的感激。 出彩这个产品的起源背景是怎么样的?我自己经常户外跑步,每次跑完都会拍照然后用 Nike Running Club 分享到 IG 和朋友圈,这基本是属于我的跑步例程了吧,但是每次一个人跑步拍照有点麻烦,因为不想浪费太多时间,而且前后有人也不方便,所以就想到用拍视频的方式来代替拍照,回去后找到一帧截图就行,可是截图分辨率不高,如果我可以直接导出跟视频一样高清的图片就好了,这就想到了做这样一个简单的工具类产品。在那个时候一个工程师朋友正好也想尝试下新技术 Flutter, 所以最后是用 Flutter 同时实现了 Android 和 iOS 两端,虽然 Flutter 在 iOS 上的体验有待提升,但作为一个简单的小工具我觉得也够了,后续我们也会持续改进,当然也有可能以后用 SwiftUI 重写 iOS 端。 作为创意工作者,你是从哪里获得灵感的?一直以来我就关注很多国外开发者、设计师、创始人、创业公司的推特,这也是我日常最大的信息来源,创造者本身是最大的灵感来源,你会被他们的想法,原型,作品以及日常生活和工作方式给影响,然后启发你用更国际化和多元的思维去创造产品。 Dribbble 对大家不陌生,也是我个人的很大的设计灵感来源,当然主要是可以发我自己的一些真实产品设计的细节。不过自从被收购后,Dribbble 变得太多,流行页面也已经被各种花里胡哨没有逻辑的 Mockup 给占领,很多很厉害的设计师离开这个平台,这是一种悲伤,但看到更多新的真实产品的 Case Study 以及插画艺术作品持续涌现,我还是会继续吸收,学习和输出的。 你的工作配置是怎么样的?工作台 macOS iPhone 你希望 iOS 13 有哪些更好的改进? 首页图标可以按颜色,应用分类,常用应用自动排列,可以隐藏应用图标,在 Spotlight 输入或者 Siri 呼唤打开就可以。 希望 Apple 能认真重视下国内 iMessage 垃圾短信拥挤不堪这个事实。 希望开放下 ScreenTime API. 最喜欢的几个 App 是什么? Sleep Cycle: 最近更新到了 2.0,设计和体验提交到了一个新的层次,累积使用超过 2 年,最喜欢它的深度睡眠检测功能。 Bear 熊掌记:这篇访谈就是这里写的,也是成功带我进入 Markdown 格式写作习惯的编辑器,之前很多我都没怎么适应得了,我讨厌需要记很多东西才能用的产品,最喜欢它的Markdown 语法提示和三种分栏自由切换。 One Switch:虽然是自己做的,但不得不说我已经很难离开它了,每次打开 Mac 的日常就是连接 AirPods,然后打开 Spotify,两个开关搞定。 Procreate Pocket:30 块钱一个 App 配一只 20 块钱触摸笔,你就可以随时画沙雕插画,最喜欢它的录制功能,以及从 iCould 云盘里导入笔刷。 我们还可以在哪里关注到你和你作品的动态?大部分时间在 Twitter 和微博,经常也会用 Instagram Story 发一些设计的过程和草稿,Dribbble 用来发设计作品,当然希望大家也可以关注下我们 Fireball 烧蛋工作室。 Twitter: @creativewang 微博:@Allen朝辉 Instagram: @alllllllllen Dribbble: openallen Fireball Twitter: @fireball_studio Fireball 微博: @烧蛋工作室","categories":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/categories/独立开发者/"}],"tags":[{"name":"采访","slug":"采访","permalink":"http://www.cmlanche.com/tags/采访/"}]},{"title":"记录一次网络拥塞导致线程池处于等待状态","slug":"记录一次网络拥塞导致线程池处于等待状态","date":"2019-07-12T09:37:44.000Z","updated":"2019-07-12T09:45:55.000Z","comments":true,"path":"2019/07/12/记录一次网络拥塞导致线程池处于等待状态/","link":"","permalink":"http://www.cmlanche.com/2019/07/12/记录一次网络拥塞导致线程池处于等待状态/","excerpt":"场景一次批量执行任务(12个),最后任务执行成功会批量上传图片,导致线程池都处于等待状态。 首先这个线程池是通过newSingleThreadExecutor来创建的,并且在执行过程中,不会有新的任务进来,当时的情况是在线程执行过程中卡主,实际发生在上传图片,类似的线程池多大12个,每个线程上传的图片可能多大上百张。","text":"场景一次批量执行任务(12个),最后任务执行成功会批量上传图片,导致线程池都处于等待状态。 首先这个线程池是通过newSingleThreadExecutor来创建的,并且在执行过程中,不会有新的任务进来,当时的情况是在线程执行过程中卡主,实际发生在上传图片,类似的线程池多大12个,每个线程上传的图片可能多大上百张。 分析分析上面jstack打印的日志(打印方法是jstack pid),有个waiting on condition,它的含义,我搜了下,是这样的 由此可以猜测,可能是大并发上传图片导致网络拥塞 解决方法做一个固定线程大小的线程池,比如3个,专门用于并发上传图片,避免网络拥塞。","categories":[],"tags":[{"name":"java性能优化","slug":"java性能优化","permalink":"http://www.cmlanche.com/tags/java性能优化/"}]},{"title":"问答四部曲","slug":"回答问题四部曲","date":"2019-06-20T02:11:05.000Z","updated":"2019-07-24T09:36:03.000Z","comments":true,"path":"2019/06/20/回答问题四部曲/","link":"","permalink":"http://www.cmlanche.com/2019/06/20/回答问题四部曲/","excerpt":"","text":"它是什么 影响和意义 怎么解决的 有什么心得","categories":[],"tags":[]},{"title":"部分手机adb install快速返回成功导致appium测试失败的解决办法","slug":"部分手机adb-install快速返回成功导致appium测试失败的解决办法","date":"2019-06-19T03:20:06.000Z","updated":"2019-06-19T03:25:47.000Z","comments":true,"path":"2019/06/19/部分手机adb-install快速返回成功导致appium测试失败的解决办法/","link":"","permalink":"http://www.cmlanche.com/2019/06/19/部分手机adb-install快速返回成功导致appium测试失败的解决办法/","excerpt":"我们可以在命令行中执行adb install安装某个应用,会发现快速返回Success了,而实际应用正在安装中,这种情况下,appium会误以为被测应用已经安装上了,然后去启动这个app,结果发现app不存在(appium重重试一次,仍然失败),这种情况下,appium测试100%会失败。 事故手机:OPPO R9sk 测试的Appium版本:1.12.1","text":"我们可以在命令行中执行adb install安装某个应用,会发现快速返回Success了,而实际应用正在安装中,这种情况下,appium会误以为被测应用已经安装上了,然后去启动这个app,结果发现app不存在(appium重重试一次,仍然失败),这种情况下,appium测试100%会失败。 事故手机:OPPO R9sk 测试的Appium版本:1.12.1 解决方案找到安装app的地方,安装完后,检测app是否真正安装成功了,如果没成功,则等待,直到超时或者成功安装。 实施办法针对UIAutomator1,我们需要更改appium-android-driver,我们找到lib/driver.js的initAUT方法,在代码await helpers.installApk(this.adb, this.opts);后加上这个检测过程: await helpers.installApk(this.adb, this.opts);log.info('安装应用后,检查被测应用是否存在');await this.waitPackagePresent(60000); waitPackagePresent就是这个检测过程的方法,代码如下: /** * 一定时间内等待某包出现 */ async waitPackagePresent (timeout) { log.info(`waitPackagePresent: ${timeout}`); let start = new Date().getTime(); while (new Date().getTime() - start < timeout) { const appState = await this.adb.getApplicationInstallState(this.opts.app, this.opts.appPackage); log.info(`app state is ${appState} 1`); switch (appState) { case 'notInstalled': log.info(`检测到${this.opts.appPackage}尚未安装上,等待1s,继续检测`); await sleep(1000); break; default: log.info('被测应用已安装上'); return true; } } log.errorAndThrow(`Could not find package ${this.opts.appPackage} on the device in ${timeout}`); } 而针对UIAutomator2的话,同样我们找到UIAutomator2的nodejs驱动工程:appium-uiautomator2-driver,然后找到lib/driver.js,同样也是在initAUT方法中: if (this.opts.app) { if (!this.opts.noSign && !await this.adb.checkApkCert(this.opts.app, this.opts.appPackage)) { await helpers.signApp(this.adb, this.opts.app); } await helpers.installApk(this.adb, this.opts); await this.waitPackagePresent(60000); // 这是新增} waitPackagePresent方法同上。","categories":[{"name":"Appium","slug":"Appium","permalink":"http://www.cmlanche.com/categories/Appium/"}],"tags":[{"name":"appium","slug":"appium","permalink":"http://www.cmlanche.com/tags/appium/"},{"name":"adb-install","slug":"adb-install","permalink":"http://www.cmlanche.com/tags/adb-install/"}]},{"title":"陪伴阿里云走过的5年之路","slug":"陪伴阿里云走过的5年之路","date":"2019-06-16T14:53:07.000Z","updated":"2019-06-17T06:39:28.000Z","comments":true,"path":"2019/06/16/陪伴阿里云走过的5年之路/","link":"","permalink":"http://www.cmlanche.com/2019/06/16/陪伴阿里云走过的5年之路/","excerpt":"第一次使用阿里云那个时候是2014年,我刚刚从大学毕业出来进入社会工作,这一年也是我萌生想额外做点产品的想法的时候,于是我找到了阿里云。 翻开阿里云的支付记录,可以清晰的看到第一笔支付发生在2014年3月9日: 126个交易记录时至今日2019年中旬618的时候,我已经从一个阿里云新手成长为了阿里云大使,总共发生了126个交易记录,这5年时间里,阿里云陪我走过了成长最快的工作生活。 开始我还只是会windows server,因为它有图形界面,那个时候对linux恐惧颇深,感觉深不可测,无法控制。而到现在,我已经精通linux,centos,Ubuntu等系统了。这也是在阿里云服务器中慢慢学习过来的。","text":"第一次使用阿里云那个时候是2014年,我刚刚从大学毕业出来进入社会工作,这一年也是我萌生想额外做点产品的想法的时候,于是我找到了阿里云。 翻开阿里云的支付记录,可以清晰的看到第一笔支付发生在2014年3月9日: 126个交易记录时至今日2019年中旬618的时候,我已经从一个阿里云新手成长为了阿里云大使,总共发生了126个交易记录,这5年时间里,阿里云陪我走过了成长最快的工作生活。 开始我还只是会windows server,因为它有图形界面,那个时候对linux恐惧颇深,感觉深不可测,无法控制。而到现在,我已经精通linux,centos,Ubuntu等系统了。这也是在阿里云服务器中慢慢学习过来的。 夭折了几个产品之前看sitebuilderreport.com的采访,链接:http://sideidea.com/article/3,知道它月赚29万人民币,这还是2年前的事情了,我就想着模仿它做类似的网站,于是用阿里云做了第一个站,叫virturlhostreport.com,意思就是虚拟主机报告,想做这一块的主机深度评测网站,但是夭折了,一部分也是因为虚拟主机已经日落西山了,另一部分是seo始终没做上去,关键还是我不够坚持吧。 后来又做了主机排行网,这次就不限定是虚拟主机了,包括云主机和vps,这个网站是采用ghost博客来做的,自己用纯html+css+js来做的,网站速度极快,有网友评论说,”快到让人窒息”。这个网站大概给我挣了2k吧,发布更新了十几个版本,但就是因为更新迭代内容太慢,每次更新基本都需要我重新升级主题,而且我在对主机评测上又有了新的见解。 现在的产品我认为大的客户厂家的服务器基本没有什么性能啊优劣之分,最大的区别在于优惠、价格,如何能帮助客户省钱省时间省力气才是重点,省钱是第一要务。而且不同的客户的省钱方式不太一样,新手和老手不一样,个人和企业又不一样,需求有多种多样。因此我开发了现在的全面的主机优惠站找主机网,简单截个图: 这个网站是我花了大概2个月的业余时间完成的,很是艰辛,基本每天都有提交记录。 中间ui写了又换,bootstrap尝试过,放弃了,因为ui实在难看,也在themesforest上买过付费主题,但太笨重,js一大推,弄的我整个工程加载都很忙,界面也不大好,最后采用了bulma的纯css框架做的前端,整个站也按标准的css设计规则来做了,都知道程序员的ui感觉不大好,其实到现在我都觉得我的网站的ui不好看,哪天我还会再整。 我也设计了后台管理,方面我快速增加内容,修改需求,这也是从上面的失败案例主机排行网而来的,我得能够轻松快速的紧跟市场啊。我甚至自己设计SEO还有站点地图,稍微截个图吧: 为什么要自己写,而不是用WordPress? 因为WordPress我不太会那些优化,而且关键是不符合我的需求,我是要能够定制不同的客户需求的,WordPress就是无脑的堆砌文章,现在市面上很多主机推荐网站,基本都采用WordPress,但是说真的,里面的内容眼花缭乱的,我认为根本给不到用户实质的价值,这些乱七八糟的内容其实是给搜索引擎看的,至于用户体验,拉倒吧,随你咋看。你去看sitebuilderreport.com,它的内容就井然有序,用户看着非常舒服,它也是作者完全自己写的。 云大使的收入相比很多大佬来说很少,目前总共1427.97元,其中还有80000云气还没兑现。 总共推广了大概150人,累积8人购买。 关于未来坚持把找主机网zhaozhuji.info做下去,坚持每天更新一篇文章,把seo做上去,能够形成不错的自然流量,到那时,也不至于跟现在这样推广的如此艰辛。 可以的话,帮我点个赞,地址:https://www.aliyun.com/acts/hi618/delivery?storyId=874749&userCode=mm1tv2if,谢谢!","categories":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/categories/独立开发者/"}],"tags":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/tags/独立开发者/"},{"name":"找主机网","slug":"找主机网","permalink":"http://www.cmlanche.com/tags/找主机网/"}]},{"title":"奔三分水岭杂想","slug":"奔三分水岭杂想","date":"2019-06-16T11:00:38.000Z","updated":"2019-06-16T11:01:59.000Z","comments":true,"path":"2019/06/16/奔三分水岭杂想/","link":"","permalink":"http://www.cmlanche.com/2019/06/16/奔三分水岭杂想/","excerpt":"昨天周六,在家折腾一天新创意,比如做dota2的赛况的小程序,结果审核不让通过,说需要提供4种”让人生畏”的材料,如下图:","text":"昨天周六,在家折腾一天新创意,比如做dota2的赛况的小程序,结果审核不让通过,说需要提供4种”让人生畏”的材料,如下图: 基本上你想做类似于游戏沾边,热门关键词的话,个人和部分企业是无法申请的,直接把你们拒之门外。 想到,在中国做点东西真tm难,做出来难,能不能做更是难上加难,尤其对独立开发者来说。国内对创业的环境月来越差,对独立开发者来说也更加艰难,那些能在夹缝中生存的独立开发者,内心真心的佩服。 我接触了大概600位独立开发者和对独立开发者有兴趣的人,能够真正全职独立的少指又少,阿里一位个人开发者跟我说,”我看大佬们一通操作之后,发现还是上班最靠谱!”,确实啊,真正能做到独立开发大笔盈利的很少。 我以前对自己说,只要我能够自己稳定一个月赚5k,我就要全职,然后把三倍的时间把5k换成15k。想法很理想,能不能实现倒不说,就算能实现,那也是要经历极其残忍和痛苦的过程,可能这期间你的婚姻、家庭、父母等都承受更多压力,并且以后每天都在担心受怕中,也没有社保公积金等一些社会福利,你是独自一个人在战斗,风险过于庞大,如果你没有靠谱的资金后盾,那这几乎是送死的行为! 可能是我太着急,也可能是我压力太大,我个人家庭情况不允许我做这样的行为,我之前过的很理想,现在想明白了,我应该找一座能让我干一辈子的一个城,把自己最擅长的专业做到好,这是基本,可以填补我内心的焦躁不安。 可是我内心的理想却无法填补,一直对自己说,人生不干出名堂,枉来人世了,我的大学的座右铭还是”会当凌绝顶”,我的个人博客标题还是”金鳞岂是池中物”的豪言壮语,可是如今已奔三的我,感受到了什么是婚姻压力,什么是父母健康,什么是未来,我站在人生的分水岭,彷徨而不安,可又没有办法,在奔三的时候,我感觉我看到了我的未来。 我内心已有答案,人生什么最重要?我觉得是快乐,是父母的快乐,是兄弟姐妹的快乐,是妻子儿女的快乐,也是我的快乐,人生确实要有追求,但是不能急于给自己太大的压力去速成,犹记得三国杀某个英雄的一句话”静待良机,一鸣惊人”,慢慢来。 答案是什么?我的答案就是莫把客为本,过快乐生活,我想当个”真正自由人”,想财务解放,奈何实力不允许,那就静待良机吧。 每个人的答案都可能不一样,莫抄! 说完,我拿起手机,打开沉睡半年的王者荣耀(滑稽 ~ 玩笑话) — 补充 今天父亲节,深知老爸不容易,祝老爸身体健康,万事如意。也祝各位围观的同学的父亲健健康康,一切都好!","categories":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/categories/独立开发者/"}],"tags":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/tags/独立开发者/"}]},{"title":"推广渠道系列 - 豆瓣帖子","slug":"推广渠道系列---豆瓣","date":"2019-06-12T13:46:10.000Z","updated":"2019-06-17T01:55:57.000Z","comments":true,"path":"2019/06/12/推广渠道系列---豆瓣/","link":"","permalink":"http://www.cmlanche.com/2019/06/12/推广渠道系列---豆瓣/","excerpt":"独立开发者往往都不擅长推广,我也一样,但也得学会做推广,推广是慢慢的事情,无法一气呵成,因为穷。 在做完zhaozhuji.info这个站点后,发现最终还是得落实到推广上,因此我推出”推广渠道系列”文章,把我遇到的好的推广工具、方式分享出来,供大家参考。 今天分享的是,豆瓣帖子。","text":"独立开发者往往都不擅长推广,我也一样,但也得学会做推广,推广是慢慢的事情,无法一气呵成,因为穷。 在做完zhaozhuji.info这个站点后,发现最终还是得落实到推广上,因此我推出”推广渠道系列”文章,把我遇到的好的推广工具、方式分享出来,供大家参考。 今天分享的是,豆瓣帖子。 经常搜索,发现很多内容豆瓣帖子都排在前面。 举例子:搜索”小众it技术社区” 其实内容不多,一个标题还有内容,没有seo的关键字和描述,但是这个页面被百度收录了,通过标题可以搜得到。 我们不妨效仿一下,在豆瓣上发布你的推广,注重标题和内容,期待它哪一天能够爆发! 比如我推广我的找主机网 标题:”2019年阿里云2.4折优惠购买指南” 内容:”阿里云对中国用户来说是最好的云计算服务厂商了,而它提供的优惠也多种多样,不管对玩主机的新手还是老手,面对各种各样的活动都会陷入眼花缭乱的困境,因此,我专门写一篇文章来给大家抽丝剥茧,详细描绘一下对不同的用户怎么购买主机才是最优惠的。” 外加一张截图和原文链接,猛戳看效果:https://www.douban.com/group/topic/143072327/","categories":[{"name":"推广渠道系列","slug":"推广渠道系列","permalink":"http://www.cmlanche.com/categories/推广渠道系列/"}],"tags":[{"name":"推广","slug":"推广","permalink":"http://www.cmlanche.com/tags/推广/"},{"name":"推广渠道","slug":"推广渠道","permalink":"http://www.cmlanche.com/tags/推广渠道/"}]},{"title":"2019年阿里云主机优惠购买指南","slug":"2019年阿里云主机优惠购买指南","date":"2019-06-09T04:03:27.000Z","updated":"2019-06-09T04:05:27.000Z","comments":true,"path":"2019/06/09/2019年阿里云主机优惠购买指南/","link":"","permalink":"http://www.cmlanche.com/2019/06/09/2019年阿里云主机优惠购买指南/","excerpt":"阿里云对中国用户来说是最好的云计算服务厂商了,而它提供的优惠也多种多样,不管对玩主机的新手还是老手,面对各种各样的活动都会陷入眼花缭乱的困境,因此,我专门写一篇文章来给大家抽丝剥茧,详细描绘一下对不同的用户怎么购买主机才是最优惠的。 优惠活动图本人是拥有十年程序猿开发经验,大学本科四年,工作6年,软件工程出身,所以我用我们程序猿特有的软件来绘制一个优惠活动图。 大概花了半个多小时绘制玩这张活动图 /(ㄒoㄒ)/~~","text":"阿里云对中国用户来说是最好的云计算服务厂商了,而它提供的优惠也多种多样,不管对玩主机的新手还是老手,面对各种各样的活动都会陷入眼花缭乱的困境,因此,我专门写一篇文章来给大家抽丝剥茧,详细描绘一下对不同的用户怎么购买主机才是最优惠的。 优惠活动图本人是拥有十年程序猿开发经验,大学本科四年,工作6年,软件工程出身,所以我用我们程序猿特有的软件来绘制一个优惠活动图。 大概花了半个多小时绘制玩这张活动图 /(ㄒoㄒ)/~~ 名词解释: 新用户:没注册过阿里云的用户或者注册且实名了但从未购买任何产品的用户 首购用户:表示已注册并实名认证后但从未购买该产品的用户 老用户:已实名并购买过该产品的用户 活动列表: 阿里云新用户2000元新手红包 阿里云Hi拼团活动 阿里云首购3折活动 优惠路线 新用户路线 领取2000元红包 —> 找老用户开hi拼团活动 —> 2.4折优惠 领取2000元红包 —> 参加首购活动 —> 3折优惠 首购用户路线 找新用户开hi拼团活动 —> 可以找到 —> 2.4折优惠 找新用户开hi拼团活动 —> 没找到 —> 3折优惠 参加首购活动 —> 3折优惠 老用户且非首购路线 找新用户开hi拼团活动 —> 可以找到 —> 2.4折优惠 找新用户开hi拼团活动 —> 没找到 —> 直接购买 —> 3折优惠 花了大概一个多小时认真写了这篇,也是想给自己刚上线的网站【zhaozhuji.info】做做推广,谢谢拜访! 其实做这个图也是得益于我的一个微信群”独立开发者”的,感谢小程序【魅力拍】作者徐玉丰,算也是给他做做推广,感谢他的启发! 原文地址:https://zhaozhuji.info/post/2019-aliyun-youhui-tips","categories":[{"name":"主机优惠","slug":"主机优惠","permalink":"http://www.cmlanche.com/categories/主机优惠/"}],"tags":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/tags/独立开发者/"},{"name":"找主机网","slug":"找主机网","permalink":"http://www.cmlanche.com/tags/找主机网/"},{"name":"阿里云主机优惠","slug":"阿里云主机优惠","permalink":"http://www.cmlanche.com/tags/阿里云主机优惠/"}]},{"title":"免费logo创建器launchaco","slug":"免费logo创建器launchaco.com","date":"2019-05-27T04:36:00.000Z","updated":"2019-05-27T08:25:31.000Z","comments":true,"path":"2019/05/27/免费logo创建器launchaco.com/","link":"","permalink":"http://www.cmlanche.com/2019/05/27/免费logo创建器launchaco.com/","excerpt":"今天发现一个超级好用,并且免费的logo创建器,launchaco.com,之前用过shapefactory.co,也很好用,但是贵的令人发指,一个做好的logo,需要44美金。而今天发现了launchaco.com,感觉发现了一块宝藏,忍不住想分享。好了,现在来复盘一下整个使用流程。","text":"今天发现一个超级好用,并且免费的logo创建器,launchaco.com,之前用过shapefactory.co,也很好用,但是贵的令人发指,一个做好的logo,需要44美金。而今天发现了launchaco.com,感觉发现了一块宝藏,忍不住想分享。好了,现在来复盘一下整个使用流程。 以我最近正在开发的产品:找主机 zhaozhuji.info为例 开始打开launchaco.com官网,选择logo菜单,点击Create Your Logo for Free 产品取名字注意只支持英文字母,不支持中文,这是唯一的瑕疵。而本身这个产品是建立在人工智能上的,中文不仅字体不支持,也不支持含义联想。 选字体没三种选一种字体,会让你选择四五次,它是期望发现你的字体爱好! 选择主题色下图中列出了6中不同的主题色,每一种都代表不同的含义,并且明确标出了,请注意看图的下面。 这里我选择第一种,Friendly, Loyal, & Strong,代表着友好、尊贵、强大。 每种主题色都给你配置好了3种色调,并且标明了适用的含义,是不是很方便啦! 这里我们选择第三种:Peaceful, Limitless, Tranquil, & Friendly 选择logo选择3个你喜欢的logo! 说真的,这个网站的logo真的超级棒,每个我都很喜欢! 选择搭配最后,系统会根据你的选择,给出一系列智能搭配。 保存搭配中,随便选一个,然后编辑,然后点击右上角保存! 下载 右上角箭头下载后,你会得到png和svg很多资源,想要的都有了,如下图: 总结有没有感觉很方便?关键是免费啊,基本类似的logo maker都收费,难得发现个免费的。后续我会继续发布有助于独立开发者产品开发的工具,关注我公众号及时提醒。","categories":[{"name":"独立开发者必备产品帮助工具","slug":"独立开发者必备产品帮助工具","permalink":"http://www.cmlanche.com/categories/独立开发者必备产品帮助工具/"}],"tags":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/tags/独立开发者/"},{"name":"logo创建器","slug":"logo创建器","permalink":"http://www.cmlanche.com/tags/logo创建器/"}]},{"title":"独立开发者的觉悟","slug":"独立开发者必须要有付出成倍努力才能成功的觉悟","date":"2019-05-20T04:44:55.000Z","updated":"2019-05-20T05:17:58.000Z","comments":true,"path":"2019/05/20/独立开发者必须要有付出成倍努力才能成功的觉悟/","link":"","permalink":"http://www.cmlanche.com/2019/05/20/独立开发者必须要有付出成倍努力才能成功的觉悟/","excerpt":"经常会做梦,我能躺着挣钱吗? 作为一穷二白的白丁来讲,这只能做梦了。 做任何一个产品,都需要花心血去维护运营的。 不管这个产品是需要怎样的劳动,是轻工具型,还是重内容型,都需要长时间的专注努力,才可能达到你想要的成功。","text":"经常会做梦,我能躺着挣钱吗? 作为一穷二白的白丁来讲,这只能做梦了。 做任何一个产品,都需要花心血去维护运营的。 不管这个产品是需要怎样的劳动,是轻工具型,还是重内容型,都需要长时间的专注努力,才可能达到你想要的成功。 如果你发现你的产品更新迭代速度很慢,每次更新都要耗费很大的精力,那么请改进迭代流程,用技术手段提高更新效率。 如果你发现你的产品的访客如流水般,来也快去也快,那就请把产品做得更好,增强用户粘性,让用户一看到你的产品,就欢喜的不得了。小水管一般的流量,用竹篮子如何能装得下?请记住,这样的产品再好的推广也无济于事! 当然这是目前我出现的问题,所以我才得以轻松说出口,我也会努力做到这两条。 独立开发者应该要有觉悟,不管产品简单还是复杂,它就像你的孩子,必须得用心呵护和照料,必须的付出成倍的努力才能茁壮成长!","categories":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/categories/独立开发者/"}],"tags":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/tags/独立开发者/"},{"name":"觉悟","slug":"觉悟","permalink":"http://www.cmlanche.com/tags/觉悟/"}]},{"title":"购买阿里云遇到Permission denied的问题","slug":"购买阿里云遇到Permission-denied的问题","date":"2019-05-15T13:24:11.000Z","updated":"2019-05-15T13:33:49.000Z","comments":true,"path":"2019/05/15/购买阿里云遇到Permission-denied的问题/","link":"","permalink":"http://www.cmlanche.com/2019/05/15/购买阿里云遇到Permission-denied的问题/","excerpt":"最近我的新项目友链联盟youlianplus.com项目即将完成,准备拿到阿里云试试,在阿里云华北3购买了一台迷你服务器,1核1g40g硬盘,打算测试一下。 买了之后,准备用ssh登录进去,结果出现了个permission denied权限被拒绝的问题,详情如下:","text":"最近我的新项目友链联盟youlianplus.com项目即将完成,准备拿到阿里云试试,在阿里云华北3购买了一台迷你服务器,1核1g40g硬盘,打算测试一下。 买了之后,准备用ssh登录进去,结果出现了个permission denied权限被拒绝的问题,详情如下: chengmingdeMacBook-Pro:blog cmlanche$ ssh root@47.92.24.241The authenticity of host '47.92.24.241 (47.92.24.241)' can't be established.ECDSA key fingerprint is SHA256:PYBgUIuFIYcBUCzUGG0qLJDGH2At/dQ+zk+Q9tyX7+E.Are you sure you want to continue connecting (yes/no)? yesWarning: Permanently added '47.92.24.241' (ECDSA) to the list of known hosts.Permission denied (publickey).chengmingdeMacBook-Pro:blog cmlanche$ ssh root@47.92.24.241Permission denied (publickey).chengmingdeMacBook-Pro:blog cmlanche$ ssh root@47.92.24.241Permission denied (publickey). 意思是说,无法用publickey来登录服务器,那问题肯定出在服务器那边的配置上了,百度找了个解决办法,有效!https://blog.csdn.net/wtopps/article/details/79449920 幸好阿里云控制台界面有一个远程连接的控制,通过这个入口进去,可以直接登录。 进去后,注意要保存好连接码,这个码只会出现一次!就保存在你电脑中就好了。 关键步骤: vi /etc/ssh/sshd_config 修改PasswordAuthentication的值为yes,注意把签名的#去掉 PasswordAuthentication yes 上文连接说要重启httpd服务,其实没啥作用,咱们来硬的,直接重启服务器就好了!","categories":[{"name":"阿里云","slug":"阿里云","permalink":"http://www.cmlanche.com/categories/阿里云/"}],"tags":[{"name":"阿里云","slug":"阿里云","permalink":"http://www.cmlanche.com/tags/阿里云/"}]},{"title":"springboot项目部署到ubuntu18.04的过程,http跳转https,使用acme.sh安装证书","slug":"springboot-ubuntu-https","date":"2019-05-14T10:33:53.000Z","updated":"2019-05-14T11:49:32.000Z","comments":true,"path":"2019/05/14/springboot-ubuntu-https/","link":"","permalink":"http://www.cmlanche.com/2019/05/14/springboot-ubuntu-https/","excerpt":"最近开发的友链联盟项目即将”竣工”,准备拿到公有云Ubuntu是测试下,想要达到的理想状态有如下几点: 端口保持9090,发布spring boot的jar包,后台运行 nginx部署,强制https访问应用 使用acme.sh来生成、安装ssl证书 开启防火墙,打开80、443端口,关闭9090端口(避免直接访问9090打开应用)","text":"最近开发的友链联盟项目即将”竣工”,准备拿到公有云Ubuntu是测试下,想要达到的理想状态有如下几点: 端口保持9090,发布spring boot的jar包,后台运行 nginx部署,强制https访问应用 使用acme.sh来生成、安装ssl证书 开启防火墙,打开80、443端口,关闭9090端口(避免直接访问9090打开应用) 目前已经完成上面的要求,可以尝试访问http://sitefriendlinks.com、http://sitefriendlinks.com:9090,前者会强制跳转到https://sitefriendlinks.com,后者无法打开。 环境安装 nginx apt install nginx 安装后自动就启动了,你可以用如下命令进行开启和关闭: service nginx stopservice nginx startservice nginx restart # 重启 mysql apt install mysql-server 详情请看:https://www.jianshu.com/p/3821c2603b92 需要注意修改root密码: show databases; use mysql; update user set authentication_string=PASSWORD(\"yourpassword\") where user='root'; update user set plugin=\"mysql_native_password\"; flush privileges; quit; java 8 apt install openjdk-8-jre-headless 参考链接) 部署nginxserver { listen 80; server_name sitefriendlinks.com; rewrite ^(.*)$ https://$host$1 permanent;}server { listen 443; server_name sitefriendlinks.com; ssl on; ssl_certificate /etc/nginx/ssl/fullchain.cer; ssl_certificate_key /etc/nginx/ssl/sitefriendlinks.com.key; ssl_session_timeout 5m; ssl_protocols TLSv1; ssl_ciphers HIGH:!aNULL:!MD5; ssl_prefer_server_ciphers on; location / { proxy_pass http://localhost:9090/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Port $server_port; }} 使用acem.sh安装证书参考文档:https://github.com/Neilpang/acme.sh/wiki/%E8%AF%B4%E6%98%8E 关键步骤: 创建别名 alias acme.sh=~/.acme.sh/acme.sh 生成证书 acme.sh --issue -d mydomain.com --nginx 安装证书 acme.sh --installcert -d <domain>.com \\ --key-file /etc/nginx/ssl/<domain>.key \\ --fullchain-file /etc/nginx/ssl/fullchain.cer \\ --reloadcmd \"service nginx force-reload\" 注意,你需要手动创建/etc/nginx/ssl目录,不如上面的安装命令会报路径错误 此时,你已经能正常用https打开你的网站了 打开防火墙Ubuntu的防火墙命令是ufw,参考:https://www.cnblogs.com/yuanlipu/p/7103740.html 先查询下防火墙是否打开: sudo ufw status 如果没打开,则开启防火墙 sudo ufw enable 默认情况下,防火墙是会把所有端口都关闭的,不允许外界访问,但是我们要做三件事:开启80、443端口,关闭9090端口 sudo ufw allow 80sudo ufw allow 443sudo ufw deny 9090 此时,咱们的服务器会变的非常安全,注意哦,因为没打开3306端口,所以你的数据库是无法远程访问的,如需要则打开3306端口即可。 后台运行spring boot工程把打包好的jar包发到服务器(通常用scp命令即可),然后执行如下命令即可后台启动: nohup java -jar sitefriendlinks.jar >> ./output.log 2>&1 & ok,部署完成! by cmlanche.com","categories":[{"name":"部署","slug":"部署","permalink":"http://www.cmlanche.com/categories/部署/"}],"tags":[{"name":"springboot","slug":"springboot","permalink":"http://www.cmlanche.com/tags/springboot/"},{"name":"https","slug":"https","permalink":"http://www.cmlanche.com/tags/https/"},{"name":"ssl","slug":"ssl","permalink":"http://www.cmlanche.com/tags/ssl/"},{"name":"ubuntu","slug":"ubuntu","permalink":"http://www.cmlanche.com/tags/ubuntu/"}]},{"title":"图文详解如何修改git已提交记录的邮箱?","slug":"如何修改git已提交记录的邮箱?","date":"2019-05-13T03:26:12.000Z","updated":"2019-05-13T04:51:21.000Z","comments":true,"path":"2019/05/13/如何修改git已提交记录的邮箱?/","link":"","permalink":"http://www.cmlanche.com/2019/05/13/如何修改git已提交记录的邮箱?/","excerpt":"有时候,公司提交的代码必须使用公司邮箱,而你误操作,直接把自己个人邮箱提交上去了,此时你就会遇到这样的需求:如何修改git已提交的邮箱? 而这个需求对于新手来说,往往要花费半天的时间才能理解修改过程,简直太傻比了,所以我这里做一个详细的文档来帮助自己和你搞清楚这个流程。尤其要理解变基,它不是一个命令执行就完成了,而是一连串命令的组合。","text":"有时候,公司提交的代码必须使用公司邮箱,而你误操作,直接把自己个人邮箱提交上去了,此时你就会遇到这样的需求:如何修改git已提交的邮箱? 而这个需求对于新手来说,往往要花费半天的时间才能理解修改过程,简直太傻比了,所以我这里做一个详细的文档来帮助自己和你搞清楚这个流程。尤其要理解变基,它不是一个命令执行就完成了,而是一连串命令的组合。 变基git rebase -i 执行后,会打开最近一条的提交记录,当然上面的命令可以指定某一条记录,命令是: git rebase -i "your commit id" 对于sourcetree用户来说,commit id是SHA-1,可以右键某条提交记录,选择菜单”复制SHA-1到剪贴板”,如下图: 变基rebase命令执行完成后,会打印类似如下内容: pick bd81df5 更新API# Rebase abcb9d0..bd81df5 onto abcb9d0 (1 command)## Commands:# p, pick = use commit# r, reword = use commit, but edit the commit message# e, edit = use commit, but stop for amending# s, squash = use commit, but meld into previous commit# f, fixup = like \"squash\", but discard this commit's log message# x, exec = run command (the rest of the line) using shell# d, drop = remove commit## These lines can be re-ordered; they are executed from top to bottom.## If you remove a line here THAT COMMIT WILL BE LOST.## However, if you remove everything, the rebase will be aborted.## Note that empty commits are commented out 新手往往会一脸懵逼,不止所错,此时是在rebase的过程中,你需要把pick改为edit,如下: edit bd81df5 更新API# Rebase abcb9d0..bd81df5 onto abcb9d0 (1 command)## Commands:# p, pick = use commit# r, reword = use commit, but edit the commit message# e, edit = use commit, but stop for amending# s, squash = use commit, but meld into previous commit# f, fixup = like \"squash\", but discard this commit's log message# x, exec = run command (the rest of the line) using shell# d, drop = remove commit## These lines can be re-ordered; they are executed from top to bottom.## If you remove a line here THAT COMMIT WILL BE LOST.## However, if you remove everything, the rebase will be aborted.## Note that empty commits are commented out 更改完成后,保存并退出vi编辑器::wq 然后会打印这样的消息: chengmingdeMacBook-Pro:server cmlanche$ git rebase -i \"abcb9d0d1e99cdad25d8d08119e494436b000e59\"Stopped at bd81df5... 更新APIYou can amend the commit now, with git commit --amend Once you are satisfied with your changes, run git rebase --continuechengmingdeMacBook-Pro:server cmlanche$ 给大家先科普一下这个amend英文单词,是修改的意思,对我来说好陌生,为啥不用change或者fix之类的。 上面的信息说了,如果你要amend,也就是要修改这个提交的话,那么用 git commit --amend 如果你对这次修改满意的话,就用如下命令结束此次变基 git rebase --continue 重置账户邮箱信息我们当然要修改啦,那么执行如下命令,重置提交的账户信息: git commit --amend --author=\"cmlanche <1204833748@qq.com>\" --no-edit 同事,要注意你的sourcetree,出现了新情况! 我们可以看到一个新的提交,并且,邮箱账号都经过了修改,如果你去掉--no-edit还可以修改commit message,也就是图中的”更新API”,举栗子吧,我可以继续用amend修改此次变基 git commit --amend --author=\"cmlanche <1204833748@qq.com>\" 保存退出vi编辑器,看sourcetree咋样了: 真的很完美,接下来就是合并了,退出变基。 退出变基git rebase --continue 在控制台中打印如上命令退出变基,我们看到退出变基也就是使用最新的修改了,就一条分支了。 chengmingdeMacBook-Pro:server cmlanche$ git rebase --continueSuccessfully rebased and updated refs/heads/bulma. 最后总结一下变基真的很有用,他不是一条命令搞定的,是一个过程,就像变成中打开了一个输入流,最后用完你得关闭输入流一样。 通过变基你可以轻松实现提交信息的任意重新修改!","categories":[{"name":"日常技术","slug":"日常技术","permalink":"http://www.cmlanche.com/categories/日常技术/"}],"tags":[{"name":"git","slug":"git","permalink":"http://www.cmlanche.com/tags/git/"}]},{"title":"改造思寒的AppCrawler,使其支持Appium最新版本","slug":"改造思寒的AppiumCrawler,使其支持Appium最新版本","date":"2019-05-08T03:19:06.000Z","updated":"2019-05-08T05:01:35.000Z","comments":true,"path":"2019/05/08/改造思寒的AppiumCrawler,使其支持Appium最新版本/","link":"","permalink":"http://www.cmlanche.com/2019/05/08/改造思寒的AppiumCrawler,使其支持Appium最新版本/","excerpt":"思绪最近完成了自定义Appium的需求,让Appium内置了自动识别权限框并点击的能力,参考我的知乎专栏:自定义Appium之路 但遇到另外一个问题,就是testerhome思寒开发的AppiumCrawler并不支持Appium最新版,也就是当前的1.12版本,只支持到1.8版本,让人很是捉急。 本来是想基于1.8重新自定义一个appium,但是发现这个appium实在太老了,下载下来编译都有各种问题,况且后续还要自定义appium-android-driver,appium-uiautomator2-driver和appium-uiaumator2-server,工作量至少得3天,太费时间。 索性,我来替思寒把AppCrawler来升级一下,让它支持最新appium。 刚开始觉得挺难的,毕竟我对scala只略知一二,编译打包方面还要学,但事后发现,这个工程做的确实不错,升级改造过程比预计的要简单很多,这里要给思寒大佬一个赞👍!","text":"思绪最近完成了自定义Appium的需求,让Appium内置了自动识别权限框并点击的能力,参考我的知乎专栏:自定义Appium之路 但遇到另外一个问题,就是testerhome思寒开发的AppiumCrawler并不支持Appium最新版,也就是当前的1.12版本,只支持到1.8版本,让人很是捉急。 本来是想基于1.8重新自定义一个appium,但是发现这个appium实在太老了,下载下来编译都有各种问题,况且后续还要自定义appium-android-driver,appium-uiautomator2-driver和appium-uiaumator2-server,工作量至少得3天,太费时间。 索性,我来替思寒把AppCrawler来升级一下,让它支持最新appium。 刚开始觉得挺难的,毕竟我对scala只略知一二,编译打包方面还要学,但事后发现,这个工程做的确实不错,升级改造过程比预计的要简单很多,这里要给思寒大佬一个赞👍! 改造先看看出了什么问题?我开启最新的appium: appium 执行appcrawler测试: java -jar appcrawler-2.1.3.jar -a ApiDemos-debug.apk 执行过程中在appium和appcrawler两端都报错: [HTTP] <-- GET /wd/hub/session/efdf97d8-cf46-4ffb-b2d4-7d8feb931cee/window/rect 200 7 ms - 50[HTTP] [HTTP] --> POST /wd/hub/session/efdf97d8-cf46-4ffb-b2d4-7d8feb931cee/execute/sync[HTTP] {\"script\":\"var source = document.documentElement.outerHTML; \\nif (!source) { source = new XMLSerializer().serializeToString(document); }\\nreturn source;\",\"args\":[]}[debug] [W3C (efdf97d8)] Calling AppiumDriver.execute() with args: [\"var source = document.documentElement.outerHTML; \\nif (!source) { source = new XMLSerializer().serializeToString(document); }\\nreturn source;\",[],\"efdf97d8-cf46-4ffb-b2d4-7d8feb931cee\"][debug] [W3C (efdf97d8)] Encountered internal error running command: NotImplementedError: Method is not implemented[debug] [W3C (efdf97d8)] at AndroidDriver.extensions.execute (/usr/local/lib/node_modules/appium/node_modules/appium-android-driver/lib/commands/execute.js:12:9)[debug] [W3C (efdf97d8)] at curCommandCancellable._bluebird.default.resolve.then (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/lib/basedriver/driver.js:291:18)[debug] [W3C (efdf97d8)] at tryCatcher (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/util.js:26:23)[debug] [W3C (efdf97d8)] at Promise._settlePromiseFromHandler (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/promise.js:510:31)[debug] [W3C (efdf97d8)] at Promise._settlePromiseAt (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/promise.js:584:18)[debug] [W3C (efdf97d8)] at Promise._settlePromiseAtPostResolution (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/promise.js:248:10)[debug] [W3C (efdf97d8)] at Async._drainQueue (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/async.js:128:12)[debug] [W3C (efdf97d8)] at Async._drainQueues (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/async.js:133:10)[debug] [W3C (efdf97d8)] at Immediate.Async.drainQueues (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/async.js:15:14)[debug] [W3C (efdf97d8)] at runCallback (timers.js:705:18)[debug] [W3C (efdf97d8)] at tryOnImmediate (timers.js:676:5)[debug] [W3C (efdf97d8)] at processImmediate (timers.js:658:5)[HTTP] <-- POST /wd/hub/session/efdf97d8-cf46-4ffb-b2d4-7d8feb931cee/execute/sync 405 11 ms - 1600[HTTP] [HTTP] --> POST /wd/hub/session/efdf97d8-cf46-4ffb-b2d4-7d8feb931cee/execute/sync[HTTP] {\"script\":\"var source = document.documentElement.outerHTML; \\nif (!source) { source = new XMLSerializer().serializeToString(document); }\\nreturn source;\",\"args\":[]}[debug] [W3C (efdf97d8)] Calling AppiumDriver.execute() with args: [\"var source = document.documentElement.outerHTML; \\nif (!source) { source = new XMLSerializer().serializeToString(document); }\\nreturn source;\",[],\"efdf97d8-cf46-4ffb-b2d4-7d8feb931cee\"][debug] [W3C (efdf97d8)] Encountered internal error running command: NotImplementedError: Method is not implemented[debug] [W3C (efdf97d8)] at AndroidDriver.extensions.execute (/usr/local/lib/node_modules/appium/node_modules/appium-android-driver/lib/commands/execute.js:12:9)[debug] [W3C (efdf97d8)] at curCommandCancellable._bluebird.default.resolve.then (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/lib/basedriver/driver.js:291:18)[debug] [W3C (efdf97d8)] at tryCatcher (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/util.js:26:23)[debug] [W3C (efdf97d8)] at Promise._settlePromiseFromHandler (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/promise.js:510:31)[debug] [W3C (efdf97d8)] at Promise._settlePromiseAt (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/promise.js:584:18)[debug] [W3C (efdf97d8)] at Promise._settlePromiseAtPostResolution (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/promise.js:248:10)[debug] [W3C (efdf97d8)] at Async._drainQueue (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/async.js:128:12)[debug] [W3C (efdf97d8)] at Async._drainQueues (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/async.js:133:10)[debug] [W3C (efdf97d8)] at Immediate.Async.drainQueues (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/async.js:15:14)[debug] [W3C (efdf97d8)] at runCallback (timers.js:705:18)[debug] [W3C (efdf97d8)] at tryOnImmediate (timers.js:676:5)[debug] [W3C (efdf97d8)] at processImmediate (timers.js:658:5)[HTTP] <-- POST /wd/hub/session/efdf97d8-cf46-4ffb-b2d4-7d8feb931cee/execute/sync 405 4 ms - 1600[HTTP] [HTTP] --> POST /wd/hub/session/efdf97d8-cf46-4ffb-b2d4-7d8feb931cee/execute/sync[HTTP] {\"script\":\"var source = document.documentElement.outerHTML; \\nif (!source) { source = new XMLSerializer().serializeToString(document); }\\nreturn source;\",\"args\":[]}[debug] [W3C (efdf97d8)] Calling AppiumDriver.execute() with args: [\"var source = document.documentElement.outerHTML; \\nif (!source) { source = new XMLSerializer().serializeToString(document); }\\nreturn source;\",[],\"efdf97d8-cf46-4ffb-b2d4-7d8feb931cee\"][debug] [W3C (efdf97d8)] Encountered internal error running command: NotImplementedError: Method is not implemented[debug] [W3C (efdf97d8)] at AndroidDriver.extensions.execute (/usr/local/lib/node_modules/appium/node_modules/appium-android-driver/lib/commands/execute.js:12:9)[debug] [W3C (efdf97d8)] at curCommandCancellable._bluebird.default.resolve.then (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/lib/basedriver/driver.js:291:18)[debug] [W3C (efdf97d8)] at tryCatcher (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/util.js:26:23)[debug] [W3C (efdf97d8)] at Promise._settlePromiseFromHandler (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/promise.js:510:31)[debug] [W3C (efdf97d8)] at Promise._settlePromiseAt (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/promise.js:584:18)[debug] [W3C (efdf97d8)] at Promise._settlePromiseAtPostResolution (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/promise.js:248:10)[debug] [W3C (efdf97d8)] at Async._drainQueue (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/async.js:128:12)[debug] [W3C (efdf97d8)] at Async._drainQueues (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/async.js:133:10)[debug] [W3C (efdf97d8)] at Immediate.Async.drainQueues (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/node_modules/bluebird/js/main/async.js:15:14)[debug] [W3C (efdf97d8)] at runCallback (timers.js:705:18)[debug] [W3C (efdf97d8)] at tryOnImmediate (timers.js:676:5)[debug] [W3C (efdf97d8)] at processImmediate (timers.js:658:5)[HTTP] <-- POST /wd/hub/session/efdf97d8-cf46-4ffb-b2d4-7d8feb931cee/execute/sync 405 9 ms - 1600[HTTP] 2019-05-08 11:34:01 WARN [AppiumClient.$anonfun$getPageSource$1.340] get page source error2019-05-08 11:34:01 WARN [Crawler.refreshPage.562] page source get fail, go back2019-05-08 11:34:01 INFO [Crawler.setElementAction.660] set action to back2019-05-08 11:34:01 INFO [Crawler.runStartupScript.236] first refresh2019-05-08 11:34:01 INFO [Crawler.doElementAction.976] current element = _startupActions-Start-02019-05-08 11:34:01 INFO [Crawler.doElementAction.977] current index = 02019-05-08 11:34:01 INFO [Crawler.doElementAction.978] current action = 2019-05-08 11:34:01 INFO [Crawler.doElementAction.979] current url = 2019-05-08 11:34:01 INFO [Crawler.doElementAction.980] current xpath = startupActions-Start-02019-05-08 11:34:01 INFO [Crawler.doElementAction.981] current tag path = _startupActions-Start-02019-05-08 11:34:01 INFO [Crawler.doElementAction.982] current file name = _2019-05-08 11:34:01 INFO [Crawler.doElementAction.983] current uri = startupActions-Start-0 startupActionsException in thread \"main\" java.util.NoSuchElementException: last of empty ListBuffer at scala.collection.mutable.ListBuffer.last(ListBuffer.scala:401) at com.testerhome.appcrawler.DataRecord.last(DataRecord.scala:40) at com.testerhome.appcrawler.Crawler.doElementAction(Crawler.scala:985) at com.testerhome.appcrawler.Crawler.runStartupScript(Crawler.scala:238) at com.testerhome.appcrawler.Crawler.start(Crawler.scala:152) at com.testerhome.appcrawler.AppCrawler$.startCrawl(AppCrawler.scala:344) at com.testerhome.appcrawler.AppCrawler$.parseParams(AppCrawler.scala:312) at com.testerhome.appcrawler.AppCrawler$.main(AppCrawler.scala:92) at com.testerhome.appcrawler.AppCrawler.main(AppCrawler.scala) 分析原因我们看到Appcrawler中报了个get page source error,我们追查appcrawler的代码发现是在这里报错的: override def getPageSource(): String = { currentPageSource=null currentPageDom=null log.info(\"start to get page source from appium\") //获取页面结构, 最多重试3次 1 to 3 foreach (i => { asyncTask(20)(driver.getPageSource) match { case Some(v) => { log.trace(\"get page source success\") //todo: wda返回的不是标准的xml val xmlStr=v match { case json if json.trim.charAt(0)=='{' => { log.info(\"json format maybe from wda\") DataObject.fromJson[Map[String, String]](v).getOrElse(\"value\", \"\") } case xml if xml.trim.charAt(0)=='<' =>{ log.info(\"xml format \") xml } } Try(XPathUtil.toDocument(xmlStr)) match { case Success(v) => { currentPageDom = v } case Failure(e) => { log.warn(\"convert to xml fail\") log.warn(xmlStr) currentPageDom=null } } currentPageSource = XPathUtil.toPrettyXML(xmlStr) return currentPageSource } case None => { log.warn(\"get page source error\") } } }) currentPageSource } 我们看appium的源码,发现在appcrawler给我们的appium传递了一段js代码来获取控件树 {\"script\":\"var source = document.documentElement.outerHTML; \\nif (!source) { source = new XMLSerializer().serializeToString(document); }\\nreturn source;\",\"args\":[]} 然而,我们的appium代码对get page source这个功能接口做了限制,源码在appium-android-driver中的lib/execute.js中: extensions.execute = async function execute (script, args) { if (script.match(/^mobile:/)) { script = script.replace(/^mobile:/, '').trim(); return await this.executeMobile(script, _.isArray(args) ? args[0] : args); } throw new errors.NotImplementedError();}; 我们可以看到,这里抛出异常了,说明可能是接口变动了,那么我这里有个大胆猜想,appcrawler所使用的java-client过老。 解决问题ok,立马开始行动,替换上最新的java-client,也就是7.0,同时我们使用最新的appcrawler2.4.0 <dependency> <groupId>com.github.appium</groupId> <artifactId>java-client</artifactId> <version>v7.0.0</version></dependency> 同时添加对应的仓库: <repository> <id>jitpack.io</id> <url>https://jitpack.io</url></repository> 打包 mvn assembly:assembly 打包完成,会再target目录下生成一个完整依赖的jar包:appcrawler-2.4.0-jar-with-dependencies.jar 重新执行,你会发现美妙的事情发生,最新appium完美支持! 提出质疑上面的完美支持,是不是因为我更新了最新版本2.4.0,而不是使用的最开始的2.1.3版本呢? 有可能!!! 撤销修改,直接打包2.4.0,执行测试看是否正常。 结果就是:最开始的get page source问题没了,但出现另外一个问题: 2019-05-08 11:55:46 INFO [AppiumClient.30.initLog] already existException in thread \"main\" scala.MatchError: [app, appium, deviceName, dontStopAppOnReset, fullReset, noReset] (of class java.util.Collections$UnmodifiableSet) at com.testerhome.appcrawler.driver.AppiumClient.appium(AppiumClient.scala:94) at com.testerhome.appcrawler.driver.AppiumClient.<init>(AppiumClient.scala:40) at com.testerhome.appcrawler.Crawler.setupAppium(Crawler.scala:277) at com.testerhome.appcrawler.Crawler.restart(Crawler.scala:221) at com.testerhome.appcrawler.Crawler.crawl(Crawler.scala:201) at com.testerhome.appcrawler.Crawler.start(Crawler.scala:170) at com.testerhome.appcrawler.AppCrawler$.startCrawl(AppCrawler.scala:323) at com.testerhome.appcrawler.AppCrawler$.parseParams(AppCrawler.scala:291) at com.testerhome.appcrawler.AppCrawler$.main(AppCrawler.scala:91) at com.testerhome.appcrawler.AppCrawler.main(AppCrawler.scala)chengmingdeMacBook-Pro:AppCrawler cmlanche$ [debug] [W3C (dd159942)] Encountered internal error running command: NoSuchDriverError: A session is either terminated or not started[debug] [W3C (dd159942)] at asyncHandler (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/lib/protocol/protocol.js:298:15)[debug] [W3C (dd159942)] at asyncHandler (/usr/local/lib/node_modules/appium/node_modules/appium-base-driver/lib/protocol/protocol.js:489:15)[debug] [W3C (dd159942)] at Layer.handle [as handle_request] (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/layer.js:95:5)[debug] [W3C (dd159942)] at next (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/route.js:137:13)[debug] [W3C (dd159942)] at Route.dispatch (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/route.js:112:3)[debug] [W3C (dd159942)] at Layer.handle [as handle_request] (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/layer.js:95:5)[debug] [W3C (dd159942)] at /usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:281:22[debug] [W3C (dd159942)] at param (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:354:14)[debug] [W3C (dd159942)] at param (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:365:14)[debug] [W3C (dd159942)] at Function.process_params (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:410:3)[debug] [W3C (dd159942)] at next (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:275:10)[debug] [W3C (dd159942)] at logger (/usr/local/lib/node_modules/appium/node_modules/morgan/index.js:144:5)[debug] [W3C (dd159942)] at Layer.handle [as handle_request] (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/layer.js:95:5)[debug] [W3C (dd159942)] at trim_prefix (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:317:13)[debug] [W3C (dd159942)] at /usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:284:7[debug] [W3C (dd159942)] at Function.process_params (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:335:12)[debug] [W3C (dd159942)] at next (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:275:10)[debug] [W3C (dd159942)] at jsonParser (/usr/local/lib/node_modules/appium/node_modules/body-parser/lib/types/json.js:110:7)[debug] [W3C (dd159942)] at Layer.handle [as handle_request] (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/layer.js:95:5)[debug] [W3C (dd159942)] at trim_prefix (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:317:13)[debug] [W3C (dd159942)] at /usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:284:7[debug] [W3C (dd159942)] at Function.process_params (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:335:12)[debug] [W3C (dd159942)] at next (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:275:10)[debug] [W3C (dd159942)] at Layer.handle [as handle_request] (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/layer.js:91:12)[debug] [W3C (dd159942)] at trim_prefix (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:317:13)[debug] [W3C (dd159942)] at /usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:284:7[debug] [W3C (dd159942)] at Function.process_params (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:335:12)[debug] [W3C (dd159942)] at next (/usr/local/lib/node_modules/appium/node_modules/express/lib/router/index.js:275:10)[HTTP] <-- GET /wd/hub/session/dd159942-ed6d-411c-8dcc-b43d7fc26284/source 404 5 ms - 3173 还是错的! 那就用我的最新java-client 7.0吧,重新执行一次完整的测试,发现没有任何问题!","categories":[{"name":"Appium","slug":"Appium","permalink":"http://www.cmlanche.com/categories/Appium/"}],"tags":[{"name":"appium","slug":"appium","permalink":"http://www.cmlanche.com/tags/appium/"},{"name":"appiumcrawler","slug":"appiumcrawler","permalink":"http://www.cmlanche.com/tags/appiumcrawler/"}]},{"title":"开源项目filepond的独立自由之路:城市套路深","slug":"开源项目filepond的独立自由之路:城市套路深","date":"2019-04-20T01:19:41.000Z","updated":"2019-05-08T05:01:58.000Z","comments":true,"path":"2019/04/20/开源项目filepond的独立自由之路:城市套路深/","link":"","permalink":"http://www.cmlanche.com/2019/04/20/开源项目filepond的独立自由之路:城市套路深/","excerpt":"微信原文更清晰:https://mp.weixin.qq.com/s/dv39XvvDNlDqvSgrhN2f7A 最近一直在做一个有关独立开发者友链联盟的插件项目,在做到上传头像时,满网络找最好的头像上传、剪裁插件,最后终于找到了filepond,满心欢喜的认为,这么好的东西居然是开源项目,简直牛的不要不要的。","text":"微信原文更清晰:https://mp.weixin.qq.com/s/dv39XvvDNlDqvSgrhN2f7A 最近一直在做一个有关独立开发者友链联盟的插件项目,在做到上传头像时,满网络找最好的头像上传、剪裁插件,最后终于找到了filepond,满心欢喜的认为,这么好的东西居然是开源项目,简直牛的不要不要的。 这个filepond到底怎么好呢?给大家看俩图,或许能了解,她如此美丽简洁,真漂亮! 看到效果,是不是很激动?是不是特别想要? 想必肯定是了,我也是啊。 立马,我就开始准备继承她了。。。 集成中,才发现,filepond是一个设计也精美的插件,它由主filepond工程和几个不同功能的插件组成,你可以根据实际需求来添加插件,默认主工程可以直接使用,但只有基本的上传功能。插件如下: File encode File rename File size validation File type validation File metadata File poster Image editor Image size validation Image preview Image crop Image resize Image transform Image EXIF orientation 并且作者pqina还适配了不同的前端框架: React Vue jQuery Angular Angular 1 Ember 真的可以说是用心良苦啊!不得不佩服。 说这么多,只是说它有多牛逼,还没说到独立开发者上来,这个也是我集成filepond的图片编辑功能之后才了解到的,也就是上面中第二章图展示的功能。 我擦,我好不容易把7个插件的js、css引入进来,结果还有个一Doka的东西!!这是什么鬼啊??? 刚开始还以为是另外一个项目没引入,赶紧去filepond的README中找。。。最后找到这样几句话: 打开Doka一看,原来TM是个收费项目啊,最牛逼最亮点的东西居然收费,我TM。。。。。服了,后来一转想,人家东西做的这么好,独立开发如此不容易,收个费怎么了,想到这,我就特想买了。 不过,人家价格方案比较贵,最低一年要79美金,而且不适用于saas,所以它这个项目,一开始就把目标人群定位在了企业级的用户上,很明确。 我是买不起你了,要是以后独立开发者友链联盟这个项目能做起来,做大了,我肯定买! 自此,我是深感套路深啊,但就算套路深,我却很喜欢这种感觉,毕竟人家很挣钱。 继续了解下背后的作者pqina吧,个人网站是:pqina.nl,个人网站上没有写任何有关自己的事情,基本都是自己的几个关键的独立产品,FilePond、Doka、Flip和Soon,但是公布的推特,当发现你很崇拜一个人的时候,你就特别想了解他的一切,那就fq去推特看看吧。 推特上显示他来自荷兰(Netherlands),主要给WordPress、jQuery、React、Angular设计高质量的Web插件,并在推特上发布有趣的web相关的新闻。 信息就这点,现在来复盘一下filepond的盈利模式。 创建开源项目filepond,开源绝大部分基础功能 在更高层次的图片编辑功能上做限制,开发出Doke.js的付费项目 filepond负责引流,filepond提供了很好的插件模式,虽然你也可能开发出类似的插件,但终究门槛太高,基本上如果你要图片编辑功能,那你是必须购买Doka.js的 但,就是因为pgina把这块的功能做的足够好,并且市面上并没有比他更好的,所以才有这么大的自信做收费,还不便宜。 对我们独立开发者的反思: 有时候我们并不需要做太多产品,做一款产品做到极致那抵得过”千军万马” 开源基础功能,高纬度功能收费,定价合理,这种盈利模式,独立开发者可以考虑一下 好的有价值的产品,能够做到自我传播,就像filepond一样,让我使劲夸它,要是一个烂东西,看都懒得看。所以独立开发者做的东西,一定要有很高的价值才行,一定不要敷衍你的用户! 还有一点要分享就是关于谷歌搜索引擎,它的目标是让你找到对你最有价值的东西,只要你的东西做的够好,总会把这种价值带给用户,就像我找filepond,所以谷歌SEO不仅仅是字面的意思,还是背后一个价值的意思。 关注我的订阅号:","categories":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/categories/独立开发者/"}],"tags":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/tags/独立开发者/"},{"name":"filepond","slug":"filepond","permalink":"http://www.cmlanche.com/tags/filepond/"}]},{"title":"centos7开放端口命令","slug":"centos7开放端口命令","date":"2019-04-19T09:22:36.000Z","updated":"2019-05-14T08:52:50.000Z","comments":true,"path":"2019/04/19/centos7开放端口命令/","link":"","permalink":"http://www.cmlanche.com/2019/04/19/centos7开放端口命令/","excerpt":"","text":"亲测有效! centos查询端口是不是开放的firewall-cmd --permanent --query-port=8080/tcp 添加对外开放端口firewall-cmd --permanent --add-port=8080/tcp 重启防火墙firewall-cmd --reload","categories":[{"name":"日常技术","slug":"日常技术","permalink":"http://www.cmlanche.com/categories/日常技术/"}],"tags":[{"name":"centos7","slug":"centos7","permalink":"http://www.cmlanche.com/tags/centos7/"}]},{"title":"健康才是福,工作和生活的心态要平衡","slug":"健康才是福,工作和生活的心态要平衡","date":"2019-04-14T13:02:44.000Z","updated":"2019-04-15T06:15:39.000Z","comments":true,"path":"2019/04/14/健康才是福,工作和生活的心态要平衡/","link":"","permalink":"http://www.cmlanche.com/2019/04/14/健康才是福,工作和生活的心态要平衡/","excerpt":"今天一早8点,肾结石突发,疼死我了,急忙跑到医院打针吃药,下午终于有点好转,感谢生命!但是基本上一坐下来就疼,只能站着。","text":"今天一早8点,肾结石突发,疼死我了,急忙跑到医院打针吃药,下午终于有点好转,感谢生命!但是基本上一坐下来就疼,只能站着。 可能很多人都有结石,体验结石带来的痛苦也有不少吧,那种疼痛感,很容易让人产生悲观厌世的感觉,终于不那么疼了,才想到,原来此前的每一刻那坐下来的舒适,都是幸福! 但是幸福如此在你身边,你却不以为意,感觉有无数的欲望和野心在等着你完成,马云说,能够996是种幸福,我说健康才是福,现在的每一刻每一秒都是幸福,工作是工作,生活是生活,都要有,而且要有滋有味,不要总是忙忙碌碌,不知所终。有野心没问题,但是切忌急躁,慢慢来。 工作和生活的心态要平衡,其实现在就是幸福,可以想太多,但不可以着急,不要给自己太大的压力。 鉴于结石的情况,给自己立几个规矩: 晚上11点之前睡觉 少吃豆制品,少吃鸡蛋,多吃蔬菜 多喝水,多运动 此文,你我诸君共勉!","categories":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/categories/独立开发者/"}],"tags":[{"name":"生活","slug":"生活","permalink":"http://www.cmlanche.com/tags/生活/"}]},{"title":"gradle构建appium-uiautomator2-server出现gradle版本不匹配","slug":"gradle构建appium-uiautomator2-server出现gradle版本不匹配","date":"2019-04-12T04:31:31.000Z","updated":"2019-04-15T06:14:06.000Z","comments":true,"path":"2019/04/12/gradle构建appium-uiautomator2-server出现gradle版本不匹配/","link":"","permalink":"http://www.cmlanche.com/2019/04/12/gradle构建appium-uiautomator2-server出现gradle版本不匹配/","excerpt":"在克隆代码:appium-uiautomator2-server后,按照说明文档README.md中所说,进行构建 gradle clean assembleE2ETestDebug assembleE2ETestDebugAndroidTest","text":"在克隆代码:appium-uiautomator2-server后,按照说明文档README.md中所说,进行构建 gradle clean assembleE2ETestDebug assembleE2ETestDebugAndroidTest 出现错误: FAILURE: Build failed with an exception.* Where:Build file '/Users/cmlanche/sourcetree/appium/appium-uiautomator2-server/app/build.gradle' line: 14* What went wrong:A problem occurred evaluating project ':app'.> Failed to apply plugin [id 'com.android.application'] > Minimum supported Gradle version is 4.10.1. Current version is 4.1. If using the gradle wrapper, try editing the distributionUrl in /Users/cmlanche/sourcetree/appium/appium-uiautomator2-server/gradle/wrapper/gradle-wrapper.properties to gradle-4.10.1-all.zip* Try:Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output.* Get more help at https://help.gradle.orgBUILD FAILED in 1s 错误中说,当前工程最低要求的gradle版本是4.10.1,而当前的版本是4.1。 当时我并不知道,或者说已经忘记gradle是怎么安装的,索性做了两件事: 使用gradle -v查询当前的版本号,确实显示的是4.1 使用brew upgrade gradle升级我的gradle,果不其然,我的gradle确实是用brew来管理的(各个系统不一样哈,我的是mac,不仅仅是可能操作系统的差异,也有可能是使用gradle也有差异,比如你可以用本地gradle来编译) chengmingdeMacBook-Pro:appium-uiautomator2-server cmlanche$ brew upgrade gradleUpdating Homebrew...==> Auto-updated Homebrew!Updated 2 taps (dart-lang/dart and homebrew/core).==> New Formulaealiyun-cli breezy embree frpc frps lazygit ospray volt zabbix-cli==> Updated Formulaego ✔ direnv grafana mariadb simple-scanhugo ✔ dita-ot gromacs maxwell smimesignswagger-codegen ✔ django-completion groonga mesa snapcraftwget ✔ dnscrypt-proxy gsoap meson snortace docfx gssdp metabase sourcekittenalgernon docker gtk+3 micronaut sshguardansible docker-completion gupnp mmseqs2 ssllabs-scananyenv docker-compose-completion gupnp-av mps-youtube stepapache-arrow docker-credential-helper-ecr gupnp-tools nginx streamlinkapache-arrow-glib doctl hadolint nsd stunnelapache-flink doitlive haproxy opam svgoapache-spark dynare hatari open-mpi swiftformatapr easyengine helmfile openjpeg syncthingasciidoctor embulk ispc openttd taskellasdf erlang istioctl pacapt tbbatlassian-cli ethereum jemalloc paket telegrafatomist-cli exploitdb jenkins pandoc terraform_landscapeaws-okta faas-cli jenkins-lts pegtl tguiaws-sdk-cpp ffmpeg jfrog-cli-go php tippecanoeawscli file-roller jmeter php-code-sniffer tmuxazure-cli fn juju php@7.1 topgradebinaryen fonttools kafkacat php@7.2 ttfautohintbitrise freetds kitchen-sync phpunit typescriptcake futhark krakend planck unboundcarthage fx kube-aws pmd ungitcertbot gdcm kubecfg postgis v8cfssl get_iplayer ledger pre-commit valacgal gitbucket libcroco prometheus vert.xcheckstyle gitlab-runner libdazzle prototool vimcitus gjs libnotify pulumi vte3cmark glib-networking libphonenumber putty winecollector-sidecar gmic librealsense pyenv wireguard-gocomposer gmsh libsoup rakudo-star wireguard-toolsconan gnu-getopt libvirt riff xmakeconfluent-oss gnumeric libxlsxwriter rke yle-dlconjure-up gnunet links rtv youtube-dlcpprestsdk gnutls lmod ruby-build zabbixcrowdin gobject-introspection logtalk ruby@2.4 zigcryfs gocr lolcat sbcl zimdcos-cli godep lumo sdldhall gomplate lxc serverlessdhall-json goreleaser mackup ship==> Deleted Formulaepdftoedn ruby@2.3==> Upgrading 1 outdated package:gradle 4.1 -> 5.3.1==> Upgrading gradle ==> Downloading https://services.gradle.org/distributions/gradle-5.3.1-all.zip==> Downloading from https://downloads.gradle.org/distributions/gradle-5.3.1-all.zip######################################################################## 100.0%🍺 /usr/local/Cellar/gradle/5.3.1: 13,686 files, 236.8MB, built in 1 minuteRemoving: /usr/local/Cellar/gradle/4.1... (169 files, 71.7MB) 升级完后,就可以正常使用gradle命令来打包了: chengmingdeMacBook-Pro:appium-uiautomator2-server cmlanche$ gradle cleanWelcome to Gradle 5.3.1!Here are the highlights of this release: - Feature variants AKA \"optional dependencies\" - Type-safe accessors in Kotlin precompiled script plugins - Gradle Module Metadata 1.0For more details see https://docs.gradle.org/5.3.1/release-notes.htmlStarting a Gradle Daemon (subsequent builds will be faster)> Configure project :appNDK is missing a \"platforms\" directory.If you are using NDK, verify the ndk.dir is set to a valid NDK directory. It is currently set to /Users/cmlanche/Library/Android/sdk/ndk-bundle.If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning.WARNING: Configuration 'compile' is obsolete and has been replaced with 'implementation' and 'api'.It will be removed at the end of 2018. For more information see: http://d.android.com/r/tools/update-dependency-configurations.htmlDeprecated Gradle features were used in this build, making it incompatible with Gradle 6.0.Use '--warning-mode all' to show the individual deprecation warnings.See https://docs.gradle.org/5.3.1/userguide/command_line_interface.html#sec:command_line_warningsBUILD SUCCESSFUL in 1m 3s2 actionable tasks: 2 executed 其实,并不是gradle这个版本不能用,而是对appium-uiautomator2-server这个工程不能拿用,因为gradle版本和谷歌的com.android.tools.build:gradle插件是有个对应关系的,这个server工程中使用的是3.3.2,其最小的gradle就是4.10.1,对应关系链接: https://developer.android.google.cn/studio/releases/gradle-plugin.html#updating-gradle","categories":[{"name":"Appium","slug":"Appium","permalink":"http://www.cmlanche.com/categories/Appium/"}],"tags":[{"name":"appium","slug":"appium","permalink":"http://www.cmlanche.com/tags/appium/"},{"name":"uiautomator2","slug":"uiautomator2","permalink":"http://www.cmlanche.com/tags/uiautomator2/"},{"name":"appium-uiautomator2-server","slug":"appium-uiautomator2-server","permalink":"http://www.cmlanche.com/tags/appium-uiautomator2-server/"}]},{"title":"给Appium内置系统对话框自动处理 - appium-uiautomator2-driver篇","slug":"给Appium内置系统对话框自动处理---appium-uiautomator2-driver篇","date":"2019-04-11T09:02:20.000Z","updated":"2019-04-15T06:15:06.000Z","comments":true,"path":"2019/04/11/给Appium内置系统对话框自动处理---appium-uiautomator2-driver篇/","link":"","permalink":"http://www.cmlanche.com/2019/04/11/给Appium内置系统对话框自动处理---appium-uiautomator2-driver篇/","excerpt":"欢迎关注我的Appium知乎专栏:自定义Appium之路 当appium脚本的uiautomationName设置为UiAutomator2时,就会启动appium-uiautomator2-driver这个driver来执行你的脚本测试,而它的系统对话框的处理跟UIAutomator1的就不一样了,更加复杂一点。 因为UIAutomator2是一个apk形式的,本身可以认为是一个应用,是需要安装的,所以在启动UIAutomator2之前,就会碰到系统对话框的问题,此时,我们需要借助UIAutomator1来做这件事。","text":"欢迎关注我的Appium知乎专栏:自定义Appium之路 当appium脚本的uiautomationName设置为UiAutomator2时,就会启动appium-uiautomator2-driver这个driver来执行你的脚本测试,而它的系统对话框的处理跟UIAutomator1的就不一样了,更加复杂一点。 因为UIAutomator2是一个apk形式的,本身可以认为是一个应用,是需要安装的,所以在启动UIAutomator2之前,就会碰到系统对话框的问题,此时,我们需要借助UIAutomator1来做这件事。 大体流程是: 启动UIAutomator1 用UIAutomator1来处理权限框 安装各种apk,包括UIAutomator2的apk 杀死UIAutomator1服务 启动UIAutomator2 使用UIAutomator2来监控界面,处理系统对话框 怎么做呢?首先我们需要修改appium-uiautomator2-driver,因为原本的执行流程根本没有UIAutomator1的事情,我们需要把UIAutomator1引用进来:(appium-uiautomator2-driver/lib/driver.js) import { androidHelpers, androidCommands, SETTINGS_HELPER_PKG_ID, UiAutomator } from 'appium-android-driver-cmext'; 上面代码中的UiAutomator是我新加的,但是在appium-android-driver中并没有导出UiAutomator的,我们需要给它导出来:(在appium-uiautomator2-driver/index.js) import * as driver from './lib/driver';import * as androidHelperIndex from './lib/android-helpers';import * as commandIndex from './lib/commands/index';import * as webview from './lib/webview-helpers';import * as caps from './lib/desired-caps';import * as uia from './lib/uiautomator'; // 这是新增const { AndroidDriver } = driver;const { UiAutomator } = uia; // 这是新增const { helpers: webviewHelpers, NATIVE_WIN, WEBVIEW_WIN, WEBVIEW_BASE, CHROMIUM_WIN } = webview;const { commonCapConstraints } = caps;const { commands: androidCommands } = commandIndex;const { helpers: androidHelpers, SETTINGS_HELPER_PKG_ID } = androidHelperIndex;export default AndroidDriver;export { androidHelpers, androidCommands, AndroidDriver, startServer, commonCapConstraints, webviewHelpers, NATIVE_WIN, WEBVIEW_WIN, WEBVIEW_BASE, CHROMIUM_WIN, SETTINGS_HELPER_PKG_ID, UiAutomator // 这是新增}; 添加一个启动UIAutomator1服务的方法:(appium-uiautomator2-driver/lib/driver.js) async startUiAutomator1Service () { const rootDir = path.resolve(__dirname, '..', '..', '..', 'appium-android-driver'); const startDetector = (s) => { return /Appium Socket Server Ready/.test(s); }; const bootstrapJar = path.resolve(rootDir, 'bootstrap', 'bin', 'AppiumBootstrap.jar'); this.uiAutomator = new UiAutomator(this.adb); await this.uiAutomator.start( bootstrapJar, 'io.appium.android.bootstrap.Bootstrap', startDetector);} 因为这个driver.js没有导入path,我们还要导入path: import path from 'path'; 然后我们找到startUiAutomator2Session方法,分别添加如下代码: 启动UIAutomator1的代码: // 启动UIAutomator2之前,先启动UIAutomator1服务来处理系统框await this.startUiAutomator1Service(); kill UIAutomator1的代码: // 在启动UIAutomator2之前,要先杀死UIAutomator1服务,否则UIAutomator2无法启动// 因为UIAutomator只允许同时存在一个if (this.uiAutomator) { this.uiAutomator.shutdown();} 位置如下图: 自此,nodejs的事情就完成了,剩下的就是要改造appium-uiautomator2-server的代码,让它能够像UIAutomator1一样监听界面,自动处理。 演示过程: 本地启动appium 执行appium脚本测试 logcat展示监控过程","categories":[{"name":"Appium","slug":"Appium","permalink":"http://www.cmlanche.com/categories/Appium/"}],"tags":[{"name":"appium","slug":"appium","permalink":"http://www.cmlanche.com/tags/appium/"}]},{"title":"自定义系统框处理 – 有哪些方法?","slug":"自定义系统框处理-–-有哪些方法?","date":"2019-04-11T08:20:22.000Z","updated":"2019-04-15T06:15:14.000Z","comments":true,"path":"2019/04/11/自定义系统框处理-–-有哪些方法?/","link":"","permalink":"http://www.cmlanche.com/2019/04/11/自定义系统框处理-–-有哪些方法?/","excerpt":"系统框处理流程有哪些方法?","text":"系统框处理流程有哪些方法? adb赋予权限 优点:用户无感知 缺点: 只能解决小部分问题,无法对所有手机和系统版本兼容 不仅仅是系统权限框,其他弹出的系统框也要处理,如系统更新对话框 单独写appium脚本去处理此方案无法做到,因为权限框处理在脚本之前 在PC端额外开启一个线程去做权限框处理 优点:无需改造Appium 缺点:在pc端额外维护一个处理流程,并且需要不断的去请求dump控件树,再拉取到pc端解析 流程过于复杂,不稳定 处理不及时 控件树信息塞选可能有漏,无法从根本上解决 自定义appium,添加监控代码,使appium自带这种功能 优点:深度定制Appium,内置权限框处理 内置,有问题可以从根本上解决 原生,处理速度及时,速度快 系统框信息自定义配置,有新的无法解决的对话框,可以只修改配置即可** 缺点: 需要定制Appium,难度大 Appium版本更新的话,需要拉取,会有代码上的冲突**","categories":[{"name":"Appium","slug":"Appium","permalink":"http://www.cmlanche.com/categories/Appium/"}],"tags":[]},{"title":"appium的技术架构","slug":"appium的技术架构","date":"2019-04-11T07:29:27.000Z","updated":"2019-04-15T06:15:10.000Z","comments":true,"path":"2019/04/11/appium的技术架构/","link":"","permalink":"http://www.cmlanche.com/2019/04/11/appium的技术架构/","excerpt":"欢迎关注我的Appium知乎专栏:自定义Appium之路 appium是基于nodejs来打包、发布的,也用它来管理各个driver,如下图所示,它的结构图如下:","text":"欢迎关注我的Appium知乎专栏:自定义Appium之路 appium是基于nodejs来打包、发布的,也用它来管理各个driver,如下图所示,它的结构图如下: 从上图就可以看到:android的自动化比iOS的要难多了! appium主程序,依赖各个driver程序,其中,安卓的自动化有3个driver,分别是: appium-android-driver - 用于驱动UIAutomator1 appium-uiautomator2-driver - 用于驱动UIAutomator2 appium-espresso-driver - 用于驱动Espresso 列个表对比一下: 功能\\Driver appium-android-driver appium-uiautomator2-driver appium-espresso-driver 用途 驱动UIAutomator1 驱动UIAutomator2 驱动espresso automationName UiAutomator1 UiAutomator2 Espresso 包形式 AppiumBootstrap.jar appium-uiautomator2-server-v${version}.apk TODO待研究 包依赖地址 bootstrap/bin/ appium-uiautomator2-server/apks/ TODO 优点 jar包形式,免安装,一个命令直接启动,权限级别是shell级别 官方推荐使用2,对高版本兼容性好 控件识别能力强 缺点 对高版本兼容性差,容易无法识别控件 apk形式,需要安装 apk形式,需求安装,并且是侵入式的,可能带来风险 Server模块 在相同工程中,Bootstrap目录,maven工程,主要目标是在bin目录下输出AppiumBootstrap.jar 不同工程,单独的另外一个Nodejs工程:appium-uiautomator2-server 相同Nodejs工程,espress-server目录,gradle工程","categories":[{"name":"Appium","slug":"Appium","permalink":"http://www.cmlanche.com/categories/Appium/"}],"tags":[{"name":"appium","slug":"appium","permalink":"http://www.cmlanche.com/tags/appium/"}]},{"title":"给 Appium 内置系统对话框处理吧!appium-android-driver 篇","slug":"Appium的UIAutomator1的自定义权限框处理","date":"2019-04-11T07:18:46.000Z","updated":"2019-04-15T06:15:18.000Z","comments":true,"path":"2019/04/11/Appium的UIAutomator1的自定义权限框处理/","link":"","permalink":"http://www.cmlanche.com/2019/04/11/Appium的UIAutomator1的自定义权限框处理/","excerpt":"欢迎关注我的Appium知乎专栏:自定义Appium之路 当Appium脚本中的uiautomationName设置为UiAutomator1时,会启动UIAutomator1的Driver来测试你的Appium脚本,在脚本之前之前,会有很多权限框弹出,此时就需要我们的UIAutomator1来自动处理这样的对话框,并且要在安装apk之前就启动UIAutomator1的服务。 很不幸,appium的代码中,是先安装Appium Setting APK和被测应用的,那怎么改呢?","text":"欢迎关注我的Appium知乎专栏:自定义Appium之路 当Appium脚本中的uiautomationName设置为UiAutomator1时,会启动UIAutomator1的Driver来测试你的Appium脚本,在脚本之前之前,会有很多权限框弹出,此时就需要我们的UIAutomator1来自动处理这样的对话框,并且要在安装apk之前就启动UIAutomator1的服务。 很不幸,appium的代码中,是先安装Appium Setting APK和被测应用的,那怎么改呢? 我们找到appium-android-driver工程,找打lib/driver.js,然后找到startAndroidSession方法,将如下代码提到这个方法的最前面: // start UiAutomator (改动:优先启动UIAutomator1)this.bootstrap = new helpers.bootstrap(this.adb, this.bootstrapPort, this.opts.websocket);await this.bootstrap.start(this.opts.appPackage, this.opts.disableAndroidWatchers, this.opts.acceptSslCerts);// handling unexpected shutdownthis.bootstrap.onUnexpectedShutdown.catch(async (err) => { // eslint-disable-line promise/prefer-await-to-callbacks if (!this.bootstrap.ignoreUnexpectedShutdown) { await this.startUnexpectedShutdown(err); }}); 如下图所示: 经过测试,这样不会影响UIAutomator1的正常启动,不会带来负面影响。 既然都优先启动了,我们就要让UIAutomator1去监控手机界面了。 不幸的是,Appium并没有给我们写类似的监听代码,我们得自己动手了,其实很简单,大体思路就是,dump控件树,检测界面控件,检测到权限框,就点”允许”、”是”之类的,这里就需要不断的枚举了。国内手机产商众多,android版本也多,这里就有大量的工作要做了。 幸运的是,Appium给我们做好了监控的线程,只是空实现,基本啥都没干,代码在SocketServer.java的listenForever中: public void listenForever(boolean disableAndroidWatchers, boolean acceptSSLCerts) throws SocketServerException { Logger.debug(\"Appium Socket Server Ready\"); UpdateStrings.loadStringsJson(); if (disableAndroidWatchers) { Logger.debug(\"Skipped registering crash watchers.\"); } else { dismissCrashAlerts(); // 看这里,就是这个每隔100毫秒触发一次的check !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! final TimerTask updateWatchers = new TimerTask() { @Override public void run() { try { watchers.check(); } catch (final Exception e) { e.printStackTrace(); } } }; timer.scheduleAtFixedRate(updateWatchers, 100, 100); } if (acceptSSLCerts) { Logger.debug(\"Accepting SSL certificate errors.\"); acceptSSLCertificates(); } try { client = server.accept(); Logger.debug(\"Client connected\"); in = new BufferedReader(new InputStreamReader(client.getInputStream(), \"UTF-8\")); out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(), \"UTF-8\")); while (keepListening) { handleClientData(); } in.close(); out.close(); client.close(); Logger.debug(\"Closed client connection\"); } catch (final IOException e) { throw new SocketServerException(\"Error when client was trying to connect\"); }}","categories":[{"name":"Appium","slug":"Appium","permalink":"http://www.cmlanche.com/categories/Appium/"}],"tags":[{"name":"appium","slug":"appium","permalink":"http://www.cmlanche.com/tags/appium/"},{"name":"uiautomator","slug":"uiautomator","permalink":"http://www.cmlanche.com/tags/uiautomator/"}]},{"title":"独立开发者的开放心态:帮助他人,其实就是在帮你自己!","slug":"Open心态","date":"2019-04-11T03:01:19.000Z","updated":"2019-04-15T06:15:53.000Z","comments":true,"path":"2019/04/11/Open心态/","link":"","permalink":"http://www.cmlanche.com/2019/04/11/Open心态/","excerpt":"","text":"不要怕被抄袭,心态要开放,往往收获会更多 理念:你身边的朋友都富有了,你才更有机会富有;帮助他人,其实就是帮你自己!","categories":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/categories/独立开发者/"}],"tags":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/tags/独立开发者/"}]},{"title":"appium怎么本地执行、调试","slug":"appium怎么本地执行、调试","date":"2019-04-09T06:36:34.000Z","updated":"2019-04-15T06:15:23.000Z","comments":true,"path":"2019/04/09/appium怎么本地执行、调试/","link":"","permalink":"http://www.cmlanche.com/2019/04/09/appium怎么本地执行、调试/","excerpt":"欢迎关注我的Appium知乎专栏:自定义Appium之路 本地执行通常的执行方式是: # 下载npm库中的appiumnpm i -g appium # 启动appium服务器appium 但如果是本地appium代码怎么执行呢?请看如下shell脚本","text":"欢迎关注我的Appium知乎专栏:自定义Appium之路 本地执行通常的执行方式是: # 下载npm库中的appiumnpm i -g appium # 启动appium服务器appium 但如果是本地appium代码怎么执行呢?请看如下shell脚本 # 克隆appium代码git clone https://github.com/appium/appium.git# 安装依赖npm install# 编译gulp transpile# 本地启动appium./build/lib/main.js 只查看AndroidDriver的日志怎么做? ./build/lib/main.js | grep AndroidDriver 本地调试 配置VS Code的调试器 按F5,打开调试器,选择node.js,会生成一个launch.json的配置文件,这个配置文件用来启动程序的: { // 使用 IntelliSense 了解相关属性。 // 悬停以查看现有属性的描述。 // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 \"version\": \"0.2.0\", \"configurations\": [ { \"type\": \"node\", \"request\": \"launch\", \"name\": \"Appium\", \"program\": \"${workspaceFolder}/appium/build/lib/main.js\" } ]} 配置好launch.json之后,启动调试,会出现如下工具栏: 其含义分别是:继续执行、单步到下一步、跳转到里面、跳出、重启、停止。 本地其他库的调试方法1:npm link 创建软链通过上面的配置,我们可以调试appium工程,但是本地其他工程无法调试,因为都是直接走的依赖库,此时我们只需要用link命令就可以完成本地库与实际依赖库的关联。 npm link ../appium-android-driver 输出: chengmingdeMacBook-Pro:appium cmlanche$ npm link ../appium-android-driver> appium-android-driver-cmext@4.11.0-20190409a prepare /Users/cmlanche/sourcetree/appium/appium-android-driver> gulp prepublish[15:50:55] Using gulpfile ~/sourcetree/appium/appium-android-driver/gulpfile.js[15:50:55] Starting 'prepublish'...[15:50:55] Starting 'clean'...[15:50:55] Finished 'clean' after 71 ms[15:50:55] Starting 'transpile'...[15:50:59] Finished 'transpile' after 3.75 s[15:50:59] Finished 'prepublish' after 3.83 snpm notice created a lockfile as package-lock.json. You should commit this file.audited 29538 packages in 14.08sfound 2 moderate severity vulnerabilities run `npm audit fix` to fix them, or `npm audit` for details/usr/local/lib/node_modules/appium-android-driver-cmext -> /Users/cmlanche/sourcetree/appium/appium-android-driver/Users/cmlanche/sourcetree/appium/appium/node_modules/appium-android-driver-cmext -> /usr/local/lib/node_modules/appium-android-driver-cmext -> /Users/cmlanche/sourcetree/appium/appium-android-driver 方法2:依赖本地仓库切换到appium工程目录下,安装我自定义的uiautomator2-driver npm i appium-uiautomator2-driver-cmext@\"file:../appium-uiautomator2-driver\" 输出: chengmingdeMacBook-Pro:appium cmlanche$ npm i appium-uiautomator2-driver-cmext@\"file:../appium-uiautomator2-driver\"+ appium-uiautomator2-driver-cmext@1.33.0-20190410fremoved 1 package and updated 1 package in 24.494s 当本地库修改后,运行npm i重新编译一下,就可以继续调试了。","categories":[{"name":"Appium","slug":"Appium","permalink":"http://www.cmlanche.com/categories/Appium/"}],"tags":[]},{"title":"怎么证明你买的域名是你的?","slug":"怎么证明你买的域名是你的","date":"2019-04-04T11:59:39.000Z","updated":"2019-04-15T06:18:04.000Z","comments":true,"path":"2019/04/04/怎么证明你买的域名是你的/","link":"","permalink":"http://www.cmlanche.com/2019/04/04/怎么证明你买的域名是你的/","excerpt":"这是我的”独立开发者”微信群中网友问的,我之前还从来没想过这个问题,自认为买了就是我的。 但仔细一想,其实这件事没有那么简单,比如我在阿里云万网买了域名,我可以对这个域名做任何操作,然而这个操作是建立在阿里云这个平台上的,也就是说,阿里云也可以随时强制收回这个域名,可以强制更改这个域名的所有者,可以禁用转移等,也就是说,你买了的这个域名,真正的所有者是这个平台,是阿里云!","text":"这是我的”独立开发者”微信群中网友问的,我之前还从来没想过这个问题,自认为买了就是我的。 但仔细一想,其实这件事没有那么简单,比如我在阿里云万网买了域名,我可以对这个域名做任何操作,然而这个操作是建立在阿里云这个平台上的,也就是说,阿里云也可以随时强制收回这个域名,可以强制更改这个域名的所有者,可以禁用转移等,也就是说,你买了的这个域名,真正的所有者是这个平台,是阿里云! 我特意在知乎和v2ex这两个平台发出这样的提问: 知乎:https://www.zhihu.com/question/318726785 v2ex:https://www.v2ex.com/t/552018 大体上有个初步的认识: 你所购买的域名确实是你的,你可以用whoise查询,查询结果中是你的联系人信息。 你买的域名,你可以获得一个转移码的东西,你可以用它来转移的你的域名到另外一个平台,比如从阿里云转到godaddy.com。 平台确实可以随意改动你的域名的任何信息,但是它只要改了,就会有污点,就会存在记录,你可以凭借记录到ICANN(国际域名管理组织)或者CNNIC(中国域名注册局,管理.cn和中文域名系统)申诉。 其实平台一般不会做这种蠢事,不然就失去了平台最根本的东西,这样也会给自己留下污点,对自己更不利。不过据说阿里干过,参见”万网封停慧聪网域名事件“。 总结下来就是,不要把自己的域名放在竞争对手那里,最好在国外域名服务产商购买,如godaddy,namesilo,namecheap等,因为别人跟你毫无瓜葛,没必要对你的域名干啥坏事。","categories":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/categories/独立开发者/"}],"tags":[{"name":"域名","slug":"域名","permalink":"http://www.cmlanche.com/tags/域名/"}]},{"title":"初步成功自定义Appium","slug":"初步成功自定义Appium","date":"2019-04-01T09:36:08.000Z","updated":"2019-04-15T06:15:27.000Z","comments":true,"path":"2019/04/01/初步成功自定义Appium/","link":"","permalink":"http://www.cmlanche.com/2019/04/01/初步成功自定义Appium/","excerpt":"改造appium-android-driver这个driver是UIAutomator1的driver,负责UIAutomator1的服务启动、停止、命令接收和执行。 工程结构 appium-android-driver(NodeJS工程) bootstrap(Maven工程) 本身appium-android-driver是一个nodejs工程,它还套着一个bootstrap的maven工程,这个maven工程就是用来打包UIAutomator1的,会再bootstrap/bin的目录下构建生成一个叫AppiumBootstrap.jar的供外层的NodeJS工程使用。代码在appium-android-driver/lib/bootstrap.js的start函数中","text":"改造appium-android-driver这个driver是UIAutomator1的driver,负责UIAutomator1的服务启动、停止、命令接收和执行。 工程结构 appium-android-driver(NodeJS工程) bootstrap(Maven工程) 本身appium-android-driver是一个nodejs工程,它还套着一个bootstrap的maven工程,这个maven工程就是用来打包UIAutomator1的,会再bootstrap/bin的目录下构建生成一个叫AppiumBootstrap.jar的供外层的NodeJS工程使用。代码在appium-android-driver/lib/bootstrap.js的start函数中 const rootDir = path.resolve(__dirname, '..', '..');const startDetector = (s) => { return /Appium Socket Server Ready/.test(s); };const bootstrapJar = path.resolve(rootDir, 'bootstrap', 'bin', 'AppiumBootstrap.jar');await this.init();await this.adb.forwardPort(this.systemPort, 4724);this.process = await this.uiAutomator.start( bootstrapJar, 'io.appium.android.bootstrap.Bootstrap', startDetector, '-e', 'pkg', appPackage, '-e', 'disableAndroidWatchers', disableAndroidWatchers, '-e', 'acceptSslCerts', acceptSslCerts); 修改pom.xml,编译bootstrap,输出AppiumBootstrap.jarbootstrap工程是一个maven工程,用idea直接open这个文件夹即可,找到pom.xml,右键Maven->Reimport,我们会发现有两个maven依赖无法导入,报找不到对应的jar包: <dependency> <groupId>android</groupId> <artifactId>android</artifactId> <version>4.4.2_r4</version></dependency><dependency> <groupId>android.test.uiautomator</groupId> <artifactId>uiautomator</artifactId> <version>4.4.2_r4</version></dependency> 原因是默认的仓库是从https://repo.maven.appache.org/maven2中找的,而这个仓库根本没有这两个库。 后来我发现Boundless的仓库http://repo.boundlessgeo.com/main/中是有的,在这个pom.xml中配置这个仓库就可以下载了。 <project>... <repositories> <repository> <id>Boundless</id> <url>http://repo.boundlessgeo.com/main/</url> </repository> </repositories></project> 依赖库搞定后,cmd切换到bootstrap文件夹目录下,执行mvn clean package构建maven工程,我们会发现,并没有在bin目录下生成AndroidBootstrap.jar,此时要修改pom.xml中的maven-jar-plugin: <plugin> <artifactId>maven-jar-plugin</artifactId> <configuration> <!--jar输出目录--> <outputDirectory>./bin</outputDirectory> <!--输出的jar包名称--> <finalName>AppiumBootstrap</finalName> </configuration></plugin> 重新执行mvn clean package,AppiumBootstrap.jar就完成了正常构建,也就是说UIAutomator1构建好了。 自定义appium-android-driver,并发布找到appium-android-driver/package.json,修改name,比如修改为appium-android-driver2,然后顺便修改下version,然后再appium-android-driver根目录下执行 npm install # 重新安装依赖npm publish # 发布 npm publish是发布nodejs包的命令,需要你在npmjs.com)上注册自己的账号,发布的时候需要验证你的账号。 自定义Appium跟自定义appium-android-driver一样,我们找到package.json,修改name和version,比如分别是appium2和1.12.1-20190401a,顺便我们修改一下lib/main.js中的一条语句,以验证我们的修改是否生效: async function logStartupInfo (parser, args) { let welcome = `Welcome to Appium2 v${APPIUM_VER}, modified by chengming`; // 我修改了此处 let appiumRev = await getGitRev(); if (appiumRev) { welcome += ` (REV ${appiumRev})`; } logger.info(welcome); let showArgs = getNonDefaultArgs(parser, args); if (_.size(showArgs)) { logNonDefaultArgsWarning(showArgs); } let deprecatedArgs = getDeprecatedArgs(parser, args); if (_.size(deprecatedArgs)) { logDeprecationWarning(deprecatedArgs); } if (!_.isEmpty(args.defaultCapabilities)) { logDefaultCapabilitiesWarning(args.defaultCapabilities); } // TODO: bring back loglevel reporting below once logger is flushed out // logger.info('Console LogLevel: ' + logger.transports.console.level); // if (logger.transports.file) { // logger.info('File LogLevel: ' + logger.transports.file.level); // }} 还有要在package.json中,找到dependencies,把我们的appium的UIAutomator1的依赖改为“appium-android-driver2”:”latest”,使我们自定义的appium能够使用我们自定义的UIAutomator1 driver 同样,重新构建和发布: npm installnpm publish 在npmjs.com网站中,我的项目下就会看到appium2的工程: 使用自定义的appium安装:npm i -g appium2 启动appium 效果 TODO:验证自定义appium-android-driver是否生效这个要修改bootstrap的java代码,在启动server的时候加上你的日志即可验证,后续再补充吧。 # 补充:2019-04-02 17:20纠正AppiumBootstrap.jar的打包方式官方readme.md没有说怎么打包这个jar包的事情,我按照如上述的打包方式生成的jar是不可用的,格式不正确。jar中的内容应该是一个classes.dex文件,而不是编译好的classes。 我们需要先把class文件打包成dex,然后再把dex打包成jar,shell代码如下: dx --dex --output=./classes.dex target/classesjar -cvf AppiumBootstrap.jar -C ./ ./classes.dex 你需要配置好android的环境变量,使你的dx能够全局调用。 既然打包方式知道了,并且appium是要求在appium-android-driver/bootstrap/bin下有个AppiumBootstrap.jar的,那么我们去掉此前给maven-jar-plugin设置的configuration,重新编写一个shell脚本bootstrap.sh: #!/bin/shmvn clean package # 清理环境,编译class文件dx --dex --output=./target/classes.dex target/classes # 将class文件打包,生成dex文件jar -cvf bin/AppiumBootstrap.jar -C ./ ./target/classes.dex # 将dex文件打包,生成jar 测试AppiumBootstrap.jar我们找到bootstrap工程中的io.appium.android.bootstrap.Bootstrap.java,在testRunServer方法的第一句,添加一段注释: public class Bootstrap extends UiAutomatorTestCase { public void testRunServer() { Logger.info(\"这是我自定义的Bootstrap,成功啦...\"); Find.params = getParams(); boolean disableAndroidWatchers = Boolean.parseBoolean(getParams().getString(\"disableAndroidWatchers\")); boolean acceptSSLCerts = Boolean.parseBoolean(getParams().getString(\"acceptSslCerts\")); SocketServer server; try { server = new SocketServer(4724); server.listenForever(disableAndroidWatchers, acceptSSLCerts); } catch (final SocketServerException e) { Logger.error(e.getError()); System.exit(1); } }} 电脑插上手机,执行: adb devices 输出: chengmingdeMacBook-Pro:bootstrap cmlanche$ adb devicesList of devices attachedcf02d869 device 确保你的手机是连上电脑的。 保存,执行bootstrap.sh,在bin目录会打包好AppiumBootstrap.jar,我们把它push到手机: adb push ./bin/AppiumBootstrap.jar /data/local/tmp/AppiumBootstrap.jar 完成后,我们启动UIAutomator1的测试: adb shell uiautomator runtest /data/local/tmp/AppiumBootstrap.jar -c io.appium.android.bootstrap.Bootstrap 自定义AppiumBootstrap至此流程已通,接下来就是自定义权限框处理,让Appium自主识别权限框。 —— by cmlanche.com","categories":[{"name":"Appium","slug":"Appium","permalink":"http://www.cmlanche.com/categories/Appium/"}],"tags":[{"name":"appium,自定义Appium","slug":"appium-自定义Appium","permalink":"http://www.cmlanche.com/tags/appium-自定义Appium/"}]},{"title":"主机排行网重大更新,移动端自适应","slug":"主机排行网重大更新,移动端自适应","date":"2019-03-31T08:55:24.000Z","updated":"2019-04-15T06:16:33.000Z","comments":true,"path":"2019/03/31/主机排行网重大更新,移动端自适应/","link":"","permalink":"http://www.cmlanche.com/2019/03/31/主机排行网重大更新,移动端自适应/","excerpt":"此前有网友反馈,主机排行网在移动端表现太丑了,希望我改改,今天周末,我专门花了两个多小时来好好把移动短整治了一下,比以前好看多了。 对比改变对比一下吧,下图是前版本的主机排行网:","text":"此前有网友反馈,主机排行网在移动端表现太丑了,希望我改改,今天周末,我专门花了两个多小时来好好把移动短整治了一下,比以前好看多了。 对比改变对比一下吧,下图是前版本的主机排行网: 新版效果: 体验地址:https://hostingranking.cn/ 本次修改的过程我此前也从没真正做过移动端适配的事情,只是初步了解是通过媒体查询来做的,即当浏览器界面满足一定大小时,就会触发该媒体查询器所包含的css。 主机排行网用的css框架是bootstrap4,而bs在自适应方面是沉淀了十多年的,很强悍。我参照官网教程:https://getbootstrap.com/,重新改版了排行页中的两列布局,此前的代码是: <div class=\"row\"> <div class=\"col-10\"> // part 1 </div> <div class=\"col-2\"> // part 2 </div></div> 这样的两列布局代码会让我的界面始终是两列的,因为这里没有其他的col修饰,.col-在屏幕是任何大小时都生效,如果我们想让界面缩小到一定时,两列变一列,那么就不能用.col-来修饰了,我根据我的需求,在界面大于960px才触发两列布局,所有这里要把.col-改为.col-lg-。 这个改动完了,我希望当界面缩小到1000px以下时,主机特征那部分隐藏掉,并且在界面小于720px时隐藏掉详细评价按钮,不然内容太挤,放不小。 此时bs就没有相关的类来帮助你了,需要自己动手写媒体查询: @media(max-width: 1000px) { .features { display: none; }}@media(max-width: 720px) { .btns { :nth-child(2) { display: none; } }} 总结收获此前一直不想做移动端的适配,是因为觉得很难,其实有了bs的帮助这个功能真的很好做,凡是多尝试多探索,不要不做就放弃了。","categories":[{"name":"主机排行网","slug":"主机排行网","permalink":"http://www.cmlanche.com/categories/主机排行网/"}],"tags":[]},{"title":"我也来碎碎念 - 主机排行网运营一个月小结","slug":"我也来碎碎念","date":"2019-03-29T03:00:19.000Z","updated":"2019-04-15T06:16:01.000Z","comments":true,"path":"2019/03/29/我也来碎碎念/","link":"","permalink":"http://www.cmlanche.com/2019/03/29/我也来碎碎念/","excerpt":"我也来碎碎念 - 主机排行网运营一个月小结 学习iPic作者Jason每周一的碎碎念,我也来碎碎念了,只有我有新的想法灵感,我就会立马记录下来,不管内容有多少。 今天要总结一下我的产品:主机排行网 HostingRanking.cn","text":"我也来碎碎念 - 主机排行网运营一个月小结 学习iPic作者Jason每周一的碎碎念,我也来碎碎念了,只有我有新的想法灵感,我就会立马记录下来,不管内容有多少。 今天要总结一下我的产品:主机排行网 HostingRanking.cn 盈利模式主机排行网的盈利模式很简单,就是推广返利,做affiliate链接推广,最终目标要把最好的主机带给大家,做好全面评测,不断优化迭代版本。带给大家价值了,我这个产品才有意义。 小结 目前不断更新迭代有17个版本,115个提交记录,7个分支了,主机排行网也在Vultr 日本VPS上稳定运行一个月了。 谁说Vultr VPS的IP总是被封,为啥我就从来没有,你被封肯定是用来搭梯子了吧。 到目前为止预计可营收大概有1100元 阿里云云大使:611元 Vultr VPS有两个推广,50美金 DigitalOcean有1个有效推广,25美金 关于推广目前网站的流量都是自己推一下才有人看,不推就没有,这不是良性的,也比较消耗自己的时间,最好是流量来自自然搜索,这样就比较省事了,关键是流量最大的百度SEO很不好做,百度SEO一向不是很公平,所以我想先做好谷歌和必应的搜索。百度慢慢做好了。 关于本文通常国内独立开发不会写自己产品的盈利模式和收入来源,我这里完全开放出来,不排斥有同行跟我做一样的网站,也欢迎你做,如果你做了,到时候告诉我,我们互加友链,中国市场这么大,不怕再多你一人。😊 独立开发者微信群目前我运营维护一个独立开发者群,里面有月入几万的大神,也有去学习的菜鸟,欢迎加入。加我微信cmlanche,我拉你进去。目前已有327人,快满了,手慢无啊。","categories":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/categories/独立开发者/"}],"tags":[{"name":"碎碎念","slug":"碎碎念","permalink":"http://www.cmlanche.com/tags/碎碎念/"},{"name":"主机排行网","slug":"主机排行网","permalink":"http://www.cmlanche.com/tags/主机排行网/"}]},{"title":"我的最佳写作方式","slug":"我的最佳写作方式","date":"2019-02-26T02:37:13.000Z","updated":"2019-03-01T00:02:35.000Z","comments":true,"path":"2019/02/26/我的最佳写作方式/","link":"","permalink":"http://www.cmlanche.com/2019/02/26/我的最佳写作方式/","excerpt":"最近喜欢上了写作,喜欢自己写的东西能被别人阅读,赞赏,也希望我写的东西能更有价值,体验也更好。 今天我分享的是我的最佳写作方式。 我的最佳写作方式工具汇总 markdown编辑器 截图工具 gif录制工具 图床工具 Typora QQ / wechat licecap iPic markdown编辑器 · Typora完全免费的markdown编辑器,无与伦比的写作体验,让人爱不释手,具体的特性请移步官网:https://typora.io/ 我最常用的快捷键是Command+/,可以切换源码模式和打字机模式。","text":"最近喜欢上了写作,喜欢自己写的东西能被别人阅读,赞赏,也希望我写的东西能更有价值,体验也更好。 今天我分享的是我的最佳写作方式。 我的最佳写作方式工具汇总 markdown编辑器 截图工具 gif录制工具 图床工具 Typora QQ / wechat licecap iPic markdown编辑器 · Typora完全免费的markdown编辑器,无与伦比的写作体验,让人爱不释手,具体的特性请移步官网:https://typora.io/ 我最常用的快捷键是Command+/,可以切换源码模式和打字机模式。 图传工具 · iPic仅仅有好的编辑器还不够,还需要图床工具将你的图片自动上传到云端,最好是能够结合你使用的markdown编辑器一起使用,最好是截图后直接粘贴,然后上传。 那么iPic是首选,因为Typora内置了iPic的功能,如下图: iPic支持很多种图传工具,我现在用的是腾讯COS,这个工具默认是免费的,但是默认只支持免费的新浪微博图传,如果你要使用其他图传就要收费了,年费50元。 iPic的作者是全职独立开发者Jason,已经写了很多类似的mac工具,都很优秀,官网:https://toolinbox.net/ 截图工具 · QQQQ和微信截图应该是大家都在用的,非常方便,截图后可以粘贴到任何地方,QQ还支持视频录制功能。 gif录制工具 · licecap这里我就要介绍强大的LICEcap了,用来做屏幕截图的,录制出来的gif很小,我的所有gif都是通过它来制作的。 详细请移步官网:https://www.cockos.com/licecap/","categories":[],"tags":[{"name":"写作方式","slug":"写作方式","permalink":"http://www.cmlanche.com/tags/写作方式/"}]},{"title":"CSDN、博客园等6大技术博客平台的写作体验测评","slug":"CSDN、博客园等6大技术博客平台的写作体验测评","date":"2019-02-25T07:38:38.000Z","updated":"2019-03-12T12:12:35.000Z","comments":true,"path":"2019/02/25/CSDN、博客园等6大技术博客平台的写作体验测评/","link":"","permalink":"http://www.cmlanche.com/2019/02/25/CSDN、博客园等6大技术博客平台的写作体验测评/","excerpt":"功能对比","text":"功能对比 markdown编辑器写作体验比较 markdown标准语法请参考:CommonMark, 学习指南https://commonmark.org/help/tutorial/,而本文要比较的各家markdown编辑器遵守的协议都不太一样,比如csdn在标准语法上做了更多事情,如图片支持大小和居中设定,这一点虽好,但其他平台不支持,那也导致你的文章不具备通用性。所以我们并不需要独特的支持,都遵守标准语法,文章的移植性就更强。 1. segmentfault.com 体验5星,极好 markdown语法说明:https://segmentfault.com/markdown segmentfault只支持markdonwn编辑器,是因为它是最新的平台,然后面向的用户都是程序员群体,而markdown已经是大家默认的使用的编辑器,这是共识,csdn前几年都不支持markdown,现在都支持了,会用markdown已经是程序员群体最基础的能力,所以它不需要支持富文本编辑器,有点多余。 特点: 界面简洁,没有多余的东西 聚焦当前写作行,实时预览 发布原创可注明版权,同时可以同步到新浪微博,支持定时发布 可以给文章设置预定的标签,创建一个系统不存在的标签要求你的声望值达到1500。这样其实很有好处,可以让segmentfault整个系统共用一套标签体系,可以把相同兴趣的人组织到一块,同时也避免了标签混乱。 2. CSDN 体验5星,极好 csdn的markdown编辑器很强大,支持很全面,图片也支持大小和居中设置。刚打开编辑器的时候,就告诉你所有csdn的增强版markdown语法知识。 markdown增强点有: 新增文章目录语法:@[TOC](文章目录) 图片支持大小和居中设定 全新的界面设计 ,将会带来全新的写作体验; 在创作中心设置你喜爱的代码高亮样式,Markdown 将代码片显示选择的高亮样式 进行展示; 增加了 图片拖拽 功能,你可以将本地的图片直接拖拽到编辑区域直接展示; 全新的 KaTeX数学公式 语法; 增加了支持甘特图的mermaid语法[^1] 功能; 增加了 多屏幕编辑 Markdown文章功能; 增加了 焦点写作模式、预览模式、简洁写作模式、左右区域同步滚轮设置 等功能,功能按钮位于编辑区域与预览区域中间; 增加了 检查列表 功能。 3. 开源中国 OSChina.net 体验3星,一般开源中国的markdown编辑器体验很差,做的比较敷衍,我随便找个开源的都能做成这样。 而且markdown也不是默认编辑器,说明也不注重这一块。 4. 博客园 cnblogs.com 体验1星,极差博客园的markdown编辑器就太差了,没有任何写作体验,就是一个输入框,让你把提前写好的markdown文本贴进去了事,也没有预览功能,写了啥也不知道。 可能有很多朋友还不知道怎么启用markdown编辑器呢,在后台页面【选项】-【默认编辑器】-【Markdown】 5. 知乎 无markdown编辑器,富文本编辑器体验5星,极好 知乎面向的客户是全社会各类的人,绝大多数并不是程序员群体,所有知乎是不会支持markdown编辑器的。 虽然这样,知乎还是提供给我们强大的定制的富文本编辑器,主要特性有: 界面简洁 图片支持一次性上传多张 支持文章封面 6. 简书 评价5星,很好简书面向的群体也不只是it技术人员,程序员群体仅仅是很小的一部分,但仍旧支持markdown编辑器,只是不是默认的编辑器而已,富文本的支持也很好,主要有如下特点: 文集形式,不仅仅包含编辑器,一次性打开所有文章,方便你随时切换其他文章进行修改 和知乎一样,一次性可上传多张图片 支持数学公式,并有友好的提示。 常用表达式 常用函数 希腊字母 常用符号 特殊符号 简书切换markdown的方式比较隐蔽,在左下角的设置切换:","categories":[],"tags":[{"name":"评测","slug":"评测","permalink":"http://www.cmlanche.com/tags/评测/"},{"name":"csdn","slug":"csdn","permalink":"http://www.cmlanche.com/tags/csdn/"},{"name":"博客园","slug":"博客园","permalink":"http://www.cmlanche.com/tags/博客园/"}]},{"title":"面试题·HashMap和Hashtable的区别(转载再整理)","slug":"HashMap和Hashtable的区别(转)","date":"2019-02-25T03:26:49.000Z","updated":"2019-02-26T06:41:11.000Z","comments":true,"path":"2019/02/25/HashMap和Hashtable的区别(转)/","link":"","permalink":"http://www.cmlanche.com/2019/02/25/HashMap和Hashtable的区别(转)/","excerpt":"原文链接: Javarevisited 翻译: ImportNew.com - 唐小娟译文链接: http://www.importnew.com/7010.html HashMap和Hashtable的比较是Java面试中的常见问题,用来考验程序员是否能够正确使用集合类以及是否可以随机应变使用多种思路解决问题。HashMap的工作原理、ArrayList与Vector的比较以及这个问题是有关Java 集合框架的最经典的问题。Hashtable是个过时的集合类,存在于Java API中很久了。在Java 4中被重写了,实现了Map接口,所以自此以后也成了Java集合框架中的一部分。Hashtable和HashMap在Java面试中相当容易被问到,甚至成为了集合框架面试题中最常被考的问题,所以在参加任何Java面试之前,都不要忘了准备这一题。 这篇文章中,我们不仅将会看到HashMap和Hashtable的区别,还将看到它们之间的相似之处。","text":"原文链接: Javarevisited 翻译: ImportNew.com - 唐小娟译文链接: http://www.importnew.com/7010.html HashMap和Hashtable的比较是Java面试中的常见问题,用来考验程序员是否能够正确使用集合类以及是否可以随机应变使用多种思路解决问题。HashMap的工作原理、ArrayList与Vector的比较以及这个问题是有关Java 集合框架的最经典的问题。Hashtable是个过时的集合类,存在于Java API中很久了。在Java 4中被重写了,实现了Map接口,所以自此以后也成了Java集合框架中的一部分。Hashtable和HashMap在Java面试中相当容易被问到,甚至成为了集合框架面试题中最常被考的问题,所以在参加任何Java面试之前,都不要忘了准备这一题。 这篇文章中,我们不仅将会看到HashMap和Hashtable的区别,还将看到它们之间的相似之处。 HashMap和Hashtable的区别HashMap和Hashtable都实现了Map接口,但决定用哪一个之前先要弄清楚它们之间的分别。主要的区别有:线程安全性,同步(synchronization),以及速度。 HashMap几乎可以等价于Hashtable,除了HashMap是非synchronized的,并可以接受null(HashMap可以接受为null的键值(key)和值(value),而Hashtable则不行)。 HashMap是非synchronized,而Hashtable是synchronized,这意味着Hashtable是线程安全的,多个线程可以共享一个Hashtable;而如果没有正确的同步的话,多个线程是不能共享HashMap的。Java 5提供了ConcurrentHashMap,它是HashTable的替代,比HashTable的扩展性更好。 另一个区别是HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException,但迭代器本身的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。这条同样也是Enumeration和Iterator的区别。 由于Hashtable是线程安全的也是synchronized,所以在单线程环境下它比HashMap要慢。如果你不需要同步,只需要单一线程,那么使用HashMap性能要好过Hashtable。 HashMap不能保证随着时间的推移Map中的元素次序是不变的。 要注意的一些重要术语: sychronized意味着在一次仅有一个线程能够更改Hashtable。就是说任何线程要更新Hashtable时要首先获得同步锁,其它线程要等到同步锁被释放之后才能再次获得同步锁更新Hashtable。 Fail-safe和iterator迭代器相关。如果某个集合对象创建了Iterator或者ListIterator,然后其它的线程试图“结构上”更改集合对象,将会抛出ConcurrentModificationException异常。但其它线程可以通过set()方法更改集合对象是允许的,因为这并没有从“结构上”更改集合。但是假如已经从结构上进行了更改,再调用set()方法,将会抛出IllegalArgumentException异常。 结构上的更改指的是删除或者插入一个元素,这样会影响到map的结构。 我们能否让HashMap同步?HashMap可以通过下面的语句进行同步:Map m = Collections.synchronizeMap(hashMap); 结论Hashtable和HashMap有几个主要的不同:线程安全以及速度。仅在你需要完全的线程安全的时候使用Hashtable,Hashtable是java 4时代的过时产物,ConcurrentHashMap是它的替代品。而如果你使用Java 5或以上的话,请使用ConcurrentHashMap吧。","categories":[],"tags":[{"name":"hashmap","slug":"hashmap","permalink":"http://www.cmlanche.com/tags/hashmap/"},{"name":"hashtable","slug":"hashtable","permalink":"http://www.cmlanche.com/tags/hashtable/"},{"name":"面试题","slug":"面试题","permalink":"http://www.cmlanche.com/tags/面试题/"}]},{"title":"hostingranking.cn·基于ghost的轻量技术架构整理","slug":"hostingranking.cn技术架构","date":"2019-02-24T05:40:05.000Z","updated":"2019-04-15T06:16:35.000Z","comments":true,"path":"2019/02/24/hostingranking.cn技术架构/","link":"","permalink":"http://www.cmlanche.com/2019/02/24/hostingranking.cn技术架构/","excerpt":"本篇纯粹只讲hostingranking.cn网站的技术架构,也就是怎么做到的,达到什么效果。至于它是什么,为什么要做暂且不说,另篇会分享。 技术组成首先hostingranking.cn是基于ghost博客平台而构建的,ghost最强大的部分就是可以最大限度的让你DIY网站,能力特别强。如下是技术组成图: 技术讲解 运行环境 用途 Ghost Nodejs 博客平台,可供主题创作的环境,博客管理,SEO等一系列可插拔的功能 JQuery Javascript 前端js交互 handlebar - 网页模板 spring boot java 提供后端服务,连接第三方服务 typeform 第三方问卷调查服务 mailchimp 第三方邮件服务","text":"本篇纯粹只讲hostingranking.cn网站的技术架构,也就是怎么做到的,达到什么效果。至于它是什么,为什么要做暂且不说,另篇会分享。 技术组成首先hostingranking.cn是基于ghost博客平台而构建的,ghost最强大的部分就是可以最大限度的让你DIY网站,能力特别强。如下是技术组成图: 技术讲解 运行环境 用途 Ghost Nodejs 博客平台,可供主题创作的环境,博客管理,SEO等一系列可插拔的功能 JQuery Javascript 前端js交互 handlebar - 网页模板 spring boot java 提供后端服务,连接第三方服务 typeform 第三方问卷调查服务 mailchimp 第三方邮件服务 主要功能 ghost主题制作,呈现网站基本内容 个性化主机推荐 博客 ghost主题制作ghost主题制作非常简单,只要你会写js+html+css即可制作,另外最好要学会handlebar模板语言,会让你制作主题事半功倍,入门制作教程参考我另一篇文章:https://cmlanche.com/2018/08/26/%E5%88%B6%E4%BD%9Cghost%E4%B8%BB%E9%A2%98/ 个性化主机推荐#####基本流程如下 采用typeform来制作表单 开启typeform的webhook,意思就是当客户提交表单的时候,会往这个webhook url发送一个post请求,来告诉你有用户提交了,以及提交的数据。 spring boot是我们的后端服务,专门用来制作webhook接口的,不要把webhook想的很神圣,没什么难的,webhook接口其实就是一个普通接口,只是它被用来处理webhook发送来的数据罢了。 spring boot收到typeform提交的问卷信息后,去调用mailchimp的api,创建邮件,发送给客户。 活动图如下 我最终通过spring Boot接收webhook传递过来的不同数据,生成不同的推荐结果。 你可以在hostingranking.cn的实现效果,看自己是否能收到邮件,收到的是什么。 typeform强大的问卷调查产品,体验无与伦比,生成的文件简单简洁,完整的api支撑,对刚起步的新手产品免费支持,更重要的是,支持中文!在国内访问畅通无阻!下图是我hostingranking.cn产品涉及到的问卷设计: mailchimp世界上最常用的邮箱市场营销工具,好处如下: 完全的开放,完整的api支持,支持用zapper连接上百款常用产品 中国访问速度还可以,用api调用无碍 诚意满满的新手产品扶助计划,帮助新产品达到足够规模再收费! 按照上面说的个性化功能,当你提交问卷之后,我的mailchimp后台会看到发送状态,以及打开和点击的状态,如下图: 一点吐槽:看到typeform和mailchimp,我想国外的东西做的真是开放,各种api都支持,反观国内,则各种保护,就比如知乎、博客园、segmentfault等都是不开放api的,这点让我觉得国内还不够开放。 博客博客功能是ghost内置的核心功能,刚刚开发好(2019-03-13),访问:https://hostingranking.cn/blog 附hostingranking.cn网站托管在Vultr主机上,买的最低配的VPS,每个月5美金 如果你要买它的主机并且你的主要客户在大陆的话,你一定要买日本的主机,不要买美国和新加坡的,因为日本的平均ping值是最低的,大约一百多,新加坡的主机会绕过日本再到中国,慢一些,而美国的大概两三百。","categories":[{"name":"主机排行网","slug":"主机排行网","permalink":"http://www.cmlanche.com/categories/主机排行网/"}],"tags":[{"name":"hostingranking.cn","slug":"hostingranking-cn","permalink":"http://www.cmlanche.com/tags/hostingranking-cn/"},{"name":"技术架构","slug":"技术架构","permalink":"http://www.cmlanche.com/tags/技术架构/"}]},{"title":"程序员的微创业","slug":"微创业","date":"2019-02-24T01:00:10.000Z","updated":"2019-04-15T06:16:04.000Z","comments":true,"path":"2019/02/24/微创业/","link":"","permalink":"http://www.cmlanche.com/2019/02/24/微创业/","excerpt":"不知道有没有觉得程序员是吃青春饭的; 有没有发现很多公司的招聘需求上写着35岁以上不要; 有没有发现一转眼都已奔三,却依旧一事无成,无房无车; 怎么办?我想过创业,我也创业过,15年的时候和同学一块做人脸识别,种种原因最后没成功,此前两年的积蓄也清零,创业?人家都说是九死一生,我说就是,在你没有足够人脉钱脉的时候你去创业,无异于以卵击石。 程序员是吃请青春饭的,因为这个行业加班最严重,年轻人不断涌上,没时间谈恋爱,谈了恋爱的不敢结婚,结婚的不敢生孩子,生孩子了也不能自己养,要爸妈带,中国现在社会就是这样,年轻人压力巨大,上有老下有小,奔三的我感觉鸭梨山大。 既然不能创业那就好好工作,把工作的事情做好,我也觉得,但是2018年底都知道大裁员,公司也未必是可靠的,如果你没有足够好的技能和其他的收入傍身,最后哭的怎么不会是你?","text":"不知道有没有觉得程序员是吃青春饭的; 有没有发现很多公司的招聘需求上写着35岁以上不要; 有没有发现一转眼都已奔三,却依旧一事无成,无房无车; 怎么办?我想过创业,我也创业过,15年的时候和同学一块做人脸识别,种种原因最后没成功,此前两年的积蓄也清零,创业?人家都说是九死一生,我说就是,在你没有足够人脉钱脉的时候你去创业,无异于以卵击石。 程序员是吃请青春饭的,因为这个行业加班最严重,年轻人不断涌上,没时间谈恋爱,谈了恋爱的不敢结婚,结婚的不敢生孩子,生孩子了也不能自己养,要爸妈带,中国现在社会就是这样,年轻人压力巨大,上有老下有小,奔三的我感觉鸭梨山大。 既然不能创业那就好好工作,把工作的事情做好,我也觉得,但是2018年底都知道大裁员,公司也未必是可靠的,如果你没有足够好的技能和其他的收入傍身,最后哭的怎么不会是你? 地心引力这个社会是一张网紧紧的黏住你,让你无法动弹,无法很好的去世界各地自己想去的地方,无法任性而为还本真的自我,要想摆脱这种地心引力,你要实现的目标就是要实现财务自由,这样你将不再受到工作、家庭的制约。 财务自由近十几年,不同的人,实现财务自由的方式不尽然相同。 有些人天生就财务自由,爸妈给的,就像王思聪,可能你姓王,但是你爸不是王健林; 有些人搭上了不错的“班车”,比如滴滴,美团,小米,公司给予了丰厚股票奖励,也实现了财务自由,很可惜我并没有搭上。 有些人运气比较好,赶上14年股市普涨,又能及时悬崖勒马,狠狠赚了一匹,但我觉得这只是让你一下子突然获得了一箩筐的“鱼”,你没有鱼竿,等你的鱼都吃完的时候,你还是没法学会更好的生存。 同样的还有炒比特币的,赌场要是赚了,不要高兴太早,及早收心。 而绝大多数人,是没有这个福气和运气的,很多都同我一样,默默无闻。 我想实现财务自由,那怎么办呢? 让我们微创业吧先让我们牢记第一条使命,就是把工作上老板交代的活干好 然后是有余力,就微创业吧 微创业,目标是在不影响工作的情况下,额外的创收。 都有哪些做法?第一就是接外包,这种方式来钱明显,但是很多外包都是想要最少的钱,让别人干最多的活,十分的累,还不如工作赚的多,而且十分影响你现在的工作,我极力反对用这个方式创收。 第二就是要在工作之外把自己变成独立的开发者,依靠我们自身的技术优势,来做点小而美的产品,例如iPic的作者Jason(产品经理出生,为了做独立开发者,自学ios相关的技术,https://toolinbox.net/),还有码力全开工作室http://maliquankai.com/的Larry,虽然我们中绝大多数人没办法像他们一样做全职的独立开发者,但是我们在工作中好好打磨一款产品,等它的收入达到一定时,你就可以全职来彻底搞它了。 关键:坚持选好点子,然后就是要长期坚持,如果你放弃了,这一切都完了! 附 我一直在努力的网站:主机排行网 https://hostingranking.cn/ 后续我会慢慢分享独立开发者心得和干货 加我好友,拉你进独立开发者群","categories":[{"name":"独立开发者","slug":"独立开发者","permalink":"http://www.cmlanche.com/categories/独立开发者/"}],"tags":[{"name":"程序员","slug":"程序员","permalink":"http://www.cmlanche.com/tags/程序员/"},{"name":"微创业","slug":"微创业","permalink":"http://www.cmlanche.com/tags/微创业/"}]},{"title":"安装指定版本的node的方法","slug":"安装指定版本的node","date":"2019-01-23T13:19:19.000Z","updated":"2019-02-24T00:51:09.000Z","comments":true,"path":"2019/01/23/安装指定版本的node/","link":"","permalink":"http://www.cmlanche.com/2019/01/23/安装指定版本的node/","excerpt":"","text":"安装指定版本的node的方法sudo npm cache clean -f # 清除缓存sudo npm install -g n # 安装node版本工具nsudo n 10.13.0 # 10.13.0 是版本号# sudo n stable # 安装当前最新的最稳定版本的node","categories":[],"tags":[{"name":"nodejs","slug":"nodejs","permalink":"http://www.cmlanche.com/tags/nodejs/"}]},{"title":"开闭原则","slug":"开闭原则","date":"2018-12-10T03:00:57.000Z","updated":"2019-02-26T06:40:05.000Z","comments":true,"path":"2018/12/10/开闭原则/","link":"","permalink":"http://www.cmlanche.com/2018/12/10/开闭原则/","excerpt":"再谈开闭原则最开始了解设计模式之开闭原则是在6年前,那个时候我还是在校大学生,我是读《设计模式之禅》了解到它的。开闭原则是说,对扩展开发,对修改关闭,当时我看书的时候还不太了解它的含义,只知道这是设计模式最重要的原则,其他5大原则(如最小接口原则、迪米特原则、里式替换原则等)都是为了更好的实现开闭原则而总结出来的一套方法论,而书中说的23大设计模式都是基于这些模式的实践。 今天我又一次感受到了开闭原则的牛逼,我感受到,它不仅仅可以用在实际的代码编写上,对整个系统的架构都有指导借鉴意义。","text":"再谈开闭原则最开始了解设计模式之开闭原则是在6年前,那个时候我还是在校大学生,我是读《设计模式之禅》了解到它的。开闭原则是说,对扩展开发,对修改关闭,当时我看书的时候还不太了解它的含义,只知道这是设计模式最重要的原则,其他5大原则(如最小接口原则、迪米特原则、里式替换原则等)都是为了更好的实现开闭原则而总结出来的一套方法论,而书中说的23大设计模式都是基于这些模式的实践。 今天我又一次感受到了开闭原则的牛逼,我感受到,它不仅仅可以用在实际的代码编写上,对整个系统的架构都有指导借鉴意义。 我目前负责的是我司自动化测试的执行流程,今天有个需求是要给各个手机agent server添加一个doctor的诊断命令。目前的架构是这样的,有3个手机agent server,分别是Robotium、UIAutomator和iOS的XCUTest,执行端这边负责建立socket短连接与这3个agent server进行通信,目前在我写的一个AgentManger来协调管理他们,这部分已经完美运行一年半了,改动很小,需求是现在要添加一个新命令doctor,然而我发现这三个agent server都通过socket连接,但他们的通信内容的协议居然完全不一样,Robotium agent server这边是以“OKEY%s”来格式化返回值,单不看整个的通信架构是否合理,就这个返回的字符串我就很想吐槽,哪有这样的?!通常都是用统一格式的JSON来表达返回结果的,不管是几个agent server,都可以用一套处理模式来处理,甚至分出一个独立的工程模块来做这件事情,而UIAutomator Agent server它的返回值的结构则是{‘success’: true, ‘msg’: ‘xxxxx’}的一个json结构,这个表示也很糟糕,虽然是json的,但是只有true和false两种状态,如果你说你把它改一下不就行了吗,但是你要知道,以往的工程已经积累了几十个接口了,如果去改它以前的通信结构,那么会改动特别大,整个程序要进行回归测试才可以重新上线,所以从一开始就设计出统一的通信协议是多么重要,而如果是在统一的模块中处理的话,那就更容易了,只要在这个模块中更改协议就好了,做少量测试就可以知道你的改动是否覆盖所有的接口,这样可以大大减少后面的工作量! 再联系到我们的开闭原则,原则说,要对修改关闭,对扩展开放,上面的那个垃圾通信协议结构,你要对它进行修改是无比的困难,而添加新的接口,又会让这样的垃圾结构继续存在,当某一天你发现这样的通信结构无法满足要求时,你会发现你必须得修改,也就是要重构了。所以从一开始设计出一套能够更容易扩展,无需修改的结构是多么重要!","categories":[],"tags":[{"name":"开闭原则","slug":"开闭原则","permalink":"http://www.cmlanche.com/tags/开闭原则/"},{"name":"设计模式","slug":"设计模式","permalink":"http://www.cmlanche.com/tags/设计模式/"}]},{"title":"面基","slug":"面基青春无罪","date":"2018-08-26T10:41:07.000Z","updated":"2019-02-24T00:56:59.000Z","comments":true,"path":"2018/08/26/面基青春无罪/","link":"","permalink":"http://www.cmlanche.com/2018/08/26/面基青春无罪/","excerpt":"","text":"“青春无罪”是我QQ群(518914410)的一个网友,是我第一个线下交流的网友,是北工大仪器测量专业方向的博士生,为人非常爽朗,乐于助人。今天跟他见面非常开心,跟他学习了很多,主要有两点要说的: 第一就是不要着急,做好当下需要做的事情,一步一个脚印,一个大的目标的达成不是一下子就能达成的,需要慢慢积累; 第二个就是对怎么做好一个产品刷新了我的认识,以前我太过于盲目,自己的需求并不是大众的需求,做好一个产品是要找准大众的一个需求才行,更不能没想清楚就开始写代码实现,一个产品的实际需求可能需要结合产品的实际情况来分析,马斯洛金字塔需求最底下是生理需求,比如吃喝拉撒睡,中间是安全需求,最顶层是自我价值的实现与超越,你的产品需要根据实际场景来具体分析,不能你想是什么就是什么。","categories":[],"tags":[{"name":"面基","slug":"面基","permalink":"http://www.cmlanche.com/tags/面基/"}]},{"title":"制作ghost主题","slug":"制作ghost主题","date":"2018-08-26T05:16:04.000Z","updated":"2019-02-26T06:40:56.000Z","comments":true,"path":"2018/08/26/制作ghost主题/","link":"","permalink":"http://www.cmlanche.com/2018/08/26/制作ghost主题/","excerpt":"以开发模式启动ghostcd yourghostpathghost start --development# 如果你已经启动了ghost,但不是开发模式,你可以用ghost stop来结束ghost 建立主题目录首先主题的开发环境是nodejs,所以要我们要先用nodejs构建一个项目,比如我们的项目是example: cd yourghostpath/content/themesmkdir examplecd examplenpm init 按照提示建立好基本的package.json,ghost主题最少的要求是有2个文件:index.hbs和post.hbs,这里hbs文件是handlerbars文件,它是一个模板引擎,而ghost只支持用handlerbars,所以掌握handlerbars很重要,其实也不难,关键是要了解怎么传递值的。目录和文件建立好了,主题就算完成了,虽然一句代码都没有,但他确实能够正常运转!","text":"以开发模式启动ghostcd yourghostpathghost start --development# 如果你已经启动了ghost,但不是开发模式,你可以用ghost stop来结束ghost 建立主题目录首先主题的开发环境是nodejs,所以要我们要先用nodejs构建一个项目,比如我们的项目是example: cd yourghostpath/content/themesmkdir examplecd examplenpm init 按照提示建立好基本的package.json,ghost主题最少的要求是有2个文件:index.hbs和post.hbs,这里hbs文件是handlerbars文件,它是一个模板引擎,而ghost只支持用handlerbars,所以掌握handlerbars很重要,其实也不难,关键是要了解怎么传递值的。目录和文件建立好了,主题就算完成了,虽然一句代码都没有,但他确实能够正常运转! 激活主题打开你的网站后台http://localhost:2368/ghost,在Design-Themes中可以看到你的主题example,然后点击active激活,此时会弹出一个警告框,不管它,主题已经可以正常使用了,只是它是个空的,打开http://localhost:2368验证一下 ###热加载 它的目的是可以实时加载刷新页面,你需要安装nodemon # 需要先停止ghostghost stop# 安装nodemonnpm install -g nodemon@latest# 切换到你的主题目录下cd yourthemedevpath# 热加载nodemon index.js --watch ./ --ext hbs,js,css 这是制作主题的基本套路,完成这些后就需要编写主题的代码了","categories":[],"tags":[{"name":"ghost","slug":"ghost","permalink":"http://www.cmlanche.com/tags/ghost/"},{"name":"ghost主题","slug":"ghost主题","permalink":"http://www.cmlanche.com/tags/ghost主题/"}]},{"title":"工作无非是温水煮青蛙","slug":"温水煮青蛙","date":"2018-08-24T08:50:25.000Z","updated":"2018-08-24T08:57:50.000Z","comments":true,"path":"2018/08/24/温水煮青蛙/","link":"","permalink":"http://www.cmlanche.com/2018/08/24/温水煮青蛙/","excerpt":"","text":"在别人公司上班工作是下策,看似光鲜亮丽的生活,其实是温水煮青蛙,当某一天公司倒闭,或个人技术跟不上、在公司各种不服等等因素,不知道你有没有感受到面临淘汰的危机感。我时常有这样的感觉,不是非得自己给自己打工,而是你需要有不受制于人的技术、财富。你就必须要勤奋努力,抓紧时间做出一个有价值的优秀赚钱来源。加油! ————————————————————————————————————————————致己书","categories":[],"tags":[]},{"title":"令人绝望的UIAutomator WebView自动化测试","slug":"令人绝望的UIAutomator-WebView自动化测试","date":"2018-08-17T11:02:00.000Z","updated":"2019-04-15T06:16:54.000Z","comments":true,"path":"2018/08/17/令人绝望的UIAutomator-WebView自动化测试/","link":"","permalink":"http://www.cmlanche.com/2018/08/17/令人绝望的UIAutomator-WebView自动化测试/","excerpt":"","text":"特别特别想吐槽Android UIAutomator对WebView的控件树渲染,谷歌简直就写了一坨屎,又乱又臭 为什么要吐槽?需求是这样的,我们期望通过UIAutomator对WebView来dump结构一致的控件树结构,以便在使用XPath定位的时候能够精准查询每个控件。然而实际情况是: 1. 可以与不可以的问题UIAutomator在某些Android版本(好像是4.4.4以下不支持)上无法dump,只有一个android.webkit.WebView节点。 2. 就算可以dump,结构和内容也极度不统一UIAutomator就算能很好的dump应用中WebView的元素,但是结构也非常不统一,结构混乱。 目前我碰到的情况有: 1. 识别能力不一致android 8.0会正确识别应用中的图片,把它标记为android.widget.Image,而在7.1等上却不能,只能识别成android.view.View 2.识别出的结构不一致有些可能会多增加一些android.view.View的包装视图,可能不仅仅是一层包装 3. 识别出的内容也不一样通常在高版本手机,比如8.0+上能把一些图片识别出带文本的View,低版本却不能 吐槽就算你内容识别出文本了,View能正确识别为Image了,我都不怪你,我都可以做转化,比如我忽略问题,Image我都统一转化为View,但是结构不一致那就问题太大了,XPAth查找完全失效!在WebView中你无法利用其它条件来定位一个控件,能定位控件的文本、ID、class在WebView中都是不稳定因素。 寻找思路解决这个问题1. 将WebView设置为可调试模式,远程调试它本方案的目的就是要能够向WebView注入JavaScript代码,然后输出我们自己的一个查询结果,因为我们面对的是一个固定的网页html,所以它是兼容性很好的方式。 实现方式就是调用WebView的静态方法WebView.setWebContentsDebuggingEnabled(true),然后打开chrome://inspect调试当前WebView页面,但是很明显无效,设置调试模式仅仅对当前应用有效,对其他应用不产生任何效果,不然WebView就没啥安全性可言了。 网上说(https://blog.csdn.net/zhulin2609/article/details/51437821)可以用root权限强制开启,但是我们的场景是面对成千上万的没root权限手机,去root显然不现实,本方案放弃 2. 利用VisualXposed来架设一个类似虚拟机的东西,用这个虚拟机来启动被测应用开源地址:https://github.com/android-hacker/VirtualXposed 这个方案你需要掌握VisualApp和epic项目,它可以实现对被测应用的完全掌控,也不需要root权限,但是它过于复杂,不稳定性因素太多,兼容性有待验证,本方案可行,但对暂时Testin云测是不适用,留作待定研究吧。 3. 利用UIAutomator渲染的不稳定坑逼的WebView的AccessbilityNodeInfo来重新构造我们自己的控件树结构这种方案来源于我对界面控件元素区域的思考,虽然UIAutomator给的控件树不靠谱,但是界面上的信息它都有(如果能dump的话),如果我们按照控件的区域重新组织这个WebView的结构的话,是否可行? 比如A区域在B区域的里面,那么我们认定A是B的子节点,如果A和B没有父子关系,他们处于同一Y坐标,那么他们可以认为是兄弟节点,如果他们的区域是一致的,那么他们其中之一是可以被忽略的,至于忽略谁,就要看谁附带的信息更有价值,比如A的带有文本或class是Image,显然A就更有价值,忽略B。 按照上面的大致逻辑,我们可以构造一个自己的控件树,这样是否可以提高兼容性? 实际上我做了测试,用云测Testin的700+个手机做了验证,在未使用本方案之前,通过了98台设备,使用本方案我测试了两次,第一次通过223,第二次是194次,提高了一倍 使用本方案之前 使用本方案之后 有一些效果,但是还不够o(╥﹏╥)o 最后我想问 谷歌的UIAutomator2.0测试框架在WebView上测试是很坑爹的,也是特别难解的,为什么不能让我们自定义渲染逻辑呢? 有哪位同学有更好的方式?能够兼容上千款不同的设备不同的版本?","categories":[{"name":"Appium","slug":"Appium","permalink":"http://www.cmlanche.com/categories/Appium/"}],"tags":[{"name":"UIAutomator吐槽","slug":"UIAutomator吐槽","permalink":"http://www.cmlanche.com/tags/UIAutomator吐槽/"}]},{"title":"Android自动化·细数UIAutomator的坑·UIAutomator渲染WebView控件树在不同手机上的差异","slug":"Android自动化·细数UIAutomator的坑·控件树在不同手机上的差异","date":"2018-08-13T10:50:31.000Z","updated":"2018-08-13T11:09:34.000Z","comments":true,"path":"2018/08/13/Android自动化·细数UIAutomator的坑·控件树在不同手机上的差异/","link":"","permalink":"http://www.cmlanche.com/2018/08/13/Android自动化·细数UIAutomator的坑·控件树在不同手机上的差异/","excerpt":"我想只有Testin云测才会遇到这样的问题,云测的自动化技术是要抹掉手机的差异性的,就是说一套脚本可以在不同的手机产商不同的手机版本上成功运行,而云测会遇到很多很多各种各样因为手机产商与版本的差异导致脚本不兼容的问题,而今天我讲的是最近发现的UIAutomator在WebView控件树渲染在不同手机上的差异。 Testin云测已跨越自动化测试的万水千山,欢迎来测!","text":"我想只有Testin云测才会遇到这样的问题,云测的自动化技术是要抹掉手机的差异性的,就是说一套脚本可以在不同的手机产商不同的手机版本上成功运行,而云测会遇到很多很多各种各样因为手机产商与版本的差异导致脚本不兼容的问题,而今天我讲的是最近发现的UIAutomator在WebView控件树渲染在不同手机上的差异。 Testin云测已跨越自动化测试的万水千山,欢迎来测! 额外话对WebView来说,UIAutomator的能力是很有限的,更别提小程序使用的腾讯X5内核的Webview,以及国外流行的Crosswalk,他们俩UIAutomator是根本无法识别的,但是云测对小程序有独特的支持。下回我会分析UIAutomator为啥无法很好识别WebView。 正题中国手机产商太多了,而且Android版本分布也特别凌乱,从4.3到8.0都有,而它对WebView的渲染能力也各有不同,经过我分析云测700个手机在同一WebView页面的执行结果,我发现有的控件树可能是这样的: <node class=\"android.webkit.WebView\" bounds=\"[0,0,100,100]\"> <node class=\"android.view.View\" bounds=\"[0,0,20,20]\"> <node class=\"android.view.TextView\" bounds=\"[0,0,20,20]\"> </node> </node></node> 而有的是这样的 <node class=\"android.webkit.WebView\" bounds=\"[0,0,100,100]\"> <node class=\"android.view.TextView\" bounds=\"[0,0,20,20]\"> </node></node> 我们发现TextView外层居然有一层皮,而有的居然没有,这是不一致的,这样会导致后续依赖控件树结构的xpath查询会失败,怎么抹掉这种差异性呢?问题我已道出,怎么解我也有办法,但是涉及到公司机密,我也只能点到为止。","categories":[],"tags":[{"name":"Android自动化","slug":"Android自动化","permalink":"http://www.cmlanche.com/tags/Android自动化/"},{"name":"细数UIAutomator的坑","slug":"细数UIAutomator的坑","permalink":"http://www.cmlanche.com/tags/细数UIAutomator的坑/"}]},{"title":"android自动化研发日志 - 细数UIAutomator缺点 - 1.0和2.0的区别","slug":"android自动化研发日志---细数UIAutomator缺点---关于版本的吐槽","date":"2018-08-13T09:18:06.000Z","updated":"2018-08-13T10:49:03.000Z","comments":true,"path":"2018/08/13/android自动化研发日志---细数UIAutomator缺点---关于版本的吐槽/","link":"","permalink":"http://www.cmlanche.com/2018/08/13/android自动化研发日志---细数UIAutomator缺点---关于版本的吐槽/","excerpt":"","text":"Hi,我是云测自动化研发工程师,关于我可以看这个链接:一只误入歧途的资深自动化研发(待写) 系列文章:细数UIAutomator缺点(待写) 专题:android自动化测试(待写) 关于云测的自动化测试技术:我不说你肯定不知道原来Testin云测自动化技术在某种程度上讲已经超越了谷歌(待写) 综述本文讨论的是关于UIAutomator版本的吐槽,我们都知道UIAutomator分为两个版本,1.0和2.0 如下是两者的对比 1.0 2.0 最低Android版本分界线 大于或等于16 大于或等于18 运行包形式 jar apk 权限 shell级别 自身apk权限赋予 UIAutomator1.0官网已经没有1.0的链接了,只有2.0的,我在其他地方找到老学习链接:https://stuff.mit.edu/afs/sipb/project/android/docs/tools/help/uiautomator/index.html UIAutomator的运行包是一个jar包,运行命令大致如 # yourtest.jar是你的jar在android系统的具体文件路径,通常放在/data/local/tmp/目录下# yourjarclass是你要执行的测试方法,例如com.test.YourTestClass#functionnameadb shell uiautomator runtest yourtest.jar -c yourjarclass#function 运行起来后,我们用命令查看UIAutomator1.0的进程是shell的 chengmingdembp:Downloads cmlanche$ adb shell ps | grep uiashell 4101 4098 2048608 61680 futex_wait 7f81656170 S uiautomatorchengmingdembp:Downloads cmlanche$ 所以它的权限是比较高的,启动之后便可以执行,也不需要安装,虽然它在这方面很便利,但它的获取控件信息的能力很鸡肋,只能获取到一个AccessibilityNodeInfo的root节点,而实际上,多root的情况是普遍存在的,尤其是5.0以后的android版本。 虽然它很鸡肋,但是它可以用来做安装UIAutomator2.0之前的操作,UIAutomator2.0需要自动安装,那么1.0的话就必须安装了,用它来自动点击UIAutomator2.0的安装对话框,在2.0启动之前的所有安装与权限框处理过程,都可以用1.0来做。 UIAutomator2.02.0是一个安装包的形式来做测试的,它拥有什么权限需要你自己去设定,谷歌现在官方只支持2.0,就说明谷歌对它有足够的重视。 官网:https://developer.android.com/training/testing/ui-automator 更好上网请用:expressvpn 2.0加入了Instrument支持,它的执行命令类似这样的: # 详情请到官网学习adb shell am instrument -w -r -e debug false -e class ....","categories":[],"tags":[{"name":"android自动化测试","slug":"android自动化测试","permalink":"http://www.cmlanche.com/tags/android自动化测试/"},{"name":"细数UIAutomator缺点","slug":"细数UIAutomator缺点","permalink":"http://www.cmlanche.com/tags/细数UIAutomator缺点/"}]},{"title":"你真的了解java的lambda吗?- java lambda用法与源码分析","slug":"lambda用法与源码分析","date":"2018-07-22T01:44:12.000Z","updated":"2018-07-23T08:21:46.000Z","comments":true,"path":"2018/07/22/lambda用法与源码分析/","link":"","permalink":"http://www.cmlanche.com/2018/07/22/lambda用法与源码分析/","excerpt":"用法示例:最普遍的一个例子,执行一个线程new Thread(() -> System.out.print(\"hello world\")).start(); ->我们发现它指向的是Runnable接口 @FunctionalInterfacepublic interface Runnable { /** * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object's * <code>run</code> method to be called in that separately executing * thread. * <p> * The general contract of the method <code>run</code> is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run();} 分析 ->这个箭头是lambda表达式的关键操作符 ->把表达式分成两截,前面是函数参数,后面是函数体。 Thread的构造函数接收的是一个Runnable接口对象,而我们这里的用法相当于是把一个函数当做接口对象传递进去了,这点理解很关键,这正是函数式编程的含义所在。 我们注意到Runnable有个注解@FunctionalInterface,它是jdk8才引入,它的含义是函数接口。它是lambda表达式的协议注解,这个注解非常重要,后面做源码分析会专门分析它的官方注释,到时候一目了然。 /* @jls 4.3.2. The Class Object * @jls 9.8 Functional Interfaces * @jls 9.4.3 Interface Method Body * @since 1.8 */@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface FunctionalInterface {}","text":"用法示例:最普遍的一个例子,执行一个线程new Thread(() -> System.out.print(\"hello world\")).start(); ->我们发现它指向的是Runnable接口 @FunctionalInterfacepublic interface Runnable { /** * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object's * <code>run</code> method to be called in that separately executing * thread. * <p> * The general contract of the method <code>run</code> is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run();} 分析 ->这个箭头是lambda表达式的关键操作符 ->把表达式分成两截,前面是函数参数,后面是函数体。 Thread的构造函数接收的是一个Runnable接口对象,而我们这里的用法相当于是把一个函数当做接口对象传递进去了,这点理解很关键,这正是函数式编程的含义所在。 我们注意到Runnable有个注解@FunctionalInterface,它是jdk8才引入,它的含义是函数接口。它是lambda表达式的协议注解,这个注解非常重要,后面做源码分析会专门分析它的官方注释,到时候一目了然。 /* @jls 4.3.2. The Class Object * @jls 9.8 Functional Interfaces * @jls 9.4.3 Interface Method Body * @since 1.8 */@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface FunctionalInterface {} 由此引发的一些案例有参数有返回值的实例:集合排序List<String> list = new ArrayList<>();Collections.sort(list, (o1, o2) -> { if(o1.equals(o2)) { return 1; } return -1;}) 我们知道Collections.sort方法的第二个参数接受的是一个Comparator<T>的对象,它的部分关键源码是这样的: @FunctionalInterfacepublic interface Comparator<T> { int compare(T o1, T o2);} 如上已经去掉注释和部分其他方法。 我们可以看到sort的第二个参数是Comparator的compare方法,参数类型是T,分别是o1和o2,返回值是一个int。 疑问 上面的示例我们看到接口都有个@FunctionalInterface的注解,但是我们在实际编程中并没有加这个注解也可以实现lambda表达式,例如: public class Main { interface ITest { int test(String string); } static void Print(ITest test) { test.test(\"hello world\"); } public static void main(String[] args) { Print(string -> { System.out.println(string); return 0; }); }} 如上所示,确实不需要增加@FunctionInterface注解就可以实现 如果在1中的示例的ITest接口中增加另外一个接口方法,我们会发现不能再用lambda表达式。 我们带着这两个疑问来进入源码解析。 源码解析必须了解注解 @FunctionInterface上源码: package java.lang;import java.lang.annotation.*;/** * An informative annotation type used to indicate that an interface * type declaration is intended to be a <i>functional interface</i> as * defined by the Java Language Specification. * * Conceptually, a functional interface has exactly one abstract * method. Since {@linkplain java.lang.reflect.Method#isDefault() * default methods} have an implementation, they are not abstract. If * an interface declares an abstract method overriding one of the * public methods of {@code java.lang.Object}, that also does * <em>not</em> count toward the interface's abstract method count * since any implementation of the interface will have an * implementation from {@code java.lang.Object} or elsewhere. * * <p>Note that instances of functional interfaces can be created with * lambda expressions, method references, or constructor references. * * <p>If a type is annotated with this annotation type, compilers are * required to generate an error message unless: * * <ul> * <li> The type is an interface type and not an annotation type, enum, or class. * <li> The annotated type satisfies the requirements of a functional interface. * </ul> * * <p>However, the compiler will treat any interface meeting the * definition of a functional interface as a functional interface * regardless of whether or not a {@code FunctionalInterface} * annotation is present on the interface declaration. * * @jls 4.3.2. The Class Object * @jls 9.8 Functional Interfaces * @jls 9.4.3 Interface Method Body * @since 1.8 */@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface FunctionalInterface {} 我们说过这个注解用来规范lambda表达式的使用协议的,那么注释中都说了哪些呢? 一种给interface做注解的注解类型,被定义成java语言规范 * An informative annotation type used to indicate that an interface* type declaration is intended to be a <i>functional interface</i> as* defined by the Java Language Specification. 一个被它注解的接口只能有一个抽象方法,有两种例外。 第一是接口允许有实现的方法,这种实现的方法是用default关键字来标记的(java反射中java.lang.reflect.Method#isDefault()方法用来判断是否是default方法),例如: 当然这是jdk8才引入的特性,到此我们才知道,知识是一直在变化的,我们在学校中学到interface接口不允许有实现的方法是错误的,随着时间推移,一切规范都有可能发生变化。 如果声明的方法和java.lang.Object中的某个方法一样,它可以不当做未实现的方法,不违背这个原则:一个被它注解的接口只能有一个抽象方法 例如同样是Compartor接口中,它重新声明了equals方法: 这些是对如下注释的翻译和解释 * Conceptually, a functional interface has exactly one abstract* method. Since {@linkplain java.lang.reflect.Method#isDefault()* default methods} have an implementation, they are not abstract. If* an interface declares an abstract method overriding one of the* public methods of {@code java.lang.Object}, that also does* <em>not</em> count toward the interface's abstract method count* since any implementation of the interface will have an* implementation from {@code java.lang.Object} or elsewhere. 如果一个类型被这个注解修饰,那么编译器会要求这个类型必须满足如下条件 这个类型必须是一个interface,而不是其他的注解类型、枚举enum或者类class 这个类型必须满足function interface的所有要求,如你个包含两个抽象方法的接口增加这个注解,会有编译错误。 * <p>If a type is annotated with this annotation type, compilers are* required to generate an error message unless:** <ul>* <li> The type is an interface type and not an annotation type, enum, or class.* <li> The annotated type satisfies the requirements of a functional interface.* </ul> 编译器会自动把满足function interface要求的接口自动识别为function interface,所以你才不需要对上面示例中的ITest接口增加@FunctionInterface注解。 * <p>However, the compiler will treat any interface meeting the* definition of a functional interface as a functional interface* regardless of whether or not a {@code FunctionalInterface}* annotation is present on the interface declaration. 通过了解function interface我们能够知道怎么才能正确的创建一个function interface来做lambda表达式了。接下来的是了解java是怎么把一个函数当做一个对象作为参数使用的。 穿越:对象变身函数让我们重新复盘一下上面最开始的实例: new Thread(() -> System.out.print(\"hello world\")).start(); 我们知道在jdk8以前我们都是这样来执行的: Runnable r = new Runnable(){ System.out.print(\"hello world\");};new Thread(r).start(); 我们知道两者是等价的,也就是说r 等价于()->System.out.print("hello world"),一个接口对象等于一个lambda表达式?那么lambda表达式肯定做了这些事情(未看任何资料,纯粹推理,有误再改正): 创建接口对象 实现接口对象 返回接口对象 关于UnaryOperator上篇文章(聊一聊JavaFx中的TextFormatter以及一元操作符UnaryOperator)关于UnaryOperator草草收尾,在这里给大家重新梳理一下,关于它的使用场景以及它与lambda表达式的关系 使用场景要先理解它的作用,它是接受一个参数并返回与该类型同的值,来看一个List怎么用它的,java.util.List中的replaceAll就用它了: default void replaceAll(UnaryOperator<E> operator) { Objects.requireNonNull(operator); final ListIterator<E> li = this.listIterator(); while (li.hasNext()) { li.set(operator.apply(li.next())); }} 我们可以看到这个方法的目的是把list中的值经过operator操作后重新返回一个新值,例如具体调用 List<String> list = new ArrayList<>();list.add(\"abc\");list.replaceAll(s -> s + \"efg\");System.out.println(list); 其中lambda表达式s->s+"efg"就是这个operator对象,那么最终list中的值就变成了[“abcefg”],由此我们可以知道它的作用就是对输入的值再加工,并返回同类型的值,怎么用就需要你自己扩展发挥了。 与lambda表达式的关系?在我看来,它跟lambda表达式的关系并不大,只是它是jdk内置的一种标准操作,类似的二元操作符BinaryOperator它可以接受两个同类型参数,并返回同类型参数的值。 关于UnaryOperator,我们百尺竿头更进一步,深入到核心先贴出它的源码: @FunctionalInterfacepublic interface UnaryOperator<T> extends Function<T, T> { /** * Returns a unary operator that always returns its input argument. * * @param <T> the type of the input and output of the operator * @return a unary operator that always returns its input argument */ static <T> UnaryOperator<T> identity() { return t -> t; }} 我们看到这个function interface居然没有抽象方法,不,不是没有,我们继续看Function接口 @FunctionalInterfacepublic interface Function<T, R> { /** * Applies this function to the given argument. * * @param t the function argument * @return the function result */ R apply(T t); /** * Returns a composed function that first applies the {@code before} * function to its input, and then applies this function to the result. * If evaluation of either function throws an exception, it is relayed to * the caller of the composed function. * * @param <V> the type of input to the {@code before} function, and to the * composed function * @param before the function to apply before this function is applied * @return a composed function that first applies the {@code before} * function and then applies this function * @throws NullPointerException if before is null * * @see #andThen(Function) */ default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } /** * Returns a composed function that first applies this function to * its input, and then applies the {@code after} function to the result. * If evaluation of either function throws an exception, it is relayed to * the caller of the composed function. * * @param <V> the type of output of the {@code after} function, and of the * composed function * @param after the function to apply after this function is applied * @return a composed function that first applies this function and then * applies the {@code after} function * @throws NullPointerException if after is null * * @see #compose(Function) */ default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } /** * Returns a function that always returns its input argument. * * @param <T> the type of the input and output objects to the function * @return a function that always returns its input argument */ static <T> Function<T, T> identity() { return t -> t; }} 既然他们都被注解为@FunctionInterface了,那么他们肯定有一个唯一的抽象方法,那就是apply 我们知道->lambda表达式它是不需要关心函数名字的,所以不管它叫什么,apply也好,apply1也好都可以,但jdk肯定要叫一个更加合理的名字,那么我们知道s -> s + "efg"中->调用的就是apply方法 而且我们注意到这里有一个identity()的静态方法,它返回一个Function对象,它其实跟lambda表达式关系也不大,它的作用是返回当前function所要表达的lambda含义。相当于创建了一个自身对象。 Function算是lambda的一种扩展应用,这个Function的的作用是Represents a function that accepts one argument and produces a result.意思是接受一个参数,并产生(返回)一个结果(类型可不同)。 类似的还有很多Function,都在包java.util.Function中 你也可以创建自己的Function,它是用来表达操作是怎样的。如传入的参数是什么,返回的是什么。 其实你只要明白它抽象的是操作就可以了。 到此就知道,原来UnaryOperator没啥神秘的,jdk把这些操作放在java.util.function中也正说明了它是一个工具类,是为了提取重复代码,让它可以重用,毕竟需要用到这样的操作的地方太多了,提取是有必要的。 转载请注明来源:cmlanche.com","categories":[],"tags":[{"name":"java","slug":"java","permalink":"http://www.cmlanche.com/tags/java/"},{"name":"lambda","slug":"lambda","permalink":"http://www.cmlanche.com/tags/lambda/"},{"name":"UnaryOperator","slug":"UnaryOperator","permalink":"http://www.cmlanche.com/tags/UnaryOperator/"}]},{"title":"聊一聊JavaFx中的TextFormatter以及一元操作符UnaryOperator","slug":"聊一聊JavaFx中的TextFormater","date":"2018-07-21T07:49:27.000Z","updated":"2019-04-15T06:19:47.000Z","comments":true,"path":"2018/07/21/聊一聊JavaFx中的TextFormater/","link":"","permalink":"http://www.cmlanche.com/2018/07/21/聊一聊JavaFx中的TextFormater/","excerpt":"直击主题:它在JavaFx中可以实现什么效果它可以格式化输入文本的内容,可以允许输入哪种值,可以规定光标的位置,例如可以实现一个输入框只允许输入数字, 例如textfield表示输入框对象,那么设置格式化内容的话就应该像这样子:textfield.setTextformatter(new TextFormatter<String>(IntegerFilter)), 而其中IntegerFilter就是只允许输入数字的过滤器,它的代码是怎样的呢? /** * Created by cmlanche on 2017/7/10. * 整数过滤器 * 应用:比如使一个输入框只能输入数字 */public class IntegerFilter implements UnaryOperator<TextFormatter.Change> { private final static Pattern DIGIT_PATTERN = Pattern.compile(\"\\\\d*\"); @Override public TextFormatter.Change apply(TextFormatter.Change change) { return DIGIT_PATTERN.matcher(change.getText()).matches() ? change : null; }} DIGIT_PATTERN大家都能看出来它是正则表达式,是匹配文本是否是整数的表达式。 apply方法中的实现的意思是,只要符合整数就返回change,否则返回null","text":"直击主题:它在JavaFx中可以实现什么效果它可以格式化输入文本的内容,可以允许输入哪种值,可以规定光标的位置,例如可以实现一个输入框只允许输入数字, 例如textfield表示输入框对象,那么设置格式化内容的话就应该像这样子:textfield.setTextformatter(new TextFormatter<String>(IntegerFilter)), 而其中IntegerFilter就是只允许输入数字的过滤器,它的代码是怎样的呢? /** * Created by cmlanche on 2017/7/10. * 整数过滤器 * 应用:比如使一个输入框只能输入数字 */public class IntegerFilter implements UnaryOperator<TextFormatter.Change> { private final static Pattern DIGIT_PATTERN = Pattern.compile(\"\\\\d*\"); @Override public TextFormatter.Change apply(TextFormatter.Change change) { return DIGIT_PATTERN.matcher(change.getText()).matches() ? change : null; }} DIGIT_PATTERN大家都能看出来它是正则表达式,是匹配文本是否是整数的表达式。 apply方法中的实现的意思是,只要符合整数就返回change,否则返回null 经过测试发现,当返回change的时候,可以允许输入,如输入0~9中的任意数字都可以输入,但输入非数字的话,会返回null,此时发现输入框光标不会移动,而且内容也不会变化,说明null是禁用的意思。 还有个细节就是当按下delete键时,change对象中有个方法叫isDeleted返回true,而文本是空,当按下其他字符,如1时,change中的getText为1,change还有个getControl和getControlText可以返回控件和控件的文本。说明change是包含了当前变化的内容和不变的内容。 那么这个TextFormatter就厉害了,利用change中的信息,可以实现对输入框的各种格式需求,上面例子中让输入框只能输入数字只是TextFormatter的冰山一角,还可以实现各种各样的其他需求,例如让输入框中的值只能是浮点数,只能是字母,字符数只能是6位。有了它,都可以不用对它的值再进行额外的校验,而且可以通用起来,只需要编写不同的过滤器就可以了。 源码解析:TextFormatter是如何发挥作用的?从上面的分析我们可以清楚的看到是TextFormatter中的filter发挥了过滤作用,而TextFormatter是给TextField使用的,那么TextFormatter必定有函数给TextField来调用,所以我们找到了getFilter,我们在TextField中找这个函数可以看到: /** * Replaces a range of characters with the given text. * * @param start The starting index in the range, inclusive. This must be &gt;= 0 and &lt; the end. * @param end The ending index in the range, exclusive. This is one-past the last character to * delete (consistent with the String manipulation methods). This must be &gt; the start, * and &lt;= the length of the text. * @param text The text that is to replace the range. This must not be null. */public void replaceText(final int start, final int end, final String text) { if (start > end) { throw new IllegalArgumentException(); } if (text == null) { throw new NullPointerException(); } if (start < 0 || end > getLength()) { throw new IndexOutOfBoundsException(); } if (!this.text.isBound()) { final int oldLength = getLength(); TextFormatter<?> formatter = getTextFormatter(); TextFormatter.Change change = new TextFormatter.Change(this, getFormatterAccessor(), start, end, text); if (formatter != null && formatter.getFilter() != null) { change = formatter.getFilter().apply(change); if (change == null) { return; } } // Update the content updateContent(change, oldLength == 0); }} /** * Positions the anchor and caretPosition explicitly. */public void selectRange(int anchor, int caretPosition) { caretPosition = Utils.clamp(0, caretPosition, getLength()); anchor = Utils.clamp(0, anchor, getLength()); TextFormatter.Change change = new TextFormatter.Change(this, getFormatterAccessor(), anchor, caretPosition); TextFormatter<?> formatter = getTextFormatter(); if (formatter != null && formatter.getFilter() != null) { change = formatter.getFilter().apply(change); if (change == null) { return; } } updateContent(change, false);} private boolean filterAndSet(String value) { // Send the new value through the textFormatter, if one exists. TextFormatter<?> formatter = getTextFormatter(); int length = content.length(); if (formatter != null && formatter.getFilter() != null && !text.isBound()) { TextFormatter.Change change = new TextFormatter.Change( TextInputControl.this, getFormatterAccessor(), 0, length, value, 0, 0); change = formatter.getFilter().apply(change); if (change == null) { return false; } replaceText(change.start, change.end, change.text, change.getAnchor(), change.getCaretPosition()); } else { replaceText(0, length, value, 0, 0); } return true;} 如上,从TextField源码中我们找到了三个与TextFormatter的filter有关的方法,他们的大致意思就是当有变化产生时(例如按下字符1),就会触发一个change产生,然后就会调用filter来产生一个新的change对象,这个对象会改变最终输入框中的内容。 陌生知识:UnaryOperator和大家一样,平时很少看到这个类,我百度查了一下,这个类叫一元运算符,它继承自java.util.function.Function,是jdk中的内容,不是javafx的(包括UnaryOperator也是jdk的内容),源码是这样的: /** * Represents an operation on a single operand that produces a result of the * same type as its operand. This is a specialization of {@code Function} for * the case where the operand and result are of the same type. * * <p>This is a <a href=\"package-summary.html\">functional interface</a> * whose functional method is {@link #apply(Object)}. * * @param <T> the type of the operand and result of the operator * * @see Function * @since 1.8 */@FunctionalInterfacepublic interface UnaryOperator<T> extends Function<T, T> { /** * Returns a unary operator that always returns its input argument. * * @param <T> the type of the input and output of the operator * @return a unary operator that always returns its input argument */ static <T> UnaryOperator<T> identity() { return t -> t; }} 它的意思是输入和输出是同一个值,函数identity的意思是总是返回输入的参数,而且只有一个参数,这个一元操作符UnaryOperator被注解@FuntionalInterface了,它是java.lang.包中的内容,代码如下: @Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface FunctionalInterface {} 已经涉及到很深的内容了,而identity我猜测应该是与jdk内部实现有关的,会被自动调用的,所有关于他们就到此为止,不再深入分析。 而我注意到一点,一元操作符UnaryOperator有更优雅的用法。文章开头我们注意到IntegerFilter,我们的写法是让它实现UnaryOperator,但其实可以这么做: textfield.setTextFormatter(new TextFormatter<String>((change)->{ Pattern DIGIT_PATTERN = Pattern.compile(\"\\\\d*\"); return DIGIT_PATTERN.matcher(change.getText()).matches() ? change : null;})) 为啥变化这么大? 慢慢分析发现对lambda知识的了解的欠缺,接下来的内容涉及到java函数式编程lambda表达式的核心内容,敬请下篇文章更新(^▽^) 转载请注明出处:https://www.cmlanche.com/","categories":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/categories/javafx/"}],"tags":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/tags/javafx/"},{"name":"textformatter","slug":"textformatter","permalink":"http://www.cmlanche.com/tags/textformatter/"},{"name":"unaryoperator","slug":"unaryoperator","permalink":"http://www.cmlanche.com/tags/unaryoperator/"},{"name":"lambda表达式","slug":"lambda表达式","permalink":"http://www.cmlanche.com/tags/lambda表达式/"}]},{"title":"Unable to start monitor 4454, An other instance is problaly using the same port","slug":"Unable-to-start-monitor-4454,-An-other-instance-is-problaly-using-the-same-port","date":"2018-07-18T07:00:26.000Z","updated":"2018-07-18T07:12:58.000Z","comments":true,"path":"2018/07/18/Unable-to-start-monitor-4454,-An-other-instance-is-problaly-using-the-same-port/","link":"","permalink":"http://www.cmlanche.com/2018/07/18/Unable-to-start-monitor-4454,-An-other-instance-is-problaly-using-the-same-port/","excerpt":"最近两个月启动IDEA一直这个错误,终于今天忍不住了,找了下解决这个问题的方法 造成IDEA启动失败。","text":"最近两个月启动IDEA一直这个错误,终于今天忍不住了,找了下解决这个问题的方法 造成IDEA启动失败。 解决办法禁用或者卸载Log4JPlugin插件,这个插件没什么卵用,14年之后就不再更新了。 卸载方法菜单Preferences->Plugins,然后搜索log4JPlugin就出来了,然后Uninstall卸载","categories":[],"tags":[{"name":"idea","slug":"idea","permalink":"http://www.cmlanche.com/tags/idea/"}]},{"title":"解决Android Robotium(Instrumentation)初始化时getActivity阻塞的问题","slug":"解决Android-Robotium初始化时getActivity不返回的问题","date":"2018-07-17T03:52:23.000Z","updated":"2018-07-18T05:56:09.000Z","comments":true,"path":"2018/07/17/解决Android-Robotium初始化时getActivity不返回的问题/","link":"","permalink":"http://www.cmlanche.com/2018/07/17/解决Android-Robotium初始化时getActivity不返回的问题/","excerpt":"如果应用没有启动,阻塞了这种情况getActivity肯定会阻塞的,你需要调用startActivity启动起来: getInstrumentation().getTargetContext().startActivity(intent); 如果在Robotium中还是没启动,你就需要借助外力来启动它了,比如命令: am start .... // 代码未写完整,意思就是你需要借助am的命令来启动应用","text":"如果应用没有启动,阻塞了这种情况getActivity肯定会阻塞的,你需要调用startActivity启动起来: getInstrumentation().getTargetContext().startActivity(intent); 如果在Robotium中还是没启动,你就需要借助外力来启动它了,比如命令: am start .... // 代码未写完整,意思就是你需要借助am的命令来启动应用 如果应用启动了,但还是阻塞了有两种方式可以尝试,第一种: ActivityMonitor monitor = getInstrumentation().addMonitor(activity.getName(), null, false);Activity activity = getInstrumentation().waitForMonitorWithTimeout(monitor, 10000); 这种方式的意思是,对测试的Activity追加一个Monitor来追踪它的状态,并等待返回一个Activity对象,超时时间为10s。但是这种方式未必总是有效,在极端情况下,还是会阻塞。此时你就需要用第二种方式尝试获取Activity了 第二种方式:Java反射 通过观察第一种方式的实现源码,我们发现被测Activity都是存放在Instrumentation => ActivityThread(mThread) => ArrayMap<IBinder, ActivityClientRecord>(mActivities) => Activity的一个map中的,那么我们通过反射的方式剥离3层,可以拿到这个对象。 Object mThread = ReflectHelper.getField(getInstrumentation(), Instrumentation.class.getName(), \"mThread\");Log.e(\"tag\", \"inst mthread = \" + mThread);if (mThread != null) { Object mActivities = ReflectHelper.getField(mThread, null, \"mActivities\"); Log.e(\"tag\", \"inst mActivities = \" + mActivities); if (mActivities != null && mActivities instanceof Map) { Map actMap = (Map) mActivities; Set<Map.Entry> sets = actMap.entrySet(); Iterator<Map.Entry> iterable = sets.iterator(); while (iterable.hasNext()) { Map.Entry entry = iterable.next(); Object actRecObj = entry.getValue(); Log.e(\"tag\", \"ActivityClientRecord: \" + actRecObj); Object actObj = ReflectHelper.getField(actRecObj, null, \"activity\"); Log.e(\"tag\", \"Activity: \" + actObj); if (actObj != null) { return (Activity) actObj; } } }} 上述的种种方式可以完全解决getActivity阻塞的问题!","categories":[],"tags":[{"name":"Robotium","slug":"Robotium","permalink":"http://www.cmlanche.com/tags/Robotium/"},{"name":"自动化研发","slug":"自动化研发","permalink":"http://www.cmlanche.com/tags/自动化研发/"}]},{"title":"Bloogle开发日记 | 制作一个滚动大纲的前端网页","slug":"从零制作一个滚动markdown大纲的前端网页","date":"2018-07-11T00:19:58.000Z","updated":"2018-07-17T05:20:09.000Z","comments":true,"path":"2018/07/11/从零制作一个滚动markdown大纲的前端网页/","link":"","permalink":"http://www.cmlanche.com/2018/07/11/从零制作一个滚动markdown大纲的前端网页/","excerpt":"从零教学怎么制作一个滚动大纲","text":"从零教学怎么制作一个滚动大纲 前端页面大纲区域是一个列表,列表中的每项的超链接<a>都有一个#id的超链接指向gif中左侧的内容区域的heading标题 JS代码 当滚动时,我们要求大纲区域停靠在顶部 // 获取大纲侧边栏的元素对象var aside = document.getElementsByClassName(\"outline\")[0];// 获取大纲侧边栏的元素对象距离浏览器顶部的距离var aside_sticky_offset_top = aside.offsetTop;// 当窗口滚动会触发window.onscroll方法window.onscroll = function() { // window.pageYOffset是滚动条在垂直方向上的滚动距离 // 当滚动距离大于aside_sticky_offset_top时,表示大纲侧边栏滚动到了顶部,要求大纲停靠在顶部不动 // 则可以给css增加一个class,aside-sticky if(window.pageYOffset > aside_sticky_offset_top) { aside.classList.add(\"aside-sticky\"); } else { aside.classList.remove(\"aside-sticky\"); }}; .aside-sticky { position: sticky; top: 0; width: 100%;} 继续滚动,当浏览器窗口越过某个heading标题的时候,要求将对应的大纲侧边栏的列表项设置为active活动状态。 function scrollDetactHeading() { // 获取所有的heading标题对象 var headings = document.getElementsByClassName(\"heading\"); var nearestHeading = null, nearestHeadingPageYOffset = null; console.clear(); // 循环遍历所有标题对象,找出当前选择的是哪个标题 for (var i = 0; i < headings.length; i++) { var h = headings[i]; // 计算标题距离浏览器视窗顶部的距离 var result = h.offsetTop - window.pageYOffset; console.log(result); // 只有为0或者为负数才表示浏览器窗口顶部穿过了标题栏下的内容区域 // 并且result的负数值越大,越表示当前最近标题是谁 if (0 === result) { nearestHeading = h; break } else if (result < 0) { if (nearestHeadingPageYOffset == null || nearestHeadingPageYOffset <= result) { nearestHeadingPageYOffset = result; nearestHeading = h; } } } // 如果nearestHeading不为空,表示找到了当前所选择的标题 if (null != nearestHeading) { // 找到侧边栏对象,清空侧边栏对象列表项的所有活动状态 var menulist = document.querySelector(\".outline .menu-list\"); for (var i = 0; i < menulist.children.length; i++) { var a = menulist.children[i].getElementsByTagName(\"a\")[0]; a.classList.remove(\"is-active\"); } // 找出最终活动的列表项,设置为active的状态 var activeA = document.querySelector(\"a[href='#\" + nearestHeading.id + \"']\"); activeA.classList.add(\"is-active\"); }} window.onscroll = function() { if(window.pageYOffset > aside_sticky_offset_top) { aside.classList.add(\"aside-sticky\"); } else { aside.classList.remove(\"aside-sticky\"); } // 当滚动时触发大纲滚动检测 scrollDetactHeading();}; 开源地址:https://github.com/letsblogio/website-pure-html","categories":[],"tags":[{"name":"bloogle","slug":"bloogle","permalink":"http://www.cmlanche.com/tags/bloogle/"},{"name":"网页前端","slug":"网页前端","permalink":"http://www.cmlanche.com/tags/网页前端/"}]},{"title":"腾讯云建站主机的一次奇妙之旅","slug":"腾讯云建站主机的一次奇妙之旅","date":"2018-06-21T14:07:07.000Z","updated":"2018-07-17T05:19:01.000Z","comments":true,"path":"2018/06/21/腾讯云建站主机的一次奇妙之旅/","link":"","permalink":"http://www.cmlanche.com/2018/06/21/腾讯云建站主机的一次奇妙之旅/","excerpt":"","text":"十星主机 - 腾讯云建站主机 体验过无数个主机产商的虚拟主机服务,今天我把最佳虚拟主机产商颁给腾讯云,因为它给我太多惊喜。 惊喜1:配置极高腾讯云建站主机只配置了三款,基础版、专业版、旗舰版,而且网页空间最低都是40G起步,CPU独享,内存独享,不限流量,比阿里云独享的配置都高。 惊喜2:自动开启SSL数字证书,并且是免费的,浏览器https绿色标识在购买建站主机后,会有一个默认的临时域名,你会发现证书就已经签发好了,例如我购买签发的临时域名是247915467.mylightsite.com,打开后会发现自动变成https,非常棒。当你绑定域名(注意必须是在腾讯购买的域名,如果不是可以把域名转移到腾讯云来)后会自动给这个域名签发证书,自动开启https访问,例如我绑定的域名是elementor.net.cn。 惊喜3:Wordpress建站主机自动安装Wordpress当你购买wordpress建站主机,wordpress你会发现已经安装好了,已经可以访问了。 惊喜4:香港主机免备案这个其实也不算什么惊喜,因为所有的相关主机都是免备案的,但是我原先以为腾讯的主机如果是在中国大陆开展业务都是要备案的,香港主机是不需要的。 惊喜5:价格低基础班单年售价5折,只要294元,买两年4折,只要470元。而且初次购买建站主机可以领取一个购买域名送30元代金券礼包,相当于再优惠30元,这太优惠了。相比已经很便宜的阿里云独享香港主机经济版(单年售价298元,两年538元),还要优惠,并且配置更好(主要是数据空间要更好很多,腾讯云是40G,阿里云是5G)。Elementor中文网就买了两年的,这样优惠更多。 腾讯主机Review:Hostreport.cn 腾讯云年终钜惠:cloud.tencent.com 关于我 一个不想命运低头的黑衣剑士 个人博客:cmlanche.com 我的产品:主机深度评测网 CSDN个人主页:cmlanche SegmentFault个人主页:cmlanche 博客园个人主页:cmlanche","categories":[],"tags":[{"name":"腾讯云","slug":"腾讯云","permalink":"http://www.cmlanche.com/tags/腾讯云/"},{"name":"建站主机","slug":"建站主机","permalink":"http://www.cmlanche.com/tags/建站主机/"}]},{"title":"hexo自动部署到git、ftp(虚拟主机等)、云服务器的方式","slug":"hexo-deployers","date":"2018-06-20T00:19:12.000Z","updated":"2018-07-17T05:18:52.000Z","comments":true,"path":"2018/06/20/hexo-deployers/","link":"","permalink":"http://www.cmlanche.com/2018/06/20/hexo-deployers/","excerpt":"","text":"自动部署很有用,当你写完文章后,直接使用hexo d就可以自动更新你的网站了 部署到git首先你需要在你的blog下安装git deployer插件:npm install hexo-deployer-git --save,然后再把如下代码添加到你的_config.yml文件中 ### git deploydeploy: type: git repo: https://github.com/cmlanche/cmlanche.github.io.git # 你的远程仓库 branch: master # 你的远程残酷分支 message: \"hello guys\" # 每次提交的信息 需要注意的是,部署到git需要再本地安装你ssh key,也就是说允许本地进行读写远程git仓库,否则你会没权限的 部署到ftp服务器你可能会用一台虚拟主机来部署你的hexo个人站点,那么fip怎么部署? 首先你需要下载安装ftp deployer插件:npm install hexo-deployer-ftpsync --save,然后把你下面的代码贴到你的_config.yml中,注意修改ftp的相关参数。 #### ftp deploydeploy: type: ftpsync host: ftpserver # ftp服务器地址 user: ftpusername # ftp用户名 pass: xxxx # 你的ftp用户密码 remote: xxx # 你要上传到的地址,例如/wwwroot port: 21 # ftp端口,不同的ftp可能会不一样 delete: true # 上传本地文件是否删除ftp中的所有文件 verbose: true # 是否打印调试信息 ignore_errors: false # 是否忽略错误 部署到远程主机,通常如VPS或者云服务器同样要下载hexo deployer: npm install hexo-deployer-rsync --save,然后代码奉上: deploy: type: rsync host: <host> # 主机地址 user: <user> # 用户名 root: <root> # 要上传到的目录 port: [port] # Default is 22 delete: [true|false] # Default is true args: <rsync args> verbose: [true|false] # Default is true ignore_errors: [true|false] # Default is false 关于我 一个试图摆脱“地心引力”的黑衣剑士 一个主机深度评测站主:HostReport.cn 一个向往自由职业的自由人 个人博客:cmlanche.com","categories":[],"tags":[{"name":"hexo","slug":"hexo","permalink":"http://www.cmlanche.com/tags/hexo/"},{"name":"git","slug":"git","permalink":"http://www.cmlanche.com/tags/git/"},{"name":"ftp","slug":"ftp","permalink":"http://www.cmlanche.com/tags/ftp/"},{"name":"云服务器","slug":"云服务器","permalink":"http://www.cmlanche.com/tags/云服务器/"}]},{"title":"静态模板方法的用法","slug":"静态模板方法的用法","date":"2018-06-20T00:04:38.000Z","updated":"2018-07-17T05:18:28.000Z","comments":true,"path":"2018/06/20/静态模板方法的用法/","link":"","permalink":"http://www.cmlanche.com/2018/06/20/静态模板方法的用法/","excerpt":"","text":"静态模板方法首先是一个静态的方法,然后有指定模板,例如 public class Utils { public static void test(){} // 这是静态方法 public static void <T> test(int a){} // 这是静态模板方法} 我们使用静态方法是直接类名.方法名,例如Utils.test(),那静态模板方法呢? // 假如我们的模板是StringUtils.<String>test(100) 这种写法我还是头一次见,感觉很新奇,所有会记录一下。 如果直接写Utils.test(100)会在java6编译不过,你需要指定模板类型,它相当于函数的一部分(通常来说函数包含函数名称、返回值、参数三个部分,而模板是第四个部分)","categories":[],"tags":[{"name":"java","slug":"java","permalink":"http://www.cmlanche.com/tags/java/"},{"name":"静态模板方法","slug":"静态模板方法","permalink":"http://www.cmlanche.com/tags/静态模板方法/"}]},{"title":"js判断某元素是否真的可见(以人的视角的可见)","slug":"js判断某元素是否真的可见(以人的视角的可见)","date":"2018-01-27T09:19:52.000Z","updated":"2018-01-27T13:53:00.000Z","comments":true,"path":"2018/01/27/js判断某元素是否真的可见(以人的视角的可见)/","link":"","permalink":"http://www.cmlanche.com/2018/01/27/js判断某元素是否真的可见(以人的视角的可见)/","excerpt":"","text":"代码如下: function isElementVisible(el) { var rect = el.getBoundingClientRect(), vWidth = window.innerWidth || document.documentElement.clientWidth, vHeight = window.innerHeight || document.documentElement.clientHeight, efp = function (p, x, y) { var els = document.elementsFromPoint(x, y); // 获取某点的所有元素, 最顶层的元素在最前面 for (var index = 0; index < els.length; index++) { var style = getComputedStyle(els[index]); // 如果此前的元素是半透明的,并且不是当前元素,则跳过当前元素 if (p != els[index] && (style.opacity < 1 || style.display == 'none' || ['collapse', 'hidden'].indexOf(el.style.visibility) == -1)) { continue; } else return els[index]; } return els[0]; }; // Return false if it's not in the viewport if (rect.right < 0 || rect.bottom < 0 || rect.left > vWidth || rect.top > vHeight) return false; return ( el.contains(efp(el, rect.left, rect.top)) || el.contains(efp(el, rect.right, rect.top)) || el.contains(efp(el, rect.right, rect.bottom)) || el.contains(efp(el, rect.left, rect.bottom))) || el.contains(efp(el, rect.left + (rect.right - rect.left) / 2, rect.top + (rect.bottom - rect.top) / 2)); } 大致思路: 先判断元素是否在视窗区域内(视窗指浏览器窗口,webview的窗口) 在判断元素的四角和中心点是否在最顶层,如果有遮罩则去掉遮罩的影响(遮罩比如是透明或者半透明的元素)","categories":[],"tags":[]},{"title":"java内存泄漏分析","slug":"java内存泄漏分析","date":"2017-12-27T07:30:04.000Z","updated":"2019-04-15T06:19:14.000Z","comments":true,"path":"2017/12/27/java内存泄漏分析/","link":"","permalink":"http://www.cmlanche.com/2017/12/27/java内存泄漏分析/","excerpt":"什么是内存泄漏?内存泄漏就是一些已经不使用的对象还存在于内存之中且垃圾回收机制无法回收它们,导致它们常驻内存,会使内存消耗越来越大,最终导致程序性能变差。 java导出heap数据的方法jmap -heap pid 这个是获取某java进程的heap基本信息。 jmap -dump:format=b,file=heap.bin pid 这个是dump一份heap的内存分析状态文件,然后你用eclipse mat软件来导入这个文件,然后看看给出的分析报告。","text":"什么是内存泄漏?内存泄漏就是一些已经不使用的对象还存在于内存之中且垃圾回收机制无法回收它们,导致它们常驻内存,会使内存消耗越来越大,最终导致程序性能变差。 java导出heap数据的方法jmap -heap pid 这个是获取某java进程的heap基本信息。 jmap -dump:format=b,file=heap.bin pid 这个是dump一份heap的内存分析状态文件,然后你用eclipse mat软件来导入这个文件,然后看看给出的分析报告。 理解什么是heap(堆)先来看一幅图,java内存模型 如果你用过visualvm,就会看到heap size的一个图,如下所示: 图中的最大heapsize为2G,目前已申请的heap size为125M左右,在使用中的heapsize为67M左右,这个数据你得看懂,最大heapsize是你在启动java程序时指定的,如果没指定就jvm会有一个默认的指定,指定的方式是java -Xms1024M -Xmx2048M,其中-xms表示最小的堆大小,也就是默认启动会占用的堆大小,-xmx为最大的堆大小,也就是heap堆的容量,如果你的申请不到内存了,爆出oom错误,那就是说你的heap已经用光了,你就应该检讨为啥gc没有合理回收heap,造成内存泄漏了。 实际上,你在资源管理器中看到的内存大小它是总大小,heap只是其中的一部分,heap占用1G,资源管理器中可能是1.5G。 解决方案:优化内存使用!!!怎么优化内存使用? 首先从外部着手,要么适当增加heap size,方法使给vm增加参数-Xmx2G(表示heap堆的最大大小为2G,其实-Xmx2048M也是2G,一个意思);要么更换垃圾回收器,比如你可以换换g1垃圾回收器,方法是-XX:+UseG1GC,g1垃圾回收器是并发的回收期,回收效率很高。 然后从你的程序内不着手,分析你程序中占用内存大的,申请内存频繁的代码处是否有不必要的内存申请,我在我公司的itestin自动化录制工具中就发现了很多这样的案例。 关于使用g1gc垃圾回收器,上两幅图对比看下: 首先是默认的垃圾回收器(一般来说是串型) 然后是g1gc 注意String!学会StringBuilder和StringBuffer关于这三者的基础知识很多了,不赘述,给个链接自己学习:这里 我要说的就是,如果一个字符串很大,并且频繁的对它进行操作,比如replace,substring等,这样会造成很多同样大小的这样的字符串,非常消耗内存,使用StringBuilder之后,始终是对一个对象操作,不仅不需要生成额外的字符串变量而造成不必要的内存,而且可以提高执行速度!","categories":[],"tags":[]},{"title":"使用java-api在windows上打开本地html文件无法传递query参数","slug":"使用java-api在windows上打开本地html文件无法传递query参数","date":"2017-12-26T08:41:42.000Z","updated":"2018-07-17T05:19:31.000Z","comments":true,"path":"2017/12/26/使用java-api在windows上打开本地html文件无法传递query参数/","link":"","permalink":"http://www.cmlanche.com/2017/12/26/使用java-api在windows上打开本地html文件无法传递query参数/","excerpt":"","text":"使用java api,比如: awt api: Desktop.getDesktop().browse(URI.create(\"file:///c:\\\\test.html?q=abc\")); javafx api: getHostServices().showDocument(\"file:///c:\\\\test.html?q=abc\") 打开的浏览器会发现query参数q=abc无法传递到浏览器,只能单纯的打开test.html,但是mac上经过测试上述两种方式都是可以正常打开的并传递参数的。 值得一提的是,如果uri不是file:///的文件协议的话,是可以传递参数的,比如url变成http://www.baidu.com?q=abc,是能把q参数传递给浏览器。 怎么解决呢?你可以让你的本地html文件再关联一个js文件,然后打开的时候你把要传递的参数写到文件中,当第一次打开html文件时会从js读取参数,然后重新加载,这样就可以解决啦,只是会加载两次而已。","categories":[],"tags":[{"name":"java","slug":"java","permalink":"http://www.cmlanche.com/tags/java/"}]},{"title":"JavaFx中gif图片显示内存泄漏","slug":"JavaFx中gif图片显示内存泄漏","date":"2017-12-09T10:44:40.000Z","updated":"2019-04-15T06:19:04.000Z","comments":true,"path":"2017/12/09/JavaFx中gif图片显示内存泄漏/","link":"","permalink":"http://www.cmlanche.com/2017/12/09/JavaFx中gif图片显示内存泄漏/","excerpt":"","text":"在javafx中显示gif是很方便的,直接把gif文件放到Image对象中即可,比如下代码: FileInputStream fis = new FileInputStream(new File(yourgiffile));imageview.setImage(new Image(fis))fis.close(); 显示方便,不表示能用啊!!! 我加载一个369kb的gif文件,每次显示都会增加100M左右的内存!!!,加载一个2.8M的gif,每次都会增加300M以上的内存!!! Oh my god!!! 简直无法忍受,各种谷歌发现以前有提过这个bug: https://bugs.openjdk.java.net/browse/JDK-8119730 https://bugs.openjdk.java.net/browse/JDK-8117172 Christian Schudt (Inactive) added a comment - 2013-08-29 05:36 I also tested with my sample code from RT-28782:With JavaFX 2.2: OutOfMemoryError after a few iterations.With JavaFX 8: No error. Memory stays low after 1000+ iterations. Permalink Alexander Kirov (Inactive) added a comment - 2013-08-29 05:58 ok, close as verified on b104 按照上面的说法,说是已经解决了,可是真解决了吗? 不过有一点可疑的地方就是,我的gif文件尺寸都比较大,369kb的文件尺寸是1500 × 448,2.8M的文件的尺寸是2184 × 1300,查看内存占用发现堆大小涨的非常大,而使用的堆非常小,或许是因为尺寸过大,一次性申请了过大的内存导致的。","categories":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/categories/javafx/"}],"tags":[]},{"title":"图片压缩(tinypng)+七牛云存储客户端","slug":"图片压缩(tinypng)+七牛云存储客户端","date":"2017-12-03T03:28:41.000Z","updated":"2019-02-26T06:40:33.000Z","comments":true,"path":"2017/12/03/图片压缩(tinypng)+七牛云存储客户端/","link":"","permalink":"http://www.cmlanche.com/2017/12/03/图片压缩(tinypng)+七牛云存储客户端/","excerpt":"想法来源个人写博客,很多情况下需要图片,而图片的话需要压缩,否则占用空间太大,使得网站反应慢,而且占流量,同时减少云存储大小,减少损失,对静态网站比如hexo或者hugo直接写markdown这种形式的博客,需要上传文件取得一个文件url,那么现有就没有一个很好的方式来自动处理这种需求了。","text":"想法来源个人写博客,很多情况下需要图片,而图片的话需要压缩,否则占用空间太大,使得网站反应慢,而且占流量,同时减少云存储大小,减少损失,对静态网站比如hexo或者hugo直接写markdown这种形式的博客,需要上传文件取得一个文件url,那么现有就没有一个很好的方式来自动处理这种需求了。 ###目前想做的功能 压缩图片(支持png,gif,jpg) 图片上传(使用七牛云) 直接复制markdown文本到剪贴板(直接在客户端赋值粘贴到你用的编辑器即可) ###目前是否有这样方便的客户端呢? 从上图来看,社区插件有很多,但大多数都是针对编辑器或者博客论坛系统的,但毕竟无法针对所有的编辑器都提供支持,像使用typora等编辑器直接编写markdown的人来说,就没办法使用七牛云了,况且需要对图片进行压缩处理,不然对你使用七牛云来说可能会涉及到付费了,对你的网站来说有负载,加载也慢。 所以开发一款集合图片上传和图片压缩的跨平台客户端是有多重要,而对这种需求没有比用Javafx更有快速有效的了。 图片压缩测试使用tinypng来测试对png图片的压缩看看就知道效果了 工欲善其事,必先利其器写博客,内容是关键,但是如果说复制图片粘贴图片都弄的那么麻烦,要手动打开各种网站去操作一系列才能完成一张图片的工作,那太繁琐了!工欲善其事,必先利其器,我的本职工作是做自动化测试的,见到这种情况必须自动化起来,所以我决定使用JavaFx来开发这样一款客户端。开源地址:https://github.com/cmlanche/javafx-qiniu-tinypng-client.git","categories":[],"tags":[{"name":"tinypng","slug":"tinypng","permalink":"http://www.cmlanche.com/tags/tinypng/"},{"name":"七牛云","slug":"七牛云","permalink":"http://www.cmlanche.com/tags/七牛云/"},{"name":"七牛云客户端","slug":"七牛云客户端","permalink":"http://www.cmlanche.com/tags/七牛云客户端/"},{"name":"个人博客客户端","slug":"个人博客客户端","permalink":"http://www.cmlanche.com/tags/个人博客客户端/"}]},{"title":"JavaFx新手教程-布局-StackPane","slug":"stackpane","date":"2017-12-02T14:10:37.000Z","updated":"2017-12-03T05:38:39.000Z","comments":true,"path":"2017/12/02/stackpane/","link":"","permalink":"http://www.cmlanche.com/2017/12/02/stackpane/","excerpt":"","text":"cmlanche: 您叫什么名字? StackPane cmlanche: 您好,StackPane君,可以问下您在JavaFX家族中是什么地位? stackpane君: 我可重要了,我是在JavaFx中所有布局中使用常用的,是大哥的角色,我经常罩这那帮小弟 cmlanche: 你说的小弟是指谁? stackpane君:我小弟可多了,比如著名的vbox和hbox这对孪生兄弟,还有超级明星BorderPane,还有一些不怎么出名但是也经常使用的GridPane,TiledPane,AnchorPane等等 cmlanche: 您有什么特点吗?有那么重要吗? stackpane君: 你知道多重宇宙吧?我就像那样, 在我的空间里,可以让我的孩子们共同拥有我的全部空间。对了, 默认情况下我让我的孩子们都是居中的,当然你可以调整它,在我的孩子上面增加一个属性StackPane.aligment,比如下面的代码使用了StackPane.alignment="TOP_LEFT",这样button1就在左上角的位置了! > <StackPane xmlns=\"http://javafx.com/javafx\"> xmlns:fx=\"http://javafx.com/fxml\"> fx:controller=\"com.cmlanche.javafx.layouts.stackpane.StackPaneTest\"> prefHeight=\"400.0\" prefWidth=\"600.0\" style=\"-fx-background-color: grey;\">>> <Button fx:id=\"button1\" text=\"button1\" prefWidth=\"100\" prefHeight=\"200\" StackPane.alignment=\"TOP_LEFT\">> </Button>>> <Button fx:id=\"button2\" text=\"button2\" prefWidth=\"200\" prefHeight=\"100\">> </Button>> </StackPane>> 而且我的空间大小是充满我的父空间的。看如下图的灰色区域,都是我的空间 cmlanche: 那您有哪些应用场景吗? stackpane君:比如说某一个区域需要共享,会被多个视图所共用,我就发挥作用啦。而且我可以轻易可以把元素居中,我总是铺满父布局的。 cmlanche: 有demo源码演示吗? stackpane君:我的爸爸给我做了个演示例子,是开源的哦,欢迎star,github,我自己的例子在这里","categories":[],"tags":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/tags/javafx/"},{"name":"javafx-layout","slug":"javafx-layout","permalink":"http://www.cmlanche.com/tags/javafx-layout/"},{"name":"javafx-stackpane","slug":"javafx-stackpane","permalink":"http://www.cmlanche.com/tags/javafx-stackpane/"}]},{"title":"梁文道·一千零一夜·儒学","slug":"ruxue","date":"2017-12-02T10:07:31.000Z","updated":"2017-12-02T10:29:27.000Z","comments":true,"path":"2017/12/02/ruxue/","link":"","permalink":"http://www.cmlanche.com/2017/12/02/ruxue/","excerpt":"儒家的个人始终和我们习惯的西方个人主义是不一样的观念,更加不是西方的自由主义的那套东西,儒家的个人始终是一个在社会之中的人,儒家的每一个人,每一个个体都是社会中的人,他必然是一个家庭的成员,他是一个社区邻里中的一分子,他是一个国家的一员,更是世界公民群体中的一员,更是大自然万物中的一员,每一个个人,都跟身边所有这些东西是密切联系起来的,他不是一个独立的绝对的,以己为本的一个个体。","text":"儒家的个人始终和我们习惯的西方个人主义是不一样的观念,更加不是西方的自由主义的那套东西,儒家的个人始终是一个在社会之中的人,儒家的每一个人,每一个个体都是社会中的人,他必然是一个家庭的成员,他是一个社区邻里中的一分子,他是一个国家的一员,更是世界公民群体中的一员,更是大自然万物中的一员,每一个个人,都跟身边所有这些东西是密切联系起来的,他不是一个独立的绝对的,以己为本的一个个体。 儒家特别讲究关系,要讲究名分,要讲究名位,所以我们才会听过另外一句很有名的话,叫做不在其位不谋其政,然后不在其位呢,甚至不能够作礼乐,这么听起来,儒家又好像很权威,就好像你这个人,你站在一个高位,你当领导,那你就能够说话算数,能作礼乐了,你不是领导你管那么多国家时事管那么多天下大事干什么,怪怪的种好你的地做好你的小买卖不就得了,不完全是这样,因为你还要反过来看,孔子还说什么了呢?孔子还说,“岁在其位,苟无其德,不可作礼乐”,就是说就算你有这个位置,但是你没有配得上这个位置的德行,你也是不能够作礼乐的,所以在不同的位置,人是要讲位置的,讲关系,但是每个位置,每个关系,你是不是能够在那个位置,你还要配得上那个位置,你要有那个德,那个德又是怎么来呢?就要看你对身边的人是什么样的一个状态,什么样的关系,然后同时你对身边的人是什么样的态度,你跟他如何相处,恰恰又跟你这个人自己自身的修养、学问、涵养是密切相关的,你比如说讲敬,我们现代人讲尊敬这个字,就很容易把它想象成是一个由下往上的,我看到领导说领导您好,尊敬,我看到台长,哎呀你好,尊敬,看到老板要很尊敬,这是由下对上的,但是儒家是这样子吗?不是的,你看孟子,孟子把这个敬说成是什么呢?是下对上固然要敬,但是上对下也要敬哦,上对下那个敬叫什么敬呢?那就叫做礼贤,就是上下是呼应的,所以你是不是在那个位置,你得看你配不配得上那个位置,你怎么样叫配得上在我的上位呢?那就要看你怎么样对在你下面的人,所以也就是说话我们在下面的人,也都能够决定你是不是该在那个位置,完全是靠这样相应的一个关系,这个才是儒家的真正本色。","categories":[],"tags":[{"name":"文化","slug":"文化","permalink":"http://www.cmlanche.com/tags/文化/"},{"name":"儒学","slug":"儒学","permalink":"http://www.cmlanche.com/tags/儒学/"}]},{"title":"JavaFx概要脑图","slug":"JavaFx概要脑图","date":"2017-08-25T03:50:15.000Z","updated":"2019-04-15T06:18:36.000Z","comments":true,"path":"2017/08/25/JavaFx概要脑图/","link":"","permalink":"http://www.cmlanche.com/2017/08/25/JavaFx概要脑图/","excerpt":"本脑图是我自己总结的在JavaFx开发中的要点。","text":"本脑图是我自己总结的在JavaFx开发中的要点。 如上","categories":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/categories/javafx/"}],"tags":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/tags/javafx/"},{"name":"javafx脑图","slug":"javafx脑图","permalink":"http://www.cmlanche.com/tags/javafx脑图/"}]},{"title":"Jenkins自动集成小记","slug":"Jenkins自动集成小记(一)","date":"2017-08-10T14:33:02.000Z","updated":"2017-12-02T09:27:07.000Z","comments":true,"path":"2017/08/10/Jenkins自动集成小记(一)/","link":"","permalink":"http://www.cmlanche.com/2017/08/10/Jenkins自动集成小记(一)/","excerpt":"Jenkins是用来自动构建任务的,也许你还不知道什么叫自动构建任务,它的意思是可以针对某个任务进行自动化,比如你开发的某个软件,每次写完代码提交到github之后,你可以设置让Jenkins自动进行打包构建发布包或者进行Units测试,发布报告,不用你每次手工在IDE中去Build,尤其是当打包非常繁琐的时候,用自动化构建可以极大的提高工作效率。","text":"Jenkins是用来自动构建任务的,也许你还不知道什么叫自动构建任务,它的意思是可以针对某个任务进行自动化,比如你开发的某个软件,每次写完代码提交到github之后,你可以设置让Jenkins自动进行打包构建发布包或者进行Units测试,发布报告,不用你每次手工在IDE中去Build,尤其是当打包非常繁琐的时候,用自动化构建可以极大的提高工作效率。 前言公司是同事使用Jenkins做的自动化构建,非常方便,每次提交代码,自动检测打包是否成功,然后发送邮件通知,因为我们集成了findingbugs插件,在打包前还会对代码进行findingbus检索,告诉你findingbus的状态,还会生成release的发布包,非常之方便。 wement.io这个项目我想多人协作开发,自然想到了Jenkins,让大家能够配合的非常的舒服,昨天折腾了三小时,搞定了Jenkins,其中包括怎么创建一个任务,怎么设置github,和github的项目挂钩,怎么设置邮件通知。 安装Jenkins直接去Jenkins官网下载war包,我这里是直接部署在阿里云的centos7上的,然后用scp xxx.war root@yourip:/home直接从本地拷贝到云服务器上,拷贝之后直接用java -jar xxx.war就可以运行成功了,如果你期望退出命令行还不关闭Jenkins的话,就用命令nohug java -jar xxx.war(centos 用nohup)来运行包,java进程自动在后台运行了,关闭命令行窗口也不会退出程序,这样你的Jenkins就运行了,默认端口监听在8080,如果你也是用的云服务器,请确保你的8080端口是允许访问的。 当然,要运行Jenkins,你需要安装Java环境,如果你的代码仓库用git的话请安装git,分别命令如下: yum install java yum install git 还没有完,Jenkins运行成功了,你再在浏览器上打开yourip:8080跟随Jenkins安装向导,用默认提示安装完成即可,接下来就是创建任务了。 创建一个任务 选择第一性,构建一个自由风格的软件项目 然后保存即可,任务创建完成。但是如果你的项目是私有项目,你需要在你的服务器上生成ssh,然后贴到你的github上,这样做的目的是然github信任你的这个服务器,同时要注意你的Jenkins控制台,有可能要你输入ssh的证书密码。 邮件通知一般来说这个时候右键通知是没问题的,但是我今天发现我提交代码之后,任务并没有自动构建,然后看Jenkins打的log是这样的: 22:23:55 [WARNING] mvn dependency:tree -Ddetail=true and the above output.22:23:55 [WARNING] See http://maven.apache.org/plugins/maven-shade-plugin/22:23:56 [INFO] ------------------------------------------------------------------------22:23:56 [INFO] BUILD SUCCESS22:23:56 [INFO] ------------------------------------------------------------------------22:23:56 [INFO] Total time: 10.112s22:23:56 [INFO] Finished at: Thu Aug 10 22:23:56 CST 201722:23:56 [INFO] Final Memory: 20M/59M22:23:56 [INFO] ------------------------------------------------------------------------22:23:56 Archiving artifacts22:23:56 Email was triggered for: Always22:23:56 Sending email for trigger: Always22:23:56 Not sending mail to unregistered user chengming@testin.cn because your SCM claimed this was associated with a user ID ‘chengming' which your security realm does not recognize; you may need changes in your SCM plugin22:23:56 An attempt to send an e-mail to empty list of recipients, ignored.22:23:56 Finished: SUCCESS 意思是构建成功了,但是发送邮件发现提交代码的用户名是chengming而不是我在Jenkins中设置的cmlanche,然后Jenkins自动忽略了发送邮件的这个行为。 然后我在我的sourcetree中把提交代码的用户换回了cmlanche,任务自动构建,邮件发送成功! Jenkins分享(一)小结Jenkins是一个非常方便的工具,免费开源,推荐大家都来学习,都折腾下,对你以后大有好处。我其实也是刚刚学这个,知道的还不多,还需要花很多时间去折腾,有更新的收获我会慢慢分享出来。 欢迎加入我的Java栈群:518914410,讨论有关Java的一切技术。 参考另一个小菜的文章:Jenkins+Github持续集成","categories":[],"tags":[{"name":"jenkins","slug":"jenkins","permalink":"http://www.cmlanche.com/tags/jenkins/"}]},{"title":"JavaFx TableView疑难详解","slug":"JavaFx-TableView详解","date":"2017-06-08T02:11:14.000Z","updated":"2019-04-15T06:18:41.000Z","comments":true,"path":"2017/06/08/JavaFx-TableView详解/","link":"","permalink":"http://www.cmlanche.com/2017/06/08/JavaFx-TableView详解/","excerpt":"TableView是个十分有用的控件,适应性和灵活性非常强,可以对它进行任意的修改,比如界面样式、功能。本文将从一步步提问的方式讲解TableView","text":"TableView是个十分有用的控件,适应性和灵活性非常强,可以对它进行任意的修改,比如界面样式、功能。本文将从一步步提问的方式讲解TableView 创建已知列的TableView已知列的表格的创建,需要把TableView的TableColumn关联到模型的属性,TableView是个模板类,其实是TableView,这个T就是模型,例如下代码: // MyModel.javapublic class MyModel{ private String name; private String url; // getters, setters ...}// init your tableViewTableColumn<MyModel, String> t1 = new TableColumn();// 关联MyModel中的name属性t1.setCellValueFactory(new PropertyValueFactory<>(\"name\")); t1.setCellFactory(p->{ // 创建此列的Cell的时候的回调,允许让你自己去创建}); 特别要说明的是,setCellValueFactory和setCellFactory不是冲突的,我用的时候一直以为是冲突,就是只能用其中一个,另外一个就失效了,其实不是,setCellFactory它的意图是在创建这列的时候要做的事情,你可以改变TableCell的任何内容,包括UI和Value,而setCellValueFactory呢,它的重点是关联属性,从你传递给它的Model中通过对应属性的getter来获取值。在setCellFactory中的TableCell有个回调,叫updateItem,它可以获取到你设置到此Cell的值,这个值是跟setCellValueFactory所关联的属性有关。 创建动态列的TableView参考:https://community.oracle.com/thread/2474328 因为列是不定的,模型是没有属性对应的,创建列的时候你根本不知道列是什么,看如下实现代码: column.setCellValueFactory(param -> { ObservableList<VarCell> values = param.getValue(); if (columnIndex >= values.size()) { return new SimpleObjectProperty<>(null); } else { return new SimpleObjectProperty<>(param.getValue().get(columnIndex)); } }); 创建动态列的tableview,它的模型是一个ObservableList<T>,你的setCellValueFactory不能使用PropertyValueFactory,而是如上代码所示,通过列的索引来获取此列的值 [列拖动] 如何捕获列拖动事件?列拖动是tablview一个默认的自带的效果,但是并没有专门的事件给你去监听它,而是监听列的变化,方法:给tableview的columns添加Listener,判断变动列的状态是否是replaced的状态,例如: tableView.getColumns().addListener(new ListChangeListener<TableColumn<ObservableList<VarCell>, ?>>() { @Override public void onChanged(Change<? extends TableColumn<ObservableList<VarCell>, ?>> change) { change.next(); if (change.wasReplaced()) { // 表示当前拖动过了 } } }); [列拖动] 如何防止第一列被拖动?在上一问的基础上,实现第一列不允许被拖动的功能。 参考:https://stackoverflow.com/questions/30645606/javafx-restrict-column-rearrangement-on-drag-and-drop tableView.getColumns().addListener(new ListChangeListener<TableColumn<ObservableList<String>, ?>>() { private boolean suspended; @Override public void onChanged(Change<? extends TableColumn<ObservableList<String>, ?>> change) { change.next(); if (change.wasReplaced() && !suspended) { List<TableColumn<ObservableList<String>, ?>> oldList = new ArrayList<>(change.getRemoved()); List<TableColumn<ObservableList<String>, ?>> newList = new ArrayList<>(tableView.getColumns()); // first column changed => revert to original list if (oldList.get(0) != newList.get(0)) { this.suspended = true; tableView.getColumns().setAll(oldList); this.suspended = false; } } } }); 上面的代码中有三个关键的地方,是tableview原本提供的api,一个是change.wasReplaced表示当前的变动是否被替换了,第二个是change.getRemoved,表示获取要移除掉的列,进一步的意思就是原来的列,也就是此前的tablecolumns,第三个是tableview.getColumns这个是获取现在列,有了这些信息,就可以判断,oldList.get(0)!=newList(0),表示如果新老列的第一列不相同,表示是第一列是变动的,但是我们不允许变动,因此,调用tableview.getColumns().setAll(oldList)用来恢复原来的列。这样就禁止拖动第一列了。 [列拖动] 如何禁用列拖动效果?参考:https://stackoverflow.com/questions/22202782/how-to-prevent-tableview-from-doing-tablecolumn-re-order-in-javafx-8 给列设置一个属性:column.impl_setReorderable(false); impl_setReorderable前面带impl_前缀,表示它是一个将来可能会被删除的方法,但是为了解决目前无法解决的问题,暂时把impl的私有方法改为了public方法,参考我的博客中的如何自定义Taborder的文章,是一样的道理。 [行拖动] 如何拖动行,进行换行?参考:https://stackoverflow.com/questions/28603224/sort-tableview-with-drag-and-drop-rows 已经测试过的代码: tableView.setRowFactory(tv -> { TableRow<ObservableList<String>> row = new TableRow<>(); row.setOnDragDetected(event -> { log.info(\"row drag detected\"); if (!row.isEmpty()) { Integer index = row.getIndex(); Dragboard db = row.startDragAndDrop(TransferMode.MOVE); db.setDragView(row.snapshot(null, null)); ClipboardContent cc = new ClipboardContent(); cc.put(SERIALIZED_MIME_TYPE, index); db.setContent(cc); event.consume(); } }); row.setOnDragOver(event -> { log.info(\"row drag over\"); Dragboard db = event.getDragboard(); if (db.hasContent(SERIALIZED_MIME_TYPE)) { if (row.getIndex() != ((Integer) db.getContent(SERIALIZED_MIME_TYPE)).intValue()) { event.acceptTransferModes(TransferMode.COPY_OR_MOVE); event.consume(); } } }); row.setOnDragDropped(event -> { log.info(\"row drag dropped\"); Dragboard db = event.getDragboard(); if (db.hasContent(SERIALIZED_MIME_TYPE)) { int draggedIndex = (Integer) db.getContent(SERIALIZED_MIME_TYPE); ObservableList<String> draggedPerson = tableView.getItems().remove(draggedIndex); int dropIndex; if (row.isEmpty()) { dropIndex = tableView.getItems().size(); } else { dropIndex = row.getIndex(); } tableView.getItems().add(dropIndex, draggedPerson); event.setDropCompleted(true); tableView.getSelectionModel().select(dropIndex); event.consume(); } }); return row; }); 修改TableView样式使用css,参考如下我的测试代码 .table-view { -fx-border-width: 1px; -fx-border-color: #CACACA; -fx-background-color: transparent;}.table-view:focused { -fx-background-color: transparent;}.table-view .table-cell { -fx-font-size: 12px;}.table-view .filler { -fx-background-color: #BDE8FF;}.table-view .text { -fx-text-fill: red;}.table-view .column-header { -fx-background-color: #BDE8FF; -fx-pref-height: 37px; -fx-border-width: 1px; -fx-border-color: #D9D9D9; -fx-border-insets: -2px -2px 0px -2px;}.table-view .column-header-background .label { -fx-text-fill: #363739; -fx-font-weight: normal; -fx-font-size: 12px;}.table-row-cell { /*行高*/ -fx-cell-size: 35px;}.table-row-cell .cell { -fx-alignment: center; -fx-text-fill: #333333;}.table-view .table-cell:selected { -fx-text-fill: white;}.table-view .table-column .column-header { -fx-background-color: #363739;}.cell { /*-fx-border-width: 0px 1px 0 0;*/ /*-fx-border-color: #CACACA;*/}.table-view .scroll-bar { -fx-background-color: transparent;}.viewport { -fx-background-color: white;} 如何自定义列头,比如自己设置一个可编辑的列头呢?答案是给你的TableColumn设置setGraphic,可编辑的列头的话,你让你的graphic中有编辑框,双击显示编辑框,按enter键确认编辑,如下是一个我的实现: package com.itestin.ui.datamgt.table;import com.itestin.ui.recordNreplay.logic.CommonLogic;import javafx.beans.property.BooleanProperty;import javafx.beans.property.SimpleBooleanProperty;import javafx.beans.property.SimpleStringProperty;import javafx.beans.property.StringProperty;import javafx.geometry.Pos;import javafx.scene.control.Label;import javafx.scene.control.TableColumn;import javafx.scene.control.TextField;import javafx.scene.control.Tooltip;import javafx.scene.input.KeyCode;import javafx.scene.layout.StackPane;/** * Created by cmlanche on 2017/6/1. */public class EditColumn extends StackPane { private EditColumnCallback callback; private Label label; private TextField textField; private StringProperty title; private BooleanProperty editing; private BooleanProperty editable; private BooleanProperty textFieldFocus; private TableColumn tableColumn; private String oldTitle; public EditColumn(TableColumn tableColumn) { super(); this.tableColumn = tableColumn; this.init(); } private void init() { this.setMaxWidth(120); this.setAlignment(Pos.CENTER); this.setStyle(\"-fx-background-color: #D9D9D9;\"); label = new Label(); label.setStyle(\"-fx-font-size: 12px; -fx-text-fill: #333333;\"); textField = new TextField(); textField.getStyleClass().add(\"tablefx-header-editor\"); label.textProperty().bindBidirectional(titleProperty()); textField.textProperty().bindBidirectional(titleProperty()); label.setTooltip(new Tooltip()); label.getTooltip().textProperty().bindBidirectional(label.textProperty()); this.getChildren().addAll(label, textField); editingProperty().addListener((observable, oldValue, newValue) -> { if (isEditable()) { if (newValue) { label.setVisible(false); textField.setVisible(true); textField.setFocusTraversable(true); textField.requestFocus(); } else { label.setVisible(true); textField.setVisible(false); } } }); textField.textProperty().addListener((observable, oldValue, newValue) -> { textField.setStyle(\"-fx-border-color: #25B3FA;\"); }); this.setStyle(\"-fx-background-color: transparent;\"); textField.setOnKeyPressed(event -> { if (isEditing()) { if (event.getCode() == KeyCode.ENTER) { event.consume(); // 放置对tabview的其他列产生影响,不让消息透传 oldTitle = textField.getText(); setEditing(false); // 提交编辑 if (callback != null) { callback.editCommit(tableColumn, textField.getText()); } } else if (event.getCode() == KeyCode.ESCAPE) { setTitle(oldTitle); setEditing(false); // 取消编辑 if (callback != null) { callback.cancelEdit(); } } else if (event.getCode() == KeyCode.TAB) { setEditing(false); } } }); textFieldFocusProperty().addListener((observable, oldValue, newValue) -> { if (newValue) { textField.setFocusTraversable(true); textField.requestFocus(); } }); setEditing(false); } public String getTitle() { return titleProperty().get(); } public StringProperty titleProperty() { if (title == null) { title = new SimpleStringProperty(); } return title; } public void setTitle(String title) { this.oldTitle = title; this.titleProperty().set(title); } public boolean isEditing() { return editingProperty().get(); } public BooleanProperty editingProperty() { if (editing == null) { editing = new SimpleBooleanProperty(true); } return editing; } public void setEditing(boolean editing) { this.editingProperty().set(editing); } public void setEditCallback(EditColumnCallback callback) { this.callback = callback; } public boolean isEditable() { return editableProperty().get(); } public BooleanProperty editableProperty() { if (editable == null) { editable = new SimpleBooleanProperty(true); } return editable; } public void setEditable(boolean editable) { this.editableProperty().set(editable); } public boolean isTextFieldFocus() { return textFieldFocusProperty().get(); } public BooleanProperty textFieldFocusProperty() { if (textFieldFocus == null) { textFieldFocus = new SimpleBooleanProperty(); } return textFieldFocus; } public void setTextFieldFocus(boolean textFieldFocus) { this.textFieldFocusProperty().set(textFieldFocus); }} 最后欢迎加入我的javafx探讨群:518914410本文版权原创 by cmlanche.com","categories":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/categories/javafx/"}],"tags":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/tags/javafx/"},{"name":"javafx-tableview","slug":"javafx-tableview","permalink":"http://www.cmlanche.com/tags/javafx-tableview/"}]},{"title":"JavaFx新手入门布局介绍","slug":"JavaFx新手入门布局介绍","date":"2017-06-06T12:57:29.000Z","updated":"2019-04-15T06:19:01.000Z","comments":true,"path":"2017/06/06/JavaFx新手入门布局介绍/","link":"","permalink":"http://www.cmlanche.com/2017/06/06/JavaFx新手入门布局介绍/","excerpt":"JavaFx新手入门,首先应该从布局容器入手,常用的布局容器有StackPane, BorderPane, HBox, VBox等,在使用他们时,特别是手动编写fxml布局文件时,了解他们的特性可以加速你的编写过程,能够活学活用。本文一次性介绍他们的特点。","text":"JavaFx新手入门,首先应该从布局容器入手,常用的布局容器有StackPane, BorderPane, HBox, VBox等,在使用他们时,特别是手动编写fxml布局文件时,了解他们的特性可以加速你的编写过程,能够活学活用。本文一次性介绍他们的特点。 StackPane StackPane总是充满父容器,并且,它的所有子节点Node都共用StackPane所占有的区域。所以在某个区域需要变动不同的内容的时候,经常用到它。 BorderPane BorderPane同样总是充满父容器,它同时能够控制子节点在它所占空间的方位,总共有5个方位,分别是上、下、左、右、中,不需要所有的方位都有节点,但是中间总会占有一块区域,这点需要注意。BorderPane是最为常用的容器,举例,你要两列布局,你可以用在给左侧和中间的区域设置视图,或者给中间和右侧设置,如果你给左侧和右侧设置,会发现中间会空一个区域。 HBox HBox的子节点中有容器的话,高度会被拉伸到Hbox一样的高度,但是宽度是由子节点它自身决定的。 VBox VBox的决定了子节点中的宽度,子节点的高度由子节点自己决定。 GridPane GridPane是网格布局,你可以指定它的子节点处于网格的哪行哪列。","categories":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/categories/javafx/"}],"tags":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/tags/javafx/"},{"name":"javafx-layout","slug":"javafx-layout","permalink":"http://www.cmlanche.com/tags/javafx-layout/"}]},{"title":"JavaFx新手入门之布局 - SplitPane分隔容器","slug":"JavaFx新手入门之布局SplitPane分隔容器","date":"2017-06-06T12:37:10.000Z","updated":"2019-04-15T06:18:31.000Z","comments":true,"path":"2017/06/06/JavaFx新手入门之布局SplitPane分隔容器/","link":"","permalink":"http://www.cmlanche.com/2017/06/06/JavaFx新手入门之布局SplitPane分隔容器/","excerpt":"JavaFx新手入门,首先应该从布局容器入手,常用的布局容器有StackPane, BorderPane, HBox, VBox等,本文介绍相对使用频率较少的SplitPane,分隔容器","text":"JavaFx新手入门,首先应该从布局容器入手,常用的布局容器有StackPane, BorderPane, HBox, VBox等,本文介绍相对使用频率较少的SplitPane,分隔容器 怎么创建分隔的视图区域? SplitPane的一级子Node就是SplitPane所分开的一个视图区域,例如下fxml代码: <?import javafx.scene.control.SplitPane?><?import javafx.scene.image.Image?><?import javafx.scene.image.ImageView?><?import javafx.scene.layout.StackPane?><?import javafx.scene.layout.VBox?><StackPane fx:controller=\"sample.Controller\" xmlns:fx=\"http://javafx.com/fxml\" alignment=\"center\"> <SplitPane fx:id=\"splitpane\"> <VBox fx:id=\"left\" prefWidth=\"300\" prefHeight=\"200\" style=\"-fx-background-color: antiquewhite\"> <ImageView fx:id=\"image1\" preserveRatio=\"true\" pickOnBounds=\"true\"> <image> <Image url=\"@/sample/test.jpg\"/> </image> </ImageView> </VBox> <VBox fx:id=\"right\" prefWidth=\"300\" prefHeight=\"300\" style=\"-fx-background-color: aquamarine\"> <ImageView fx:id=\"image2\" preserveRatio=\"true\" pickOnBounds=\"true\"> <image> <Image url=\"@/sample/test.jpg\"/> </image> </ImageView> </VBox> </SplitPane></StackPane> SplitPane中有图片的话,怎么让ImageView随着视图区域的变化而变化? 答案是:监听SplitPane的divider的变化,动态去改变ImageView的大小 @Override public void initialize(URL location, ResourceBundle resources){ splitpane.getDividers().get(0).positionProperty().addListener((observable, oldValue, newValue) -> { System.out.println(newValue); int w = 600; double w1 = w * newValue.doubleValue(); double w2 = w - w1; image1.setFitWidth(w1); image2.setFitWidth(w2); }); }","categories":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/categories/javafx/"}],"tags":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/tags/javafx/"},{"name":"splitpane","slug":"splitpane","permalink":"http://www.cmlanche.com/tags/splitpane/"}]},{"title":"初入vertx遇到的问题,可能你也遇到了","slug":"初入vertx遇到的问题,可能你也遇到了","date":"2017-05-29T04:35:39.000Z","updated":"2018-07-17T05:20:36.000Z","comments":true,"path":"2017/05/29/初入vertx遇到的问题,可能你也遇到了/","link":"","permalink":"http://www.cmlanche.com/2017/05/29/初入vertx遇到的问题,可能你也遇到了/","excerpt":"wement.io的服务器准备采用vertx来开发,vertx的开发速度非常快,有很多写好的组件,这篇文章作为学习vertx的第一篇文章,总结了一些新手可能会遇到的问题。","text":"wement.io的服务器准备采用vertx来开发,vertx的开发速度非常快,有很多写好的组件,这篇文章作为学习vertx的第一篇文章,总结了一些新手可能会遇到的问题。 vertx关于对StaticHandler的使用的理解vertx中的StaticHandle是一个非常有用的处理静态资源的处理器,里面已经帮你实现了对静态资源的各种访问设置,但是使用过程中,我把/*映射给了静态资源webroot,发现我无法添加新的route了,新route添加了都不接受事件响应。 原因就是所有请求都让StaticHanle拦截了,对StaticHanle的使用应该让它走一个新的不一样的route,比如/static/*,这样的好处是,区分开了静态资源的访问,同时保护你的资源目录,这样修改了之后,你的webroot中的网页链接应该都加上/static/前缀,因为他们都会通过StaticHanle拦截,没有/static/的话,你的server是不认识的。 怎样添加新route并指向静态资源的某个网页呢?比如/about我需要让它能够访问静态资源中的/webroot/about.html,因为我可能需要传递数据渲染网页中 代码示例: router.route(\"/about\").handler(rc->{ rc.reroute(\"/static/about.html\");}) 对,就是reroute,同时注意你的路径,需要加/static/前缀 这是网友大神赵尘恩给的一个解释性描述 route原理就是从上往下找match的路径,reroute就是把拦截到的再重新转发一下,如果reroute到同一个path容易造成死循环,还有web是通过前缀区分的,所以如果用/*来route的话,就会把所有的请求全部拦截到,应该加上/static/*来拦截 使用vertx的oauth2客户端验证授权github 直接撸代码把 初始化: private void init() { JsonObject credentials = new JsonObject() .put(\"clientID\", \"your github client id\") .put(\"clientSecret\", \"your github client secret\") .put(\"site\", \"https://github.com/login\") .put(\"tokenPath\", \"/oauth/access_token\") .put(\"authorizationPath\", \"/oauth/authorize\"); githubAuth = OAuth2Auth.create(vertx, OAuth2FlowType.AUTH_CODE, credentials); } 路由: router.route(\"/login/github\").handler(routingContext -> login_github(routingContext)); router.route(\"/oauth/github/code\").handler(routingContext -> oauth_github_code(routingContext)); 这样简历2个路由,分别用来请求授权,获取authcode /** * 请求github登录 * * @param rc */ private void login_github(RoutingContext rc) { String authorization_uri = githubAuth.authorizeURL(new JsonObject() .put(\"redirect_uri\", getPath() + \"/oauth/github/code\") .put(\"scope\", \"no_expiry\")); info(authorization_uri); rc.response().putHeader(\"Location\", authorization_uri) .setStatusCode(302) .end(); } /** * 获取到github的oauth code * * @param rc */private void oauth_github_code(RoutingContext rc) { String code = rc.request().getParam(\"code\"); if (code != null) { githubAuth.getToken(new JsonObject().put(\"code\", code).put(\"redirect_uri\", getPath() + \"/oauth/github/token\"), res -> { if (res.failed()) { // error, the code provided is not valid info(\"failed\"); } else { // save the token and continue... info(\"success\"); AccessToken token = res.result(); info(token.principal().toString()); String access_token = token.principal().getString(\"access_token\"); rc.response().end(access_token); } }); }} 80端口问题我使用的IDE是idea,测试发现让vertx去绑定80端口,提示严重: java.net.SocketException: Permission denied,如果你的端口被占用了,执行下面两个步骤 检查80端口是否被占用:lsof -i:80 如果被占用,则杀死进程:kill -9 [pid] 发现结果还是无权限,后来才发现原因:执行运行java程序没有sudo,尚不知怎么解决怎么从idea执行,不过你可以先打包出来,然后使用sudo java -jar XXX-production.jar来执行你的生成jar包 vertx给我的感觉vertx非常灵活,然后又帮你做了非常多的大家都要做的一致性的工作,一致性的意思就是,代码都是那样,是共同的,非常喜欢vertx,之前学过play2,springmvc之类的java web框架,最后都前者因为极少中文文档,需要翻墙,开发者更是少之又少,国内也没见有推广它的,后缀因为过于庞大,要学的东西太多而放弃了,一直在寻找一个像vertx一样的java web框架,现在终于找到了。 欢迎加入我们的社群讨论技术JavaFx最大最活跃的社群:518914410 Vertx中国用户组:515203212","categories":[],"tags":[{"name":"vertx","slug":"vertx","permalink":"http://www.cmlanche.com/tags/vertx/"}]},{"title":"wement.io介绍文档","slug":"wement.io介绍文档","date":"2017-05-27T10:16:45.000Z","updated":"2017-12-02T09:27:24.000Z","comments":true,"path":"2017/05/27/wement.io介绍文档/","link":"","permalink":"http://www.cmlanche.com/2017/05/27/wement.io介绍文档/","excerpt":"wement.io是我准备开源做的一个开源公共的评论服务,分为评论插件,数据存储服务,广告服务。developing中,敬请期待","text":"wement.io是我准备开源做的一个开源公共的评论服务,分为评论插件,数据存储服务,广告服务。developing中,敬请期待 What is wement.ioWement.io is a common comment system for all users whoever wants to quickly have a comment system. It has a shared powerfull server to provide data storage service. and it provide user ad service to ensure the benefits of users. Comment pluginThe goal of wement.io is provide a common comment plugin for users. you can directly use the api of wement.io to fetch or post data from wement.io. also for wordpress, z-blog, hexo, hugo and any other blog system, you can install the plugin which is already written by excellent coders, it really very simple and easy. Data storage serviceWement.io have a powerful server which is provide for data storage service, you can call api to post or get your websites comments. Ads serviceWhy you write things, but you can not get benefits, there are lots of vampires to squeeze your efforts with no paid for you. wement.io provide ads service for your website, all benefits are yours. Open source & servicesWement.io is open source software and service. we’d like to invite you to get join us. wement.io","categories":[],"tags":[{"name":"wement.io","slug":"wement-io","permalink":"http://www.cmlanche.com/tags/wement-io/"}]},{"title":"记录JavaFx中非常重要的细节","slug":"JavaFx细节","date":"2017-05-08T12:48:58.000Z","updated":"2019-04-15T06:18:21.000Z","comments":true,"path":"2017/05/08/JavaFx细节/","link":"","permalink":"http://www.cmlanche.com/2017/05/08/JavaFx细节/","excerpt":"JavaFx中有一些疑难杂症,或许你以为你掌握了JavaFx,但是也未必知道我所说的这些问题和解决方案,如果有帮助到你的,可以加群最大最活跃的JavaFx社群:518914410 欢迎访问我的个人博客www.cmlanche.com","text":"JavaFx中有一些疑难杂症,或许你以为你掌握了JavaFx,但是也未必知道我所说的这些问题和解决方案,如果有帮助到你的,可以加群最大最活跃的JavaFx社群:518914410 欢迎访问我的个人博客www.cmlanche.com 已经解决的细节: 鼠标单击、双击以及多击事件(大于三次基本无意义)的执行。 从图中看出,单击一次执行一次,双击,执行两次,click中走了两次,一次clickcount为1,另一次为2,多击和双击同理 ComboBox<T>这个控件中的T模板类型如果换成一个JavaBean类型的话,控件所选择的值和列表显示什么数据呢? 如果你入门了,你告诉我,会显示JavaBean对象中的toString()返回的值,是的,没错,但是加入这个JavaBean是你无法更改的,又或者说你更改它会不美观,会破坏你写代码的美感,没错,的确会这样。 然后更有经验的人会告诉我,你可以用setCellFactory来自己定义列表的值,确实列表的值变成了你想要的值,但是你同样会发现控件所选择的值还是toString()所表达的值。这怎么解决呢? 其实ComboBox中有个叫StringConverter的东西,由它完成自定义转换,示例: public class JavaBean{ public String p1; public String p2;}ComboBox<JavaBean> combox = new ComboBox<>();combox.setConverter(new StringConverter<JavaBean>(){ @Override public String toString(JavaBean bean){ return String.format(\"%s(%s)\", bean.p1, bean.p2); } @Override public JavaBean fromString(String str){return null;}}); 其实只要设置StringConverter就可以了,不用设置cellfactory,后者更多的是用来更改ui外观的 处于未知的细节问题: 是否可以自定义系统提供的标题栏,这样更加省事","categories":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/categories/javafx/"}],"tags":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/tags/javafx/"},{"name":"javafx细节记录","slug":"javafx细节记录","permalink":"http://www.cmlanche.com/tags/javafx细节记录/"}]},{"title":"Hexo使用经验总结","slug":"hexo使用总结","date":"2017-05-06T08:28:04.000Z","updated":"2017-12-02T09:24:46.000Z","comments":true,"path":"2017/05/06/hexo使用总结/","link":"","permalink":"http://www.cmlanche.com/2017/05/06/hexo使用总结/","excerpt":"首页的文章列表显示文章摘要,而不是整段文章:Hexo主页显示摘要","text":"首页的文章列表显示文章摘要,而不是整段文章:Hexo主页显示摘要 首页的文章列表显示文章摘要,而不是整段文章:Hexo主页显示摘要 给github page设置域名后,有个过程是往你的github仓库中添加CNAME文件,写上你的域名,比如我的cmlanche.com,但是hexo每次deploy的时候会清空仓库,这样的话CNAME文件也被删除了,解决办法参考","categories":[],"tags":[{"name":"hexo","slug":"hexo","permalink":"http://www.cmlanche.com/tags/hexo/"}]},{"title":"JavaFx自定义Tab-Order","slug":"JavaFx自定义Tab Order","date":"2017-05-02T10:12:46.000Z","updated":"2019-04-15T06:18:26.000Z","comments":true,"path":"2017/05/02/JavaFx自定义Tab Order/","link":"","permalink":"http://www.cmlanche.com/2017/05/02/JavaFx自定义Tab Order/","excerpt":"Tab-order是什么?在界面上当你按tab键触发焦点转移的功能,这就是tab order。但是Javafx有个缺陷就是不方便自己设置tab-order的顺序。","text":"Tab-order是什么?在界面上当你按tab键触发焦点转移的功能,这就是tab order。但是Javafx有个缺陷就是不方便自己设置tab-order的顺序。 15年JDK爆出这个bug,有人提过: https://bugs.openjdk.java.net/browse/JDK-8090501 https://bugs.openjdk.java.net/browse/JDK-8091673 最后JDK中迫不得已临时把Parent类中的私有方法setImpl_traversalEngine设置为了public,让用户可以设置Node自己的tab-order顺序。 相关解决方案 stackoverflow 示例: fxml文件 <?xml version=\"1.0\" encoding=\"UTF-8\"?><?import javafx.scene.control.Button?><?import javafx.scene.control.Label?><?import javafx.scene.control.TextField?><?import javafx.scene.layout.BorderPane?><?import javafx.scene.layout.VBox?><?import java.net.URL?><BorderPane fx:id=\"root\" xmlns=\"http://javafx.com/javafx/8.0.112\" xmlns:fx=\"http://javafx.com/fxml/1\" fx:controller=\"com.cmlanche.easymvvmfx.ui.login.LoginView\"> <center> <Label style=\"-fx-font-size: 32\" text=\"hello world\"/> </center> <bottom> <VBox fx:id=\"testbox\"> <Button fx:id=\"btn\" text=\"tray\"> </Button> <TextField fx:id=\"t1\"/> <TextField fx:id=\"t2\"/> <TextField fx:id=\"t3\"/> </VBox> </bottom></BorderPane> Controller文件 package com.cmlanche.easymvvmfx.ui.login;import com.fx.base.mvvm.BaseView;import com.sun.javafx.scene.traversal.Algorithm;import com.sun.javafx.scene.traversal.Direction;import com.sun.javafx.scene.traversal.ParentTraversalEngine;import com.sun.javafx.scene.traversal.TraversalContext;import javafx.fxml.FXML;import javafx.scene.Node;import javafx.scene.control.Button;import javafx.scene.control.TextField;import javafx.scene.input.MouseEvent;import javafx.scene.layout.VBox;import tray.notification.NotificationType;import tray.notification.TrayNotification;/** * Created by cmlanche on 2016/12/9. */public class LoginView extends BaseView<LoginViewModel> { @FXML Button btn; @FXML TextField t1; @FXML TextField t2; @FXML TextField t3; @FXML VBox testbox; @Override protected void onViewCreated() { t2.setFocusTraversable(true); t2.requestFocus(); testbox.setImpl_traversalEngine(new ParentTraversalEngine(testbox, new Algorithm() { @Override public Node select(Node owner, Direction dir, TraversalContext context) { if (\"t2\".equals(owner.getId())) { return t3; } else if (\"t3\".equals(owner.getId())) { return t1; } else if (\"t1\".equals(owner.getId())) { return btn; } else { return t2; } } @Override public Node selectFirst(TraversalContext context) { return t2; } @Override public Node selectLast(TraversalContext context) { return t1; } })); }} 原来的tab-order顺序是btn->t1->t2->t3,现在的顺序是t2->t3->t1->btn。 需要注意的是 setImpl_traversalEngine是deprecated方法,以后可能废弃的api LoginView是我的框架easyMvvmFx构建的控制器,不能直接放在你代码中运行。 欢迎加我的qq群探讨JavaFx 最大最活跃的JavaFx社群 518914410","categories":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/categories/javafx/"}],"tags":[{"name":"javafx","slug":"javafx","permalink":"http://www.cmlanche.com/tags/javafx/"},{"name":"javafx-tab-order","slug":"javafx-tab-order","permalink":"http://www.cmlanche.com/tags/javafx-tab-order/"}]},{"title":"hexo网站的搭建实践与经验感触分享","slug":"hexo网站的搭建实践与经验感触分享","date":"2017-04-02T01:55:30.000Z","updated":"2019-05-14T08:53:10.000Z","comments":true,"path":"2017/04/02/hexo网站的搭建实践与经验感触分享/","link":"","permalink":"http://www.cmlanche.com/2017/04/02/hexo网站的搭建实践与经验感触分享/","excerpt":"做个人博客网站,从我粗鄙的见识认为,有三类,一类是不懂技术的小白,使用wordpress,当然我虽然是搞技术的,在很长一段时间我长期使用wordpress,因为它非常简单容易上手,傻瓜式的操作,而且内容也非常容易管理,第二类呢是使用hexo、hugo等工具自己在服务器上进行复杂的配置然后才能搭建,我呢现在属于这类,第三类呢,是在第二类的基础上,能够自己开发网站的样式,这个需要比较深厚的前端技术,你需要懂js、html、css等很多知识,还需要很多实践才能做。","text":"做个人博客网站,从我粗鄙的见识认为,有三类,一类是不懂技术的小白,使用wordpress,当然我虽然是搞技术的,在很长一段时间我长期使用wordpress,因为它非常简单容易上手,傻瓜式的操作,而且内容也非常容易管理,第二类呢是使用hexo、hugo等工具自己在服务器上进行复杂的配置然后才能搭建,我呢现在属于这类,第三类呢,是在第二类的基础上,能够自己开发网站的样式,这个需要比较深厚的前端技术,你需要懂js、html、css等很多知识,还需要很多实践才能做。 我觉得每个写个人博客的作者都是孤独的,特别是刚开始写的时候,明明没有人关注,却要一个人默默的写东西,你期待着与很多人对话,但是当时一个人都没有,一种孤独的,难以持续坚持的感觉油然而生。 做个人博客网站,从我粗鄙的见识认为,有三类,一类是不懂技术的小白,使用wordpress,当然我虽然是搞技术的,在很长一段时间我长期使用wordpress,因为它非常简单容易上手,傻瓜式的操作,而且内容也非常容易管理,第二类呢是使用hexo、hugo等工具自己在服务器上进行复杂的配置然后才能搭建,我呢现在属于这类,第三类呢,是在第二类的基础上,能够自己开发网站的样式,这个需要比较深厚的前端技术,你需要懂js、html、css等很多知识,还需要很多实践才能做。 当然上面的分类是不靠谱的,只是我脑瓜子随便一想得出的一个初步分类。我以后肯定是要制作专属自己的皮肤的,用来表达自己的个性(装逼) 就不再唠嗑了,开始装逼了,哈哈哈 缘起请看这几个博客,都是我精心挑选的 ahonn 我的感触是,感叹这个作者写样式这么简洁大方,感叹这个网站响应速度这么快,第一个感叹是我以后要做的,第二个感叹是因为和我以前做的网站不一样,比我以前做的wordpress站点都快,后来我想到,主要有两个原因: 服务器响应速度快。我之前使用过最快的云主机尊云,这家网站速度还可以,其实国内的阿里云、腾讯云都不错,只是昨天,也就是2017/04/01,愚人节的这一天,腾讯活动给了一个月的免费主机,ping速度还不错,然后我的这个网站就选用这家了。 ahomn这个网站使用的是我们今天要介绍的hexo,它是一个静态网站生成器,也可以当一个服务器来运行,所以这个网站其实就是一些静态页面,我猜想是用nginx配置了服务器,请求这个网站只是返回一些静态网页而已,那自然是非常快的。 hexohexo目前来说是我接触最好用的博客建站网站了,不需要配置数据库,灵活的配置项,可以把你写的文章和你用的样式进行杂交,生成对应样式的文章等,当然你也可以自己开发样式,自己做其他的修改。 hexo之前用过几次,每次都是浅尝辄止,未得它的真正使用方式,我感觉很多个人博客都没有把这点说透,那是什么东西要说透呢,看下面的列表 你云服务器上放的东西应该是一个纯粹的静态网站,而不是用命令hexo server -p 80生成的hexo服务器 你的云服务器上应该是用其他服务器比如nginx,tomcat,appache,jetty等运行的一个环境,你只要把你的静态网站放在对应的网站路径就好 hexo怎么写文章?你以为是像wordpress那样有个管理网站给你吗?不是,hexo它完全是一种纯命令行形式的工具,你需要用专业的文字编辑工具去写,这里推荐大家使用Typora(我也是今天才真正投入使用,真正认为它是神器),是我最好用的markdown神器 我的神器有如下几个,我都分享给你们 Java最佳开发神器,idea 写前端最佳开发神器,webstorm 写markdown的最佳神器,Typora,这个要重点介绍下,下了好久了这个,但是一直没用起来,我以为没有实施预览,今天要写文章再次拿来使用,发现他的编辑与预览是同时产生的,这个真是牛啊,体验超级棒👍,这样的话,markdown语法也是很快就学会了,以后就不怕写不了牛逼哄哄的markdown了(以前看到大牛写的markdown那真是一个羡慕啊,无法言语啊) UML/ER图/代码生成神器,Visual Paradigm,这个网上没有破解,我只好痛心买了注册码,$349,好心疼,有按月付费的,但是我觉得我以后长期用,长痛不如短痛就买了 API文档编写与生成代码神器 Swagger,这绝壁是神器,能让你快速开发你的服务,同时写好文档和测试,太牛逼了这个,现在也是特别火 思维导图软件 Freemind,这个是免费的,从我大学就开始用了,但是当时没有领略到它加上快捷键之后是如此的简单便捷,我深深的爱上了这款软件 hexo文章怎么发布呢,使用命令hexo deploy,但是你需要配置你博客目录下的一个文件_config.yml,并配置下你要发布你本地网站上的文章到指定的服务器(发布前用命令hexo generate生成静态网站,在目录public下),怎么配置?这个分不同的服务器。 我测试过git和我自己的云服务器rsync(找到你的deploy位置) git配置: deploy: type: git repo: https://github.com/cmlanche/cmlanche.github.io.git branch: master message: “hello guys” rsync deploy: type: rsync host: 123.207.x.x(这里隐藏我的服务器ip,免得被很多人拿来测试了) user: root root: /root port: 22 delete: true verbose: true ignore_error: false 这种方式的话后边会要求你输入服务器密码的 测试发现不能同时又两个deploy,这是我比较缺憾的,比较不满意的地方。 你写的文章怎么到你的云服务器或者github上了呢?再次重复下,你写文章,先发布(怎么发布?你只要把你写的markdown文件移动或者复制到你的博客根目录下的/source/_posts/目录下即可,注意标题格式)到你的本地环境的hexo上,并用hexo generate生成静态完整,再配置好_config.yml中的deploy选项,使用命令hexo deploy完成发布,这个过程看似繁琐,其实作为一个资深技术人来说,这是你应该做的,没必要用什么图形工具,那太low了,对不对? 总结 发现我对工具往往浅尝辄止,类似的事情发生太多,就上面说的freemind,typora,hexo,没有深入去实践,当初都给他们定下不好用的标签,实际上你错了 既然这样一套成熟的开发方式已经摸熟了,以后就要坚持写博客,把我认为最有价值的内容带给大家,期待大家的关注","categories":[{"name":"日常技术","slug":"日常技术","permalink":"http://www.cmlanche.com/categories/日常技术/"}],"tags":[{"name":"hexo","slug":"hexo","permalink":"http://www.cmlanche.com/tags/hexo/"}]}]}