关于框架:OpenVI论文解读系列达摩院开源低成本大规模分类框架FFC-CVPR论文深入解读

一、背景解决该问题最直观的形式是通过集群的形式耗费更多的显卡资源,但即便如此,海量ID下的分类问题,仍然会有如下几个问题:1.)老本问题:分布式训练框架 + 海量数据状况下,内存开销、多机通信、数据存储与加载都会耗费更多的资源。2.)长尾问题:理论场景中,当数据集达到上亿ID时,往往其绝大部分ID内的图片样本数量会很少,数据长尾散布非常明显,间接训练难以获得较好成果。 二、办法在介绍办法之前,首先回顾下超大规模分类以后存在的次要挑战点: 残缺内容请点击下方链接查看: https://developer.aliyun.com/article/1212406?utm_content=g_10... 版权申明:本文内容由阿里云实名注册用户自发奉献,版权归原作者所有,阿里云开发者社区不领有其著作权,亦不承当相应法律责任。具体规定请查看《阿里云开发者社区用户服务协定》和《阿里云开发者社区知识产权爱护指引》。如果您发现本社区中有涉嫌剽窃的内容,填写侵权投诉表单进行举报,一经查实,本社区将立即删除涉嫌侵权内容。

July 5, 2023 · 1 min · jiezi

关于框架:饿了么开源自研多端框架-MorJS

MorJS 是什么?开源地址:https://github.com/eleme/morjs简介Mor (发音为 /mr/,相似 more) 是饿了么开发的一款基于小程序 DSL 的,可扩大的多端研发框架。 应用 MorJS,咱们只需书写一套(微信或支付宝)小程序,就能够通过 MorJS 的转端编译能力,将源码别离编译出能够公布在不同端(微信/支付宝/百度/字节/钉钉/快手/QQ/淘宝/H5)的产物。MorJS 还反对小程序、小程序插件、小程序分包之间的状态转换,同时也装备了从源码到构建产物全阶段的插件体系,满足各类性能扩大,晋升开发体验和开发效率。 示例以下是饿了么-美食外卖频道在微信、支付宝、抖音小程序及 H5 中的体现: 为什么要做 MorJS?目前各大平台都相继推出了本人的小程序,饿了么 C 端业务须要在不同平台小程序进行投放,这些我的项目大多是以支付宝或微信原生 DSL 编写,面对业务渠道的一直减少,咱们尝试了多种办法来兼容多端适配,但因为不同平台间小程序代码写法、能力反对的差异性逐渐变大,过来的计划无奈满足新业务的需要,咱们须要一套跨端研发框架能解决以下诉求: 原生 DSL 反对,不便现有小程序 DSL 编写的存量业务应用;升高性能开销,尽可能轻运行时,缩小编译构建的时长;便捷的应用,一键转换为反对各小程序平台应用的产物;拓展的性能,提供针对大型简单小程序的解耦计划;灵便的配置,可能简略的减少批改多套不同端的我的项目配置;产物优化能力,压缩构建产物体积,缩小小程序包大小;在明确这几点后,咱们调研了业界所有支流技术框架,发现并没有能齐全满足咱们需要的计划,所以咱们决定自研 MorJS。 如何应用?MorJS 是基于小程序原生 DSL 进行扩大的,只有你把握微信或支付宝任意一种小程序,那你就简直把握了 MorJS。MorJS 提供了官网脚手架工具用于创立新我的项目,同时也反对已有小程序引入相干依赖接入 MorJS。 开始一个新我的项目MorJS 我的项目示例:https://github.com/eleme/morjs/tree/main/examples1.创立我的项目,选定我的项目目录,在目录终端执行以下任一命令: $ npm init mor # npm 创立我的项目$ yarn create mor # yarn 创立我的项目$ pnpm create mor # pnpm 创立我的项目2.抉择对应的工程类型,依照提醒实现初始化操作 ✔ 请抉择工程类型 › 小程序✔ 请抉择源码类型 › 微信小程序 DSL✔ 是否应用 Typescript … 否 / 是✔ 请抉择 CSS 预处理器 › less✔ 请输出 小程序 的名称 … myapp✔ 请输出 小程序 的形容 … my first app✔ 用户名 … yourUserName✔ 邮箱 … your@gmail.com✔ 请输出 Git 仓库地址 … https://github.com/yourUserName/myapp✔ 请抉择 npm 客户端 › npm / pnpm / yarn…3.执行编译命令启动我的项目: ...

May 22, 2023 · 2 min · jiezi

关于框架:基于ArkUI框架开发ImageKnife渲染层重构

基于ArkUI框架开发-ImageKnife渲染层重构ImageKnife是一款图像加载缓存库,次要性能个性如下: ●反对内存缓存,应用LRUCache算法,对图片数据进行内存缓存。●反对磁盘缓存,对于下载图片会保留一份至磁盘当中。●反对进行图片变换:反对图像像素源图片变换成果。●反对用户配置参数应用:(例如:配置是否开启一级内存缓存,配置磁盘缓存策略,配置仅应用缓存加载数据,配置图片变换成果,配置占位图,配置加载失败占位图等)。 更多细节请拜访源码仓库地址:https://gitee.com/openharmony-tpc/ImageKnife 背景阐明晚期ImageKnife三方库在实现渲染局部的时候,应用的是image组件来展现图片的。因为image组件其实是一个残缺的集加载解析和图片展现的组件,渲染的模式只能通过配置固定参数进行,面对简单的需要场景,可能会呈现扩展性不够的状况。 当初随着工夫的推移渲染组件又多了一位重量级选手Canvas组件。能够通过2个组件渲染层的能力比照进行判断渲染层最终交由哪个组件展现。 如果想理解更多ImageKnife的背景常识,能够点击链接查看之前的文章介绍: 旧版本ImageKnife加载流程介绍https://developer.huawei.com/consumer/cn/forum/topic/02038645... 组件选型,能力比照首先咱们来看看Image组件和Canvas组件对于渲染这一块的反对状况。 从上表咱们能够看出: Image组件尽管反对了PixelMap的绘制,然而根本没有绘制控制能力,而且扩展性能力也比拟弱,并且渲染过程不可见,也无奈对绘制内容进行更多操作。 而Canvas组件属于更加底层的渲染组件,能够完满地管制绘制内容,并且渲染过程可见,合乎了开发者对于扩展性要求较高的定制场景。 重构前后能力比照 重构实现的内容1.应用canvas组件代替Image组件进行渲染展现图片。 2.所有图像数据在渲染层都转换为PixelMap,不便对立治理和扩大。 3.所有回调节点,对立形象成接口,不便后续进行扩大,进步代码可维护性。 4.所有的回调节点绘制的实现,都采纳了责任链模式,进步了自定义绘制扩大能力。 5.将局部通用办法封装成工厂办法,缩小开发者代码量。 6.通用办法从配置参数剥离,可采纳链式调用形式应用这些办法。 7.为了反对列表ImageKnifeOption参数应用@LinkObject润饰,同时ImageKnifeOption类型被@Observed润饰继承,不可被继承。 重构中比拟重要的点点1:回调接口形象为IDrawLifeCycle接口 渲染绘制是主线程能力操作。因而咱们能够对渲染程序进行了梳理,大抵流程:展现占位图->展现网络加载进度->展现缩略图->展现主图->展现重试图层->展现失败占位图 这里每个蓝色的小方格都代表着一个数据返回的回调接口,咱们须要在这个回调接口,解决接下来内容渲染的展现操作。因为每个回调的流程是固定的,有点像生命周期的流程。所以我这边形象成接口IDrawLifeCycle绘制生命周期进行表白。这其实也是为了前面扩大做了筹备。 点2:绘制实现采纳责任链模式 咱们反对了用户配置自定义绘制和全局配置自定义绘制的能力。采纳了责任链模式实现,用户参数设置->全局参数设置->自定义组件外部设置。这样设计的益处就是保留了用户扩大的能力,用户能够参加自定义绘制。 点3:提供了ImageKnifeDrawFactory工厂类 在开发者须要进行自定义绘制时,必须实现IDrawLifeCycle的6个接口。为了简化开发者操作,这里提供了ImageKnifeDrawFactory工厂类。 ImageKnifeDrawFactory外面封装了圆角、椭圆、百分比下载等实现,简化用户操作。当然更多的需要,能够参考该工厂类自行扩大实现。 这里咱们提供简略的场景示例: 场景1:一句代码,加个圆角成果 代码如下: import {ImageKnifeComponent} from '@ohos/imageknife'import {ImageKnifeOption} from '@ohos/imageknife'import {ImageKnifeDrawFactory} from '@ohos/imageknife'@Entry@Componentstruct Index { @State imageKnifeOption1: ImageKnifeOption = { // 加载一张本地的png资源(必选) loadSrc: $r('app.media.pngSample'), // 主图的展现模式是 缩放至适宜组件大小,并且在组件底部绘制 mainScaleType: ScaleType.FIT_END, // 占位图应用本地资源icon_loading(可选) placeholderSrc: $r('app.media.icon_loading'), // 失败占位图应用本地资源icon_failed(可选) errorholderSrc: $r('app.media.icon_failed'), // 绘制圆角30,边框5,边框"#ff00ff".用户自定义绘制(可选) drawLifeCycle:ImageKnifeDrawFactory.createRoundLifeCycle(5,"#ff00ff",30) }; build() { Scroll() { Flex({ direction: FlexDirection.Column, alignItems: ItemAlign.Center, justifyContent: FlexAlign.Center }) { ImageKnifeComponent({ imageKnifeOption: this.imageKnifeOption1 }) .width(300) // 自定义组件已反对设置通用属性和事件,这里宽高设置放在链式调用中实现 .height(300) } } .width('100%') .height('100%') }} ...

April 6, 2023 · 2 min · jiezi

关于框架:MASA-Framework-事件总线-进程内事件总线

概述事件总线是一种事件公布/订阅构造,通过公布订阅模式能够解耦不同架构层级,同样它也能够来解决业务之间的耦合,它有以下长处 松耦合横切关注点可测试性事件驱动公布订阅模式通过下图咱们能够疾速理解公布订阅模式的实质 订阅者将本人关怀的事件在调度核心进行注册事件的发布者通过调度核心把事件公布进来订阅者收到本人关怀的事件变更并执行绝对应业务 其中发布者无需晓得订阅者是谁,订阅者彼此之间也互不意识,彼此之间互不烦扰 事件总线类型在Masa Framework中,将事件划分为 过程内事件 (Event)本地事件,它的公布与订阅须要在同一个过程中,订阅方与公布方须要在同一个我的项目中 跨过程事件 (IntegrationEvent)集成事件,它的公布与订阅肯定不在同一个过程中,订阅方与公布方能够在同一个我的项目中,也能够在不同的我的项目中 上面咱们会用一个注册用户的例子来阐明如何应用本地事件 入门装置.NET 6.0新建ASP.NET Core 空我的项目Assignment.InProcessEventBus,并装置Masa.Contrib.Dispatcher.Eventsdotnet new web -o Assignment.InProcessEventBuscd Assignment.InProcessEventBusdotnet add package Masa.Contrib.Dispatcher.Events --version 0.7.0-preview.7注册EventBus (用于公布本地事件), 批改Program.csbuilder.Services.AddEventBus();新增RegisterUserEvent类并继承Event,用于公布注册用户事件public record RegisterEvent : Event{ public string Account { get; set; } public string Email { get; set; } public string Password { get; set; }}新增注册用户处理程序在指定事件处理程序办法上减少个性 EventHandler,并在办法中减少参数 RegisterUserEvent public class UserHandler{ private readonly ILogger<UserHandler>? _logger; public UserHandler(ILogger<UserHandler>? logger = null) { //todo: 依据须要可在构造函数中注入其它服务 (需反对从DI获取) _logger = logger; } [EventHandler] public void RegisterUser(RegisterUserEvent @event) { //todo: 1. 编写注册用户业务 _logger?.LogDebug("-----------{Message}-----------", "检测用户是否存在并注册用户"); //todo: 2. 编写发送注册告诉等 _logger?.LogDebug("-----------{Account} 注册胜利 {Message}-----------", @event.Account, "发送邮件提醒注册胜利"); }}注册用户的处理程序能够放到任意一个类中,但其结构函数参数必须反对从DI获取,且处理程序的办法仅反对 Task或 Void 两种, 不反对其它类型发送注册用户事件,批改Program.csapp.MapPost("/register", async (RegisterUserEvent @event, IEventBus eventBus) =>{ await eventBus.PublishAsync(@event);});进阶解决流程EventBus的 申请管道蕴含一系列申请委托,顺次调用。 它们与ASP.NET Core中间件有殊途同归之妙,区别点在于中间件的执行程序与注册程序相同,最先注册的最初执行 ...

November 24, 2022 · 3 min · jiezi

关于框架:提升-Hybrid-体验饿了么双十一-PHA-框架技术实践

作者:逍菲、崖松、子伦 饿了么端 618、国庆、双11、双12等大促会场基本上会标配底部导航,在之前一般H5容器中底部导航是前端实现,每次点击会场底部导航的tab,都会重新启动一个流动页面笼罩在下面,即便之前关上过的tab也都要从新创立和加载,体验不佳,且H5也不能很好的联合Native能力做进一步的体验和性能优化。 通过调研发现手淘PHA框架可解决上述痛点问题,PHA容器底部TabBar为Native渲染,tab点击时底部bar不会重建,tab对应的webview在整个PHA容器中也能够平滑过渡、无缝切换,无需另起容器。且加载过的tab流动页面Webview会常驻内存,当再次拜访时会间接切换至前台,更靠近native体验。 在去年 618、国庆、双11和双12大促中,联合饿了么业务个性又陆续落地了一些特色优化伎俩,带来了更好的性能体验和业务成绩。 双十一上线成果成果视频查看请点击:饿了么双十一 PHA 会场实际老容器和pha容器比照。其中左侧为老容器会场,右侧为pha容器会场。 PHA 简介什么是 PHA,PHA 全称 Progressive Hybrid App,是晋升 Hybrid 体验的一种新框架,提供了一些 Native 同层组件以及渐进式加强策略来创立 Hybrid APP 利用,让这些利用具备与 Native 雷同的用户体验。 PHA 应用了 Web Application Manifest的配置并且对配置进行了性能扩大每个 PHA 利用都会启动一个 App Worker,Worker 是独立于以后页面运行在客户端里的一段 JS 脚本。可在 Worker 中定制业务逻辑,如基于 LBS 申请底部Tab展现的数据列表利用下能够有多个页面,每个页面的默认渲染引擎是 WebView每个页面中 PHA 提供了像下拉刷新、页头等 UI 能力,都能够通过在 Manifest 中定制针对利用 PHA 还提供了 Tab 容器、Swiper容器、启动屏等 UI 能力和预申请、离线缓存等性能优化能力,可通过在 Manifest 中配置实现 pha架构图 饿了么接入计划本地生存跟淘宝等业务次要的区别为前者强依赖LBS属性,包含底部 Tab、商品、品牌等数据的召回。因而须要在用户关上PHA框架时,执行定位并加载对应的底部 Tab、顶部横滑数据后,动静组装出对应的manifest.json 数据来渲染PHA,整体架构图如下: 架构图 B端链路墨斗平台依赖天马源码页面服务来创立会场框架,沿用墨斗数据搭建来配置底部 Tab 和 顶部Swiper 的数据,实现定投,次要流程如下: ...

April 1, 2022 · 1 min · jiezi

关于框架:LeaRun敏捷框架甘特图摆脱项目管理的泥沼

项目管理是管理者在无限的资源束缚下,使用零碎的观点、办法和实践,对我的项目波及的全副工作进行无效地治理。即从我的项目的投资决策开始到我的项目完结的全过程进行打算、组织、指挥、协调、管制和评估,从而实现我的项目的指标。 一个我的项目往往蕴含很多简单的流程和具体的细节,无论是哪种我的项目,把握进度都是项目管理中的主线。在这个过程里,管理者须要将业务布局和落地执行分割起来,实现从上到下的打算分派、进度监控和从下至上的进度反馈。而甘特图能直观地表明工作打算在什么时候进行,及理论停顿与打算要求的比照,综合思考人力、资源、日期、反复因素和我的项目的要害局部,并将各个方面的甘特图集成到一张总图中,能够使企业日程进度高深莫测,实现企业资源的高度利用,因而在古代治理中,越来越多企业也装备了甘特图。 甘特图又称为横道图、条状图,以提出者亨利•L•甘特学生的名字命名。甘特图以图示的形式通过流动列表和工夫刻度形象地示意出任何特定我的项目的流动程序与持续时间。根本是一条线条图,横轴示意工夫,纵轴示意我的项目,线条示意在整个期间上打算和理论的流动实现状况。 借助甘特图,管理者能够直观地理解到每一个工作的开始工夫和完结工夫、拆散能够同时运行的工作,并确定工作的依赖关系、不同人的工作间的工夫关系和我的项目的里程碑。在古代企业中,甘特图曾经被利用于各个方面,但只管性能如此弱小,还是有许多管理者不懂如何应用业余软件绘制甘特图,还是只用Excel表格创立,这就有可能呈现信息更新和传递不及时的状况,由此导致我的项目交付品质降落等问题,并且操作太过繁琐。而低代码作为利用交付类零碎的开发平台,甘特图性能和服务更加贴近客户的痛点,可能从根本上解决企业治理的呈现的问题。 在LeaRun麻利开发平台上制作甘特图,只需简略的两步: 进入LeaRun麻利开发平台主页面,抉择[麻利开发]→[甘特图]→[甘特图利用]。 点击[新增],输出项目名称、我的项目起始工夫和完结工夫。并点击“+”号,输出划分的单个项目名称及起始完结工夫,最初点击[确定],就实现了甘特图的绘制。 同时LeaRun麻利开发平台反对甘特图四种显示模式,管理者能够依据本身须要抉择,而不必反复绘制。 个别显示 树形显示 动态显示 分页显示 甘特图创立实现后,管理者能够依据我的项目的进度实时更改甘特图数据,我的项目状态同步到团队,成员就能够依据我的项目状态来调整本身的工作和服务,晋升我的项目交付的品质,进步工作效率。 LeaRun麻利开发平台作为一款企业管理系统疾速开发平台,不仅能够治理企业各个环节,搭建ERP、MES、OA等零碎,还能够创立进度甘特图,治理我的项目、生产等进度,无需另买零碎,就能实现一平台多用的目标。更多功能请返回www.learun.cn/Home/VerificationForm进行体验。

December 21, 2021 · 1 min · jiezi

关于框架:从零打造微前端框架实战汽车资讯平台项目fsafsa

download:从零打造微前端框架:实战“汽车资讯平台”我的项目筹备工作既然要装逼,筹备工作是少不了的。所谓“站在凡人的肩膀上,做事事倍功半”,咱们这里的“凡人”就是 paddlepaddle 了,中文名称叫“飞桨”,那么这个 paddlepaddle 是什么呢? 它是“源于产业实践的开源深度学习平台,致力于让深度学习技术的翻新与利用更简略”,直白点就是我帮你实现了深度学习底层框架,你只需有创意就可能在我平台上使用大量简略代码轻松实现。它的官网是 https://www.paddlepaddle.org.cn/ 。 它的安装也比较简略,官网首页就有安装指引,咱们这里根据官网的安装指引,使用 pip 形式来安装 CPU 版本。 咱们首先执行语句: python -m pip install paddlepaddle -i https://mirror.baidu.com/pypi... 安装胜利后,咱们在 python 环境中测试一下是否安装胜利(这个也是按照官网指引来做),咱们切换到 python 环境,运行如下代码: Python 3.7.0 (v3.7.0:1bf9cc5093, Jun 26 2018, 23:26:24) [Clang 6.0 (clang-600.0.57)] on darwinType "help", "copyright", "credits" or "license" for more information. import paddle.fluidpaddle.fluid.install_check.run_check()Running Verify Paddle Program ... Your Paddle works well on SINGLE GPU or CPU.I0506 21:47:48.657404 2923565952 parallel_executor.cc:440] The Program will be executed on CPU using ParallelExecutor, 2 cards are used, so 2 programs are executed in parallel.W0506 21:47:48.658407 2923565952 fuse_all_reduce_op_pass.cc:74] Find all_reduce operators: 2. To make the speed faster, some all_reduce ops are fused during training, after fusion, the number of all_reduce ops is 1.I0506 21:47:48.658516 2923565952 build_strategy.cc:365] SeqOnlyAllReduceOps:0, num_trainers:1I0506 21:47:48.659137 2923565952 parallel_executor.cc:307] Inplace strategy is enabled, when build_strategy.enable_inplace = TrueI0506 21:47:48.659595 2923565952 parallel_executor.cc:375] Garbage collection strategy is enabled, when FLAGS_eager_delete_tensor_gb = 0Your Paddle works well on MUTIPLE GPU or CPU.Your Paddle is installed successfully! Let's start deep Learning with Paddle now ...

December 17, 2021 · 2 min · jiezi

关于框架:Baetyl推动边云融合-点亮智能物联网

随着 5G、IoT 等业务和场景倒退放慢、智能终端减少,近年来对边缘计算业务的下沉诉求越来越多。据揣测,在2025年,75%的数据将在边缘端产生并失去剖析和解决。在这一背景下,交换边缘计算相干技术的最新进展与落地实际、探讨边缘计算的现状与将来的重要性毋庸置疑。 8月1日,寰球开源技术峰会 GOTC 2021 深圳站“边缘计算专题论坛”顺利举办。本场论坛邀请了来自 Linux 基金会、百度、IBM、华为、VMWare、英特尔等组织机构的技术专家,分享 EdgeX  Foundry、Kuiper、Open Horizon 等出名我的项目的边缘计算实际。会上,百度智能云物联网主任架构师李乐丁带来了“从 Baetyl 走向智能化的数字世界”主题分享,具体介绍了 Baetyl 的建设背景、外围架构及其利用场景。 ▲ 图1:李乐丁在 GOTC 寰球开源技术峰会上发言  开源态度与 Baetyl 计划  在这个对隐衷极其敏感的时代,为了更好地保证数据安全性,数据须要从核心计算下沉到边缘计算,随之而来的数据接入、数据处理、数据贮存和数据同步便成为边缘计算目前正面临的四个重要问题。 基于不同的网络环境,终端设备须要在保障网络安全和隐衷的前提下,疾速连贯到本地数据中心。在公有网络接入数据后,通过就近的地位提供数据处理形式,响应产生的事件,进行边缘侧的数据处理。除了解决之外,还须要将就近的数据进行存储,保障更高的隐衷和合规性。当然,边缘和云并不是割裂的,边缘侧既要疾速解决数据,也要提供离线的网络存储、断点传输的能力,以及通过端到端数据加密等形式,帮忙来自边缘的数据和云上的外围算力实现互联。 ▲ 图2:百度智能边缘计算架构 在边缘计算的问题上,百度始终以来采取高度凋谢的态度,如图2所示,目前百度将技术合成为两局部,第一局部是向开发者提供 Open source 的计划,例如 Baetyl 就是由百度反对的一个开源的我的项目;另一部分则是面向企业,基于开源的我的项目、借助合作伙伴的力量,独特组建一个蕴含硬件、软件和两头平台的残缺端到端的商业化服务计划。  Baetyl 我的项目与外围架构  智能边缘开源框架 Baetyl 作为百度开源策略打算中的重要一步,该我的项目是在2019年由百度智能云向 Linux 基金会旗下 LF 基金会捐助,是LF基金会成立以来最早退出的我的项目。Baetyl 的指标是为边缘侧提供规范的云原生编排能力,从而让边缘和云端连贯,这其中 Baetyl 充当的就是边缘计算和云计算的粘合剂。Baetyl 利用云上弱小的计算能力帮忙边缘侧一直地迭代数据模型和 AI 模型,让边缘设施具备更好的常识和认知能力,将云计算的利用无缝扩大到边缘,助力云和边缘的数据实现自在替换。 在边缘上服务,为宽泛适配边缘侧各种各样碎片化的场景和设施,Baetyl 现已反对 X86、ARM、MIPS、CPU 等网络芯片,以及各种常见的 GPU 和神经网络芯片,罕用的操作系统 window 和 Linux、OS 均已适配,Baetyl 心愿能够助力更多合作伙伴独特推动边缘计算物联网的倒退,让硬件装置 Baetyl 后就能够疾速变为智能的边缘计算设施。 ▲ 图3:Baetyl 的外围架构图 图3展现了 Baety l外围架构,其深度地与云原生技术进行交融,Baetyl 采纳云端治理、边缘运行的计划,分为云端治理套件(Baetyl cloud)和边缘计算框架两局部。Baetyl cloud 运行在云端,指标是收集所有在 Kubernetes 上的配置,反对在云端配置边缘计算集群,治理所有资源,如:节点、利用、配置等。值得一提的是往年公布的 Baetyl 2.2 版本正式反对了 EdgeX Foundry 的运行,以上提及的所有动作都可在云端进行近程编排和定义,随后一键下发到边缘,下发过程即如图3左侧所示。在规范状况下,Baetyl cloud 会收集并打包来自 Kubernetes 管制面的信息,再由平安网络提供到本地的设施上,而 Baetyl 和云端治理套件之间会应用端到端的强制性的双向认证,进一步保障了平安问题。 ...

August 25, 2021 · 1 min · jiezi

关于框架:如何用7Ps框架开好会议-IDCF

会议是职场中常见的团队合作流动,也是进行决策、传递信息、达成共识的重要工具。置信屏幕旁的你也常常组织和参加各种大大小小的会议。可是,你真的会散会吗? “这还不简略,将人拽到一起,说一下主题,订1个小时,再弄个线上会议室,不就结了?” “要不要提前准备一下,会上遇到问题怎么办?” “不必,能有啥问题,到时再说呗。” 这么说也没有错。可是听起来不太对,问题在哪里呢?让我带你进一步剖析一下。前文讲到,任何能解决问题的办法,它必然有能够工作的暗藏假如和论据在撑持。 那么,上述看似不合理的对话中,你认为有哪些暗藏的假如前提呢? 答案是: TA(假如会议的组织者) 对会议的主题、波及的问题域非常分明对参会的人很理解当会上遇到问题时也能有效应对因而,当要散会的时候,组织者只有约好干系人的工夫订好会议室,那么会议就能够毋庸筹备,胜利的达成目标。 换句话说,当你对所要开的会的方方面面处于明确认知的时候,按上述做法能够达成散会的目标。 可是,事实中的会议有多少是这样的呢?事实中的组织者是否对会议都具备这样的认知程度?事实上,咱们所见到的版本大多如下: “下周大家找个工夫一起开个会讨论一下。” “散会了,散会了。” “…….” “对于这个话题,咱们再组织另一个会议讨论一下吧。” “这个会议到底是干什么的?” “叫我来散会,我须要干什么?” “探讨了半天咱们解决什么问题?” “Xxx总不在,这件事咱们不分明做不了决策。” “原来客户想要的是这个,会前基本不晓得。” “下次把内容提前收回来,会上间接探讨就行,不必耽搁大家的工夫。” 你看,这就是现实和事实的比照写照,也是你和“他人家的孩子”的差距,也是为什么一提“会议” 就会被吐槽低效、过程凌乱、浪费时间、不解决问题。 什么起因呢?据不齐全统计,99%以上的低效会议都是组织者的锅。 职场中咱们常有一些盘根错节的问题须要通过会议进行风暴和剖析,还会面对不同层级的各种干系人须要去促成共识和达成决策。对会议的组织要求就会更高。 那么,如何开好会呢? 分享一个麻利会议7Ps框架给你,帮你建设对会议的系统性认知,搞定高效会议。 一、什么是7Ps 框架?它是一个布局和筹备会议的工具,包含会议的7个要害因素。会议的组织者能够在会议筹备阶段作为自检清单应用。7Ps框架由James Macanufo设计[1]。 二、7Ps指什么?Purpose-目标: 为什么要散会?作为组织者,你须要可能分明且简洁地陈说会议的目标,给参与者明确的上下文以及为什么他们须要加入这次会议。如果讲不分明,那么你须要从新思考这次会议的必要性是什么,是否有必要持续开。很多会议其实是弥补会议,齐全不须要开。Product-产出物: 在会议期间产生哪些具体的工件?它会做什么,它将如何反对会议目标?如果会议并没有具体的产出物,那么兴许你应该思考的是一次团建而非会议。People-人员,角色: 谁须要加入会议。会议中他们的角色是什么?他们在所探讨主题中的角色是什么?比方哪些问题由A同学答复,哪些信息由B同学补充,哪些决定须要C同学提供资源反对。清晰梳理分明这一部分会议就胜利了一大半Process-流程: 会议的Agenda是什么?在会议的无限工夫内分为几步。Pitfall - 陷阱: 此次会议有哪些危险?咱们如何应答?例如,如果话题超出范围,如果某个重要的决策者长期参加不了?如果超时?如何材料不齐备等?如果主持人的电脑或网络忽然生效了?怎么办?Preparation-筹备: 会议前是否要有参与者理解上下文提前浏览的资料?有没有须要提前完成的考察问卷或作业?Practical concerns - 物料场地等后勤工作: 包含物料、场地、设施、工夫、地点、午饭等等。在这7个要害因素中,对会议的胜利和建设认知影响最大的两个因素是Purpose和People。Purpose是会议的起因,当问题定义分明,计划是更容易的事。People指与我何干,当把利益关系定义分明,参会者的定位就会清晰,就会天然遵循你设计的零碎的规定行事。 三、如何应用7Ps框架7Ps框架是一次好会议的设计框架,也能够作为散会前的自查清单。在设计时,能够采纳由Romain画的7Ps Canvas作为会议设计的模板,帮你结构化的梳理和出现会议的筹备工作。 在这里给大家应用7Ps的一些tips: 打算总会发生变化。7个元素之间相互影响,当其中一个元素产生过变动时,其它几个维度也须要进行调整。比方参加人发生变化,须要顺次调整工夫、流程等内容。用金字塔构造去构建7Ps,Purpose为中心思想。因而,要尽量用一句话去收敛Purpose,并顺次找到你的论据,设计你的逻辑链路。流程天然也就有了。会议中要让7Ps可见,线上会议则能够用白板的形式写下会议的事项,防止跑题。上面的脑图是应用7Ps筹备会议的一个示例图,你也能够设计这样的脑图模板,每次会议启动前来填充这些信息就能够了。 四、7Ps框架有多好?“在筹备战斗时,我总是发现打算没有用,然而打算又是必不可少的。” ——德怀特·艾森豪威尔 每次会议都应制订打算。好的打算的确不肯定能保障获得好的后果,但打算的过程能够帮忙你建设对事的系统性认知,7Ps框架就是这样一个帮你对会议建设系统性认知的框架。你的认知越清晰,会议成功率就会越高。 从模型分类的角度,7Ps框架是结构化模型,7Ps是会议胜利的7个要害因素(7Ps也被称为因素类模型)。它包含人、事、物的构造,又蕴含会前、会中、会后的工夫程序设计。因而,7Ps不仅可用于会议,还能够用于打算流动,比拟灵便又容易记忆。 你可能会问,每次都这样设计有些麻烦,可能须要消耗太多的工夫在设计上,还不如间接开呢? 大脑会想尽一切办法回绝思考,而为了解决问题所要耗费的思考和认知构建工夫在肯定水平上是等效的。如果你不在会前设计,那就会在会中和会后弥补。只不过越往后,你的弥补倍数越高,弥补的就不仅仅是你一个人的工夫,是几个人甚至几十个人的工夫节约。 所以,在你的下一个会议降临之前,无妨实际一下7Ps。即便只有一点点工夫,感受一下它对你的认知的影响以及对你的会议的影响。欢送留言和微信分享应用心得。 小模型大用处,试一下吧。 [1] 参考书籍-《Game Storming》 起源:Thoughtworks商业洞见 作者:禚娴静 申明:文章取得作者受权在IDCF社区公众号(devopshub)转发。优质内容共享给思否平台的技术伙伴,如原作者有其余思考请分割小编删除,致谢。 7月每周四晚8点,【冬哥有话说】研发效力工具专场,公众号留言“效力”可获取地址 7月8日,LEANSOFT-周文洋《微软DevOps工具链的 "爱恨情仇"(Azure DevOps)》7月15日,阿里云智能高级产品专家-陈逊《复杂型研发合作模式下的效力晋升实际》7月22日,极狐(GitLab)解决⽅案架构师-张扬分享《基础设施即代码的⾃动化测试摸索》7月29日,字节跳动产品经理-胡贤彬分享《自动化测试,如何做到「攻防兼备」?》8月5日,声网 AgoraCICD System负责人-王志分享《从0到1打造软件交付质量保证的闭环》

July 27, 2021 · 1 min · jiezi

关于框架:软件库与框架的区别

【注】本文译自:https://www.theserverside.com/tip/Library-vs-framework-How-these-software-artifacts-differ 库与框架:这两个软件构件的区别库(Libraries)是提供特定性能(如建设网络连接)的低级组件。框架(Framework)是已知的编程环境,比方 Spring Boot。 当软件主管开始构建新的企业应用程序时,他们必须决定要应用哪一组库和框架。 这引出了一个显著的问题:软件库和框架之间有什么区别? 库和软件框架都有助于应用程序的开发。然而,两者之间的次要区别在于它们的工作范畴,以及它们加重开发人员编写代码累赘的形式。 库提供了开发人员能够调用来执行特定性能的组件、类和办法。相比之下,框架提供的代码曾经执行了通常须要的性能,并在须要定制性能时调用开发人员提供的代码。 库与框架 库是一组相干的低级组件,开发人员调用这些组件来实现特定后果。罕用库函数包含日期格式化和建设网络连接。 框架解决更高级别的问题。框架提供了一个既定的编程环境,它自身建设在低级库之上。 例如,框架可能会解决如何最好地将应用程序中的所有对象映射到相干数据库中的表中。它还能够解决如何向最终用户提供丰盛的、基于 Web 的体验。 软件库示例 一个库的例子是 Java 日期和工夫 API。它是一组定义属性和办法的类和接口,开发人员能够应用这些属性和办法来格式化日期、执行时区转换并提供全局日历反对。Java 开发人员在须要时实例化 Date 和 Time 类,并以他们认为适合的任何形式调用库的办法。 另一个 Java 库示例是风行的 java.net 网络 I/O 包。该库由数十个类和接口组成,开发人员应用这些类和接口来关上网络端口、解决传入连贯并将数据发送到互连的客户端。 只须要应用 java.net 包,软件开发人员就能够实现解决基于 REST 的 Web 服务或通过 HTTP 协定的基于 HTML 的申请-响应循环所需的所有必要逻辑。java.net 库提供了一组低级 API,任何开发人员都能够应用这些 API 来开发通过网络进行通信的应用程序。 风行的 Java 库 其余风行的 Java 库,包含曾经利用了 Java 框架的 Spring Boot、JHipster 或 Vaadin 等 的库,包含: Apache Commons MathBouncyCastle for cryptographyJava Advanced ImagingJava SpeechJava 3DJavaMailJoda-TimeApache CollectionsJackson for JSON and XMLJava Networking软件框架的作用 企业我的项目通常须要开发人员提供一组 RESTful API,以便内部客户端与在线应用程序集成。然而,许多开发人员不仅解决了如何应用低级 java.net 库来解决 RESTful API 调用的问题,而且还将他们的工作作为开源我的项目来分享。这是软件框架的实质。它是一个现有我的项目,解决了一个常见且具备挑战性的问题,开发人员能够在本人的我的项目中应用它。 ...

July 19, 2021 · 1 min · jiezi

关于框架:分享-echoframework-项目基础框架

_ __ _ ___ ___| |__ ___ / _|_ __ __ _ _ __ ___ _____ _____ _ __| | __ / _ \/ __| '_ \ / _ \ _____| |_| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ /| __/ (__| | | | (_) |_____| _| | | (_| | | | | | | __/\ V V / (_) | | | < \___|\___|_| |_|\___/ |_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_\ echo-framework 是基于 echo 搭建用于疾速开发的我的项目框架 ...

June 20, 2021 · 1 min · jiezi

关于框架:技术实践-如何基于-Flink-实现通用的聚合指标计算框架

1 引言网易云信作为一个 PaaS 服务,须要对线上业务进行实时监控,实时感知服务的“心跳”、“脉搏”、“血压”等健康状况。通过采集服务拿到 SDK、服务器等端的心跳埋点日志,是一个十分宏大且杂乱无序的数据集,而如何能力无效利用这些数据?服务监控平台要做的事件就是对海量数据进行实时剖析,聚合出表征服务的“心跳”、“脉搏”、“血压”的外围指标,并将其直观的展现给相干同学。这其中外围的能力便是 :实时剖析和实时聚合。 在之前的《网易云信服务监控平台实际》一文中,咱们围绕数据采集、数据处理、监控告警、数据利用 4 个环节,介绍了网易云信服务监控平台的整体框架。本文是对网易云信在聚合指标计算逻辑上的进一步详述。 基于明细数据集进行实时聚合,生产一个聚合指标,业界罕用的实现形式是 Spark Streaming、Flink SQL / Stream API。不论是何种形式,咱们都须要通过写代码来指定数据起源、数据荡涤逻辑、聚合维度、聚合窗口大小、聚合算子等。如此繁冗的逻辑和代码,无论是开发、测试,还是后续工作的保护,都须要投入大量的人力/物力老本。而咱们程序员要做的便是化繁为简、实现大巧不工。 本文将论述网易云信是如何基于 Flink 的 Stream API,实现一套通用的聚合指标计算框架。 2 整体架构 如上图所示,是咱们基于 Flink 自研的聚合指标残缺加工链路,其中波及到的模块包含: source:定期加载聚合规定,并依据聚合规定按需创立 Kafka 的 Consumer,并继续生产数据。process:包含分组逻辑、窗口逻辑、聚合逻辑、环比计算逻辑等。从图中能够看到,咱们在聚合阶段分成了两个,这样做的目标是什么?其中的益处是什么呢?做过分布式和并发计算的,都会遇到一个独特的敌人:数据歪斜。在咱们 PaaS 服务中头部客户会更加显著,所以歪斜十分重大,分成两个阶段进行聚合的奥秘下文中会具体阐明。sink:是数据输入层,目前默认输入到 Kafka 和 InfluxDB,前者用于驱动后续计算(如告警告诉等),后者用于数据展现以及查问服务等。reporter:全链路统计各个环节的运行状况,如输出/输入 QPS、计算耗时、生产沉积、早退数据量等。下文将具体介绍这几个模块的设计和实现思路。 3 source规定配置为了便于聚合指标的生产和保护,咱们将指标计算过程中波及到的要害参数进行了形象提炼,提供了可视化配置页面,如下图所示。下文会联合具体场景介绍各个参数的用处。 规定加载在聚合工作运行过程中,咱们会定期加载配置。如果检测到有新增的 Topic,咱们会创立 kafka-consumer 线程,接管上游实时数据流。同理,对于曾经生效的配置,咱们会敞开生产线程,并清理相干的 reporter。 数据生产对于数据源雷同的聚合指标,咱们共用一个 kafka-consumer,拉取到记录并解析后,对每个聚合指标别离调用 collect() 进行数据散发。如果指标的数据筛选规定(配置项⑤)非空,在数据散发前须要进行数据过滤,不满足条件的数据间接抛弃。 4 process整体计算流程基于 Flink 的 Stream API 实现聚合计算的外围代码如下所示: SingleOutputStreamOperator<MetricContext> aggResult = src .assignTimestampsAndWatermarks(new MetricWatermark()) .keyBy(new MetricKeyBy()) .window(new MetricTimeWindow()) .aggregate(new MetricAggFuction());MetricWatermark():依据指定的工夫字段(配置项⑧)获取输出数据的 timestamp,并驱动计算流的 watermark 往前推动。MetricKeyBy():指定聚合维度,相似于 MySQL 中 groupby,依据分组字段(配置项⑥),从数据中获取聚合维度的取值,拼接成分组 key。MetricTimeWindow():配置项⑧中指定了聚合计算的窗口大小。如果配置了定时输入,咱们就创立滑动窗口,否则就创立滚动窗口。MetricAggFuction():实现配置项②指定的各种算子的计算,下文将具体介绍各个算子的实现原理。二次聚合对于大数据量的聚合计算,数据歪斜是不得不思考的问题,数据歪斜意味着规定中配置的分组字段(配置项⑥)指定的聚合 key 存在热点。咱们的计算框架在设计之初就思考了如何解决数据歪斜问题,就是将聚合过程拆分成2阶段: ...

June 17, 2021 · 2 min · jiezi

关于框架:联邦学习FATE框架安装搭建-CentOS8

联邦学习     FATE (Federated AI Technology Enabler) 是微众银行AI部门发动的开源我的项目,为联邦学习生态系统提供了牢靠的平安计算框架。FATE我的项目应用多方平安计算 (MPC) 以及同态加密 (HE) 技术构建底层平安计算协定,以此反对不同品种的机器学习的平安计算,包含逻辑回归、基于树的算法、深度学习和迁徙学习等。 FATE官方网站:https://fate.fedai.org/ FATE文档:https://fate.readthedocs.io/ 装置与配置-Python3.6装置 [root@localhost ~]# yum -y install epel-releaseLast metadata expiration check: 0:09:06 ago on Tue 01 Jun 2021 10:40:09 AM CST.Dependencies resolved.============================================================================================================================================================================================= Package Architecture Version Repository Size=============================================================================================================================================================================================Installing: epel-release noarch 8-8.el8 extras 23 kTransaction Summary=============================================================================================================================================================================================Install 1 PackageTotal download size: 23 kInstalled size: 32 kDownloading Packages:epel-release-8-8.el8.noarch.rpm 33 kB/s | 23 kB 00:00 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------Total 17 kB/s | 23 kB 00:01 Running transaction checkTransaction check succeeded.Running transaction testTransaction test succeeded.Running transaction Preparing : 1/1 Installing : epel-release-8-8.el8.noarch 1/1 Running scriptlet: epel-release-8-8.el8.noarch 1/1 Verifying : epel-release-8-8.el8.noarch 1/1 Installed: epel-release-8-8.el8.noarch Complete![root@localhost ~]# yum -y install python36Extra Packages for Enterprise Linux Modular 8 - x86_64 673 kB/s | 610 kB 00:00 Extra Packages for Enterprise Linux 8 - x86_64 6.9 MB/s | 9.4 MB 00:01 Dependencies resolved.============================================================================================================================================================================================= Package Architecture Version Repository Size=============================================================================================================================================================================================Installing: python36 x86_64 3.6.8-37.module_el8.5.0+771+e5d9a225 appstream 19 kInstalling dependencies: python3-pip noarch 9.0.3-19.el8 appstream 20 k python3-setuptools noarch 39.2.0-6.el8 baseos 163 kEnabling module streams: python36 3.6 Transaction Summary=============================================================================================================================================================================================Install 3 PackagesTotal download size: 201 kInstalled size: 466 kDownloading Packages:(1/3): python3-setuptools-39.2.0-6.el8.noarch.rpm 687 kB/s | 163 kB 00:00 (2/3): python3-pip-9.0.3-19.el8.noarch.rpm 47 kB/s | 20 kB 00:00 (3/3): python36-3.6.8-37.module_el8.5.0+771+e5d9a225.x86_64.rpm 46 kB/s | 19 kB 00:00 ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------Total 151 kB/s | 201 kB 00:01 Running transaction checkTransaction check succeeded.Running transaction testTransaction test succeeded.Running transaction Preparing : 1/1 Installing : python3-setuptools-39.2.0-6.el8.noarch 1/3 Installing : python36-3.6.8-37.module_el8.5.0+771+e5d9a225.x86_64 2/3 Running scriptlet: python36-3.6.8-37.module_el8.5.0+771+e5d9a225.x86_64 2/3 Installing : python3-pip-9.0.3-19.el8.noarch 3/3 Running scriptlet: python3-pip-9.0.3-19.el8.noarch 3/3 Verifying : python3-pip-9.0.3-19.el8.noarch 1/3 Verifying : python36-3.6.8-37.module_el8.5.0+771+e5d9a225.x86_64 2/3 Verifying : python3-setuptools-39.2.0-6.el8.noarch 3/3 Installed: python3-pip-9.0.3-19.el8.noarch python3-setuptools-39.2.0-6.el8.noarch python36-3.6.8-37.module_el8.5.0+771+e5d9a225.x86_64 Complete![root@localhost ~]#装置virtualenv 和 virtualenvwrapper ...

June 1, 2021 · 23 min · jiezi

关于框架:将模型转为NNIE框架支持的wk模型第一步tensorflowcaffe

摘要:本系列文章旨在分享tensorflow->onnx->Caffe->wk模型转换流程,次要针对的是HI3516CV500, Hi3519AV100 反对NNIE推理框架的海思芯片的算法工程落地。本文分享自华为云社区《将模型转为NNIE框架反对的wk模型——以tensorflow框架为例(一)》,原文作者:wwwyx_^▽^ 。 应用过NNIE框架的同学都晓得,NNIE框架只反对wk模型的推理。 理论应用过程中用海思提供的转换软件工具 RuyiStudio 将 caffe 1.0 模型转为wk。个别状况下如果购买了芯片,海思将会间接将相干的SDK包发送给客户,如果没有的话,大家能够从这个链接获取:RuyiStudio 从下面可知,最终须要将别的框架模型转为caffe才能够应用RuyiStudio,目前支流框架蕴含pytorch、tensorflow、mxnet等,对于pytorch转caffe之前曾经有大佬写过了pytorch->caffe,有须要的同学能够去看下。本文次要讲述tensorflow框架转为caffe可能会遇到的问题以及解决办法。mxnet有间接的接口能够转onnx,也能够参考这篇文章做caffe转换。 上面进入正题。 tensorflow->caffe这个真的是个大坑(哭泣),这里我应用了两头模型onnx,即最终胜利转换的门路是 pb->onnx->caffe->wk,上面就说一下具体的操作吧~ 第一步:tensorflow->onnx这一步是最简略的一步= =,目前转了一些模型还没有在这里遇到坑。应用github上的开源我的项目:tensorflow->onnx,间接应用pip install 装置后应用。 关注一下都有哪些参数,每个参数的作用,次要是输出、输入、推理应用nchw还是nhwc(caffe框架为nchw,所以这里都应用nchw)、opset(默认应用 9 ),很多的参数我没有应用到,大家有疑难能够间接去issues下面看下哈。 上面给出一个转换命令供大家参考下: python -m tf2onnx.convert --input ./model.pb --inputs input_image:0[1,112,112,3] --inputs-as-nchw input_image:0 --outputs output_0:0,output_1:0,output_2:0,output_3:0,output_4:0 --output ./convert.onnx失去onnx模型之后,能够应用onnx simplifer将一些零散算子合并,或者将一些冗余算子去除,这个工具视状况应用。 python -m onnxsim input_onnx_model output_onnx_model转换为onnx之后,须要验证输入的后果是否与pb统一,统一后再走前面的流程!! 第二步:onnx->caffe这里曾经失去了onnx模型,然而间隔胜利还有99%的路要走!! 这一大节Baseline:onnx2caffe 环境: caffe 1.0 + onnx 1.8.0次要性能代码: onnx2caffe+-- onnx2caffe| +-- _operators.py| +-- _weightloader.py+-- convertCaffe.py+-- MyCaffe.py运行命令: python convertCaffe.py ./model/MobileNetV2.onnx ./model/MobileNetV2.prototxt ./model/MobileNetV2.caffemodel在转换过程中如果遇到了问题,能够从上面几个方面来适配, (1)遇到caffe与NNIE不反对的算子,能够批改onnx模型中的node以适配caffe(这里要动员本人的小脑筋,一些算子替换能够参考一下pytorch->caffe这篇博客)。(2)如果遇到了NNIE与onnx反对的算子,然而caffe 1.0 官网不反对的话,能够在caffe中增加新的层,从新编译之后,再做转换。caffe中增加新的层能够参考:caffe 增加新node(3)caffe与NNIE都反对的算子,然而转换工具没有反对该算子的转换,在转换代码中增加相应的算子实现。(4)转换过程中算子转换胜利,然而呈现了shape问题,手动增加一些不须要参数的操作在曾经生成的prototxt中。 针对下面的每个办法给出对应的解决形式。 批改onnx模型中的node以适配caffe改写onnx模型,首先须要理解一下onnx都反对哪些算子。 onnx反对的op:onnx op ...

May 26, 2021 · 5 min · jiezi

关于小程序:如何深入分析小程序运行原理

背景小程序凭借其高曝光率、开发成本低、运行更晦涩等劣势和特点,一经推出就被宽泛应用,面对小程序的火爆,自然而然地,就有很多开发者转战小程序畛域,本文次要带大家理解下小程序运行环境背地的故事, 但对于想要学习理解这些外部架构来说,目前市面上的教程更多是通知你如何应用现有规定开发一款小程序性能,少有说明确一套小程序外部机制是如何运行起来的。本文我会具体分享小程序的运行原理。 为了更不便敌对地了解本文的一些内容,咱们先对小程序的运行环境进行一个大略的剖析,而后从上面三个层面来加深对小程序运行原理的了解 开发者工具剖析破解外围文件解读和架构流程图解小程序运行环境依据微信小程序开发文档能够得悉小程序在三端的运行环境场景: iOS :小程序逻辑层的 JavaScript 代码运行在 JavaScriptCore() 中,视图层是由 WKWebView 来渲染的Android:小程序逻辑层的JavaScript 代码运行在 V8 中,视图层是由自研 XWeb 引擎基于 Mobile( )Chrome() 内核来渲染的开发工具:小程序逻辑层的JavaScript 代码是运行在 NW.js 中,视图层是由 Chromium() Webview 来渲染的从小程序在三端中的运行环境能够看进去,它们存在逻辑层和渲染层之间的交互,具体是如何交互的呢?咱们能够从渲染层和逻辑层之间的通信模型来找答案: 从这张图中,咱们能够看出小程序采纳了一种较为适合的技术计划,实现渲染层和逻辑层别离由2个线程治理: 渲染层的界面应用了WebView进行渲染逻辑层采纳JsCore线程运行JS脚本当一个小程序存在多个界面时,渲染层就会存在多个WebView线程,这两个线程之间的通信会经由微信客户端来做直达,逻辑层发送的网络申请也经由Native转发, 为什么要采纳这种技术计划呢? 次要起因是小程序的管控限度,比方不能间接操作DOM树、页面跳转等管控措施,更好地造成本人的生态闭环。 当初咱们曾经对小程序运行环境的根本组成有了一些理解,上面我就从开发者工具剖析破解、文件解读和架构流程图解这三个层面来讲述小程序运行环境的具体知识点。 开发者工具剖析破解咱们先从微信开发者工具剖析破解说起,首先,咱们从微信开发者工具中关上官网提供的小程序demo我的项目: 从编辑栏和文档中,咱们能够晓得一个页面的组成构造存在四种文件格式 .js后缀文件示意以后页面逻辑.wxml 后缀文件是框架设计的一套标签语言,联合根底组件、事件零碎,能够构建出页面的构造.wxss 后缀文件用于示意一套款式语言,用于形容 WXML 的组件款式.json 后缀文件示意是页面的配置准则这些是咱们大部分开发者看到或晓得的一些外表内容。你可能会问,什么是深层次的内容呢?上面咱们就来一一剖析。 方才咱们也讲到了小程序中存在逻辑层和渲染层,那怎么在开发者工具中发现它呢? 鼠标操作 微信开发者工具–》调试–》调试微信开发者工具 之后就会弹出这样一个页面: 能够看到我做了两处标记,第一处标记的webview是渲染层,每个页面src对应一个地址,第二处标记的webview就是逻辑层。 仅仅是这样一个页面,咱们是没方法间接查看webview中的具体内容的,还须要一些操作。你能够在方才关上的控制台Console中输出找到对应标签,查看对应的webview: 再通过这个命令查看具体的webview内容: 如果你间接关上对应的dom树,第一个webview展现的就是渲染层相干信息: 比方从这张图中咱们就能够看到这个页面渲染层所依赖的一些文件和一些办法,通过后果论来推断这些文件从何而来有何作用。 而对于service逻辑层的webview,能够间接在开发者工具右下方控制台Console输出document,这样就能查看了: 通过下面的介绍,你就能晓得,通过dom树和source资源能够看出加载的一些本地文件资源。那么接下来,我再通知你如何找到这些文件的出处,蕴含文件自身的援用和代码执行返回的后果。 咱们从方才那张图中看到script引入了一些WA结尾的文件,这些文件其实就是小程序运行时外围的根底库文件了,咱们在控制台输出openVendor()命令,会自动弹出对应的文件框: 这些.wxvpkg后缀文件就是微信根底库的版本文件,通过相干工具解压后能够看到文件内容格局: 须要特地留神的是,还存在两个wcc和wcsc可执行文件,这两个文件有什么作用呢? wcc 将微信小程序设计的一套wxml标签语言,用于构建出页面构造,转为WebView能够了解的标签,毕竟渲染层还是运行在webview中,咱们能够通过一张图来看下它的编译流程。 它的编译流程大抵过程是 先加载小程序所有页面中wxml格局的文件代码将它们转换成一个$gwx(pagePath)的js函数,注入到webview中在小程序运行时,能够晓得以后的页面门路,执行这个函数会生成该页面的结构函数,之后承受页面数据,输入一段形容页面构造的virtual() dom json对象最初通过小程序外部组件生成页面对应的HTML标签,页面标签通过wcc编译转化成咱们相熟的节点。那么wxss文件的作用是什么呢?它次要负责把wxss内容转换为视图可应用的css内容,它同时会剖析文件之间的援用关系,增加尺寸单位rpx转换,还能依据屏幕宽度自适应以及款式导入,最初会生成一个eval()()函数用于解决rpx,输入一个款式信息数组。 小程序外围根底库文件解读方才咱们看到了小程序运行时外围的根底库文件,其中WAservice.js、WAwebView.js、appservice.js等文件是承载小程序运行环境的最外围文件,所以,有必要对这几个文件进行重点解读, 先来看下WAservice.js文件,间接关上它是一个打包混同后的文件,有大概6万多行: ...

April 7, 2021 · 1 min · jiezi

关于百度:飞桨框架20正式版重磅发布一次端到端的基础设施革新

在人工智能时代,深度学习框架下接芯片,上承各种利用,是“智能时代的操作系统”。近期,我国首个自主研发、性能齐备、开源凋谢的产业级深度学习框架飞桨公布了2.0正式版,实现了一次跨时代的降级。 这次2.0版本的公布对于飞桨来说,能够说是一次“基础设施”的全面更新换代!生存中,咱们看到过很多基础设施建设工程,例如西电东送、南水北调、高铁建设等等,这些在保障生产生存设施失常运行、推动整个社会的经济倒退和人们生存程度改善的过程中,以一种 “润物细无声”的模式扮演着要害基础性角色!此次飞桨降级就是以这样形式悄悄为整个产业及生态的倒退凋敝奠定根底、积蓄能量、削减后劲!上面将为宽广开发者具体介绍飞桨都做了哪些“基础设施”级别的要害工程。 应用飞桨框架2.0更高效地开发AI模型成熟齐备的动态图模式此次降级,飞桨将默认的开发模式降级为命令式编程模式,即大家常说的动态图。飞桨框架2.0反对用户应用动态图实现深度学习相干畛域全类别的模型算法开发。动态图模式下能够让开发者随时查看变量的输出、输入,方便快捷的调试程序, 带来最佳的开发体验。为了解决动态图的部署问题,飞桨提供了全面齐备的动转静反对,在Python语法反对覆盖度上达到领先水平。开发者在动态图编程调试的过程中,仅需增加一个装璜器,即可无缝平滑地主动实现动态图训练或模型保留。同时飞桨框架2.0还做到了模型存储和加载的接口对立,保障动转静之后保留的模型文件可能被纯动态图加载和应用。 在飞桨框架2.0版本上,官网反对的动态图算法数量达到了200+,涵盖计算机视觉、自然语言解决、语音、举荐等多个畛域,并且在动态图的训练效率和部署效率方面都有所晋升。2.0版本的动态图反对了主动混合精度和量化训练性能,实现了比动态图更简洁灵便的混合精度训练接口,达到媲美动态图的混合精度和量化训练成果。无论从性能还是性能角度,飞桨的动态图在国产深度学习框架中都处于领先地位! 同时,为了推动各个支流场景的产业级利用,飞桨的系列开发套件也随飞桨框架2.0实现了降级,全面反对动态图开发模式。从开发、训练到预测部署提供优质体验。如视觉畛域的图像宰割套件PaddleSeg,随飞桨框架2.0降级后,涵盖了高精度和轻量级等不同特点的大量高质量宰割模型,采纳模块化的设计,提供了配置驱动和API调用两种利用形式,帮忙开发者更便捷地实现全流程图像宰割利用;又如自然语言解决畛域的PaddleNLP,与飞桨框架2.0深度适配,领有笼罩多场景的网络模型、简洁易用的全流程API,以及动静对立的高性能分布式训练能力,十分便于二次开发,大大晋升建模效率。具体能够参见上面链接中的我的项目示例。 飞桨框架2.0动态图模型: https://github.com/PaddlePaddle/models/tree/develop/dygraph 全新PaddleSeg我的项目利用实例: https://aistudio.baidu.com/aistudio/projectdetail/1339458 全新PaddleNLP我的项目利用示例: https://aistudio.baidu.com/aistudio/projectdetail/1329361 API体系全新降级API是用户应用深度学习框架的间接入口,对开发者应用体验起着至关重要的作用,飞桨始终以来对API设计以及整体API体系的欠缺给予高度重视。飞桨框架2.0对 API体系进行了全新降级,让开发者们在应用飞桨研发的过程中能够体验到得心应手、畅通无阻的愉悦感觉。 体系化: 基于长期的产业实际积攒与用户应用习惯的洞察,飞桨从新梳理和优化了API的体系结构,使其更加清晰、迷信,让宽广开发者能够更容易地依据开发应用场景找到想要的API。此外能够通过class和functional两种模式的API来模块化的组织代码和搭建网络,进步开发效率。同时,API的丰盛度有了极大的晋升,共计新增API 217个,优化批改API 195个。简洁化:提供更适宜低代码编程的高层API。像数据加强、建设数据流水线、循环批量训练等能够标准化的工作流程,以及一些经典的网络模型构造,在飞桨框架2.0中,都被封装成了高层API。基于飞桨高层API,开发者只需10行左右代码就能够编写实现训练局部的程序。最为重要的是,高层API与根底API采纳一体化设计,即在编程过程中能够同时应用高层API与根底API,让用户在简捷开发与精细化调优之间自在定制。新API体系齐全兼容历史版本,同时飞桨提供了降级工具,帮忙开发者升高降级迁徙老本。 飞桨开源框架2.0 API参考文档: https://www.paddlepaddle.org.cn/documentation/docs/zh/api/index_cn.html 应用飞桨框架2.0更高效地训练AI模型训练更大规模的模型家喻户晓,飞桨框架的英文名Paddle便是并行分布式训练学习的缩写,分布式能够说是飞桨与生俱来的个性。飞桨反对包含数据并行、模型并行、流水线并行在内的宽泛并行模式和多种减速策略。在飞桨框架2.0版本中,新增反对了混合并行模式,即数据并行、模型并行、流水线并行这三种并行模式能够互相组合应用,更高效地将模型的各网络层甚至某一层的参数切分到多张GPU卡上进行训练,从而实现反对训练千亿参数规模的模型。 业内首个通用异构参数服务器架构飞桨框架2.0推出了业内首个通用异构参数服务器技术,解除了传统参数服务器模式必须严格应用同一种硬件型号Trainer节点的桎梏,使训练任务对硬件型号不敏感,即能够同时应用不同的硬件进行混合异构训练,如CPU、GPU(也包含例如V100、P40、K40的混合)、AI专用减速硬件如昆仑芯片等,同时解决了搜寻举荐畛域大规模稠密特色模型训练场景下,IO占比过高导致的计算资源利用率过低的问题。通过异构参数服务器架构,用户能够在硬件异构集群中部署分布式训练任务,实现对不同算力的芯片高效利用,为用户提供更高吞吐,更低资源耗费的训练能力。 图 异构参数服务器架构示意图 通用异构参数服务器架构之所以被称之为通用,次要在于其兼容反对三种训练模式: 可兼容全副由CPU机器组成的传统参数服务器架构所反对的训练任务。可兼容全副由GPU或其余AI减速芯片对应机器组成的参数服务器,充分利用机器外部的异构设施。反对通过CPU机器和GPU或其余AI减速芯片对应机器的混布,组成机器间异构参数服务器架构。异构参数服务器领有十分高的性价比,如下图所示,仅用两个CPU机器加两个GPU机器就能够达到与4个GPU机器相仿的训练速度,而老本至多能够节约35%。 分布式训练教程: https://fleet-x.readthedocs.io/en/latest/paddle_fleet_rst/distributed_introduction.html 应用飞桨框架2.0更宽泛地部署AI模型到各种硬件全面深度适配各种人工智能硬件AI产业的广泛应用离不开各种各样的人工智能硬件的凋敝,飞桨能够说深谙其道,继续致力打造凋敝的硬件生态。以后包含英特尔、英伟达、ARM等诸多芯片厂商纷纷发展对飞桨的反对。飞桨还跟飞腾、海光、鲲鹏、龙芯、申威等CPU进行深刻适配,并联合麒麟、统信、普华操作系统,以及百度昆仑、海光DCU、寒武纪、比特大陆、瑞芯微、高通、英伟达等AI芯片深度交融,与浪潮、中科曙光等服务器厂商单干造成软硬一体的全栈AI基础设施。以后飞桨曾经适配和正在适配的芯片或IP型号达到29种,处于业界领先地位。 图 飞桨硬件生态路线图 立刻体验飞桨开源框架2.0版本飞桨框架2.0装置: https://www.paddlepaddle.org.cn/install/quick?docurl=/documentation/docs/zh/2.0/install/pip/linux-pip.html 10分钟疾速上手飞桨框架2.0: https://www.paddlepaddle.org.cn/documentation/docs/zh/guides/02_paddle2.0_develop/01_quick_start_cn.html 飞桨框架2.0应用教程: https://www.paddlepaddle.org.cn/documentation/docs/zh/guides/index_cn.html 飞桨框架2.0利用实际: https://www.paddlepaddle.org.cn/documentation/docs/zh/tutorial/index_cn.html 如果您想具体理解更多飞桨的相干内容,请参阅以下文档。 ·飞桨官网地址· https://www.paddlepaddle.org.cn/ ·飞桨开源框架我的项目地址· GitHub: https://github.com/PaddlePaddle/Paddle Gitee: https://gitee.com/paddlepaddle/Paddle 想理解更多无关飞桨框架2.0的信息,记得关注百度深度学习趣味案例实战营,3月15日-19日晚19:00,每天一小时,手把手带你应用CV、NLP畛域最罕用模型及利用,即学即用,做我的项目还能获取积分兑换京东卡。

March 16, 2021 · 1 min · jiezi

关于框架:网易云信服务监控平台实践

文 | 戴强 网易云信数据平台资深开发工程师 导语:数据在很多业务中都至关重要,对于网易云信,咱们通过数据来晋升服务并促成业务持续增长。借助于服务监控平台的能力,咱们能够很直观的感触到线上服务的运行状况,本文将详细分析网易云信的服务监控平台具体是如何实现的。 引言通常人类的恐怖都来源于对事实世界的未知。 现实生活中存在很多的不确定性,恐怖是因为咱们以后的认知无奈对其作出正当解释。例如这次的疫情的忽然暴发,人们对死亡的恐怖一直蔓延。世界存在很多的不确定性,什么是不确定性?假如须要对今天的股票指数是涨是跌作出判断,在没有任何数据作撑持的状况下,咱们只能像抛硬币一样都是百分之50的概率。在这种场景下,咱们做出的所有判断都是不可信的,并且会对本人的决定感到不安。 如果人们理解了所有波及某种行将产生的事件的因素,那么他们就能够准确地预测到这一事件;或者相同,如果产生了某个事件,那么就能够认为,它的产生是不可避免的,这便是拉普拉斯的信条(又名决定论)。 正如以上的实践所表白的意思,数据能够帮忙咱们指引方向,并且验证咱们的方向是否正确。同样,数据对于网易云信的倒退来说也至关重要,咱们须要通过数据来晋升咱们的服务,并促成业务持续增长。 网易云信是集网易20年 IM 以及音视频技术打造的 PaaS 服务产品,咱们始终致力于提供稳固牢靠的通信服务,而如何保障稳固牢靠呢? 服务监控平台就是其中重要的一环,其就相当于布加迪威龙上的仪表盘,汽车的时速是多少,油量是否足够,以后的转速是多少,这些在仪表盘上高深莫测,能够帮忙咱们做出判断:是不是还能够踩一点油门,必要的时候是不是该刹一下车。服务监控平台的指标和价值就在这里,它也就相当于网易云信这辆布加迪威龙的仪表盘,能够通知咱们以后的服务质量怎么样,是不是须要多加点“油”,是不是须要踩一下“油门”或者“刹车”,给咱们和客户提供更多的信息,帮忙咱们提供最优质的、最牢靠、最稳固的服务。 本文就来详细分析网易云信的服务监控平台具体是如何实现的,将从整体架构登程,简略介绍网易云信服务监控平台的框架,再仔细分析包含数据采集、数据预处理、监控告警、数据利用四个模块的实现。 零碎架构当初网易云信的音视频数据根本都来源于客户端和服务端日志,所以整个数据的采集链路是其中十分重要的一环,决定了数据的有效性和时效性。 首先,咱们来看一下网易云信采集监控平台的整体架构,如下图: 采集监控平台整体架构次要分为数据采集、数据处理、数据利用、监控告警四局部,整个解决流程如下: 数据采集: 咱们次要的数据起源为业务 SDK 和应用服务器,这些数据能够通过 HTTP Api、Kafka 两种形式接入采集服务。采集服务对数据进行简略校验和拆分,而后通过 Kafka 传输到数据荡涤服务。数据处理:数据处理服务次要负责对接管到的数据进行解决而后发给上游服务应用。其中咱们提供了 JOSN 等简略的数据格式化能力,另外也提供了脚本解决模块,以反对更灵便更弱小的数据处理能力,该能力也使得咱们监控平台的数据处理能力更富多样性。监控告警:监控告警模块对于咱们一开始提到的服务监控能力来说,是最重要的一环。咱们对于采集到的数据进行分维度聚合统计和剖析,应用丰盛的聚合算法、灵便多变的规定引擎来最终达到服务预警和问题定位的目标。数据利用:荡涤后的数据能够间接写入时序数据库供问题排查平台应用,也能够通过 Kafka 接入 es、HDFS、流解决平台,最终供应用层应用。例如:品质服务平台、通用查问服务、问题排查平台等。接下来咱们会对上述四个模块进行具体的剖析。 数据采集数据采集是服务监控平台的入口,也是整个流程的第一步,下图是数据采集模块的架构图。 上文也有提到,为了便于用户接入,咱们提供了 HTTP API 和 Kafka 两个通道给业务方。 HTTP API 多用于端侧或服务器中偏实时的数据上报场景,用以反对秒级的数据接入。Kafka 多用于高吞吐量、数据实时性要求不太高的场景。数据过滤预处理模块,提前将一些非法数据进行过滤,并事后数据拆分等解决。最初通过 Kafka 传输到数据处理服务,接下来就是数据处理阶段的介绍了。 数据处理实现数据采集阶段后,即进入数据处理阶段,具体流程如下: 任务调度,次要负责数据处理线程的生命周期治理,从启动到敞开。消费者,在获取数据后应用外部的队列进行解耦,从而达到横向扩大的能力以进步数据处理线程的并行度。处理单元,能够依据须要设置并行度: 数据处理能力分成两种,通用规定和自定义脚本。通用规定就是简略的 JSON 转换、字段提取等,这些根本能够满足80%的需要,然而为了撑持相似多字段关联计算、正则表达式、多流关联解决等简单业务,咱们也提供了自定义脚本进行数据处理的能力。维度表的应用次要是针对多数据流关联解决的场景,为了解决数据量和并发高的问题应用了本地+第三方缓存的计划。时序数据库输入:时序数据库咱们应用的是NTSDB,NTSDB 是网易云基于influxdb做的集群化计划,有高可用、高压缩比、高并发等特点。数据处理完之后,接下来一个比拟重要的阶段就是监控告警。 监控告警上面这张图简略展现了监控告警的流程: 监控告警阶段分为指标聚合模块以及告警模块。 指标聚合模块反对指定字段分组统计、灵便的聚合窗口工夫、数据过滤、细粒度的算子级别的数据过滤、数据提早最大工夫。最重要的是咱们反对了十分丰盛的聚合算子:累加、最小/最大值、firstValue/lastValue、平均数、记录数、去重计数、TP系列(TP90/TP95/TP99)、环比、标准差等,同时还反对在第一次指标聚合后进行复合计算的能力(复合指标)。这些丰盛的算子为咱们实现更多灵便的监控规定提供了保障。 另外咱们将原有的一阶段聚合改成了两阶段聚合,为什么呢?因为在数据处理的过程中咱们常常会遇到的一个问题:数据热点问题导致的歪斜。所以这里咱们减少了预处理阶段,在这一阶段利用随机数进行打散,保证数据的平衡,而后预聚合的数据在第二阶段进行总的聚合解决。 告警模块和指标聚合模块从原有的一个模块拆分为两个,指标模块更多的关注如何做数据聚合,而不是和告警模块耦合在一起作为告警模块的一部分。而告警作为一个附加的性能,只须要依据接管到的数据,做一些告警的规定校验、频控校验、告警信息封装、对接音讯平台发送告警音讯,同时反对了外部 IM 平台、短信、电话等音讯通道,多样的音讯通道是为了能够第一工夫感知到问题的呈现。 数据利用数据利用现有的平台:数据可视化、品质服务平台、ELK日志平台、在线离线剖析等。上面,咱们针对每个平台简略介绍一下。 数据可视化数据可视化这部分,咱们和大部分公司一样应用 Grafana 来实现。须要做可视化的数据能够先同步到NTSDB中,而后再应用 NTSDB 作为数据制作图表和大盘。另外对于不反对的图表,咱们针对Grafana 做了二次开发以反对更多的可视化需要。 ...

February 5, 2021 · 1 min · jiezi

关于SegmentFault:实战丨借助云开发Framework快速部署Kodexplorer

背景CloudBase Framework 是云开发官网出品的云原生一体化部署工具,能够帮忙开发者将动态网站、后端服务和小程序等利用,一键部署到云开发 Serverless 架构的云平台上,主动伸缩且无需关怀运维,聚焦利用自身,无需关怀底层配置和资源 云开发 CloudBase Framework 框架「Container」插件: 通过云开发 CloudBase Framework 框架将我的项目利用一键部署到云开发的云托管环境,提供生产环境可用的主动弹性伸缩的高性能的容器计算服务。能够搭配其余插件如 Website 插件、Node 插件实现云端一体开发。 本篇文章次要以一个残缺的我的项目作为例子,应用FrameWork将Kodexplorer我的项目轻松部署到云开发上。 部署过程步骤一:筹备工作具体步骤请参照 筹备云开发环境和 CloudBase CLI 命令工具 步骤二:进入我的项目目录进行初始化进入我的项目目录后,创立云开发的配置文件 cloudbaserc.json touch cloudbaserc.json当然也能够在我的项目目录下主动创立相干配置文件 tcb 步骤三:编辑配置文件cloudbaserc.json通过参照文档的参数阐明,填写好了相干的参数 { "version": "2.0", "envId": "{{env.ENV_ID}}", "framework":{ "name":"kodexplorer", "plugins":{ "client": { "use": "@cloudbase/framework-plugin-container", "inputs": { "cpu": 0.5, "mem": 1, "serviceName": "kodexplorer", "servicePath": "/", "localPath": "./", "uploadType": "package", "containerPort": 80, "volumeMounts": { "/var/www/html": "kodexplorer-cfs" } } } }, "requirement": { "addons": [ { "type": "CFS", "name": "kodexplorer-cfs" }, { "type": "CynosDB", "name": "kodexplorer" } ] } }}更多的参数阐明可参考配置参数阐明 ...

January 20, 2021 · 1 min · jiezi

关于框架:框架VS架构看两者异同

框架是和架构比拟类似的概念,而且两者有着较强的关联关系,所以在理论工作中,很多时候这两个概念并不是分得那么清晰,参考维基百科,框架的定义如下: 软件框架(Software Framework)通常指的是为了实现某个业界规范或者实现特定根本工作的软件组件标准,也指为了实现某某个软件组件标准时,提供标准做要求之根底性能的软件产品。 框架是组件标准,比方:MVC就是一种常见的开发标准,相似的有MVP、MVVM、J2EE等框架。 框架提供根底性能的产品。比方:Spring MVC是MVC的开发框架,除了满足MVC的标准,Spring提供了很多根底性能来帮忙咱们实现性能,包含注解@Controller,Spring Security,Spring JPA等很多性能。 参考维基百科,这里简略的翻译为软件架构师指软件系统的“根底构造”,发明这些根底构造的准则,以及对这些构造的形容。 单从定义的角度来看,框架和架构的区别还是比拟显著的,框架关注的是标准,架构关注的是构造。框架的英文是Framework ,架构的英文是Architecture。Spring MVC的英文文档题目是Web Framework,包目录也有framework。 尽管如此,在理论工作中咱们却常常碰到一些似而非似的说法,比方: 咱们的零碎吃MVC架构 咱们须要将Android App重构 MVP架构 咱们的零碎基于SHH框架开发 咱们的零碎是SHH的架构以上几种说法到底是对还是错呢?其实以上说法都是对的,造成这种景象的根本原因暗藏于架构的定义中,关键字“根底构造”,这个概念并没有很明确的说分明从什么角度来讲的,从不同的角度或者维度,能够将零碎划分为不同的构造,其实咱们再“模块与组件”中的样例曾经暗含了这点,持续以学生信息管理系统为例。 从业务逻辑的角度合成,”学生信息管理系统“的架构如下: 从物理部署的角度合成,“学生信息管理系统”的架构如下: 从开发标准的角度合成,“学生信息管理系统”能够采纳规范的MVC来开发,因而架构又变成了MVC架构了,如下图: 以上这些架构 ,都是学生信息管理系统正确的架构,只是从不同的角度来合成而已。

December 27, 2020 · 1 min · jiezi

关于框架:JNPF快速开发平台30版的设计理念与功能架构解析

如果您是企业,是否感到找一款真正适宜您的现成软件真的很不容易,想要本人开发但又苦于没有专业人才,而定制开发又太贵;如果您是软件公司,是否感到开发人员工资太高、留住人才太难,人才走了,产品也就完了,而且就算一个开发好的产品,要做些个性化批改也是很艰难且还容易导致BUG;如果您是集体,是否有将本人多年的治理教训或忽然间的想法创意变成一个成型软件并带来收益的激动。 因而,有感于这些场景的触动启发,引迈软件公司便重磅的推出了JNPF疾速开发平台,来专一于解决这些软件开发难题和市场需求,而这些痛点与需要也为了JNPF疾速开发平台的设计理念。当初咱们就来简略的看下JNPF疾速开发平台的性能架构。 JNPF疾速开发平台PC端首页: JNPF疾速开发平台是一款配置型软件疾速开发框架,可一次开发多端应用,且PC端与挪动端(APP、微信小程序、钉钉等)的数据皆可互通互联、协调办公。从下面两图能够看到,JNPF平台的PC端即是JNPF云开发平台,它次要由六大功能模块组成,也正是这六大功能模块形成了弱小的JNPF云开发平台,而六大功能模块下还有很多子功能模块。 第一、在线开发有五大子功能模块:WEB设计、APP设计、报表设计、大屏设计以及门户设计。开发者通过这几个子功能模块便可自在布局页面、可拖拽相应的控件到页面来实现主动生成相应的可视化利用。 第二、代码生成有三大子功能模块:WEB表单、APP表单以及流程表单。开发者可针对数据库抉择单表或多表模式,再抉择数据关联,下一步进入表单页面,并可拖拽表单控件,而后再进入输入设置,代码便可主动输入,可下载或查看。 系统管理模块可依据公司需要进行菜单创立以及治理,它蕴含系统配置、菜单治理、翻译治理、单据规定、系统调度、零碎布告、系统日志、零碎图标、数据利用、微信配置等一系列简单灵便应用的子功能模块。而这些性能开发又简直全副是零代码,可让开发者迅速上手、疾速便捷的搭建一个性能成熟的业务管理平台。 零碎权限性能针对权限划分则别离有组织治理、部门治理、岗位治理、用户治理、角色治理、权限治理、在线用户等几大子功能模块,通过不同的角色管理系统权限,可使每个人分工明确,让企业实现更好的治理。 工作流程性能可建设企业日常工作的高效电子化、标准化、规范化的流程管理体系,晋升办公效率,将工作流动分解成定义良好的工作、过程、角色和规定来进行执行和监控,达到晋升企业管理水平和工作效率为目标。它蕴含我发动的、待办事宜、已办事宜、抄送事宜、流程委托、流程监控、流程设计等几大子功能模块。 第六、扩大利用功能模块是针对日常可见操作进行演绎,使人心涣散溶剂在一起,让开发者应用起来更加方便快捷。扩大利用的性能有:百万数据、导入导出、打印示例、电子签章、日程安排、邮件收发、常识治理、订单治理、文件预览、条码示例、项目管理、表格示例、表单示例、图表示例等一系列可拓展降级的性能利用,大大提高了软件开发的丰富性和可塑性。

September 26, 2020 · 1 min · jiezi

关于框架:LeaRun快速开发平台快速开发netjava项目

Learun软件疾速开发平台是一款轻量化多语言可视化开发工具。 平台目前分为Java和.net(core)版本,内置有多套UI格调模板,外围性能基本相同,包含:向导式开发组件、BI可视化、拖拽式表单、代码生成器、单据套打、通用app/小程序、权限治理、流程引擎页等功能模块,能够疾速无效的开发出市场上目前常见的各种管理系统,如:OA、ERP、CRM、HRM、MIS等。 Learun软件开发平台以“让开发变得简略”为主旨,深耕软件平台,领有近10年的行业开发教训,经典.net软件产品曾经服务超5000家客户,并失去市场统一好评。 框架采纳目前支流的引擎式开发,与传统的软件开发模式相比,其最大特点是通过数据汇合、表单引擎、流程引擎、报表引擎等,用可视化的模式进行设置组合,联合我的项目本身的类库,从而实现对各种简单零碎的疾速高效开发。 .net产品 .net是目前客户次要应用产品,目前已正式更新至V7.0.6;基于.net产品开发而来的.netcore产品也曾经公布,两者整体性能统一,UI格调一脉相承。 APP模块采纳支流的vue框架,同时反对微信、钉钉、支付宝等平台。 1.麻利开发 麻利开发向导:表单、流程、数据等罕用性能配置向导 代码生成器:八套开发模板,生成类、页面、映射、表单、小程序等 通用图标:PC和挪动端图标 数据看板:BI大数据看板 表格组件:各类罕用表格 甘特图:理解我的项目进度 信息可视化:货架、生产线等 门户配置:企业门户 插件配置:框架内置及第三方插件 二维码生成:企业二维码生成 D3配置:动态数据展现 2.系统管理 行政区域:全国行政区划 数据字典:各我的项目个性查问 单据编码:合同、表单等文件编码 零碎性能:零碎性能分类展现 系统日志:日志类 LOGO设置:框架logo设置 数据权限:权限类 桌面配置:首页桌面性能配置 音讯治理:音讯类 多语言治理:内置中、英、繁,可拓展 微信企业号:企业号开发 任务调度:工作的执行 Excel配置:表格导入导出 数据管理:数据表、数据源、数据库连贯及常用字段 文件治理:文件类 3.单位组织 公司治理:总/分公司治理 部门治理:部门 岗位治理:岗位 角色治理:角色 用户治理:用户 4.表单利用 表单设计:设计罕用表单 表单治理:表单根底、条件、列表设置 表单实例:示例 5.流程利用 流程设计:人事、我的项目、购销等各类流程设计 流程工作:待办/已办流程 流程委托:委托别人解决流程 流程监控:已实现/未实现流程整体监控 签章治理:签章类(反对手写) 流程实例:示例(销假流程) 6.挪动治理 挪动开发向导:挪动表单、流程、数据等罕用性能配置向导 挪动性能:挪动端罕用性能 首页图片:挪动首页图 Logo设置:挪动logo 桌面设置:挪动端桌面配置 7.报表利用 报表公布:绑定已设计报表后公布 报表设计:图标、列表设计 业余报表:葡萄城业余报表 报表实例:罕用报表示例 简洁报表:洽购、收支、仓存、收支类报表 8.利用实例 OA办公:新闻、布告、日程、签章、导出模板、邮件核心 ...

August 12, 2020 · 1 min · jiezi

关于框架:net-core快速开发平台learun自主工作流引擎设计规范

一个残缺的工作流管理系统通常由工作流引擎、工作流设计器、流程操作、工作流客户界面、流程监控、表单设计器、与表单的集成以及与应用程序的集成等几个局部组成。 1.工作流引擎 工作流引擎是工作流管理系统的外围局部,次要提供了对工作流定义的解析以及流程流转的反对。工作流定义文件形容了业务的交互逻辑,工作流引擎通过解析此工作流定义文件依照业务的交互逻辑进行业务的流转,工作流引擎通常通过参考某种模型来进行设计,通过调度算法来进行流程的流转(流程的启动、终止、挂起、复原等),通过各种环节调度算法(SPLIT、AND、OR等)来实现对于环节的流转(环节的合并、分叉、抉择、条件性的抉择等)。 2.工作流设计器 工作流设计器为可视化的流程设计工具,用户通过拖放等形式来绘制流程,并通过对于环节的配置来实现环节操作、环节表单、环节参与者的配置。工作流设计器为用户以及开发商提供了疾速绘制、批改流程的形式,工作流设计器的好坏决定到工作流管理系统的易用性。 3.流程操作 流程操作指所反对的对于流程环节的操作,如启动流程、终止流程、挂起流程、直流、分流(单人办理)、并流(多人同时办理)、联审等,象这些流程操作都是可间接基于引擎所提供的环节调度算法来间接反对的,而在理论的需要中,通常须要自在的对于流程进行干预,如取回、回退、跳转、追加、传阅、传阅办理等,而这些流程操作对于工作流引擎来说是不合理的,因而必须独自的去实现。 流程操作反对的好坏间接决定到一个工作流管理系统的实用性。 4.工作流客户界面 工作流客界面程序为工作流零碎的表现形式,通常应用Web形式进行展示,通过提供待办列表、已办列表、执行流程操作、查看流程历史信息等来展示工作流零碎的性能。 5.流程监控 流程监控通过提供图形化的形式来对流程执行过程进行监控,包含流程运行情况,每个环节所消耗的工夫等等,而通过这些可相应的进行流程的优化,以进步工作效率。 6.表单设计器 表单设计器为可视化的表单设计工具,用户通过拖放的形式来绘制业务所需的表单,并可相应的进行表单数据的绑定。 表单设计器为客户以及开发商提供了疾速批改表单的办法,表单设计器的易用与否以及性能的欠缺与否影响到工作流管理系统的易用性。 7.与表单的集成 通常业务流转须要表单来表白理论的业务,因而须要与表单进行集成来实现业务意义,与表单的集成通常包含表单数据的主动获取、存储、批改,表单域的权限管制、流程相干数据的保护以及流程环节表单的绑定。 与表单的集成的好坏影响到工作流管理系统是否能进步开发效率。 8.与应用程序的集成 通过与应用程序的集成来欠缺工作流管理系统的业务意义,次要波及到的是与权限零碎以及组织机构的集成。流程环节须要相应的绑定不同的执行角色,而流程操作通常须要与权限零碎、组织机构进行关联。 演示:http://www.learun.cn/Home/VerificationForm[](http://www.learun.cn/Home/Ver...

August 6, 2020 · 1 min · jiezi

关于框架:推荐一个非常轻量级的Mysql操作框架

举荐一个十分轻量级的Mysql操作框架:传送门 默认反对性能办法阐明boolean has(Serializable id)依据主键查看记录是否存在E findById(Serializable id)依据主键查找对象List<E> findByIds(Collection<? extends Serializable> ids)依据主键批量查找对象List<E> find()查问所有的记录List<E> findByColumn(String column, Serializable value)依据指定字段查问记录List<E> findByColumn(String column, Collection<? extends Serializable> values)依据指定字段查问记录List<E> find(Conditions conditions)依据条件查问,条件的具体用法请看上面的案例Paginator<E> findByPage(Conditions conditions, Integer pageSize, Integer pageNumber)分页查问Integer delete(Serializable id)依据主键删除Integer delete(List<? extends Serializable> ids)依据主键删除Integer delete(String column, Collection<? extends Serializable> values)依据字段删除Integer insert(E entity)保留对象Integer insert(List<E> entitys)批量保留对象Integer update(E entity)批改对象(依据主键批改)Integer update(String id, String key, Object value)批改Integer update(List<? extends Serializable> ids, Map<String, Object> data)批量批改Integer update(String id, Map<String, Object> data)批改Integer update(List<? extends Serializable> ids, String key, Object value)批改第一步:注入jdbcTemplate@Beanpublic JdbcTemplate jdbcTemplate() { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource()); return jdbcTemplate;}也能够通过xml配置文件的形式注入 ...

July 21, 2020 · 1 min · jiezi

第三篇-仿写Vue生态系列枚举与双向绑定

( 第三篇 )仿写'Vue生态'系列___" '枚举' 与 '双向绑定' " 本次任务 对'遍历'这个名词进行死磕.对defineProperty进行分析.实现cc_vue的数据双向绑定.为下一篇 Proxy 代替 defineProperty 做预热.一. 'forEach' vs 'map'很多文章都写过他们两个的区别 forEach没有返回值, map会返回一个数组map利于压缩, 因为毕竟只有三个字母.但是这些区别只是表面上的, 我来展示一个有趣的???? <div id="boss"> <div>1</div> <div>2</div> <div>3</div></div><script> let oD = document.getElementById('boss'); // 正常执行 oD.childNodes.forEach(element => {console.log(element); }); // 报错 oD.childNodes.map(element => { console.log(element); });</script>oDs.childNodes 并不是个数组, 他仍然是伪数组, 但是他可以使用forEach, 这个挺唬人的, 第一反应是这两个遍历方法在实现的方式上是不是有什么不同, 但转念一想感觉自己想歪了, 答案其实是 oDs.childNodes这个伪数组形成的时候, 往身上挂了个forEach... 通过上面的问题我有了些思考 map既然返回新数组, 那就说明他空间复杂度会大一些.某些系统提供的伪数组, 本身会挂载forEach但不会挂载map.综上所述, 还是用forEach保险! 但是就想用map怎么办那?1: slice的原理就是一个一个的循环放入一个新数组; let slice = Array.prototype.slice;slice.call(oD.childNodes).map(()=>{})2: 扩展运算符原理不太一样, 但他一样可以把所有元素都拿出来, 接下来我们就对他进行死磕. ...

August 27, 2019 · 5 min · jiezi

借助URLOS快速安装MixPHP201框架

环境需求最低硬件配置:1核CPU,1G内存(1+1)提示:如果你的应用较多,而主机节点的硬件配置较低,建议在部署节点时开通虚拟虚拟内存;生产环境建议使用2G或以上内存;推荐安装系统:Ubuntu-16.04、Ubuntu-18.04、CentOS7.X、Debian9X的64位的纯净的操作系统;URLOS安装curl -LO www.urlos.com/iu && sh iuMixPHP安装流程登录URLOS系统后台,在应用市场中搜索“MixPHP”,找到之后,直接点击安装按钮 填写服务名称、选择运行节点、服务端口、选择智能部署 填写网站域名、ssh密码(这里的密码是root密码) 然后点击“提交”按钮,等待部署完成; 访问网站访问:www.aaa.com:8088(这里是自己的域名) 登录sftp网站根目录在 data/www/mix/

July 15, 2019 · 1 min · jiezi

浅析基于-Serverless-的前后端一体化框架

概述Serverless 是一种“无服务器架构”模式,它无需关心程序运行环境、资源及数量,只需要将精力聚焦到业务逻辑上的技术。基于 Serverless 开发 web 应用,架构师总是试图把传统的解决方案移植到 Serverless 上,虽然可以做到既拥有 Serverless 新技术带来的红利,又能维持住传统开发模式的开发体验。但是,Serverless 技术带来的改变可能不止这些,可能是颠覆整个传统 web 应用开发模式的革命性技术。 开发模式业务应用的开发模式发展是从一体到分裂为前后端,再到前后端融合为一体过程。 注意:后面所说的后端特指后端业务逻辑。 1、早期,一体 没有前后端的概念,那时候的应用都是单机版,所有的业务逻辑都写一起,开发人员不需要关心网络请求,这个时期的工程师完全专注于业务代码的开发。随着业务规模的增长,也暴露了很多问题: 高并发问题高可用问题说明:业务应用升级困难等一些问题,不是本篇文章所关心,所以就不一一列举出来。 2、现在,分裂 前端 + 高可用高并发运维裹挟着的后端业务逻辑: 说明:现在 Serverless 技术已经出现有一段时间了,不但没有解决开发体验的问题,反而带来更多开发体验问题,所以,在这里我并没有突出 Serverless 技术。 解决的问题: 高并发。通过分布式部署和多级负载均衡等技术解决了业务的高并发问题高可用。通过主从架构等技术解决了业务的高可用问题解决一个问题,带来一堆问题: 分裂业务应用。为了解决高可用和高并发,业务应用引入了分布式架构,通过负载均衡和主从模式来保证高可用和高并发问题,但是这种解决方案对业务应用是侵入式的,从而导致原本高内聚一体化的应用分裂成前端和后端污染业务代码。与高可用、高并发和运维相关的逻辑与后端业务逻辑交织在一起,让后端技术门槛变高,导致需要多个后端工程师才能掌握所有后端技术增加联调成本。前后端的联调工作做日益繁重,成了工程开发效率提升的瓶颈。新功能和 BUG 需要前后端工程师配合才能完成,你如果是全栈开发工程师,你肯定深有体会,很多 BUG 一看就知道是前端问题,还是后端问题不匹配的前后端技术发展速度,前端技术发展迅猛,后端技术相对稳定,前端只能被动的去适配后端,让前端最新的技术在使用体验上大打折扣。最理想的方式是前后端通盘考量,整体发展,不要出现本来后端只需要优化一行代码的事,让前端写一百行代码来实现限制了代码抽象。因为实现的是同一个业务需求,所以前后端代码有高度的相关性,如果我们能在前后端代码之上抽象代码逻辑,肯定能有很大的作为。同时,代码的开发和维护也有质的提升,前后端分裂导致我们不得不局限在前端或者后端进行代码的抽象,抽象出来的代码可能是片面而重复的增加技术复杂度。前后端分裂,前后端工程师各自为营,形成各自的技术栈,包括语言、工具和理念,导致单个工程师维护整个业务应用变得极度困难,也让前后端工程师排斥彼此的技术栈,随着时间的推移,技术栈差异越来越大,一个项目,不管多小,至少两位工程师以上,全栈开发工程师另当别论增加运维成本。需要专门的运维工程师来运维,虽然,现在通过技术手段降低了运维的成本,但是目前运维成本依然很高,难度依然很大这也是为什么创业小公司喜欢全栈开发工程师,因为在创业早期,高可用和高并发的需求不是那么迫切,因而运维也相对简单,使用全栈开发工程师,不仅缩短了项目交付周期,而且也降低了公司的运营成本,这对创业小公司是至关重要的。 3、未来,融合回到到一体 前端 + 后端 + Serverless + 平台服务   =>  业务应用 + Serverless + 平台服务: 说明:共享逻辑是前后端的共享逻辑,在过去,由于前后端分裂,是很难做到前后端层面的代码抽象的,前后端融合后,让这件事变得简单自然。 带来困惑: 前后端分工合作,不是很好吗?在过去,将一个复杂的问题分解成多个简单的子问题,高并发和高可用没法做到不侵入业务应用,这种确实是一种很好的解法,也是没办法中的办法。前后端分工合作带来的成本问题,越发凸显。现在 Serverless 透明的解决了高并发和高可用问题,那么我们为什么还需要从技术维度来划分,我们不是更加推荐按业务维度来划分吗?后端依然很难,驾驭前后端的门槛依然很高?后端代码逻辑虽然没有了高并发和高可用的裹挟,还是会很难,比如 AI。我相信类似这种很难的业务,现在可能有,未来一定会有相关的开发工具包或者平台服务为我们解决,让这些很难的技术平民化。难的技术交给专业的人解决。找回初心: 回归业务,前后端一体化。随着 Serverless 技术的出现,解决了高可用、高并发和运维问题,作为工程师的我们是不是应该回头看看,找回初心:专注于业务代码。让原本在一起的后端业务代码与前端代码再次融合。因此,前后端一体化难道不是我们失去已久的应用开发终极解决方案吗?现状Serverless 已经做到了以下两点: 工程师只需要关心业务逻辑上的技术拥有接近于传统应用开发体验(解决历史遗留问题,可能还有些距离)传统应用框架,食之无味,弃之可惜: 目前,很多用户已经感知到了 Serverless 带来的高可用、高并发和免运维的好处,用户能够很自然的想到如果能将现有的开发框架移植到 Serverless 上,那就太好不过了。Serverless 平台很自然会提供现有框架的移植方案。解决的问题是将传统的解决方案移植到 Serverless 上,让用户在 Serverless 上拥有传统的开发体验应用框架找回初心: ...

July 2, 2019 · 1 min · jiezi

使用-NodeJS-构建现代化的命令行工具

前言这是一篇关于如何使用 NodeJS 构建高性能、高可读性的现代化命令行工具的博客。每当我们想要创建一个基于 NodeJS 的命令行工具时,就会衍生出一堆问题需要解决,比如如何准备开发环境,如何打包转译代码,如何使代码在转译后保持可调用的状态同时尽可能的压缩体积,以及怎样设计项目分配 Command 与 Option 等等,这会浪费巨大的时间,而且并非一定有成果。这时你可以注意到社区几乎所有的命令行工具都是自成一派,并没有严谨的框架或约定约束,也无所谓的最佳实践,这使想要特别是第一次想要开发命令行工具的开发者望而却步,或是几番努力最后却不尽如人意。 举个例子来说,腾讯的 omi 是一个有众多使用者的框架,但其命令行工具 omi / omi-cli 却让人贻笑大方。仅一些简单的下载和创建模板的任务,造出长篇大论的文件不说,下载时依赖数千,包的体积巨大,整体项目毫无设计几乎是随心所欲、天马行空,这就是开发者本身并不擅长此道,只学会了 糊屎。(何谓 糊屎,参阅 JS 优雅指南 2) FUNC 的出现就是为了解决这些问题。func 本身的实现参阅了社区内诸多基于 NodeJS 的命令行工具的优秀实现,与流行的框架设计思路相结合,以优雅的设计、小体积、高性能 等为目标,同时关注开发者体验,大幅度的提升了命令行工具项目的可扩展性与可读性,几乎是如今 NodeJS 社区中开发命令行工具的最优解。我们可以尝试使用 func 构建一个命令行工具。 构建项目在以前流行的一些命令行参数解析的库中,我们在构建项目前需要准备大量的脚本与配置,甚至还要解决文件权限、bin、代码转译等等问题,但使用 func,我们可以仅通过一行命令初始化项目: npm init func项目初始化后进入文件夹,随机使用 npm install 或 yarn 安装依赖,现在就可以正式开发了。可以注意到,func 的项目模板中为我们准备了 start 与 build 2 个脚本,它们都是由 func-service 驱动的,帮助你一键切换开发与生产模式,我们所要做的就是专注于命令行逻辑本身,实现逻辑就够了。 实现逻辑我们可以随意的创建一个类,当它被加上 Command 注解时这就是一个命令,而被加上 Option 注解时就会转变为一个选项: import { Command } from 'func'@Command({ name: 'test' })export class Test {}在命令行中运行 <YOUR NAME> test 时,Test 类将会被调用。选项也是同理。你可以在 constructor 中开始自己的代码,这和你在任何地方写 NodeJS 没有什么不同,当你觉得差不多的时候,运行 npm build 就可以将它打包,一切就是这么简单。 ...

June 23, 2019 · 1 min · jiezi

方案设计如何看待前端框架选型

对于前端团队,可以实现企业受益最大化要点。 一、技术选型的策略1、保证产品质量 (1)功能稳健:网页不白屏,不错位,不卡死;操作正常;数据精准。 (2)体验优秀:加载体验,交互体验,视觉体验,无障碍访问。 2、降低人力成本 (1)降低前期开发成本; (2)降低后期维护成本。 二、前端开发模式选择开发模式:1、纯前端开发;2、前后端分离开发;3、后端主导的开发。 1、纯前端开发 主要是针对静态页面。没有模板和框架参与,基本上一个人就可以hold住,比如:官方网站,招聘站,以及设计感强烈且运营活动页面等。自主权最大,正常是使用nodejs进行辅助开发,上线等。 2、前后端分离开发 现在很多公司的系统都是采用前后端分离的开发模式。根据项目的性质,使用nodejs进行模板渲染(ejs模板,jade模板,dot模板,artTemplate模板),要不是框架自带的渲染的方式(vue,react),但是实质上一样的,都是使用js对页面进行构建。控制权很大。 3、后端主导开发 由于很多历史遗留问题,有的产品还是采用后盾渲染的开发模式,比如一些内部行政系统相关的。在和他们合作的时候,交付原型的时候,需要克制自己: (1)不要使用sass,less产不多的前端预处理器; (2)不使用类似于seajs之类的模块化组件库,而是采用效率更低的人工模块耦合; (3)不追求新技术,使用更基础代码,采用更传统首发,良好的代码设计保证质量【参数接口暴露在外,后端可以轻松配置,而不是耦合在js中】。 这种前端属于支援角色,后期维护通常都是与后端开发一起维护。这就是有时候会出现维护很痛苦的问题。 缺点:增加开发人力成本; 优点:自我牺牲保证项目正常维护下去,职业的体现。 三、前端开发技术选型对于同一个类型的项目,采用开发模式,使用的基本框架都是一致的。 前端技术选型: (1)外部用户的PC站; (2)外部用户的mobile站; (3)外部用户的Native App开发; (4)内部员工的管理后台 1、外部用户的PC站需要有SEO,有加载体验,采用的是前后端分离开发模式,页面直接渲染,基于jquery。 为什么使用jquery? (1)主要是为了兼容IE8; (2)是外部用户,视觉体验高,权重高。适合先有行,再有行。就是说视觉和行为要尽可能分离,会牺牲一点开发成本,但是用户更重要。 (3)绝大多数页面交互轻量用不上数据驱动。 2、外部用户的Mobile站这里说的Mobile站主要是浏览器访问为主的,因此,页面切换都是传统连接跳转,属于传统web应用,前后端分离开发模式,页面直接渲染,采用react。大致原因:使用react 是为了 和APP端的react native保持同步。 3、外部用户的Native App开发前端组有直接参与 Native APP 开发的项目,使用的是 React Native 进行开发。 为啥选择RN,之前Hybrid模式开发有性能优化瓶颈,采用React Native性能可以突破这个瓶颈,有原生的性能,且支持热更新,上手不算太难,跨平台,IOS和android代码复用率90%。适合交互和动画不太复杂的项目,最终要根据项目来。特别适合快速迭代的项目或者前期需要大量试错的项目。 (1)不要随意使用第三方库,后期修改维护不方便,尽量自己写还是自己写; (2)前期还是需要客户端帮忙配合,项目搭建。 4、内部员工的管理后台前后端分离开发,页面侧重异步渲染,使用vuejs。 大致内容是:后台管理系统有大量的增删改查操作,适合具有双向绑定的类库或者框架进行渲染。同时没有兼容性的要求(SEO,首屏渲染),因此单页面是合适的。可以选择vue,react,angular。因为vue对api,文档对开发者更友好。选用好的UI组件,规范贯彻,拆分和按需加载,自动化测试有待加强。 四、总结对于比较正式的项目,前端技术选型策略一定是产品收益最大化,用户在首位。考虑到亿级的用户量,自然技术选型更为谨慎,于是优先选成熟,经典的解决方案。 但是不是说,排斥热门技术。相反,就算还是很不成熟的新技术,只要对产品带来收益的,一定要鼓励应用的,比如AMP和PWA的实践。 其实这时候也会带来一个问题,技术人员对新技术有着天然的学习和实践需求,因为这有助于降低内心的焦虑和不安全感。 尤其是对技术有着狂热的爱好的小伙伴,这些成熟项目由于规范约束,不能随便乱来,很容易让开发人员报国无门的感觉,这该如何达到心理的平衡? 通过边缘项目,实践性的项目,以及团队会自发发起一些有价值的内部项目来满足这样的需求,同时积累宝贵的经验。相当于嵌入了新的平台,让产品,团队和个人都达到非常好的平衡。 产品驱动的文化下,心中想的更多是把用户和产品做更好,让技术服务产品,因产品而技术,而非因技术而技术。 运营驱动的文化下,本质上是吆喝做买卖,成为前言技术的弄潮儿就是和企业文化的契合。 【注:我是saucxs,也叫songEagle,松宝写代码,文章首发于sau交流学习社区https://www.mwcxs.top),关注我们每天阅读更多精彩内容】

June 20, 2019 · 1 min · jiezi

UI2CODE再进化结合Redux的框架升级

摘要: 自从有了ui2code,妈妈再也不用担心我加班背景UI2CODE的目标是通过分析视觉稿得到对应的代码,让AI提高开发效率。然而过去静态化页面的产出,不能得到业务场景的需求。针对于此,我们以UI2CODE自动化开发为基底,结合Redux的消息机制,将自动化生成的维度提升到页面的处理。 透过框架,可自动化生成页面代码,并且具有数据驱动展示、消息派送等动态性能力。期望在复杂的业务场景下,简化开发的工作。并且在使用一致化的架构下,避免未来业务代码耦合严重,使代码分工明确,容易修改维护。 进化后的UI2CODE? 开发者可以透过Intellij Plugin分析视觉稿后会生成对应的视图代码,以及和此页面框架结合的能力。 在整体开发的定位上我们的野心是,提供一套可扩充的页面消息框架,并且自动生成大部分的UI代码。目标带来以下的好处: 快速建构新应用,框架将完成大部分的事,业务开发同学只要专注于业务代码让开发人员的进入门槛降低,在我们落地的经验中,后端同学只要有基本的概念,无需花费太多经历,可直接上手帮忙写代码让页面的架构统一化,让页面的开发有统一的规则,也方便后续的维护提供通用的组件库,可重复利用核心设计思路我们在设计上主要参考于MVP、CLEAN、VIPER以及FISH_REDUX等框架。目的在实践高聚合低耦合的前提下,分拆为视图组装层、视图展示层、数据层、事件交互层。层层分离职责,不互相干扰,又可互相通讯。 分层拆开的好处为容易维护,并且可以单元测试"业务展示"以及"业务逻辑",框架上清晰,容易有一个统一的遵循规则,达到简单编写重复可利用。 UI2CODE页面框架的设计概念为,主要分为Page、Card、Reaction三大元素。在上层的Page负责组装页面,制定页面的风格。Card则为可重复利用的视图展示元素。Reaction则为事件反应的监听。 在整个页面框架上,可以透过UI2CODE Plugin分析自动化生成业务UI,产生出Page、Card、Card(DataModel)。仅需修改Card上额外的业务展示,以及撰写Reaction中的业务逻辑。 具体实现架构在介绍框架组件前,先理解UI2CODE的基本组成页面目录如下: 以Page为单位,页面本体demo_page为其他页面路由调用的起点,透过设置Config.dart决定内部含的卡片列表以及事件处理列表,组合出Card以及Reaction的关联。 其详细的架构关系如下: PagePage为框架基础的单位,为单一页面,负责决定视图的组装以及页面的样式(Template)。 在Page之内可包含若干的Card以及Reaction,分别为视图的展示以及视图的事件处理。可以很清晰地将业务场景做分割成小模块,不互相影响。 Abstract class PageStatelessWidget extends StatelessWidgetimplements Lifecycle 可由UI2CODE Plugin自动化产生框架统一分发管理页面生命周期Lifecycle透过设定Template指定页面要呈现的样版,或者修改如背景等属性透过设定Config指定这个页面含有的Cards和Reactions透过设定PageState可添加额外的数据 Page TemplateTemplate样板为页面的抽象化,在整体页面上分为多个样板可选择。并且支持设置AppBar(非必选)、Header(非必选)、Body、Stack(非必选)等子样板。 样板可比喻为页面的容器,目前支持以下样板,并且可扩充: PageTemplate,通用页面容器,并支持NestedScrollView的Silver Header滚动(若需要)PageBottomNavigatorTemplate,含有底部导航的容器,如首页PageSwitchTabTemplate,含有分页Tab功能的容器各个子样板也有相对应的Template可选择,如在Body内的样板功能为决定内部Cards排列的方式。举例来说,BodyListViewTemplate则是列表展示。 使用Template最大的好处为减少开发工作,可直接使用封装后的接口。并且页面内的所有样板将共用消息机制,可以互相传递消息,如Body内部的卡片很容易发送消息给AppBar等。这是框架上的有力之处。 Page ConfigConfig决定页面的组装,包含了元件有哪些,以及事件处理反射的类绑定。 Extends PageConfig 可由UI2CODE Plugin自动化产生透过设定cards注册这个页面所载入的卡片透过设定actions注册这个页面所响应的类,支持卡片事件以及页面事件支持额外设置AppBar、Header、Stack等组件(非必须)如何绑定,举例来说: void actionConfig(List< List < Function>> actions) {//卡片Card8575, 响应事件的类为Card8575Reactionactions.add(< Function>[() => Card8575, () => new Card8575Reaction()]);} CardCard代表基本的视图展示,业务UI,其中包含了View widget以及DataModel数据。框架内会将两者Data binding起来,由数据来驱动最终展示的呈现。达到如MVP中View和ViewModel的隔离效果。 Abstract class BaseCard<T extends DataModel> extends StatefulWidget 可由UI2CODE Plugin分析视觉稿产生透过BaseCard<T extends DataModel>的标准化,指定数据DataModel绑定Card可以发出事件,不直接操控数据,而让接收者响应 ...

June 6, 2019 · 1 min · jiezi

漫谈分布式计算框架

摘要: 本文主要谈了一些分布式计算框架方面的心得。如果问 mapreduce 和 spark 什么关系,或者说有什么共同属性,你可能会回答他们都是大数据处理引擎。如果问 spark 与 tensorflow 呢,就可能有点迷糊,这俩关注的领域不太一样啊。但是再问 spark 与 MPI 呢?这个就更远了。虽然这样问多少有些不严谨,但是它们都有共同的一部分,这就是我们今天谈论的一个话题,一个比较大的话题:分布式计算框架。 不管是 mapreduce,还是 spark 亦或 tensorflow,它们都是利用分布式的能力,运行某些计算,解决一些特定的问题。从这个 level 讲,它们都定义了一种“分布式计算模型”,即提出了一种计算的方法,通过这种计算方法,就能够解决大量数据的分布式计算问题。它们的区别在于提出的分布式计算模型不同。Mapreduce 正如其名,是一个很基本的 map-reduce 式的计算模型(好像没说一样)。Spark 定义了一套 RDD 模型,本质上是一系列的 map/reduce 组成的一个 DAG 图。Tensorflow 的计算模型也是一张图,但是 tensorflow 的图比起 spark 来,显得更“复杂”一点。你需要为图中的每个节点和边作出定义。根据这些定义,可以指导 tensorflow 如何计算这张图。Tensorflow 的这种具体化的定义使它比较适合处理特定类型的的计算,对 tensorflow 来讲就是神经网络。而 spark 的 RDD 模型使它比较适合那种没有相互关联的的数据并行任务。那么有没有一种通用的、简单的、性能还高的分布式计算模型?我觉着挺难。通用往往意味着性能不能针对具体情形作出优化。而为专门任务写的分布式任务又做不到通用,当然也做不到简单。 插一句题外话,分布式计算模型有一块伴随的内容,就是调度。虽然不怎么受关注,但这是分布式计算引擎必备的东西。mapreduce 的调度是 yarn,spark 的调度有自己内嵌的调度器,tensorflow 也一样。MPI 呢?它的调度就是几乎没有调度,一切假设集群有资源,靠 ssh 把所有任务拉起来。调度实际上应当分为资源调度器和任务调度器。前者用于向一些资源管理者申请一些硬件资源,后者用于将计算图中的任务下发到这些远程资源进行计算,其实也就是所谓的两阶段调度。近年来有一些 TensorflowOnSpark 之类的项目。这类项目的本质实际上是用 spark 的资源调度,加上 tensorflow 的计算模型。 当我们写完一个单机程序,而面临数据量上的问题的时候,一个自然的想法就是,我能不能让它运行在分布式的环境中?如果能够不加改动或稍加改动就能让它分布式化,那就太好了。当然现实是比较残酷的。通常情况下,对于一个一般性的程序,用户需要自己手动编写它的分布式版本,利用比如 MPI 之类的框架,自己控制数据的分发、汇总,自己对任务的失败做容灾(通常没有容灾)。如果要处理的目标是恰好是对一批数据进行批量化处理,那么 可以用 mapreduce 或者 spark 预定义的 api。对于这一类任务,计算框架已经帮我们把业务之外的部分(脚手架代码)做好了。同样的,如果我们的任务是训练一个神经网络,那么用 tensorflow pytorch 之类的框架就好了。这段话的意思是,如果你要处理的问题已经有了对应框架,那么拿来用就好了。但是如果没有呢?除了自己实现之外有没有什么别的办法呢? ...

June 6, 2019 · 2 min · jiezi

Schedulerx20分布式计算原理最佳实践

1. 前言Schedulerx2.0的客户端提供分布式执行、多种任务类型、统一日志等框架,用户只要依赖schedulerx-worker这个jar包,通过schedulerx2.0提供的编程模型,简单几行代码就能实现一套高可靠可运维的分布式执行引擎。 这篇文章重点是介绍基于schedulerx2.0的分布式执行引擎原理和最佳实践,相信看完这篇文章,大家都能写出高效率的分布式作业,说不定速度能提升好几倍:) 2. 可扩展的执行引擎Worker总体架构参考Yarn的架构,分为TaskMaster, Container, Processor三层: TaskMaster:类似于yarn的AppMaster,支持可扩展的分布式执行框架,进行整个jobInstance的生命周期管理、container的资源管理,同时还有failover等能力。默认实现StandaloneTaskMaster(单机执行),BroadcastTaskMaster(广播执行),MapTaskMaster(并行计算、内存网格、网格计算),MapReduceTaskMaster(并行计算、内存网格、网格计算)。Container:执行业务逻辑的容器框架,支持线程/进程/docker/actor等。Processor:业务逻辑框架,不同的processor表示不同的任务类型。以MapTaskMaster为例,大概的原理如下图所示: 3. 分布式编程模型之Map模型Schedulerx2.0提供了多种分布式编程模型,这篇文章主要介绍Map模型(之后的文章还会介绍MapReduce模型,适用更多的业务场景),简单几行代码就可以将海量数据分布式到多台机器上进行分布式跑批,非常简单易用。 针对不同的跑批场景,map模型作业还提供了并行计算、内存网格、网格计算三种执行方式: 并行计算:子任务300以下,有子任务列表。内存网格:子任务5W以下,无子任务列表,速度快。网格计算:子任务100W以下,无子任务列表。4. 并行计算原理因为并行任务具有子任务列表: 如上图,子任务列表可以看到每个子任务的状态、机器,还有重跑、查看日志等操作。 因为并行计算要做到子任务级别的可视化,并且worker挂了、重启还能支持手动重跑,就需要把task持久化到server端: 如上图所示: server触发jobInstance到某个worker,选中为master。MapTaskMaster选择某个worker执行root任务,当执行map方法时,会回调MapTaskMaster。MapTaskMaster收到map方法,会把task持久化到server端。同时,MapTaskMaster还有个pull线程,不停拉取INIT状态的task,并派发给其他worker执行。5. 网格计算原理网格计算要支持百万级别的task,如果所有任务都往server回写,server肯定扛不住,所以网格计算的存储实际上是分布式在用户自己的机器上的: 如上图所示: server触发jobInstance到某个worker,选中为master。MapTaskMaster选择某个worker执行root任务,当执行map方法时,会回调MapTaskMaster。MapTaskMaster收到map方法,会把task持久化到本地h2数据库。同时,MapTaskMaster还有个pull线程,不停拉取INIT状态的task,并派发给其他worker执行。6. 最佳实践6.1 需求 举个例子: 读取A表中status=0的数据。处理这些数据,插入B表。把A表中处理过的数据的修改status=1。数据量有4亿+,希望缩短时间。6.2 反面案例 我们先看下如下代码是否有问题? public class ScanSingleTableProcessor extends MapJobProcessor { private static int pageSize = 1000; @Override public ProcessResult process(JobContext context) { String taskName = context.getTaskName(); Object task = context.getTask(); if (WorkerConstants.MAP_TASK_ROOT_NAME.equals(taskName)) { int recordCount = queryRecordCount(); int pageAmount = recordCount / pageSize;//计算分页数量 for(int i = 0 ; i < pageAmount ; i ++) { List<Record> recordList = queryRecord(i);//根据分页查询一页数据 map(recordList, "record记录");//把子任务分发出去并行处理 } return new ProcessResult(true);//true表示执行成功,false表示失败 } else if ("record记录".equals(taskName)) { //TODO return new ProcessResult(true); } return new ProcessResult(false); }}如上面的代码所示,在root任务中,会把数据库所有记录读取出来,每一行就是一个Record,然后分发出去,分布式到不同的worker上去执行。逻辑是没有问题的,但是实际上性能非常的差。结合网格计算原理,我们把上面的代码绘制成下面这幅图: ...

May 31, 2019 · 2 min · jiezi

独家揭秘阿里小程序的一云多端看这篇就够了

摘要: 阿里巴巴小程序一云多端的整体战略,以及阿里小程序后续为开发者提供的云服务(云应用、云开发等)、开发者工具链(IDE、插件、SDK等)、跨端框架能力说明。同时结合繁星计划后续提供给开发者的扶持和ISV的权益体系做一个整体的介绍。专家介绍 视频回放https://yq.aliyun.com/live/1097 阿里小程序的一云多端相信绝大部分同学知道阿里一云多端的项目,最早始于19年三月份在北京云栖大会上,阿里云的CEO在云栖大会上对外发布了一云多端的项目。 一云多端是什么?大家今天常见都是微信小程序,微信小程序实际上是一个变化的体系,在它上面开发一个小程序,只能在微信上跑。是不是可以有另外一种方式,能不能开发一个小程序,比如:我写了前端代码,既能在微信上跑,也能在支付宝、高德、头条、百度、哪里都能跑。 这样对研发同学的成本要低很多,这就是多端的概念。 相比较我们App的生态体系,微信其实类似于iOS,他自己自成了一个闭环。 阿里巴巴想做的事情就是我们希望类似于 Android 这样一个开放联盟,能形成整个除了阿里内部,包括阿里生态公司,包括外部的一些公司,都能共用整个小程序的一个框架,共用小程序的一个体系,这是当时对外宣称要做一云多端目的。 一云指的是什么呢?一云指的是:给大家举个例子,我们今天在做微信的小程序,我们都知道他的ID里其实是有自己的云服务的,那我们在做支付宝小程序的时候,大家也会感知到它其实也是一个闭环。 那我们从业务的视角来看,比如:我作为星巴克的开发者,我肯定希望我后端的服务都供用在我自己的服务里,让所有的端都能供链到我这里,这才是对业务来讲价值最大、最好的一个点。这就是阿里巴巴想强调的一云,通过我们这样整体的云,来支持我们这样所有的端。 这是一个大的背景,具体我们来看一下,微信小程序大概在2016年开始做,差不多历时了三年才有了今天我们看到的小程序这样一个繁荣的生态,真正让大家感知到这样一个风口差不多是在18年,大概是跳一跳那个小程序开始火起来以后,大家才感知到小程序原来可以这么玩,越来越多这样的玩家入驻了。 截止到2018年底,全网的小程序已经超过了200万,整个小程序的开发其实呈现出井喷的现状。 200万是什么概念呢?现场有多少同学知道,当前iOS系统中的 App Store,它里面有多少应用? 据我了解的一个数据 ,18年的时候,整个 App Store 里也才230万个 App 应用。大家看小程序这个行业,才经历了两三年基本已经到了我们这样一个量级,根据我们现在了解的一些调研报告的数据,2019年可能预计会到500万的量级,今年还会有一个很快速增长的过程。 整个微信小程序活跃用户的增长量其实是趋于平缓的,获客成本其实是逐渐逐渐的高起,整个发展的核心已经由传统意义上的拉新到现在更多的是运营。 微信小程序的活跃用户现在已经有7亿多,整个微信App,活跃用户也才10亿到11亿左右,它的天花板其实已经很低了,随时就可以触碰到,在这种背景下,我们作为一个开发者,作为一个企业,其实是希望能够有更多其他平台的拥抱 ,我们可以通过其他的渠道来获取到我们这样的流量。 阿里巴巴正在做的事情是:会全面的拥抱小程序,为小程序提供全面的技术、业务、生态的支持,能够帮助我们的企业在未来的云生态里面走的更远。 下面这张图,是我们刚才介绍的数据: 左边的数据是我们小程序的增长量,17年数据是100多万,到18年已经200多万了,按照我们现在预测的数据,到了2019年可能有四五百万,基本上是每年翻倍的节奏。 右边的数据是全网小程序用户数的规模,大概分布情况是:支付宝大概是四五亿,微信大概是七亿左右,百度大概是两三亿,加在一起应该有十几亿的数据。后续随着越来越多大平台的参与 ,小程序的用户规模也会越来越大的。 具体到阿里巴巴,我们有一些面向场景主流的端,比如:我们面向电商购物场景的淘宝,面向出行领域的高德,面向我们金融和本地生活的支付宝,面向这种企业服务这块的钉钉,这些端都会全面的拥抱小程序。 具体这些端后面会做什么,接下来几位讲师会和大家详细分享,我们在不同端里,小程序具体是什么样的玩法?会给我们的开发者提供什么样的业务能力?我在这里就不做太多的介绍了。 对于个人开发者,对于企业而言,当前阿里在做的一云多端对我们来讲有什么价值,对我们来讲有什么样的机会,我们可以看一下这张图。 我们传统意义上讲,大家其实都知道小程序,大家能感知到的就是微信,因为只有这样一个声音,后续我们期望能让大家知道小程序不只等于微信小程序, 阿里其实也会有相应的能力。除了阿里以外,大家已经知道的,像今日头条,像百度也都陆陆续续加入了小程序战场,后续小程序真的不等于就是微信小程序了,全网主流平台都会去做支持。 其次我们传统意义上,小程序在微信覆盖的用户群体,覆盖的场景以社交场景为主,后续随着更多的App和场景的加入,我们的小程序基本上可以覆盖全场景,不仅仅是当前的社交 ,我们有支付场景、有金融场景、有出行场景、有企业服务场景,有越来越多的场景。随着阿里小程序战略的演进,后续会把阿里小程序的开发框架、开发标准对外开放,除了阿里内部小程序能用以外,整个阿里系的一些App,比如:像微博等一些App都可以直接运行阿里的小程序。再往后会把开发框架开放给企业自己的App,可以真正的做到一个小程序在全网都能跑,能支持全网的用户覆盖。 基于这样的背景,现在这样流量红利,如果我们不仅仅看微信的话,流量红利其实又有一波已经进来了,不仅仅是微信平台,现在全网已经有十几个小程序的平台在加入到战场,整体的活跃用户现在已经能突破十亿以上,这种小程序的入口其实也很多。 从场景上来说:除了像微信社交场景以外,电商的LBS、搜索、内容,能覆盖的场景也是会越来越多的。 对企业而言,对个人开发者而言,价值在于:现在中国这个人口红利已经逐渐的消失了,如果自己做一个App,获客成本其实已经很高了,即使是微信小程序做了这么长时间,微信小程序的获客成本也是越来越高。 我们如何来降低获客成本,一个比较好的方式就是借助不同App平台,通过不同的小程序平台来获取我们的流量扶持,能够通过低成本的方式来获取我们的客户,这是一个我们价值点所在。 不同的这个App有不同的业务能力,比如:高德,大家更多的就是用它的LBS能力,我们可以获得位置,出行数据等等,可能这些能力你在微信里是获取不到的,每个不同的开发者,所面向的场景是不一样的,所要的业务能力也是会有比较大的差异的。如果可以借助平台的这个业务的赋能,让业务场景能够快速的扩展,这对大家来讲是一个比较好的机会。 从阿里本身的经济体而言,其实会给不同的开发者提供业务的赋能,比如:一些API的能力,地图的API、商家的API、风控的API、支付的API,我相信大部分的开发者可能更多的都是奔着更好用的业务能力来的。 对企业大的战略而言,也有几个比较好的点。一个点是前面说的,微信小程序的获客成本已经逐渐提高了,其他一些平台属于刚起步的阶段,流量其实相对来讲还属于比较充沛。如果能抓住这样的机会,能早一点进去,流量的获客成本相对比较低的,业务的扶持也能让自身的小程序,自身的业务有快速的发展。 通常情况下,大家都知道,鸡蛋不要放在一个篮子里,因为放在一个篮子里风险是比较高的,如果我们把所有的业务全部承载在微信的小程序里,万一微信的小程序开发的规则以及的业务变化,实际上对大家自身的业务影响是很大的,甚至是致命的影响。多元发展其实是所有开发者,所有企业都必须考虑这样的点,今天刚好也确实是有这样的机会。 多端小程序的价值多端小程序对大家到底有哪些核心的价值? 第一个是场景,在于我们传统意义上讲微信,更多的是我们有人际关系的关系链在微信上,其他的场景,比如:我是做汽配相关的,这时候我在微信上很难获取到适合的用户群体。 我不知道大家有没有看过一份数据:现在支付宝、微信、百度的小程序的留存率,从数据上看,支付宝的小程序留存率是最高的,为什么呢?原因在于支付宝是一个场景化的App,它主要面向的是一个支付的场景和本地生活化的场景,大家用这个App的时候其实就是它的目标用户群体,基于这个场景来开发App,其实就很容易获客,如果我们的业务其实做的还ok的话,这批用户的留存和后续的转化其实是很高的。 第二个是流量的价值,流量的价值在于由单一的微信生态流量逐渐转变为全网的流量,因为我们有越来越多的App加入到小程序的战场。除此之外,像阿里内部的高德、钉钉、淘宝是有大量的企业能力,大量的设备能力的数据在里面,通过这些能够帮助大家更好的获取流量。 第三个是业务,相比较其他的平台,阿里的一个很大的优势在于相对的业务能力板块是比较全的,金融支付能力、企业的服务能力、物流能力,一系列的能力都可以帮助大家来做赋能,让大家更好更快的开发自己的业务。 第四个是用户粘性,之前数据也举例了,大家通过单一渠道来获取以及通过社交渠道来获取的流量,相对来讲粘性是比较差的,因为它使用的场景是面向我们当前社交的,我们跟朋友的聊天,不大会关注其他的场景,可能也有一定的转化,但这个转化率一定是不高的。如果是奔着特定场景的,相对来讲这个粘性要高很多。 第五个是成本,一个产品它的生命周期如果从刚起步到成熟到后续的衰落,那么微信当前就处于偏成熟的阶段,这时候大家认知的很多,使用的人也很多,很多人去抢那一点流量,成本逐渐越来越高,如果有一个新的战场,一片新的领域,大家能早一点有机会进去,这个时候获客成本其实是很低的。 第六个是品牌的效应,我们可以让整个小程序的品牌,能够更好的扩展,能做到所有人都能够共知的状况。 前面介绍的是小程序的背景,对当前的机会所在,具体到阿里小程序,这张图就是阿里产品的能力大图,对应的小程序解决方案,从最底下看是我们阿里经济体的能力的输出,后续大家通过我们的阿里小程序云,可以获取到阿里内部的所有这些业务能力,支付宝的能力、钉钉、高德、淘宝所有的能力都会通过小程序云来对外进行透出。 小程序云本身它会提供哪些能力呢?小程序云里包含两个部分: 第一部分是云应用,云应用来帮助大家来做线上的资源编排和应用拓广,比如:作为一个开发者,我们可能有自己的后端服务,后端服务可能想自己去做部署,部署是有成本的,可能首先要去买ECS、买服务器、买数据库、买IDS、还得买流量、买官网IP,买好了一系列的原子的原产品,接下来要做的事情就是把环境给打好,具备一个网络环境,具备可访问的环境,有了可访问的环境,接下来还得想怎么去做部署,做更新,云应用核心所解决的就是以上事情。 第二部分是云开发,云开发简单地讲它是一个Serverless 的套件,云开发不仅仅是面向开发者,在开发者的领域会提供函数计算的能力、存储能力、数据库的能力,同时也会面向运营测,会提供你当前小程序端测的数据统计分析,提供对应的用户反馈的能力,接下来还有类似做运营提供图片设计的在线能力,这些能力都会通过 Serverless 的套件对外透出。 具体到业务会更多,阿里经济体大家想核心想使用的业务能力,比如:云视频、内容安全能力等都会通过当前的 Serverless 的套件对外透出。另一个是小程序云的整体价值,前面强调的一云多端的一云目的是什么,一云并不是希望大家都把数据统一的放在阿里云上,一云的核心对客户支撑的价值在于我们把所有的资源都聚拢在一起,而不是面向不同的场景,来提供不同的后端服务。这样是一种极大效率的降低。那另外一个是期望大家数据能统一,有了数据以后,我们才好有后续基于数据的运营,基于数据的业务分析和扩展,这是我们希望做到一云的效果,通过一朵云来支持整个小程序业务的发展,支持企业,支持我们个人开发者业务的发展。 ...

May 29, 2019 · 1 min · jiezi

开发函数计算的正确姿势-移植-nextjs-服务端渲染框架

首先介绍下在本文出现的几个比较重要的概念: 函数计算(Function Compute): 函数计算是一个事件驱动的服务,通过函数计算,用户无需管理服务器等运行情况,只需编写代码并上传。函数计算准备计算资源,并以弹性伸缩的方式运行用户代码,而用户只需根据实际代码运行所消耗的资源进行付费。函数计算更多信息 参考。Fun: Fun 是一个用于支持 Serverless 应用部署的工具,能帮助您便捷地管理函数计算、API 网关、日志服务等资源。它通过一个资源配置文件(template.yml),协助您进行开发、构建、部署操作。Fun 的更多文档 参考。2.0 版本的 Fun,在部署这一块做了很多努力,并提供了比较完善的功能,能够做到将云资源方便、平滑地部署到云端。但该版本,在本地开发上的体验,还有较多的工作要做。于是,我们决定推出 Fun Init 弥补这一处短板。Fun Init: Fun Init 作为 Fun 的一个子命令存在,只要 Fun 的版本大于等于 2.7.0,即可以直接通过 fun init 命令使用。Fun Init 工具可以根据指定的模板快速的创建函数计算应用,快速体验和开发函数计算相关业务。官方会提供常用的模板,用户也可以自定自己的模板。背景next.js 是一种 React 的服务端渲染框架,且 next.js 集成度极高,框架自身集成了 webpack、babel、express 等,使得开发者可以仅依赖 next、react、react-dom 就可以非常方便的构建自己的 SSR React 应用,开发者甚至都不用像以前那样关心路由。 next.js 的高度集成性,使得我们很容易就能实现代码分割、路由跳转、热更新以及服务端渲染和前端渲染。 next.js 可以与 express、koa 等服务端结合使用。为了能让 next.js 在函数计算运行,首先需要让 next.js 在 express 中运行起来,然后再移植 express 到函数计算中运行。express 应用移植相关文章: 开发函数计算的正确姿势——移植 Express移植 express.js 应用到函数计算next.js 运行在 express 中现在,我们提供了一个 fun 模块,通过该模板,三分钟就可以让 next.js 应用在函数计算中运行起来。效果如下: 快速开始1. 安装 node ...

May 24, 2019 · 1 min · jiezi

首次披露阿里线下智能方案进化史

阿里妹导读:AI 技术已经从互联网走向零售、汽车、银行等传统行业。受限于延时、成本、安全等多方面的限制,单一的云解决方案往往不能满足场景需求。线下智能方案逐步成为了智能化过程中重要的一环,今天,我们就一起来了解这一环,希望这些内容可以让同学了解线下智能的前景和其中待解决的技术点。前言阿里巴巴机器智能实验室线下智能团队从16年底开始涉及线下智能领域,从算法、工程、产品化、业务落地多个方面入手,与合作伙伴们一起取得了一些小小的成绩。算法方面,我们提出了自主研发的模型压缩方法,新型模型结构和目标检测框架;工程方面,我们研发出一套非数据依赖的量化训练工具,并且针对不同硬件平台,研发了高效推理计算库;同时我们也和服务器研发团队一起抽象出了一套软硬件产品化方案,以服务多样的业务形式,并在真实业务场景中实验落地。 在后面的篇幅中,我们主要会从算法探索、训练工具、推理框架、产品化和业务模式等方面对之前的工作做一个总结和分享。 算法探索基于 ADMM 的低比特量化 低比特量化是模型压缩( ModelCompression )和推理加速( Inference Acceleration )中一个核心的问题,目的是将神经网络中原有的浮点型参数量化成 1-8Bits 的定点参数,从而减小模型大小和计算资源消耗。为了解决这个问题,我们提出了基于 ADMM(Alternating Direction Method ofMultipliers)的低比特量化方案。在公开数据集 ImageNet 上,我们在 Alexnet,ResNet-18,Resnet-50 等经典 CNN 网络结构上做了实验,无论是精度上还是速度上均超过了目前已知的算法。我们可以在 3-bit 上面做到几乎无损压缩。目前该方法已经被广泛应用到各种端上目标检测和图像识别的实际项目中。相关成果已经在 AAAI 2018 上发表。 统一量化稀疏框架 量化技术可以通过简化计算单元(浮点计算单元->定点计算单元)提升推理速度。 稀疏化( Pruning ) 技术则是通过对神经网络中的通路进行裁剪来减少真实计算量。我们很自然的将这两个技术融合到了一起,来获取极限的理论加速比。在剪枝过程中,我们采用了渐进式的训练方法,并结合梯度信息决定网络中路径的重要程度。在 ResNet 结构上,我们可以做到90%稀疏度下的近似无损压缩。 在稀疏化研究过程中,我们发现了一个问题,更细粒度的裁剪往往会获得更高的精度,但是代价是牺牲了硬件友好性,很难在实际应用中获得理论加速比。在后面的章节中,我们会通过两个角度来解决这个问题: 软硬件协同设计,从软硬件角度同时出发解决问题;新型轻量级网络,从软件角度设计适合更适合现有硬件的结构。软硬件协同网络结构 通过量化和稀疏技术,我们可以获得一个理论计算量足够低,所需计算单元足够简单的深度网络模型。下一个要解决的问题就是我们如何将其转换成一个真实推理延时低的算法服务。为了挑战极限的推理加速效果,我们和服务器研发团队一起,从软硬件联合设计出发解决该问题。在该项目中,我们提出了以下几个创新点,其中包括: 软硬件协同设计方面,我们针对硬件物理特性提出了异构并行分支结构,最大化并行效率。算法方面,我们利用量化、稀疏、知识蒸馏等技术,将理论计算量压缩到原始模型的18%。硬件方面,我们通过算子填充技术解决稀疏计算带来的带宽问题,利用算子重排技术平衡PE负载。通过上述方案,我们只需要 0.174ms 的 latency 就可以完成 resnet-18 复杂程度的模型推理,达到业内最佳水平。该方案在对 latency 敏感的领域具有极大的优势。相关成果已经在 HotChips 30 上展出。 新型轻量级网络 软硬件协同设计是一个非常好的推理解决方案,但是改方案的开发成本和硬件成本都很高。某些特定的场景对于 latency 和 accuracy 的容忍度比较高(例如人脸抓拍)。为了解决这类需求,我们提出了一种多联合复用网络 (Multi-Layer Feature Federation Network, MuffNet) ,该结构同时具有3个特点: ...

May 21, 2019 · 2 min · jiezi

mybatis处理枚举类

mybatis自带对枚举的处理类org.apache.ibatis.type.EnumOrdinalTypeHandler<E> :该类实现了枚举类型和Integer类型的相互转换。但是给转换仅仅是将对应的枚举转换为其索引位置,也就是"ordinal()"方法获取到的值。对应自定义的int值,该类无能为力。 org.apache.ibatis.type.EnumTypeHandler<E>:该类实现了枚举类型和String类型的相互转换。对于想将枚举在数据库中存储为对应的int值的情况,该类没办法实现。 基于以上mybatis提供的两个枚举处理类的能力有限,因此只能自己定义对枚举的转换了。 自定义mybatis的枚举处理类EnumValueTypeHandler该类需要继承org.apache.ibatis.type.BaseTypeHandler<E>,然后在重定义的方法中实现自有逻辑。 import java.sql.CallableStatement;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.SQLException;import org.apache.ibatis.type.MappedTypes;import org.apache.ibatis.type.BaseTypeHandler;import org.apache.ibatis.type.JdbcType;/** * 处理实现了{@link EsnBaseEnum}接口的枚举类 * @author followtry * @time 2016年8月16日 下午8:06:49 * @since 2016年8月16日 下午8:06:49 */ //在 xml 中添加该 TypeHandler 时需要使用该注解@MappedTypes(value = { QcListTypeEnum.class, SellingQcBizTypeEnum.class})public class EnumValueTypeHandler<E extends EsnBaseEnum> extends BaseTypeHandler<E> { private Class<E> type; private final E[] enums; public EnumValueTypeHandler(Class<E> type) { if (type == null) { throw new IllegalArgumentException("Type argument cannot be null"); } this.type = type; this.enums = type.getEnumConstants(); if (this.enums == null) { throw new IllegalArgumentException(type.getSimpleName() + " does not represent an enum type."); } } @Override public void setNonNullParameter(PreparedStatement ps, int i, E parameter, JdbcType jdbcType) throws SQLException { //获取非空的枚举的int值并设置到statement中 ps.setInt(i, parameter.getValue()); } @Override public E getNullableResult(ResultSet rs, String columnName) throws SQLException { int i = rs.getInt(columnName); if (rs.wasNull()) { return null; } else { try { return getEnumByValue(i); } catch (Exception ex) { throw new IllegalArgumentException( "Cannot convert " + i + " to " + type.getSimpleName() + " by ordinal value.", ex); } } } /** * 通过枚举类型的int值,获取到对应的枚举类型 * @author jingzz * @param i */ protected E getEnumByValue(int i) { for (E e : enums) { if (e.getValue() == i) { return e; } } return null; } @Override public E getNullableResult(ResultSet rs, int columnIndex) throws SQLException { int i = rs.getInt(columnIndex); if (rs.wasNull()) { return null; } else { try { return getEnumByValue(i); } catch (Exception ex) { throw new IllegalArgumentException( "Cannot convert " + i + " to " + type.getSimpleName() + " by ordinal value.", ex); } } } @Override public E getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { int i = cs.getInt(columnIndex); if (cs.wasNull()) { return null; } else { try { return getEnumByValue(i); } catch (Exception ex) { throw new IllegalArgumentException( "Cannot convert " + i + " to " + type.getSimpleName() + " by ordinal value.", ex); } } }}该处理器是处理继承了EsnBaseEnum接口的枚举类,因为该接口中定义了获取自定义int值的方法。 ...

May 13, 2019 · 3 min · jiezi

从-React-Native-到-Flutter移动跨平台方案的真相

作者:LeanCloud 郑鹏 2018 年 12 月,Google 发布了 Flutter 1.0 正式版,似乎再次点燃了人们对移动跨平台开发的热情。上一次出现类似的情况,是在 15 年年初,Facebook 发布 React Native 的时候。四年不到的时间里,有两家大公司相继推出了自己的移动跨平台方案(当然还有 16 年的时候,微软收购了 Xamarin,不过没有前两个那么引人注目罢了),同时这些方案也受到了市场的追逐。这些现象,似乎预示着,跨平台开发才是移动开发的未来,或者说,跨平台开发才是一种更好的开发方式。 既然它是热点,那肯定有可以讨论的地方。不过,在说 React Native 和 Flutter 之前,我觉得要先谈一谈「跨平台开发」。 移动跨平台方案那什么是「跨平台开发」呢? 通常意义上来说,如果你想在 iOS 以及 Android 系统里,提供有相同内容的 App,那么使用 Apple 提供的构建工具,开发一个 App,然后上架到 AppStore,同时使用 Google 提供的构建工具,开发一个 App,然后上架到 Google Play。这两个 App 的实现,除了使用的工具不同之外,大部分业务逻辑是相同的。你可以发现,在这个过程中,产生了「重复」。 在重构时,如果项目里有大量的重复代码,或者重复逻辑,我们一般会将这些代码或逻辑以函数,模块或库的形式做封装,这个过程最大化的消除了重复的代码,最终达到简化项目的代码这一目的。 所以在我看来,「跨平台开发」也是基于这个思想而产生的,人们想要一套减少甚至不用写重复逻辑的解决方案,然后市场给予了人们期望的方案。跨平台方案的最大特点,可以用 Sun 当年在推广 Java 时,所使用的一句口号:”Write once, run anywhere” 作为总结。这一句话,也被如今的 React Native 以及 Flutter 引用或继承。 React NativeReact Native 是由 Facebook 所主导的跨平台方案,得益于 Javascript 以及 ReactJS 的流行,React Native 在推出时,便受到了大量的追捧。除了跨平台的特性,React Native 最大的特点就是,可以使用 Javascript 来构建移动应用,并且最终应用的表现形式,可以做到和使用原生开发套件开发的应用相差无几。 ...

May 13, 2019 · 2 min · jiezi

fong-纯typescript的node-gRPC微服务框架

简介fong: A service framework of node gRPC. github: https://github.com/xiaozhongliu/fong fong是一个完全用typescript编写的node gRPC框架, 可以基于它很方便地编写gRPC微服务应用. 一般是用来编写service层应用, 以供bff层或前端层等调用. 优点1.纯typescript编写, typescript的好处不用多说了. 并且用户使用这个框架框架时, 查看定义都是ts源码, 用户使用框架感受不到type definition文件. 2.效仿egg.js的『约定优于配置』原则, 按照统一的约定进行应用开发, 项目风格一致, 开发模式简单, 上手速度极快. 如果用过egg, 就会发现一切都是那么熟悉. 对比目前能找到的开源node gRPC框架很少, 跟其中star稍微多点的mali简单对比一下: 对比方面malifong项目风格约定 √定义查看跳转definition源代码编写语言javascripttypescriptproto文件加载仅能加载一个按目录加载多个代码生成 √中间件√√配置 √日志 √controller加载 √service加载 即将支持, 目前可以自己import即可util加载 即将支持, 目前可以自己import即可入参校验 即将支持插件机制 打算支持更多功能 TBD示例示例项目github: https://github.com/xiaozhongliu/ts-rpc-seed 运行服务使用vscode的话直接进F5调试typescript. 或者: npm start测试请求ts-node tester# 或者:npm run tscnode dist/tester.js使用目录约定不同类型文件只要按以下目录放到相应的文件夹即可自动加载. root├── proto| └── greeter.proto├── config| ├── config.default.ts| ├── config.dev.ts| ├── config.test.ts| ├── config.stage.ts| └── config.prod.ts├── midware| └── logger.ts├── controller| └── greeter.ts├── service| └── sample.ts├── util| └── sample.ts└── typings| ├── enum.ts| └── indexed.d.ts├── log| ├── common.20190512.log| ├── common.20190513.log| ├── request.20190512.log| └── request.20190513.log├── app├── packagen├── tsconfign└── tslintn入口文件import App from 'fong'new App().start()配置示例默认配置config.default.ts与环境配置config.<NODE_ENV>.ts是必须的, 运行时会合并. 配置可从ctx.config和app.config获取. ...

May 13, 2019 · 2 min · jiezi

阿里新一代分布式任务调度平台Schedulerx20破土而出

1. 产品简介Schedulerx2.0是阿里中间件自研的基于Akka架构的新一代分布式任务调度平台,提供定时、任务编排、分布式跑批等功能。使用Schedulerx2.0,您可以在控制台配置管理您的定时任务,查询历史执行记录,查看运行日志。借助Schedulerx2.0,您还可以通过工作流进行任务编排和数据传递。Schedulerx2.0还提供了简单易用的分布式编程模型,简单几行代码就可以将海量数据分布式到多台机器上执行。 Schedulerx2.0提供了任务调度与执行的一整套解决方案,在阿里巴巴集团内部广泛使用并久经考验,具有高可靠、海量任务、秒级别调度等能力。 上线时间:2019-04-30 2. 背景Schedulerx2.0是Schedulerx1.0(DTS)的下一代产品,采用全新的架构,是全新自研的下一代分布式任务调度平台,不但解决了老产品的性能瓶颈,还提供了更多更快更强的能力。 更多:支持多种时间表达式,任务编排,支持更多的业务场景。单集群支持上千万任务,一天上十亿次调度,支持更多的任务数。更快:支持秒级别调度,处理准实时业务。更强:支持日志查询、原地重跑、重刷数据等多种操作,提供更强的运维能力和排错手段,解决为什么没跑,为什么失败,为什么跑得慢等问题。3. 功能3.1 强大的定时调度器3.1.1 Crontab 支持unix crontab表达式,不支持秒级别。 3.1.2 Fixed rate 众所周知,crontab必须被60整除,比如想每隔40分钟跑一次,cron不支持。Fixed rate专门用来做定期轮询,表达式简单,不支持秒级别。 3.1.3 Fixed delay 适合对实时性要求比较高的业务,比如每次执行完成隔10秒再跑,那么second delay非常适合你。并且second delay能支持到秒级别。 3.1.4 日历 支持多种日历,还可以自定义导入日历。比如金融业务需要在每个交易日执行。 3.1.5 时区 跨国的业务,需要在每个国家的时区定时执行某个任务。 3.2 任务编排支持工作流(DAG)进行任务编排,操作简单,前端直接单手操作拖拖拽拽即可。详细的任务状态图能一目了然看到下游任务为什么没跑。 3.3 任务类型支持多种任务类型,可以无限扩展。 java:可以跑在用户进程中,也可以上传jar包动态加载。shell:前端直接写shell脚本。python:前端直接写python脚本,需要机器有python环境。go:前端直接写go脚本,需要机器有go环境。自定义:用户甚至可以自定义任务类型,然后实现一个plugin就行了。3.4 执行方式&分布式编程模型3.4.1 执行方式 单机:随机挑选一台机器执行广播:所有机器同时执行且等待全部结束并行计算:map/mapreduce模型,1~300个子任务,有子任务列表。内存网格:map/mapreduce模型,10W以下子任务,无子任务列表,基于内存计算,比网格计算快。网格计算:map/mapreduce模型,100W以下子任务,无子任务列表,基于文件H2计算。3.4.2 分布式编程模型 Map模型:类似于hadoop mapreduce里的map。只要实现一个map方法,简单几行代码就可以将海量数据分布式到客户自己的多台机器上执行,进行跑批。MapReduce模型:MapReduce模型是Map模型的扩展,新增reduce接口,所有子任务完成后会执行reduce方法,可以在reduce方法中返回该任务实例的执行结果,或者回调业务。3.5 强大的运维能力数据大盘:控制台提供了执行记录大盘和执行列表,可以看到每个任务的执行历史,并提供操作。查看日志:每条执行记录,都可以详情中的日志页面实时看到日志。如果任务运行失败了,前端直接就能看到错误日志,非常方便。原地重跑:任务失败,修改完代码发布后,可以点击原地重跑。标记成功:任务失败,如果后台把数据处理正确了,重跑又需要好几个小时,直接标记成功就好了。Kill:实现JobProcessor的kill()接口,你就可以在前端kill正在运行的任务,甚至子任务。3.6 数据时间Schedulerx2.0可以处理有数据状态的任务。创建任务的时候可以填数据偏移。比如一个任务是每天00:30运行,但是实际上要处理上一天的数据,就可以向前偏移一个小时。运行时间不变,执行的时候通过context.getDataTime()获得的就是前一天23:30。 3.7 重刷数据既然任务具有了数据时间,一定少不了重刷数据。比如一个任务/工作流最终产生一个报表,但是业务发生变更(新增一个字段),或者发现上一个月的数据都有错误,那么就需要重刷过去一个月的数据。 通过重刷数据功能,可以重刷某些任务/工作流的数据(只支持天级别),每个实例都是不同的数据时间。 3.8 失败自动重试实例失败自动重试:在任务管理的高级配置中,可以配置实例失败重试次数和重试间隔,比如重试3次,每次间隔30秒。如果重试3次仍旧失败,该实例状态才会变为失败,并发送报警。子任务失败自动重试:如果是分布式任务(并行计算/内网网格/网格计算),子任务也支持失败自动重试和重试间隔,同样可以通过任务管理的高级配置进行配置。3.9 支持原生Spring之前的老产品Schedulerx1.0(DTS)和spring的结合非常暴力,对bean的命名有强要求,经常遇到注入失败的问题。Schedulerx2.0支持原生spring语法,接入更加的方便。 3.10 报警监控失败报警超时报警报警方式:短信 本文作者:黄晓萌阅读原文 本文为云栖社区原创内容,未经允许不得转载。

April 24, 2019 · 1 min · jiezi

一次开发、多端分发,阿里巴巴发布AliOS车载小程序

4月16日上海国际车展首日,阿里巴巴小程序有了新动态:正在研发基于AliOS的车载小程序。作为阿里巴巴小程序在车载场景的重要延伸,AliOS车载小程序和支付宝、高德等小程序一样,将采用统一的开发框架和开放标准,依托于小程序云的一站式云服务,可进行统一的应用发布、资源管理和数据管理,大幅降低小程序开发者的运营和维护成本。基于算法和庞大的生态服务体系,AliOS车载小程序自带场景智能感知的基因。得到车主授权后,车载小程序可以围绕行车场景,实现上车前、行车中、下车后自然串联的智能化场景服务。譬如,你可以在车上通过触控、语音、手势等多模态交互方式,咨询附近的推荐餐厅,小程序会基于你的喜好作出推荐,还可以预约排号;到达餐厅附近,系统会自动唤醒小程序,为你找到停车场;下车后,车载小程序会无缝连接到手机小程序端,你可以在手机上查看预约餐厅的楼层位置、出示预约信息等。此前在2019阿里云峰会·北京上,阿里云、支付宝、淘宝、钉钉、高德等联合发布“阿里巴巴小程序繁星计划”,用20亿元补贴扶持200万以上小程序开发者、100万以上商家。此次AliOS车载小程序的发布,将为阿里巴巴小程序再增车载新场景。打造一个以用户为中心的小程序生态服务体系,实现支付宝、淘宝、高德、UC、AliOS等多端服务场景的打通,一次开发、多端分发,为用户提供从出行到生活的一站式服务。此次车展上,AliOS还展出AI HUD、AI驾驶舱等最新技术。作为国内最大的互联网汽车操作系统,AliOS正在构建一个可持续发展的整合平台,通过对新交互、新科技的探索,创造具有便捷、愉悦、个性的互联网汽车产品。更多关于阿里巴巴小程序繁星计划的内容请访问专题页:https://yq.aliyun.com/activity/820本文作者:阿里云头条阅读原文本文为云栖社区原创内容,未经允许不得转载。

April 16, 2019 · 1 min · jiezi

浏览器会内置类react框架

DOM操作从Prototype.js到风靡全球的jQuery.js,都是在解决浏览器间DOM操作的差异化问题,同时也提供了简洁友好的API,但是随着标准的不断的推进,现在浏览器间的差异化可以说已经没有了,像Github就宣布了弃用jQuery.js,直接使用浏览器提供的DOM操作更新界面。尽管浏览器提供的DOM操作API有时候看上去比较啰嗦,但是只要所有浏览器实现一致,前端就不需再使用一层封装来间接操作DOM,只需要学习标准化的API即可网络请求从IE的ActiveXObject到XMLHttpRequest Level1再到XMLHttpRequest Level2,然后fetch出现一统网络请求。在我们平常的开发中,可以直接使用fetch进行请求,无需再引入其它的网络请求库。不过目前fetch提供的API不够丰富,可能在使用时还要简单封装模块化从最早的对象模块命名空间,到amd,cmd等模块化工具require.js,sea.js等,再到es module,目前chrome中已经可以直接使用,并且动态的import也已经支持,从此可以告别那些第三方的模块加载器,学习并使用标准的es module即可功能点比如以往我们要实现平滑滚动,我们要用setTimeout或setInterval先实现一下基础的动画引擎,然后再实现一下相应的tween缓动算法,然后再应用到我们的滚动上。现在浏览器已经支持通过css给要滚动的节点添加scroll-behavior: smooth,然后再操作相应的scrollTop或scrollLeft即可实现相应的平滑滚动,省去了原来大量的代码或引用第三方类库的事情再比如某个节点滚动到或即将滚动到可视区域做一些事情(像瀑布流等),以往像平滑滚动一样,我们要监听滚动事件,我们要计算节点的位置信息等一大堆事情要做,现在有IntersectionObserver,我们完成类似的功能只需要几行代码即可对于图片多的网站,前端经常使用的图片懒加载,现在也有了原生支持,给图片加上<img loading=“lazy”/>即可,不但省去了大量的javascript,也提升了易用性web components通过前面的一些基础点,我们可以看到浏览器越来越多的把一些常用功能内置进去,可以预见未来也会更多的把常用功能内置进去。内置的功能不但方便开发人员,同时在内存管理上,性能上,资源使用上都要远远优于javascript的实现长远看,现在前端开发的模式:界面管理+数据管理=应用。界面管理也很有可能被内置到浏览器里,简单理解就是把页面组件化的功能内置进去,比如内置一个react。开发人员只需要管理好自己的业务数据即可。目前这个内置的界面管理浏览器提供的是web components,但是它在使用起来仍然不够方便,不过随着时间的发展,也许一年半载之后浏览器发力web compoents,把它打造的更顺手易用也说不定。浏览器的未来从前面的一些例子我们可以看到,浏览器也在不断的吸收好的开发思路和方式,同时开放更基础,更易用的API给到开发人员,这是一个互相辅助的过程。一但某些库或框架成为事实上的标准,那为什么不推进它把它写进标准,然后让浏览器实现呢?比如jQuery.js就是成功的案例,比如图片懒加载也是很好的说明,也许浏览器会很快内置lodash呢?如果浏览器没能发展好web components,如果react发展成熟稳定,浏览器或许就直接内置了,让我们只关注业务即可。如果你有想法,来留言讨论一下吧 ^_^

April 10, 2019 · 1 min · jiezi

有赞美业店铺装修前端解决方案

一、背景介绍做过电商项目的同学都知道,店铺装修是电商系统必备的一个功能,在某些场景下,可能是广告页制作、活动页制作、微页面制作,但基本功能都是类似的。所谓店铺装修,就是用户可以在 PC 端进行移动页面的制作,只需要通过简单的拖拽就可以实现页面的编辑,属于用户高度自定义的功能。最终编辑的结果,可以在 H5、小程序进行展示推广。有赞美业是一套美业行业的 SaaS 系统,为美业行业提供信息化和互联网化解决方案。有赞美业本身提供了店铺装修的功能,方便用户自定义网店展示内容,下面是有赞美业店铺装修功能的截图:上面的图片是 PC 端的界面,下面两张图分别是 H5 和小程序的最终展示效果。可以简单地看到,PC 端主要做页面的编辑和预览功能,包括了丰富的业务组件和详细的自定义选项;H5 和小程序则承载了最终的展示功能。再看看有赞美业当前的技术基本面:目前我们的 PC 端是基于 React 的技术栈,H5 端是基于 Vue 的技术栈,小程序是微信原生开发模式。在这个基础上,如果要做技术设计,我们可以从以下几个角度考虑:三端的视图层都是数据驱动类型,如何管理各端的数据流程?三个端三种不同技术栈,业务中却存在相同的内容,是否存在代码复用的可能?PC 最终生成的数据,需要与 H5、小程序共享,三端共用一套数据,应该通过什么形式来做三端数据的规范管理?在扩展性上,怎么低成本地支持后续更多组件的业务加入?二、方案设计所以我们针对有赞美业的技术基本面,设计了一个方案来解决以上几个问题。首先摆出一张架构图:2.1 数据驱动首先关注 CustomPage 组件,这是整个店铺装修的总控制台,内部维护三个主要组件 PageLeft、 PageView 和 PageRight,分别对应上面提到的 PC 端3个模块。为了使数据共享,CustomPage 通过 React context 维护了一个”作用域“,提供了内部三个组件共享的“数据源”。 PageLeft 、 PageRight 分别是左侧组件和右侧编辑组件,共享 context.page 数据,数据变更则通过 context.pageChange 传递。整个过程大致用代码表示如下:// CustomerPageclass CustomerPage extends React.Component { static childContextTypes = { page: PropTypes.object.isRequired, pageChange: PropTypes.func.isRequired, activeIndex: PropTypes.number.isRequired, }; getChildContext() { const { pageInfo, pageLayout } = this.state; return { page: { pageInfo, pageLayout }, pageChange: this.pageChange || (() => void 0), activeIndex: pageLayout.findIndex(block => block.active), }; } render() { return ( <div> <PageLeft /> <PageView /> <PageRight /> </div> ); }}// PageLeftclass PageLeft extends Component { static contextTypes = { page: PropTypes.object.isRequired, pageChange: PropTypes.func.isRequired, activeIndex: PropTypes.number.isRequired, }; render() {…}}// PageRightclass PageRight extends Component { static contextTypes = { page: PropTypes.object.isRequired, pageChange: PropTypes.func.isRequired, activeIndex: PropTypes.number.isRequired, }; render() {…}}至于 H5 端,可以利用 Vue 的动态组件完成业务组件的动态化,这种异步组件的方式提供了极大的灵活性,非常适合店铺装修的场景。<div v-for=“item in components”> <component :is=“item.component” :options=“convertOptions(item.options)” :isEdit=“true”> </component></div>小程序因为没有动态组件的概念,所以只能通过 if else 的面条代码来实现这个功能。更深入的考虑复用的话,目前社区有开源的工具实现 Vue 和小程序之间的转换,可能可以帮助我们做的更多,但这里就不展开讨论了。PC 编辑生成数据,最终会与 H5、小程序共享,所以协商好数据格式和字段含义很重要。为了解决这个问题,我们抽取了一个npm包,专门管理3端数据统一的问题。这个包描述了每个组件的字段格式和含义,各端在实现中,只需要根据字段描述进行对应的样式开发就可以了,这样也就解决了我们说的扩展性的问题。后续如果需要增加新的业务组件,只需要协商好并升级新的npm包,就能做到3端的数据统一。/** * 显示位置 */export const position = { LEFT: 0, CENTER: 1, RIGHT: 2,};export const positionMap = [{ value: position.LEFT, name: ‘居左’,}, { value: position.CENTER, name: ‘居中’,}, { value: position.RIGHT, name: ‘居右’,}];2.2 跨端复用PageView 是预览组件,是这个设计的核心。按照最直接的思路,我们可能会用 React 把所有业务组件都实现一遍,然后把数据排列展示的逻辑实现一遍;再在 H5 和小程序把所有组件实现一遍,数据排列展示的逻辑也实现一遍。但是考虑到代码复用性,我们是不是可以做一些“偷懒”?如果不考虑小程序的话,我们知道 PC 和 H5 都是基于 dom 的样式实现,逻辑也都是 js 代码,两端都实现一遍的话肯定做了很多重复的工作。所以为了达到样式和逻辑复用的能力,我们想了一个方法,就是通过 iframe 嵌套 H5 的页面,通过 postmessage 来做数据交互,这样就实现了用 H5 来充当预览组件,那么 PC 和 H5 的代码就只有一套了。按照这个实现思路,PageView 组件可以实现成下面这样:class PageView extends Component { render() { const { page = {} } = this.props; const { pageInfo = {}, pageLayout = [] } = page; const { loading } = this.state; return ( <div className={style}> <iframe title={pageInfo.title} src={this.previewUrl} frameBorder=“0” allowFullScreen=“true” width=“100%” height={601} ref={(elem) => { this.iframeElem = elem; }} /> </div>); }}PageView 代码很简单,就是内嵌 iframe,其余的工作都交给 H5。H5 将拿到的数据,按照规范转换成对应的组件数组展示:<template> <div> <component v-for="(item, index) in components" :is=“item.component” :options=“item.options” :isEdit=“false”> </component> </div></template><script> computed: { components() { return mapToComponents(this.list); }, },</script>因为有了 iframe ,还需要利用 postmessage 进行跨源通信,为了方便使用,我们做了一层封装(代码参考自有赞餐饮):export default class Messager { constructor(win, targetOrigin) { this.win = win; this.targetOrigin = targetOrigin; this.actions = {}; window.addEventListener(‘message’, this.handleMessageListener, false); } handleMessageListener = (event) => { // 我们能相信信息的发送者吗? (也许这个发送者和我们最初打开的不是同一个页面). if (event.origin !== this.targetOrigin) { console.warn(${event.origin}不对应源${this.targetOrigin}); return; } if (!event.data || !event.data.type) { return; } const { type } = event.data; if (!this.actions[type]) { console.warn(${type}: missing listener); return; } this.actionstype; }; on = (type, cb) => { this.actions[type] = cb; return this; }; emit = (type, value) => { this.win.postMessage({ type, value, }, this.targetOrigin); return this; }; destroy() { window.removeEventListener(‘message’, this.handleMessageListener); }}在此基础上,业务方就只需要关注消息的处理,例如 H5 组件接收来自 PC 的数据更新可以这样用:this.messager = new Messager(window.parent, ${window.location.protocol}//mei.youzan.com);this.messager.on(‘pageChangeFromReact’, (data) => { …});这样通过两端协商的事件,各自进行业务逻辑处理就可以了。这里有个细节需要处理,因为预览视图高度会动态变化,PC 需要控制外部视图高度,所以也需要有动态获取预览视图高度的机制。// vue scriptupdated() { this.$nextTick(() => { const list = document.querySelectorAll(’.preview .drag-box’); let total = 0; list.forEach((item) => { total += item.clientHeight; }); this.messager.emit(‘vueStyleChange’, { height: total }); }}// react scriptthis.messsager.on(‘vueStyleChange’, (value) => { const { height } = value; height && (this.iframeElem.style.height = ${height}px);});2.3 拖拽实现拖拽功能是通过 HTML5 drag & drop api 实现的,在这次需求中,主要是为了实现拖动过程中组件能够动态排序的效果。这里有几个关键点,实现起来可能会花费一些功夫:向上向下拖动过程中视图自动滚动拖拽结果同步数据变更适当的动画效果目前社区有很多成熟的拖拽相关的库,我们选用了vuedraggable。原因也很简单,一方面是避免重复造轮子,另一方面就是它很好的解决了我们上面提到的几个问题。vuedraggable 封装的很好,使用起来就很简单了,把我们前面提到的动态组件再封装一层 draggable 组件:<draggable v-model=“list” :options=“sortOptions” @start=“onDragStart” @end=“onDragEnd” class=“preview” :class="{dragging: dragging}"> <div> <component v-for="(item, index) in components" :is=“item.component” :options=“item.options” :isEdit=“false”> </component> </div></draggable>const sortOptions = { animation: 150, ghostClass: ‘sortable-ghost’, chosenClass: ‘sortable-chosen’, dragClass: ‘sortable-drag’,};// vue scriptcomputed: { list: { get() { return get(this.designData, ‘pageLayout’) || []; }, set(value) { this.designData.pageLayout = value; this.notifyReact(); }, }, components() { return mapToComponents(this.list); },},三、总结到这里,所有设计都完成了。总结一下就是:PC 端组件间主要通过 React context 来做数据的共享;H5 和 小程序则是通过数据映射对应的组件数组来实现展示;核心要点则是通过 iframe 来达到样式逻辑的复用;另外可以通过第三方npm包来做数据规范的统一。当然除了基本架构以外,还会有很多技术细节需要处理,比如需要保证预览组件不可点击等,这些则需要在实际开发中具体处理。 ...

April 8, 2019 · 3 min · jiezi

小程序框架选择

一、小程序开发框架包括哪些wepy、mpvue、taro是目前最火的三个框架,分开介绍一下wepy:腾讯团队开源的一款类vue语法规范的小程序框架,借鉴了Vue的语法风格和功能特性,支持了Vue的诸多特征,比如父子组件、组件之间的通信、computed属性计算、wathcer监听器、props传值、slot槽分发,还有很多高级的特征支持:Mixin混合、拦截器等;WePY发布的第一个版本是2016年12月份,也就是小程序刚刚推出的时候,到目前为止,WePY已经发布了52个版本, 最新版本为1.7.2;mpvue:美团团队开源的一款使用 Vue.js 开发微信小程序的前端框架。使用此框架,开发者将得到完整的 Vue.js 开发体验,同时为 H5 和小程序提供了代码复用的能力。mpvue在发布后的几天间获得2.7k的star,上升速度飞起,截至目前为止已经有16.6k的star;taro京东凹凸实验室开源的一款使用 React.js 开发微信小程序的前端框架。它采用与 React 一致的组件化思想,组件生命周期与 React 保持一致,同时支持使用 JSX 语法,让代码具有更丰富的表现力,使用 Taro 进行开发可以获得和 React 一致的开发体验。,同时因为使用了react的原因所以除了能编译h5, 小程序外还可以编译为ReactNative;三框架对比表格三个框架目前都是相对生态完善且成熟的框架。各位开发者可根据自己的业务需要选择合适的框架使用。

March 31, 2019 · 1 min · jiezi

详解服务器端的项目框架

导读我一直相信这句话,他山之石可以攻玉。在自己能力不够时,多学习别人的东西。这样,对自己只有好处,没有坏处。因而,经过将近一年的工作,研读了公司所使用的框架。我本想往架构师的方向靠近,但,自己的能力可能还不够,因而,不断地给自己充电。公司的项目是前后端分离的,前端使用HTML5,css3、jquery、vue.js、bootstrap等,以SVN做代码托管。后端采用maven构建项目,以git lab做代码托管。肯定有人会问,这两个都是版本库,但它们有什么区别?如果想要了解的话,可以参考该文档:Svn与Git的区别。现在几乎所有的公司都采用maven构建项目,很少会采用导入jar包的方式依赖第三方框架。maven介绍maven构建的项目有很多好处,首先其可以统一管理jar包,也就是说,我们在项目中不用手动导入jar包,我们只要添加依赖即可,如代码所示:<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${jdbc.version}</version></dependency>添加依赖之后,maven就会导入该jar包,导入jar包的顺序为:首先查找本地仓库,如果本地仓库没有,进入下面步骤。maven settings profile中的repository;pom.xml中profile中定义的repository。profile激活配置文件,比如正式环境的配置文件prd.properties和开发环境的Dev.properties文件。这也是打包的依据,是打开发环境的包,还是打正式环境的包,如图所示:pom.xml中的repositorys(定义多个repository,按定义顺序找);如果经过上面的步骤,没有找到相应的jar包,最后到我们的镜像(mirror)中查找。如果mirror中存在该jar包,从mirror中拷贝下来,存储到本地仓库中,进入到最初一步。如果mirror中也没有,maven就会报相应的错误。maven报出相应的错误时,也许,是我们本地没有该jar包,远程仓库也没有该jar包,我们可以参考这篇博客:在maven的pom.xml中添加本地jar包。它会教你如何创建本地仓库,并导入创建好的本地仓库。【备注】这篇博客以架构师的角度来讲解maven,所以,不具体讲解maven各个标签的含义。如果你想了解pom的各个标签的含义,可以参考这篇文档:pom.xml详解,或者,参考这篇教程:maven教程|菜鸟教程上面解说只是配置jar文件,但是,maven的功能远不止这些,从我们创建maven项目时,就已经进入到maven的开发环境中。maven项目有人和我说过,学一项知识,什么方式最快?那就是通过做项目。你在做项目的过程中,肯定会遇到很多问题,而你不得不去查找资料,从根源上认识到这个问题。因而,我以公司所做的某个项目为例,来讲解maven的依赖、继承、聚合等关系。我们所说的maven中的关系,其实就是pom的关系,即项目对象模型(Project Object Model)的简称。maven聚合首先,我们在创建cloudCodeSale项目时,就已经创建了父pom文件,如图所示:上图就是我们的父pom文件,你很清楚的看到gav坐标。同时,你从这张图上,也能得到其他信息,其打包方式是 pom,其还关联其他module,module名称和左边的列表名称一样。这就是我们所说的maven的聚合。父类同时聚合其子类。聚合的条件有两个:修改被聚合项目的pom.xml中的packaging元素的值为pom在被聚合项目的pom.xml中的modules元素下指定它的子模块项目既然所有的子模块的pom都继承父pom,为什么父pom要聚合子模块的pom文件?这个问题很好。因为对于聚合而言,当我们在被聚合的项目上使用Maven命令时,实际上这些命令都会在它的子模块项目上使用。这就是Maven中聚合的一个非常重要的作用。在实际开发的过程中,我们只需要打包(mvn install)父pom文件。我们在父pom上使用mvn celan、mvn compile和mvn package,其会自动对子模块:platform-core、platform-core-controller、portal/member-portal、portal/platform-portal、platform-cms、platform-cms-controller、platform-custom执行mvn celan、mvn compile和mvn package。没必要一个一个地打包,这样极容易出现错误,如图所示:maven继承机制如果很多模块都需要相同的jar包,我们可以单独写在一个pom中,其他模块使用该模块的公共部分,这就是我们常说的父类。不论是java语言,还是c++语言,或者现在的pom,其都体现这个思想。我们在上文也提到了子模块,现在模块platform-core讲解。继承父类的结构一般是这样的:<parent> <groupId>parent.groupId</groupId> <artifactId>parent.artifactId</artifactId> <version>parent.version</version> <relativePath>../pom.xml</relativePath> </parent> relativePath是父pom.xml文件相对于子pom.xml文件的位置,针对被继承的父pom与继承pom的目录结构是不是父子关系。如果是父子关系,则不用该标签;如果不是,那么就用该标签。因为在当前项目中,platform-core模块的目录的pom在父目录的pom中,其和父pom的目录结构是父子关系,因而可以省略relativePath该标签,如图所示:parent标签中的groupId、artifactId、version要和父pom中的标签中一致。maven的依赖关系正如我们所知道的,maven构建项目,不仅是因为其能够管理jar包,其还使模块化开发更简单了。因而,我们在开发的过程中,一般都分模块化开发。模块与模块之间的信息是不通的,但是,我们想要模块间的之间能够通信,这时,我们就想到了java中的依赖关系。比如,我们有一个模块,这个模块封装好了微信支付、支付宝支付、处理json格式、操作文件、ResultUtil、lambdaUtil、commonUtil等工具类,还有附件、头像、用户等实体类。这些工具类在任何项目中不会轻易改变,如果为了满足某些需求而不得不得修改,需要得到架构师的同意。因而,我们可以把它拿出来,单独定义为一个模块,也就是platform-core模块。但是,我们还有一个模块,在这个模块中,根据不同的项目,其定义不同的实体类、dao层类、事务层类、枚举类、接收前端传来参数封装成的query类、从数据库中取出的数据封装成的data类,到事务层可能会调用模块plateform-core中的方法,比如调用第三方系统接口的HTTPClientUtil.doPost(String url, Map<String, String> param),判断处理lambda表达式的LambdaUtil.ifNotBlankThen(String value, Consumer<String> function) ,等等。这个自定义类的模块,我们可定义为plateform-custom。plateform-custom需要用到platform-core中的方法,因而,这时,我们就需要考虑依赖关系,怎么添加对platform-core的依赖呢?如代码所示:<dependency> <groupId>com.zfounder.platform</groupId> <artifactId>platform-core</artifactId></dependency>我们这边是前后台分离的,后台用来录入数据,前台用来展示数据,因而,我们有一个portal目录,该目录下有两个子模块。一个是member-portal模块,一个是platform-portal模块,前者接收前台的接口参数,后者接收后台的接口参数。但不论哪个模块,都需要依赖plateform-custom中的事务层方法,同时,我们传的参数,可能信息分发的platform-cms-controller中的接口,也可能是核心接口platform-core-controller中的接口。因而,我们这里以member-portal模块来举例,依赖其他模块的代码如下:<dependencies> <dependency> <groupId>com.zfounder.platform</groupId> <artifactId>platform-core-controller</artifactId> </dependency> <dependency> <groupId>com.zfounder.platform</groupId> <artifactId>platform-cms-controller</artifactId> </dependency> <dependency> <groupId>com.zfounder.platform</groupId> <artifactId>platform-custom</artifactId> <version>1.0-SNAPSHOT</version> </dependency></dependencies>这些模块你会在上面的图片找得到。同时,我们来看member-portal的pom文件继承的父pom是怎么写的:补充上面的继承关系。这里面用到了<relativePath>../../pom.xml</relativePath>你会奇怪的是,为什么这里面用到了呢?其和父pom不是父子关系,而是孙子关系。这里使用到了两次点点,这是什么意思呢? ..表示上级目录。举个例子说明:比如,在我的服务器上的www目录中,有三个文件,分别是rsa_private_key.pem, rsa_private_key_pkcs8.pem, rsa_public_key.pem,还有一个testDir目录,testDir目录中还有目录testDir,现在我们通过cd ../testDir/testDir进入到子目录中,现在,我们想返回到www的根目录中,并查看rsa_public_key.pem文件的内容,因而,我们可以用cat ../../rsa_public_key.pem命令,其首先返回两级目录,然后找到rsa_public_key.pem文件并打开该文件。“被继承的父pom与继承pom的目录结构是不是父子关系”也不是绝对的,主要是选择继承者的pom中的子目录和父目录之间的关系,其中间隔了几层目录。maven激活文件激活文件在上文也提到了,我们为什么需要激活文件?如下面的两个配置文件,一个是测试环境的配置文件,名为platform-dev.properties,一个是正式环境的配置文件,名为platform-prd.properties。两个配置文件中都存在与数据库的连接,但是呢,数据库的ip地址是不一样的。如一下的代码所示:正式服的platform-prd.properties配置文件jdbc.url=jdbc:mysql://localhost/prd_databasejdbc.username=prd_usernamejdbc.password=prd_passwordjdbc.validationQuery=select 1 from dualjdbc.removeAbandonedTimeout=180jdbc.initialSize=10jdbc.minIdle=30jdbc.maxActive=100jdbc.maxWait=30000。。。测试服的platform-dev.properties配置文件jdbc.url=jdbc:mysql://intranet_ip/dev_databasejdbc.username=dev_usernamejdbc.password=dev_passwordjdbc.validationQuery=select 1 from dualjdbc.removeAbandonedTimeout=180jdbc.initialSize=10jdbc.minIdle=30jdbc.maxActive=100jdbc.maxWait=30000。。。我们的在配置文件中配置好了数据项,但是呢,我们怎么切换不同的配置文件呢?换句话说,我们怎么想要打正式服的包放到正式服上,怎么选择platform-prd.properties的配置文件呢?反之,怎么选择platform-dev.properties配置文件?这时,我们就用到了maven当中的profile标签,如下代码所示: <profiles> <profile> <id>dev</id> <activation> <activeByDefault>true</activeByDefault> </activation> <build> <filters> <filter>../../platform-dev.properties</filter> </filters> </build> </profile> <profile> <id>prd</id> <build> <filters> <filter>../../platform-prd.properties</filter> </filters> </build> </profile></profiles>这些配置文件时写在member-portal、platform-portal、plateform-core和plateform-cms、plateform-customer模块的pom中的。但是,plateform-core和plateform-cms的配置中的filter和上面连个略有差异,其filter是这样的 <filter>../platform-dev.properties</filter>和 <filter>../platform-prd.properties</filter>,这就涉及到目录点的问题。maven依赖第三方包maven项目除了依赖本项目的,其还会依赖第三方包,比如自动生成set和get方法的lombok包,处理json格式的阿里巴巴下的fastjson包等等,我们也可以使用这种格式的依赖:<!–mysql jdbc驱动包 开始–><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${jdbc.version}</version></dependency><!–mysql jdbc驱动包 结束–>开发常用的jar包lombok<!– lombok驱动包 开始–><dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.16.10</version></dependency><!– lombok驱动包 开始–>我们在没有使用lombok之前,经常手动创建javabean的set个get方法,使用这个框架之后,其以注解的方式,可以自动生成set和get方法。同时,其强大的功能远不止这些,也可以生成无参构造器、全参构造器,指定参数构造器、重写toString方法、重写Hashcode、equals方法等等。如代码所示:/** * Created By zby on 17:37 2019/1/30 /@AllArgsConstructor@NoArgsConstructor@Data@ToString@EqualsAndHashCodepublic class Address { /* * 收货人 / private String consignee; /* * 手机号码 / private String phone; /* * 所在地区 / private String area; /* * 详细地址 / private String detail; /* * 标签 / private AddressTagEnum addressTag;}想要更深层次的了解这个框架,可以参考这个博客:Lombok使用详解fastjson<!– fastjson驱动包 开始–><dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.28</version></dependency><!– fastjson驱动包 结束–>fastjson是阿里巴巴开源的框架,其用来处理服务器端的json格式的数据。比如,我们需要将服务端的对象以json(JavaScript object Notation,js对象标记)格式传输到前端,但是,如果自己手动创建的话,势必会非常的麻烦,于是,我们借助这个框架,帮助我们生成json格式的对象。同时,如果我们要调用第三方接口,比如调用连连绑定银行卡的接口,其返回给我们的也是json对象的数据。但是,我们需要将其转化为我们定义的对象,调用其save方法,保存到数据库中,对账所用。对于,将对象转化为json格式的对象,如代码所示:@Testpublic void test() {// 地址 Address address = new Address(); address.setAddressTag(AddressTagEnum.ADDRESS_TAG_COMPANY); address.setArea(“杭州市….”); address.setConsignee(“zby”);// 用户 User user = new User(); user.setHobby(HobbyEnum.HOBBY_DANCING); user.setGender(“男”); user.setUserName(“蒋三”);// 订单 OrderSnapshot orderSnapshot = new OrderSnapshot(); orderSnapshot.setAddress(address); orderSnapshot.setId(1L); orderSnapshot.setName(“复读机”); orderSnapshot.setOrderNo(Long.valueOf(System.currentTimeMillis()).toString() + “1L”); orderSnapshot.setUser(user); System.out.println(JSON.toJSON(orderSnapshot));}其输出结果如图所示:但是,类似于解析json格式的数据,不只有fastjson框,还有org.json框架、Jackson框架。但经过有人验证呢,还是fastjson的效率更高一些。可以参考这篇博客:Gson、FastJson、org.JSON到底哪一个效率更高,速度更快org.json也是通过maven配置的,如代码所示:<!– json驱动包 开始–><dependency> <groupId>org.json</groupId> <artifactId>json</artifactId> <version>20140107</version></dependency><!– json驱动包 开始–>如果想要深层次了解org.json,可以参考这篇博客:Java使用org.json.jar构造和解析Json数据想要更深层次的了解fastjson,可以参考这篇博客:Fastjson 简明教程spring相关配置如果从事java-web开发,一般会用到spring框架,这方面的教程太多了,笔者就不在这介绍,但我们会用到spring的这些框架:<!–spring 相关配置开始–><dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId></dependency><dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId></dependency><dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId></dependency><dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId></dependency><dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId></dependency>spring会集合很多框架,比如具有拦截效果的shiro框架,持久层的hibernate和mybatis框架等等。spring通过配置文件通过注解或者配置文件的方式,实现依赖注入(dependency injection)和控制反转(inversion of control)。通过@controller遍历相应的接口,实现前后端的接口对接。spring可以实现面向切面编程,实现某个业务点的单一执行。比如,专门处理事务的业务点。spring并不难,很快就能掌握到其精髓。如果想深入了解,可以参考这篇教程:Spring教程hibernate框架hibernate框架就类似于mybatis框架,其专门处理持久层的技术。我们将瞬时状态的对象存储到数据库中,变成持久状态的对象。我们也可以从数据库中取数据,以瞬时态的对象返回到前端。这就是一存一取的框架。其可以使用注解的方式创建数据表,也可以通过配置文件创建瞬时态的对象。但就目前为止,在很多情况下,我们都是通过注解的方式,实现数据表的创建。导入hibernate相关的框架,如下所示:<!–hibernate相关配置 开始–><!–hibernateh核心框架–><dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId></dependency><!–hibernateh验证器–><dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-validator</artifactId></dependency><!–hibernateh缓存技术–><dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-ehcache</artifactId></dependency> <!–Java Persistence API ORM映射元数据 查询语言–><dependency> <groupId>org.hibernate.java-persistence</groupId> <artifactId>jpa-api</artifactId></dependency><!–hibernate相关配置 结束–>hibernate和mybatis具有同样的功能,如果想要了解mybatis,可以参考这篇教程:mybatis教程想要深入理解hibernate,可参考这篇教程:hibernate教程_w3cschooljbdc驱动包我们上面说了hibernate框架,但前提是,我们需要导入jdbc的框架包,如代码所示: <!– 数据库驱动包相关 开始–><dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId></dependency><!– 数据库驱动包相关 结束–>这个驱动包主要处理java与数据库的连接,实现数据的增、删、改、查。我们没有用到这个包,但是hibernate用到了这个包,因而,我们需要导入这个包,以免数据库报错。alibaba的Druid包这个包有什么用吗?我们既然通过hibernate实现与数据库的交互,那么就需要在初始化时创建连接池。现在连接池有很多种,我们为什么选择了它Druid。<!– Druid驱动包相关 开始–><dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId></dependency><!– Druid驱动包相关 结束–>它是目前最好的数据库连接池,在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource。Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。Druid是阿里巴巴开发的号称为监控而生的数据库连接池!时代在变化,我们也应该应世而生,与时俱进,才不会和时代脱轨。我们使用Druid来实现数据的配置,如代码所示: <bean id=“dataSource” class=“com.alibaba.druid.pool.DruidDataSource” init-method=“init” destroy-method=“close”> <property name=“driverClassName” value=“com.mysql.jdbc.Driver”/> <property name=“url” value="${jdbc.url}"/> <property name=“username” value="${jdbc.username}"/> <property name=“password” value="${jdbc.password}"/> <property name=“maxActive” value="${jdbc.maxActive}"/> <property name=“initialSize” value="${jdbc.initialSize}"/> <property name=“removeAbandoned” value=“true”/> <property name=“removeAbandonedTimeout” value="${jdbc.removeAbandonedTimeout}"/> <property name=“testOnBorrow” value=“true”/> <property name=“minIdle” value="${jdbc.minIdle}"/> <property name=“maxWait” value="${jdbc.maxWait}"/> <property name=“validationQuery” value="${jdbc.validationQuery}"/> <property name=“connectionProperties” value=“clientEncoding=UTF-8”/></bean>你们可以看到,value值是形参,而不是具体的值。因为我们根据不同的打包方式,其传入形参对应的实参不同。这也就是我们上文提到的,platform-dev.properties和platform-prd.properties配置文件,以及maven配置的激活文件。如果想要深入了解阿里巴巴的Druid框架,可以参考这篇博客:DRUID连接池的实用 配置详解阿里云短信短信业务一般固定不变,由架构师封装好,其他人直接调用即可,因而,该框架可以写进plateform-core模块中,其配置的代码如下所示:<!– 阿里短息驱动包配置 开始 –><dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-dysmsapi</artifactId></dependency><dependency> <groupId>com.aliyun</groupId> <artifactId>aliyun-java-sdk-core</artifactId></dependency><!– 阿里短息驱动包配置 结束 –>日志相关配置我们在开发的过程中,经常会使用到日志,来记录相应的错误、警告、信息。比如,我在使用连连支付做提现业务时,提现成功后其会回调我们的接口,从而显示在服务端的Tomcat页面中。再比如,我们在登录时,其会在Tomcat中显示相关信息,如图所示:我们都知道日志分为几种级别。这里就不再赘述了。日志分为好多种,我们推荐使用slf4j+logback模式。因为logback自身实现了slf4j的接口,无须额外引入适配器,另外logback是log4j的升级版,具备比log4j更多的优点,我们可以通过如下配置进行集成:<!– 日志驱动包配置 开始 –><dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.21</version></dependency><dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.1.7</version></dependency><dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.1.7</version></dependency><!– 日志驱动包配置 结束 –>我们这时就用到了plateform-prd.properties文件和plateform-dev.properties文件,因为,我们需要在这里面配置日志的输出位置。然后,在logback.xml中以参数的形式,调用文件中的输出位置,如图所示:如果想要了解更多的配置文件信息,请参考这篇博客:使用 logback + slf4j 进行日志记录commons家族我们在开发的过程中,经常用到Commons家族的驱动包,比如文件操作的io包,MD5加密和解密用的codec包。当然,我们也会用到java自带的local_policy驱动包,但有时需要替换替换该驱动包,否则,就会报出Illegal Key Size的错误。文件上传下载的fileupload驱动包,操作字符串类型的lang3包,配置的驱动包如下所示:<!–comon包相关配置–><commons-io.version>2.4</commons-io.version><commons-lang3.version>3.4</commons-lang3.version><commons-codec.version>1.10</commons-codec.version><commons-fileupload.version>1.3.1</commons-fileupload.version><!– apache common 开始 –><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>${commons-lang3.version}</version></dependency><dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>${commons-io.version}</version></dependency><dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>${commons-codec.version}</version></dependency><dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>${commons-fileupload.version}</version></dependency><!– apache common 结束 –>lang3包我们可以用其分割字符串,判断字符串是否为空格,判断字符串是否为空等等。如以下代码所示:public static void main(String[] args) { String keyword = “1-1-2”; if (StringUtils.isNotBlank(keyword)) { System.out.println(“keyword = " + keyword); } String[] keys = StringUtils.split(keyword, “-”); for (String key : keys) { System.out.println(“key=” + key); }}我们有时还会用其操作时间类,比如格式化时间等等,入一下代码:@Testpublic void testDate(){ String date1= FastDateFormat.getInstance(“yyyy-MM-dd”).format(System.currentTimeMillis()); System.out.println(“System.currentTimeMillis:"+date1); String date2= FastDateFormat.getInstance(“yyyy-MM-dd”).format(new Date()); System.out.println(“new Date:"+date2);}其功能远不止这些,具体可以参考这篇博客:commons-lang3工具包io包见名知意,IO即input和output的简写,即输入流和输出流。因而,我们经常使用到java自带的InputStream或FileInputStream的字节输入流,以及OutputStream或FileOutputStream的输出流。如果更高级的话,那么,就使用到了带有缓存效果的bufferReader输入流和bufferWrite输出流。这里面用到了装饰设计模式。什么是装修设计模式,可以自行学习。上面的操作比较复杂,我们就用到了apache下的io驱动包。这里就当做抛砖引玉了,想要有更深的了解,可以参考这篇博客:io包工具类codec包codec包是Commons家族中的加密和解密用的包,这里不做任何解释,具体可以参考这篇博客:Commons Codec基本使用fileupload包我们如果做java-web开发,经常会有文件上传和文件下载的功能。这时,我们就考虑到了Apache下面的 fileupload包,这可以完成文件的上传和下载。这里的文件不单单是指doc文件,也会指图片和视频文件。具体想要有更多的理解,可以参考这篇文档:commons-fileupload上传下载shiro包我们在web开发时,经常会涉及到权限问题,比如哪些页面不需要登录就能看,而哪些页面只能登录才能看。当用户在打开该页面之前,就进入到相应的过滤器中,来做相关业务的判断。如果通过,就进入到controller层;不通过,则抛出相应的异常给前端。这里就需要相应的权限控制。说到权限控制,我们不得不提到shiro框架。其有三大核心组件Subject, SecurityManager 和 Realms。这个百度百科上也说了,可以查看其解说内容:java安全框架 <!– shiro驱动包 开始 –><dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId></dependency><dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId></dependency><dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId></dependency><dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId></dependency><!–shiro驱动包 结束 –>公司也会做相应的配置,配置如下:<?xml version=“1.0” encoding=“UTF-8”?><beans xmlns:util=“http://www.springframework.org/schema/util" xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://www.springframework.org/schema/beans" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <!– 缓存管理器 –> <bean id=“cacheManager” class=“com..shared.framework.SpringCacheManagerWrapper”> <property name=“cacheManager” ref=“springCacheManager”/> </bean> <bean id=“springCacheManager” class=“org.springframework.cache.ehcache.EhCacheCacheManager”> <property name=“cacheManager” ref=“ehcacheManager”/> </bean> <bean id=“ehcacheManager” class=“org.springframework.cache.ehcache.EhCacheManagerFactoryBean”> <property name=“configLocation” value=“classpath:ehcache.xml”/> </bean> <!– 凭证匹配器 –> <bean id=“credentialsMatcher” class=“com.*.RetryLimitHashedCredentialsMatcher”> <constructor-arg ref=“cacheManager”/> <property name=“hashAlgorithmName” value=“md5”/> <property name=“hashIterations” value=“2”/> <property name=“storedCredentialsHexEncoded” value=“true”/> </bean> <!– Realm实现 –> <bean id=“userRealm” class=“com..shared.web.listener.MemberSecurityRealm”> <!–<property name=“credentialsMatcher” ref=“credentialsMatcher”/>–> <property name=“cachingEnabled” value=“false”/> <!–<property name=“authenticationCachingEnabled” value=“true”/>–> <!–<property name=“authenticationCacheName” value=“authenticationCache”/>–> <!–<property name=“authorizationCachingEnabled” value=“true”/>–> <!–<property name=“authorizationCacheName” value=“authorizationCache”/>–> </bean> <!– 会话ID生成器 –> <!–<bean id=“sessionIdGenerator” class=“org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator”/>–> <!– 会话Cookie模板 –> <bean id=“sessionIdCookie” class=“org.apache.shiro.web.servlet.SimpleCookie”> <constructor-arg value=“platform-portal-sid”/> <property name=“httpOnly” value=“true”/> <property name=“maxAge” value=“7200”/> </bean> <!– 会话管理器 –> <bean id=“sessionManager” class=“org.apache.shiro.web.session.mgt.DefaultWebSessionManager”> <property name=“globalSessionTimeout” value=“43200000”/> <property name=“deleteInvalidSessions” value=“true”/> <property name=“sessionIdCookieEnabled” value=“true”/> <property name=“sessionIdCookie” ref=“sessionIdCookie”/> </bean> <!– 安全管理器 –> <bean id=“securityManager” class=“org.apache.shiro.web.mgt.DefaultWebSecurityManager”> <property name=“realm” ref=“userRealm”/> <property name=“sessionManager” ref=“sessionManager”/> <property name=“cacheManager” ref=“cacheManager”/> </bean> <!– 相当于调用SecurityUtils.setSecurityManager(securityManager) –> <bean class=“org.springframework.beans.factory.config.MethodInvokingFactoryBean”> <property name=“staticMethod” value=“org.apache.shiro.SecurityUtils.setSecurityManager”/> <property name=“arguments” ref=“securityManager”/> </bean> <!– Shiro的Web过滤器 –> <bean id=“shiroFilter” class=“org.apache.shiro.spring.web.ShiroFilterFactoryBean” depends-on=“securityManager,memberShiroFilerChainManager”> <property name=“securityManager” ref=“securityManager”/> </bean> <!– 基于url+角色的身份验证过滤器 –> <bean id=“urlAuthFilter” class=“com.zfounder.platform.core.shared.web.filter.UrlAuthFilter”> <property name=“ignoreCheckUriList”> <list> <value>//common/enums/</value> <value>//security/</value> <value>//common/dd/</value> <value>//pictures/</value> <value>//common/sms/</value> <value>//wx/</value> </list> </property> </bean> <bean id=“memberFilterChainManager” class=“com.zfounder.platform.core.shared.web.listener.CustomDefaultFilterChainManager”> <property name=“customFilters”> <util:map> <entry key=“roles” value-ref=“urlAuthFilter”/> </util:map> </property> </bean> <bean id=“memberFilterChainResolver” class=“com..shared.web.listener.CustomPathMatchingFilterChainResolver”> <property name=“customDefaultFilterChainManager” ref=“memberFilterChainManager”/> </bean> <bean class=“org.springframework.beans.factory.config.MethodInvokingFactoryBean” depends-on=“shiroFilter”> <property name=“targetObject” ref=“shiroFilter”/> <property name=“targetMethod” value=“setFilterChainResolver”/> <property name=“arguments” ref=“memberFilterChainResolver”/> </bean> <!– Shiro生命周期处理器–> <bean id=“lifecycleBeanPostProcessor” class=“org.apache.shiro.spring.LifecycleBeanPostProcessor”/></beans>想要对其有更深的理解,请参考这篇博客:Shiro讲解工具类<!–汉字转拼音开源工具包–> <dependency> <groupId>com.github.stuxuhai</groupId> <artifactId>jpinyin</artifactId></dependency><!–网络爬虫的驱动包–><dependency> <groupId>org.jsoup</groupId> <artifactId>jsoup</artifactId></dependency><!–验证码生成工具包–><dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId></dependency><!–发送邮件–><dependency> <groupId>javax.mail</groupId> <artifactId>mail</artifactId></dependency>因为篇幅的限制,这里就不再细说了,如果想要更深层次的了解,可以参考以下博客:汉字转拼音开源工具包Jpinyin介绍爬虫+jsoup轻松爬知乎使用kaptcha生成验证码使用javax.mail发送邮件图片验证码的配置文件如下:<?xml version=“1.0” encoding=“UTF-8”?><beans xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance" xmlns=“http://www.springframework.org/schema/beans" xsi:schemaLocation=“http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd" default-lazy-init=“true”> <bean id=“captchaProducer” class=“com.google.code.kaptcha.impl.DefaultKaptcha”> <property name=“config”> <bean class=“com.google.code.kaptcha.util.Config”> <constructor-arg> <props> <prop key=“kaptcha.border”>${kaptcha.border}</prop> <prop key=“kaptcha.border.color”>${kaptcha.border.color}</prop> <prop key=“kaptcha.textproducer.font.color”>${kaptcha.textproducer.font.color}</prop> <prop key=“kaptcha.textproducer.char.space”>${kaptcha.textproducer.char.space}</prop> <prop key=“kaptcha.textproducer.font.size”>${kaptcha.textproducer.font.size}</prop> <prop key=“kaptcha.image.width”>${kaptcha.image.width}</prop> <prop key=“kaptcha.image.height”>${kaptcha.image.height}</prop> <prop key=“kaptcha.textproducer.char.length”>${kaptcha.textproducer.char.length}</prop> <prop key=“kaptcha.textproducer.char.string”>1234567890</prop> <prop key=“kaptcha.textproducer.font.names”>宋体,楷体,微软雅黑</prop> <prop key=“kaptcha.noise.color”>${kaptcha.noise.color}</prop> <prop key=“kaptcha.noise.impl”>com.google.code.kaptcha.impl.NoNoise</prop> <prop key=“kaptcha.background.clear.from”>${kaptcha.background.clear.from}</prop> <prop key=“kaptcha.background.clear.to”>${kaptcha.background.clear.to}</prop> <prop key=“kaptcha.word.impl”>com.google.code.kaptcha.text.impl.DefaultWordRenderer</prop> <prop key=“kaptcha.obscurificator.impl”>com.google.code.kaptcha.impl.ShadowGimpy</prop> </props> </constructor-arg> </bean> </property> </bean></beans>里面的占位符来源于plateform-dev.properties或者plateform-prd.properties,这就是我们maven激活的配置文件的作用。测试依赖包我们在开发完一个功能后,首先会想到测试它走不走得通。我们可能会在main方法中测试,一个项目类中可以写多个main方法。如果每个功能类中都写一个main方法,未免会造成代码的混乱,一点都不美观和儒雅。java为什么一直推崇面向对象,任何在现实中真实的、虚拟的事物,都可以将其封装为为java中的对象类。对象与对象之间以方法作为消息传递机制,以属性作为数据库存储的机制。如果我们在每个功能中都写一个main方法,势必会破坏这种对象的美观性。因而,我们把测试的数据以对象的方式操作,这样,将其封装为一个测试包,比如,在我写的spring框架中,就把测试类单独拿出来,如图所示:<!– 测试依赖包 开始–><!– spring test –><dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version></dependency><!– 路径检索json或设置Json –><dependency> <groupId>com.jayway.jsonpath</groupId> <artifactId>json-path</artifactId> <version>${jsonpath.version}</version> <scope>test</scope></dependency><!– testng –><dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>${testng.version}</version></dependency><!– 单元测试的powermock –><dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-testng</artifactId> <version>${powermock.version}</version></dependency><dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito</artifactId> <version>${powermock.version}</version></dependency><!–测试相关 结束–>以上是几种测试包的依赖,一个是spring的测试包,这里由于篇幅的限制,就不做详细的介绍了,网上有很多这方面的教程,想要深入的了解,可参考这篇博客:Spring-Test(单元测试)我们有时也会用到TestNG框架,它是Java中的一个测试框架,类似于JUnit 和NUnit,功能都差不多,只是功能更加强大,使用也更方便。测试人员一般用TestNG来写自动化测试,开发人员一般用JUnit写单元测试。如果你是测试人员,想对其有更全面的了解,可以参考这篇教程:TestNG教程,或者这篇博客::testNG常用用法总结如果想要更深层次的了解powermock,可以参考这篇博客:PowerMock从入门到放弃再到使用如果想要更深层次的了解JsonPath,可以参考这篇博客:JsonPath教程图片处理我们在开发的过程中,会把图片存放到服务器的某个文件夹下,即某个磁盘上。如果图片过大,会占用服务器的磁盘,因而,我们需要将图片缩略,来减少对内存的占用。这时,我们如果使用java原生的图片缩略图,是非常复杂的,因而,我们可以使用以下框架对图片进行操作。<!–图片处理驱动包 开始–><dependency> <groupId>net.coobird</groupId> <artifactId>thumbnailator</artifactId></dependency><!–图片处理驱动包 结束–>这里不再细说,想要有更多的了解,可以参考这篇博客:Thumbnailator框架的使用Excel操作我们在工作的过程中,经常会将数据导出到Excel表,或将Excel表的数据导入数据库。我们以前使用poi框架,但是,超过一定量的时候,会占用大量的内存,从而降低导入的效率。阿里巴巴现在开放出操作Excel表的easyexcel框架,对百万级的导入影响不是很大。以下是maven配置两个驱动依赖:<!–阿里巴巴的easyexcel驱动包 –><dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>{latestVersion}</version></dependency><!–poi驱动包 –><dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>${poi.version}</version></dependency><dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>${poi-ooxml.version}</version></dependency>这两个就不再细说,如果想要对easyexcel更深的了解,可以参考这篇博客:alibaba/easyexcel 框架使用。如果想要对poi有更深的了解,可以参考这篇博客:Apache POI使用详解guava包我们在开发的过程中,有时会用到guava驱动包。它是为了方便编码,并减少编码错误,用于提供集合,缓存,支持原语句,并发性,常见注解,字符串处理,I/O和验证的实用方法。使用它有以下好处:标准化 - Guava库是由谷歌托管。高效 - 可靠,快速和有效的扩展JAVA标准库优化 -Guava库经过高度的优化。同时,又有增加Java功能和处理能力的函数式编程,提供了需要在应用程序中开发的许多实用程序类的,提供了标准的故障安全验证机制,强调了最佳的做法等等。它的宗旨就是:提高代码质量、简化工作,促使代码更有弹性、更加简洁的工具。我们在项目中的配置包为:<!–guava驱动包 开始–> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId></dependency><!–guava驱动包 结束–>如果想要对其有更深的了解,可以参考这篇教程:guava入门教程freemarker包我们在开发的过程中,也许会用到这个框架。为什么要用到这个框架呢?我们有时需要动态地将xml文件转为doc文件,这个时候,就用到了freemarker包,如图所示:截图不是很全面,你会看到画红框的部分,这是一种占位符的标记,就相当于java中的形参一样。 当用户点击前端的下载按钮时,有些数据是无法直接转换成doc的,因为我们先把数据写进xml中,再将xml转化为doc。具体如何转换的可以参考该博客:Java将xml模板动态填充数据转换为word文档我们可以引用这个包: <!–freemarker驱动包 开始–><dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>${freemarker.version}</version></dependency><!–freemarker驱动包 结束–>由于篇幅限制,想要详细了解,可以参考这篇手册: freemarker在线手册servlet驱动包我记得当时在学java-web开发时,最开始用的就是servlet。接收客户端的输入,并经过一系列DB操作,将数据返回给客户端。但使用纯servlet不利于可视化界面。后来,使用了JSP开发,其是可视化界面。但是,当我们启动Tomcat后,JSP通过JSP引擎还是会转为servlet。从本质上来说,JSP和servlet是服务端语言。我最初用servlet和JSP开发的源码地址:图书馆项目后来,工作了以后。后端就用了springMVC,hibernate框架等,前端使用的是HTML、jQuery等。慢慢地脱离了JSP和servlet。但是,并没与完全与servlet分隔开,我们还时不时会用到servlet的一些类,比如HttpServletRequest,HttpServletResponse等类。既然使用了spring MVC框架,为什么还要用servlet的东西,比如,我们在导入和导出时,一个是接收前端导入的请求,一个是响应前端导出的请求。响应前端导出的代码,这里就用到了响应private static void downloadExcel(HttpServletResponse response, File newFile, String fileName) throws IOException { InputStream fis = new BufferedInputStream(new FileInputStream( newFile)); String substring = fileName.substring(fileName.indexOf(”/”) + 1); byte[] buffer = new byte[fis.available()]; fis.read(buffer); fis.close(); response.reset(); response.setContentType(“text/html;charset=UTF-8”); OutputStream toClient = new BufferedOutputStream( response.getOutputStream()); response.setContentType(“application/x-msdownload”); String newName = URLEncoder.encode( substring + System.currentTimeMillis() + “.xlsx”, “UTF-8”); response.addHeader(“Content-Disposition”, “attachment;filename="” + newName + “"”); response.addHeader(“Content-Length”, "” + newFile.length()); toClient.write(buffer); toClient.flush();}接收前端导入的请求 public static LinkedHashMap<String, List<JSONObject>> importMultiSheetExcel(HttpServletRequest request, LinkedHashMap<Integer, Integer> sheetDataStartRowMap, LinkedHashMap<Integer, String> sheetDataEndColMap) { LinkedHashMap<String, List<JSONObject>> resMap = new LinkedHashMap<>(); try { MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request; ifNullThrow(multipartRequest, ResultCodeEnum.ILLEGAL_PARAM); MultipartFile file = multipartRequest.getFile(“file”); Workbook work = getWorkbook(file.getInputStream(), file.getOriginalFilename()); ifNullThrow(work, ResultCodeEnum.ILLEGAL_PARAM); 。。。}虽然我们现在使用了spring MVC,还是用到了servlet,而且shiro里面要使用到,以下是代码的配置:<!–servlet 开始–><!–shiro里面要使用到–><dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>${servlet.version}</version></dependency><!–servlet 结束–><dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>${jstl.version}</version></dependency><!–servlet 结束–>如果想要了解servlet的话,可以参考该文档:Java Servlet API中文说明文档Lucene全文检索有时,我们在开发的过程中,需要做全文检索数据,就比如,我在Word文档中,全文检索某个词、某句话等,如图所示:这就是web端的全文检索。但是我做Java,当然,也需要全文检索。因而,我们就想到了Lucene。它是一套用于全文检索和搜寻的开源程式库,由Apache软件基金会支持和提供。提供了一个简单却强大的应用程式接口,能够做全文索引和搜寻。在Java开发环境里,它是一个成熟的免费开源工具。就其本身而言,它是当前以及最近几年最受欢迎的免费Java信息检索程序库。我们在java的maven库中的配置为: <!– lucene 开始 –><dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-core</artifactId> <version>${lucene.version}</version></dependency><dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-highlighter</artifactId> <version>${lucene.version}</version></dependency><dependency> <groupId>org.apache.lucene</groupId> <artifactId>lucene-analyzers-common</artifactId> <version>${lucene.version}</version></dependency><!– lucene 结束 –>想要对其有更深的了解,可以参考这篇笔记:Lucene学习笔记Quartz任务调度我们在开发的过程中,总想着要在某个时间,执行什么样的事情,于是呢,我们就相当了任务调度,比如:每天八点按时起床每年农历什么的生日每个星期都要爬一次山我们就可以用到Quartz这个框架,我们需要做一些配置,如图所示:我们可以在maven中的配置为:<!– quartz驱动包 开始–><dependency> <groupId>org.quartz-scheduler</groupId> <artifactId>quartz</artifactId> <version>${quartz.version}</version></dependency><!– quartz驱动包 结束–>想要对其有根深多的了解,可参考这篇博客:Quartz使用总结zxing二维码我们经常使用到二维码,比如,添加微信好友的二维码,支付二维码、扫一扫二维码等等,那么,这是怎么实现的呢,其实,这有一个工具包,就是zxing工具包。它是谷歌旗下的工具类,我们可以用它来生成我们想要的二维码,但是,我们先要在maven项目中配置它。如代码所示:<!– 二维码驱动包 开始–><dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version>${zxing.version}</version></dependency><dependency> <groupId>com.google.zxing</groupId> <artifactId>javase</artifactId> <version>${zxing.se.version}</version></dependency><!– 二维码驱动包 开始–>想要对其有根深的了解,可以参考这篇博客:zxing实现二维码生成和解析WSDL包这个我也不大懂,也没有操作过,如果想要了解的话,可以参考这篇文档:WebService中的WSDL详细解析我们在maven中的怕配置为:<!– WSDL驱动包 开始–> <dependency> <groupId>wsdl4j</groupId> <artifactId>wsdl4j</artifactId> <version>${wsdl4j.version}</version></dependency><!– WSDL驱动包 结束–>配置文件配置文件来源于框架中的以下文件,如图所示:所有的配置文件都来源于资源包。这里就不再细说。总结要想成为架构师,首先学习别人的东西,他山之石,可以攻玉。 ...

March 30, 2019 · 5 min · jiezi

PHP & MySQL 「数据关联一对一」的最佳实践

前言在开发过程中,通常会遇到很多 一对一 数据的处理情况。而很多时候我们会要取到的是一个列表,然后列表的单条记录的对应另外一张表,来实现业务。比如下面的商品信息 和 商品详情 两个表,这里为了演示只是使用了基础字段,实际开发中可能会复杂的多,下方演示代码中数据库连接使用 PDO 进行处理。表结构goods列类型注释idint(11) 自动增量主键IDtitlevarchar(100)商品名称pricedecimal(10,2)商品价格covervarchar(100)商品封面goods_detail列类型注释idint(11) 自动增量主键IDgoods_idint(11)商品IDcontentvarchar(5000)商品图文介绍初级坦言,无论是在公司,还是在一些开源项目上,我都看到过如下的代码。$query = $db->query(‘select * from goods’);$result = $query->fetchAll();// 方案一foreach($result as $key => $item){ $query = $db->query(‘select * from goods_detail where goods_id=’ . $item[‘id’]); $result[$key][‘goods_detail’] = $query->fetch();}var_dump($result);// 方案二foreach($result as &$item){ $query = $db->query(‘select * from goods_detail where goods_id=’ . $item[‘id’]); $item[‘goods_detail’] = $query->fetch();}unset($item);var_dump($result);// 方案三$result = array_map(function($item){ $query = $db->query(‘select * from goods_detail where goods_id=’ . $item[‘id’]); $item[‘goods_detail’] = $query->fetch(); return $item;},$result);var_dump($result);这是最暴力的方式,也是立杆见影,而且方案一看起来代码貌似还很繁琐,不是吗?如果学过 引用这一节的朋友,应该知道第二种用法,直接用引用去操作源数据,当然最后最好别忘了 unset 掉 $item,除了第二种,我们还可以用第三种方式,使用 array_map,诚然,这和第二种方式没什么区别,但是这其中有着一个非常大的问题:数据库查询的N+1 。从执行中我们就可以看到,除了查询列表的一条 SQL 外,每查询一条记录对应的都需要执行一条 SQL ,导致了额外的查询,想想一下如果查询没有 limit 限制。会是什么样子的情况?进阶看到这里,有人可能会想到了另一种方案来,先查询列表,然后取出列表里面的 goods_id 之后使用 in 查询,然后再循环分配给列表,看代码。$goods_id = array_column($result,‘id’);$goods_id_str = implode(’,’,$goods_id);$query = $db->query(sprintf(‘select * from goods_detail where goods_id in (%s)’,$goods_id_str));$goods_detail_list = $query->fetchAll();foreach($result as &$item){ $item[‘goods_detail’] = array_first($goods_detail_list,function($item1){ return $item[‘id’] == $item1[‘goods_id’]; });}unset($item);var_dump($result);/** * 来自 Laravel /if (!function_exists(‘value’)) { function value($value) { return $value instanceof Closure ? $value() : $value; }}/* * 来自 Laravel /if (!function_exists(‘array_first’)) { /* * @param $array * @param callable|null $callback * @param null $default * @return mixed */ function array_first($array, callable $callback = null, $default = null) { if (is_null($callback)) { if (empty($array)) { return value($default); } foreach ($array as $item) { return $item; } } foreach ($array as $key => $value) { if (call_user_func($callback, $value, $key)) { return $value; } } return value($default); }}在这个代码中,我们完美避开了 N+1 的窘境,使用了in查询,然后遍历数组,再使用 array_first 方法来查找后传递给 goods_detail 索引,虽然这样的效率相比第一次的要高了很多,但是并不完美,接下来来看最后一种方案。关于 array_first 可以看我的另一篇文章 『PHP 多维数组中的 array_find』。最佳实践$goods_detail_list_by_keys = array_column($goods_detail_list,null,‘goods_id’);foreach($result as &$item){ $item[‘goods_detail’] = array_key_exists($goods_detail_list_by_keys,$item[‘id’]) ? $goods_detail_list_by_keys[$item[‘id’]] : null ; // php 7.1+ // $item[‘goods_detail’] = $goods_detail_list_by_keys[$item[‘id’]] ?? null;}unset($item);var_dump($result);这一次,我们用到了其他两个函数。array_column 、 array_key_exists,接下里一一道来,其实在array_column的官方手册中的我们就能 Example #2 中就介绍了我们想要的方法。套用在这里就是重置goods_detail_list 里面元素的 key 为 单个元素下的 goods_id。在后面我们直接用 array_key_exists 判断是否存在,然后做出相应的处理就好了。在这里我们还可以做另外一个操作,那就是默认值,因为有时候,数据有可能会因对不上,如果查出来直接返回给前端,前端没有预料到这种情况没有做容错处理就会导致前端页面崩溃,下面来改写一下代码// 在 「进阶」 板块中,我们用到了 「array_first」 函数,该函数第三个参数可以直接设置默认值,我们就不多讲了,主要讲讲最后一个$goods_detail_default = [ ‘content’ => ‘默认内容’, ‘id’ => null, ‘goods_id’=> null,];foreach($result as &$item){ $tmp = array_key_exists($goods_detail_list_by_keys,$item[‘id’]) ? $goods_detail_list_by_keys[$item[‘id’]] : [] ; // php 7.1+ // $tmp = $goods_detail_list_by_keys[$item[‘id’]] ?? []; $item[‘goods_detail’] = array_merge($goods_detail_default,$tmp);}unset($item);var_dump($result);结束看到这里就算是完结了但是有的朋友会说,为什么不用 leftJoin 来处理?确实,在处理一对一关系中很多时候我们都会选择 innerJoin 或者 leftJoin 来进行处理,一条 SQL 就能搞定,很少会用到类似于这种方案,其实不然,在主流的框架中,默认的解决方案几乎都是这样处理的,比如Laravel、ThinkPHP,考虑到的场景会有很多,比如有的时候我只是需要按需取一部分的,或者我需要根据我后面的业务结果来决定是不是要加载一对一,然而这种情况下 join 似乎就不太适合。 ...

March 30, 2019 · 2 min · jiezi

如何利用 Webshell 诊断 EDAS Serverless 应用

本文主要介绍 Serverless 应用的网络环境以及 Serverless 应用容器内的环境,了解背景知识以及基本的运维知识后可以利用 Webshell 完成基本的运维需求。Webshell 简介用户可以通过阿里云控制台直接获取 ECS 的 Shell,从而完成自己的运维需求。如果 ECS 内开启了 SSH 服务,且 ECS 存在弹性公网 IP,那么用户也可以在本地通过 SSH 服务获取 ECS 的 Shell 完成运维需求。由于 EDAS Serverless 特殊的架构以及网络环境,用户暂时无法直接从本地通过 SSH 服务获取应用容器的 Shell。在 Serverless 场景中,容器是一个暂态的、供应用运行的环境,一般来说不需要进入运维。为了方便用户进行线上问题定位排查,EDAS 在控制台提供了一个简单的Webshell,供用户查看调试自己的容器。EDAS 默认给出的 Jar War 类型应用的容器基础镜像主要是面向应用运行时,不带有冗余的排查工具,因此对运维人员可能不够友好。对于用户自身的镜像,不需要镜像中启动 SSH 服务,只需要带有可执行的/bin/bash即可。用户自己的镜像可以带上必须的运维工具方便排查。目前 Webshell 不支持 Windows 镜像。EDAS 应用节点的网络环境EDAS 应用节点处于用户自己购买的阿里云 VPC 内。在 EDAS 中,还额外提供了一层中间件服务调用隔离的手段:EDAS 命名空间。EDAS 命名空间与 VPC 内的 VSWITCH 是绑定关系,一个 EDAS 命名空间对应一个 VSWITCH,一个 VSWITCH 可以对应多个EDAS命名空间。VPC 的原理以及基本的产品情况可以在阿里云VPC官方文档了解。简单来讲,VPC 内的 IP 地址为局域网地址,不同 VPC 内的2层以上数据包无法路由到目的地。EDAS 命名空间主要做中间件逻辑隔离,不同命名空间内的应用在中间件层面是隔离的,如服务发现以及配置下发等。由于 VPC 的产品特性以及当前的 EDAS Serverless 的产品特性,容器无法直接触达 VPC 外的服务(阿里云产品除外,如 OSS、镜像服务等)。在没有额外配置的情况下,你的容器运行在网络“孤岛”环境。了解了基本的网络情况,现在可以明白为什么用户无法直接触达自己的容器了。容器内需要访问公网服务,可以通过购买 NAT,并配置 VPC 内 VSWITCH 的SNAT规则即可,详见阿里云Serverless文档。SNAT规则可以让VPC内地址访问公网地址,从而使用公网暴露的服务,获取到公网的资源。EDAS 构建的镜像的方案基于阿里云容器镜像服务,EDAS 集成了为用户构建以及管理镜像的功能。用于构建的基础镜像为centos:7,在此基础上为用户配置好了时区、语言与编码方式、Open JDK 运行环境。容器存在的目的是为了让应用运行起来,EDAS 不可能以占用所有用户运行时资源为代价,集成过多的工具,对于容器内工具有需求的用户,建议自行构建镜像,或者按需从 OSS 拉取。常见的分析手段线上容器的运维一般是不必要的。如果你确定需要进入容器进行运维,请务必了解你的操作对线上业务的风险:对于单点应用,你的行为可能导致容器 OOM,从而导致分钟级别的业务中断,而对于多点部署的业务,上述现象可能造成业务秒级中断。诊断 EDAS 应用一般从这几个方面入手:常规检查,上传搜集的日志。常规检查常规检查的方法比较多,以 Java 应用为例,一般是检查进程、线程以及 JVM 的健康状态。首先执行命令ps -ef | grep java检查你的 Java 进程是否还存在。这里必须特别说明的是,容器内一般需要使用主进程启动你的应用,这样一旦你的应用被kill掉,容器也会退出,EDAS 会将退出的容器重新启动,防止业务中断。如果进程不见了,可以执行命令dmesg | grep -i kill查看OOM相关日志。如果存在日志,那么说明你的应用进程被 kill 掉了,接着检查工作目录下hs_err_pid{PID}.log日志文件,定位具体的原因。Java 类型应用的在线分析可以使用阿里巴巴开源软件 Arthas 解决,建议在测试镜像中集成Arthas工具进行常规诊断。Arthas可以很方便地实时查看类加载情况,观察方法出入参,环境变量等。# 接入arthas,需求打通公网wget https://alibaba.github.io/arthas/arthas-boot.jarjava -jar arthas-boot.jar对于网络层的诊断,在了解上述EDAS应用节点网络情况的前提下,一般可以通过curl -v {host/ip} {port}检查域名解析以及连通性,通过tcpdump抓包观察分析网络调用情况。日志上传解决方案受限于容器内工具的匮乏,比较推荐的方案是将容器内搜集到的日志上传到云端,然后下载到本地进行分析。目前,EDAS 暂时没有提供容器内日志的下载功能,这里给出一种基于阿里云 OSS 服务的解决方案。OSS 打通了阿里云生态几乎所有的网络环境,你几乎可以在任何网络环境下上传以及下载 OSS 上的文件。首先在容器内部安装OSS命令行工具。以64位centos系统,root下没有打通公网的情况下可以选择在本地下载,然后将这个文件上传到oss,然后取oss的vpc内地址进行下载wget http://gosspublic.alicdn.com/...chmod 755 ossutil64* 然后配置你的 OSS 命令行工具,附上当前 region VPC 内的endpoint(VPC内的上传不要求打通公网,也不消耗公网带宽流量,更加经济),填写用于接收上传文件的账号的AK/SK,然后查看已经创建的Bucket,来检查你的OSS服务是否可用。请先确保账号(不必是当前账号,任意开通阿里云oss服务的账号均可)已开通 OSS 服务按照提示配置你的 AK SK endpoint信息,ststoken 不需要填写./ossutil64 config检查账号是否可用,如果报错则配置错误,如果没有bucket,则建议前往oss控制台创建,命令行工具也支持创建./ossutil64 ls这里创建一个模拟的日志文件,用于上传echo “Hello” > edas-app.log./ossutil64 cp edas-app.log {bucket-address,例如:oss://test-bucket,可以从上述命令"./ossutil64 ls"中查看}* 从 OSS 控制台或其他工具中找到你的日志文件,下载到本地,并使用你熟悉的工具进行分析。本文作者:落语(阿里云智能中间件技术开发工程师,负责分布式应用服务 EDAS 的开发和维护。)<hr>本文作者:中间件小哥阅读原文 ...

March 27, 2019 · 1 min · jiezi

模拟spring框架,深入讲解spring的对象的创建

导读项目源码地址因为公司使用的是spring框架,spring是什么?它就像包罗万象的容器,我们什么都可以往里面填,比如集合持久层的hibernate或mybatis框架,类似于拦截器的的shiro框架等等。它的好处是可以自动创建对象。以前,在没有使用spring框架时,我们必须自己创建对象。但自从有了spring框架后,Java开发就像迎来了春天,一切都变的那么简单。它有几种自动创建对象的方式,比如构造器创建对象,set创建对象。。。如果想要对其有更多的了解,那么,下载有很多博客,都对其做了详细的介绍。我在这里不必再做详解了。项目使用了logback和slf4j记录日志信息,因为它们两个是经常合作的。同时,也使用了lombok框架,这个框架可以自动生成set、get、toString、equals、hashcode方法等。下面,便详细介绍我的这个项目。设计模式本项目采用工厂和建造者设计模式。工厂设计模式用来加载配置文件。在没有使用注解的前提下,我们把所有的将要创建对象的信息写进配置文件中,这就是我们常说的依赖注入。而当代码加载时,需要加载这些配置文。这里需要两个雷来支撑。一个是XmlConfigBean,记录每个xml文件中的bean信息。XmlBeanProperty这里记录每个bean中的属性信息。加载文件方法中调用了这两个类,当然,我是用了org下的jdom来读取xml文件,正如以下代码所示。 /** * Created By zby on 22:57 2019/3/4 * 加载配置文件 * * @param dirPath 目录的路径 /public static LoadConfig loadXmlConFig(String dirPath) { if (StringUtils.isEmpty(dirPath)){ throw new RuntimeException(“路径不存在”); } if (null == config) { File dir = new File(dirPath); List<File> files = FactoryBuilder.createFileFactory().listFile(dir); if (CollectionUtil.isEmpty(files)) { throw new RuntimeException(“没有配置文件files=” + files); } allXmls = new HashMap<>(); SAXBuilder saxBuilder = new SAXBuilder(); Document document = null; for (File file : files) { try { Map<String, XmlConfigBean> beanMaps = new HashMap<>(); //创建配置文件 String configFileName = file.getName(); document = saxBuilder.build(file); Element rootEle = document.getRootElement(); List beans = rootEle.getChildren(“bean”); if (CollectionUtil.isNotEmpty(beans)) { int i = 0; for (Iterator beanIterator = beans.iterator(); beanIterator.hasNext(); i++) { Element bean = (Element) beanIterator.next(); XmlConfigBean configBean = new XmlConfigBean(); configBean.setId(attributeToConfigBeanProps(file, i, bean, “id”)); configBean.setClazz(attributeToConfigBeanProps(file, i, bean, “class”)); configBean.setAutowire(attributeToConfigBeanProps(file, i, bean, “autowire”)); configBean.setConfigFileName(configFileName); List properties = bean.getChildren(); Set<XmlBeanProperty> beanProperties = new LinkedHashSet<>(); if (CollectionUtil.isNotEmpty(properties)) { int j = 0; for (Iterator propertyIterator = properties.iterator(); propertyIterator.hasNext(); j++) { Element property = (Element) propertyIterator.next(); XmlBeanProperty beanProperty = new XmlBeanProperty(); beanProperty.setName(attributeToBeanProperty(file, i, j, property, “name”)); beanProperty.setRef(attributeToBeanProperty(file, i, j, property, “ref”)); beanProperty.setValue(attributeToBeanProperty(file, i, j, property, “value”)); beanProperties.add(beanProperty); } configBean.setProperties(beanProperties); } beanMaps.put(configBean.getId(), configBean); } } allXmls.put(configFileName, beanMaps); } catch (JDOMException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } return new LoadConfig(); } return config;}上面使用到了文件工厂设计模式,内部使用深度递归算法。如果初始目录下,仍旧有子目录,调用自身的方法,直到遇见文件,如代码所示:/Created By zby on 14:04 2019/2/14获取文件的集合/private void local(File dir) {if (dir == null) { logger.error(“文件夹为空dir=” + dir); throw new RuntimeException(“文件夹为空dir=” + dir);}File[] fies = dir.listFiles();if (ArrayUtil.isNotEmpty(fies)) { for (File fy : fies) { if (fy.isDirectory()) { local(fy); } String fileName = fy.getName(); boolean isMatch = Pattern.compile(reg).matcher(fileName).matches(); boolean isContains = ArrayUtil.containsAny(fileName, FilterConstants.FILE_NAMES); if (isMatch && !isContains) { fileList.add(fy); } }}}建造者设计模式这里用来修饰类信息的。比如,将类名的首字母转化为小写;通过类路径转化为类字面常量,如代码所示: / * Created By zby on 20:19 2019/2/16 * 通过类路径转为类字面常量 * * @param classPath 类路径 /public static <T> Class<T> classPathToClazz(String classPath) { if (StringUtils.isBlank(classPath)) { throw new RuntimeException(“类路径不存在”); } try { return (Class<T>) Class.forName(classPath); } catch (ClassNotFoundException e) { logger.error(“路径” + classPath + “不存在,创建失败e=” + e); e.printStackTrace(); } return null;}类型转换器如果不是用户自定义的类型,我们需要使用类型转化器,将配置文件的数据转化为我们Javabean属性的值。因为,从配置文件读取过来的值,都是字符串类型的,加入Javabean的id为long型,因而,我们需要这个类型转换。/* * Created By zby on 22:31 2019/2/25 * 将bean文件中的value值转化为属性值 /public final class Transfomer { public final static Integer MAX_BYTE = 127; public final static Integer MIN_BYTE = -128; public final static Integer MAX_SHORT = 32767; public final static Integer MIN_SHORT = -32768; public final static String STR_TRUE = “true”; public final static String STR_FALSE = “false”; /* * Created By zby on 22:32 2019/2/25 * 数据转化 * * @param typeName 属性类型的名字 * @param value 值 / public static Object transformerPropertyValue(String typeName, Object value) throws IllegalAccessException { if (StringUtils.isBlank(typeName)) { throw new RuntimeException(“属性的类型不能为空typeName+” + typeName); } if (typeName.equals(StandardBasicTypes.STRING)) { return objToString(value); } else if (typeName.equalsIgnoreCase(StandardBasicTypes.LONG)) { return stringToLong(objToString(value)); } else if (typeName.equals(StandardBasicTypes.INTEGER) || typeName.equals(StandardBasicTypes.INT)) { return stringToInt(objToString(value)); } else if (typeName.equalsIgnoreCase(StandardBasicTypes.BYTE)) { return stringToByte(objToString(value)); } else if (typeName.equalsIgnoreCase(StandardBasicTypes.SHORT)) { return stringToShort(objToString(value)); } else if (typeName.equalsIgnoreCase(StandardBasicTypes.BOOLEAN)) { return stringToBoolean(objToString(value)); } else if (typeName.equalsIgnoreCase(StandardBasicTypes.DOUBLE)) { return stringToDouble(objToString(value)); } else if (typeName.equalsIgnoreCase(StandardBasicTypes.FLOAT)) { return stringToFloat(objToString(value)); } else if (typeName.equals(StandardBasicTypes.DATE)) { return stringToDate(objToString(value)); } else if (typeName.equals(StandardBasicTypes.BIG_DECIMAL)) { return stringToBigDecimal(objToString(value)); } else { return value; } } /* * Created By zby on 22:32 2019/2/25 * 数据转化 / public static void transformerPropertyValue(Object currentObj, Field field, Object value) throws IllegalAccessException { if (null == currentObj && field == null) { throw new RuntimeException(“当前对象或属性为空值”); } String typeName = field.getType().getSimpleName(); field.setAccessible(true); field.set(currentObj, transformerPropertyValue(typeName, value)); } /* * Created By zby on 23:29 2019/2/25 * obj to String / public static String objToString(Object obj) { return null == obj ? null : obj.toString(); } /* * Created By zby on 23:54 2019/2/25 * String to integer / public static Integer stringToInt(String val) { if (StringUtils.isBlank(val)) { return 0; } if (val.charAt(0) == 0) { throw new RuntimeException(“字符串转为整形失败val=” + val); } return Integer.valueOf(val); } /* * Created By zby on 23:31 2019/2/25 * String to Long / public static Long stringToLong(String val) { return Long.valueOf(stringToInt(val)); } /* * Created By zby on 23:52 2019/2/26 * String to byte / public static Short stringToShort(String val) { Integer result = stringToInt(val); if (result >= MIN_SHORT && result <= MAX_SHORT) { return Short.valueOf(result.toString()); } throw new RuntimeException(“数据转化失败result=” + result); } /* * Created By zby on 0:03 2019/2/27 * String to short / public static Byte stringToByte(String val) { Integer result = stringToInt(val); if (result >= MIN_BYTE && result <= MAX_BYTE) { return Byte.valueOf(result.toString()); } throw new RuntimeException(“数据转化失败result=” + result); } /* * Created By zby on 0:20 2019/2/27 * string to double / public static Double stringToDouble(String val) { if (StringUtils.isBlank(val)) { throw new RuntimeException(“数据为空,转换失败”); } return Double.valueOf(val); } /* * Created By zby on 0:23 2019/2/27 * string to float / public static Float stringToFloat(String val) { if (StringUtils.isBlank(val)) { throw new RuntimeException(“数据为空,转换失败”); } return Float.valueOf(val); } /* * Created By zby on 0:19 2019/2/27 * string to boolean / public static boolean stringToBoolean(String val) { if (StringUtils.isBlank(val)) { throw new RuntimeException(“数据为空,转换失败val=” + val); } if (val.equals(STR_TRUE)) { return true; } if (val.equals(STR_FALSE)) { return false; } byte result = stringToByte(val); if (0 == result) { return false; } if (1 == result) { return true; } throw new RuntimeException(“数据转换失败val=” + val); } /* * Created By zby on 0:24 2019/2/27 * string to Date / public static Date stringToDate(String val) { if (StringUtils.isBlank(val)) { throw new RuntimeException(“数据为空,转换失败val=” + val); } SimpleDateFormat format = new SimpleDateFormat(); try { return format.parse(val); } catch (ParseException e) { throw new RuntimeException(“字符串转为时间失败val=” + val); } } /* * Created By zby on 0:31 2019/2/27 * string to big decimal / public static BigDecimal stringToBigDecimal(String val) { if (StringUtils.isBlank(val)) { throw new RuntimeException(“数据为空,转换失败val=” + val); } return new BigDecimal(stringToDouble(val)); }}常量类型自动装配类型/* * Created By zby on 13:50 2019/2/23 * 装配类型 /public class AutowireType { /* * 缺省情况向,一般通过ref来自动(手动)装配对象 / public static final String NONE = null; /* * 根据属性名事项自动装配, * 如果一个bean的名称和其他bean属性的名称是一样的,将会自装配它。 / public static final String BY_NAME = “byName”; /* * 根据类型来装配 * 如果一个bean的数据类型是用其它bean属性的数据类型,兼容并自动装配它。 / public static final String BY_TYPE = “byType”; /* * 根据构造器constructor创建对象 / public static final String CONSTRUCTOR = “constructor”; /* * autodetect – 如果找到默认的构造函数,使用“自动装配用构造”; 否则,使用“按类型自动装配”。 / public static final String AUTODETECT = “autodetect”; }属性类型常量池/* * Created By zby on 22:44 2019/2/25 * 类型常量池 /public class StandardBasicTypes { public static final String STRING = “String”; public static final String LONG = “Long”; public static final String INTEGER = “Integer”; public static final String INT = “int”; public static final String BYTE = “Byte”; public static final String SHORT = “Short”; public static final String BOOLEAN = “Boolean”; public static final String DOUBLE = “double”; public static final String FLOAT = “float”; public static final String DATE = “Date”; public static final String TIMESTAMP = “Timestamp”; public static final String BIG_DECIMAL = “BigDecimal”; public static final String BIG_INTEGER = “BigInteger”;}getBean加载上下文文件首先需要一个构造器,形参时文件的名字;getBean方法,形参是某个bean的id名字,这样,根据当前bean的自动装配类型,来调用响应的方法。 /* * Created By zby on 11:17 2019/2/14 * 类的上下文加载顺序 /public class ClassPathXmlApplicationContext {private static Logger logger = LoggerFactory.getLogger(ClassPathXmlApplicationContext.class.getName());private String configXml;public ClassPathXmlApplicationContext(String configXml) { this.configXml = configXml;}/* * Created By zby on 18:38 2019/2/24 * bean对应的id的名称 /public Object getBean(String name) { String dirPath="../simulaspring/src/main/resources/"; Map<String, Map<String, XmlConfigBean>> allXmls = LoadConfig.loadXmlConFig(dirPath).getAllXmls(); boolean contaninsKey = MapUtil.findKey(allXmls, configXml); if (!contaninsKey) { throw new RuntimeException(“配置文件不存在” + configXml); } Map<String, XmlConfigBean> beans = allXmls.get(configXml); contaninsKey = MapUtil.findKey(beans, name); if (!contaninsKey) { throw new RuntimeException(“id为” + name + “bean不存在”); } XmlConfigBean configFile = beans.get(name); if (null == configFile) { throw new RuntimeException(“id为” + name + “bean不存在”); } String classPath = configFile.getClazz(); if (StringUtils.isBlank(classPath)) { throw new RuntimeException(“id为” + name + “类型不存在”); } String autowire = configFile.getAutowire(); if (StringUtils.isBlank(autowire)) { return getBeanWithoutArgs(beans, classPath, configFile); } else { switch (autowire) { case AutowireType.BY_NAME: return getBeanByName(beans, classPath, configFile); case AutowireType.CONSTRUCTOR: return getBeanByConstruct(classPath, configFile); case AutowireType.AUTODETECT: return getByAutodetect(beans, classPath, configFile); case AutowireType.BY_TYPE: return getByType(beans, classPath, configFile); } } return null; }}下面主要讲解默认自动装配、属性自动装配、构造器自动装配默认自动装配如果我们没有填写自动装配的类型,其就采用ref来自动(手动)装配对象。 /* * Created By zby on 18:33 2019/2/24 * 在没有设置自动装配时,通过ref对象 /private Object getBeanWithoutArgs(Map<String, XmlConfigBean> beans, String classPath, XmlConfigBean configFile) {//属性名称String proName = null;try { Class currentClass = Class.forName(classPath); //通过引用 ref 创建对象 Set<XmlBeanProperty> properties = configFile.getProperties(); //如果没有属性,就返回,便于下面的递归操作 if (CollectionUtil.isEmpty(properties)) { return currentClass.newInstance(); } Class<?> superClass = currentClass.getSuperclass(); //TODO 父类的集合// List<Class> superClasses = null; //在创建子类构造器之前,创建父类构造器, // 父类构造器的参数子类构造器的参数 Object currentObj = null; //当前构造器 Object consArgsObj = null; String consArgsName = null; boolean hasSuperClass = (null != superClass && !superClass.getSimpleName().equals(“Object”)); if (hasSuperClass) { Constructor[] constructors = currentClass.getDeclaredConstructors(); ArrayUtil.validateArray(superClass, constructors); Parameter[] parameters = constructors[0].getParameters(); if (parameters == null || parameters.length == 0) { consArgsObj = constructors[0].newInstance(); } else { ArrayUtil.validateArray(superClass, parameters); consArgsName = parameters[0].getType().getSimpleName(); //配置文件大类型,与参数构造器的类型是否相同 for (XmlBeanProperty property : properties) { String ref = property.getRef(); if (StringUtils.isNotBlank(ref) && ref.equalsIgnoreCase(consArgsName)) { classPath = beans.get(ref).getClazz(); Class<?> clazz = Class.forName(classPath); consArgsObj = clazz.newInstance(); } } currentObj = constructors[0].newInstance(consArgsObj); } } else { currentObj = currentClass.newInstance(); } for (XmlBeanProperty property : properties) { //这里适合用递归,无限调用自身 //通过name找到属性,配置文件中是否有该属性,通过ref找到其对应的bean文件 proName = property.getName(); Field field = currentClass.getDeclaredField(proName); if (null != field) { String ref = property.getRef(); Object value = property.getValue(); //如果没有赋初值,就通过类型创建 if (null == value && StringUtils.isNotBlank(ref)) { boolean flag = StringUtils.isNotBlank(consArgsName) && null != consArgsObj && consArgsName.equalsIgnoreCase(ref); //递归调用获取属性对象 value = flag ? consArgsObj : getBean(ref); } field.setAccessible(true); Transfomer.transformerPropertyValue(currentObj, field, value); } } return currentObj;} catch (ClassNotFoundException e) { logger.error(“名为” + classPath + “类不存在”); e.printStackTrace();} catch (InstantiationException e) { e.printStackTrace();} catch (IllegalAccessException e) { e.printStackTrace();} catch (InvocationTargetException e) { e.printStackTrace();} catch (NoSuchFieldException e) { logger.error(classPath + “类的属性” + proName + “不存在”); throw new RuntimeException(classPath + “类的属性” + proName + “不存在”);}return null;}构造器创建对象根据构造器constructor创建对象/* * Created By zby on 23:06 2019/3/2 * * @param classPath 类路径 * @param configFile 配置文件 /private Object getBeanByConstruct(String classPath, XmlConfigBean configFile) { try { Class currentClass = Class.forName(classPath); Set<XmlBeanProperty> properties = configFile.getProperties(); if (CollectionUtil.isEmpty(properties)) { return currentClass.newInstance(); } ///构造器参数类型和构造器对象集合 Object[] objects = new Object[properties.size()]; Class<?>[] paramType = new Class[properties.size()]; Field[] fields = currentClass.getDeclaredFields(); int i = 0; for (Iterator iterator = properties.iterator(); iterator.hasNext(); i++) { XmlBeanProperty property = (XmlBeanProperty) iterator.next(); String proName = property.getName(); String ref = property.getRef(); Object value = property.getValue(); for (Field field : fields) { Class<?> type = field.getType(); String typeName = type.getSimpleName(); String paramName = field.getName(); if (paramName.equals(proName) && ObjectUtil.isNotNull(value) && StringUtils.isBlank(ref)) { objects[i] = Transfomer.transformerPropertyValue(typeName, value); paramType[i] = type; break; } else if (paramName.equals(proName) && StringUtils.isNotBlank(ref) && ObjectUtil.isNull(value)) { objects[i] = getBean(ref); paramType[i] = type; break; } } } return currentClass.getConstructor(paramType).newInstance(objects); } catch (ClassNotFoundException e) { logger.error(“名为” + classPath + “类不存在”); e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchMethodException e) { e.printStackTrace(); } return null;}属性自动装配根据属性名事项自动装配,如果一个bean的名称和其他bean属性的名称是一样的,将会自装配它。 /* * Created By zby on 21:16 2019/3/1 * 根据属性名事项自动装配, * @param classPath 类路径 * @param configFile 配置文件 */private Object getBeanByName( String classPath, XmlConfigBean configFile) { String proName = null; try { Class currentClass = Class.forName(classPath); Class superclass = currentClass.getSuperclass(); Method[] methods = currentClass.getDeclaredMethods(); List<Method> methodList = MethodHelper.filterSetMethods(methods); Object currentObj = currentClass.newInstance(); Set<XmlBeanProperty> properties = configFile.getProperties(); //配置文件中,但是有父类, if (CollectionUtil.isEmpty(properties)) { boolean isExit = null != superclass && !superclass.getSimpleName().equals(“Object”); if (isExit) { Field[] parentFields = superclass.getDeclaredFields(); if (ArrayUtil.isNotEmpty(parentFields)) { if (CollectionUtil.isNotEmpty(methodList)) { for (Field parentField : parentFields) { for (Method method : methodList) { if (MethodHelper.methodNameToProName(method.getName()).equals(parentField.getName())) { //如果有泛型的话 Type genericType = currentClass.getGenericSuperclass(); if (null != genericType) { String genericName = genericType.getTypeName(); genericName = StringUtils.substring(genericName, genericName.indexOf("<") + 1, genericName.indexOf(">")); Class genericClass = Class.forName(genericName); method.setAccessible(true); method.invoke(currentObj, genericClass); } break; } } break; } } } } return currentObj; } //传递给父级对象 service – 》value List<Method> tmpList = new ArrayList<>(); Map<String, Object> map = new HashMap<>(); Object value = null; for (XmlBeanProperty property : properties) { proName = property.getName(); if (ArrayUtil.isNotEmpty(methods)) { String ref = property.getRef(); value = property.getValue(); for (Method method : methodList) { String methodName = MethodHelper.methodNameToProName(method.getName()); Field field = currentClass.getDeclaredField(methodName); if (methodName.equals(proName) && null != field) { if (null == value && StringUtils.isNotBlank(ref)) { value = getBean(ref); } else if (value != null && StringUtils.isBlank(ref)) { value = Transfomer.transformerPropertyValue(field.getType().getSimpleName(), value); } method.setAccessible(true); method.invoke(currentObj, value); map.put(proName, value); tmpList.add(method); break; } } } } tmpList = MethodHelper.removeMethod(methodList, tmpList); for (Method method : tmpList) { Class<?>[] type = method.getParameterTypes(); if (ArrayUtil.isEmpty(type)) { throw new RuntimeException(“传递给父级对象的参数为空type=” + type); } for (Class<?> aClass : type) { String superName = ClassHelper.classNameToProName(aClass.getSimpleName()); value = map.get(superName); method.setAccessible(true); method.invoke(currentObj, value); } } return currentObj; } catch (ClassNotFoundException e) { logger.error(“名为” + classPath + “类不存在”); e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InvocationTargetException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { logger.error(“类” + classPath + “属性” + proName + “不存在”); e.printStackTrace(); } return null;}总结这里没有使用注解,我们可以使用注解的方式实现自动装配,但这不spring的核心,应该时spring的美化,核心值如何实现自动装配。 ...

March 17, 2019 · 10 min · jiezi

模仿hibernate框架,详解hibernate部分方法设计

导读源码地址公司的持久层采用的hibernate框架,这也是很多公司使用的一种持久层框架。它将瞬时态的数据转化为持久态、或将持久态的数据转化为瞬时态数据。我比较喜欢看源码,看别人的架构思想,因为,笔者想向架构师的方向进发。看了别人的源码,突然想模拟hibernate框架,自己写个框架出来。 这里去除了hibernate框架晦涩的地方,当做自己学习材料还是不错的。里面涉及到反射、连接池等等。 这个项目中,你可以知道数据库连接池是怎么建的,又是怎么回收的。 使用警惕代码块加载配置文件以下详细介绍我个人的项目,但肯定没有人家源码写得好,这里仅作为学习使用。如果不懂的,可以私信我。配置文件本项目以idea为开发环境和以maven搭建的,分为java包和test包。java包的配置文件放在resources下,代码放在com.zby.simulationHibernate包下,如下是配置文件:连接池我们在使用hibernate时,一般会配置连接池,比如,初始化连接数是多少,最大连接数是多少?这个连接的是什么?我们在启动项目时,hibernate根据初始的连接数,来创建多少个数据库连接对象,也就是jdbc中的Connection对象。为什么要有这个连接池?因为,每次开启一个连接和关闭一个连接都是消耗资源的,我们开启了这些连接对象之后,把它们放在一个容器中,我们何时需要何时从容器中取出来。当不需要的时候,再将踏进放回到容器中。因而,可以减少占用的资源。如下,是初始化的连接对象:package com.zby.simulationHibernate.util.factory;import com.zby.simulationHibernate.util.exception.GenericException;import org.apache.commons.lang3.StringUtils;import java.io.IOException;import java.io.InputStream;import java.sql.DriverManager;import java.sql.SQLException;import java.util.Properties;/** * Created By zby on 21:23 2019/1/23 * 数据库的连接 /public class Connect { /* * 连接池的初始值 / private static int initPoolSize = 20; /* * 创建property的配置文件 / protected static Properties properties; /* * 连接池的最小值 / protected static int minPoolSize; /* * 连接池的最大值 / protected static int maxPoolSize; //【2】静态代码块 static { //加载配置文件 properties = new Properties(); InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream(“db.properties”); try { properties.load(is); minPoolSize = Integer.valueOf(properties.getProperty(“jdbc.minConnPool”)); if (minPoolSize <= initPoolSize) minPoolSize = initPoolSize; maxPoolSize = Integer.valueOf(properties.getProperty(“jdbc.maxConnPool”)); if (minPoolSize > maxPoolSize) throw new GenericException(“连接池的最小连接数不能大于最大连接数”); } catch (IOException e) { System.out.println(“未找到配置文件”); e.printStackTrace(); } } /* * Created By zby on 16:50 2019/1/23 * 获取数据连接 / protected java.sql.Connection createConnect() { String driverName = properties.getProperty(“jdbc.driver”); if (StringUtils.isEmpty(driverName)) { driverName = “com.mysql.jdbc.Driver”; } String userName = properties.getProperty(“jdbc.username”); String password = properties.getProperty(“jdbc.password”); String dbUrl = properties.getProperty(“jdbc.url”); try { Class.forName(driverName); return DriverManager.getConnection(dbUrl, userName, password); } catch (ClassNotFoundException e) { System.out.println(“找不到驱动类”); e.printStackTrace(); } catch (SQLException e) { System.out.println(“加载异常”); e.printStackTrace(); } return null; }}创建Session会话我们在使用hibernate时,不是直接使用连接对象,而是,以会话的方式创建一个连接。创建会话的方式有两种。一种是openSession,这种是手动提交事务。getCurrentSession是自动提交事务。如代码所示:package com.zby.simulationHibernate.util.factory;import java.sql.Connection;import java.util.ArrayList;import java.util.Iterator;import java.util.List;/* * Created By zby on 15:43 2019/1/23 /public class SqlSessionFactory implements SessionFactory { /* * 连接池 / private static List<Connection> connections; /* * 连接对象 * * @return / private static Connect connect = new Connect(); protected static List<Connection> getConnections() { return connections; } //静态代码块,初始化常量池 static { connections = new ArrayList<>(); Connection connection; for (int i = 0; i < Connect.minPoolSize; i++) { connection = connect.createConnect(); connections.add(connection); } } @Override public Session openSession() { return getSession(false); } @Override public Session getCurrentSession() { return getSession(true); } /* * 获取session * * @param autoCommit 是否自动提交事务 * @return / private Session getSession(boolean autoCommit) { //【1】判断连接池有可用的连接对象 boolean hasNoValidConn = hasValidConnction(); //【2】没有可用的连接池,使用最大的连接池 if (!hasNoValidConn) { for (int i = 0; i < (Connect.maxPoolSize - Connect.minPoolSize); i++) { connections.add(connect.createConnect()); } } //【3】有可用的连接 for (Iterator iterator = connections.iterator(); iterator.hasNext(); ) { Connection connection = null; try { connection = (Connection) iterator.next(); connection.setAutoCommit(autoCommit); Session session = new Session(connection); iterator.remove(); return session; } catch (Exception e) { e.printStackTrace(); } } return null; } /* * Created By zby on 21:50 2019/1/23 * 当我们没开启一个连接,连接池的数目减少1,直到连接池的数量为0 / private boolean hasValidConnction() { return null != connections && connections.size() != 0; }}数据查找我们既然使用这个框架,必然要有数据查找的功能。返回结果分为两种,一种是以实体类直接返回,调用AddEntity方法。但是,有时时多张表查询的结果,这种情况下,直接以实体类肯定不可以的,因而,我们需要使用自定义接收对象,并将查找结果进行过滤,再封装成我们想要的对象。第一种,以实体类返回/* * Created By zby on 23:19 2019/1/23 * 体检反射的实体类 /public SqlQuery addEntity(Class<T> persistenceClass) { this.persistenceClass = persistenceClass; return this;}第二种,过滤后返回数据 /* * Created By zby on 19:18 2019/1/27 * 创建类型 /public SqlQuery addScalar(String tuple, String alias) { if (CommonUtil.isNull(aliasMap)) { aliasMap = new HashMap<>(); } for (Map.Entry<String, String> entry : aliasMap.entrySet()) { String key = entry.getKey(); if (key.equals(tuple)) throw new GenericException(“alias已经存在,即alias=” + key); String value = aliasMap.get(key); if (value.equals(alias) && key.equals(tuple)) throw new GenericException(“当前alias的type已经存在,alias=” + key + “,type=” + value); } aliasMap.put(tuple, alias); return this;}/* * Created By zby on 9:20 2019/1/28 * 数据转换问题 /public SqlQuery setTransformer(ResultTransformer transformer) { if (CommonUtil.isNull(aliasMap)) { throw new IllegalArgumentException(“请添加转换的属性数量”); } transformer.transformTuple(aliasMap); this.transformer = transformer; return this;}以集合的方式返回数据:/* * Created By zby on 17:02 2019/1/29 * 设置查找参数 /public SqlQuery setParamter(int start, Object param) { if (CommonUtil.isNull(columnParamer)) columnParamer = new HashMap<>(); columnParamer.put(start, param); return this;}/* * Created By zby on 16:41 2019/1/24 * 查找值 /public List<T> list() { PreparedStatement statement = null; ResultSet resultSet = null; try { statement = connection.prepareStatement(sql); if (CommonUtil.isNotNull(columnParamer)) { for (Map.Entry<Integer, Object> entry : columnParamer.entrySet()) { int key = entry.getKey(); Object value = entry.getValue(); statement.setObject(key + 1, value); } } resultSet = statement.executeQuery(); PersistentObject persistentObject = new PersistentObject(persistenceClass, resultSet); if (CommonUtil.isNotNull(aliasMap)) return persistentObject.getPersist(transformer); return persistentObject.getPersist(); } catch (Exception e) { e.printStackTrace(); } finally { SessionClose.closeConnStateResSet(connection, statement, resultSet); } return null;}返回唯一值/* * Created By zby on 16:41 2019/1/24 * 查找值 /public T uniqueResult() { List<T> list = list(); if (CommonUtil.isNull(list)) return null; if (list.size() > 1) throw new GenericException(“本来需要返回一个对象,却返回 " + list.size() + “个对象”); return list.get(0);}测试 @Test public void testList() { Session session = new SqlSessionFactory().openSession(); String sql = “SELECT " + " customer_name AS customerName, " + " name AS projectName " + “FROM " + " project where id >= ? and id <= ?”; SqlQuery query = session.createSqlQuery(sql); query.setParamter(0, 1); query.setParamter(1, 2); query.addScalar(“customerName”, StandardBasicTypes.STRING) .addScalar(“projectName”, StandardBasicTypes.STRING); query.setTransformer(Transforms.aliasToBean(ProjectData.class)); List<ProjectData> projects = query.list(); for (ProjectData project : projects) { System.out.println(project.getCustomerName() + " " + project.getProjectName()); } } @Ignore public void testListNoData() { Session session = new SqlSessionFactory().openSession(); String sql = “SELECT " + " customer_name AS customerName, " + " name AS projectName " + “FROM " + " project where id >= ? and id <= ?”; SqlQuery query = session.createSqlQuery(sql). setParamter(0, 1). setParamter(1, 2). addEntity(Project.class); List<Project> projects = query.list(); for (Project project : projects) { System.out.println(project.getCustomerName() + " " + project.getGuestCost()); } }保存数据我们这里以 merger来保存数据,因为这个方法非常的特殊。如果该瞬时态的独享有主键,而且,其在数据库中依旧存在该主键的数据,我们此时就更新数据表。如果数据表中没有当前主键的数据,我们向数据库中添加该对象的值。如果该瞬时态的对象没有主键,我们直接在数据表中添加该对象。如代码所示: /* * Created By zby on 15:41 2019/1/29 * 合并,首先判断id是否存在,若id存在则更新,若id不存在,则保存数据 /public T merge(T t) { if (CommonUtil.isNull(t)) throw new IllegalArgumentException(“参数为空”); Class<T> clazz = (Class<T>) t.getClass(); Field[] fields = clazz.getDeclaredFields(); boolean isContainsId = CommonUtil.isNotNull(PropertyUtil.containId(fields)) ? true : false; long id = PropertyUtil.getIdValue(fields, t, propertyAccessor); if (isContainsId) { return id > 0L ? update(t) : save(t); } return save(t);} /* * Created By zby on 17:37 2019/1/29 * 保存数据 /public T save(T t) { if (CommonUtil.isNull(t)) throw new RuntimeException(“不能保存空对象”); PreparedStatement statement = null; ResultSet resultSet = null; StringBuilder columnJoint = new StringBuilder(); StringBuilder columnValue = new StringBuilder(); try { Field[] fields = t.getClass().getDeclaredFields(); String sql = " insert into " + ClassUtil.getClassNameByGenericity(t) + “(”; for (int i = 0; i < fields.length; i++) { String propertyName = fields[i].getName(); Object propertyValue = propertyAccessor.getPropertyValue(t, propertyName); if (CommonUtil.isNotNull(propertyValue)) { String columnName = PropertyUtil.propertyNameTransformColumnName(propertyName, true); if (StandardBasicTypes.BOOLEAN.equalsIgnoreCase(fields[i].getGenericType().toString())) { columnJoint.append(“is_” + columnName + “,”); columnValue.append(propertyValue + “,”); } else if (StandardBasicTypes.LONG.equalsIgnoreCase(fields[i].getGenericType().toString()) || StandardBasicTypes.FLOAT.equalsIgnoreCase(fields[i].getGenericType().toString()) || StandardBasicTypes.DOUBLE.equalsIgnoreCase(fields[i].getGenericType().toString()) || StandardBasicTypes.INTEGER.equalsIgnoreCase(fields[i].getGenericType().toString())) { columnJoint.append(columnName + “,”); columnValue.append(propertyValue + “,”); } else if (StandardBasicTypes.DATE.equalsIgnoreCase(fields[i].getGenericType().toString())) { columnJoint.append(columnName + “,”); columnValue.append(”’” + DateUtil.SIMPLE_DATE_FORMAT.format((Date) propertyValue) + “’,”); } else { columnJoint.append(columnName + “,”); columnValue.append(”’" + propertyValue + “’,”); } } } columnJoint = StringUtil.replace(columnJoint, “,”); columnValue = StringUtil.replace(columnValue, “,”); sql += columnJoint + “) VALUES(” + columnValue + “)”; System.out.println(sql); statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); statement.executeUpdate(); resultSet = statement.getGeneratedKeys(); while (resultSet.next()) { return load((Class<T>) t.getClass(), resultSet.getLong(1)); } return t; } catch (SQLException e) { System.out.println(“保存数据出错,实体对象为=” + t); e.printStackTrace(); } finally { SessionClose.closeConnStateResSet(connection, statement, resultSet); } return null;}测试代码: @Testpublic void testSave() { Session session = new SqlSessionFactory().getCurrentSession(); Project project = new Project(); project.setCustomerName(“hhhh”); project.setCreateDatetime(new Date()); project.setDeleted(true); project = (Project) session.save(project); System.out.println(project.getId());}通过id加载对象有时,我们只要根据当前对象的id,获取当前对象的全部信息,因而,我们可以这样写: /* * Created By zby on 16:36 2019/1/29 * 通过id获取对象 /public T load(Class<T> clazz, Long id) { if (CommonUtil.isNull(clazz)) throw new IllegalArgumentException(“参数为空”); String className = ClassUtil.getClassNameByClass(clazz); String sql = " select * from " + className + " where id= ? “; SqlQuery query = createSqlQuery(sql) .setParamter(0, id) .addEntity(clazz); return (T) query.uniqueResult();}测试代码:@Testpublic void testload() { Session session = new SqlSessionFactory().openSession(); Project project = (Project) session.load(Project.class, 4L); System.out.println(project);}回收连接对象当我们使用完该连接对象后,需要将对象放回到容器中,而不是直接调用connection.close()方法,而是调用这个方法: /* * Created By zby on 16:10 2019/3/17 * 获取容器的对象,如果是关闭session,则将连接对象放回到容器中 * 如果是开启session,则从容器中删除该连接对象 /protected static List<Connection> getConnections() { return connections;} /* * Created By zby on 22:45 2019/1/23 * <p> * 当关闭当前会话时,这并非真正的关闭会话 * 只是将连接对象放回到连接池中 */public static void closeConn(Connection connection) { SqlSessionFactory.getConnections().add(connection);}总结写框架其实是不难的,难就难在如何设计框架。或者说,难就难在基础不牢。如果基础打不牢的话,很难网上攀升。 ...

March 17, 2019 · 6 min · jiezi

框架与RTTI的关系,RTTI与反射之间的关系

导读在之后的几篇文章,我会讲解我自己的hibernate、spring、beanutils框架,但讲解这些框架之前,我需要讲解RTTI和反射。工作将近一年了,我们公司项目所使用的框架是SSH,或者,其他公司使用的是SSM框架。不管是什么样的框架,其都涉及到反射。那么,什么是反射?我们在生成对象时,事先并不知道生成哪种类型的对象,只有等到项目运行起来,框架根据我们的传参,才生成我们想要的对象。比如,我们从前端调用后端的接口,查询出这个人的所有项目,我们只要传递这个人的id即可。当然,数据来源于数据库,那么,问题来了,数据是怎么从持久态转化成我们想要的顺时态的?这里面,就涉及到了反射。但是,一提到反射,我们势必就提到RTTI,即运行时类型信息(runtime Type Infomation)。RTTIpo类/** * Created By zby on 16:53 2019/3/16 /@AllArgsConstructor@NoArgsConstructorpublic class Pet { private String name; private String food; public void setName(String name) { this.name = name; } public void setFood(String food) { this.food = food; } public String getName() { return name; } public String getFood() { return food; }}/* * Created By zby on 17:03 2019/3/16 /public class Cat extends Pet{ @Override public void setFood(String food) { super.setFood(food); }}/* * Created By zby on 17:04 2019/3/16 /public class Garfield extends Cat{ @Override public void setFood(String food) { super.setFood(food); }}/* * Created By zby on 17:01 2019/3/16 /public class Dog extends Pet{ @Override public void setFood(String food) { super.setFood(food); }}以上是用来说明的persistent object类,也就是,我们在进行pojo常用的javabean类。其有继承关系,如下图:展示信息如一下代码所示,方法eatWhatToday有两个参数,这两个参数一个是接口类,一个是父类,也就是说,我们并不知道打印出的是什么信息。只有根据接口的实现类来和父类的子类,来确认打印出的信息。这就是我们输的运行时类型信息,正因为有了RTTI,java才有了动态绑定的概念。/* * Created By zby on 17:05 2019/3/16 /public class FeedingPet { /* * Created By zby on 17:05 2019/3/16 * 某种动物今天吃的是什么 * * @param baseEnum 枚举类型 这里表示的时间 * @param pet 宠物 / public static void eatWhatToday(BaseEnum baseEnum, Pet pet) { System.out.println( pet.getName() + “今天” + baseEnum.getTitle() + “吃的” + pet.getFood()); } }测试类 @Testpublic void testPet(){ Dog dog=new Dog(); dog.setName(“宠物狗京巴”); dog.setFood(FoodTypeEnum.FOOD_TYPE_BONE.getTitle()); FeedingPet.eatWhatToday(DateTypeEnum.DATE_TYPE_MORNING,dog); Garfield garfield=new Garfield(); garfield.setName(“宠物猫加菲猫”); garfield.setFood(FoodTypeEnum.FOOD_TYPE_CURRY.getTitle()); FeedingPet.eatWhatToday(DateTypeEnum.DATE_TYPE_MIDNIGHT_SNACK,garfield);}打印出的信息为:那么,这和反射有什么关系呢?反射获取当前类信息正如上文提到的运行时类型信息,那么,类型信息在运行时是如何表示的?此时,我们就想到了Class这个特殊对象。见名知其意,即类对象,其包含了类的所有信息,包括属性、方法、构造器。我们都知道,类是程序的一部分,每个类都有一个Class对象。每当编写并且执行了一个新类,就会产生一个Class对象(更恰当地说,是被保存在一个同名的.class文件中)。为了生成这个类的对象,运行当前程序的jvm将使用到类加载器。jvm首先调用bootstrap类加载器,加载核心文件,jdk的核心文件,比如Object,System等类文件。然后调用plateform加载器,加载一些与文件相关的类,比如压缩文件的类,图片的类等等。最后,才用applicationClassLoader,加载用户自定义的类。加载当前类信息反射正式利用了Class来创建、修改对象,获取和修改属性的值等等。那么,反射是怎么创建当前类的呢?第一种,可以使用当前上下文的类路径来创建对象,如我们记载jdbc类驱动的时候,如以下代码:/* * Created By zby on 18:07 2019/3/16 * 通过上下文的类路径来加载信息 /public static Class byClassPath(String classPath) { if (StringUtils.isBlank(classPath)) { throw new RuntimeException(“类路径不能为空”); } classPath = classPath.replace(" “, “”); try { return Class.forName(classPath); } catch (ClassNotFoundException e) { e.printStackTrace(); } return null;}第二种,通过类字面常量,这种做法非常简单,而且更安全。因为,他在编译时就会受到检查,我们不需要将其置于try catch的代码快中,而且,它根除了对forName的方法调用,所以,更高效。这种是spring、hibernate等主流框架使用的。框架hibernate的内部使用类字面常量去创建对象后,底层通过jdbc获取数据表的字段值,根据数据表的字段与当前类的属性进行一一匹配,将字段值填充到当前对象中。匹配不成功,就会报出相应的错误。类字面常量获取对象信息,如代码所示。下文,也是通过类字面常量创建对象。 /* * Created By zby on 18:16 2019/3/16 * 通过类字面常量加载当前类的信息 /public static void byClassConstant() { System.out.println(Dog.class);}第三种,是通过对象来创建当前类,这种会在框架内部使用。/** Created By zby on 18:17 2019/3/16* 通过类对象加载当前类的信息*/public static Class byCurrentObject(Object object) { return object.getClass();}反射创建当前类对象我们创建当前对象,一般有两种方式,一种是通过clazz.newInstance();这种一般是无参构造器,并且创建对对象后,可以获取其属性,通过属性赋值和方法赋值,如如代码所示:第一种,通过clazz.newInstance()创建对象/** * Created By zby on 18:26 2019/3/16 * 普通的方式创建对象 /public static <T> T byCommonGeneric(Class clazz, String name, BaseEnum baseEnum) { if (null == clazz) { return null; } try { T t = (T) clazz.newInstance(); //通过属性赋值,getField获取公有属性,获取私有属性 Field field = clazz.getDeclaredField(“name”); //跳过检查,否则,我们没办法操作私有属性 field.setAccessible(true); field.set(t, name); //通过方法赋值 Method method1 = clazz.getDeclaredMethod(“setFood”, String.class); method1.setAccessible(true); method1.invoke(t, baseEnum.getTitle()); return t; } catch (Exception e) { e.printStackTrace(); } return null;}测试: @Testpublic void testCommonGeneric() { Dog dog= GenericCurrentObject.byCommonGeneric(Dog.class, “宠物狗哈士奇”, FoodTypeEnum.FOOD_TYPE_BONE); FeedingPet.eatWhatToday(DateTypeEnum.DATE_TYPE_NOON,dog);}叔叔出结果为:你会发现一个神奇的地方,就是名字没有输出来,但我们写了名字呀,为什么没有输出来?因为,dog是继承了父类Pet,当我们在创建子类对象时,首先,会加载父类未加载的构造器、静态代码块、静态属性、静态方法等等。但是,Dog在这里是以无参构造器加载的,当然,同时也通过无参构造器的实例化了父类。我们在给dog对象的name赋值时,、并没有给父类对象的name赋值,所以,dog的name是没有值的。父类引用指向子类对象,就是这个意思。如果我们把Dog类中的 @Override public void setFood(String food) {super.setFood(food); }的super.setFood(food); 方法去掉,属性food也是没有值的。如图所示:通过构造器创建对象 /* * Created By zby on 18:26 2019/3/16 * 普通的方式创建对象 */ public static <T> T byConstruct(Class clazz, String name, BaseEnum baseEnum) { if (null == clazz) { return null; }// 参数类型, Class paramType[] = {String.class, String.class}; try {// 一般情况下,构造器不止一个,我们根据构器的参数类型,来使用构造器创建对象 Constructor constructor = clazz.getConstructor(paramType);// 给构造器赋值,赋值个数和构造器的形参个数一样,否则,会报错 return (T) constructor.newInstance(name, baseEnum.getTitle()); } catch (Exception e) { e.printStackTrace(); } return null; } 测试: @Test public void testConstruct() { Dog dog= GenericCurrentObject.byConstruct(Dog.class, “宠物狗哈士奇”, FoodTypeEnum.FOOD_TYPE_BONE); System.out.println(“输出宠物的名字:"+dog.getName()+"\n”); System.out.println(“宠物吃的什么:"+dog.getFood()+"\n”); FeedingPet.eatWhatToday(DateTypeEnum.DATE_TYPE_MIDNIGHT_SNACK,dog); }测试结果:这是通过构造器创建的对象。但是注意的是,形参类型和和参数值的位数一定要相等,否则,就会报出错误的。总结为什么写这篇文章,前面也说了,很多框架都用到了反射和RTTI。但是,我们的平常的工作,一般以业务为主。往往都是使用别人封装好的框架,比如spring、hibernate、mybatis、beanutils等框架。所以,我们不大会关注反射,但是,你如果想要往更高的方向去攀登,还是要把基础给打捞。否则,基础不稳,爬得越高,摔得越重。我会以后的篇章中,通过介绍我写的spring、hibernate框架,来讲解更好地讲解反射。 ...

March 16, 2019 · 2 min · jiezi

基本功 | Litho的使用及原理剖析

什么是Litho?Litho是Facebook推出的一套高效构建Android UI的声明式框架,主要目的是提升RecyclerView复杂列表的滑动性能和降低内存占用。下面是Litho官网的介绍:Litho is a declarative framework for building efficient user interfaces (UI) on Android. It allows you to write highly-optimized Android views through a simple functional API based on Java annotations. It was primarily built to implement complex scrollable UIs based on RecyclerView.With Litho, you build your UI in terms of components instead of interacting directly with traditional Android views. A component is essentially a function that takes immutable inputs, called props, and returns a component hierarchy describing your user interface.Litho是高效构建Android UI的声明式框架,通过注解API创建高优的Android视图,非常适用于基于Recyclerview的复杂滚动列表。Litho使用一系列组件构建视图,代替了Android传统视图交互方式。组件本质上是一个函数,它接受名为Props的不可变输入,并返回描述用户界面的组件层次结构。Litho是一套完全不同于传统Android的UI框架,它继承了Facebook一向大胆创新的风格,突破性地在Android上实现了React风格的UI框架。架构图如下:应用层:上层Android应用接入层。规范层(API):允许用户使用声明式的API(注解)来构建符合Flexbox规范的布局。布局层:Litho使用可挂载组件、布局组件和Flexbox组件来构建布局,其中可挂载组件和布局组件允许用户使用规范来定义,各个组件的具体用法下面的组件规范中会详细介绍。在Litho中每一个组件都是一个独立的功能模块。Litho的组件和React的组件相类似,也具有属性和状态的概念,通过状态的变更来控制组件的展示样式。布局测量:Litho使用Yoga来完成组件布局的异步或同步(可根据场景定制)测量和计算,实现了布局的扁平化。布局渲染:Litho不仅支持使用View来渲染视图,还可以使用更轻量的Drawable来渲染视图。Litho实现了大量使用Drawable来渲染的基础组件,可以进一步拍平布局。除了上面提到的扁平化布局,Litho还实现了布局的细粒度复用和异步计算布局的能力,对于这些功能的实现在Litho的特性及原理剖析中详细介绍。下面先介绍一下大家比较关心的Litho使用方法。2. Litho的使用Litho的使用方式相比于传统的Android来说有些另类,它抛弃了通过XML定义布局的方式,采用声明式的组件在Java中构建布局。2.1 Litho和原生Android在使用上的区别Android传统布局:首先在资源文件res/layout目录下定义布局文件xx.xml,然后在Activity或Fragment中引用布局文件生成视图,示例如下:<?xml version=“1.0” encoding=“utf-8”?><TextView xmlns:android=“http://schemas.android.com/apk/res/android" android:layout_width=“wrap_content” android:layout_height=“wrap_content” android:text=“Hello World” android:textAlignment=“center” android:textColor="#666666” android:textSize=“40dp” />public class MainActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.helloworld); }}Litho布局:Litho抛弃了Android原生的布局方式,通过组件方式构建布局生成视图,示例如下:public class MainActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ComponentContext context = new ComponentContext(this); final Text.Builder builder = Text.create(context); final Component = builder.text(“Hello World”) .textSizeDip(40) .textColor(Color.parseColor("#666666")) .textAlignment(Layout.Alignment.ALIGN_CENTER) .build(); LithoView view = LithoView.create(context, component); setContentView(view); }}2.2 Litho自定义视图Litho中的视图单元叫做Component,可以直观的翻译为“组件”,它的设计理念来自于React组件化的思想。每个组件持有描述一个视图单元所必须的属性和状态,用于视图布局的计算工作。视图最终的绘制工作是由组件指定的绘制单元(View或者Drawable)来完成的。Litho组件的创建方式也和原生View的创建方式有着很大的区别。Litho使用注解定义了一系列的规范,我们需要使用Litho的注解来定义自己的组件生成规则,最终由Litho在编译期自动编译生成真正的组件。2.2.1 组件规范Litho提供了两种类型的组件规范,分别是Layout Spec规范和Mount Spec规范。下面分别介绍两种规范的使用方式:Layout Spec规范:用于生成布局类型组件的规范,布局组件在逻辑上等同于Android中的ViewGroup,用于组织其他组件构成一个布局。它要求我们必须使用@LayoutSpec注解来注明,并实现一个标注了@OnCreateLayout注解的方法。示例如下:@LayoutSpecclass HelloComponentSpec { @OnCreateLayout static Component onCreateLayout(ComponentContext c, @Prop String name) { return Column.create(c) .child(Text.create(c) .text(“Hello, " + name) .textSizeRes(R.dimen.my_text_size) .textColor(Color.BLACK) .paddingDip(ALL, 10) .build()) .child(Image.create(c) .drawableRes(R.drawable.welcome) .scaleType(ImageView.ScaleType.CENTER_CROP) .build()) .build(); }}最终Litho会在编译时生成一个名为HelloComponent的组件。public final class HelloComponent extends Component { @Prop(resType = ResType.NONE,optional = false) String name; private HelloComponent() { super(); } @Override protected Component onCreateLayout(ComponentContext c) { return (Component) HelloComponentSpec.onCreateLayout((ComponentContext) c, (String) name); } … public static Builder create(ComponentContext context, int defStyleAttr, int defStyleRes) { Builder builder = sBuilderPool.acquire(); if (builder == null) { builder = new Builder(); } HelloComponent instance = new HelloComponent(); builder.init(context, defStyleAttr, defStyleRes, instance); return builder; } public static class Builder extends Component.Builder<Builder> { private static final String[] REQUIRED_PROPS_NAMES = new String[] {“name”}; private static final int REQUIRED_PROPS_COUNT = 1; HelloComponent mHelloComponent; … public Builder name(String name) { this.mHelloComponent.name = name; mRequired.set(0); return this; } @Override public HelloComponent build() { checkArgs(REQUIRED_PROPS_COUNT, mRequired, REQUIRED_PROPS_NAMES); HelloComponent helloComponentRef = mHelloComponent; release(); return helloComponentRef; } }}Mount Spec规范:用来生成可挂载类型组件的规范,用来生成渲染具体View或者Drawable的组件。同样,它必须使用@MountSpec注解来标注,并至少实现一个标注了@onCreateMountContent的方法。Mount Spec相比于Layout Spec更复杂一些,它拥有自己的生命周期:@OnPrepare,准备阶段,进行一些初始化操作。@OnMeasure,负责布局的计算。@OnBoundsDefined,在布局计算完成后挂载视图前做一些操作。@OnCreateMountContent,创建需要挂载的视图。@OnMount,挂载视图,完成布局相关的设置。@OnBind,绑定视图,完成数据和视图的绑定。@OnUnBind,解绑视图,主要用于重置视图的数据相关的属性,防止出现复用问题。@OnUnmount,卸载视图,主要用于重置视图的布局相关的属性,防止出现复用问题。除了上述两种组件类型,Litho中还有一种特殊的组件——Layout,它不能使用规范来生成。Layout是Litho中的容器组件,类似于Android中的ViewGroup,但是只能使用Flexbox的规范。它可以包含子组件节点,是Litho各组件连接的纽带。Layout组件只是Yoga在Litho中的代理,组件的所有布局相关的属性都会直接设置给Yoga,并由Yoga完成布局的计算。Litho实现了两个Layout组件Row和Column,分别对应Flexbox中的行和列。2.2.2 Litho的属性在Litho中属性分为两种,不可变属性称为Props,可变属性称为State,下面分别介绍一下两种属性:Props属性:组件中使用@Prop注解标注的参数集合,具有单向性和不可变性。下面通过一个简单的例子了解一下如何在组件中定义和使用Props属性: @MountSpec class MyComponentSpec { @OnPrepare static void onPrepare( ComponentContext c, @Prop(optional = true) String prop1) { … } @OnMount static void onMount( ComponentContext c, SomeDrawable convertDrawable, @Prop(optional = true) String prop1, @Prop int prop2) { if (prop1 != null) { … } } }在上面的代码中,共使用了三次Prop注解,分别标注prop1和prop2两个变量,即定义了prop1和prop2两个属性。Litho会在自动编译生成的MyComponent类的Builder类中生成这两个属性的同名方法。按照如下代码,便可以去使用上面定义的属性: MyComponent.create(c) .prop1(“My prop 1”) .prop2(256) .build();State属性:意为“状态”属性,State属性虽然可变,但是其变化由组件内部控制,例如:输入框、Checkbox等都是由组件内部去感知用户行为,并更新组件的State属性。所以一个组件一旦创建,我们便无法通过任何外部设置去更改它的属性。组件的State属性虽然不允许像Props属性那样去显式设置,但是我们可以定义一个单独的Props属性来当做某个State属性的初始值。3. Litho的特性及原理剖析Litho官网首页通过4个段落重点介绍了Litho的4个特性。3.1 声明式组件Litho采用声明式的API来定义UI组件,组件通过一组不可变的属性来描述UI。这种组件化的思想灵感来源于React,关于声明式组件的用法上面已经详细介绍过了。传统Android布局因为UI与逻辑分离,所以开发工具都有强大的预览功能,方便开发者调整布局。而Litho采用React组件化的思想,通过组件连接了逻辑与布局UI,虽然Litho也提供了对Stetho的支持,借助于Chrome开发者工具对界面进行调试,不过使用起来并没有那么方便。3.2 异步布局Android系统在绘制时为了防止页面错乱,页面所有View的测量(Measure)、布局(Layout)以及绘制(Draw)都是在UI线程中完成的。当页面UI非常复杂、视图层级较深时,难免Measure和Layout的时间会过长,从而导致页面渲染时候丢帧出现卡顿情况。Litho为解决该问题,提出了异步布局的思想,利用CPU的闲置时间提前在异步线程中完成Measure和Layout的过程,仅在UI线程中完成绘制工作。当然,Litho只是提供了异步布局的能力,它主要使用在RecyclerView等可以提前知道下一个视图长什么样子的场景。3.2.1 异步布局原理剖析针对RecyclerView等滑动列表,由于可以提前知道接下来要展示的一个甚至多个条目的视图样式,所以只要提前创建好下一个或多个条目的视图,就可以提前完成视图的布局工作。那么Android原生为什么不支持异步布局呢?主要有以下两个原因:View的属性是可变的,只要属性发生变化就可能导致布局变化,因此需要重新计算布局,那么提前计算布局的意义就不大了。而Litho组件的属性是不可变的,所以对于一个组件来说,它的布局计算结果是唯一且不变的。提前异步布局就意味着要提前创建好接下来要用到的一个或者多个条目的视图,而Android原生的View作为视图单元,不仅包含一个视图的所有属性,而且还负责视图的绘制工作。如果要在绘制前提前去计算布局,就需要预先去持有大量未展示的View实例,大大增加内存占用。反观Litho的组件则没有这个问题,Litho的组件只是视图属性的一个集合,仅负责计算布局,绘制工作由指定的绘制单元来完成,相比与传统的View显然Litho的组件要轻量的多。所以在Litho中,提前创建好接下来要用到的多个条目的组件,并不会带来性能问题,甚至还可以直接把组件当成滑动列表的数据源。如下图所示:3.3 扁平化的视图使用Litho布局,我们可以得到一个极致扁平的视图效果。它可以减少渲染时的递归调用,加快渲染速度。下面是同一个视图在Android和Litho实现下的视图层级效果对比。可以看到,同样的样式,使用Litho实现的布局要比使用Android原生实现的布局更加扁平。3.3.1 扁平化视图原理剖析Litho使用Flexbox来创建布局,最终生成带有层级结构的组件树。然后Litho对布局层级进行了两次优化。使用了Yoga来进行布局计算,Yoga会将Flexbox的相对布局转成绝对布局。经过Yoga处理后的布局没有了原来的布局层级,变成了只有一层。虽然不能解决过度绘制的问题,但是可以有效地减少渲染时的递归调用。前面介绍过Litho的视图渲染由绘制单元来完成,绘制单元可以是View或者更加轻量的Drawable,Litho自己实现了一系列挂载Drawable的基本视图组件。通过使用Drawable可以减少内存占用,同时相比于View,Android无法检查出Drawable的视图层级,这样可以使视图效果看起来更加扁平。原理如下图所示,Litho会先把组件树拍平成没有层级的列表,然后使用Drawable来绘制对应的视图单元。Litho使用Drawable代替View能带来多少好处呢?Drawable和View的区别在于前者不能和用户交互,只能展示,因此Drawable不会像View那样持有很多变量和引用,所以Drawable比View从内存上看要轻量很多。举个例子:50个同样展示“Hello world”的TextView和TextDrawable在内存占比上,前者几乎是后者的8倍。对比图如下,Shallow Size表示对象自身占用的内存大小。3.3.2 绘制单元的降级策略由于Drawable不具有交互能力,所以对于使用Drawable无法实现的交互场景,Litho会自动降级成View。主要有以下几种场景:有监听点击事件。限制子视图绘出父布局。有监听焦点变化。有设置Tag。有监听触摸事件。有光影效果。对于以上场景的使用请仔细考虑,过多的使用会导致Litho的层级优化效果变差。3.3.3 对比Android的约束布局为了解决布局嵌套问题,Android推出了约束布局(ConstraintLayout),使用约束布局也可以达到扁平化视图的目的,那么使用Litho的好处是什么呢?Litho可以更好地实现复杂布局。约束布局虽然可以实现扁平效果,但是它使用了大量的约束来固定视图的位置。随着布局复杂程度的增加,约束条件变得越来越多,可读性也变得越来越差。而Litho则是对Flexbox布局进行的扁平化处理,所以实际使用的还是Flexbox布局,对于复杂的布局Flexbox布局可读性更高。3.4 细粒度的复用Litho中的所有组件都可以被回收,并在任何位置进行复用。这种细粒度的复用方式可以极大地提高内存使用率,尤其适用于复杂滑动列表,内存优化非常明显。3.4.1 原生RecyclerView复用原理剖析原生的RecyclerView视图按模板类型进行存储并复用,也就是说模板类型越多,所需存储的模板种类也就越多,导致内存占用越来越大。原理如下图。滑出屏幕的itemType1和itemType2都会在Recycler缓存池保存,等待后面滑进屏幕的条目的复用。3.4.2 细粒度复用优化内存原理剖析在Litho中,item在回收前,会把LithoView中挂载的各个绘制单元拆分出来(解绑),由Litho自己的缓存池去分类回收,在展示前由LithoView按照组件树的样式组装(挂载)各个绘制单元,这样就达到了细粒度复用的目的。原理如下图。滑出屏幕的itemType1会被拆分成一个个的视图单元。LithoView容器由Recycler缓存池回收,其他视图单元由Litho的缓存池分类回收。使用细粒度复用的RecyclerView的缓存池不再需要区分模板类型来缓存大量的视图模板,只需要缓存LithoView容器。细粒度回收的视图单元数量要远远小于原来缓存在各个视图模板中的视图单元数量。4. 实践美团对Litho进行了二次开发,在美团的MTFlexbox动态化实现方案(简称动态布局)中把Litho作为底层UI渲染引擎来使用。通过动态布局的预览工具,为Litho提供实时预览能力,同时可以有效发挥Litho的性能优化效果。目前Litho+动态布局的实现方案已经应用在了美团App中,给美团App带来了不错的性能提升。后续博主会详细介绍Litho+动态布局在美团性能优化的实践方案。4.1 内存数据由于Litho中使用了大量Drawable替换View,并且实现了视图单元的细粒度复用,因此复杂列表滑动时内存优化比较明显。美团首页内存占用随滑动页数变化走势图如下。随着一页一页地滑动,内存优化了30M以上。(数据采集自Vivo x20手机内存占用情况)4.2 FPS数据FPS的提升主要得益于Litho的异步布局能力,提前计算布局可以减少滑动时的帧率波动,所以滑动过程较平稳,不会有高低起伏的卡顿感。(数据采集自魅蓝2手机一段时间内连续fps的波动情况)5. 总结Litho相对于传统Android是颠覆式的,它采用了React的思路,使用声明式的API来编写UI。相比于传统Android,确实在性能优化上有很大的进步,但是如果完全使用Litho开发一款应用,需要自己实现很多组件,而Litho的组件需要在编译时生成,实时预览方面也有所欠缺。相对于直接使用Litho的高成本,把Litho封装成Flexbox布局的底层渲染引擎是个不错的选择。6. 参考资料Litho官网说一说 Facebook 开源的 LithoReact官网Yoga官网7. 作者简介何少宽,美团Android开发工程师,2015年加入美团,负责美团平台终端业务研发工作。张颖,美团Android开发工程师,2017年加入美团,负责美团平台终端业务研发工作。

March 15, 2019 · 2 min · jiezi

微信小程序框架wepy踩坑记录(与vue对比)

wepy框架借鉴了vue的语法风格和功能特性,但是在使用过程中还是发现与vue有很大的不同。现在总结一下自己开发中遇到的问题,共大家参考一下。如果第一次用wepy开发,强烈建议仔细阅读一下这篇文章,一定对你有帮助,会帮你节约很多宝贵的时间。开发过程中也建议大家时不时的回来阅读一次,巩固加强记忆。wepy中的组件组件里面的坑还不是一般的多!首先来说说组件间的数据共享。在vue中你也能做到这一点,只要把 data 写成一个对象就可以了,当然你不想让所有的子组件都共享同一份数据,vue中的解决方案是给 data 写成一个函数就好了,return出来所有的数据,这样组件间的数据就不会共享了。但是wepy中不能。文档中介绍:WePY中的组件都是静态组件,是以组件ID作为唯一标识的,每一个ID都对应一个组件实例,当页面引入两个相同ID的组件时,这两个组件共用同一个实例与数据,当其中一个组件数据变化时,另外一个也会一起变化。所以如果同一个页面引用多个组件,你只能给每个组件定义不同的ID,类似这样import Child from ‘../components/child’; export default class Index extends wepy.page { components = { //为两个相同组件的不同实例分配不同的组件ID,从而避免数据同步变化的问题 child: Child, anotherchild: Child }; }看起来是不是很蠢。但是没办法,你只能这么用。如果页面中只引用两三个同类型组件还好,但是如果我是一个循环,我也不知道我要引用多少组件,该怎么办?接下来再说说组件的循环。wepy官方文档中说明:当需要循环渲染WePY组件时(类似于通过wx:for循环渲染原生的wxml标签),必须使用WePY定义的辅助标签<repeat>。但是不支持在 repeat 的组件中去使用 props, computed, watch 等等特性。什么?props 都不让用??那父组件如何给子组件传参??后来实践发现,如果 props 中的数据在 template 中是能取到的,但是在 method 或者event 中就取不到了,你说神不神奇!所以最后的解决办法是用的 wepy-redux,类似vuex,放在 store 中实现的。视图的渲染之异步数据异步数据的获取后需要手动调用 this.$apply() 方法才能重新渲染视图,这一点也一定要记得。刚开始做的时候是在页面 data 中写的假数据,渲染的好好的。但是数据换成从接口读取后,死活视图出不来。琢磨了半天才想起来需要手动调用 this.$apply()。而 vue 是不需要这么做的。方法定义wepy中页面中的事件需要些在 methods 中,组件之间的处理函数需要写在 events 中,而自己写的自定义方法需要写在与 methods 同级中。不像vue,可以写在 methods 里。在 events 中写的函数,不需要在调用子组件的时候写在子组件中,子组件 $emit 会自动去 events 中寻找同名方法执行。这点也与vue不同。事件传参wepy优化了原生小程序在事件中的传参形式。比如页面中有一个方法,叫 getIndex,目的是取一个循环的 index 属性,在原生中需要额外定义一个 data-index 属性,然后在 getIndex 中通过event.currentTarget.dataset.index 来获取。而wepy中可以直接在事件里传递,但需要加上{{}},写成 getIndex({{index}})这样,这点也与vue不同。数据绑定这个是小程序原生方法中的不好点,wepy不能帮忙背这个锅。数据绑定也是使用 {{}},但是{{}} 里面只能进行简单的运算,具体支持哪些运算可以看官方文档。需求是一个列表,选中的变个样式,正常的思路就是选中的时候触发一个方法,将 index 赋值给 currentActive,在 {{}} 中判断如果 currentActive == index 就应用 active 样式,命名很简单的一个需求。但是写好了就是不好使,找了半天也没发现哪错了,最后看文档,原来是根本就不支持这种写法!!只支持简单的运算,这种不属于简单的范围!!最后的解决办法是弄了一个数组 arr,选中将对应位置置为 true,在 {{}} 判断 arr[index] 是否为 true 解决了这个问题。总结一句话:{{}} 一点也不强大。动态绑定classwepy中需要遵循小程序原生的绑定方式,与vue中也不同。在vue中,动态的和非动态的需要分别写,类似这样:<div class=“class-a” :class="{true ? ‘class-b’: ‘class-c’}"></div>。但是在wepy中,动态和非动态的可以写在一起,类似这样:<view class=“class-a {{true ? ‘class-b’ : ‘class-c’}}">mixin混合wepy中的 mixin 分为两种。对于组件data数据,compontents 组件,events 事件及其他自定义方法采用默认式混合,即如果组件中未定义这些东西,那么 mixin 中的将生效,如果组件中已经定义了,将以组件中定义的为主,mixin 中定义的不会生效。但对于 methods 事件及小程序页面事件,将采用兼容式混合,只要定义了就都会生效。但是先响应组件中定义的,再响应 mixin 中定义的。而vue组件中 methods 里的事件如果与 mixin 中的重名,会采用组件中的事件。而生命周期的钩子函数则是先响应 mixin 中的,在响应组件中的。注:以上问题均是采用wepy1.7.2的版本,祝大家开发愉快,少踩些坑。最后附上官方文档链接,供大家参考:小程序官方文档wepy官方文档 ...

March 11, 2019 · 1 min · jiezi

轻量级高性能PHP框架ycroute

YCRoutegithub: https://github.com/caohao-php…目录框架介绍运行环境代码结构路由配置过滤验签控制层加载器模型层数据交互dao层(可选)Redis缓存操作数据库操作配置加载公共类加载公共函数日志模块视图层RPC 介绍 - 像调用本地函数一样调用远程函数RPC ServerRPC ClientRPC 并行调用附录 - Core_Model 中的辅助极速开发函数框架介绍框架由3层架构构成,Controller、Model、View 以及1个可选的Dao层,支持PHP7,优点如下:1、框架层次分明,灵活可扩展至4层架构、使用简洁(开箱即用)、功能强大。2、基于 yaf 路由和 ycdatabase 框架,两者都是C语言扩展,保证了性能。3、ycdatabase 是强大的数据库 ORM 框架,功能强大,安全可靠,支持便捷的主从配置,支持稳定、强大的数据库连接池。具体参考 https://blog.csdn.net/caohao0...4、支持Redis代理,简便的主从配置,支持稳定的redis连接池。具体参考:https://blog.csdn.net/caohao0…5、强大的日志模块、异常捕获模块,便捷高效的类库、共用函数加载模块6、基于PHP7,代码缓存opcache。运行环境运行环境: PHP 7 依赖扩展: yaf 、 ycdatabase 扩展 创建日志目录:/data/app/logs ,目录权限为 php 项目可写。 yaf 介绍以及安装: https://github.com/laruence/yafycdatabase 介绍以及安装: https://github.com/caohao-php…代码结构———————————————— |— system //框架系统代码|— conf //yaf配置路径 |— application //业务代码 |—– config //配置目录 |—– controller //控制器目录 |—— User.php //User控制器 |—– core //框架基类目录 |—– daos //DAO层目录(可选) |—– errors //错误页目录 |—– helpers //公共函数目录 |—– library //公共类库目录 |—– models //模型层目录 |—– plugins //yaf路由插件目录,路由前后钩子,(接口验签在这里) |—– third //第三方类库 |—– views //视图层路由配置路由配置位于: framework/conf/application.ini示例: http://localhost/index.php?c=…详细参考文档: http://php.net/manual/zh/book…控制器由参数c决定,动作有 m 决定。参数方式描述cGET控制器,路由到 /application/controller/User.php 文件mGET入口方法, User.php 里面的 getUserInfoAction 方法程序将被路由到 framework/application/controllers/User.php文件的 UserController::getUserInfoAction方法,其它路由细节参考Yaf框架class UserController extends Core_Controller { public function getUserInfoAction() { } } 过滤验签framework/application/plugins/Filter.php , 在 auth 中写入验签方法,所有接口都会在这里校验, 所有GET、POST等参数放在 $this->params 里。class FilterPlugin extends Yaf_Plugin_Abstract { var $params; //路由之前调用 public function routerStartUp ( Yaf_Request_Abstract $request , Yaf_Response_Abstract $response) { $this->params = & $request->getParams(); $this->auth(); } //验签过程 protected function auth() { //在这里写你的验签逻辑 } …}控制层所有控制器位于:framework/application/controllers 目录,所有控制器继承自Core_Controller方法,里面主要获取GET/POST参数,以及返回数据的处理,Core_Controller继承自 Yaf_Controller_Abstract, init方法会被自动调用,更多细节参考 Yaf 框架控制器。class UserController extends Core_Controller { public function init() { parent::init(); //必须 $this->user_model = Loader::model(‘UserinfoModel’); //模型层 $this->util_log = Logger::get_instance(‘user_log’); //日志 Loader::helper(‘common_helper’); //公共函数 $this->sample = Loader::library(‘Sample’); //加载类库,加载的就是 framework/library/Sample.php 里的Sample类 } //获取用户信息接口 public function getUserInfoAction() { $userId = $this->params[‘userid’]; $token = $this->params[’token’]; if (empty($userId)) { $this->response_error(10000017, “user_id is empty”); } if (empty($token)) { $this->response_error(10000016, “token is empty”); } $userInfo = $this->user_model->getUserinfoByUserid($userId); if (empty($userInfo)) { $this->response_error(10000023, “未找到该用户”); } if (empty($token) || $token != $userInfo[’token’]) { $this->response_error(10000024, “token 校验失败”); } $this->response_success($userInfo); }}通过 $this->response_error(10000017, ‘user_id is empty’); 返回错误结果 { “errno”:10000017, “errmsg”:“user_id is empty”}通过 $this->response_success($result); 返回JSON格式成功结果,格式如下:{ “errno”:0, “union”:"", “amount”:0, “session_key”:“ZqwsC+Spy4C31ThvqkhOPg==”, “open_id”:“oXtwn4_mrS4zIxtSeV0yVT2sAuRo”, “nickname”:“凉之渡”, “last_login_time”:“2018-09-04 18:53:06”, “regist_time”:“2018-06-29 22:03:38”, “user_id”:6842811, “token”:“c9bea5dee1f49488e2b4b4645ff3717e”, “updatetime”:“2018-09-04 18:53:06”, “avatar_url”:“https://wx.qlogo.cn/mmopen/vi_32/xfxHib91BictV8T4ibRQAibD10DfoNpzpB1LBqZvRrz0icPkN0gdibZg62EPJL3KE1Y5wkPDRAhibibymnQCFgBM2nuiavA/132", “city”:“Guangzhou”, “province”:“Guangdong”, “country”:“China”, “appid”:“wx385863ba15f573b6”, “gender”:1, “form_id”:”"}加载器通过 Loader 加载器可以加载模型层,公共类库,公共函数,数据库,缓存等对象, Logger 为日志类。模型层framework/application/models/Userinfo.php ,模型层,你可以继承自Core_Model, 也可以不用,Core_Model 中封装了许多常用SQL操作。最后一章会介绍各个函数用法。通过 $this->user_model = Loader::model(‘UserinfoModel’) 加载模型层,模型层与数据库打交道。class UserinfoModel extends Core_Model { public function construct() { $this->db = Loader::database(‘default’); $this->util_log = Logger::get_instance(‘userinfo_log’); } function register_user($appid, $userid, $open_id, $session_key) { $data = array(); $data[‘appid’] = $appid; $data[‘user_id’] = $userid; $data[‘open_id’] = $open_id; $data[‘session_key’] = $session_key; $data[’last_login_time’] = $data[‘regist_time’] = date(‘Y-m-d H:i:s’, time()); $data[’token’] = md5(TOKEN_GENERATE_KEY . time() . $userid . $session_key); $ret = $this->db->insert(“user_info”, $data); if ($ret != -1) { return $data[’token’]; } else { $this->util_log->LogError(“error to register_user, DATA=[".json_encode($data).”]"); return false; } } …}数据交互Dao层(可选)如果你习惯了4层结构,你可以加载Dao层,作为与数据库交互的层,而model层作为业务层。这个时候 Model 最好不要继承 Core_Model,而由 Dao 层来继承。framework/application/daos/UserinfoDao.php ,数据库交互层,你可以继承自Core_Model, 也可以不用,Core_Model 中封装了许多常用SQL操作。最后一章会介绍各个函数用法。通过 $this->user_dao = Loader::dao(‘UserinfoDao’) 加载dao层,我们建议一个数据库对应一个Dao层。redis 缓存操作加载 redis 缓存: Loader::redis(‘default_master’); 参数为framework/application/config/redis.php 配置键值,如下:$redis_conf[‘default_master’][‘host’] = ‘127.0.0.1’;$redis_conf[‘default_master’][‘port’] = 6379;$redis_conf[‘default_slave’][‘host’] = ‘/tmp/redis_pool.sock’; //unix socket redis连接池,需要配置 openresty-pool/conf/nginx.conf,并开启代理,具体参考 https://blog.csdn.net/caohao0591/article/details/85679702$redis_conf['userinfo']['host'] = ‘127.0.0.1’;$redis_conf[‘userinfo’][‘port’] = 6379;return $redis_conf;使用例子:$redis = Loader::redis(“default_master”); //主写$redis->set(“pre_redis_user${userid}”, serialize($result));$redis->expire(“pre_redis_user${userid}”, 3600);$redis = Loader::redis(“default_slave”); //从读$data = $redis->get(“pre_redis_user${userid}”);连接池配置 openresty-pool/conf/nginx.conf :worker_processes 1; #nginx worker 数量error_log logs/error.log; #指定错误日志文件路径events { worker_connections 1024;}stream { lua_code_cache on; lua_check_client_abort on; server { listen unix:/tmp/redis_pool.sock; content_by_lua_block { local redis_pool = require “redis_pool” pool = redis_pool:new({ip = “127.0.0.1”, port = 6380, auth = “password”}) pool:run() } } server { listen unix:/var/run/mysql_sock/mysql_user_pool.sock; content_by_lua_block { local mysql_pool = require “mysql_pool” local config = {host = “127.0.0.1”, user = “root”, password = “test123123”, database = “userinfo”, timeout = 2000, max_idle_timeout = 10000, pool_size = 200} pool = mysql_pool:new(config) pool:run() } }}数据库操作数据库加载: Loader::database(“default”); 参数为 framework/application/config/database.php 里配置键值,如下:$db[‘default’][‘unix_socket’] = ‘/var/run/mysql_sock/mysql_user_pool.sock’; //unix socket 数据库连接池,具体使用参考 https://blog.csdn.net/caohao0591/article/details/85255704$db['default']['pconnect'] = FALSE;$db[‘default’][‘db_debug’] = TRUE;$db[‘default’][‘char_set’] = ‘utf8’;$db[‘default’][‘dbcollat’] = ‘utf8_general_ci’;$db[‘default’][‘autoinit’] = FALSE;$db[‘payinfo_master’][‘host’] = ‘127.0.0.1’; //地址$db[‘payinfo_master’][‘username’] = ‘root’; //用户名$db[‘payinfo_master’][‘password’] = ’test123123’; //密码$db[‘payinfo_master’][‘dbname’] = ‘payinfo’; //数据库名$db[‘payinfo_master’][‘pconnect’] = FALSE; //是否连接池$db[‘payinfo_master’][‘db_debug’] = TRUE; //debug标志,线上关闭,打开后,异常SQL会显示到页面,不安全,仅在测试时打开,(注意,上线一定得将 db_debug 置为 FALSE,否则一定概率可能暴露数据库配置)$db[‘payinfo_master’][‘char_set’] = ‘utf8’;$db[‘payinfo_master’][‘dbcollat’] = ‘utf8_general_ci’;$db[‘payinfo_master’][‘autoinit’] = FALSE; //自动初始化,Loader的时候就连接,建议关闭$db[‘payinfo_master’][‘port’] = 3306;$db[‘payinfo_slave’][‘host’] = ‘192.168.0.7’;$db[‘payinfo_slave’][‘username’] = ‘root’;$db[‘payinfo_slave’][‘password’] = ’test123123’;$db[‘payinfo_slave’][‘dbname’] = ‘payinfo’;$db[‘payinfo_slave’][‘pconnect’] = FALSE;$db[‘payinfo_slave’][‘db_debug’] = TRUE;$db[‘payinfo_slave’][‘char_set’] = ‘utf8’;$db[‘payinfo_slave’][‘dbcollat’] = ‘utf8_general_ci’;$db[‘payinfo_slave’][‘autoinit’] = FALSE;$db[‘payinfo_slave’][‘port’] = 3306;原生SQL:$data = $this->db->query(“select * from user_info where country=‘China’ limit 3”);查询多条记录:$data = $this->db->get(“user_info”, [‘regist_time[<]’ => ‘2018-06-30 15:48:39’, ‘gender’ => 1, ‘country’ => ‘China’, ‘city[!]’ => null, ‘ORDER’ => [ “user_id”, “regist_time” => “DESC”, “amount” => “ASC” ], ‘LIMIT’ => 10], “user_id,nickname,city”);echo json_encode($data);exit;[ { “nickname”:“芒果”, “user_id”:6818810, “city”:“Yichun” }, { “nickname”:“Smile、格调”, “user_id”:6860814, “city”:“Guangzhou” }, { “nickname”:“Yang”, “user_id”:6870818, “city”:“Hengyang” }, { “nickname”:“凉之渡”, “user_id”:7481824, “city”:“Guangzhou” }]查询单列$data = $this->db->get(“user_info”, [‘regist_time[<]’ => ‘2018-06-30 15:48:39’, ‘gender’ => 1, ‘country’ => ‘China’, ‘city[!]’ => null, ‘ORDER’ => [ “user_id”, “regist_time” => “DESC”, “amount” => “ASC” ], ‘LIMIT’ => 10], “nickname”);echo json_encode($data);exit;[ “芒果”, “Smile、格调”, “Yang”, “凉之渡”]查询单条记录$data = $this->db->get_one(“user_info”, [‘user_id’ => 6818810]);{ “union”:null, “amount”:0, “session_key”:“Et1yjxbEfRqVmCVsYf5qzA==”, “open_id”:“oXtwn4wkPO4FhHmkan097DpFobvA”, “nickname”:“芒果”, “last_login_time”:“2018-10-04 16:01:27”, “regist_time”:“2018-06-29 21:24:45”, “user_id”:6818810, “token”:“5a350bc05bbbd9556f719a0b8cf2a5ed”, “updatetime”:“2018-10-04 16:01:27”, “avatar_url”:“https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83epqg7FwyBUGd5xMXxLQXgW2TDEBhnNjPVla8GmKiccP0pFiaLK1BGpAJDMiaoyGHR9Nib2icIX9Na4Or0g/132", “city”:“Yichun”, “province”:“Jiangxi”, “country”:“China”, “appid”:“wx385863ba15f573b6”, “gender”:1, “form_id”:”" }插入数据function register_user($appid, $userid, $open_id, $session_key) { $data = array(); $data[‘appid’] = $appid; $data[‘user_id’] = $userid; $data[‘open_id’] = $open_id; $data[‘session_key’] = $session_key; $data[’last_login_time’] = $data[‘regist_time’] = date(‘Y-m-d H:i:s’, time()); $data[’token’] = md5(TOKEN_GENERATE_KEY . time() . $userid . $session_key); $ret = $this->db->insert(“user_info”, $data); if ($ret != -1) { return $data[’token’]; } else { $this->util_log->LogError(“error to register_user, DATA=[".json_encode($data).”]"); return false; }}更新数据function update_user($userid, $update_data) { $redis = Loader::redis(“userinfo”); $redis->del(“pre_redis_user_info” . $userid); $ret = $this->db->update(“user_info”, [“user_id” => $userid], $update_data); if ($ret != -1) { return true; } else { $this->util_log->LogError(“error to update_user, DATA=[".json_encode($update_data).”]"); return false; }}删除操作$ret = $this->db->delete(“user_info”, [“user_id” => 7339820]);更多操作参考通过 $this->db->get_ycdb(); 可以获取ycdb句柄进行更多数据库操作, ycdb 的使用教程如下:英文: https://github.com/caohao-php…中文: https://blog.csdn.net/caohao0…配置加载通过 Loader::config(‘xxxxx’); 加载 /application/config/xxxxx.php 的配置。例如:$config = Loader::config(‘config’);var_dump($config);公共类加载所有的公共类库位于superci/application/library目录,但是注意的是, 如果你的类位于library子目录下面,你的类必须用下划线"“分隔;$this->sample = Loader::library(‘Sample’);加载的就是 framework/application/library/Sample.php 中的 Sample类。$this->ip_location = Loader::library(‘Ip_Location’);加载的是 framework/application/library/Ip/Location.php 中的Ip_Location类公共函数所有的公共类库位于superci/application/helpers目录,通过 Loader::helper(‘common_helper’); 方法包含进来。日志日志使用方法如下:$this->util_log = Logger::get_instance(‘userinfo’);$this->util_log->LogInfo(“register success”);$this->util_log->LogError(“not find userinfo”);日志级别:const DEBUG = ‘DEBUG’; /* 级别为 1 , 调试日志, 当 DEBUG = 1 的时候才会打印调试 /const INFO = ‘INFO’; / 级别为 2 , 应用信息记录, 与业务相关, 这里可以添加统计信息 /const NOTICE = ‘NOTICE’; / 级别为 3 , 提示日志, 用户不当操作,或者恶意刷频等行为,比INFO级别高,但是不需要报告*/const WARN = ‘WARN’; /* 级别为 4 , 警告, 应该在这个时候进行一些修复性的工作,系统可以继续运行下去 /const ERROR = ‘ERROR’; / 级别为 5 , 错误, 可以进行一些修复性的工作,但无法确定系统会正常的工作下去,系统在以后的某个阶段, 很可能因为当前的这个问题,导致一个无法修复的错误(例如宕机),但也可能一直工作到停止有不出现严重问题 /const FATAL = ‘FATAL’; / 级别为 6 , 严重错误, 这种错误已经无法修复,并且如果系统继续运行下去的话,可以肯定必然会越来越乱, 这时候采取的最好的措施不是试图将系统状态恢复到正常,而是尽可能的保留有效数据并停止运行 /FATAL和ERROR级别日志文件以 .wf 结尾, DEBUG级别日志文件以.debug结尾,日志目录存放于 /data/app/localhost 下面,localhost为你的项目域名,比如:[root@gzapi: /data/app/logs/localhost]# lsuserinfo.20190211.log userinfo.20190211.log.wf日志格式: [日志级别] [时间] [错误代码] [文件|行数] [ip] [uri] [referer] [cookie] [统计信息] “内容”[INFO] [2019-02-11 18:57:01] - - [218.30.116.8] - - - [] “register success”[ERROR] [2019-02-11 18:57:01] [0] [index.php|23 => | => User.php|35 => Userinfo.php|93] [218.30.116.8] [/index.php?c=user&m=getUserInfo&userid=6842811&token=c9bea5dee1f49488e2b4b4645ff3717e] [] [] - “not find userinfo"VIEW层视图层参考yaf视图渲染那部分, 我没有写案例。RPC 介绍 - 像调用本地函数一样调用远程函数传统web应用弊端传统的Web应用, 一个应用随着业务快速增长, 开发人员的流转, 就会慢慢的进入一个恶性循环, 代码量上只有加法没有了减法. 因为随着系统变复杂, 牵一发就会动全局, 而新来的维护者, 对原有的体系并没有那么多时间给他让他全面掌握. 即使有这么多时间, 要想掌握以前那么多的维护者的思维的结合, 也不是一件容易的事情…那么, 长次以往, 这个系统将会越来越不可维护…. 到一个大型应用进入这个恶性循环, 那么等待他的只有重构了.那么, 能不能对这个系统做解耦呢? 我们已经做了很多解耦了, 数据, 中间件, 业务, 逻辑, 等等, 各种分层. 但到Web应用这块, 还能怎么分呢, MVC我们已经做过了….解决利器—微服务目前比较流行的解决方案是微服务,它可以让我们的系统尽可能快地响应变化,微服务是指开发一个单个小型的但有业务功能的服务,每个服务都有自己的处理和轻量通讯机制,可以部署在单个或多个服务器上。微服务也指一种种松耦合的、有一定的有界上下文的面向服务架构。也就是说,如果每个服务都要同时修改,那么它们就不是微服务,因为它们紧耦合在一起;如果你需要掌握一个服务太多的上下文场景使用条件,那么它就是一个有上下文边界的服务,这个定义来自DDD领域驱动设计。相对于单体架构和SOA,它的主要特点是组件化、松耦合、自治、去中心化,体现在以下几个方面:一组小的服务服务粒度要小,而每个服务是针对一个单一职责的业务能力的封装,专注做好一件事情。独立部署运行和扩展 每个服务能够独立被部署并运行在一个进程内。这种运行和部署方式能够赋予系统灵活的代码组织方式和发布节奏,使得快速交付和应对变化成为可能。独立开发和演化 技术选型灵活,不受遗留系统技术约束。合适的业务问题选择合适的技术可以独立演化。服务与服务之间采取与语言无关的API进行集成。相对单体架构,微服务架构是更面向业务创新的一种架构模式。独立团队和自治 团队对服务的整个生命周期负责,工作在独立的上下文中,自己决策自己治理,而不需要统一的指挥中心。团队和团队之间通过松散的社区部落进行衔接。我们可以看到整个微服务的思想就如我们现在面对信息爆炸、知识爆炸是一样的:通过解耦我们所做的事情,分而治之以减少不必要的损耗,使得整个复杂的系统和组织能够快速的应对变化。微服务的基石—RPC服务框架微服务包含的东西非常多,这里我们只讨论RPC服务框架,ycroute框架基于Yar扩展为我们提供了RPC跨网络的服务调用基础,Yar是一个非常轻量级的RPC框架, 使用非常简单, 对于Server端和Soap使用方法很像,而对于客户端,你可以像调用本地对象的函数一样,调用远程的函数。RPC Server安装环境 (客户端服务端都需要安装)扩展: yar.so 扩展: msgpack.so 可选,一个高效的二进制打包协议,用于客户端和服务端之间包传输,还可以选php、json, 如果要使用Msgpack做为打包协议, 就需要安装这个扩展。服务加载我们在 framework/application/controllers/Rpcserver.php 中将 Model 层作为服务,提供给远程的其它程序调用,RPC Client 便可以像调用本地函数一样,调用远程的服务,如下我们将 UserinfoModel 和 TradeModel 两个模型层提供给远程程序调用。class RpcserverController extends Core_Controller { public function init() { parent::init(); //必须 } //用户信息服务 public function userinfoModelAction() { $user_model = Loader::model(‘UserinfoModel’); //模型层 $yar_server = new Yar_server($user_model); $yar_server->handle(); exit; } //支付服务 public function tradeModelAction() { $trade_model = Loader::model(‘TradeModel’); //模型层 $yar_server = new Yar_server($trade_model); $yar_server->handle(); exit; }}上面一共提供了2个服务,UserinfoModel 和 TradeModel 分别通过http://localhost/index.php?c=… 和 http://localhost/index.php?c=… 来访问,我们来看看 UserinfoModel 一共有哪些服务:从上图可以看到,UserinfoModel 类的所有 public 方法都会被当做服务提供,包括他继承的父类 public 方法。服务校验为了安全,我们最好对客户端发起的RPC服务请求做校验。在 framework/application/plugins/Filter.php 中做校验:class FilterPlugin extends Yaf_Plugin_Abstract { var $params; //路由之前调用 public function routerStartUp ( Yaf_Request_Abstract $request , Yaf_Response_Abstract $response) { $this->params = & $request->getParams(); $this->_auth(); if(!empty($this->params[‘rpc’])) { $this->_rpc_auth(); //rpc 调用校验 } } //rpc调用校验 protected function rpc_auth() { $signature = $this->get_rpc_signature($this->params); if($signature != $this->params[‘signature’]) { $this->response_error(1, ‘check failed’); } } //rpc签名计算,不要改函数名,在RPC客户端中 system/YarClientProxy.php 我们也会用到这个函数,做签名。 public function get_rpc_signature($params) { $secret = ‘MJCISDYFYHHNKBCOVIUHFUIHCQWE’; unset($params[‘signature’]); ksort($params); reset($params); unset($auth_params[‘callback’]); unset($auth_params[’’]); $str = $secret; foreach ($params as $value) { $str = $str . trim($value); } return md5($str); } … }切记不要修改签名生成函数 get_rpc_signature 的名字和参数,因为在 RPC Client 我们也会利用这个函数做签名,如果需要修改,请在 system/YarClientProxy.php 中做相应修改,以保证客户端和服务器之间的调用正常。RPC Clientyar 除了支持 http 之外,还支持tcp, unix domain socket传输协议,不过ycroute中只用了 http ,当然 http 也可以开启 keepalive 以获得更高的传输性能,只不过相比 socket, http 协议还是多了不少的协议头部的开销。安装环境扩展: yar.so 扩展: msgpack.so 可选,一个高效的二进制打包协议,用于客户端和服务端之间包传输,还可以选php、json, 如果要使用Msgpack做为打包协议, 就需要安装这个扩展。调用逻辑例子:class UserController extends Core_Controller { … //获取用户信息(从远程) public function getUserInfoByRemoteAction() { $userId = $this->params[‘userid’]; if (empty($userId)) { $this->response_error(10000017, “user_id is empty”); } $model = Loader::remote_model(‘UserinfoModel’); $userInfo = $model->getUserinfoByUserid($userId); $this->response_success($userInfo); } …}通过 $model = Loader::remote_model(‘UserinfoModel’); 可以获取远程 UserinfoModel,参数是framework/application/config/rpc.php配置里的键值:$remote_config[‘UserinfoModel’][‘url’] = “http://localhost/index.php?c=rpcserver&m=userinfoModel&rpc=true”; //服务地址$remote_config[‘UserinfoModel’][‘packager’] = FALSE; //RPC包类型,FALSE则选择默认,可以为 “json”, “msgpack”, “php”, msgpack 需要安装扩展$remote_config[‘UserinfoModel’][‘persitent’] = FALSE; //是否长链接,需要服务端支持keepalive$remote_config[‘UserinfoModel’][‘connect_timeout’] = 1000; //连接超时(毫秒),默认 1秒 $remote_config[‘UserinfoModel’][’timeout’] = 5000; //调用超时(毫秒), 默认 5 秒$remote_config[‘UserinfoModel’][‘debug’] = TRUE; //DEBUG模式,调用异常是否会打印到屏幕,线上关闭$remote_config[‘TradeModel’][‘url’] = “http://localhost/index.php?c=rpcserver&m=tradeModel&rpc=true”;$remote_config[‘TradeModel’][‘packager’] = FALSE;$remote_config[‘TradeModel’][‘persitent’] = FALSE;$remote_config[‘TradeModel’][‘connect_timeout’] = 1000; $remote_config[‘TradeModel’][’timeout’] = 5000; $remote_config[‘TradeModel’][‘debug’] = TRUE; 这样,我们就可以把 model 当成本地对象一样调用远程 UserinfoModel 的成员方法。url签名调用远程服务的时候,system/YarClientProxy.php 会从配置中获取服务的 url, 然后调用 FilterPlugin::get_rpc_signature 方法对 URL 做签名,并将签名参数拼接到 url 结尾,发起调用。class YarClientProxy { … public static function get_signatured_url($url) { $get = array(); $t = parse_url($url, PHP_URL_QUERY); parse_str($t, $get); $get[’timestamp’] = time(); $get[‘auth’] = rand(11111111, 9999999999); $signature = FilterPlugin::get_rpc_signature($get); return $url . “&timestamp=” . $get[’timestamp’] . “&auth=” . $get[‘auth’] . “&signature=” . $signature; } …}调用异常日志日志位于 /data/app/logs/localhost 下,localhost 为项目域名。[root@gzapi: /data/app/logs/localhost]# lsyar_client_proxy.20190214.log.wf[ERROR] [2019-02-14 18:57:13] [0] [index.php|23 => | => User.php|61 => YarClientProxy.php|46] [218.30.116.3] [/index.php?c=user&m=getUserInfoByRemote&userid=6818810&token=c9bea5dee1f49488e2b4b4645ff3717e1] [] [] - “yar_client_call_error URL=[http://tr.gaoqu.site/index.ph…] , Remote_model=[UserinfoModel] Func=[getUserinfoByUserid] Exception=[server responsed non-200 code ‘500’]“RPC 并行调用yar框架支持并行调用,可以同时调用多个服务,这样可以充分利用CPU性能,避免IO等待,提升系统性能,按照yar的流程,你首先得一个个注册服务,然后发送注册的调用,然后reset 重置调用。在ycroute 中,一个函数就可以了。用 Loader::concurrent_call($call_params); 来并行调用RPC服务, 其中 call_params是调用参数数组。如下数组包含4个元素,每个调用都包含 model, method 两个必输参数,以及 parameters, callback , error_callback 三个可选参数。model : 服务名,是framework/application/config/rpc.php配置里的键值。method : 调用函数parameters : 函数的参数,是一个数组,数组的个数为参数的个数callback : 回调函数,调用成功之后回调,针对的是各自的回调。error_callback : 调用失败之后会回调这个函数,其中调用超时不会回调该方法, 针对的也是各自的回调。class UserController extends Core_Controller { //获取用户信息(并行远程调用) public function multipleGetUsersInfoByRemoteAction() { $userId = $this->params[‘userid’]; $call_params = array(); $call_params[] = [‘model’ => ‘UserinfoModel’, ‘method’ => ‘getUserinfoByUserid’, ‘parameters’ => array($userId), “callback” => array($this, ‘callback1’)]; $call_params[] = [‘model’ => ‘UserinfoModel’, ‘method’ => ‘getUserInUserids’, ‘parameters’ => array(array(6860814, 6870818)), “callback” => array($this, ‘callback2’), “error_callback” => array($this, ’error_callback’)]; $call_params[] = [‘model’ => ‘UserinfoModel’, ‘method’ => ‘getUserByName’, ‘parameters’ => array(‘CH.smallhow’)]; //不存在的方法 $call_params[] = [‘model’ => ‘UserinfoModel’, ‘method’ => ‘unknownMethod’, ‘parameters’ => array(), “error_callback” => array($this, ’error_callback’)]; Loader::concurrent_call($call_params); echo json_encode($this->retval); exit; } //回调函数1 public function callback1($retval, $callinfo) { $this->retval[‘callback1’][‘retval’] = $retval; $this->retval[‘callback1’][‘callinfo’] = $callinfo; } //回调函数2 public function callback2($retval, $callinfo) { $this->retval[‘callback2’][‘retval’] = $retval; $this->retval[‘callback2’][‘callinfo’] = $callinfo; } //错误回调 public function error_callback($type, $error, $callinfo) { $tmp[’type’] = $type; $tmp[’error’] = $error; $tmp[‘callinfo’] = $callinfo; $this->retval[’error_callback’][] = $tmp; }}我特意将第4个调用的method设置一个不存在的函数,大家可以看下上面的并行调用的结果:{ “error_callback”:[ { “type”:4, “error”:“call to undefined api ::unknownMethod()”, “callinfo”:{ “sequence”:4, “uri”:“http://tr.gaoqu.site/index.php?c=rpcserver&m=userinfoModel&rpc=true×tamp=1550142590&auth=5930400101&signature=fc0ed911c624d9176523544421a0248d”, “method”:“unknownMethod” } } ], “callback1”:{ “retval”:{ “user_id”:“6818810”, “appid”:“wx385863ba15f573b6”, “open_id”:“oXtwn4wkPO4FhHmkan097DpFobvA”, “union”:null, “session_key”:“Et1yjxbEfRqVmCVsYf5qzA==”, “nickname”:“芒果”, “city”:“Yichun”, “province”:“Jiangxi”, “country”:“China”, “avatar_url”:“https://wx.qlogo.cn/mmopen/vi_32/DYAIOgq83epqg7FwyBUGd5xMXxLQXgW2TDEBhnNjPVla8GmKiccP0pFiaLK1BGpAJDMiaoyGHR9Nib2icIX9Na4Or0g/132", “gender”:“1”, “form_id”:””, “token”:“5a350bc05bbbd9556f719a0b8cf2a5ed”, “amount”:“0”, “last_login_time”:“2018-10-04 16:01:27”, “regist_time”:“2018-06-29 21:24:45”, “updatetime”:“2018-10-04 16:01:27” }, “callinfo”:{ “sequence”:1, “uri”:“http://tr.gaoqu.site/index.php?c=rpcserver&m=userinfoModel&rpc=true×tamp=1550142590&auth=8384256613&signature=c0f9c944ae070d2eb38c8e9638723a2e”, “method”:“getUserinfoByUserid” } }, “callback2”:{ “retval”:{ “6860814”:{ “user_id”:“6860814”, “nickname”:“Smile、格调”, “avatar_url”:“https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKNE5mFLk33q690Xl1N6mrehQr0ggasgk8Y4cuaUJt4CNHORwq8rVjwET7H06F3aDjU5UiczjpD4nw/132", “city”:“Guangzhou” }, “6870818”:{ “user_id”:“6870818”, “nickname”:“Yang”, “avatar_url”:“https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTLTKBoU1tdRicImnUHyr43FdMulSHRhAlsQwuYgAyOlrwQaLGRoFEHbgfVuyEV1K1VU2NMmm0slS4w/132", “city”:“Hengyang” } }, “callinfo”:{ “sequence”:2, “uri”:“http://tr.gaoqu.site/index.php?c=rpcserver&m=userinfoModel&rpc=true×tamp=1550142590&auth=7249482640&signature=26c419450bb4747ac166fbaa4a242b77”, “method”:“getUserInUserids” } }}附录 - Core_Model 中的辅助极速开发函数(不关心可以跳过)$this->redis_conf_path = ‘default_master’; //用到快速缓存时,需要在 __construct 构造函数中加上 redis 缓存配置/* * 插入表记录 * @param string table 表名 * @param array data 表数据 * @param string redis_key redis 缓存键值, 可空, 非空时清理键值缓存 /public function insert_table($table, $data, $redis_key = “”);/* * 更新表记录 * @param string table 表名 * @param array where 查询条件 * @param array data 更新数据 * @param string redis_key redis 缓存键值, 可空, 非空时清理键值缓存 /public function update_table($table, $where, $data, $redis_key = “”);/* * 替换表记录 * @param string table 表名 * @param array data 替换数据 * @param string redis_key redis 缓存键值, 可空, 非空时清理键值缓存 /public function replace_table($table, $data, $redis_key = “”);/* * 删除表记录 * @param string table 表名 * @param array where 查询条件 * @param string redis_key redis缓存键值, 可空, 非空时清理键值缓存 /public function delete_table($table, $where, $redis_key = “”);/* * 获取表数据 * @param string table 表名 * @param array where 查询条件 * @param string redis_key redis 缓存键值, 可空, 非空时清理键值缓存 * @param int redis_expire redis 缓存到期时长(秒) * @param boolean set_empty_flag 是否标注空值,如果标注空值,在表记录更新之后,一定记得清理空值标记缓存 /public function get_table_data($table, $where = array(), $redis_key = “”, $redis_expire = 600, $set_empty_flag = true);/* * 根据key获取表记录 * @param string table 表名 * @param string key 键名 * @param string value 键值 * @param string redis_key redis 缓存键值, 可空, 非空时清理键值缓存 * @param int redis_expire redis 缓存到期时长(秒) * @param boolean set_empty_flag 是否标注空值,如果标注空值,在表记录更新之后,一定记得清理空值标记缓存 /public function get_table_data_by_key($table, $key, $value, $redis_key = “”, $redis_expire = 300, $set_empty_flag = true);/* * 获取一条表数据 * @param string table 表名 * @param array where 查询条件 * @param string redis_key redis 缓存键值, 可空, 非空时清理键值缓存 * @param int redis_expire redis 缓存到期时长(秒) * @param boolean set_empty_flag 是否标注空值,如果标注空值,在表记录更新之后,一定记得清理空值标记缓存 */public function get_one_table_data($table, $where, $redis_key = “”, $redis_expire = 600, $set_empty_flag = true); ...

March 9, 2019 · 9 min · jiezi

开源轻量级PHP数据库ORM框架ycdatabase : 构建稳定的PHP数据库连接池

ycdatabaseCatalogueInstructionRequirementCreate test tableCompire ycdatabase in linuxStart ycdatabaseInit ycdb connectionNative SQL queryError InfoWhere statementSelect statementInsert statementReplace statementUpdate statementDelete statementWhole ExampleDatabase TransactionData CachingPHP Database Connection PoolRedis Connection PoolInstruction1、Fast : ycdb is an mysql database ORM written in c, built in php extension, as we known, database ORM is a very time-consuming operation, especially for interpretive languages such as PHP, and for a project, the proportion of ORM is very high,so here I will implement the MySQL ORM operation in C language, and use the performance of C language to improve the performance of ORM. 2、Safe : ycdb can solve SQL injection through parameter binding. 3、Powerful : concise and powerful usage , support any operation in database. 4、Easy : Extremely easy to learn and use, friendly construction. 5、Data-cache : ycdb supports data caching. You can use redis as a medium to cache database data, but remember that when the update, insert, and delete operations involve caching data, you need to delete your cache to ensure data consistency. 6、Connection-pool : ycdb uses a special way to establish a stable connection pool with MySQL. performance can be increased by at least 30%, According to PHP’s operating mechanism, long connections can only reside on top of the worker process after establishment, that is, how many work processes are there. How many long connections, for example, we have 10 PHP servers, each launching 1000 PHP-FPM worker processes, they connect to the same MySQL instance, then there will be a maximum of 10,000 long connections on this MySQL instance, the number is completely Out of control! And PHP’s connection pool heartbeat mechanism is not perfect 1、快速 - ycdb是一个为PHP扩展写的纯C语言写的mysql数据库ORM扩展,众所周知,数据库ORM是一个非常耗时的操作,尤其对于解释性语言如PHP,而且对于一个项目来说,ORM大多数情况能占到项目很大的一个比例,所以这里我将MySQL的ORM操作用C语言实现,利用C语言的性能,提升ORM的性能。 2、安全 - ycdb能通过参数绑定的方式解决SQL注入的问题。 3、强大 - 便捷的函数,支持所有数据库操作。 4、简单 - 使用和学习非常简单,界面友好。 5、数据缓存 - ycdb支持数据缓存,你可以采用redis作为介质来缓存数据库的数据,但是记得在update、insert、delete 操作涉及到与缓存数据相关的数据修改时,需要按key删除您的缓存,以保证数据一致性。 6、连接池 - ycdb通过一种特殊的方式来建立一个稳定的与MySQL之间的连接池,性能至少能提升30%,按照 PHP 的运行机制,长连接在建立之后只能寄居在工作进程之上,也就是说有多少个工作进程,就有多少个长连接,打个比方,我们有 10 台 PHP 服务器,每台启动 1000 个 PHP-FPM 工作进程,它们连接同一个 MySQL 实例,那么此 MySQL 实例上最多将存在 10000 个长连接,数量完全失控了!而且PHP的连接池心跳机制不完善。中文文档(Chinese Document): https://blog.csdn.net/caohao0...RequirementPHP 7.0 +need support PDO for mysqlCreate test tableCREATE TABLE user_info_test ( uid int(11) NOT NULL COMMENT ‘userid’ AUTO_INCREMENT, username varchar(64) NOT NULL COMMENT ‘username’, sexuality varchar(8) DEFAULT ‘male’ COMMENT ‘sexuality:male - 男性 female - 女性’, age int(11) DEFAULT 0 COMMENT ‘age’, height double(11,2) DEFAULT 0 COMMENT ‘height of a person, 身高’, bool_flag int(11) DEFAULT 1 COMMENT ‘flag’, remark varchar(11) DEFAULT NULL, PRIMARY KEY (uid)) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT=‘userinfo’;Compire ycdatabase in linux//// path to is your PHP install dir ////$cd /ycdatabase/ycdatabase_extension$/path/to/phpize$chmod +x ./configure$./configure –with-php-config=/path/to/php-config$make$make installStart ycdatabasenew ycdb()$db_conf = array(“host” => “127.0.0.1”, “username” => “root”, “password” => “test123123”, “dbname” => “userinfo”, “port” => ‘3306’, “option” => array( PDO::ATTR_CASE => PDO::CASE_NATURAL, PDO::ATTR_TIMEOUT => 2));$ycdb = new ycdb($db_conf);we can start by creating a ycdatabase object (ycdb) from the obove code, db_conf include host,username,password,dbname,port and option, option is a pdo attribution, you can get the detail from http://php.net/manual/en/pdo…. For example, PDO::ATTR_TIMEOUT in the above code is specifies the timeout duration in seconds, and PDO::ATTR_CASE is forcing column names to a specific case.Init ycdb connectionwe need to init pdo connection before we use ycdatabase.try{ $ycdb->initialize();} catch (PDOException $e) { echo “find PDOException when initialize\n”; var_dump($e); exit;}Native SQL queryWe can directly execute the sql statement through the exec() function,the return value is the number of rows affected by the execution, or return insert_id if it is insert statement, when the table has not AUTO_INCREMENT field, the insert_id should be zero, and execute select statement through the query() function, If $ret = -1 indicates that the sql execution error occurs, we can pass $ycdb->errorCode(), $ycdb- >errorInfo() returns the error code and error description respectively.insert data$insert_id = $ycdb->exec(“insert into user_info_test(username, sexuality, age, height) values(‘smallhow’, ‘male’, 29, 180)”);if($insert_id == -1) { $code = $ycdb->errorCode(); $info = $ycdb->errorInfo(); echo “code:” . $code . “\n”; echo “info:” . $info[2] . “\n”;} else { echo $insert_id;}update dataif we execute the following update statement, $ret returns 3 if the current data is the above image.$ret = $ycdb->exec(“update user_info_test set remark=‘test’ where height>=180”);echo $ret; //ret is 3select data$ret = $ycdb->query(“select * from user_info_test where bool_flag=1”);echo json_encode($ret); $ret = $ycdb->query(“select username from user_info_test where bool_flag=1”);echo json_encode($ret);Error InfoError codes and error messages can be obtained through the errorCode and errorInfo function$code = $ycdb->errorCode();$info = $ycdb->errorInfo();Where statementBasic usage$ycdb->select(“user_info_test”, “”, [“sexuality” => “male”]);// WHERE sexuality = ‘male’$ycdb->select(“user_info_test”, “”, [“age” => 29]); // WHERE age = 29$ycdb->select(“user_info_test”, “”, [“age[>]” => 29]); // WHERE age > 29$ycdb->select(“user_info_test”, “”, [“age[>=]” => 29]); // WHERE age >= 29$ycdb->select(“user_info_test”, “”, [“age[!]” => 29]); // WHERE age != 29$ycdb->select(“user_info_test”, “”, [“age[<>]” => [28, 29]]); // WHERE age BETWEEN 28 AND 29$ycdb->select(“user_info_test”, “”, [“age[><]” => [28, 29]]); // WHERE age NOT BETWEEN 28 AND 29$ycdb->select(“user_info_test”, “”, [“username” => [“Tom”, “Red”, “carlo”]]); // WHERE username in (‘Tom’, ‘Red’, ‘carlo’)//Multiple conditional query$data = $ycdb->select(“user_info_test”, “”, [ “uid[!]” => 10, “username[!]” => “James”, “height[!]” => [165, 168, 172], “bool_flag” => true, “remark[!]” => null]);// WHERE uid != 10 AND username != “James” AND height NOT IN ( 165, 168, 172) AND bool_flag = 1 AND remark IS NOT NULLConditional QueryYou can use “AND” or “OR” to make up very complex SQL statements.$data = $ycdb->select(“user_info_test”, “”, [ “OR” => [ “uid[>]” => 3, “age[<>]” => [28, 29], “sexuality” => “female” ]]);// WHERE uid > 3 OR age BETWEEN 29 AND 29 OR sexuality = ‘female’$data = $ycdb->select(“user_info_test”, “”, [ “AND” => [ “OR” => [ “age” => 29, “sexuality” => “female” ], “height” => 177 ]]);// WHERE (age = 29 OR sexuality=‘female’) AND height = 177//Attention: Because ycdb uses array arguments, the first OR is overwritten, the following usage is wrong, $data = $ycdb->select(“user_info_test”, “”, [ “AND” => [ “OR” => [ “age” => 29, “sexuality” => “female” ], “OR” => [ “uid[!]” => 3, “height[>=]” => 170 ], ]]);// [X] SELECT * FROM user_info_test WHERE (uid != 3 OR height >= 170)//We can use # and comments to distinguish between two diffrents OR$data = $ycdb->select(“user_info_test”, “*”, [ “AND” => [ “OR #1” => [ “age” => 29, “sexuality” => “female” ], “OR #2” => [ “uid[!]” => 3, “height[>=]” => 170 ], ]]);// [√] SELECT * FROM user_info_test WHERE (age = 29 OR sexuality = ‘female’) AND (uid != 3 OR height >= 170)Fuzzy Matching LikeLIKE USAGE [].$data = $ycdb->select(“user_info_test”, “”, [ “username[~]” => “%ide%” ]);// WHERE username LIKE ‘%ide%’$data = $ycdb->select(“user_info_test”, “”, [“username[]” => ["%ide%", “Jam%”, “%ace”]]);// WHERE username LIKE ‘%ide%’ OR username LIKE ‘Jam%’ OR username LIKE ‘%ace’$data = $ycdb->select(“user_info_test”, “*”, [ “username[!]” => “%ide%” ]);// WHERE username NOT LIKE ‘%ide%‘Use of wildcards$ycdb->select(“user_info_test”, “”, [ “username[]” => “Londo_” ]); // London, Londox, Londos…$ycdb->select(“user_info_test”, “id”, [ “username[]” => “[BCR]at” ]); // Bat, Cat, Rat$ycdb->select(“user_info_test”, “id”, [ “username[~]” => “[!BCR]at” ]); // Eat, Fat, Hat…ORDER BY And LIMIT$data = $ycdb->select(“user_info_test”, “”, [ ‘sexuality’ => ‘male’, ‘ORDER’ => [ “age”, “height” => “DESC”, “uid” => “ASC” ], ‘LIMIT’ => 100, //Get the first 100 of rows (overwritten by next LIMIT) ‘LIMIT’ => [20, 100] //Started from the top 20 rows, and get the next 100]);//SELECT * FROM user_info_test WHERE sexuality = ‘male’ ORDER BY age, height DESC, uid ASC LIMIT 100 OFFSET 20GROUP And HAVING$ycdb->select(“user_info_test”, “sexuality,age,height”, [ ‘GROUP’ => ‘sexuality’, // GROUP by array of values ‘GROUP’ => [ ‘sexuality’, ‘age’, ‘height’ ], // Must have to use it with GROUP together ‘HAVING’ => [ ‘age[>]’ => 30 ]]);//SELECT uid FROM user_info_test GROUP BY sexuality,age,height HAVING age > 30Select statementusageselect($table, $columns, $where)table [string]table namecolumns [string/array]Columns to be queried.where (optional) [array]The conditions of the query.select($table, $join, $columns, $where)table [string]table namejoin [array]Multi-table query, can be ignored if not used.columns [string/array]Columns to be queried.where (optional) [array]The conditions of the query.return: [array]Fail if -1 is returned, otherwise result array is returnedexampleYou can use * to match all fields, but if you specify columns you can improve performance.$datas = $ycdb->select(“user_info_test”, [ “uid”, “username”], [ “age[>]” => 31]);// $datas = array(// [0] => array(// “uid” => 6,// “username” => “Aiden”// ),// [1] => array(// “uid” => 11,// “username” => “smallhow”// )// )// Select all columns$datas = $ycdb->select(“user_info_test”, “”);// Select a column$datas = $ycdb->select(“user_info_test”, “username”); // $datas = array(// [0] => “lucky”,// [1] => “Tom”,// [2] => “Red”// )Table joinMulti-table query SQL is more complicated, and it can be easily solved with ycdb.// [>] == RIGH JOIN// [<] == LEFT JOIN// [<>] == FULL JOIN// [><] == INNER JOIN$ycdb->select(“user_info_test”,[ // Table Join Info “[>]account” => [“uid” => “userid”], // RIGHT JOIN account ON user_info_test.uid= account.userid // This is a shortcut to declare the relativity if the row name are the same in both table. “[>]album” => “uid”, //RIGHT JOIN album USING (uid) // Like above, there are two row or more are the same in both table. “[<]detail” => [“uid”, “age”], // LEFT JOIN detail USING (uid,age) // You have to assign the table with alias. “[<]address(addr_alias)” => [“uid” => “userid”], //LEFT JOIN address AS addr_alias ON user_info_test.uid=addr_alias.userid // You can refer the previous joined table by adding the table name before the column. “[<>]album” => [“account.userid” => “userid”], //FULL JOIN album ON account.userid = album.userid // Multiple condition “[><]account” => [ “uid” => “userid”, “album.userid” => “userid” ]], [ // columns “user_info_test.uid”, “user_info_test.age”, “addr_alias.country”, “addr_alias.city”], [ // where condition “user_info_test.uid[>]” => 3, “ORDER” => [“user_info_test.uid” => “DESC”], “LIMIT” => 50]);// SELECT // user_info_test.uid,// user_info_test.age,// addr_alias.country,// addr_alias.city // FROM user_info_test // RIGHT JOIN account ON user_info_test.uid= account.userid // RIGHT JOIN album USING (uid) // LEFT JOIN detail USING (uid,age) // LEFT JOIN address AS addr_alias ON user_info_test.uid=addr_alias.userid // FULL JOIN album ON account.userid = album.userid // INNER JOIN account ON user_info_test.uid= account.userid // AND album.userid = account.userid // WHERE user_info_test.uid > 3 // ORDER BY user_info_test.uid DESC // LIMIT 50aliasYou can use aliases to prevent field conflicts$data = $ycdb->select(“user_info_test(uinfo)”, [ “[<]account(A)” => “userid”,], [ “uinfo.uid(uid)”, “A.userid”]);// SELECT uinfo.uid AS uid, A.userid // FROM user_info_test AS uinfo // LEFT JOIN account AS A USING (userid)Insert statementinsert($table, $data, $cache_info)table [string]table namedata [array]insert datacache_info (optional) [array]cache inforeturn [int]Fail if -1 is returned, otherwise insert_id is returned, if the table has no AUTO_INCREMENT field, the insert_id is zero$data = array(‘username’ => ‘smallhow’,‘sexuality’ => ‘male’,‘age’ => 35, ‘height’ => ‘168’);$insert_id = $ycdb->insert(“user_info_test”, $data);if($insert_id == -1) { $code = $ycdb->errorCode(); $info = $ycdb->errorInfo(); echo “code:” . $code . “\n”; echo “info:” . $info[2] . “\n”;} else { echo $insert_id;}Replace statementreplace($table, $data, $cache_info)table [string]table namedata [array]replace datacache_info (optional) [array]cache inforeturn [int]Fail if -1 is returned, otherwise insert_id is returned$data = array(‘username’ => ‘smallhow’,‘sexuality’ => ‘male’,‘age’ => 35, ‘height’ => ‘168’);$insert_id = $ycdb->replace(“user_info_test”, $data);if($insert_id == -1) { $code = $ycdb->errorCode(); $info = $ycdb->errorInfo(); echo “code:” . $code . “\n”; echo “info:” . $info[2] . “\n”;} else { echo $insert_id;}Update statementupdate($table, $data, $where)table [string]table namedata [array]update datawhere (optional) [array]where condition [可选]return [int]Fail if -1 is returned, otherwise the number of update records is returned$data = array(‘height’ => 182,‘age’ => 33);$where = array(‘username’ => ‘smallhow’);$ret = $ycdb->update(“user_info_test”, $data, $where);Delete statementdelete($table, $where)table [string]table namewhere (optional) [array]where condition [可选]return [int]Fail if -1 is returned, otherwise the number of delete records is returned$where = array(‘username’ => ‘smallhow’);$ret = $ycdb->delete(“user_info_test”, $where);Whole Example$table = “table_a(a)”;$join = [ “[>]AAAA(a1)” => “id”, “[<]BBBB” => [“E1”, “E2”, “E3”], “[>]CCCC(c1)” => [ “GG” => “HH”, “II.KK” => “LL”]];$columns = [“name(a)”, “avatar(b)”, “age”];$where = [ “user.email[!]” => [“foo@bar.com”, “cat@dog.com”, “admin@ycdb.in”], “user.uid[<]” => 11111, “uid[>=]” => 222, “uid[!]” => null, “count[!]” => [36, 57, 89], “id[!]” => true, “int_num[!]” => 3, “double_num[!]” => 3.76, “AA[]” => “%saa%”, “BB[!]” => “%sbb”, “CC[]” => [“11%”, “22_”, “33%”], “DD[!]” => ["%44%", “55%”, “66%”], “EE[]” => [“AND” => ["%E11", “E22”]], “FF[]” => [“OR” => ["%F33", “F44”]], “GG[!]” => [“AND” => ["%G55", “G66”]], “HH[!]” => [“OR” => [“H77”, “H88”]], “II[<>]” => [“1”, “12”], “LL[><]” => [“1”, “12”], “AND #1” => [ “OR #1” => [ “user_name” => null, “email” => “foo@bar.com”, ], “OR #2” => [ “user_name” => “bar”, “email” => “bar@foo.com” ] ], “OR” => [ “user_name[!]” => “foo”, “promoted[!]” => true ], ‘GROUP’ => ‘userid’, ‘GROUP’ => [’type’, ‘age’, ‘gender’], ‘HAVING’ => [ “uid.num[>]” => 111, “type[>]” => “smart”, “id[!]” => false, “god3[!]” => 9.86, “uid[!]” => null, “AA[]” => “SSA%”, “CC[]” => [“11%”, “22%”, “%33”], ], ‘ORDER’ => [ “user.score”, “user.uid” => “ASC”, “time” => “DESC”, ], “LIMIT” => 33,];$ycdb->select($table, $join, $columns, $where);Database transaction$ycdb->begin();$ret1 = $ycdb->exec(“insert into user_info_test(username, sexuality, age, height) values(‘smallhow’, ‘male’, 29, 180)”);$ret2 = $ycdb->exec(“insert into user_info_test(username, sexuality, age, height) values(‘jesson’, ‘female’, 28, 175)”);if($ret1 == -1 || $ret2 == -1 ) { $ycdb->rollback();} else { $ycdb->commit()}Data CachingWe can use redis, or any other cache system that supports set/get/del/expire function as the medium to store the data returned by the database. If you do not specify the expiration time, the default storage expiration time is 5 minutes. if The cache is specified. When we call data update function such as update/delete/insert, we should pass in the same cache key so that ycdb can clear the cache to ensure data consistency.//we want cache data by redis$redis = new Redis();$redis->connect(’/home/redis/pid/redis.sock’);$option = array(“host” => “127.0.0.1”, “username” => “test”, “password” => “test”, “dbname” => “test”, “port” => ‘3306’, “cache” => $redis, //cache instance ‘option’ => array( PDO::ATTR_CASE => PDO::CASE_NATURAL, PDO::ATTR_TIMEOUT => 2));$ycdb = new ycdb($option);try{ $ycdb->initialize();} catch (PDOException $e) { echo “find PDOException when initialize\n”; exit;}// I want to keep the 29-year-old user data queried by the database in the cache, and keep it for 10 minutes.$age = 29;$cache_key = ‘pre_cache_key_’ . $age;$data = $ycdb->select(“user_info_test”, “”, [ ‘age’ => $age, ‘CACHE’ => [‘key’ => $cache_key, ’expire’ => 600] //cache key an expire time (seconds)]);echo $redis->get($cache_key) . “\n”;// If I update these 29-year-old user data, or even add a new 29-year-old user information, // it’s best to enter the cache key to clean up the cache to keep the data consistent.$ycdb->update(“user_info_test”, [‘remark’ => ‘29-year-old’], [ ‘age’ => $age, ‘CACHE’ => [‘key’ => $cache_key] //cache key]);echo $redis->get($cache_key) . “\n”;//If you are going to delete the relevant data, it is best to also clean up the cache by cache_key.$ycdb->delete(“user_info_test”, [ ‘age’ => $age, ‘CACHE’ => [‘key’ => $cache_key] //cache key]);echo $redis->get($cache_key) . “\n”;//Clean up the cache by cache_key when the data you insert is related to the cached data.$insert_data = array();$insert_data[‘username’] = ’test’;$insert_data[‘sexuality’] = ‘male’;$insert_data[‘age’] = 29;$insert_data[‘height’] = 176;$insert_id = $ycdb->insert(“user_info_test”, $insert_data, [‘key’ => $cache_key]);echo $redis->get($cache_key) . “\n”;PHP Database Connection PoolShort connection performance is generally not available. CPU resources are consumed by the system. Once the network is jittered, there will be a large number of TIME_WAIT generated. The service has to be restarted periodically or the machine is restarted periodically. The server is unstable, QPS is high and low, and the connection is stable and efficient. The pool can effectively solve the above problems, it is the basis of high concurrency. ycdb uses a special way to establish a stable connection pool with MySQL. performance can be increased by at least 30%, According to PHP’s operating mechanism, long connections can only reside on top of the worker process after establishment, that is, how many work processes are there. How many long connections, for example, we have 10 PHP servers, each launching 1000 PHP-FPM worker processes, they connect to the same MySQL instance, then there will be a maximum of 10,000 long connections on this MySQL instance, the number is completely Out of control! And PHP’s connection pool heartbeat mechanism is not perfectHow ?Let’s focus on Nginx, its stream module implements load balancing of TCP/UDP services, and with the stream-lua module, we can implement programmable stream services, that is, custom TCP/N with Nginx. UDP service! Of course, you can write TCP/UDP services from scratch, but standing on Nginx’s shoulder is a more time-saving and labor-saving choice. We can choose the OpenResty library to complete the MySQL connection pool function. OpenResty is a very powerful and well-functioning Nginx Lua framework. It encapsulates Socket, MySQL, Redis, Memcache, etc. But what is the relationship between Nginx and PHP connection pool? And listen to me slowly: Usually most PHP is used with Nginx, and PHP and Nginx are mostly on the same server. With this objective condition, we can use Nginx to implement a connection pool, connect to services such as MySQL on Nginx, and then connect to Nginx through a local Unix Domain Socket, thus avoiding all kinds of short links. Disadvantages, but also enjoy the benefits of the connection pool.OpenResty InstallOpenResty Document: https://moonbingbing.gitbooks…OpenResty Official Website : http://www.openresty.org/CentOS 6.8 Install :###### Install the necessary libraries ######$yum install readline-devel pcre-devel openssl-devel perl###### Install OpenResty ######$cd ~/ycdatabase/openresty$tar -xzvf openresty-1.13.6.1.tar.gz$cd openresty-1.13.6.1$./configure –prefix=/usr/local/openresty.1.13 –with-luajit –without-http_redis2_module –with-http_iconv_module$gmake $gmake install###### open mysql pool ######$cp -rf ~/ycdatabase/openresty/openresty-pool ~/$mkdir /openresty-pool/logs$/usr/local/openresty.1.13/nginx/sbin/nginx -p /openresty-poolMySQL Database Connection Pool Config/openresty-pool/conf/nginx.conf :worker_processes 1; #nginx worker process numerror_log logs/error.log; #nginx error log pathevents { worker_connections 1024;}stream { lua_code_cache on; lua_check_client_abort on; server { listen unix:/tmp/mysql_pool.sock; content_by_lua_block { local mysql_pool = require “mysql_pool” local config = {host = “127.0.0.1”, user = “root”, password = “test”, database = “collect”, timeout = 2000, max_idle_timeout = 10000, pool_size = 200} pool = mysql_pool:new(config) pool:run() } }}If you have more than a MySQL Server, you can start another server and add a new listener to unix domain socket.PHP CodeExcept the option is array(“unix_socket” => “/tmp/mysql_pool.sock”) , Php mysql connection pool usage is exactly the same as before,But, MySQL does not support transactions in unix domain socket mode.$option = array(“unix_socket” => “/tmp/mysql_pool.sock”);$ycdb = new ycdb($option);$ret = $ycdb->select(“user_info_test”, “*”, [“sexuality” => “male”]);if($ret == -1) { $code = $ycdb->errorCode(); $info = $ycdb->errorInfo(); echo “code:” . $code . “\n”; echo “info:” . $info[2] . “\n”;} else { print_r($ret);}Redis Connection PoolSimilarly, Redis can solve the connection pool problem in the same way.Redis Connection Pool Config/openresty-pool/conf/nginx.confworker_processes 1; #nginx worker process num error_log logs/error.log; #error log path events { worker_connections 1024;} stream { lua_code_cache on; lua_check_client_abort on; server { listen unix:/tmp/redis_pool.sock; content_by_lua_block { local redis_pool = require “redis_pool” pool = redis_pool:new({ip = “127.0.0.1”, port = 6379, auth = “password”}) pool:run() } } server { listen unix:/tmp/mysql_pool.sock; content_by_lua_block { local mysql_pool = require “mysql_pool” local config = {host = “127.0.0.1”, user = “root”, password = “test”, database = “collect”, timeout = 2000, max_idle_timeout = 10000, pool_size = 200} pool = mysql_pool:new(config) pool:run() } }}PHP Code$redis = new Redis();$redis->pconnect(’/tmp/redis_pool.sock’);var_dump($redis->hSet(“foo1”, “vvvvv42”, 2));var_dump($redis->hSet(“foo1”, “vvvv”, 33));var_dump($redis->expire(“foo1”, 111));var_dump($redis->hGetAll(“foo1”)); ...

March 8, 2019 · 14 min · jiezi

编写chameleon跨端组件的正确姿势(上篇)

在chameleon项目中我们实现一个跨端组件一般有两种思路:使用第三方组件封装与基于chameleon语法统一实现。本篇是编写chameleon跨端组件的正确姿势系列文章的上篇,以封装一个跨端的indexlist组件为例,首先介绍如何优雅的使用第三方库封装跨端组件,然后给出编写chameleon跨端组件的建议。使用chameleon语法统一实现跨端组件请关注文章《编写chameleon跨端组件的正确姿势(下篇)》依靠强大的多态协议,chameleon项目中可以轻松使用各端的第三方组件封装自己的跨端组件库。基于第三方组件可以利用现有生态迅速实现需求,但是却存在很多缺点,例如多端第三方组件本身的功能与样式差异、组件质量得不到保证以及绝大部分组件并不需要通过多态组件差异化实现,这样反而提升了长期的维护成本;使用chameleon语法统一实现则可以完美解决上述问题,并且扩展一个新的端时现有组件可以直接运行。本文的最后也会详细对比一下两种方案的优劣。 因此,建议将通过第三方库实现跨端组件库作为临时方案,从长期维护的角度来讲,建议开发者使用chameleon语法统一实现绝大部分跨端组件,只有一些特别复杂并且已有成熟第三方库或者框架能力暂时不支持的组件,才考虑使用第三方组件封装成对应的跨端组件。由于本文介绍的是使用第三方库封装跨端组件, 因此示例的indexlist组件采用第三方组件封装来实现, 通过chameleon统一实现跨端组件的方法可以看《编写chameleon跨端组件的正确姿势(下篇)》。最终实现的indexlist效果图:前期准备使用各端第三方组件实现chameleon跨端组件需要如下前期准备:项目初始化创建一个新项目 cml-demo cml init project 进入项目cd cml-demo组件设计开发一个模块时我们首先应该根据功能确定其输入与输出,对应到组件开发上来说,就是要确定组件的属性和事件,其中属性表示组件接受的输入,而事件则表示组件在特定时机对外的输出。 为了方便说明,本例暂时实现一个具备基础功能的indexlist。一个indexlist组件至少应该在用户选择某一项时抛出一个onselect事件,传递用户当前所选中项的数据;至少应该接受一个datalist,作为其渲染的数据源,这个datalist应该是一个类似于以下结构的对象数组: const dataList = [ { name: ‘阿里’, pinYin: ‘ali’, py: ‘al’ }, { name: ‘北京’, pinYin: ‘beijing’, py: ‘bj’ }, ….. ]寻找第三方组件库由于本文介绍的是如何使用第三方库封装跨端组件,因此在确定组件需求以及实现思路后去寻找符合要求的第三方库。在开发之前,作者调研了目前较为流行的各端组件库,推荐如下:web端:cube-uivuxmint-uivantwx端:iview weappvant weappweuiweex端:weex-ui除了上述组件库之外,开发者也可以根据自己的实际需求去寻找经过包装之后符合预期的第三方库。截止文章编写时,作者未找到较成熟的支付宝及百度小程序第三方库,因此暂时先实现web、微信小程序以及weex端,这也体现出了使用第三方库扩展跨端组件的局限性:当没有成熟的对应端第三方库时,无法完成该端的组件开发;而使用chameleon语法统一实现.md)则可以解决上述问题,扩展新的端时已有组件能够直接运行,无需额外扩展。 本文在实现indexlist组件时分别使用了cube-ui, iview weapp以及weex-ui, 以下会介绍具体的开发过程.组件开发初始化创建多态组件cml init component选择“多态组件”, 并输入组件名字“indexlist”, 完成组件的创建, 创建之后的组件位于src/components/indexlist文件夹下。接口校验多态组件中的.interface文件利用接口校验语法对组件的属性和事件进行类型定义,保证各端的属性和事件一致。确定了组件的属性与事件之后就开始编写.interface文件, 修改src/components/indexlist/indexlist.interface: type eventDetail = { name: String, pinYin: String, py: String}type arrayItem = { name: String, pinYin: String, py: String}type arr = [arrayItem];interface IndexlistInterface { dataList: arr, onselect(eventDetail: eventDetail): void}具体的interface文件语法可以参考此处, 本文不再赘述。web端组件开发安装cube-ui npm i cube-ui -S在src/components/indexlist/indexlist.web.cml的json文件中引入cube-ui的indexlist组件"base": { “usingComponents”: { “cube-index-list”: “cube-ui/src/components/index-list/index-list” }}修改src/components/indexlist/indexlist.web.cml中的模板代码,引用cube-ui的indexlist组件: <view class=“index-list-wrapper”> <cube-index-list :data=“list” @select=“onItemSelect”/></view>修改src/components/indexlist/indexlist.web.cml中的js代码, 根据cube-ui文档将数据处理成符合其组件预期的结构, 并向上抛出onselect事件:const words = [“A”,“B”,“C”,“D”,“E”,“F”,“G”,“H”,“I”,“J”,“K”,“L”,“M”,“N”,“O”,“P”,“Q”,“R”,“S”,“T”,“U”,“V”,“W”,“X”,“Y”,“Z”];class Indexlist implements IndexlistInterface {props = { dataList: { type: Array, default() { return [] } }}data = { list: [],}methods = { initData() { const cityData = []; words.forEach((item, index) => { cityData[index] = {}; cityData[index].items = []; cityData[index].name = item; }); this.dataList.forEach((item) => { let firstName = item.pinYin.substring(0, 1).toUpperCase(); let index = words.indexOf(firstName); cityData[index].items.push(item) }); this.list = cityData; }, onItemSelect(item) { this.$cmlEmit(‘onselect’, item); }}mounted() { this.initData();}}export default new Indexlist();编写必要的样式: .index-list-wrapper { width: 750cpx; height: 1200cpx;}以上便使用cube-ui完成了web端indexlist组件的开发,效果如下: weex端组件开发安装weex-uinpm i weex-ui -S在src/components/indexlist/indexlist.weex.cml的json文件中引入weex-ui的wxc-indexlist组件:“base”: { “usingComponents”: { “wex-indexlist”: “weex-ui/packages/wxc-indexlist” } }修改src/components/indexlist/indexlist.weex.cml中的模板代码,引用weex-ui的wxc-indexlist组件: <view class=“index-list-wrapper”> <wex-indexlist :normal-list=“list” @wxcIndexlistItemClicked=“onItemSelect” /> </view>修改src/components/indexlist/indexlist.weex.cml中的js代码:class Indexlist implements IndexlistInterface { props = { dataList: { type: Array, default() { return [] } } } data = { list: [], } mounted() { this.initData(); } methods = { initData() { this.list = this.dataList; }, onItemSelect(e) { this.$cmlEmit(‘onselect’, e.item); } }}export default new Indexlist();编写必要样式,此时发现weex端与web端有部分重复样式,因此将样式抽离出来创建indexlist.less,在web端与weex端的cml文件中引入该样式<style lang=“less”> @import ‘./indexlist.less’;</style> indexlist.less文件内容:.index-list-wrapper { width: 750cpx; height: 1200cpx;}以上便使用weex-ui完成了weex端indexlist组件的开发,效果如下: wx端组件编写根据iview weapp文档, 首先到Github下载iview weapp代码,将dist目录拷贝到项目的src目录下,然后在src/components/indexlist/indexlist.wx.cml的json文件中引入iview的index与index-item组件:“base”: { “usingComponents”: { “i-index”:"/iview/index/index", “i-index-item”: “/iview/index-item/index” }},修改src/components/indexlist/indexlist.wx.cml中的模板代码,引用iview的index与index-item组件: <view class=“index-list-wrapper”> <i-index height=“1200rpx” > <i-index-item wx:for="{{cities}}" wx:for-index=“index” wx:key="{{index}}" wx:for-item=“item” name="{{item.key}}" > <view class=“index-list-item” wx:for="{{item.list}}" wx:for-index=“in” wx:key="{{in}}" wx:for-item=“it” c-bind:tap=“onItemSelect(it)” > <text>{{it.name}}</text> </view> </i-index-item> </i-index> </view>修改src/components/indexlist/indexlist.wx.cml中的js代码, 根据iview weapp文档将数据处理成符合其组件预期的结构, 并向上抛出onselect事件:const words = [“A”,“B”,“C”,“D”,“E”,“F”,“G”,“H”,“I”,“J”,“K”,“L”,“M”,“N”,“O”,“P”,“Q”,“R”,“S”,“T”,“U”,“V”,“W”,“X”,“Y”,“Z”];class Indexlist implements IndexlistInterface { props = { dataList: { type: Array, default() { return [] } } } data = { cities: [] } methods = { initData() { let storeCity = new Array(26); words.forEach((item,index)=>{ storeCity[index] = { key: item, list: [] }; }); this.dataList.forEach((item)=>{ let firstName = item.pinYin.substring(0,1).toUpperCase(); let index = words.indexOf(firstName); storeCity[index].list.push(item); }); this.cities = storeCity; }, onItemSelect(item) { this.$cmlEmit(‘onselect’, item); } } mounted() { this.initData(); }}export default new Indexlist();编写必要样式:@import ‘indexlist.less’;.index-list { &-item { height: 90cpx; padding-left: 20cpx; justify-content: center; border-bottom: 1cpx solid #F7F7F7 }}以上便使用iview weapp完成了wx端indexlist组件的开发, 效果如下: 组件使用修改src/pages/index/index.cml文件里面的json配置,引用创建的indexlist组件"base": { “usingComponents”: { “indexlist”: “/components/indexlist/indexlist” }},修改src/pages/index/index.cml文件中的模板部分,引用创建的indexlist组件 <view class=“page-wrapper”> <indexlist dataList="{{dataList}}" c-bind:onselect=“onItemSelect” /> </view>其中dataList是一个对象数组,表示组件要渲染的数据源。具体结构为:const dataList = [ { name: ‘阿里’, pinYin: ‘ali’, py: ‘al’ }, { name: ‘北京’, pinYin: ‘beijing’, py: ‘bj’ }, ….. ]开发总结根据上述例子可以看出,chameleon项目可以轻松结合第三方库封装自己的跨端组件库。使用第三方组件封装跨端组件库的步骤大致如下:跨端组件设计根据实际需求引入合适的第三方组件根据第三方组件文档,将数据处理成符合预期的结构,并在适当时机抛出事件编写必要样式一些思考理解*.[web|wx|weex].cml根据组件多态文档, 像indexlist.web.cml、indexlist.wx.cml与indexlist.weex.cml的这些文件是灰度区, 它们是唯一可以调用下层端能力的CML文件,这里的下层端能力既包含下层端组件,例如在web端和weex端的.vue文件等;也包含下层端的api,例如微信小程序的wx.pageScrollTo等。这一层的存在是为了调用下层端代码,各端具体的逻辑实现应该在下层来实现, 这种规范的好处是显而易见的: 随着业务复杂度的提升,各个下层端维护的功能逐渐变多,其中通用的部分又可以通过普通cml文件抽离出来被统一调用,这样可以保证差异化部分始终是最小集合,灰度区是存粹的;如果将业务逻辑都放在了灰度区,随着功能复杂度的上升,三端通用功能/组件就无法达到合理的抽象,导致灰度层既有相同功能,又有差异化部分,这显然不是开发者愿意看到的场景。在灰度区的模板、逻辑、样式和json文件中分别具有如下规则:模板调用下层组件时,既可以使用chameleon语法,也可以使用各端原生语法;在灰度区chameleon编译器不会编译各个端原生语法,例如v-for,bindtap等。建议在模板部分仍然使用chameleon模板语法,只有在实现对应平台不支持的语法(例如web端v-html等)时才使用原生语法。引用下层全局组件时需要添加origin-前缀,这样可以“告诉”chameleon编译器是在引用下层的原生组件,chameleon编译器就不会对其进行处理了。这种做法同时解决了组件命名冲突问题,例如在微信小程序端引用<origin-button>表示调用小程序原生的button组件而不是chameleon内置的button组件。逻辑在script逻辑代码中,除了编写普通cml逻辑代码之外,开发者还可以使用下层端的全局变量和任意方法,包括生命周期函数。这种机制保证开发者可以灵活扩展各端特有功能,而不需要依赖多态接口。样式既可以使用cmss语法也可以使用下层端的css语法。json文件*web.cml:base.usingComponents可以引入普通cml组件和任意.vue扩展名组件,路径规则见组件配置。*wx.cml:base.usingComponents可以引入普通cml组件和普通微信小程序组件,路径规则见组件配置。*weex.cml:base.usingComponents可以引入普通cml组件和任意.vue扩展名组件,路径规则见组件配置。在各端对应的灰度区文件中均可以根据上述规范使用各端的原生语法,但是为了规范仍然建议使用chameleon体系的语法规则。总体来说,灰度区可以认为是chameleon体系与各端原生组件/方法的衔接点,向下使用各端功能/组件,向上通过多态协议提供各端统一的调用接口。 ...

March 7, 2019 · 2 min · jiezi

教你从头写游戏服务器框架

本文由云+社区发表作者:韩伟前言大概已经有差不多一年没写技术文章了,原因是今年投入了一些具体游戏项目的开发。这些新的游戏项目,比较接近独立游戏的开发方式。我觉得公司的“祖传”服务器框架技术不太适合,所以从头写了一个游戏服务器端的框架,以便获得更好的开发效率和灵活性。现在项目将近上线,有时间就想总结一下,这样一个游戏服务器框架的设计和实现过程。这个框架的基本运行环境是 Linux ,采用 C++ 编写。为了能在各种环境上运行和使用,所以采用了 gcc 4.8 这个“古老”的编译器,以 C99 规范开发。需求由于“越通用的代码,就是越没用的代码”,所以在设计之初,我就认为应该使用分层的模式来构建整个系统。按照游戏服务器的一般需求划分,最基本的可以分为两层:底层基础功能:包括通信、持久化等非常通用的部分,关注的是性能、易用性、扩展性等指标。高层逻辑功能:包括具体的游戏逻辑,针对不同的游戏会有不同的设计。我希望能有一个基本完整的“底层基础功能”的框架,可以被复用于多个不同的游戏。由于目标是开发一个 适合独立游戏开发 的游戏服务器框架。所以最基本的需求分析为:功能性需求并发:所有的服务器程序,都会碰到这个基本的问题:如何处理并发处理。一般来说,会有多线程、异步两种技术。多线程编程在编码上比较符合人类的思维习惯,但带来了“锁”这个问题。而异步非阻塞的模型,其程序执行的情况是比较简单的,而且也能比较充分的利用硬件性能,但是问题是很多代码需要以“回调”的形式编写,对于复杂的业务逻辑来说,显得非常繁琐,可读性非常差。虽然这两种方案各有利弊,也有人结合这两种技术希望能各取所长,但是我更倾向于基础是使用异步、单线程、非阻塞的调度方式,因为这个方案是最清晰简单的。为了解决“回调”的问题,我们可以在其上再添加其他的抽象层,比如协程或者添加线程池之类的技术予以改善。通信:支持 请求响应 模式以及 通知 模式的通信(广播视为一种多目标的通知)。游戏有很多登录、买卖、打开背包之类的功能,都是明确的有请求和响应的。而大量的联机游戏中,多个客户端的位置、HP 等东西都需要经过网络同步,其实就是一种“主动通知”的通信方式。持久化:可以存取 对象 。游戏存档的格式非常复杂,但其索引的需求往往都是根据玩家 ID 来读写就可以。在很多游戏主机如 PlayStation 上,以前的存档都是可以以类似“文件”的方式存放在记忆卡里的。所以游戏持久化最基本的需求,就是一个 key-value 存取模型。当然,游戏中还会有更复杂的持久化需求,比如排行榜、拍卖行等,这些需求应该额外对待,不适合包含在一个最基本的通用底层中。缓存:支持远程、分布式的对象缓存。游戏服务基本上都是“带状态”的服务,因为游戏要求响应延迟非常苛刻,基本上都需要利用服务器进程的内存来存放过程数据。但是游戏的数据,往往是变化越快的,价值越低,比如经验值、金币、HP,而等级、装备等变化比较慢的,价值则越高,这种特征,非常适合用一个缓存模型来处理。协程:可以用 C++ 来编写协程代码,避免大量回调函数分割代码。这个是对于异步代码非常有用的特性,能大大提高代码的可读性和开发效率。特别是把很多底层涉及IO的功能,都提供了协程化 API,使用起来就会像同步的 API 一样轻松惬意。脚本:初步设想是支持可以用 Lua 来编写业务逻辑。游戏需求变化是出了名快的,用脚本语言编写业务逻辑正好能提供这方面的支持。实际上脚本在游戏行业里的使用非常广泛。所以支持脚本,也是一个游戏服务器框架很重要的能力。其他功能:包括定时器、服务器端的对象管理等等。这些功能很常用,所以也需要包含在框架中,但已经有很多成熟方案,所以只要选取常见易懂的模型即可。比如对象管理,我会采用类似 Unity 的组件模型来实现。非功能性需求灵活性:支持可替换的通信协议;可替换的持久化设备(如数据库);可替换的缓存设备(如 memcached/redis);以静态库和头文件的方式发布,不对使用者代码做过多的要求。游戏的运营环境比较复杂,特别是在不同的项目之间,可能会使用不同的数据库、不同的通信协议。但是游戏本身业务逻辑很多都是基于对象模型去设计的,所以应该有一层能够基于“对象”来抽象所有这些底层功能的模型。这样才能让多个不同的游戏,都基于一套底层进行开发。部署便利性:支持灵活的配置文件、命令行参数、环境变量的引用;支持单独进程启动,而无须依赖数据库、消息队列中间件等设施。一般游戏都会有至少三套运行环境,包括一个开发环境、一个内测环境、一个外测或运营环境。一个游戏的版本更新,往往需要更新多个环境。所以如何能尽量简化部署就成为一个很重要的问题。我认为一个好的服务器端框架,应该能让这个服务器端程序,在无配置、无依赖的情况下独立启动,以符合在开发、测试、演示环境下快速部署。并且能很简单的通过配置文件、或者命令行参数的不同,在集群化下的外部测试或者运营环境下启动。性能:很多游戏服务器,都会使用异步非阻塞的方式来编程。因为异步非阻塞可以很好的提高服务器的吞吐量,而且可以很明确的控制多个用户任务并发下的代码执行顺序,从而避免多线程锁之类的复杂问题。所以这个框架我也希望是以异步非阻塞作为基本的并发模型。这样做还有另外一个好处,就是可以手工的控制具体的进程,充分利用多核 CPU 服务器的性能。当然异步代码可读性因为大量的回调函数,会变得很难阅读,幸好我们还可以用“协程”来改善这个问题。扩展性:支持服务器之间的通信,进程状态管理,类似 SOA 的集群管理。自动容灾和自动扩容,其实关键点是服务进程的状态同步和管理。我希望一个通用的底层,可以把所有的服务器间调用,都通过一个统一的集权管理模型管理起来,这样就可以不再每个项目去关心集群间通信、寻址等问题。一旦需求明确下来,基本的层级结构也可以设计了:层次功能约束逻辑层实现更具体的业务逻辑能调用所有下层代码,但应主要依赖接口层实现层对各种具体的通信协议、存储设备等功能的实现满足下层的接口层来做实现,禁止同层间互相调用接口层定义了各模块的基本使用方式,用以隔离具体的实现和设计,从而提供互相替换的能力本层之间代码可以互相调用,但禁止调用上层代码工具层提供通用的 C++ 工具库功能,如 log/json/ini/日期时间/字符串处理 等等不应该调用其他层代码,也不应该调用同层其他模块第三方库提供诸如 redis/tcaplus 或者其他现成功能,其地位和“工具层”一样不应该调用其他层代码,甚至不应该修改其源码最后,整体的架构模块类似:说明通信处理器缓存持久化功能实现TcpUdpKcpTlvLineJsonHandlerObjectProcessorSessionLocalCacheRedisMapRamMapZooKeeperMapFileDataStoreRedisDataStroe接口定义TransferProtocolServerClientProcessorDataMapSerializableDataStore工具类库ConfigLOGJSONCoroutine 通信模块对于通信模块来说,需要有灵活的可替换协议的能力,就必须按一定的层次进行进一步的划分。对于游戏来说,最底层的通信协议,一般会使用 TCP 和 UDP 这两种,在服务器之间,也会使用消息队列中间件一类通信软件。框架必须要有能同事支持这几通信协议的能力。故此设计了一个层次为: Transport在协议层面,最基本的需求有“分包”“分发”“对象序列化”等几种需求。如果要支持“请求-响应”模式,还需要在协议中带上“序列号”的数据,以便对应“请求”和“响应”。另外,游戏通常都是一种“会话”式的应用,也就是一系列的请求,会被视为一次“会话”,这就需要协众需要有类似 Session ID 这种数据。为了满足这些需求,设计一个层次为: Protocol拥有了以上两个层次,是可以完成最基本的协议层能力了。但是,我们往往希望业务数据的协议包,能自动化的成为编程中的 对象,所以在处理消息体这里,需要一个可选的额外层次,用来把字节数组,转换成对象。所以我设计了一个特别的处理器:ObjectProcessor ,去规范通信模块中对象序列化、反序列化的接口。输入层次功能输出dataTransport通信bufferbufferProtocol分包MessageMessageProcessor分发objectobject处理模块处理业务逻辑Transport此层次是为了统一各种不同的底层传输协议而设置的,最基本应该支持 TCP 和 UDP 这两种协议。对于通信协议的抽象,其实在很多底层库也做的非常好了,比如 Linux 的 socket 库,其读写 API 甚至可以和文件的读写通用。C# 的 Socket 库在 TCP 和 UDP 之间,其 api 也几乎是完全一样的。但是由于作用游戏服务器,很多适合还会接入一些特别的“接入层”,比如一些代理服务器,或者一些消息中间件,这些 API 可是五花八门的。另外,在 html5 游戏(比如微信小游戏)和一些页游领域,还有用 HTTP 服务器作为游戏服务器的传统(如使用 WebSocket 协议),这样就需要一个完全不同的传输层了。服务器传输层在异步模型下的基本使用序列,就是:在主循环中,不断尝试读取有什么数据可读如果上一步返回有数据到达了,则读取数据读取数据处理后,需要发送数据,则向网络写入数据根据上面三个特点,可以归纳出一个基本的接口:class Transport {public: /** * 初始化Transport对象,输入Config对象配置最大连接数等参数,可以是一个新建的Config对象。 / virtual int Init(Config config) = 0; /** * 检查是否有数据可以读取,返回可读的事件数。后续代码应该根据此返回值循环调用Read()提取数据。 * 参数fds用于返回出现事件的所有fd列表,len表示这个列表的最大长度。如果可用事件大于这个数字,并不影响后续可以Read()的次数。 * fds的内容,如果出现负数,表示有一个新的终端等待接入。 / virtual int Peek(int fds, int len) = 0; /** * 读取网络管道中的数据。数据放在输出参数 peer 的缓冲区中。 * @param peer 参数是产生事件的通信对端对象。 * @return 返回值为可读数据的长度,如果是 0 表示没有数据可以读,返回 -1 表示连接需要被关闭。 / virtual int Read( Peer peer) = 0; /** * 写入数据,output_buf, buf_len为想要写入的数据缓冲区,output_peer为目标队端, * 返回值表示成功写入了的数据长度。-1表示写入出错。 / virtual int Write(const char output_buf, int buf_len, const Peer& output_peer) = 0; /** * 关闭一个对端的连接 / virtual void ClosePeer(const Peer& peer) = 0; /* * 关闭Transport对象。 / virtual void Close() = 0;}在上面的定义中,可以看到需要有一个 Peer 类型。这个类型是为了代表通信的客户端(对端)对象。在一般的 Linux 系统中,一般我们用 fd (File Description)来代表。但是因为在框架中,我们还需要为每个客户端建立接收数据的缓存区,以及记录通信地址等功能,所以在 fd 的基础上封装了一个这样的类型。这样也有利于把 UDP 通信以不同客户端的模型,进行封装。///@brief 此类型负责存放连接过来的客户端信息和数据缓冲区class Peer {public: int buf_size_; ///< 缓冲区长度 char const buffer_;///< 缓冲区起始地址 int produced_pos_; ///< 填入了数据的长度 int consumed_pos_; ///< 消耗了数据的长度 int GetFd() const; void SetFd(int fd); /// 获得本地地址 const struct sockaddr_in& GetLocalAddr() const; void SetLocalAddr(const struct sockaddr_in& localAddr); /// 获得远程地址 const struct sockaddr_in& GetRemoteAddr() const; void SetRemoteAddr(const struct sockaddr_in& remoteAddr);private: int fd_; ///< 收发数据用的fd struct sockaddr_in remote_addr_; ///< 对端地址 struct sockaddr_in local_addr_; ///< 本端地址};游戏使用 UDP 协议的特点:一般来说 UDP 是无连接的,但是对于游戏来说,是肯定需要有明确的客户端的,所以就不能简单用一个 UDP socket 的fd 来代表客户端,这就造成了上层的代码无法简单在 UDP 和 TCP 之间保持一致。因此这里使用 Peer 这个抽象层,正好可以接近这个问题。这也可以用于那些使用某种消息队列中间件的情况,因为可能这些中间件,也是多路复用一个 fd 的,甚至可能就不是通过使用 fd 的 API 来开发的。对于上面的 Transport 定义,对于 TCP 的实现者来说,是非常容易能完成的。但是对于 UDP 的实现者来说,则需要考虑如何宠妃利用 Peer ,特别是 Peer.fd_ 这个数据。我在实现的时候,使用了一套虚拟的 fd 机制,通过一个客户端的 IPv4 地址到 int 的对应 Map ,来对上层提供区分客户端的功能。在 Linux 上,这些 IO 都可以使用 epoll 库来实现,在 Peek() 函数中读取 IO 事件,在 Read()/Write() 填上 socket 的调用就可以了。另外,为了实现服务器之间的通信,还需要设计和 Tansport 对应的一个类型:Connector 。这个抽象基类,用于以客户端模型对服务器发起请求。其设计和 Transport 大同小异。除了 Linux 环境下的 Connecotr ,我还实现了在 C# 下的代码,以便用 Unity 开发的客户端可以方便的使用。由于 .NET 本身就支持异步模型,所以其实现也不费太多功夫。/** * @brief 客户端使用的连接器类,代表传输协议,如 TCP 或 UDP /class Connector {public: virtual ~Connector() {} /* * @brief 初始化建立连接等 * @param config 需要的配置 * @return 0 为成功 / virtual int Init(Config config) = 0; /** * @brief 关闭 / virtual void Close() = 0; /* * @brief 读取是否有网络数据到来 * 读取有无数据到来,返回值为可读事件的数量,通常为1 * 如果为0表示没有数据可以读取。 * 如果返回 -1 表示出现网络错误,需要关闭此连接。 * 如果返回 -2 表示此连接成功连上对端。 * @return 网络数据的情况 / virtual int Peek() = 0; /* * @brief 读取网络数 * 读取连接里面的数据,返回读取到的字节数,如果返回0表示没有数据, * 如果buffer_length是0, 也会返回0, * @return 返回-1表示连接需要关闭(各种出错也返回0) / virtual int Read(char ouput_buffer, int buffer_length) = 0; /** * @brief 把input_buffer里的数据写入网络连接,返回写入的字节数。 * @return 如果返回-1表示写入出错,需要关闭此连接。 / virtual int Write(const char input_buffer, int buffer_length) = 0;protected: Connector(){}};Protocol对于通信“协议”来说,其实包含了许许多多的含义。在众多的需求中,我所定义的这个协议层,只希望完成四个最基本的能力:分包:从流式传输层切分出一个个单独的数据单元,或者把多个“碎片”数据拼合成一个完整的数据单元的能力。一般解决这个问题,需要在协议头部添加一个“长度”字段。请求响应对应:这对于异步非阻塞的通信模式下,是非常重要的功能。因为可能在一瞬间发出了很多个请求,而回应则会不分先后的到达。协议头部如果有一个不重复的“序列号”字段,就可以对应起哪个回应是属于哪个请求的。会话保持:由于游戏的底层网络,可能会使用 UDP 或者 HTTP 这种非长连接的传输方式,所以要在逻辑上保持一个会话,就不能单纯的依靠传输层。加上我们都希望程序有抗网络抖动、断线重连的能力,所以保持会话成为一个常见的需求。我参考在 Web 服务领域的会话功能,设计了一个 Session 功能,在协议中加上 Session ID 这样的数据,就能比较简单的保持会话。分发:游戏服务器必定会包含多个不同的业务逻辑,因此需要多种不同数据格式的协议包,为了把对应格式的数据转发。除了以上三个功能,实际上希望在协议层处理的能力,还有很多,最典型的就是对象序列化的功能,还有压缩、加密功能等等。我之所以没有把对象序列化的能力放在 Protocol 中,原因是对象序列化中的“对象”本身是一个业务逻辑关联性非常强的概念。在 C++ 中,并没有完整的“对象”模型,也缺乏原生的反射支持,所以无法很简单的把代码层次通过“对象”这个抽象概念划分开来。但是我也设计了一个 ObjectProcessor ,把对象序列化的支持,以更上层的形式结合到框架中。这个 Processor 是可以自定义对象序列化的方法,这样开发者就可以自己选择任何“编码、解码”的能力,而不需要依靠底层的支持。至于压缩和加密这一类功能,确实是可以放在 Protocol 层中实现,甚至可以作为一个抽象层次加入 Protocol ,可能只有一个 Protocol 层不足以支持这么丰富的功能,需要好像 Apache Mina 这样,设计一个“调用链”的模型。但是为了简单起见,我觉得在具体需要用到的地方,再额外添加 Protocol 的实现类就好,比如添加一个“带压缩功能的 TLV Protocol 类型”之类的。消息本身被抽象成一个叫 Message 的类型,它拥有“服务名字”“会话ID”两个消息头字段,用以完成“分发”和“会话保持”功能。而消息体则被放在一个字节数组中,并记录下字节数组的长度。enum MessageType { TypeError, ///< 错误的协议 TypeRequest, ///< 请求类型,从客户端发往服务器 TypeResponse, ///< 响应类型,服务器收到请求后返回 TypeNotice ///< 通知类型,服务器主动通知客户端};///@brief 通信消息体的基类///基本上是一个 char[] 缓冲区struct Message {public: static int MAX_MAESSAGE_LENGTH; static int MAX_HEADER_LENGTH; MessageType type; ///< 此消息体的类型(MessageType)信息 virtual ~Message(); virtual Message& operator=(const Message& right); /** * @brief 把数据拷贝进此包体缓冲区 / void SetData(const char input_ptr, int input_length); ///@brief 获得数据指针 inline char* GetData() const{ return data_; } ///@brief 获得数据长度 inline int GetDataLen() const{ return data_len_; } char* GetHeader() const; int GetHeaderLen() const;protected: Message(); Message(const Message& message); private: char* data_; // 包体内容缓冲区 int data_len_; // 包体长度};根据之前设计的“请求响应”和“通知”两种通信模式,需要设计出三种消息类型继承于 Message,他们是:Request 请求包Response 响应包Notice 通知包Request 和 Response 两个类,都有记录序列号的 seq_id 字段,但 Notice 没有。Protocol 类就是负责把一段 buffer 字节数组,转换成 Message 的子类对象。所以需要针对三种 Message 的子类型都实现对应的 Encode() / Decode() 方法。class Protocol {public: virtual ~Protocol() { } /** * @brief 把请求消息编码成二进制数据 * 编码,把msg编码到buf里面,返回写入了多长的数据,如果超过了 len,则返回-1表示错误。 * 如果返回 0 ,表示不需要编码,框架会直接从 msg 的缓冲区读取数据发送。 * @param buf 目标数据缓冲区 * @param offset 目标偏移量 * @param len 目标数据长度 * @param msg 输入消息对象 * @return 编码完成所用的字节数,如果 < 0 表示出错 / virtual int Encode(char buf, int offset, int len, const Request& msg) = 0; /** * 编码,把msg编码到buf里面,返回写入了多长的数据,如果超过了 len,则返回-1表示错误。 * 如果返回 0 ,表示不需要编码,框架会直接从 msg 的缓冲区读取数据发送。 * @param buf 目标数据缓冲区 * @param offset 目标偏移量 * @param len 目标数据长度 * @param msg 输入消息对象 * @return 编码完成所用的字节数,如果 < 0 表示出错 / virtual int Encode(char buf, int offset, int len, const Response& msg) = 0; /** * 编码,把msg编码到buf里面,返回写入了多长的数据,如果超过了 len,则返回-1表示错误。 * 如果返回 0 ,表示不需要编码,框架会直接从 msg 的缓冲区读取数据发送。 * @param buf 目标数据缓冲区 * @param offset 目标偏移量 * @param len 目标数据长度 * @param msg 输入消息对象 * @return 编码完成所用的字节数,如果 < 0 表示出错 / virtual int Encode(char buf, int offset, int len, const Notice& msg) = 0; /** * 开始编码,会返回即将解码出来的消息类型,以便使用者构造合适的对象。 * 实际操作是在进行“分包”操作。 * @param buf 输入缓冲区 * @param offset 输入偏移量 * @param len 缓冲区长度 * @param msg_type 输出参数,表示下一个消息的类型,只在返回值 > 0 的情况下有效,否则都是 TypeError * @return 如果返回0表示分包未完成,需要继续分包。如果返回-1表示协议包头解析出错。其他返回值表示这个消息包占用的长度。 / virtual int DecodeBegin(const char buf, int offset, int len, MessageType* msg_type) = 0; /** * 解码,把之前DecodeBegin()的buf数据解码成具体消息对象。 * @param request 输出参数,解码对象会写入此指针 * @return 返回0表示成功,-1表示失败。 / virtual int Decode(Request request) = 0; /** * 解码,把之前DecodeBegin()的buf数据解码成具体消息对象。 * @param request 输出参数,解码对象会写入此指针 * @return 返回0表示成功,-1表示失败。 / virtual int Decode(Response response) = 0; /** * 解码,把之前DecodeBegin()的buf数据解码成具体消息对象。 * @param request 输出参数,解码对象会写入此指针 * @return 返回0表示成功,-1表示失败。 / virtual int Decode(Notice notice) = 0;protected: Protocol() { }};这里有一点需要注意,由于 C++ 没有内存垃圾搜集和反射的能力,在解释数据的时候,并不能一步就把一个 char[] 转换成某个子类对象,而必须分成两步处理。先通过 DecodeBegin() 来返回,将要解码的数据是属于哪个子类型的。同时完成分包的工作,通过返回值来告知调用者,是否已经完整的收到一个包。调用对应类型为参数的 Decode() 来具体把数据写入对应的输出变量。对于 Protocol 的具体实现子类,我首先实现了一个 LineProtocol ,是一个非常不严谨的,基于文本ASCII编码的,用空格分隔字段,用回车分包的协议。用来测试这个框架是否可行。因为这样可以直接通过 telnet 工具,来测试协议的编解码。然后我按照 TLV (Type Length Value)的方法设计了一个二进制的协议。大概的定义如下:协议分包: [消息类型:int:2] [消息长度:int:4] [消息内容:bytes:消息长度]消息类型取值:0x00 Error0x01 Request0x02 Response0x03 Notice包类型字段编码细节Request服务名字段:int:2[字符串内容:chars:消息长度]序列号字段:int:2 会话ID字段:int:2 消息体字段:int:2[字符串内容:chars:消息长度] Response服务名字段:int:2[字符串内容:chars:消息长度]序列号字段:int:2 会话ID字段:int:2 消息体字段:int:2[字符串内容:chars:消息长度] Notice服务名字段:int:2[字符串内容:chars:消息长度]消息体字段:int:2[字符串内容:chars:消息长度] 一个名为 TlvProtocol 的类型完成对这个协议的实现。Processor处理器层是我设计用来对接具体业务逻辑的抽象层,它主要通过输入参数 Request 和 Peer 来获得客户端的输入数据,然后通过 Server 类的 Reply()/Inform() 来返回 Response 和 Notice 消息。实际上 Transport 和 Protocol 的子类们,都属于 net 模块,而各种 Processor 和 Server/Client 这些功能类型,属于另外一个 processor 模块。这样设计的原因,是希望所有 processor 模块的代码单向的依赖 net 模块的代码,但反过来不成立。Processor 基类非常简单,就是一个处理函数回调函数入口 Process():///@brief 处理器基类,提供业务逻辑回调接口class Processor {public: Processor(); virtual ~Processor(); /** * 初始化一个处理器,参数server为业务逻辑提供了基本的能力接口。 / virtual int Init(Server server, Config* config = NULL); /** * 处理请求-响应类型包实现此方法,返回值是0表示成功,否则会被记录在错误日志中。 * 参数peer表示发来请求的对端情况。其中 Server 对象的指针,可以用来调用 Reply(), * Inform() 等方法。如果是监听多个服务器,server 参数则会是不同的对象。 / virtual int Process(const Request& request, const Peer& peer, Server server); /** * 关闭清理处理器所占用的资源 / virtual int Close();};设计完 Transport/Protocol/Processor 三个通信处理层次后,就需要一个组合这三个层次的代码,那就是 Server 类。这个类在 Init() 的时候,需要上面三个类型的子类作为参数,以组合成不同功能的服务器,如:TlvProtocol tlv_protocol; // Type Length Value 格式分包协议,需要和客户端一致TcpTransport tcp_transport; // 使用 TCP 的通信协议,默认监听 0.0.0.0:6666EchoProcessor echo_processor; // 业务逻辑处理器Server server; // DenOS 的网络服务器主对象server.Init(&tcp_transport, &tlv_protocol, &echo_processor); // 组装一个游戏服务器对象:TLV 编码、TCP 通信和回音服务Server 类型还需要一个 Update() 函数,让用户进程的“主循环”不停的调用,用来驱动整个程序的运行。这个 Update() 函数的内容非常明确:检查网络是否有数据需要处理(通过 Transport 对象)有数据的话就进行解码处理(通过 Protocol 对象)解码成功后进行业务逻辑的分发调用(通过 Processor 对象)另外,Server 还需要处理一些额外的功能,比如维护一个会话缓存池(Session),提供发送 Response 和 Notice 消息的接口。当这些工作都完成后,整套系统已经可以用来作为一个比较“通用”的网络消息服务器框架存在了。剩下的就是添加各种 Transport/Protocol/Processor 子类的工作。class Server {public: Server(); virtual ~Server(); /* * 初始化服务器,需要选择组装你的通信协议链 / int Init(Transport transport, Protocol* protocol, Processor* processor, Config* config = NULL); /** * 阻塞方法,进入主循环。 / void Start(); /* * 需要循环调用驱动的方法。如果返回值是0表示空闲。其他返回值表示处理过的任务数。 / virtual int Update(); void ClosePeer(Peer peer, bool is_clear = false); //关闭当个连接,is_clear 表示是否最终整体清理 /** * 关闭服务器 / void Close(); /* * 对某个客户端发送通知消息, * 参数peer代表要通知的对端。 / int Inform(const Notice& notice, const Peer& peer); /* * 对某个 Session ID 对应的客户端发送通知消息,返回 0 表示可以发送,其他值为发送失败。 * 此接口能支持断线重连,只要客户端已经成功连接,并使用旧的 Session ID,同样有效。 / int Inform(const Notice& notice, const std::string& session_id); /* * 对某个客户端发来的Request发回回应消息。 * 参数response的成员seqid必须正确填写,才能正确回应。 * 返回0成功,其它值(-1)表示失败。 / int Reply(Response response, const Peer& peer); /** * 对某个 Session ID 对应的客户端发送回应消息。 * 参数 response 的 seqid 成员系统会自动填写会话中记录的数值。 * 此接口能支持断线重连,只要客户端已经成功连接,并使用旧的 Session ID,同样有效。 * 返回0成功,其它值(-1)表示失败。 / int Reply(Response response, const std::string& session_id); /** * 会话功能 / Session GetSession(const std::string& session_id = “”, bool use_this_id = false); Session* GetSessionByNumId(int session_id = 0); bool IsExist(const std::string& session_id); };有了 Server 类型,肯定也需要有 Client 类型。而 Client 类型的设计和 Server 类似,但就不是使用 Transport 接口作为传输层,而是 Connector 接口。不过 Protocol 的抽象层是完全重用的。Client 并不需要 Processor 这种形式的回调,而是直接传入接受数据消息就发起回调的接口对象 ClientCallback。class ClientCallback {public: ClientCallback() { } virtual ~ClientCallback() { // Do nothing } /** * 当连接建立成功时回调此方法。 * @return 返回 -1 表示不接受这个连接,需要关闭掉此连接。 / virtual int OnConnected() { return 0; } /* * 当网络连接被关闭的时候,调用此方法 / virtual void OnDisconnected() { // Do nothing } /* * 收到响应,或者请求超时,此方法会被调用。 * @param response 从服务器发来的回应 * @return 如果返回非0值,服务器会打印一行错误日志。 / virtual int Callback(const Response& response) { return 0; } /* * 当请求发生错误,比如超时的时候,返回这个错误 * @param err_code 错误码 / virtual void OnError(int err_code){ WARN_LOG(“The request is timeout, err_code: %d”, err_code); } /* * 收到通知消息时,此方法会被调用 / virtual int Callback(const Notice& notice) { return 0; } /* * 返回此对象是否应该被删除。此方法会被在 Callback() 调用前调用。 * @return 如果返回 true,则会调用 delete 此对象的指针。 / virtual bool ShouldBeRemoved() { return false; }};class Client : public Updateable { public: Client(); virtual ~Client(); /* * 连接服务器 * @param connector 传输协议,如 TCP, UDP … * @param protocol 分包协议,如 TLV, Line, TDR … * @param notice_callback 收到通知后触发的回调对象,如果传输协议有“连接概念”(如TCP/TCONND),建立、关闭连接时也会调用。 * @param config 配置文件对象,将读取以下配置项目:MAX_TRANSACTIONS_OF_CLIENT 客户端最大并发连接数; BUFFER_LENGTH_OF_CLIENT客户端收包缓存;CLIENT_RESPONSE_TIMEOUT 客户端响应等待超时时间。 * @return 返回 0 表示成功,其他表示失败 / int Init(Connector connector, Protocol* protocol, ClientCallback* notice_callback = NULL, Config* config = NULL); /** * callback 参数可以为 NULL,表示不需要回应,只是单纯的发包即可。 / virtual int SendRequest(Request request, ClientCallback* callback = NULL); /** * 返回值表示有多少数据需要处理,返回-1为出错,需要关闭连接。返回0表示没有数据需要处理。 / virtual int Update(); virtual void OnExit(); void Close(); Connector connector() ; ClientCallback* notice_callback() ; Protocol* protocol() ;};至此,客户端和服务器端基本设计完成,可以直接通过编写测试代码,来检查是否运行正常。此文已由腾讯云+社区在各渠道发布,一切权利归作者所有获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号 ...

March 5, 2019 · 7 min · jiezi

刚刚,阿里开源 iOS 协程开发框架 coobjc!

阿里妹导读:刚刚,阿里巴巴正式对外开源了基于 Apache 2.0 协议的协程开发框架 coobjc,开发者们可以在 Github 上自主下载。coobjc是为iOS平台打造的开源协程开发框架,支持Objective-C和Swift,同时提供了cokit库为Foundation和UIKit中的部分API提供了协程化支持,本文将为大家详细介绍coobjc的设计理念及核心优势。开源地址https://github.com/alibaba/coobjciOS异步编程问题从2008年第一个iOS版本发布至今的11年时间里,iOS的异步编程方式发展缓慢。基于 Block 的异步编程回调是目前 iOS 使用最广泛的异步编程方式,iOS 系统提供的 GCD 库让异步开发变得很简单方便,但是基于这种编程方式的缺点也有很多,主要有以下几点:容易进入"嵌套地狱"错误处理复杂和冗长容易忘记调用 completion handler条件执行变得很困难从互相独立的调用中组合返回结果变得极其困难在错误的线程中继续执行(如子线程操作UI)难以定位原因的多线程崩溃(手淘中多线程crash已占比60%以上)锁和信号量滥用带来的卡顿、卡死针对多线程以及尤其引发的各种崩溃和性能问题,我们制定了很多编程规范、进行了各种新人培训,尝试降低问题发生的概率,但是问题依然很严峻,多线程引发的问题占比并没有明显的下降,异步编程本来就是很复杂的事情,单靠规范和培训是难以从根本上解决问题的,需要有更加好的编程方式来解决。解决方案上述问题在很多系统和语言开发中都可能会碰到,解决问题的标准方式就是使用协程,C#、Kotlin、Python、Javascript 等热门语言均支持协程极其相关语法,使用这些语言的开发者可以很方便的使用协程及相关功能进行异步编程。2017 年的 C++ 标准开始支持协程,Swift5 中也包含了协程相关的标准,从现在的发展趋势看基于协程的全新的异步编程方式,是我们解决现有异步编程问题的有效的方式,但是苹果基本已经不会升级 Objective-C 了,因此使用Objective-C的开发者是无法使用官方的协程能力的,而最新 Swift 的发布和推广也还需要时日,为了让广大iOS开发者能快速享受到协程带来的编程方式上的改变,手机淘宝架构团队基于长期对系统底层库和汇编的研究,通过汇编和C语言实现了支持 Objective-C 和 Swift 协程的完美解决方案 —— coobjc。核心能力提供了类似C#和Javascript语言中的Async/Await编程方式支持,在协程中通过调用await方法即可同步得到异步方法的执行结果,非常适合IO、网络等异步耗时调用的同步顺序执行改造。提供了类似Kotlin中的Generator功能,用于懒计算生成序列化数据,非常适合多线程可中断的序列化数据生成和访问。提供了Actor Model的实现,基于Actor Model,开发者可以开发出更加线程安全的模块,避免由于直接函数调用引发的各种多线程崩溃问题。提供了元组的支持,通过元组Objective-C开发者可以享受到类似Python语言中多值返回的好处。内置系统扩展库提供了对NSArray、NSDictionary等容器库的协程化扩展,用于解决序列化和反序列化过程中的异步调用问题。提供了对NSData、NSString、UIImage等数据对象的协程化扩展,用于解决读写IO过程中的异步调用问题。提供了对NSURLConnection和NSURLSession的协程化扩展,用于解决网络异步请求过程中的异步调用问题。提供了对NSKeyedArchieve、NSJSONSerialization等解析库的扩展,用于解决解析过程中的异步调用问题。coobjc设计最底层是协程内核,包含了栈切换的管理、协程调度器的实现、协程间通信channel的实现等。中间层是基于协程的操作符的包装,目前支持async/await、Generator、Actor等编程模型。最上层是对系统库的协程化扩展,目前基本上覆盖了Foundation和UIKit的所有IO和耗时方法。核心实现原理协程的核心思想是控制调用栈的主动让出和恢复。一般的协程实现都会提供两个重要的操作:Yield:是让出cpu的意思,它会中断当前的执行,回到上一次Resume的地方。Resume:继续协程的运行。执行Resume后,回到上一次协程Yield的地方。我们基于线程的代码执行时候,是没法做出暂停操作的,我们现在要做的事情就是要代码执行能够暂停,还能够再恢复。 基本上代码执行都是一种基于调用栈的模型,所以如果我们能把当前调用栈上的状态都保存下来,然后再能从缓存中恢复,那我们就能够实现yield和 resume。实现这样操作有几种方法呢?第一种:利用glibc 的 ucontext组件(云风的库)。第二种:使用汇编代码来切换上下文(实现c协程),原理同ucontext。第三种:利用C语言语法switch-case的奇淫技巧来实现(Protothreads)。第四种:利用了 C 语言的 setjmp 和 longjmp。第五种:利用编译器支持语法糖。上述第三种和第四种只是能过做到跳转,但是没法保存调用栈上的状态,看起来基本上不能算是实现了协程,只能算做做demo,第五种除非官方支持,否则自行改写编译器通用性很差。而第一种方案的 ucontext 在iOS上是废弃了的,不能使用。那么我们使用的是第二种方案,自己用汇编模拟一下 ucontext。模拟ucontext的核心是通过getContext和setContext实现保存和恢复调用栈。需要熟悉不同CPU架构下的调用约定(Calling Convention). 汇编实现就是要针对不同cpu实现一套,我们目前实现了 armv7、arm64、i386、x86_64,支持iPhone真机和模拟器。Show me the code说了这么多,还是看看代码吧,我们从一个简单的网络请求加载图片功能来看看coobjc到底是如何使用的。下面是最普通的网络请求的写法:下面是使用coobjc库协程化改造后的代码:原本需要20行的代码,通过coobjc协程化改造后,减少了一半,整个代码逻辑和可读性都更加好,这就是coobjc强大的能力,能把原本很复杂的异步代码,通过协程化改造,转变成逻辑简洁的顺序调用。coobjc还有很多其他强大的能力,本文对于coobjc的实际使用就不过多介绍了,感兴趣的朋友可以去官方github仓库自行下载查看。性能提升我们在iPhone7 iOS11.4.1的设备上使用协程和传统多线程方式分别模拟高并发读取数据的场景,下面是两种方式得到的压测数据。测试机器:iPhone7 iOS11.4.1数据文件大小:20M协程最多使用线程数:4数据测试结果(统计的是所有并发访问结束的总耗时):从上面的表格我们可以看到使用在并发量很小的场景,由于多线程可以完全使用设备的计算核心,因此coobjc总耗时要比传统多线程略高,但是由于整体耗时都很小,因此差异并不明显,但是随着并发量的增大,coobjc的优势开始逐渐体现出来,当并发量超过1000以后,传统多线程开始出现线程分配异常,而导致很多并发任务并没有执行,因此在上表中显示的是大于20秒,实际是任务已经无法正常执行了,但是coobjc仍然可以正常运行。我们在手机淘宝这种超级App中尝试了协程化改造,针对部分性能差的页面,我们发现在滑动过程中存在很多主线程IO调用、数据解析,导致帧率下降严重,通过引入coobjc,在不改变原有业务代码的基础上,通过全局hook部分IO、数据解析方法,即可让原来在主线程中同步执行的IO方法异步执行,并且不影响原有的业务逻辑,通过测试验证,这样的改造在低端机(iPhone6及以下的机器)上的帧率有20%左右的提升。优势简明概念少:只有很少的几个操作符,相比响应式几十个操作符,简直不能再简单了。原理简单:协程的实现原理很简单,整个协程库只有几千行代码。易用使用简单:它的使用方式比GCD还要简单,接口很少。改造方便:现有代码只需要进行很少的改动就可以协程化,同时我们针对系统库提供了大量协程化接口。清晰同步写异步逻辑:同步顺序方式写代码是人类最容易接受的方式,这可以极大的减少出错的概率。可读性高:使用协程方式编写的代码比block嵌套写出来的代码可读性要高很多。性能调度性能更快:协程本身不需要进行内核级线程的切换,调度性能快,即使创建上万个协程也毫无压力。减少卡顿卡死: 协程的使用以帮助开发减少锁、信号量的滥用,通过封装会引起阻塞的IO等协程接口,可以从根源上减少卡顿、卡死,提升应用整体的性能。总结程序是写来给人读的,只会偶尔让机器执行一下。——Abelson and Sussman基于协程实现的编程范式能够帮助开发者编写出更加优美、健壮、可读性更强的代码。协程可以帮助我们在编写并发代码的过程中减少线程和锁的使用,提升应用的性能和稳定性。本文作者:淘宝技术阅读原文本文来自云栖社区合作伙伴“ 阿里技术”,如需转载请联系原作者。

March 1, 2019 · 1 min · jiezi

Spring Cloud Alibaba迁移指南(四):零代码兼容 Api-Gateway

自 Spring Cloud 官方宣布 Spring Cloud Netflix 进入维护状态后,我们开始制作《Spring Cloud Alibaba迁移指南》系列文章,向开发者提供更多的技术选型方案,并降低迁移过程中的技术难度。第一篇:一行代码从 Hystrix 迁移到 Sentinel第二篇:零代码替换 Eureka第三篇:极简的 Config如果你为 Api-Gateway(可能是 Zuul,也可能是 spring cloud gateway) 选择了 Eureka 为注册中心, 找不到一个合适的替换方案而苦苦烦恼时,那接下来的内容将是非常值得你一读。Spring Cloud Alibaba 不管是开源的服务注册组件还是商业化,都实现了 Spring Cloud 服务注册的标准规范。这就天然的给开发者提供了一种非常便利的方式将服务注册中心的 Eureka 迁移到开源的 Nacos。兼容 Api-Gateway:零代码替换 Eureka使用 Spring Cloud Alibaba 的开源组件 spring-cloud-starter-alibaba-nacos-discovery 来替换 Eureka,兼容 Api-Gateway(注意: 这里的 Api-Gateway 是一个统称,有可能是基于 Zuul 来实现,也有能可能是基于 spring cloud gateway 来实现。)仅需要完成以下几个简单的步骤即可。环境准备工作:本地需要安装 Nacos。Nacos 的安装方式也是极其的简单,参考 Nacos 官网。假设现在已经正常启动了 Nacos 。添加 Nacos 的 pom 依赖,同时去掉 Eureka。 在需要替换的工程目录下找到 maven 的配置文件 pom.xml。添加如下的 pom 依赖:<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>0.2.1.RELEASE</version> </dependency>同时将依赖的 spring-cloud-starter-netflix-eureka-client pom 给去掉。 application.properties 配置。 一些关于 Nacos 基本的配置也必须在 application.properties(也可以是application.yaml)配置,如下所示:spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848同时将与 Eureka 相关的配置删除。(可选) 更换 EnableEurekaClient 注解。 如果在你的应用启动程序类加了 EnableEurekaClient 注解,这个时候需要更符合 Spring Cloud 规范的一个注解 EnableDiscoveryClient 。注意:以上几个步骤不仅仅是在集成 Api-Gateway 网关的项目中做相应的更改,通过 Api-Gateway 网关进行转发的后端服务也都要做相应的更改。完成以上三个步骤,就已经兼容了 Api-Gateway 网关的路由转发。关于如何使用 Spring Cloud Alibaba 的商业化组件 ANS 来替换掉 Api-Gateway 的注册中心 Eureka,详细的文档可参考这里。至此,《Spring Cloud Alibaba迁移指南》系列文章的四篇已全部,若您在迁移过程遇到了其他难题,欢迎到Spring Cloud Alibaba@GitHub 提issue。本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 1, 2019 · 1 min · jiezi

Spring Cloud Alibaba迁移指南(二):零代码替换 Eureka

自 Spring Cloud 官方宣布 Spring Cloud Netflix 进入维护状态后,我们开始制作《Spring Cloud Alibaba迁移指南》系列文章,向开发者提供更多的技术选型方案,并降低迁移过程中的技术难度。第二篇,Spring Cloud Alibaba 实现了 Spring Cloud 服务注册的标准规范,这就天然的给开发者提供了一种非常便利的方式将服务注册中心的 Eureka 迁移到开源的 Nacos 。 第一篇回顾:一行代码从 Hystrix 迁移到 Sentinel零代码使用 Nacos 替换 Eureka如果你需要使用 Spring Cloud Alibaba 的开源组件 spring-cloud-starter-alibaba-nacos-discovery 来替换 Eureka。需要完成以下几个简单的步骤即可。1. __本地需要安装 Nacos。__Nacos 的安装方式也是极其的简单,参考 Nacos 官网。假设现在已经正常启动了 Nacos 。2. 添加 Nacos 的 pom 依赖,同时去掉 Eureka。 在需要替换的工程目录下找到 maven 的配置文件 pom.xml。添加如下的 pom 依赖:<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>0.2.1.RELEASE</version> </dependency></dependencies>同时将依赖的 spring-cloud-starter-netflix-eureka-client pom 给去掉。 3. application.properties 配置。 一些关于 Nacos 基本的配置也必须在 application.properties(也可以是application.yaml)配置,如下所示: application.properties:spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848同时将和 Eureka 相关的配置删除。4. (可选) 更换 EnableEurekaClient 注解。如果在你的应用启动程序类加了 EnableEurekaClient 注解,这个时候需要更符合 Spring Cloud 规范的一个注解EnableDiscoveryClient 。直接启动你的应用即可。到目前为止,就已经完成了 “零行代码使用 Nacos 替换 Eureka”。完整的方式可参考 Spring Cloud Alibaba 的官方 Wiki 文档。零代码使用 ANS 替换 Eureka如果你需要使用 Spring Cloud Alibaba 的商业化组件 spring-cloud-starter-alicloud-ans 来替换 Eureka。也是仅需完成以下几个简单的步骤即可。1. 本地需要安装 轻量版配置中心。 轻量版配置中心的下载和启动方式可参考 这里。假设现在已经正常启动了轻量版配置中心 。2. 添加 ANS 的 pom 依赖,同时去掉 Eureka。 在需要替换的工程目录下找到 maven 的配置文件 pom.xml。添加如下的 pom 依赖:<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-alicloud-ans</artifactId> <version>0.2.1.RELEASE</version> </dependency></dependencies>同时将依赖的 org.springframework.cloud:spring-cloud-starter-netflix-eureka-client pom 给去掉。 3. (可选) application.properties 配置。 一些关于 ANS 基本的配置也可以在 application.properties(也可以是application.yaml)配置,如下所示: application.properties:spring.cloud.alicloud.ans.server-list=127.0.0.1spring.cloud.alicloud.ans.server-port=8080如果不配置的话,默认值就是 127.0.0.1 和 8080 ,因此这一步是可选的。同时将和 Eureka 相关的配置删除。4. (可选) 更换 EnableEurekaClient 注解。如果在你的应用启动程序类加了 EnableEurekaClient 注解,这个时候需要更符合 Spring Cloud 规范的一个注解EnableDiscoveryClient 。代码层面不需要改动任何代码,直接启动你的应用即可。到目前为止,就已经完成了 “零代码使用 ANS 替换 Eureka”。完整的使用方式可参考 Spring Cloud Alibaba 的官方 Wiki 文档。本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 27, 2019 · 1 min · jiezi

开源中国专访:Chameleon原理首发,其它跨多端统一框架都是假的?

开源中国专访:Chameleon原理首发,其它跨多端统一框架都是假的?原创: 嘉宾-张楠 开源中国 以往我们说某一功能跨多端,往往是指在诸如 PC、移动等不同类型的设备之间都能实现;或者更加具体一点,指的是“跨平台”,可能是大到跨操作系统,比如 Windows、macOS、Linux、iOS 与 Android 等,可能是小到跨某个具体技术的不同实现库。但是今天我们要介绍的是关于跨 MVVM 架构模式各种环境的场景。Chameleon 是一套开源跨端解决方案,它的目标是让 MVVM 跨端环境大一统,实现任意使用 MVVM 架构设计的终端,都能使用其进行开发并运行。 在这样一个 MVVM 环境中,涉及到了 Weex、React-Native、WebView/浏览器与 Flutter 等各种跨端技术,还有它们实现的具体业务产品,比如微信小程序、快应用、支付宝小程序、百度智能小程序、今日头条小程序与其它各类小程序。也许你发现了,这里提到了许多种“小程序”,虽然最早微信小程序的概念甚至早期版本出现的时候,有过不少不看好的声音,但是随着它不断发展,目前已经成为了大众生活不可或缺的应用形态。马化腾透露过,截至 2018 年 11 月有 150 万微信小程序开发者,小程序应用数量超过 100 万,覆盖 200 多个细分行业,日活用户达到 2 亿。这样的成功经验与几乎触及到生活方方面面的巨大流量入口,大家都想入场,于是可以看到后来其它公司纷纷给出了类似的小程序方案。另一方面,除了小程序百花齐放,2018 年小米、华为、OPPO 等 10 家安卓手机厂商还结成了快应用联盟,并且先后发布了一系列快应用。Chameleon 目标就是要跨这些端,而随着各家不同实现越来越多,跨端场景也不断变得更加复杂。我们采访了 Chameleon 创始人张楠,请他为读者具体分享了 Chameleon 在这个过程中的成长。项目地址:https://github.com/didi/chame…本文是 Chameleon 首次对外公开实现原理!干货超多,包括:终端开发未来的开发模式Chameleon 跨端实现原理当前各种跨端方案原理对比(各种小程序、快应用等)与 Taro 的对比演进过程中遇到的困难与思考当初为什么去研发 Chameleon?关于这个问题可以从行业背景讲起。中国互联网络信息中心(CNNIC)发布的《中国互联网络发展状况统计报告》显示,截至 2018 年 6 月,我国网民规模达 8.02 亿人,微信月活 10 亿 、支付宝月活 4 亿、百度月活 3.3 亿;另一方面,2018 Q3 中国 Android 手机占智能手机整体的比例超过 80%,月活约 6 亿。BAT 与 Android 成为了中国互联网真正的用户入口。但凡流量高的入口级别 APP 都希望做平台,成为一个生态平台和互联网流量入口,大量第三方应用的接入,从业务层让公司 APP 关联上更多企业的利益,并且拥有更强的生命力;从技术层面可以利用“本地能力接口层”收集大量用户数据,从消费互联网到产业互联网需要大量各行各业基础用户数据线索进行驱动和决策。在这么一种背景下,再结合计算机技术的发展历史,我们知道每一种新技术的出现都会经历“各自为政”的阶段,小程序技术也不例外,所以我们看到了其它各种小程序平台出现。微信小程序作为首创者,虽然其它小程序都有在技术实现原理、接口设计上刻意模仿,但是作为一线开发者在不同平台发布小程序,往往还是需要重复开发、测试,从前 1 单位的工作量变成了 N 单位的工作量。而这还没算上快应用等其它入口。这种情况下,滴滴的研发工程师是其中最显著的“受害者”之一,滴滴出行在微信钱包、支付宝、Android 快应用都有相关入口,而且用户流量占比不低。研发同学在端内既追求 H5 的灵活性,也要追求性能趋近于原生。面对入口扩张,主端、独立端、微信小程序、支付宝小程序、百度小程序、安卓厂商联盟快应用,单一功能在各平台都要重复实现,开发和维护成本成倍增加。迫切需要一个只维护一套代码就可以构建多入口的解决方案,于是我们着手去打造了 Chameleon(CML,卡梅龙)这么一个项目,真正专注于让一套代码运行多端。Chameleon 核心是运用了 MVVM 架构,为什么它可以实现跨多端?MVVM 也就是 Model View ViewModel,它本质上是 MVC( Model View Controller)的进化版本,将 View 的状态和行为抽象化,使得视图 UI 和业务逻辑分开。它是一种让数据驱动反射视图的模式,发展到现在可能会偏离它的初衷了,更像是一个视图数据间的“通信协议”,让终端开发变得更加单纯,这是一种趋势,面向未来框架都采用这种模式。Facebook 在 2013 年开源 React,React 这个项目本身是一个 Web UI 引擎,随着不断发展,它衍生出 React Native 项目,用来编写原生移动应用。正是它给跨端方向带来了 MVVM 模式。Vue.js 于 2014 年左右发布,逆流而上占据了大量用户群体,2016 阿里巴巴也基于它发布了 Weex 项目,使得可以用 Vue 编写 Native App。Google 在 2018 年末正式发布了面向未来的跨 Android、iOS 端的 Flutter 1.0.0。原理我们知道终端开发离不开三大要素——界面表现(结构、外观)层、逻辑处理层与系统接口层(网络、存储与媒体等)。开发者编写代码时在初始化阶段(生命周期)调用“界面表现层”界面模型的接口绘制界面,当用户触摸界面时,“界面表现层”将事件发送给用户“逻辑处理层”,后者经过条件判断再处理并反馈到用户界面,处理过程可能需要调用“系统接口层”,反馈过程需要调用“界面表现层”的接口。常规的终端开发架构模式下,无论是 Web 端、Android 端还是 iOS 端的项目开发,都强依赖各端的环境接口,特别是依赖界面相关模型设计。iOS 系统下绘制界面基于 Objective-C 语言环境下的 UIKit 框架;Android 系统下用户绘制界面基于 Java 语言环境,由 LayoutInflater 处理 XML 结构层次树;Web 端使用 DOM 模型和 CSS 来描述绘制界面。 MVVM 中的关键是它通过 ViewModel 这一层将界面和逻辑层彻底隔离开来,负责关联界面表现和逻辑处理层的响应事件(update/notify)关系,这一“隔离层”上下通信足够规范、足够纯净单一。 Model 进行逻辑处理是纯业务响应逻辑,任何一种语言都可以实现,你可以用 Android 的 Java,也可以用 iOS 的 Objective-C,你心情好用“世界第一语言 PHP”也能实现。之所以普遍选择 JavaScript,很大程度是因为在这个领域内它的优点显著,如学习成本低、天生具备跨端属性、虚拟机(V8、JavaScriptCore)和各方向组件建设较好、生态活跃。而系统接口层则更简单了,只需穷举统一基础接口+可扩展接口能力即可。各种 MVVM 方案具体来看看各种 MVVM 方案都是怎么样的。React Native、Weex 与快应用的 MVVM开发者编写的代码在虚拟机(V8、JavaScriptCore)里面运行,虚拟机容器里面包含扩展的系统基础接口。运行时,将描述界面的数据(主要是 CSS+DSL 所描述内容)通过通信层传递给 Android、iOS 端的渲染引擎,用户触摸界面时,通过通信层传递给虚拟机里面的业务处理代码,业务处理代码可能调用网络、储存与媒体等接口,最后再次反馈到界面。Flutter 的 MVVMFlutter 和 RN 的最大区别在于将“JavascriptCore/V8+JS”替换成“C++ 实现的 engine+Dart 实现的 Framework+静态类型 Dart+编译成机器码”。Flutter 的方案如下图所示:Service 其实就是本地能力接口层,Widget 树是视图层模型。Flutter 和 RN 的使用面设计上类似,Flutter 文档中提到“In Flutter, almost everything is a widget.”,widget 的调用从 RN 的 JSX 变成 Flutter 的 widget 调用,UI 的外观描述从 RN 的 CSS(文本样式、布局模型、盒模型)到定制化 Flutter Widget(textStyle 、Layout Widget、Widget)。本质上 Flutter 也是 MVVM 架构,逻辑层通过 setState 通知视图层更新,一定程度上这也是为什么 Flutter 敢说能转成 Web 框架的原因,核心还是基于这类数据驱动视图架构模式,业务代码不会深度依赖任何一端特有的“视图模型”。各类小程序的 MVVM小程序本质上和 Weex、React Native 的设计思路基本一样,最大区别在于前者还是用浏览器 WebView 做渲染引擎,而后者是单独实现了渲染引擎(所以大量的 CSS 布局模型不支持)。具体到 Chameleon 上是怎么实现的?首先任何一份应用层的高级语言代码块分成几层:语言层(Language)、框架层(Framewrok)与库层(Library):Language —— 通俗来说,实现程序所需的基本逻辑命令:逻辑判断(if)、循环(for)与函数调用(foo())等。Framewrok —— 通俗来说,完成一个 App 应用交互任务所需规范,例如生命周期(onLoad、onShow)、模块化与数据管理等。Library —— 可以理解就是“方法封装集合”。比如 Web 前端中 Vue 更适合叫框架,而 jQuery 更适合叫库;Android 系统下 activity manager + window Manager View System 等的集合叫框架,而 SQLite 、libc 更适合叫库。对应到 Chameleon 就是这样:具体到实现原理全景架构图如下:你可以理解 Chameleon 为了实现“让 MVVM 跨端环境大统一”的目标做了以下工作:定义了标准的 Language(CML DSL)、Framework 与 Library(内置组件和 API)协议层。在线下编译时将 DSL 转译成各端 DSL,只编译 Language 层面足够基础且稳定的代码。在各个端运行时分别实现了 Framework 统一,在各个端尽量使用原有框架,方便利用其生态,这样很多组件可以直接用起来。在各个端运行时分别实现了 Library(内置组件和 API)。为用户提供多态协议,方便扩展以上几方面的内容,触达底层端特殊属性,同时提升可维护性。实现思路很简单,所有设计为了 MVVM 标准化,不做多余设计,所以宏观的角度就像 Node.js(libuv)同时运行在 Windows 和 macOS 系统,都提供了一个跨平台抽象层。从 MVVM 角度来看的话:View(展现层)第三方 Render Engine:各类框架已有框架,浏览器的 Vue、Webview 里的小程序引擎、Android、iOS 里面的 React Native/Weex 引擎、甚至 Flutter 里面的 Dart Framework。Chameleon 内置组件库:多态协议定义统一组件 view、input、text、block 与 cell 等,它是界面组层的原始基类,衍生出多复杂界面功能。ViewModel(关联层)Chameleon 语法转译组件调用循环条件判断事件回调关联父子关系……Model(逻辑响应层)JavaScript 代码CML Runtime 框架Chameleon API:多态协议定义统一接口,cml.request、cml.store 等Chameleon 的跨多端方案给开发者的开发带来了极大的便利,具体表现是怎么样的?一句话:基于 Chameleon 开发,效率会越来越高。各个端的涌现,让原本是 1 的工作量因为多端存在而变成 N 倍,使用 Chameleon,工作量会变回 1.2。这多出来的 0.2 工作量是要处理各端的差异化功能,比如以下场景:某业务线迁入 Chameleon 时,发现没有“passport登录组件”,在各类小程序里面能免密登录了,在 Web、Native 端是弹出登录框登录,不同业务用户交互形态不一样所以 Chameleon 没有提供组件;开发者需要基于多态协议扩展单独一个登录组件<passport/>,无论如何最后返回一个登录后的回调 token 即可,外部无需组件关心里面如何操作。用户需要分享功能,发现没有“share组件”,在微信 Web 端可以引导右上角分享,在小程序直接分享,不同业务用户交互形态不一样,用户需要基于多态协议扩展单独一个登录组件<share/>。这种各端差异较大的例子,随着业务的积累,可以变成了一个个业务组件单独维护,后面也不需要重复开发了,且反推产品体验一致化,组件三层结构“CML框架内置组件->CML扩展组件->业务开发者自己扩展的多态组件”达成 100% 统一。随着组件积累业务开发工作量越来少,工程师可以专注做更加有意义的事情,这就是 Chameleon 存在的目的。基于统一的跨端抽象,用户在 Chameleon 项目持续维护过程中,Chameleon 发布新增一个端之后,你的业务代码基本不用改动即可无缝发布成新端。比如这个 cml-yanxuan 项目开发时支持 3 个端,后面新增了百度、支付宝小程序端,原有代码直接能跑起来运行 5 个端,一端所见即多端所见。开发时只能跑 3 个端原有代码无缝支持 5 个端另外特别强调的是,对于大公司团队,如果有很强的技术能力,希望开发的代码掌控在自己手里,对输出结果有更好控制能力。其实 Chameleon 内置组件和内置 API 是可以替换的,那么所有组件都是业务方自己开发了,哪天不想用了直接导出原生组件即可离开 Chameleon,如下图:目前跨多端统一的方案中,Taro 是比较亮眼的,能否具体对比一下 Chameleon 与 Taro。我们觉得 Chameleon 与其它解决方案的最大区别在于其它框架都是小程序增强,即用 Vue 或者 React 写小程序,这些框架官方给的已接入例子也都是跑微信小程序。它们更加类似 Chameleon 的前身 MPV(Mini Program View),即考虑如何增强小程序开发。2017 年微信小程序发布时,滴滴作为白名单用户首先开始尝试接入,开始面对重复开发的难题。这时候我们专门成立了一个小项目组,完成一个名为 MPV 的项目,一期目标是“不影响用户发挥,不依赖框架方的原则性实现一套代码运行 Web 和微信小程序”。看着很美好,用这样的方案实现 Web 端和小程序端,也确实完成了超过 90% 代码重用,总体上开发效率和测试效率都有了一定提升,但是却不是真正意义上的跨多端统一。单独说到 Chameleon 与 Taro 的区别,总体上看,可以归为这样一个表:表中每一项都是在做跨端方案时需要考虑到的。我们说除了 Chameleon,其它方案都只是在对小程序进行增强,或者说是模仿微信小程序的 API 和组件的接口设计。Taro 是通过将 JSX 转成小程序模板,在其它端模拟微信小程序的接口和组件,让其它端更像微信小程序,业务开发时不一致的地方需要环境变量判断差异分别调用,会造成端差异逻辑和产品逻辑混合在一起。此外,它要跟随小程序更新,业务方会有双重依赖;其它端的和小程序不能保持一致,用户要各种差异化兼容,不利于维护。那 Chameleon 呢?Chameleon 把这些问题都考虑到了,所以在早期伪跨端 MiniProgram View 成型之后不断演进的过程中,把它发展成为一个真正的跨多端方案。前边的表格显示了,Chameleon 既考虑统一性,又考虑差异性,且差异性不会影响可维护性;当各端差异确实太大,那就不要用一套代码实现多个端同一页面,而是统一公用组件。这还只是拿 Chameleon 与 Taro 的重合点进行了对比,但是别忘了 Chameleon 不仅仅是前端框架,它:还有统一的 Chameleon Native SDK,Chameleon 不仅仅希望统一各类小程序,还要覆盖自家 APP,会持续通过 Native SDK 扩展 API 和组件,期望有与小程序一样的本地能力。理想情况下,一套代码就能在各类小程序、自家 APP 里面无缝平滑运行。还有待开源的后台管理系统。还有待开源的 XEdtior 非研发用编辑器,可以直接编辑跨端页面、直接发布。另外,未来还将带来以下能力:后端统一接口(消息推送、分享与支付等)基于统一的 MVVM 标准,更有基于 Flutter 的原生 APP当前的各类小程序和 Native 跨端框架,类似当年多个浏览器时,Safari、Chrome、Firefox、IE 6/7/8/9、Android 浏览器等盛行的时代。以这个来类比,那么 Chameleon 的接口组件设计上更像一个 jQuery。网络请求有的是 XHRHttprequest 有的是 ActiveXObject,jQuery 考虑的是用户需要什么,需要一个网路请求接口,支持 get、post 等,所以 jQuery 写一个既非 ActiveXObject 又非 XHRHttprequest 的名为 $.ajax 接口,提供一个封装网络接口,你不用关心内部在不同端怎么调用的,jQuery 内部会帮你兼容。Chameleon 也是一样的思路,所有的接口设计都是真正能兼容跨所有的端,没有差异性,而且只保留当前所在端的接口调用代码:IE 里面只保留 ActiveXObject,Chrome 只保留 XHRHttprequest。Chameleon 的接口设计上比 jQuery 更强的地方在于,使用标准的多态协议,保障可维护性,性能上只保留当前端代码,且将多态协议暴露出来,让用户也能扩展自己想要的 API(类比 $.xxx)。当然时代已经变了,监听视图不在是 $(’#xxx’).click(fn),而是 MVVM 数据驱动视图方式了,所以提供了 Chameleon 双向绑定这样的 VM 层。前边讲到了 Chameleon 的前身 MPV,那具体分享一下 Chameleon 的整个演进过程吧。出生期:选择转译还是模拟小程序环境?前面讲到,2017 年的时候,我们完成一个名为 MPV 的项目,一期目标是不影响用户发挥,不依赖框架方的原则性实现一套代码运行 Web 和微信小程序。当时缺乏小程序资料是遇到的最大问题(就更别提今天讲到的业内这么多解决方案了),当时唯一一个可以参考的开源项目是 WEPT,WEPT 是一个微信小程序实时开发环境,它的目标是为小程序开发提供高效、稳定、友好、无限制的运行环境。它的设计思路是在 Web 端模仿小程序环境执行。于是我们在开发 MPV 时考虑了两种实现策略:1、在 Web 端像 WEPT 一样 mock 小程序环境;就像微信开发者工具里面也模拟了小程序执行环境,WAServie、WAWebview 提供的两套环境源码做底层,在页面中开启三个独立运行环境运行并用 iframe 通讯模拟微信小程序的 3 个 Webview 之间的联通关系。2、逐个转译代码支持小程序,缺点是可能会有 edge case 需要处理以及潜在的 bug 会比较多。最终在看完 WEPT 源码和微信开发者工具的情况下,我们明确放弃了第 1 条实现策略,选择了逐个转译代码支持小程序的路线,主要原因是于 Web 端兼容微信所有的功能,尺寸过于庞大。经过三个月紧锣密鼓的开发终于实现了第一版本 MPV: 经过实现几个 demo 之后,开始执行迁移计划: MPV 在 Webapp 上实践最终实现效果如下:最终实现效果挺美好,也确实完成了超过 90% 的代码重用,总体上开发效率和测试效率都有了明显提升。但是在后续实践过程中,发现存在大量的问题,并且项目越大问题越凸显出来,总结如下:可维护性问题,没有隔离公用代码和各端差异代码。项目中不止有业务逻辑,还混杂着 Web 端和小程序端产品功能差异化逻辑。比如前边举过的例子,分享功能 Web 端无法实现(引导分享),小程序可以实现,意味着各种环境判断各种差异化逻辑,牵一发动全身,还要来回测试。方向选择错误,MPV 使用了小程序语法标准(小程序的生命周期、API 接口等),导致用户使用上无法清晰理解。不能直接使用各端已有生态组件,即缺乏标准规范接入某个端已有开源组件。比如 Web 端 pick.js 组件缺乏快速接入规范,用户要么重新开发,或者在模板和 js 代码中使用环境判断的方式针对引入。最终导致同一功能不同端的调用方式、输入与输出不一致。业务项目依赖 MPV 框架。框架依赖微信小程序接口(模板、生命周期与接口),扩展了统一接口。例如微信小程序更新了 wx.request 时,业务项目方无法立刻使用,需要等框架更新。文件夹结构混乱,混杂着多个端代码文件,且识别成本高。不支持 vuex、redux 等高效数据管理方式尺寸单位不统一,px 和 rpx 不一致周边小型差异点太多:协议不一致,例如 Web 端可以用 //:www.didiglobal.com/passenger/create ,小程序只能用 https://:www.didiglobal.com/passenger/create打开一个新页面时链接不统一,例如打开发单页时,Web 端是 //:www.didiglobal.com/passenger/create,小程序是 /page/create页面之间跳转时,传参不统一debug 成本高,修改完代码之后两端需要测试两端界面效果不一致,基础内置组件统一性建设不足工程化建设落后,例如不支持 liveroload、数据 mock、资源定位、proxy、多端统一预览接口设计不完整,生命周期、组件分层、本地 API 设计等模板 DSL 语法不规范成长期:从伪统一到大一统在 MPV 的实践积累下,有了一定的底气和把握,后续的规划更加明确。2018 年 4 月我们把跨端项目规模进一步扩大,想要做一个真正跨 N 端的解决方案,目标是提供标准的 MVVM 架构开发模式统一各类终端。这就是 Chameleon 的出现契机。Chameleon 真正想要一套代码运行多端,总结下来要解决几大问题:要全面完成端开发的所有细节的统一性,特别是界面统一性有大量细节要做要在完成上一条的前提下考虑差异化定制空间持续可维护目标理想业务形态是这样的:图中上半部分是传统开发方式,下半部分 Chameleon 的模式抽象出了 UI 渲染层和本地接口能力层,业务代码一部分简单页面由 XEditor(h5Editor 的前身)编辑工具产出,另一部分工程师使用 Chameleon 开发,不止解决跨端问题,还弥补改进了工程开发过程中的效率、质量、性能与稳定性问题,让工程师专注有意义的业务,成长更快。首个 Native 渲染引擎选择——小程序架构、RN/Weex 架构从 MPV 到 Chameleon,外界看来最明显的变化是从跨 2 端(Web、小程序)升级到跨多端(Web、小程序、Android、iOS),最开始纠结于首个端上版本的渲染引擎使用小程序架构还是 RN/Weex 架构。RN/Weex 网上有大量资料可查,但是小程序方面则不然。千辛万苦搜索之后,根据一位知道内情的朋友的描述分享,才有了一定的了解。 这里分享几个印象深刻的要点:小程序展现层使用 Webview,里面内置了一套 JS 框架用来和 Native 通信,真正业务代码执行在单独 JS 虚拟机容器实例中JS 虚拟机容器使用情况,iOS 系统是 JavaScriptCore,Android 系统使用 QQ 浏览器的 X5 内核小程序的各个 TAG 组件使用的数据驱动用的是 Web Components显而易见,部分性能要求较高的使用原生控件(视频、键盘等等)插入到 Webview 里面。原生控件的具体位置 Native 怎么获取?答案是由嵌入到 Webview 的一套小程序框架通知给原生层原生控件怎么保证在内部可滚动的元素(Scroll-view)里面正常滚动?答案是 CSS 设置 -webkit-over-scroll:touch 时,iOS 的实现是原生的 UIScrollView,Native 可以通过一些黑科技找到视图层级中的 UIScrollView,然后对原生控件进行插入和处理;而 Android 直接绘制没办法做到这点。现在(截至 4 月)仅仅是直接覆盖到 Webview 最外层的 scrollview 上,由内置到 Webview 的一套 JS 框架控制原生控件位置最终多方面分析如下:虽然小程序方案看起来很简单,但其实很多细节点需要大量打磨,从确认方案到真正可以跑起来可以线上发布,仅仅花费在终端上的研发人力为 20P*6 个月,微信小程序团队的目标和我们跨端目标不一样,他们投入这么多成本是值得的,我们为了跨端没必要投入这么高成本。所以我们选择放弃小程序渲染方案,而使用已开源的 RN/Weex 方案。第一个版本最终使用 Weex,包括团队同学去看了 Weex 源码实现。在整体设计上仅仅使用 Weex 渲染功能,外层包装接口,保障后续能有更高扩展性。Chameleon Native SDK针对 Native SDK 我们主要从原生能力扩展、性能与稳定等三个方面做了工作。 原生能力扩展:无论是 Webview 还是 React Native、Weex 甚至 Flutter 都只提供渲染能力(以及一些最基础本地接口),更多完成业务功能所需本地环境的能力(例如分享到微信)需要 Android 和 iOS 的 Native 往容器去扩展。本地能力包含 2 种,涉及 UI 界面的统一叫组件(UI 组件如登录、支付),涉及到纯能力调用的统一叫 API(网络、存储等)性能:界面展现和交互耗时关键取决于 2 块,资源加载耗时(非打包到安装包部分代码)、执行耗时稳定:主要关注灰度发布(风险可控)和线上止损,主要工作是按用户灰度发布、可以快速降级到 H5以下是性能方向中的首屏加载时间的优化数据,原有 H5 使用 SSR(Server Side Render)已经算是最快的 Web 首屏技术方案了(不考虑优化后端多模块耗时的 BIGPIPE),它保持在 1.5 秒以下,在优化后降到 0.5 秒左右。 性能优化中我们有一个关于执行速度的 TODO 计划。同样是跨端,Flutter 之所以比 Weex 和 RN 执行速度快,主要原因是前者是编译型,客户端机器运行前已经是 CPU 可识别的机器码;后者是解释型,到客户端运行前是字符串,边编译边执行,虽然做了 JIT 尽量优化,差距还是较大。其实在这中间还有一个抹平了不同 CPU 架构下机器码差异的中间码;当然前提是开发语言改成静态类型,这里不作展开。原本分 5 次开发的 Web 端、支付宝小程序、快应用、微信小程序、Native 端变成了 1.2 次左右开发了。最重要的是随着业务级别各端差异化的多态组件和跨端组件积累,后续 1.2 工作量也会变成 0.8,0.4 的优化主要来自两个方面:0.2 是普通跨端组件的积累,复用度变高0.2 是各类业务级别的差异化多态组件,例如登录功能,在 Web端、Native 端和小程序端实现和交互是不一致的,这时候业务形态不一样,设计的 <passport> 组件也不一样,只能各业务线去封装。介绍一下接下来的 roadmap。我们的最终目标是提供标准的 MVVM 架构开发模式统一各类终端。接下来的具体 roadmap 如下表所示:欢迎有共同愿景的同学加入我们一起共建,往仓库贡献自己的代码。项目地址:https://github.com/didi/chame…QQ 群:公众号:采访嘉宾介绍张楠,Chameleon 创始人,技术团队负责人,前百度资深工程师,终身学习者。 ...

February 26, 2019 · 4 min · jiezi

指明方向与趋势!2019开发者技能报告出炉!!!

近日国外开发者平台 HankerRank 发布了 2019 年开发者技能调查报告( https://research.hackerrank.com/developer-skills/2019 ),该报告根据对71,281开发者的调查得出。2018 年最受欢迎的开发语言经过调查,2018年的所有开发语言中,JavaScript是最受欢迎的语言,2017年最受欢迎的语言是Java,今年被JavaScript超越,位居第二。2019年开发者最想学的语言报告调查了开发者最想学习的开发语言,结果显示,Go语言、Kotlin语言和Python语言位列前三。 Go语言 Go语言是谷歌2009发布的第二款开源编程语言。Go语言专门针对多处理器系统应用程序的编程进行了优化,使用Go编译的程序可以媲美C或C++代码的速度,而且更加安全、支持并行进程。Go语言是谷歌推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。Kotlin Kotlin 是一个用于现代多平台应用的静态编程语言 ,由 JetBrains 开发。Kotlin可以编译成Java字节码,也可以编译成JavaScript,方便在没有JVM的设备上运行。Kotlin已正式成为Android官方支持开发语言。Python Python是一种计算机程序设计语言。是一种动态的、面向对象的脚本语言,最初被设计用于编写自动化脚本(shell),随着版本的不断更新和语言新功能的添加,越来越多被用于独立的、大型项目的开发。2018年最闻名的开发框架2018年,最闻名的开发框架是AngularJS、其次是Spring。AngularJS AngularJS 是一个 JavaScript框架。它是一个以 JavaScript 编写的库。它可通过 标签添加到HTML 页面。Spring Spring是一个开放源代码的设计层面框架,它解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson创建。简单来说,Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架。2019最想学习的框架2019年,开发者最想学洗的框架是React,Java系的Spring排名第七。React React主要用于构建UI。你可以在React里传递多种类型的参数,如声明代码,帮助你渲染出UI、也可以是静态的HTML DOM元素、也可以传递动态变量、甚至是可交互的应用组件。最容易落地的新技术是什么最近几年,新技术层出不穷,如IoT(物联网)、深度学习、机器学习、计算机视觉、区块链、量子计算、AR(增强现实)、VR(虚拟现实)等。这些新技术,到底哪个在开发者心目中是最接近现实,目前看来最容易落地的呢。经过调查,IoT以53%占比获得第一名、量子计算排名最后,区块链倒数第二。找工作最看重什么不同程序员找工作的时候,会看重不同的东西,比如薪资、成长等。那么报告结果是如何的呢?初级开发者和高级开发者找工作最看重的东西排名前三名是一致的:个人成长和学习空间、其次是工作与生活的平衡,也就是加班的多少、排名第三的是有竞争力的薪酬。总结以上就是2018开发者技能报告的所有主要内容。报告中分别围绕开发者、编程语言等展开。涉及到多个方面。希望能对所有读者有所启发。活在当下,既要脚踏实地,也要仰望星空。作为一名程序员,我们也要时不时的抬起头,看一看自己所在的行业。本文作者:阿里高级开发工程师 洪亮阅读原文本文首发自微信公众号“Hollis”,如需转载请联系原作者。

February 19, 2019 · 1 min · jiezi

Python微型异步爬虫框架

AmipyPython微型异步爬虫框架(A micro asynchronous Python website crawler framework)基于Python 3.5 + 的异步async-await 框架,搭建一个模块化的微型异步爬虫。可以根据需求控制异步队列的长度和延迟时间等。配置了可以去重的布隆过滤器,网页内容正文过滤等,完全自主配置使用。GitHub地址:源码适用环境windows 7 +Python 3.5 +安装直接使用pip安装即可:pip install amipy基础命令1.查看当前路径下的可用命令,在DOS命令行下输入:>amipy会出现命令帮助界面。2.创建一个新的项目,在DOS命令行下输入:>amipy cproject myproject会在当前路径下创建一个Amipy爬虫项目myproject。如果想要创建在指定目录下,可以加上附加参数,-d,如:> amipy cproject myproject -d D:\somefolder项目myproject便会在路径D:somefolder下创建。项目的目录结构应该如下:–myproject |-spiders | |-init.py |-init.py |-settings.py其中:settings.py 为整个项目的配置文件,可以为整个项目下的爬虫安装共有的中间件,控制整个项目的请求并发数,设置日志级别、文件路径等。3.进入项目路径,创建一个新的爬虫,在DOS命令行下输入:>amipy cspider myspider此时在项目myproject目录下的spiders文件夹中会创建一个爬虫目录myspider,此时的项目结构为:–myproject |-spiders | |-init.py | |-myspider | | |-init.py | | |-cookies.info | | |-item.py | | |-settings.py | | |-site_record.info | | |-spider.py | | |-url_record.info |-init.py |-settings.py |-log.log其中:位于myspider文件夹下的settings.py为爬虫myspider的配置文件,该配置只对当前爬虫有效。可以对该爬虫的布隆过滤器进行配置,安装中间件等。cookies.info 为爬虫的请求cookie保存文件,该爬虫爬过的所有网站的cookie会保存进该文件。可以通过爬虫配置文件settings.py进行路径加载和保存。site_record.info 为爬虫爬取过的网站的布隆过滤器记录文件,方便下次爬取的时候加载,会把爬取过的网站自动去掉。防止重复爬取。url_record.info 为该爬虫发出的请求url+headers+method+数据的去重后集合,爬虫结束运行时,如果配置保存去重url集合。下次爬取时加载该文件可以自动过滤爬取过的所有url+headers+method+数据。item.py 为ORM的MongoDB数据集合对象,对应的类属性可以映射到数据库集合中的字段,类名为数据表名。spider.py 为当前爬虫的主要文件,自己编写爬取逻辑,提取规则和数据保存脚本等。4.运行项目下的所有爬虫,进入项目路径,在DOS命令行下输入:>amipy runproject则该项目下的所有爬虫会开始运行,如果不想运行某个爬虫,只需要加上参数 -e,如:>amipy runproject -e No1spider No2spider则名为“No1spider”、“No2spider”的爬虫均不会运行。5.运行指定的爬虫,进入项目路径,在DOS命令行下输入:>amipy runspider myspider01 则名为“myspider01”的爬虫便会被启动。可以加上多个爬虫名称,用空格隔开即可。6.列出当前项目下的所有爬虫信息。在DOS命令行下输入:>amipy list便会将当前项目下的所有爬虫信息列出。使用Amipy爬虫编写流程编写自己的爬虫。【假设你已经安装前面"基础命令"创建了一个项目,并且创建了一个爬虫名为myspider】只需要进入myspider文件夹,按照需求修改当前爬虫的配置settings.py 以及数据存储需要用到的表模型item.py编写,编辑文件spider.py,加入爬取规则逻辑等。Url类对象Url类对象是一个规则匹配类,它提供了许多种模式的url规则匹配。比如:from amipy import Url# 表示匹配到正则模式’http://www.170mv.com/song.‘的所有链接Url(re=‘http://www.170mv.com/song.’)# 表示匹配到正则模式’http://www.170mv.com/song.‘的所有链接其回调函数为’getmp3’Url(re=‘http://www.170mv.com/song/.’,callback=‘getmp3’)# 表示匹配到地址为http协议,且路径为‘/novel/chapter1’,参数number=2的所有链接Url(scheme=‘http’,path=’/novel/chapter1’,params=‘number=2’)# 表示匹配到域名为www.baidu.com的所有链接,为该链接请求设置代理为'127.0.0.1:1080’Url(domain=‘www.baidu.com’,proxy=‘127.0.0.1:1080’)# 表示匹配到域名为www.baidu.com的所有链接,直接扔掉这些链接。Url(domain=‘www.baidu.com’,drop=True)Url类应用的还在于黑白名单属性中,如在爬虫类中的属性:whitelist = [ Url(re=‘http://www.170mv.com/song.’), Url(re=‘http..sycdn.kuwo.cn.’),]blacklist = [ Url(re=‘http://www.170mv.com/song.’), Url(re=‘http..sycdn.kuwo.cn.’),] 表示爬虫请求的url黑白名单匹配规则。必要属性打开spider.py ,可以看到有两个默认的必要属性:name 爬虫的唯一标识,项目下不能有该属性重名的爬虫。urls 起始链接种子,爬虫开始的url列表这两个属性是必须的。回调函数整个项目的主要实现在于回调函数的使用,利用异步请求得到响应后马上调用其请求绑定的回调函数来实现爬虫的异步爬取。请求后响应的回调函数(类方法)有:parse 返回状态200,请求正常响应正常,可以编写正常的规则提取、数据保存等。error 状态码非200,出现异常状态码,编写错误处理逻辑等。exception 请求出现异常,异常自定义处理。数据存储Amipy目前只支持MongoDB数据库,默认的数据库设置在爬虫配置文件settings.py中。对于爬取的数据进行保存,默认只使用MongoDB进行数据存储(后续可以自己扩展编写ORM)。只需要打开item.py,修改其中的示例类,原先为:from amipy.BaseClass.orm import Model,Fieldclass DataItemName(Model): …修改其内容为:from amipy.BaseClass.orm import Model,Fieldclass MyTableName(Model): ID = Field(‘索引’) content = Field(‘内容’)则类名 MyTableName为保存在指定数据库中的数据集合名称,ID为列对象,名称为“索引”,以此类推,content也为列对象,名称为“内容”。可以按照自己的需求进行添加删减列。数据的保存只需要在回调函数中对对应的列对象进行赋值,而后调用ORM对象的save函数即可。比如在spider.py的爬虫类中的成功回调函数parse中保存爬取到的数据: … def parse(self,response): self.item.ID = 200 self.item.content = ‘这是内容’ self.item.save() …则 数据集合 MyTableName中会自动保存一行数据:列“索引”为200,列“内容”为“这是内容”的数据行。引用orm数据模型对象只需要调用爬虫类的item属性,如上面示例中的self.item即是。获取其数据库对象可以使用:self.item.db来获得当前爬虫连接的MongoDB数据库对象。可以通过self.item.db.save()self.item.db.delete()self.item.db.update()…等api来实现数据库操作。事件循环loopAmipy爬虫的异步请求基于python3的协程async框架,所以项目全程只有一个事件循环运行,如果需要添加更多的爬虫请求,可以通过回调函数传进事件循环,加入请求队列。具体做法便是通过在爬虫类的回调函数中使用send函数来传递请求Request对象:import amipyfrom amipy import Request,sendclass MySpider(amipy.Spider): … def parse(self,response): … # 加入新的爬虫请求 url = ‘http://www.170mv.com/download/' send(Request(self,url)) …可以在项目配置文件settings.py中设置整个项目最大的协程并发数CONCURRENCY,以及协程请求的延时等。Telnet连接Amipy爬虫内置一个服务线程,可以通过Telnet进行连接来查看操作当前项目的爬虫,在启动爬虫后,可以通过新开一个DOS命令窗口,输入:>telnet 127.0.0.1 2232进行Telnet连接至项目服务线程,可以使用的命令有: show spiders show all running spiders and their conditions. list list a general situation of all spiders. echo echo a running spider and its attributes. pause pause a running spider by a give name. stop stop a running/paused spider by a give name. close close a spider by a give name. restart restart a stopped spider by a give name. resume resume a paused spider by a give name. quit quit the Spider-Client. help show all the available commands usage.举例,假设当前爬虫唯一标识名称为lianjia,则可以通过:$amipy> pause lianjia来暂停爬虫lianjia的爬取进度,在爬虫将当前请求队列清空后会一直暂停,直到收到Telnet端发出的其他命令。恢复爬虫使用:$amipy> resume lianjia查看当前项目下所有爬虫:$amipy> list详细查看则使用:$amipy> show spiders开启关闭Telnet在项目的配置文件settings.py中设置SPIDER_SERVER_ENABLE。例子1. 使用Amipy创建链家网爬虫(LianJiaSpider)爬虫目的:爬取链家网上北京当前最新的租房信息,包含“价格”,“房屋基本信息”、“配套设施”、“房源描述”、“联系经纪人”、“地址和交通”存入MongoDB数据库中创建项目进入到D:LianJia路径,创建Amipy项目LJproject:D:\LianJia> amipy cproject LJproject创建爬虫进入到项目路径D:LianJiaLJproject,创建Amipy爬虫lianjia:D:\LianJia\LJproject> amipy cspider lianjia编写数据库模型打开D:LianJiaLJprojectspidersLianjiaitem.py,编写数据保存模型:#coding:utf-8from amipy.BaseClass.orm import Model,Fieldclass LianJiaRenting(Model): price = Field(‘价格’) infos = Field(‘房屋基本信息’) facility = Field(‘配套设施’) desc = Field(‘房源描述’) agent = Field(‘联系经纪人’) addr = Field(‘地址与交通’)设置数据库连接打开 D:LianJiaLJprojectspidersLianjiasettings.py,找到MongoDB数据库连接设置,进行设置:# MongoDB settings for data saving.DATABASE_SETTINGS = { ‘host’:‘127.0.0.1’, ‘port’:27017, ‘user’:’’, ‘password’:’’, ‘database’:‘LianJiaDB’,}要先确保系统安装好MongoDB数据库并已经开启了服务。编写爬虫脚本打开 D:LianJiaLJprojectspidersLianjiaspider.py,编写爬虫采集脚本:import amipy,refrom amipy import send,Request,Urlfrom bs4 import BeautifulSoup as bs class LianjiaSpider(amipy.Spider): name = ’lianjia’ # 设置爬取初始链接 urls = [‘https://bj.lianjia.com/zufang/’] # 设置爬虫白名单,只允许爬取匹配的链接 whitelist = [ Url(re=‘https://bj.lianjia.com/zufang/.*’), ] # 自定义的属性 host =‘https://bj.lianjia.com’ page = 1 # 请求成功回调函数 def parse(self,response): soup = bs(response.text(),’lxml’) item_list = soup(‘div’,class_=‘content__list–item’) for i in item_list: # 获取详情页链接 并发送至爬虫请求队列 url = self.host+i.a[‘href’] send(Request(self,url,callback=self.details)) # 添加下一页 totalpage = soup(‘div’,class_=‘content__pg’)[0][‘data-totalpage’] if self.page>=int(totalpage): return self.page +=1 send(Request(self,self.host+’/zufang/pg{}/’.format(self.page))) def details(self,response): infos = {} agent = {} facility = [] soup = bs(response.text(),’lxml’) infos_li = soup(‘div’,class_=‘content__article__info’)[0].ul(’li’) facility_li = soup(‘ul’,class_=‘content__article__info2’)0 agent_ul = soup(‘ul’,id=‘agentList’)[0] addr_li = soup(‘div’,id=‘around’)[0].ul.li desc_li = soup(‘div’,id=‘desc’)[0].li desc_li.div.extract() desc = desc_li.p[‘data-desc’] if desc_li.p else ’’ for i in infos_li: text = i.text if ‘:’ in text: infos.update({text.split(’:’)[0]:text.split(’:’)[1]}) for i in facility_li[1:]: if ‘no’ not in i[‘class’][-2]: facility.append(i.text) for div in agent_ul(‘div’,class=‘desc’): name = div.a.text phone = div(‘div’,class_=‘phone’)[0].text agent[name]=phone # 数据模型对应并保存 self.item.desc = desc self.item.addr = re.sub(r’[\r\n ]’,’’,addr_li.text) if addr_li else ’’ self.item.price = soup(‘p’,class_=‘content__aside–title’)[0].text self.item.infos = infos self.item.agent = agent self.item.facility = facility self.item.save()如果在爬虫配置文件settings.py中设置遵守目标网站机器人协议可能会被禁止采集,可以自行关闭设置。另外,开启网页内容相似过滤BLOOMFILTER_HTML_ON可能会使爬取的结果数较少,爬虫只会采集相似度不同的网页内容的链接,如果需要大批量采集,而网页正文较少的,可以关闭这个设置。代码比较粗糙,但可以知道Amipy爬虫基本的实现流程。运行爬虫在项目根路径下,输入:D:\LianJia\LJproject> amipy runspider查看数据库进入MongoDB数据库:可以看到在数据库‘LianJiaDB’下的集合“LianJiaRenting”中已经保存有我们爬取的数据,格式如下:{ “_id” : ObjectId(“5c6541b065b2fd1cf002c565”), “价格” : “7500元/月 (季付价)”, “房屋基本信息” : { “发布” : “20天前”, “入住” : “随时入住”, “租期” : “2~3年”, “看房” : “暂无数据”, “楼层” : “中楼层/6层”, “电梯” : “无”, “车位” : “暂无数据”, “用水” : “民水”, “用电” : “民电”, “燃气” : “有”, “采暖” : “集中供暖” }, “配套设施” : [ “电视”, “冰箱”, “洗衣机”, “空调”, “热水器”, “床”, “暖气”, “宽带”, “衣柜”, “天然气” ], “房源描述” : “【交通出行】 小区门口为八里庄南里公交车站,75,675等多路公交经过。地铁6号线十里堡站D口,距离地铁口400米,交通十分方便,便于出行。<br />\n【周边配套】 此房位置棒棒哒,有建设银行,中国银行,交通银行,邮政储蓄,果多美水果超市,购物,金旭菜市场,娱乐,休闲,便利。旁边首航超市,姥姥家春饼,味多美蛋糕店,生活方便。<br />\n【小区介绍】 该小区中此楼是1981建成,安全舒适,小区内主力楼盘为6层板楼,前后无遮挡,此楼是多见的板楼,楼层高视野好。<br />\n”, “联系经纪人” : { “宋玉恒” : “4000124028转7907” }, “地址与交通” : “距离6号线-十里堡192m”}查看当前爬取进度新开一个DOS端口,输入:> telnet 127.0.0.1 2232进行Telnet连接,可以使用命令操作查看当前爬虫的爬取状态。例如使用echo命令:$amipy> echo lianjia可以查看当前爬虫的状态:—————-Spider-lianjia——————– Name:lianjia Status:RUNNING- Class:LianjiaSpider- Success:25 Fail:0 Exception:0- Priority:0- SeedUrls:[‘https://bj.lianjia.com/zufang/’]- Path:D:\LianJia\LJproject\spiders\Lianjia- Session:<aiohttp.client.ClientSession object at 0x000000000386FE10>- StartAt:Thu Feb 14 20:30:21 2019- PausedAt:None- ResumeAt:None- StopAt:None- RestartAt:None- CloseAt:None————————————————– ...

February 14, 2019 · 3 min · jiezi

2亿用户背后的Flutter应用框架Fish Redux

背景在闲鱼深度使用 Flutter 开发过程中,我们遇到了业务代码耦合严重,代码可维护性糟糕,如入泥泞。对于闲鱼这样的负责业务场景,我们需要一个统一的应用框架来摆脱当下的开发困境,而这也是 Flutter 领域空缺的一块处女地。Fish Redux 是为解决上面问题上层应用框架,它是一个基于 Redux 数据管理的组装式 flutter 应用框架, 特别适用于构建中大型的复杂应用。它的最大特点是配置式组装, 一方面将一个大的页面,对视图和数据层层拆解为互相独立的 Component|Adapter,上层负责组装,下层负责实现,另一方面将 Component|Adapter 拆分为 View,Reducer,Effect 等相互独立的上下文无关函数。所以它会非常干净,易编写、易维护、易协作。Fish Redux 的灵感主要来自于 Redux、React、Elm、Dva 这样的优秀框架,而 Fish Redux 站在巨人的肩膀上,将集中,分治,复用,隔离做的更进一步。分层架构图架构图,主体自底而上,分三层,每一层用来解决不通层面的问题和矛盾,下面依次来展开。ReduxRedux 是来自前端社区的一个数据管理框架, 对 Native 开发同学来说可能会有一点陌生,我们做一个简单的介绍。Redux 做什么的?Redux 是一个用来做可预测易调试的数据管理的框架。所有对数据的增删改查等操作都由 Redux 来集中负责。Redux 是怎么设计和实现的?Redux 是一个函数式的数据管理的框架。传统 OOP 做数据管理,往往是定义一些 Bean,每一个 Bean 对外暴露一些 Public-API 用来操作内部数据(充血模型)。函数式的做法是更上一个抽象的纬度,对数据的定义是一些 Struct(贫血模型),而操作数据的方法都统一到具有相同函数签名 (T, Action) => T 的 Reducer 中。FP:Struct(贫血模型) + Reducer = OOP:Bean(充血模型)同时 Redux 加上了 FP 中常用的 Middleware(AOP) 模式和 Subscribe 机制,给框架带了极高的灵活性和扩展性。贫血模型、充血模型 参考:https://en.wikipedia.org/wiki/Plain_old_Java_objectRedux 的缺点Redux 核心仅仅关心数据管理,不关心具体什么场景来使用它,这是它的优点同时也是它的缺点。在我们实际使用 Redux 中面临两个具体问题Redux 的集中和 Component 的分治之间的矛盾。Redux 的 Reducer 需要一层层手动组装,带来的繁琐性和易错性。Fish Redux 的改良Fish Redux 通过 Redux 做集中化的可观察的数据管理。然不仅于此,对于传统 Redux 在使用层面上的缺点,在面向端侧 flutter 页面纬度开发的场景中,我们通过更好更高的抽象,做了改良。一个组件需要定义一个数据(Struct)和一个 Reducer。同时组件之间存在着父依赖子的关系。通过这层依赖关系,我们解决了【集中】和【分治】之间的矛盾,同时对 Reducer 的手动层层 Combine 变成由框架自动完成,大大简化了使用 Redux 的困难。我们得到了理想的集中的效果和分治的代码。对社区标准的 followState、Action、Reducer、Store、Middleware 以上概念和社区的 ReduxJS 是完全一致的。我们将原汁原味地保留所有的 Redux 的优势。如果想对 Redux 有更近一步的理解,请参考 https://github.com/reduxjs/reduxComponent组件是对局部的展示和功能的封装。 基于 Redux 的原则,我们对功能细分为修改数据的功能(Reducer)和非修改数据的功能(副作用 Effect)。于是我们得到了,View、 Effect、Reducer 三部分,称之为组件的三要素,分别负责了组件的展示、非修改数据的行为、修改数据的行为。这是一种面向当下,也面向未来的拆分。在面向当下的 Redux 看来,是数据管理和其他。在面向未来的 UI-Automation 看来是 UI 表达和其他。UI 的表达对程序员而言即将进入黑盒时代,研发工程师们会把更多的精力放在非修改数据的行为、修改数据的行为上。组件是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。关于 ViewView 仅仅是一个函数签名: (T,Dispatch,ViewService) => Widget它主要包含三方面的信息视图是完全由数据驱动。视图产生的事件/回调,通过 Dispatch 发出“意图”,不做具体的实现。需要用到的组件依赖等,通过 ViewService 标准化调用。比如一个典型的符合 View 签名的函数关于 EffectEffect 是对非修改数据行为的标准定义,它是一个函数签名: (Context, Action) => Object它主要包含四方面的信息接收来自 View 的“意图”,也包括对应的生命周期的回调,然后做出具体的执行。它的处理可能是一个异步函数,数据可能在过程中被修改,所以我们不崇尚持有数据,而通过上下文来获取最新数据。它不修改数据, 如果修要,应该发一个 Action 到 Reducer 里去处理。它的返回值仅限于 bool or Future, 对应支持同步函数和协程的处理流程。比如:良好的协程的支持关于 ReducerReducer 是一个完全符合 Redux 规范的函数签名:(T,Action) => T一些符合签名的 Reducer同时我们以显式配置的方式来完成大组件所依赖的小组件、适配器的注册,这份依赖配置称之为 Dependencies。所以有这样的公式 Component = View + Effect(可选) + Reducer(可选) + Dependencies(可选)。一个典型的组装通过 Component 的抽象,我们得到了完整的分治,多纬度的复用,更好的解耦。AdapterAdapter 也是对局部的展示和功能的封装。它为 ListView 高性能场景而生,它是 Component 实现上的一种变化。它的目标是解决 Component 模型在 flutter-ListView 的场景下的 3 个问题1)将一个"Big-Cell"放在 Component 里,无法享受 ListView 代码的性能优化。2)Component 无法区分 appear|disappear 和 init|dispose 。3)Effect 的生命周期和 View 的耦合,在 ListView 的场景下不符合直观的预期。概括的讲,我们想要一个逻辑上的 ScrollView,性能上的 ListView ,这样的一种局部展示和功能封装的抽象。做出这样独立一层的抽象是,我们看实际的效果, 我们对页面不使用框架,使用框架 Component,使用框架 Component+Adapter 的性能基线对比Reducer is long-lived, Effect is medium-lived, View is short-lived.我们通过不断的测试做对比,以某 android 机为例:使用框架前 我们的详情页面的 FPS,基线在 52FPS。使用框架, 仅使用 Component 抽象下,FPS 下降到 40, 遭遇“Big-Cell”的陷阱。使用框架,同时使用 Adapter 抽象后,FPS 提升到 53,回到基线以上,有小幅度的提升。Directory推荐的目录结构会是这样sample_page– action.dart– page.dart– view.dart– effect.dart– reducer.dart– state.dartcomponentssample_component– action.dart– component.dart– view.dart– effect.dart– reducer.dart– state.dart上层负责组装,下层负责实现, 同时会有一个插件提供, 便于我们快速填写。以闲鱼的详情场景为例的组装:组件和组件之间,组件和容器之间都完全的独立。Communication Mechanism组件|适配器内通信组件|适配器间内通信简单的描述:采用的是带有一段优先处理的广播, self-first-broadcast。发出的 Action,自己优先处理,否则广播给其他组件和 Redux 处理。最终我们通过一个简单而直观的 dispatch 完成了组件内,组件间(父到子,子到父,兄弟间等)的所有的通信诉求。Refresh Mechanism数据刷新局部数据修改,自动层层触发上层数据的浅拷贝,对上层业务代码是透明的。层层的数据的拷贝一方面是对 Redux 数据修改的严格的 follow。另一方面也是对数据驱动展示的严格的 follow。视图刷新扁平化通知到所有组件,组件通过 shouldUpdate 确定自己是否需要刷新优点数据的集中管理通过 Redux 做集中化的可观察的数据管理。我们将原汁原味地保留所有的 Redux 的优势,同时在 Reducer 的合并上,变成由框架代理自动完成,大大简化了使用 Redux 的繁琐度。组件的分治管理组件既是对视图的分治,也是对数据的分治。通过逐层分治,我们将复杂的页面和数据切分为相互独立的小模块。这将利于团队内的协作开发。View、Reducer、Effect 隔离将组件拆分成三个无状态的互不依赖的函数。因为是无状态的函数,它更易于编写、调试、测试、维护。同时它带来了更多的组合、复用和创新的可能。声明式配置组装组件、适配器通过自由的声明式配置组装来完成。包括它的 View、Reducer、Effect 以及它所依赖的子项。良好的扩展性核心框架保持自己的核心的三层关注点,不做核心关注点以外的事情,同时对上层保持了灵活的扩展性。框架甚至没有任何的一行的打印的代码,但我们可通过标准的 Middleware 来观察到数据的流动,组件的变化。在框架的核心三层外,也可以通过 dart 的语言特性 为 Component 或者 Adapter 添加 mixin,来灵活的组合式地增强他们的上层使用上的定制和能力。框架和其他中间件的打通,诸如自动曝光、高可用等,各中间件和框架之间都是透明的,由上层自由组装。精小、简单、完备它非常小,仅仅包含 1000 多行代码。它使用简单,完成几个小的函数,完成组装,即可运行。它是完备的。Fish Redux 目前已在阿里巴巴闲鱼技术团队内多场景,深入应用。本文作者:闲鱼技术-吉丰阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 18, 2019 · 2 min · jiezi

玩转Elasticsearch源码-使用Intellij IDEA和remote debug调试源代码

开篇学习源码第一步就是搭建调试环境,但是看了网上大部分Elasticsearch调试方式都是配置各种环境变量然后直接启动Main方法,而且还各种报错。今天提供新的方式--remote debug来避免这些麻烦。步骤环境首先要安装jdk8,gradle和Intellij IDEA源码下载拉取代码,checkout到想要调试的版本(这里切到v6.1.0,需要注意的是不同ES分支对gradle版本要求不一样,可以到README文件中查看对应到gradle版本要求)git clone git@github.com/elastic/elasticsearchcd elasticsearchgit checkout v6.1.0导入到IDEA执行gradle idea,成功后会提示BUILD SUCCESSFUL,然后导入到IDEA:test:fixtures:hdfs-fixture:idea:test:fixtures:krb5kdc-fixture:ideaModule:test:fixtures:krb5kdc-fixture:idea:test:fixtures:old-elasticsearch:ideaModule:test:fixtures:old-elasticsearch:ideaBUILD SUCCESSFULTotal time: 2 mins 2.159 secs使用gradle启动Elasticsearchgradle run –debug-jvm执行成功后是这样的,其中8000就是远程debug端口配置remote debug点击IDEA的Edit Configurations,再点击➕填写主机和端口,Name是配置名称,可以自定义(我这里就填es),点OK保存配置搜一下源码里面Elasticsearch类,,看到Main方法,先打个断点等会看效果最后再点下绿色小虫子启动debug是不是在断点停下来了跳过断点再看下控制台,是不是启动日志都出来了再验证下是否启动成功原理一切源于被称作 Agent 的东西。JVM有一种特性,可以允许外部的库(Java或C++写的libraries)在运行时注入到 JVM 中。这些外部的库就称作 Agents, 他们有能力修改运行中 .class 文件的内容。这些 Agents 拥有的这些 JVM 的功能权限, 是在 JVM 内运行的 Java Code 所无法获取的, 他们能用来做一些有趣的事情,比如修改运行中的源码, 性能分析等。 像 JRebel 工具就是用了这些功能达到魔术般的效果。传递一个 Agent Lib 给 JVM, 通过添加 agentlib:libname[=options] 格式的启动参数即可办到。像上面的远程调试我们用的就是 -agentlib:jdwp=… 来引入 jdwp 这个 Agent 的。jdwp 是一个 JVM 特定的 JDWP(Java Debug Wire Protocol) 可选实现,用来定义调试者与运行JVM之间的通讯,它的是通过 JVM 本地库的 jdwp.so 或者 jdwp.dll 支持实现的。简单来说, jdwp agent 会建立运行应用的 JVM 和调试者(本地或者远程)之间的桥梁。既然他是一个Agent Library, 它就有能力拦截运行的代码。在 JVM 架构里, debugging 功能在 JVM 本身的内部是找不到的,它是一种抽象到外部工具的方式(也称作调试者 debugger)。这些调试工具或者运行在 JVM 的本地 或者在远程。这是一种解耦,模块化的架构。关于Agent还有很多值得研究的细节,甚至基于JVMTI自己实现。参考https://www.ibm.com/developerworks/cn/java/j-lo-jpda2/index.html ...

January 9, 2019 · 1 min · jiezi

【转】2019年Web开发指南

文章总结自视频Web Development In 2019 - A Practical Guide眨眼2018过去了,还有很多计划学习的东西恐怕都还没有完成,时间不等人,我们要开始看看2019年有什么要关注学习的了。视频大纲:0:28 - What Is In This Guide?(指南主要内容介绍)1:24 - Basic Software & Tools(基本开发软件和工具)3:43 - HTML & CSS(HTML和CSS)5:06 - Responsive Layout(响应式布局)5:55 - Basic Deployment(部署介绍)7:35 - Sass Pre-Processor(Sass预处理器)8:38 - Vanilla JavaScript(原生Javascript)10:08 - Basic Front-End Web Developer(前端开发介绍)11:13 - What To Learn Next(学些什么)11:53 - HTML / CSS Framework(HTML/CSS框架)13:21 - Git & Tooling(Git和相关工具)16:58 - Front-End Framework(前端框架)19:10 - State Management(状态管理器)20:29 - Full Fledged Front-End Web Developer(优秀的前端开发者)21:24 - Server Side Language(服务端语言)24:16 - Server Side Framework(服务端框架)27:52 - Database(数据库介绍)29:34 - Server Rendered Pages(服务端渲染)30:41 - CMS(内容管理系统)31:44 - DevOps, Deployment & More(部署等)34:40 - Full Stack Badass(全栈)34:57 - Mobile Development(移动端开发)35:58 - Desktop Apps With Electron(Electron的桌面应用)36:33 - GraphQL & Apollo(GraphQL和Apollo)37:28 - TypeScript(TypeScript)38:15 - Serverless Architecture(无服务器架构)38:52 - AI & Machine Learning(智能和机器学习)39:23 - Blockchain Technology(区块链技术)40:07 - PWA(渐进式页面应用)40:42 - Web Assembly(不知道如何解释)我就几个重点来介绍一下:基本开发软件和工具编辑器:VSCode,这两年来,它的Web开发的使用比例急速上升,如果你是一名前端,非常推荐使用哦。另外对我非常有帮助的VSCode插件Settings Sync,我也是强烈推荐的,使用方法可以阅读我曾经写的Visual Studio Code 设置同步到github的插件介绍及使用方法(Settings Sync)浏览器:Chrome是我目前用的最顺手的了,开发调试也是非常强大,作为一名Web开发者,还在使用360,或许有点显得太不专业了????其他:Windows下的终端强烈推荐Git Bash,至少我是极度反感每次按完ctrl+C还要Y一下的,如果使用VSCode,可以修改以下设置(默认git安装路径的话)“terminal.integrated.shell.windows”: “C:\Program Files\Git\bin\bash.exe”,如果有用到设计相关的,可以考虑学习XD,PS,Sketch…基础知识掌握HTMT5, CSS3, Javascript:这三个依旧是需要熟练的!HTML5:面世很久了,其实很多时候我们并未熟练掌握各个标签的使用,以及一些高效API还是有必要进一步学习的。CSS3:最多的最复杂的应该是transform和flex这块了,了解他们有哪些功能的前提下,没事多看看文档,是不是可以更快的提高工作效率呢Javascript:ES6趋势越来越明显,各类构建工具配合Babel强大到了简单的配置即可兼容大部分浏览器,因此使用ES6+进行JS开发实在是会轻松一些,因此,请多阅读阮一峰的ES6文档。响应式开发可以考虑放弃使用px,如果需要做响应式的Web应用,Rem或许是更好的选择,当然你也可以使用VW单位,还有设置网格,Viewport,媒体查询等等方式让你的响应式应用更加完美。Sass,PostCSS手写CSS,真的很慢,如果可以的话,非常推荐在开发环境下使用Sass和PostCSS,最大的便利之处是代码更加好维护和管理了。前端框架三大开发框架,Angular,React,Vue,各有特点!很有必要去了解,即便只会其中一个,也推荐去了解其他的。这里就不细说了。UI框架:ElementUI,Ant Desgin,等等,太多了。也是各有特点,大家请多多尝试。CSS框架:BootStrap,Bulma(我也没用过)等等,我认为熟悉这些框架对于规范化话CSS是有较大的帮助的。服务端语言前端工程师还是需要熟悉Node.js及相关主流框架,比如:Express,KOA,Egg.js等等。而其他Web开发者如有需要可能会使用到Java,PHP(Laravel,ThinkPHP),Python(DJango),Go等等。数据存储关系型数据库:MySQL,PostgreSQLNoSQL:MongoDB云:Firebase,AWS,LeanCloud(比较推荐看看)轻存储:SQLite,Redis服务端渲染三大框架对应的三套:Augular Universal,Next.js,Nuxt.js(使用Vue的同学,可以试试这个,以前问题挺多的,不过最近除了新版也是挺强大的。)网站部署不仅仅是运维需要熟悉的这些Linux,SSH,Git,Nginx等等。其他开发人员也有必要了解。国内比较有名的平台,阿里云,腾讯云,华为云等,都有比较完善的方案,不过这里有一个国外的Digital Ocean,不熟悉的可以多去看看,很多比较好的关于服务器维护管理等知识。我以前经常阅读,受益匪浅,强烈推荐。Docker也是越发流行,互不干扰的环境非常适合很多个项目。趋势及总结这里对2019的趋势做了简单的预测,很多其实并不是新知识了,但是他们依然有着极大的Web开发地位,依旧要反复学习,下面就几个重点,关键词有兴趣的可以了解一套代码实现多端应用的最佳方案,Ionic,React Native,Flutter等,他们也是各有利弊,有必要学习了解下。TypeScript;GraphQL & Apollo;AI和机器学习;区块链技术;PWA(渐进式Web应用)等等好了,其实没有什么太多干货,更多的是对视频内的一些总结,其实这几天我也看过不少Web开发的2018和2019,基本上大同小异。就我个人而言,我今天最大的目标就是更加熟练的掌握ES6,将Vue和React玩到飞起,Node.js也更进一步,再小试TS。好了,差不多就这些了,那你的2019目标是什么呢,欢迎讨论哦~其他视频参考(需要梯子):10 Predictions about 2019 for DevelopersTop 8 Web Development Trends 2019 ...

January 2, 2019 · 1 min · jiezi

阿里开源首个深度学习框架 X-Deep Learning!

刚刚,阿里妈妈正式对外发布了X-Deep Learning(下文简称XDL)的开源代码地址,开发者们可以在Github上自主下载。此前,在11月底,阿里妈妈就公布了这项开源计划,引来了业界的广泛关注。XDL突破了现有深度学习开源框架大都面向图像、语音等低维稠密数据而设计的现状,面向高维稀疏数据场景进行了深度优化,并已大规模应用于阿里妈妈的业务及生产场景。本文将为大家详细介绍XDL的设计理念及关键技术。概述以深度学习为核心的人工智能技术,过去的几年在语音识别、计算机视觉、自然语言处理等领域获得了巨大的成功,其中以GPU为代表的硬件计算力,以及优秀的开源深度学习框架起到了巨大的推动作用。尽管以TensorFlow、PyTorch、MxNet等为代表的开源框架已经取得了巨大的成功,但是当我们把深度学习技术应用在广告、推荐、搜索等大规模工业级场景时,发现这些框架并不能很好的满足我们的需求。矛盾点在于开源框架大都面向图像、语音等低维连续数据设计,而互联网的众多核心应用场景(如广告/推荐/搜索)往往面对的是高维稀疏离散的异构数据,参数的规模动辄百亿甚至千亿。进一步的,不少产品应用需要大规模深度模型的实时训练与更新,现有开源框架在分布式性能、计算效率、水平扩展能力以及实时系统适配性的等方面往往难以满足工业级生产应用的需求。X-DeepLearning正是面向这样的场景设计与优化的工业级深度学习框架,经过阿里巴巴广告业务的锤炼,XDL在训练规模和性能、水平扩展能力上都表现出色,同时内置了大量的面向广告/推荐/搜索领域的工业级算法解决方案。系统核心能力1) 为高维稀疏数据场景而生。支持千亿参数的超大规模深度模型训练,支持批学习、在线学习等模式。2) 工业级分布式训练能力。支持CPU/GPU的混合调度,具备完整的分布式容灾语义,系统的水平扩展能力优秀,可以轻松做到上千并发的训练。3) 高效的结构化压缩训练。针对互联网样本的数据特点,提出了结构化计算模式。典型场景下,相比传统的平铺样本训练方式,样本存储空间、样本IO效率、训练绝对计算量等方面都大幅下降,推荐等场景下整体训练效率最大可提升10倍以上。4) 成熟多后端支持。单机内部的稠密网络计算复用了成熟开源框架的能力,只需要少量的分布式驱动代码修改,就可以把TensorFlow/MxNet等的单机代码运行在XDL上,获得XDL分布式训练与高性能稀疏计算的能力。内置工业级算法解决方案1)点击率预估领域的最新算法,包括深度兴趣网络(Deep Interest Network, DIN),用户兴趣演化模型(Deep Interest Evolution Network, DIEN),跨媒介网络(Cross Media Network,CMN)。2)点击率&转化率联合建模的全空间多任务模型(Entire Space Multi-task Model, ESMM)。3)匹配召回领域的最新算法——深度树匹配模型(Tree-based Deep Match,TDM)。4)轻量级通用模型压缩算法(Rocket Training)系统设计与优化XDL-Flow:数据流与分布式运行时XDL-Flow驱动整个深度学习计算图的生成与执行,包括样本流水线、稀疏表征学习、稠密网络学习。同时,XDL-Flow也负责分布式模型的存储与交换控制逻辑,分布式容灾与恢复控制等全局一致性协调的工作。在搜索、推荐、广告等场景下的样本量巨大,通常达到几十TB至数百TB,如果不能很好的优化样本流水线,样本IO系统很容易成为整个系统的瓶颈,从而导致计算硬件的利用率低下。在大规模稀疏场景下,样本读取的特点是IO密集,稀疏表征计算的特点是参数交换网络通信密集,稠密深度计算是计算密集型。XDL-Flow通过把三个主要环节异步流水线并行,较好的适配了3种不同类型任务的性能。最好的情况下,前两个阶段的延时都被隐藏了。同时,我们也正在尝试自动化的Tunning异步流水线的各个参数,包括各个Step的并行度、Buffer大小等,尽可能让用户不需要关心整个异步流水线并行的细节。AMS:高效模型服务器AMS是面向稀疏场景专门设计与优化的分布式模型存储与交换子系统。我们综合小包网络通信、参数存储结构、参数分布式策略等进行了大量的软硬件优化,使得AMS在吞吐力和水平扩展力上都大幅优于传统的Parameter Server,AMS也支持内置的深度网络计算,使得你可以使用AMS进行表征子网络的二阶计算。1)AMS通过软硬件结合在网络通信层做了大量优化,包括使用Seastar,DPDK,CPUBind,ZeroCopy等技术,充分压榨硬件性能,经过我们实际测试,大规模并发训练下,参数交换导致的小包吞吐能力是传统RPC框架的5倍以上。2)通过内置的参数动态均衡策略,可以在运行过程中找到最优的稀疏参数分布策略,有效解决传统参数服务器由于参数分布式不均匀带来的热点问题,大幅提高了系统在高并发情况下的水平扩展能力。3)AMS同样支持通过GPU加速大Batch Size场景下的Sparse Embedding计算,针对超大Batch的场景,可以起到很好的加速作用。4)AMS支持内部定义子网络。例如我们的算法解决方案中提供的Cross-Media建模,图像部分的表征子网络就是以AMS内运行的方式定义的,大幅减少了重复计算和网络吞吐。Backend Engine:桥接技术复用成熟框架的单机能力为了充分利用现有开源深度学习框架在稠密深度网络上的能力,XDL使用桥接技术(Bridging),把开源深度学习框架(本期开源版XDL支持了TensorFlow、MxNet)作为我们的单机稠密网络的计算引擎后端。用户可以在保留TensorFlow或MxNet网络开发习惯的同时,通过少量的驱动代码修改,就直接获得XDL在大规模稀疏计算上的分布式训练能力。换句话说,使用XDL时无需再学习一门新的框架语言,这带来另一个好处是XDL可以跟现有成熟的开源社区无缝对接——用户可以很轻松地将tensorflow社区的某个开源模型通过XDL拓展到工业级场景。Compact Computation:结构化计算模式大幅提升训练效率工业界稀疏场景下的样本表征,往往呈现很强的结构化特点,例如用户特征、商品特征、场景特征。这种构建方式决定了某些特征会大量出现在重复的样本中——隶属于同一个用户的多条样本中,用户特征很大一部分是相同的。结构化样本压缩正是利用海量样本中,大量局部特征重复这一特点,在存储和计算两个维度上对特征进行压缩,节省了存储、计算和通信带宽资源。样本预处理阶段,对需要聚合的特征进行排序(例如按用户ID排序,聚合用户特征);batching阶段,在tensor层面进行压缩;计算阶段,压缩特征只有在最后一层才会展开,极大节省深层网络的计算开销。 推荐场景下的效果验证表示,在典型的生产数据上,使用聚合排序的样本和完全shuffle的样本评估AUC指标一致,整体性能提升10倍以上。Online-Learning:大规模在线学习在线学习近年来在工业界开始被大规模应用,它是工程与算法的深入结合,赋予模型实时捕捉线上流量变化的能力,在一些对时效性要求很高的场景,有十分大的价值。例如在电商大促等场景下,在线学习可以更加实时的捕捉用户行为的变化,显著的提升模型的实时效果。XDL提供了一套完整的在线学习的解决方案,支持基于全量模型,读取实时消息队列里的样本进行实时持续学习,我们内置支持了Kafka等作为Message Source,并允许按照用户设置控制模型写出的周期。另外,为了避免无限制的新特征流入导致的实时模型爆炸问题,XDL内置了实时特征自动选择与过期特征淘汰等功能,保证用户使用XDL进行在线学习的简便性。1)去ID化的稀疏特征学习:传统的机器学习框架一般要求对稀疏特征进行ID化表征(从0开始紧凑编码),以此来保证训练的高效性。XDL则允许直接以原始的特征进行训练,大幅简化了特征工程的复杂度,极大地增加了全链路数据处理效率,这一特性在实时在线学习场景下显得更加有意义。2)实时特征频控:用户可以设置一个特征过滤的阈值,例如出现次数大于N次的特征才纳入模型训练,系统会自动的采用自动概率丢弃的算法进行特征选择,这样可以大幅降低无效超低频特征在模型中的空间占用。3)过期特征淘汰:长周期的在线学习时,用户也可以通过打开过期特征淘汰功能,系统会自动的对影响力弱且长周期没有碰触到的特征参数进行自动淘汰。X-DeepLearning算法解决方案典型的点击率(Click-Through Rate)预估模型DIN(Deep Interest Network)传统的Embedding&MLP类的模型并未对用户的表达做过多的工作。往往通过embedding的机制将用户的历史行为投影到一个定长的向量空间,再经过一个sum/avg pooling操作得到一个定长的用户向量表达。但是用户的兴趣是多种多样的,用一个固定的向量去表达用户不同的兴趣是非常难的。事实上用户在面对不同商品的时候,其兴趣表现也不一样,仅仅和这个商品相关的兴趣会影响用户的决策。因此我们在预估用户对一个具体商品的点击率的时候只需要表达其与此商品相关的兴趣。在DIN中我们提出了一个兴趣激活机制,通过被预估的商品去激活用户历史行为中相关的部分,从而获取用户在这个具体商品上的兴趣。论文地址:https://arxiv.org/abs/1706.06978DIEN(Deep Interest Evolution Network)DIEN主要解决两个问题:兴趣提取和兴趣演化。在兴趣提取这部分,传统的算法直接将用户的历史行为当做用户的兴趣。同时整个建模过程中的监督信息全部集中于广告点击样本上。而单纯的广告点击样本只能体现用户在决策是否点击广告时的兴趣,很难建模好用户历史每个行为时刻的兴趣。本文中我们提出了auxiliary loss 用于兴趣提取模块,约束模型在对用户每一个历史行为时刻的隐层表达能够推测出后续的行为,我们希望这样的隐层表达能更好的体现用户在每一个行为时刻的兴趣。在兴趣提取模块后我们提出了兴趣演化模块,传统的RNN类似的方法只能建模一个单一的序列,然而在电商场景 用户不同的兴趣其实有不同的演化过程。在本文中我们提出AUGRU(Activation Unit GRU),让GRU的update门和预估的商品相关。在建模用户的兴趣演化过程中,AUGRU会根据不同的预估目标商品构建不同的兴趣演化路径,推断出用户和此商品相关的兴趣。论文地址:https://arxiv.org/abs/1809.03672CMN(Cross Media Network)CMN旨在CTR预估模型中引入更多的模态数据,如图像信息。在原有ID类特征基础上,增加了图像视觉特征,共同加入广告CTR预估模型,在阿里妈妈大规模数据上取得了显著的效果提升。CMN包括多项技术特色:第一,图像内容特征抽取模型与主模型共同训练,联合优化; 第二,同时使用图像信息表达广告和用户,其中用户表达采用用户历史行为对应的图片; 第三,为处理训练涉及到的海量图像数据,提出了“高级模型服务”的计算范式,有效减少训练过程中的计算、通信、存储负载。CMN除用于图像特征引入外,对于文本、视频等内容特征也可以以合适的特征提取网络、用同样的模型处理。论文地址:https://arxiv.org/abs/1711.06505典型的转化率(Conversion Rate)预估模型ESMM(Entire Space Multi-task Model)Entire Space Multi-task Model (ESMM) 是阿里妈妈研发的新型多任务联合训练算法范式。ESMM模型首次提出了利用学习CTR和CTCVR的辅助任务迂回学习CVR的思路,利用用户行为序列数据在完整样本空间建模,避免了传统CVR模型经常遭遇的样本选择偏差和训练数据稀疏的问题,取得了显著的效果。ESMM 可以很容易地推广到具有序列依赖性的用户行为(浏览、点击、加购、购买等)预估中,构建全链路多目标预估模型。ESMM模型中的BASE子网络可以替换为任意的学习模型,因此ESMM的框架可以非常容易地和其他学习模型集成,从而吸收其他学习模型的优势,进一步提升学习效果,想象空间巨大。论文地址:https://arxiv.org/abs/1804.07931典型的匹配召回模型TDM(Tree-based Deep Match)TDM自主创新提出了一套完整的基于树的复杂深度学习推荐匹配算法框架,它通过建立用户兴趣层次树结构实现了高效的全库检索,并以此为基础赋能深度模型引入Attention等更先进的计算结构,达到了在精度、召回率以及新颖性等指标上相对于传统推荐方法的显著效果提升。进一步的,TDM设计实现了一套完整的 初始树-模型训练-树重建-模型再训练 的联合训练迭代框架,更加促进了效果的提升。联合训练赋予了TDM算法框架较好的通用性,为TDM向新场景、新领域的迁移扩展提供了良好的理论基础和极大的工程可行性。论文地址:https://arxiv.org/abs/1801.02294典型的模型压缩算法Rocket Training工业上在线模型的实时推理对响应时间提出非常严苛的要求,从而一定程度上限制了模型的复杂程度。模型复杂程度的受限可能会导致模型学习能力的降低从而带来效果的下降。目前有2种思路来解决这个问题:一方面,可以在固定模型结构和参数的情况下,用计算数值压缩来降低inference时间,同时也有设计更精简的模型以及更改模型计算方式的工作,如Mobile Net和ShuffleNet等工作。另一方面,利用复杂的模型来辅助一个精简模型的训练,测试阶段,利用学习好的小模型来进行推理。这两种方案并不冲突,在大多数情况下第二种方案可以通过第一种方案进一步降低inference时间,同时,考虑到相对于严苛的在线响应时间,我们有更自由的训练时间,有能力训练一个复杂的模型。Rocket Training属于第二种思路,它比较的轻巧优雅,方法具有很强的通用性,可以根据系统能力来定制模型复杂度,提供了一种"无极调速"手段。在阿里妈妈的生产实践中,Rocket Training可以极大地节省在线计算资源,显著提升系统应对双十一大促等流量洪峰的能力。论文地址:https://arxiv.org/abs/1708.04106BenchMark我们提供几组Benchmark数据供大家参考,重点看一下XDL在大batch、小batch等场景下的训练性能以及水平可扩展能力,以及结构化压缩训练带来的提速。基于CPU训练的深度CTR模型我们选取模型结构为Sparse Embedding DNN结构,N路Sparse特征分别做Embedding,再通过BiInteraction得到若干路NFM特征。选择两个特征规模的场景,Sparse特征总规模分别约为10亿(对应百亿参数)/100亿(对应千亿参数),dense维度为数百维,单条样本Sparse特征id数量约100+/300+个。训练模式:BatchSize=100,异步SGD训练。从bechmark结果可以看到,在高维稀疏场景下,XDL有明显的优势,在相当大并发的情况下,保持了良好的线性可扩展能力。基于GPU训练的深度CTR模型本文作者:XDL阅读原文本文来自云栖社区合作伙伴“阿里技术”,如需转载请联系原作者。

December 25, 2018 · 1 min · jiezi

用Java构建一个简单的WebSocket聊天室

前言首先对于一个简单的聊天室,大家应该都有一定的概念了,这里我们省略用户模块的讲解,而是单纯的先说说聊天室的几个功能:自我对话、好友交流、群聊、离线消息等。今天我们要做的demo就能帮我们做到这一点啦!!!采用框架我们整个Demo基本不需要大家花费太多时间,就可以实现以上的几个功能。首先,我们需要介绍一下我们今天打算采用的框架,InChat : 一个轻量级、高效率的支持多端(应用与硬件Iot)的异步网络应用通讯框架,采用这个框架,我们基本上只需要两三个类就可以实现我们今天需要的功能了。需要了解SSM & SpringBoot 吗?InChat ,本身不依赖于任何的底层框架,所以大家只要会基本的Java语言就可以实现一套自己的WebSocket聊天室。框架使用手册关于详细的手册说明,大家可以看看官网的介绍:InChatV1.1.0版本使用说明开始Demo搭建构建一个空的Maven项目我们不需要依赖其他的Maven包,只要本文提及的框架即可。com.github.UncleCatMySelfInChat1.1.0-alpha对接两个接口与实现一个是框架提供给我们用户进行数据保存与读取的,通过这个接口的实现,我们可以异步拿到每个聊天的通信数据。这里的InChatMessage是一个框架自定义的通信对象。public class ToDataBaseServiceImpl implements InChatToDataBaseService{ @Override public Boolean writeMapToDB(InChatMessage message) { System.out.println(message.toString()); return true; }}还有一个接口是对登录的校验(这里我们审理用户登录与校验模块,所以直接返回true即可),还有一个是返回群聊的数组信息。public class verifyServiceImpl implements InChatVerifyService { @Override public boolean verifyToken(String token) { //登录校验 return true; } @Override public JSONArray getArrayByGroupId(String groupId) { //根据群聊id获取对应的群聊人员ID JSONArray jsonArray = JSONArray.parseArray("["1111","2222","3333"]"); return jsonArray; }}我们可以再详细的说下,获取群聊信息,是通过一个groupId来获取对应的用户Id数组,我们可以自己做一个数据查询。核心的框架启动代码直接上代码,然后我们再讲解一下。public class DemoApplication { public static void main(String[] args) { //配置InChat配置工厂 ConfigFactory.inChatToDataBaseService = new ToDataBaseServiceImpl(); ConfigFactory.inChatVerifyService = new verifyServiceImpl(); //默认启动InChat InitServer initServer = new InitServer(new InitNetty()); initServer.open(); //获取用户值 WebSocketChannelService webSocketChannelService = new WebSocketChannelService(); //启动新线程 new Thread(new Runnable() { @Override public void run() { //设定默认服务器发送值 Map map = new HashMap<>(); map.put(“server”,“服务器”); //获取控制台用户想发送的用户Token Scanner scanner = new Scanner(System.in); String token = scanner.nextLine(); //获取用户连接 Channel channel = (Channel) webSocketChannelService.getChannel(token); //调用接口发送 webSocketChannelService.sendFromServer(channel,map); } }).start(); }}好了,以上已经基本完成了我们的聊天室Demo了,是不是很简单!?首先,我们将实现的两个类,配置到框架的配置工厂中,然后启动框架即可,相关的类,都是框架提供的。下面的线程是一个框架的接口,以服务器第一人称发送给针对用户通知信息,输入“1111”,Demo演示的用户token值。关于前端InChat : 一个轻量级、高效率的支持多端(应用与硬件Iot)的异步网络应用通讯框架,大家可以直接来这个项目下获取前端页面,或者直接访问这个地址:https://github.com/UncleCatMy…对于这个前端页面,我们需要更改一下IP地址。运行调试项目接下来直接启动后端项目,当我们看到以下的信息,则项目启动成功。 INFO - 服务端启动成功【192.168.1.121:8090】这里的IP需要更换以下读者启动后的IP地址。接着直接用浏览器打开chat.html的页面即可,关于js的方法,大家可以看看InChatV1.1.0版本使用说明。运行效果如下: INFO - 服务端启动成功【192.168.1.121:8090】DEBUG - -Dio.netty.buffer.bytebuf.checkAccessible: trueDEBUG - Loaded default ResourceLeakDetector: io.netty.util.ResourceLeakDetector@68ad4247 INFO - [DefaultWebSocketHandler.channelActive]/192.168.1.121:17330链接成功DEBUG - -Dio.netty.recycler.maxCapacityPerThread: 4096DEBUG - -Dio.netty.recycler.maxSharedCapacityFactor: 2DEBUG - -Dio.netty.recycler.linkCapacity: 16DEBUG - -Dio.netty.recycler.ratio: 8DEBUG - [id: 0xabb0dbad, L:/192.168.1.121:8090 - R:/192.168.1.121:17330] WebSocket version V13 server handshakeDEBUG - WebSocket version 13 server handshake key: JYErdeATDgbPmgK0mZ+IlQ==, response: YK9ZiJehNP+IwtlkpoVkPt94yWY=DEBUG - Decoding WebSocket Frame opCode=1DEBUG - Decoding WebSocket Frame length=31 INFO - [DefaultWebSocketHandler.textdoMessage.LOGIN]DEBUG - Encoding WebSocket Frame opCode=1 length=33DEBUG - Decoding WebSocket Frame opCode=1DEBUG - Decoding WebSocket Frame length=43 INFO - [DefaultWebSocketHandler.textdoMessage.SENDME]1111DEBUG - Encoding WebSocket Frame opCode=1 length=28 INFO - 【异步写入数据】InChatMessage{time=Mon Dec 24 10:03:00 CST 2018, type=‘sendMe’, value=’’, token=‘1111’, groudId=‘null’, online=‘null’, onlineGroup=null, one=‘null’}DEBUG - Decoding WebSocket Frame opCode=1DEBUG - Decoding WebSocket Frame length=56 INFO - [DefaultWebSocketHandler.textdoMessage.SENDTO]1111DEBUG - Encoding WebSocket Frame opCode=1 length=41 INFO - 【异步写入数据】InChatMessage{time=Mon Dec 24 10:03:01 CST 2018, type=‘sendTo’, value=’’, token=‘1111’, groudId=‘null’, online=‘2222’, onlineGroup=null, one=‘2222’}DEBUG - Decoding WebSocket Frame opCode=1DEBUG - Decoding WebSocket Frame length=60 INFO - [DefaultWebSocketHandler.textdoMessage.SENDGROUP]1111DEBUG - Encoding WebSocket Frame opCode=1 length=59 INFO - 【异步写入数据】InChatMessage{time=Mon Dec 24 10:03:02 CST 2018, type=‘sendGroup’, value=’’, token=‘1111’, groudId=‘2’, online=‘null’, onlineGroup=[2222, 3333], one=‘null’}1111DEBUG - Encoding WebSocket Frame opCode=1 length=22 ...

December 24, 2018 · 2 min · jiezi

前端技术演进(五):现代前端交互框架

这个来自之前做的培训,删减了一些业务相关的,参考了很多资料(参考资料列表),谢谢前辈们,么么哒 ????随着前端技术的发展,前端框架也在不断的改变。操作DOM时代DOM(Document Object Model,文档对象模型)将 HTML 文档表达为树结构,并定义了访问和操作 HTML 文档的标准方法。前端开发基本上都会涉及到HTML页面,也就避免不了和DOM打交道。最早期的Web前端,就是一个静态的黄页,网页上的内容不能更新。慢慢的,用户可以在Web页面上进行一些简单操作了,比如提交表单,文件上传。但是整个页面的部分或者整体的更新,还是靠刷新页面来实现的。随着AJAX技术的出现,前端页面上的用户操作越来越多,越来越复杂,所以就进入了对DOM元素的直接操作时代。要对DOM元素操作,就要使用DOM API,常见的DOM API有:类型方法节点查询getElementById、getElementsByName、getElementsByClassName、getElementsByTagName、querySelector、querySelectorAll节点创建createElement、createDocumentFragment、createTextNode、cloneNode节点修改appendChild、replaceChild、removeChild、insertBefore、innerHTML节点关系parentNode、previousSibling、childNodes节点属性innerHTML、attributes、getAttribute、setAttribure、getComputedStyle内容加载XMLHttpRequest、ActiveX使用DOM API可以完成前端页面中的任何操作,但是随着网站应用的复杂化,使用原生的API非常低效。所以 jQuery 这个用来操作DOM的交互框架就诞生了。jQuery 为什么能成为在这个时代最流行的框架呢?主要是他帮前端开发人员解决了太多问题:封装了DOM API,提供了统一和方便的调用方式。简化了元素的选择,可以很快的选取到想要的元素。提供了AJAX接口,对XMLHttpRequest和ActiveX统一封装。统一了事件处理。提供异步处理机制。兼容大部分主流浏览器。除了解决了上面这些问题,jQuery还拥有良好的生态,海量的插件拿来即用,让前端开发比以前流畅很多。尤其是在IE6、IE7时代,没有jQuery,意味着无穷的兼容性处理。// DOM API:document.querySelectorAll(’#container li’);// jQuery$(’#container’).find(’li’);随着HTML5技术的发展,jQuery提供的很多方法已经在原生的标准中实现了,慢慢的,jQuery的必要性在逐渐降低。http://youmightnotneedjquery.com/渐渐地,SPA(Single Page Application,单页面应用)开始被广泛认可,整个应用的内容都在一个页面中并完全通过异步交互来加载不同的内容,这时候使用 jQuery 直接操作DOM的方式就不容易管理了,页面上事件的绑定会变得混乱,在这种情况下,迫切需要一个可以自动管理页面上DOM和数据之间交互操作的框架。MV* 模式MVC,MVP和MVVM都是常见的软件架构设计模式(Architectural Pattern),它通过分离关注点来改进代码的组织方式。单纯从概念上,很难区分和感受出来这三种模式在前端框架中有什么不同。我们通过一个例子来体会一下:有一个可以对数值进行加减操作的组件:上面显示数值,两个按钮可以对数值进行加减操作,操作后的数值会更新显示。Model层用于封装和应用程序的业务逻辑相关的数据以及对数据的处理方法。这里我们把需要用到的数值变量封装在Model中,并定义了add、sub、getVal三种操作数值方法。var myapp = {}; // 创建这个应用对象myapp.Model = function() { var val = 0; // 需要操作的数据 /* 操作数据的方法 / this.add = function(v) { if (val < 100) val += v; }; this.sub = function(v) { if (val > 0) val -= v; }; this.getVal = function() { return val; };};View作为视图层,主要负责数据的展示。myapp.View = function() { / 视图元素 / var $num = $(’#num’), $incBtn = $(’#increase’), $decBtn = $(’#decrease’); / 渲染数据 / this.render = function(model) { $num.text(model.getVal() + ‘rmb’); };};这里,通过Model&View完成了数据从模型层到视图层的逻辑。但对于一个应用程序,这远远是不够的,我们还需要响应用户的操作、同步更新View和Model。前端 MVC 模式MVC(Model View Controller)是一种很经典的设计模式。用户对View的操作交给了Controller处理,在Controller中响应View的事件调用Model的接口对数据进行操作,一旦Model发生变化便通知相关视图进行更新。Model层用来存储业务的数据,一旦数据发生变化,模型将通知有关的视图。// Modelmyapp.Model = function() { var val = 0; this.add = function(v) { if (val < 100) val += v; }; this.sub = function(v) { if (val > 0) val -= v; }; this.getVal = function() { return val; }; / 观察者模式 / var self = this, views = []; this.register = function(view) { views.push(view); }; this.notify = function() { for(var i = 0; i < views.length; i++) { views[i].render(self); } };};Model和View之间使用了观察者模式,View事先在此Model上注册,进而观察Model,以便更新在Model上发生改变的数据。View和Controller之间使用了策略模式,这里View引入了Controller的实例来实现特定的响应策略,比如这个栗子中按钮的 click 事件:// Viewmyapp.View = function(controller) { var $num = $(’#num’), $incBtn = $(’#increase’), $decBtn = $(’#decrease’); this.render = function(model) { $num.text(model.getVal() + ‘rmb’); }; / 绑定事件 / $incBtn.click(controller.increase); $decBtn.click(controller.decrease);};控制器是模型和视图之间的纽带,MVC将响应机制封装在Controller对象中,当用户和应用产生交互时,控制器中的事件触发器就开始工作了。// Controllermyapp.Controller = function() { var model = null, view = null; this.init = function() { / 初始化Model和View / model = new myapp.Model(); view = new myapp.View(this); / View向Model注册,当Model更新就会去通知View啦 / model.register(view); model.notify(); }; / 让Model更新数值并通知View更新视图 */ this.increase = function() { model.add(1); model.notify(); }; this.decrease = function() { model.sub(1); model.notify(); };};这里我们实例化View并向对应的Model实例注册,当Model发生变化时就去通知View做更新。可以明显感觉到,MVC模式的业务逻辑主要集中在Controller,而前端的View其实已经具备了独立处理用户事件的能力,当每个事件都流经Controller时,这层会变得十分臃肿。而且MVC中View和Controller一般是一一对应的,捆绑起来表示一个组件,视图与控制器间的过于紧密的连接让Controller的复用性成了问题,如果想多个View共用一个Controller该怎么办呢?前端 MVP 模式MVP(Model-View-Presenter)是MVC模式的改良。和MVC的相同之处在于:Controller/Presenter负责业务逻辑,Model管理数据,View负责显示。在MVC里,View是可以直接访问Model的。而MVP中的View并不能直接使用Model,而是通过为Presenter提供接口,让Presenter去更新Model,再通过观察者模式更新View。与MVC相比,MVP模式通过解耦View和Model,完全分离视图和模型使职责划分更加清晰;由于View不依赖Model,可以将View抽离出来做成组件,它只需要提供一系列接口提供给上层操作。// Modelmyapp.Model = function() { var val = 0; this.add = function(v) { if (val < 100) val += v; }; this.sub = function(v) { if (val > 0) val -= v; }; this.getVal = function() { return val; };};Model层依然是主要与业务相关的数据和对应处理数据的方法,很简单。// Viewmyapp.View = function() { var $num = $(’#num’), $incBtn = $(’#increase’), $decBtn = $(’#decrease’); this.render = function(model) { $num.text(model.getVal() + ‘rmb’); }; this.init = function() { var presenter = new myapp.Presenter(this); $incBtn.click(presenter.increase); $decBtn.click(presenter.decrease); };};MVP定义了Presenter和View之间的接口,用户对View的操作都转移到了Presenter。比如这里的View暴露setter接口(render方法)让Presenter调用,待Presenter通知Model更新后,Presenter调用View提供的接口更新视图。// Presentermyapp.Presenter = function(view) { var _model = new myapp.Model(); var _view = view; _view.render(_model); this.increase = function() { _model.add(1); _view.render(_model); }; this.decrease = function() { _model.sub(1); _view.render(_model); };};Presenter作为View和Model之间的“中间人”,除了基本的业务逻辑外,还有大量代码需要对从View到Model和从Model到View的数据进行“手动同步”,这样Presenter显得很重,维护起来会比较困难。如果Presenter对视图渲染的需求增多,它不得不过多关注特定的视图,一旦视图需求发生改变,Presenter也需要改动。前端 MVVM 模式MVVM(Model-View-ViewModel)最早由微软提出。ViewModel指 “Model of View”——视图的模型。MVVM把View和Model的同步逻辑自动化了。以前Presenter负责的View和Model同步不再手动地进行操作,而是交给框架所提供的数据绑定功能进行负责,只需要告诉它View显示的数据对应的是Model哪一部分即可。我们使用Vue来完成这个栗子。在MVVM中,我们可以把Model称为数据层,因为它仅仅关注数据本身,不关心任何行为(格式化数据由View的负责),这里可以把它理解为一个类似json的数据对象。// Modelvar data = { val: 0};和MVC/MVP不同的是,MVVM中的View通过使用模板语法来声明式的将数据渲染进DOM,当ViewModel对Model进行更新的时候,会通过数据绑定更新到View。<!– View –><div id=“myapp”> <div> <span>{{ val }}rmb</span> </div> <div> <button v-on:click=“sub(1)">-</button> <button v-on:click=“add(1)">+</button> </div></div>ViewModel大致上就是MVC的Controller和MVP的Presenter了,也是整个模式的重点,业务逻辑也主要集中在这里,其中的一大核心就是数据绑定。与MVP不同的是,没有了View为Presente提供的接口,之前由Presenter负责的View和Model之间的数据同步交给了ViewModel中的数据绑定进行处理,当Model发生变化,ViewModel就会自动更新;ViewModel变化,Model也会更新。new Vue({ el: ‘#myapp’, data: data, methods: { add(v) { if(this.val < 100) { this.val += v; } }, sub(v) { if(this.val > 0) { this.val -= v; } } }});整体来看,比MVC/MVP精简了很多,不仅仅简化了业务与界面的依赖,还解决了数据频繁更新(之前用jQuery操作DOM很繁琐)的问题。因为在MVVM中,View不知道Model的存在,ViewModel和Model也察觉不到View,这种低耦合模式可以使开发过程更加容易,提高应用的可重用性。数据绑定在Vue中,使用了双向绑定技术(Two-Way-Data-Binding),就是View的变化能实时让Model发生变化,而Model的变化也能实时更新到View。其实双向数据绑定,可以简单地理解为一个模版引擎,但是会根据数据变更实时渲染。有人还不要脸的申请了专利:数据变更检测不同的MVVM框架中,实现双向数据绑定的技术有所不同。目前一些主流的实现数据绑定的方式大致有以下几种:手动触发绑定手动触发指令绑定是比较直接的实现方式,主要思路是通过在数据对象上定义get()方法和set()方法,调用时手动触发get ()或set()函数来获取、修改数据,改变数据后会主动触发get()和set()函数中View层的重新渲染功能。脏检测机制Angularjs是典型的使用脏检测机制的框架,通过检查脏数据来进行View层操作更新。脏检测的基本原理是在ViewModel对象的某个属性值发生变化时找到与这个属性值相关的所有元素,然后再比较数据变化,如果变化则进行Directive 指令调用,对这个元素进行重新扫描渲染。前端数据对象劫持数据劫持是目前使用比较广泛的方式。其基本思路是使用 Object.defineProperty 和 Object.defineProperies 对ViewModel数据对象进行属性get ()和set()的监听,当有数据读取和赋值操作时则扫描元素节点,运行指定对应节点的Directive指令,这样ViewModel使用通用的等号赋值就可以了。Vue就是典型的采用数据劫持和发布订阅模式的框架。Observer 数据监听器:负责对数据对象的所有属性进行监听(数据劫持),监听到数据发生变化后通知订阅者。Compiler 指令解析器:扫描模板,并对指令进行解析,然后绑定指定事件。Watcher 订阅者:关联Observer和Compile,能够订阅并收到属性变动的通知,执行指令绑定的相应操作,更新视图。ES6 Proxy之前我们说过 Proxy 实现数据劫持的方法:总结来看,前端框架从直接DOM操作到MVC设计模式,然后到MVP,再到MVVM框架,前端设计模式的改进原则一直向着高效、易实现、易维护、易扩展的基本方向发展。虽然目前前端各类框架也已经成熟并开始向高版本迭代,但是还没有结束,我们现在的编程对象依然没有脱离DOM编程的基本套路,一次次框架的改进大大提高了开发效率,但是DOM元素运行的效率仍然没有变。对于这个问题的解决,有的框架提出了Virtual DOM的概念。Virtual DOMMVVM的前端交互模式大大提高了编程效率,自动双向数据绑定让我们可以将页面逻辑实现的核心转移到数据层的修改操作上,而不再是在页面中直接操作DOM。尽管MVVM改变了前端开发的逻辑方式,但是最终数据层反应到页面上View层的渲染和改变仍是通过对应的指令进行DOM操作来完成的,而且通常一次ViewModel的变化可能会触发页面上多个指令操作DOM的变化,带来大量的页面结构层DOM操作或渲染。比如一段伪代码:<ul> <li repeat=“list”>{{ list.value }}</li></ul>let viewModel = new VM({ data:{ list:[{value: 1},{value: 2},{value: 3}] }})使用MVVM框架生成一个数字列表,此时如果需要显示的内容变成了 [{value: 1}, {value: 2}, {value: 3}, {value: 4}],在MVVM框架中一般会重新渲染整个列表,包括列表中无须改变的部分也会重新渲染一次。 但实际上如果直接操作改变DOM的话,只需要在<ul>子元素最后插入一个新的<li>元素就可以了。但在一般的MVVM框架中,我们通常不会这样做。毫无疑问,这种情况下MVVM的View层更新模式就消耗了更多没必要的性能。那么该如何对ViewModel进行改进,让浏览器知道实际上只是增加了一个元素呢?通过对比[{value: 1},{value: 2},{value: 3}] 和 [{value: 1}, {value: 2}, {value: 3}, {value: 4}]其实只是增加了一个 {value: 4},那么该怎样将这个增加的数据反映到View层上呢?可以将新的Model data 和旧的Model data 进行对比,然后记录ViewModel的改变方式和位置,就知道了这次View 层应该怎样去更新,这样比直接重新渲染整个列表高效得多。这里其实可以理解为,ViewModel 里的数据就是描述页面View 内容的另一种数据结构标识,不过需要结合特定的MVVM描述语法编译来生成完整的DOM结构。可以用JavaScript对象的属性层级结构来描述上面HTML DOM对象树的结构,当数据改变时,新生成一份改变后的Elements,并与原来的Elemnets结构进行对比,对比完成后,再决定改变哪些DOM元素。刚才例子里的 ulElement 对象可以理解为VirtualDOM。通常认为,Virtual DOM是一个能够直接描述一段HTMLDOM结构的JavaScript对象,浏览器可以根据它的结构按照一定规则创建出确定唯一的HTML DOM结构。整体来看,Virtual DOM的交互模式减少了MVVM或其他框架中对DOM的扫描或操作次数,并且在数据发生改变后只在合适的地方根据JavaScript对象来进行最小化的页面DOM操作,避免大量重新渲染。diff算法Virtual-DOM的执行过程:用JS对象模拟DOM树 -> 比较两棵虚拟DOM树的差异 -> 把差异应用到真正的DOM树上在Virtual DOM中,最主要的一环就是通过对比找出两个Virtual DOM的差异性,得到一个差异树对象。对于Virtual DOM的对比算法实际上是对于多叉树结构的遍历算法。但是找到任意两个树之间最小的修改步骤,一般会循环递归对节点进行依次对比,算法复杂度达到 O(n^3),这个复杂度非常高,比如要展示1000多个节点,最悲观要依次执行上十亿次的比较。所以不同的框架采用的对比算法其实是一个略简化的算法。拿React来说,由于web应用中很少出现将一个组件移动到不同的层级,绝大多数情况下都是横向移动。因此React尝试逐层的对比两棵树,一旦出现不一致,下层就不再比较了,在损失较小的情况下显著降低了比较算法的复杂度。前端框架的演进非常快,所以只有知道演进的原因,才能去理解各个框架的优劣,从而根据应用的实际情况来选择最合适的框架。对于其他技术也是如此。 ...

December 14, 2018 · 3 min · jiezi

来!狂撸一款PHP现代化框架 (路由的设计)

前言上一篇的标题改了一下,以一、二、三为章节对读者来说是种困扰,现在的标题是依照项目进度来编写的。上篇文章地址为 https://segmentfault.com/a/11…这一系列文章并不准备写太多章节,大概规划的只有4~5章左右,具体实现代码还请移步Githubhttps://github.com/CrazyCodes…本章详细讲解一下Route(路由的实现),Come on Up Image上图大概说明了实现路由要经过两个步骤将所有路由信息存储到超全局变量中用户请求时从全局变量中查找路由映射的服务脚本并实例化OK,大概流程就是酱紫,下面开始“撸”目录路由的代码暂分为以下几个文件(这并不是确定的,详细可查看Github)文件名注释Route转发文件:为实现 Route::get 效果RouteCollection路由信息处理存储RouteInterface无需解释RouteModel路由模型,将每个路由信息以结构体方式存储到$_SERVERRouter路由的核心类莫急,我们一个一个文件来看。先从RouteInterface开始RouteInterface参照RESTful规定设定接口方法分别为 GET、POST、PATCH、PUT、DELETE、OPTIONS,当然Laravel也是规范了以上标准请求。GitHub : https://github.com/CrazyCodes...interface RouteInterface{ /** * @param $uri * @param null $action * * @return mixed / public function get($uri, $action = null); /* * @param $uri * @param null $action * * @return mixed / public function post($uri, $action = null); /* * @param $uri * @param null $action * * @return mixed / public function patch($uri, $action = null); /* * @param $uri * @param null $action * * @return mixed / public function put($uri, $action = null); /* * @param $uri * @param null $action * * @return mixed / public function delete($uri, $action = null); /* * @param $uri * @param null $action * * @return mixed / public function options($uri, $action = null);}Router先写一个栗子public function get($uri, $action = null){ return $this->addRoute(“GET”, $uri, $action);}用户调用下方代码会指向上述方法,方法既调用addRoute方法将路由信息存储到$_SERVER中Route::get(’/’,‘Controller’)以下为addRoute部分的代码public function addRoute($methods, $uri, $action){ // 这里判断请求方式是否合规,既是否存在 GET、POST、PATCH、PUT、DELETE、OPTIONS其中之一 if ($this->verify($methods) == false) { return false; } // 之后我们去往RouteCollection路由信息的处理类中 return $this->routes->add($uri, $this->createRoute($methods, $action));}RouteCollection最终达到 add 方法,将路由信息存储到$_SERVER中public function add($uri, RouteModel $model){ if (empty($_SERVER[“routes”][$uri])) { $_SERVER[“routes”][$uri] = $model; }}第二个参数RouteModel开始我们说过这是路由模型,将每个路由以结构体的方式存储到变量中,存储后的结果’routes’ => array(6) { ’test/get’ => class Zero\Routing\RouteModel#13 (2) { public $method => string(3) “GET” public $action => string(19) “testController@test” } ’test/post’ => class Zero\Routing\RouteModel#14 (2) { public $method => string(4) “POST” public $action => string(19) “testController@test” } ’test/put’ => class Zero\Routing\RouteModel#15 (2) { public $method => string(3) “PUT” public $action => string(18) “testController@put” } ’test/del’ => class Zero\Routing\RouteModel#16 (2) { public $method => string(6) “DELETE” public $action => string(18) “testController@del” } ’test/patch’ => class Zero\Routing\RouteModel#17 (2) { public $method => string(5) “PATCH” public $action => string(20) “testController@patch” } ’test/opt’ => class Zero\Routing\RouteModel#18 (2) { public $method => string(7) “OPTIONS” public $action => string(18) “testController@opt” } }Route最后通过__callStatic将代码重定向到核心类中public static function __callStatic($name, $arguments){ $router = new Router; return $router->{$name}($arguments[0], $arguments[1]);}上述套路部分是Laravel的设计思想,通过这款简单的框架可对Laravel核心设计有丁点的理解。测试测试上次做的有点糙,从本章到系列结束,我们都以PHPunit来测试。/* * @content tests all methods storage -> $_SERVER[“routes”] /public function testAllMethodsStorage(){ $this->routes->get($methodGet = “test/get”, “testController@test”); $this->assertArrayHasKey($methodGet, $_SERVER[$this->methodsDataKey]); $this->routes->post($methodPost = “test/post”, “testController@test”); $this->assertArrayHasKey($methodPost, $_SERVER[$this->methodsDataKey]); $this->routes->put($methodPut = “test/put”, “testController@put”); $this->assertArrayHasKey($methodPut, $_SERVER[$this->methodsDataKey]); $this->routes->delete($methodDel = “test/del”, “testController@del”); $this->assertArrayHasKey($methodDel, $_SERVER[$this->methodsDataKey]); $this->routes->patch($methodPatch = “test/patch”, “testController@patch”); $this->assertArrayHasKey($methodPatch, $_SERVER[$this->methodsDataKey]); $this->routes->options($methodOpt = “test/opt”, “testController@opt”); $this->assertArrayHasKey($methodOpt, $_SERVER[$this->methodsDataKey]);}上述贴出部分代码,以过程化的方法去测试。查看存储是否符合预期。/* * @content RouteModel Success */public function testCreateRoute(){ $response = $this->routes->createRoute(“GET”, “TestController@Get”); $this->assertInstanceOf(RouteModel::class, $response);}包括测试对路由创建后是否为RouteModel的实现。具体可查看Githubhttps://github.com/CrazyCodes…致谢上述已完成了路由的基本设计,下一章将讲解从启动到请求路由映射到服务脚本的过程。希望本章可以帮到你,谢谢。 ...

December 14, 2018 · 2 min · jiezi

来!狂撸一款PHP现代化框架 (一)

前言从本章开始,我们继续造轮子,去完成一款类似于Laravel的现代化PHP框架,为什么说是现代化?因为他必须具备一下几点遵守PSR-4加载规范使用Composer进行包管理标准的HTTP请求方式优雅的使用设计模式开始我们无需关心性能问题,先考虑框架具体需要实现哪些功能,这与实现业务就大不相同了,来!开始我的表演。前期做任何一件事情都要有个前期准备工作。作为PSR-4的规定,我们命名空间得有一个祖宗名字,这里我叫他神圣的 《z_framework》至少需要一个GITHUB库来存储这个项目 https://github.com/CrazyCodes…创建一个composer.json文件用于进行包管理,灰常简单,phpunit搞进来。通过psr-4加载个项目命名{ “name”: “z framework”, “require-dev”: { “phpunit/phpunit”: “^7.0” }, “autoload”: { “psr-4”: { “Zero\”: “src/Zero”, } }, “autoload-dev”: { “psr-4”: { “Zero\Tests\”: “tests/” } }}最后我们就需要考虑下目录的结构及其我们第一步要完成的功能,核心的结构(这里并非只的项目结构哦。是框架的核心结构)暂且是这样srcZeroConfig // 可能存放一些配置文件的解析器Container // 容器的解析器Http // 请求处理的一些工具Routes // 路由处理的一些功能Bootstrap.php // 这可能是一个启动脚本Zero.php // 可能是核心的入口文件tests // 测试目录.gitignorecomposer.jsonLICENSEREADME.md路由还记得第一次使用Laravel时我们第一步做的事情吗?是的,去研究路由,所以我们把路由作为框架的第一步。在研究路由前,我们要知道http://www.domain.com/user/create是如何实现的,php默认是必须请求index.php或者default.php的,上述链接实际隐藏了index.php或default.php ,这是Nginx等服务代理帮我们做到的优雅的链接,具体配置如下,实际与Laravel官方提供无差别server { listen 80; server_name www.zf.com; root /mnt/app/z_framework/server/public; index index.php index.html index.htm; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ .php$ { fastcgi_pass php71:9000; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; }}通过try_files $uri $uri/ /index.php?$query_string;去解析请求,通过上述可以得出http://www.domain.com/user/create=======http://www.domain.com/index.php?user/create好了,明白了其中奥秘后,我们开始路由的编写,在src/Routes/Route.phpnamespace Zero\Routes; class Route {}实现首先我们先创建一个简单的接口文件src/Routes/RouteInterface.phpnamespace Zero\Routes; interface RouteInterface{ public function Get($url, $callFile); public function Post($url, $callFile); public function Put($url, $callFile); public function Delete($url, $callFile);}从Get请求开始namespace Zero\Routes; class Route implements RouteInterface{ public function Get($url, $callFile) { }}最后实现Get代码块if (parent::isRequestMethod(“GET”)) { // 判读请求方式 if (is_callable($callFile)) { // 判断是否是匿名函数 return $callFile(); } if ($breakUpString = parent::breakUpString($callFile)) { // 获取Get解析。既/user/create header(‘HTTP/1.1 404 Not Found’); } try { // 通过反射类获取对象 $breakUpString[0] = user $reflectionClass = new \ReflectionClass(‘App\Controllers\’ . $breakUpString[0]); // 实例化对象 $newInstance = $reflectionClass->newInstance(); // 获取对象中的指定方法,$breakUpString[1] = create call_user_func([ $newInstance, $breakUpString[1], ], []); } catch (\ReflectionException $e) { header(‘HTTP/1.1 404 Not Found’); }} else { header(‘HTTP/1.1 404 Not Found’);}return “";如果你想测试上述代码,可使用phpunit,或者傻大粗的方式,这里便于理解使用傻大粗的方式创建一个目录,随后按照Laravel的目录形式创建几个目录,<?php namespace App\Controllers;class UserController{ public function create() { var_dump(0); }}最后public/index.php文件中去调用路由require_once “../../vendor/autoload.php”;Zero\Zero::Get(“user”, “UserController@create”);到这里我们就基本完成了路由的功能,下一章将完善路由的编码致谢感谢你看到这里,希望本篇可以帮到你。具体代码在 https://github.com/CrazyCodes… ...

December 6, 2018 · 1 min · jiezi

Serverless架构详解:开发者如何专注于业务代码本身?

本文来自腾讯云技术沙龙,本次沙龙主题为Serverless架构开发与SCF部署实践 演讲嘉宾:黄文俊,曾负责企业级存储、企业级容器平台等产品的架构与开发,目前主要负责SCF腾讯无服务器云函数产品相关。对容器平台、微服务架构、无服务器架构以及DevOps等多种热门技术领域均有涉猎。大家好,自我介绍一下,目前我是腾讯云无服务器云函数产品负责人。我做了很多年后端开发。今天是从一个程序员角度讲解一下我们怎么样用Serverless架构。我将本次讲解分为几块:第一,Serverless架构介绍;第二,对云函数产品介绍;第三,Serverless使用场景。讲Serverless架构之前我们可以来看一下整个云的发展过程,在没有云之前大家可能都是用的物理服务器,早期时候大家都用的物理机托管方式,采购一些服务器在机房里托管,这个时候大家前期要选择物理机型号,要做好IDC网络;如果出了问题还要请IDC人员帮你操作。这些设备的投入和运维成本还是很高的。云时代到来之后,由于虚拟化技术的运用,我们用上了云主机。云主机是大家直接在云上做虚拟机购买,开通就可以使用。这时候我们称之为IaaS(基础设施即服务),这种情况下就无需物理机运营,直接放到云平台来做。而之后随着容器技术的发展,我们有了容器平台,或者叫PaaS(平台即服务)。在容器平台到来之后实际上还存在一部分基础设施运维问题,但是这时候基础设施逐渐下沉到运维人员进行操作;而从应用开发者角度来看,他们已经不用再去关心虚拟机,或者操作系统。在这种情况下,应用开发人员更多的去关注应用所需要的计算资源或者存储资源的使用。继续向前发展,我们到了FaaS(函数即服务)。这时候运维人员不需要关注底层的运维,而是按需运行的能力。业务开发人员能够进一步做与业务相关的事情。接下来我们来看一下Serverless架构是什么。Serverless从物理机或虚拟机的使用上进行了分离,更关注上层业务的运行情况。Serverless架构包含两块:函数即服务和后端即服务。函数即服务提供的是计算能力。原有的计算能力,无论是容器也好,虚拟机也好都承载在一定的操作系统之上,函数即服务把计算能力进行了进一步抽象,我们在后文再继续进行展开。另外,Serverless还有后端即服务,比如对象存储,数据库应用,缓存服务,我们也可以称之为Serverless,因为这些服务也能够在云上提供开通即服务,开通即使用的能力。在使用这些产品时同样不需要关注它的服务器是什么样的,它的服务器部署在哪里,而是服务开通就可以使用了,后面的运维工作都交给了云,所以不用感知它的最底层服务器,因此我们也可以把它称之为Serverless。这种服务就称之为Serverless后端即服务。这两个合起来可以称为Serverless架构。函数即服务的工作原理是什么样的?在Serverless上是怎样提供计算能力的?大家原来使用容器或者虚拟机的时候都可以知道,我们把代码上传到容器或者上传到虚拟机,然后启动一个进程,代码就可以运行,它就可以接受外部的请求,做一些实时的响应。Serverless和原有的容器或虚拟机不同,实现的是计算托管服务,Serverless用户首先要做的,是把我们称为云函数的代码,提交到平台上进行代码托管;然后要做的是配置触发器。为什么需要配置触发器?因为云函数的运行方式是触发式运行,有触发的时候,代码才会真正运行起来。所以配置触发器意味着我们给它设置了一个触发源,也就是定义了在什么事件下代码才真正运行起来。用户代码托管到平台之后,事件没有到来之前,它仅仅是代码文件和配置存储,代码并没有运行。什么情况下运行?是当事件触发真正到来的时候,云函数才会真正启动一个实例,这个实例就意味着一个计算单元。计算单元被拉起后,这个事件就被传到这个计算单元中进行计算处理。如果这个触发源的事件很多,并发很高的情况下,平台会根据事件的堆积情况,或者事件到达的速度,自动把同一份代码和配置拉起多个实例进行并发处理。因此可以看到Serverless的运行是按需运行,意味着只有在事件到来的情况下,代码才会被拉起,才会运行起来。自动并发,是指云函数平台会根据事件堆积情况自动的进行并发,自动拉起多个实例进行处理。而原有的容器或者虚拟机如果要进行并发的话还是要有一定的手工参与,比如启动更多的容器,或者加入更多的虚拟机来承载高并发的请求。而函数即服务是完全自动的运行。按需运行带来的另外一个特点,是代码在运行起来之后上才会占用计算资源。函数即服务的费用也是根据按需运行来的,也就是函数运行的时候才进行计费;而没有使用的情况下不会计费。实际上大多数互联网业务只有白天的时候,甚至六点之后大家下班之后业务才会迎来高峰,而到凌晨之后实际上没有多少请求的,因此函数即服务能够很好的满足波峰波谷来削峰填谷的能力。从上面的原理可以看出函数即服务的一些特点,比如说代码托管,云函数平台所提供的直接就是运行环境,也就是支持各种开发语言的环境;对于开发者或者函数服务使用者来说,并没有感知到它下面的服务器在哪里,而是由函数平台完成了函数运行的调度。因此实际来讲不需要运维,包括操作系统优化,服务器维护等等这些都是由平台进行承载。而秒级部署意味着函数在真正的被请求的时候才运行。而这个请求才运行代表着当请求到达平台的时候函数才会被实时拉起并运行。运行完成后如果没有后续请求,实例也会退还。由于函数运行是事件触发的,而事件其实包含很多种类,有各种触发器都可以对接云函数。有越多的触发器对接,云函数所能提供的场景也就越多。对于开发者来说,使用云函数的情况下,他真正关注的是应该业务,是使用代码去聚焦他的业务逻辑,例如是拿到这个事件后该进行什么样的逻辑操作,进行什么样的业务存储,而不需要去关注怎么使用业务代码实现高并发,怎么样实现高请求的承载能力。因此这里看到函数即服务能够为应用开发者带来一些便利,而自动并发本身也是函数即服务所具有的特点。而对于腾讯云无服务器云函数,在最开始开发产品的时候,我们目标也是一样,就是把计算进行托管。在计算托管的情况下,我们使用计算就像我们使用腾讯云对象存储一样,在使用的时候不用关心最底层的运维,不用关心虚拟机或者物理机是否安全。和对象存储进行对比也能看到,我们计算也是按照实际使用情况进行计费。当然现在云函数还处于免费期,大家可以随时使用。从使用方法来说,云函数本身,或者说函数即服务这种产品本身的使用方法都是很简单的。我们在开发的时候更多的关注于核心代码的编写。核心代码的意思实际上就是真正的业务逻辑。而且业务逻辑里不需要考虑高并发,因为由刚才给出来的函数即服务这种计算特点来看的话,在高并发请求的时候是通过多个实例处理进行,因此业务代码在编写的时候,就关注单个事件的处理就行。因此,第一步的核心的就是编写核心业务代码,就是用代码要实现什么样的业务。后续就是配置触发方式。配置触发方式就是把函数代码和触发源对接起来。和云平台上其他的产品进行对接,需要什么样的事件,处理什么样的事件,进行什么样的逻辑处理,做好这样的触发源对接后,函数就能够在事件产生的情况下运行。因此,从整个使用方法来看的话,大家真正要做的是两步:第一,编写代码,第二,配置好触发。而对于底层的基础设施,环境配置这块都不需要大家操心的。目前,腾讯云函数从运行环境来说目前已经支持了Python、Nodejs、PHP、Golang、Java等语言的开发运行环境。接下来是触发器,因为触发器越多,云函数所能去使用的场景其实也越多,我们已经实现的触发器有定时触发器;腾讯云对象存储服务,包括文件的上传、删除等时间;CMQ 消息队列服务;API 网关服务,这个是通过serverless 架构实现 API 服务的一款重要触发器;另外,还有ckafka,这个是腾讯云提供的kafka能力。目前kafka算是一个开源产品,我们腾讯云把它包装后放到云上来,也是兼容标准的kafka协议。因此在很多情况下直接迁移到腾讯云不需要任何修改。因为kafka本身作为消息传递的载体,跟腾讯原有的消息队列类似,由消息来执行云函数。下面介绍一下在什么场景下Serverless可以落地?第一,在Serverless场景中最常用到的就是API服务。大家知道实现一个API服务,无论是把API给到浏览器应用,还是给到手机APP使用,还是给到小程序应用,给到它们的时候是以API实现的。要实现这个要有WEB服务器接收连接,对接后端的业务代码,如果你要再进行文件存储,后端的结构化存储,或者有一些缓存需要读写,你的应用服务器后面可能还要对接相应的文件存储,结构化数据库,后续如果想使用缓存,再对接到相应的服务器或相应产品。如果把现有的API服务向Serverless架构演进,那么它将怎么样呈现呢?在不改变 API 的情况下,它的前端浏览器应用、APP、小程序,都可以无缝对接上来。而使用API网关来承接 API 请求,当这个请求来到API网关,由它转发给云函数,触发云函数执行。云函数执行时运行业务逻辑。实际上云函数运行时要求无状态,因此这样的状态存储也需要用到后面的一些存储,无论做缓存也好还是数据库也好都要用上。因此,云上提供的产品一样可以进行对接。像文件存储的话可以用对象存储来进行。数据库的话一样的有相应的数据库产品,结构化还是非结构化数据库都有相应的产品可以使用。同样的,缓存也有相应的产品做对接。云函数通过代码编写,直接进行数据库的读写,或者缓存的读写都是可以的。从整个服务架构来看的话,我们使用最前面的API网关,提供是API能力,甚至进一步能够直提供有SDK服务,更加的方便开发。SDK提供了各种开发语言来直接进行API调用。云函数在中间起到的是业务逻辑处理的作用,而状态数据或者其他业务数据的存储是依赖于后面的文件存储或者数据库进行的。API服务也是Serverless最常用的一种落地形式。这里介绍的场景,都是我们客户在实际使用的场景。在 serverless落地场景中,对对象文件的处理也很常见。对象文件处理指的是对对象文件进行操作后的回调处理。回调通常是在对象文件创建或删除操作后产生的事件。云函数可以在获取到这个事件后进行后续的处理。这里常见的处理逻辑是下面几种,比如说图片处理,针对图片去生成各种尺寸的缩略图或者进行裁剪,然后再次存储到对象存储数据中,之后可以根据不同客户端的请求展示不同大小的图片到前端。文件批量打包,用户需要进行文件筛选和打包的时候可以通过使用云函数来处理。在上传文件后,如果需要选择哪些文件来打包,把文件生成压缩包以供下载,这都可以由事件处理来进行。日志归档分析,以及业务系统回调,也是云函数所承载的业务逻辑。比如说日志归档分析这种用法,用户会把每天的前端应用服务器的日志上传到对象存储中归档,归档后会触发云函数执行,云函数会拉下这些日志文件进行实时分析,它会抽取这些日志中的错误数,或者是其他业务相关或者用户关注的内容,然后再把它抽取到的信息或者统计到的信息写回数据库,供用户后续进行排查、使用。用户自身API调用也是,例如用户生成的一些视频文件上传到对象存储,会触发云函数,将上传文件的信息通知到用户的转码系统,通过视频转码转成不同分辨率然后再进行存储。当然转码是用户自身实现的业务系统,这块通过回调通知,通知它自身的业务系统。这些就是云函数在Serverless架构和对象存储连用的落地场景。再就是CKafka消息处理。CKafka目前比较多的应用场景是做日志存储和日志搜集,例如有多台应用服务器在不断产生日志的情况下,可以把日志写到CKafka,然后CKafka再进行归档和后续分析。而 CKafka和云函数对接是由CKafka收到的信息来进行触发的。日志搜集后,要归档的日志,一般存储到对象存储当中。这种情况CKafka消息,会被推送给云函数,云函数再再把这些消息写到对象存储中去。有些用户不是写对象存储,而是写数据库,以数据库形式归档,其实也是一样的。有的使用场景,需要进行消息分析,会实时拿到消息后立刻分析里面的关键字,如果捕捉到了关键字,会立刻把这些消息推送到ckafka 的另一个topic 中,去及时的发出告警给到业务和运维人员。这也是 serverless 的一种用法,就是对消息的分析和转发。消息队列和CKafka类似,但是消息队列一般不是进行日志的搜集,而是进行业务解耦。消息队列 CMQ 是腾讯云提供的一个高可靠金融级消息队列,通常进行一些业务级消息转发和处理。使用这个产品,实际上做的是业务解耦。云函数在这里承载着消息的逻辑处理过程,它能够在接收到消息后对消息立刻进行业务处理。这个业务处理就是实际的业务逻辑,比如我要根据里面某个消息进行判断,判断它是否合适,要不要进行后续的转发,或者转发到另外的业务系统中去?这就是业务之间执行的逻辑。同时,我们也可以使用云函数,再次进行消息的分派,做状态转移。这个状态转移和后面消息转发都是一样的,它会识别消息里的内容,根据消息里的内容进行转发。这种情况下类似于我们使用云函数进行逻辑处理,把它转移到合适的消息队列,然后再进行处理。这也是我们所见过直接用云函数进行消息派发的使用方式。最后一种形式现在也不少,就是利用定时器触发。原本大家更多是在运维场景下使用定时任务,在原有使用 crontab 脚本的情况下,大家通常还要关心脚本运行是否成功,这台虚拟机是否还在工作。云函数抛弃了大家使用传统的虚拟机或者物理机来去写crontab脚本还要确保可靠性的问题。而在实际使用定时器触发的场景下,这里也有几种用法:一种是业务拨测。这个是周期性的去拨测业务是否还在工作,如果出现异常的情况下能够及时的发出告警,发出邮件或者短信告诉到运维或开发人员。另一个是定时备份,这个是在所需要的周期内,比如每天,或者每两天对数据库进行备份,针对数据库需要做数据导出,导出后再将导出内容以文件的形式存储到合适的地方,例如对象存储中,做好定时备份。还有一个是定时数据计算。因为有些计算是根据一段时间内的统计之后,进行计算并展示。在实际场景中,我们腾讯云内部有业务就是在进行定时数据计算,每两小时做一次统计,然后再把统计数据写到数据库做后续业务的展示以及业务分析。总结:Serverless架构本身给用户带来什么?它实际上就是允许我们更关注业务代码,因此可以更快速的构建业务然后上线。现在互联网开发速度越来越快,因此大家期望的是进一步加快开发和业务真正上线的速度,提高迭代的能力。因此,使用Serverless的话可以更快速让业务上线,让我们更快实现我们的想法。而按需使用是我们这个业务在上线之后,在真正产生请求后,业务才会被调动触发,才会有计算。而如果你的业务产生了爆发式增长,其实也不需要担心平台承载能力或者业务扩展是否跟得上,因为平台提供自动扩展能力,降低了大家对运维的诉求,大家不用关心很底层的东西,而运维人员也可以更偏重流程化和业务相关的运维。这就是Serverless架构给大家带来的一些好处。而作为Serverless里的核心,函数即服务这种产品,是Serverless中所呈现出来的计算型的组件,大家也可以看到它和触发源和后端的各种产品或服务有紧密关联,它可以更多的被看做是云时代的脚本,类似于黏合剂,把前面的触发源和后端的各种存储,数据,服务进行了黏合,真正实现架构落地,才是真正实现业务逻辑落地的能力。Q&AQ:云函数有无限扩展能力,但是整个系统也有可能是有限制的,比如它的背后的数据库和存储,我能不能设这样的一个扩展上限?A:这可以设置上限的。目前可以通过提交工单的方式来设置期望的合适上限。扩展可以在后台设置一个合适值,并发实例扩展到这个就不会再扩展了,避免大量实例连接造成后端的数据库或存储超过连接数限制。Q:实现 API 服务有哪些开发方式?A:云函数实现 API 服务的开发方式有好几种,一个是全部在一个函数里完成,路径和方法解析都在函数里进行。这也是偏传统的开发方式。另外一种是进行拆解,每个函数处理一个 API 路径和方法的请求,这种是微服务的开发方式。而函数和函数之间调用也是可以实现。一种是直接使用云API,一种是使用 API网关包装后的 API。云函数被触发调用的话,除了介绍的很多触发器,在不使用这些触发器的情况下,通过代码或者脚本也可以通过腾讯云的云API调用。Q:在事件触发的时候,就是CMQ事件触发的时候,是否可以保证函数被执行呢?因为不像API网关,调用一下函数可以启动,前端可以感知到。但是CMQ,就是扔到消息队列能否保证这个函数被执行呢?A:因为不像API网关是同步调用。同步调用就是这个调用在运行过程中如果出了问题,无论是平台,比如说资源不足,并发不够,或者比如使用的超时了,这个时候可以立刻感知到。 而像CMQ或者CKafka都是异步的,这样意味着调用后你感知不到,这个消息什么时候执行,执行结果都没法感知。这种解决方法有两种:一种是函数运行后的结果输出,把消息处理后的输出结果再放到另一个消息队列中去,让你外部的业务系统能够感知到。当然这种对外通知也是异步通知。同步通知是另外一种,就是函数里可以对自身业务进行回调API,可以通过代码知道现在的数据处理是什么样的结果,处理完后可以立刻回调到API让业务系统接收到处理结果。Q:像COS触发,拿视频转码来说,这个有可能在300秒内处理不完。现在函数设置时间只能最高300秒,这个有什么解决方案吗?A:为什么各家的云平台,都把这个时间大致定在这个范围内,就是不希望在云函数中进行太重的计算。视频转码就属于太重的计算,而云函数提供的包括CPU能力,内存大小都有限,实际上都不太适合在云函数内进行转码。实际上可以用一些视频服务来实现转码,使用云函数来做这两者之间的桥梁,例如对象存储的事件触发后,云函数拿到这个事件通过调用视频转码服务来转码,而不是在云函数转码。目前腾讯云有这个服务,你可以试试看。获取更多详细资料,请戳以下链接:Serverless 架构.pdf问答 serverless:如何删除一个函数?相关阅读 让业务感知不到服务器的存在——基于弹性计算的无服务器化实践 使用 SCF 无服务器云函数定时备份数据库 云学院 · 课程推荐 | 腾讯专项技术测试组长,结合8年经验为你细说冷热分离法则

August 30, 2018 · 1 min · jiezi