关于andriod:拍乐云-x-美上美学|监管当下素质教育快马加鞭的背后服务逻辑

近几月以来,随着政策对K12教育的监管继续收紧和“五育并举”的重申,素质教育逐步走到台前,从应试教育转向素质教育是我国教育倒退的必然趋势。更多的教育企业开始布局、转型,连教育巨头玩家新东方、猿辅导、瑞思教育也纷纷入局。素质教育正如大江大河个别,时刻筹备着奔流入海。在泛滥素质教育的品类中,美术赛道仍然是高热区。 作为国内当先的高端美育品牌,美上美学「MaySun MayShine」开办于2016年,孵化于A股上市公司信雅达,由信雅达文化艺术「Sunyard Art」深耕艺术多年的业余团队倾力打造,旨在研习艺术、流传美育,引领时代美学新风尚。创建至今,美上美学已签约中国八大美院、九大艺术院校的业余师资,在上海、杭州、大连等地设立多家生存艺术学院,现有学员5000多人。 美学是一种信奉,应该被流传 信雅达文化艺术董事长兼总经理杨肖蓉说:“美学是一种信奉,应该被流传,连续和影响更多人”。美上美学从成立开始,就围绕着整个美学体系来设置课程。第1个板块是艺术美学,譬如国画、油画、书法、插画、素描等;第2个板块是包含了调香插花、社交礼仪等修身习艺课程的生存美学;第3个板块是人文美学,美学的最高境界就是进入哲学概念。三大板块相辅相成,让美上美学造成了本人独有的特色。 美上美学的愿景是心愿为所有渴望通过美学扭转、晋升本人的集体或企业,打造一个真正将美学进行细分与实际的教育平台。它是一家专业性的综合美育平台,而不是一个单纯以培训来定义的培训机构,致力于流传美、遍及美,让美学成为更多人的信奉。 为了更好地流传美学,美上美学 Online 于2020年被重磅推出,它定位于成人美育线上学院,为每个酷爱艺术、享受生存的个体提供个性化定制美育课程。美上美学 Online 采纳真人在线实时小班教学,联合精品录播课与独创的双师课程,打造“1+1+1”的全新课程体系,依靠于手机、iPad、电脑等挪动终端,学员可随时随地承受来自于寰球明星师资的业余互动授课。 实时音视频加持下,艺术没有围墙 在线上美术的互动授课场景中,所有的美育机构都心愿能为学员提供实在、丰盛且身临其境的用户体验,让艺术走出围墙。 拍乐云为美上美学 Online 提供了一站式的实时互动云服务。技术团队深度优化视频编码器,交融多种当先的AI技术晋升画质,利用高清编码能力高度还原画纸的图像色调。为了丰盛教学场景,老师和学生可开启多摄像头性能,采集画纸图像的同时,通过外接摄像头采集视频头像。为了精细化的教学效果,老师还能够在实时拍摄的画纸图像上,随时指导、批注,精密领导每一笔每一色。当老师须要上传教学资料,能够随时上传各种格局的高清多媒体课件,实时共享、同步播放。在课程完结后,依据配置的录制布局,后盾会主动生成课程回放,学生能够点播观看。美术教育互动场景的最大难点在于在实时视频过程中如何保障画纸图像的不变形。拍乐云计划通过几何失真参数估计、映射关系计算等低复杂度、高效率的毫秒级视频解决算法对原始视频进行改正解决,很好地解决了美术视频教学中产生的画纸变形问题。 素质教育快马加鞭,做好品质最重要 整个素质教育赛道虽迎来热潮,但随着更多的选手入局,行业的竞争势必会加剧。毫无疑问的是,无论是学科教育还是素质教育,借助资本的力量蒙眼狂奔已无胜利可能,回归教育的实质,踏踏实实做好产品和口碑,才是长存之道。拍乐云愿携手更多的教育行业客户,踊跃拥抱变动,拓展素质教育赛道,打造线上教学品质,在教育的这条路上一起短跑上来。 ◆ ◆ ◆ ◆ 美上美学技术总监陈晓示意:“咱们从原来的SaaS到当初基于PaaS的自研,是因为SaaS模式存在各种业务细节无奈定制的痛点。拍乐云领有极强的音视频技术能力和粗疏的客户服务态度,同时可能深刻咱们的场景打磨能力。在从SaaS到PaaS的转型过程中,咱们接受了很大的压力,拍乐云小伙伴全程跟进到位,最终按时达标实现上线。愿单方单干越来越好,一起把素质教育的品质做到极致!” ◆ ◆ ◆ ◆

June 24, 2021 · 1 min · jiezi

关于andriod:JAVA升级导致Android编译时Jack-server出错

记录下 Java 8 policy tool 降级导致编译安卓时 Jack server 出错,次要是其中的java启动参数 -Djavax.net.debug=ssl 调试办法,当前遇到相似问题好疾速的解决。 另外仅Android6~Android8.1应用jack编译,8.1之后已废除该工具,详情可看下 https://source.android.google... 背景大家的电脑降级后,编译安卓8.1的编译出错,出错为 communication error with Jack server (35), try 'jack-diagnose' or see Jack server logcurl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to localhost:8077 而后就解决吧,后果依照网上之前呈现问题的计划解决都不行,这个也节约了大家好几天的工夫。 我之前是依照笨办法比照替换解决的,之后网上查资料才找到调试办法(原网页已不记得是哪个了,见谅),如果按该办法调试,应该能够很快就能够解决问题。 调试办法jack server的日志寄存在 ~/.jack-server/logs , 然而日志里没有谬误日志。 通过剖析 prebuilts/sdk/tools/jack-admin , 发现能够用 curl -v https://127.0.0.1:8076 命令去测试,这样不便疾速调试。 然而呢就是https连贯,第二阶段握不上手, # 连贯jack端口握手失败$ curl -v https://127.0.0.1:8076 * Trying 127.0.0.1:8076...* TCP_NODELAY set* Connected to 127.0.0.1 (127.0.0.1) port 8076 (#0)...* TLSv1.3 (OUT), TLS handshake, Client hello (1):* OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to 127.0.0.1:8076 * Closing connection 0curl: (35) OpenSSL SSL_connect: SSL_ERROR_SYSCALL in connection to 127.0.0.1:8076 失常状况下,第一阶段 Client hello 后会有第二阶段 Server hello, 但就是没呈现这样的语句 ...

May 6, 2021 · 2 min · jiezi

关于andriod:HMS怎么给开发者保障应用安全

先说个大伙儿应该都晓得的背景:9月11日下午,华为开发者大会平安与隐衷分论坛在松山湖举办了。 其中,华为消费者业务云服务平安技术专家,对HMS平安架构与数据保护做了解析。内容上总的来说就是,介绍了HMS Core从开发者接入到服务解决的全流程平安机制、列举了典型HMS发放能力的平安与爱护技术。 一、HMS Core的“凋谢”能力,对谁“凋谢”的? 先科普一下: HMS Core(华为挪动外围服务)是华为挪动服务(HMS,HUAWEI Mobile Services)助力开发者高效构建精品利用,是华为为其设施生态系统提供的一套应用程序和服务。开发者只需集成HMS SDK即可应用华为的多个凋谢能力。(百度百科) 意思就是开发者能够通过接入HMS Core的凋谢能力,以低成本、低门槛地开发、更高效地翻新精品利用内容、服务及体验。 二、被“凋谢”的HMS Core,平安如何保障? 首先,HMS Core的“凋谢”给开发者,并不意味着每个开发者能够随便用。个别须要通过三步:开发者联盟门户的注册 - 申请接入和获取认证凭证 - 开发者集成HMS SDK(HMS软件开发工作包)应用凋谢能力,HMS进行接入认证。 其中5大平安技术,能够保障基于HMS Core开发的利用的平安: Ø 认证鉴权:用户认证、接入认证、设施认证; Ø 数据安全与隐衷爱护:数据安全存储、数据应用平安、数据传输平安、密钥治理、隐衷爱护; Ø 内容爱护:版权保护、数字水印、防盗链; Ø 利用平安:上架四重检测、下载安装保障、运行防护机制; Ø 业务风控:帐号风控、交易风控、内容风控、广告防舞弊; 也就是说,从开发者接入HMS Core,到HMS App和三方App的最终利用,每一步都有一道密不透风的平安防线。 三、开发者接入HMS Core的“敲门砖”:认证凭据 HMS Core接入认证时的平安保障有认证凭据、接入束缚和权限管制3种措施。开发者拜访HMS Core的凋谢能力时,须要先在开发者联盟网站创立认证凭据,开发者利用通过携带的认证凭据拜访HMS凋谢能力。以后反对的凭据有API Key、Oauth2.0 ClientID、Service Account Key。这些凭据应用平安随机数生成,生成后在服务器应用AES-GCM减速算法进行加密后存储,避免认证凭据泄露。 除了开发者层面上的保障措施,在利用市场层面,HMS Core履行上架四重检测、下载安装保障、运行防护机制的保障机制。 四、几种印象比拟深的HMS Core凋谢能力的数据保护 账号总被盗、数据被泄露怎么办?帐号平安保障方面,Account kit为利用提供平安便捷的登录能力,例如采纳当下支流的FIDO免密身份认证登录,确保账户数据等平安。除了集成FIDO Kit,华为帐号服务在登录、重置明码等环节都设置了被动风控监测机制来避免帐号盗用,并将操作异样等多种危险辨认伎俩与专家规定、机器学习联合,用来辨认虚伪帐号、避免垃圾注册。这样,华为终端云风控平台就能够做到疾速准确地辨认危险。 指纹和人脸领取、活体检测是怎么保障平安的?以基于PKI(公钥基础设施)的指纹及人脸领取,和交易领取时的活体检测这一更加强有力的风控措施为例,IAP kit能够在利用中提供平安便捷的领取服务,在PKI体系下,线上领取更加平安。这些密钥或证书被无效治理,从而为客户建设起一个平安的网络运行环境。 Push里一堆不感兴趣的音讯?手机上Push推送服务很常见,Push kit基于Push Token接入认证App,为不同设施里的各个利用都调配一个举世无双的token,并对Push音讯加密缓存、主动审核敏感信息,使得跨平台的推送服务更精准牢靠;传输平安方面,应用会话密钥加密Push音讯,辅以订阅音讯完整性保护措施,使得信息传输更平安! 五、不放过每个细节:全流程的平安隐衷质量保证 HMS Core的平安防护措施,从刚开始的需要剖析、平安设计,到平安代码的开发、平安测试都尽量做到抓准每一步、不放过每个细节。例如平安编码方面,要求输入针对HMS我的项目的平安开发指南,并输入能够笼罩到43个端云安全漏洞的防护沙盘,提供优良的平安编码实际;再比方平安测试方面,施行双重防线,除了华为终端云服务部,还有ICSL(华为公司网络安全实验室)投入40+平安测试人员重重防守。 ...

September 25, 2020 · 1 min · jiezi

获取 Andriod keystore签名证书文件,用于打包APP应用

生成 Android 签名文件,目前有两种一种是 eclipse 开发工具生成的后缀名 .keystore 签名文件,一种是Android studio 开发工具生成的后缀名.jks 签名文件,那它们有什么不同?1.一个是 eclipse 生成的签名,用于eclipse 开发的APP应用打包2.一个是 Android studio 就生成的签名,用于Android studio 开发的APP应用打包eclipse 和 Android studio 都是用来开发安卓应用的工具,eclipse 因为各种原因被谷歌公司抛弃了, 谷歌公司推出新的 Android studio 安卓开发工具,更方便,更容易上手。因为我们没有使用到Android studio 开发APP或用于离线打包APP, 所以.jks 签名比较少用。目前我们比较常用的是 .keystore 因为很多第三方工具还是使用.keystore 来进行签名,像腾讯或360的应用加固,都是使用.keystore ,包括我们这里介绍的 DCloud 在线打包APP,也是使用 .keystore 来进行签名。我们没有安装 eclipse 工具如何生成 .keystore 签名文件?获取 .keystore 的步骤一、安装 JDK ,JDK 是开发安卓的必须的组件,如果已安装过了,直接跳过, 如果没有安装 进入 Java JDK 官网下载 安装二、用命令来生成 签名证书首先找到安装的JDK 文件路径 ,例如:C:Program FilesJavajdk1.6.0_43 ,我安装在C盘下,找到 jdk1.6.0_43 文件夹下的 bin 文件夹里的 keytool.exe 文件,它就是用于生成密钥的程序。打开命令cmd , 快捷键是win+r 输入cmd如果不是C盘根目录,可以用输入 cd.. ,返回上一级到出现:C:>然后进去JDK 文件: C:> cd C:Program FilesJavajdk1.6.0_43bin 然后回车键,我的JDK安装在C盘Program Files下,如果你不是可以更改,然后进入到:C:Program FilesJavajdk1.6.0_43bin>把比较常用的属性列出来,大家修改下,下面这段代码就可以直接使用了-alias键的别名,只有前八位字符有效。domekey-keyalg产生键的加密算法。支持DSA和RSA。RSA-keypass键的密码pwd123456-validity键的有效期,单位:天3500天-keystore键,存储的路径c:\key\dome.keystore-keysize产生键的长度1024-keystore 存储的路径,例如:c:keydome.keystore ,C盘下要保证存在key文件夹,不如会报错,dome.keystore ,名称随便填写 3.在命令里 C:Program FilesJavajdk1.6.0_43bin> keytool -genkey -alias domekey -keyalg RSA -keysize 1024 -keypass pwd123456 -validity 3500 -keystore c:keydome.keystore 然后回车键,会提示你输入密码,和一些相关信息,填完全,最后填写 Y 回车键确定就可以,此时在C:key 目录下就会多一个 dome.keystore 签名文件,代表已经完成了。上面填写的keypass 密钥密码是: pwd123456 ,接下来,输入命令后,提示输入的密码是查询密码,两个密码设置为一样就可以。-keypass pwd123456查看 keystore 信息当我们想查看 keystore 的信息时,可以用命令:keytool -list -v -keystore 例如: C:\Program Files\Java\jdk1.6.0_43\bin> keytool -list -v -keystore c:\key\dome.keystore然后输入密码 ,就可以查看到信息最后打包APP即可: ...

March 15, 2019 · 1 min · jiezi

Ionic开发App中重要的部分

写在前面APP赶在了春节之前上线了,所以这次我们分享一下使用Ionic3 + Angular5构建一个Hybird App过程中的经验。什么是Hybird App以及一些技术的选型这里就不讨论了。我每次完成一个部分就写一部分,所以有文章有点长。如果有错误的地方感谢大家指正为什么选了Ionic ?有些朋友说Angular/Ionic不大行,但是我觉的技术没有好坏之分,只有适合不适合。首先在我看来Ionic已经在Hybird App开发领域立足多年,已经相当的成熟了,我觉的比大部分的解决方案都要好。其次因为我们的App是一个弱交互多展示类型的,Ionic满足我们的需求。最后是因为如果你想在没有Android团队和IOS团队支持的情况下独立完成一款APP,那么Ionic我觉的是不二之选。因为Ionic4还在beta版本,并且是公司项目所以依然选用了稳定的3.X版本。注意:非基础入门教程,所以在读这篇文章之前建议你最好先了解Angular, TS, Ionic的基础知识,这里主要是希望大家在使用Ionic的时候能少走一些弯路。由于我自己用的不是很熟练Rxjs这一块就没有写,等以后对Rxjs的理解更加深刻了再加上Angular汇总部分既然是基于Angular那我们首先来了解一下Angular,这个地方积累的是Angular中零散的部分。如果内容多的话后期会拆分为单独的部分Angular组件生命周期Angular的生命周期Hooks官方介绍constructor() : 在任何其它生命周期钩子之前调用。可以用它来注入依赖项,但不要在这里做正事。ngOnChanges(changes: SimpleChanges) => void: 当被绑定的输入属性的值发生变化时调用,首次调用一定会发生在 ngOnInit() 之前ngOnInit() => void: 在第一轮 ngOnChanges() 完成之后调用。只调用一次ngDoCheck() => void: 在每个变更检测周期中调用,ngOnChanges() 和 ngOnInit() 之后ngAfterContentInit() => void: Angular 把外部内容投影进组件/指令的视图之后调用。可以认为是外部内容初始化ngAfterContentChecked() => void: Angular 完成被投影组件内容的变更检测之后调用。可以认为是外部内容更新ngAfterViewInit() => void: 每当 Angular 初始化完组件视图及其子视图之后调用。只调用一次。ngAfterViewChecked() => void:每当 Angular 做完组件视图和子视图的变更检测之后调用, ngAfterViewInit() 和每次 ngAfterContentChecked() 之后都会调用。ngOnDestroy() => void:在 Angular 销毁指令/组件之前调用。Angular中内容映射(插槽)的实现<ng-content></ng-content>默认映射这个内容映射方向是由父组件映射到子组件中这个就相当于vue中的slot,用法也都是一样的:<!– 父组件 –><child-component> 我是父组件中的内容默认映射过来的</child-component><!– 子组件 –><!– 插槽 –> <ng-content> </ng-content>上面是最简单的默认映射使用方式针对性映射(具名插槽)我们也可以通过<ng-content>的select属性实现我们的具名插槽。这个是可以根据条件进行填充。select属性支持根据CSS选择器(ELement, Class, [attribute]…)来匹配你的元素,如果不设置就全部接受,就像下面这样:<!– 父组件 –><child-component> 我是父组件中的内容默认映射过来的 <header> 我是根据header来映射的 </header> <div class=“class”> 我是根据class来映射的 </div> <div name=“attr”> 我是根据attr来映射的 </div></child-component><!– 子组件 –><!– 具名插槽 –><ng-content select=“header”></ng-content><ng-content select=".class"></ng-content><ng-content select="[name=attr]"></ng-content>ngProjectAs上面那些都是映射都是作为直接子元素进行的映射,那要不是呢? 我想在外面再套一层呢?<!– 父组件 –><child-component> <!– 这个时不是直接子节点了 这肯定是不行的 那我们就用到ngProjectAs了–> <div> <header> 我是根据header来映射的 </header> </div></child-component>使用ngProjectAs,它可以作用于任何元素上。<!– 父组件 –><child-component> <div ngProjectAs=“header”> <header> 我是根据ngProjectAs header来映射的 </header> </div></child-component>ng-content有一个@ContentChild装饰器,可以用来调用和投影内容。但是要注意:只有在ngAfterContentInit声明周期中才能成功获取到通过ContentChild查询的元素。既然提到了ng-content那我们就来聊一聊ng-template和ng-containerng-template<ng-template> 元素是动态加载组件的最佳选择,因为它不会渲染任何额外的输出<div class=“ad-banner-example”> <h3>Advertisements</h3> <ng-template ad-host></ng-template></div>ng-container<ng-container> 是一个由 Angular 解析器负责识别处理的语法元素。 它不是一个指令、组件、类或接口,更像是 JavaScript 中 if 块中的花括号。一般用来把一些兄弟元素归为一组,它不会污染样式或元素布局,因为 Angular 压根不会把它放进 DOM 中。<p> I turned the corner <ng-container ngIf=“hero”><!– ng-container不会被渲染 –> and saw {{hero.name}}. I waved </ng-container> and continued on my way.</p>Angular指令Angular中的指令分为组件,属性指令和结构形指令。属性型指令用于改变一个 DOM 元素的外观或行为,例如NgStyle。结构型指令的职责是 HTML 布局。 它们塑造或重塑 DOM 的结构,比如添加、移除或维护这些元素,例如NgFor和NgIf。属性型指令通过Directive装饰符把一个类标记为 Angular 指令, 该选项提供配置元数据,用于决定该指令在运行期间要如何处理、实例化和使用。@Directive通过ElementRef获取绑定元素的DOM对象,ElementRef。通过HostListener响应用户引发的事件,把一个事件绑定到一个宿主监听器,并提供配置元数据。 当宿主元素发出特定的事件时,Angular 就会执行所提供的处理器方法,并使用其结果更新所绑定到的元素。 如果该事件处理器返回 false,则在所绑定的元素上执行 preventDefault。HostListener通过Input装饰符把某个类字段标记为输入属性,并且提供配置元数据。 声明一个可供数据绑定的输入属性,在变更检测期间,Angular 会自动更新它,@Input。@Input(‘appHighlight’) highlightColor: string;下面是一个完整的属性形指令的例子import {Directive, ElementRef, HostListener, Input} from ‘@angular/core’;@Directive({ selector: ‘[sxylight]’})export class SxylightDirective { constructor(private el: ElementRef) { el.nativeElement.style.backgroundColor = ‘yellow’; } // 指令绑定的值 @Input(‘sxylight’) highlightColor: string; // 在指令内部,该属性叫 highlightColor,在外部,你绑定到它地方,它叫 sxylight 这个是绑定的别名 // 指令宿主绑定的值 @Input() defaultColor: string; // 监听宿主事件 @HostListener(‘mouseenter’) onMouseEnter() { this.highlight(this.highlightColor || this.defaultColor || ‘red’); } @HostListener(‘mouseleave’) onMouseLeave() { this.highlight(null); } private highlight(color: string) { this.el.nativeElement.style.backgroundColor = color; }}结构型指令星号()前缀:这个东西其实是语法糖,Angular 把 ngIf 属性 翻译成一个 <ng-template> 元素 并用它来包裹宿主元素。<ng-template>: 它是一个 Angular 元素,用来渲染 HTML。 它永远不会直接显示出来。 事实上,在渲染视图之前,Angular 会把 <ng-template> 及其内容替换为一个注释。<ng-container>: 它是一个分组元素,但它不会污染样式或元素布局,因为 Angular 压根不会把它放进 DOM 中。TemplateRef: 可以使用TemplateRef取得 <ng-template> 的内容,TemplateRef<any>ViewContainerRef: 可以通过ViewContainerRef来访问这个视图容器,ViewContainerRef。完整示例import { Directive, Input, TemplateRef, ViewContainerRef } from ‘@angular/core’;/** Input, TemplateRef, ViewContainerRef 这三个模块是构建一个结构型指令必须的模块* Input: 传值* TemplateRef: 表示一个内嵌模板,它可用于实例化内嵌的视图。 要想根据模板实例化内嵌的视图,请使用 ViewContainerRef 的 createEmbeddedView() 方法。* ViewContainerRef: 表示可以将一个或多个视图附着到组件中的容器。/@Directive({ selector: ‘[structure]’ // Attribute selector})export class StructureDirective { private hasView = false @Input() set structure(contion: boolean) { console.log(contion) if (!contion && !this.hasView) { this.viewCon.createEmbeddedView(this.template) // 实例化内嵌视图并插入到容器中 this.hasView = true } else if (contion && this.hasView) { this.viewCon.clear() // 销毁容器中的所有试图 this.hasView = false } } constructor( private template: TemplateRef<any>, private viewCon: ViewContainerRef ) { console.log(‘Hello StructureDirective Directive’); }}Angular中的Module首先我们来看看NgModuleinterface NgModule { // providers: 这个选项是一个数组,需要我们列出我们这个模块的一些需要共用的服务 // 然后我们就可以在这个模块的各个组件中通过依赖注入使用了. providers : Provider[] // declarations: 数组类型的选项, 用来声明属于这个模块的指令,管道等等. // 然后我们就可以在这个模块中使用它们了. declarations : Array<Type<any>|any[]> // imports: 数组类型的选项,我们的模块需要依赖的一些其他的模块,这样做的目的使我们这个模块 // 可以直接使用别的模块提供的一些指令,组件等等. imports : Array<Type<any>|ModuleWithProviders|any[]> // exports: 数组类型的选项,我们这个模块需要导出的一些组件,指令,模块等; // 如果别的模块导入了我们这个模块, // 那么别的模块就可以直接使用我们在这里导出的组件,指令模块等. exports : Array<Type<any>|any[]> // entryComponents: 数组类型的选项,指定一系列的组件,这些组件将会在这个模块定义的时候进行编译 // Angular会为每一个组件创建一个ComponentFactory然后把它存储在ComponentFactoryResolver entryComponents : Array<Type<any>|any[]> // bootstrap: 数组类型选项, 指定了这个模块启动的时候应该启动的组件.当然这些组件会被自动的加入到entryComponents中去 bootstrap : Array<Type<any>|any[]> // schemas: 不属于Angular的组件或者指令的元素或者属性都需要在这里进行声明. schemas : Array<SchemaMetadata|any[]> // id: 字符串类型的选项,模块的隐藏ID,它可以是一个名字或者一个路径;用来在getModuleFactory区别模块,如果这个属性是undefined // 那么这个模块将不会被注册. id : string}app.module.tsapp.module.ts└───@NgModule └───declarations // 告诉Angular哪些模块属于NgModule │───imports // 导入需要使用的模块 │───bootstrap // 启动模块 │───entryComponents // 定义组建时应该被编译的组件 └───providers // 服务配置entryComponents:Angular使用entryComponents来启用tree-shaking,即只编译项目中实际使用的组件,而不是编译所有在ngModule中声明但从未使用的组件。离线模板编译器(OTC)只生成实际使用的组件。如果组件不直接用于模板,OTC不知道是否需要编译。有了entryComponents,你可以告诉OTC也编译这些组件,以便在运行时可用。Ionic工程目录结构首先来看项目目录Ionic-frame│ build // 打包扩展│ platforms // Android/IOS 平台代码│ plugins // cordova插件│ resources└───src // 业务逻辑代码│ │ app // 启动组件│ │ assets // 资源│ │ components // 公共组件│ │ config // 配置文件│ │ directive // 公共指令│ │ interface // interface配置中心│ │ pages // 页面│ │ providers // 公共service│ │ service // 业务逻辑service│ │ shared // 共享模块│ │ theme // 样式模块│ │ index.d.ts // 声明文件└───www // 打包后静态资源Ionic视图生命周期生命周期的重要性不用多说,这是Ionic官网的介绍constrctor => void: 构造函数启动,构造函数在ionViewDidLoad之前被触发ionViewDidLoad => void: 资源加载完毕时触发。ionViewDidLoad只在第一次进入页面时触发只触发一次ionViewWillEnter => void: 页面即将给进入时触发每次都会触发ionViewDidEnter => void: 进入视图之后出发每次都会触发ionViewWillLeave => void: 即将离开(仅仅是触发要离开的动作)时触发每次都会触发ionViewDidLeave => void: 已经离开页面时触发每次都会触发ionViewWillUnload => void: 在页面即将被销毁并删除其元素时触发ionViewCanEnter => boolean:在视图可以进入之前运行。 这可以在经过身份验证的视图中用作一种“保护”,您需要在视图可以进入之前检查权限ionViewCanLeave => boolean:在视图可以离开之前运行。 这可以在经过身份验证的视图中用作一种“防护”,您需要在视图离开之前检查权限注意: 当你想使用ionViewCanEnter/ionViewCanLeave进行对路由的拦截时,你需要返回一个Boolen。返回true进入下一个视图,返回fasle留在当前视图。可以按照下面的代码感受一下生命周期的顺序constructor(public navCtrl: NavController) { console.log(‘触发构造函数’)}/** * 页面加载完成触发,这里的“加载完成”指的是页面所需的资源已经加载完成,但还没进入这个页面的状态(用户看到的还是上一个页面)。全程只会调用一次 /ionViewDidLoad () { console.log(Ionic触发ionViewDidLoad); // Step 1: 创建 Chart 对象 const chart = new F2.Chart({ id: ‘myChart’, pixelRatio: window.devicePixelRatio // 指定分辨率 }) // Step 2: 载入数据源 chart.source(data) chart.interval().position(‘genresold’).color(‘genre’) chart.render()}/** * 即将进入Ionic视图 这时对页面的数据进行预处理 每次都会触发 /ionViewWillEnter(){ console.log(Ionic触发ionViewWillEnter)}/* * 已经进入Ionic视图 每次都会触发 /ionViewDidEnter(){ console.log(Ionic触发ionViewDidEnter)}/* * 页面即将 (has finished) 离开时触发 每次都会触发 /ionViewWillLeave(){ console.log(Ionic触发ionViewWillLeave)}/* * 页面已经 (has finished) 离开时触发,页面处于非激活状态了。 每次都会触发 /ionViewDidLeave(){ console.log(Ionic触发ionViewDidLeave)}/* * 页面中的资源即将被销毁 一般用处不大 /ionViewWillUnload(){ console.log(Ionic触发ionViewWillUnload)}//守卫导航钩子: 返回true或者false/* * 在视图可以进入之前运行。 这可以在经过身份验证的视图中用作一种“保护”,您需要在视图可以进入之前检查权限 /ionViewCanEnter(){ console.log(Ionic触发ionViewCanEnter) const date = new Date().getHours() console.log(date) if (date > 22) { return false } return true}/* * 在视图可以离开之前运行。 这可以在经过身份验证的视图中用作一种“防护”,您需要在视图离开之前检查权限 /ionViewCanLeave(){ console.log(Ionic触发ionViewCanLeave) const date = new Date().getHours() console.log(date) if (date > 10) { return false } return true}项目配置文件设置Ionic3.X中并没有提供相应的的配置文件,所以我们需要自己按照下面步骤手动去添加配置文件来对项目进行配置。新增config目录src |__config |__config.dev.ts |__config.prod.tsconfig.dev.ts / config.prod.tsexport const CONFIG = { BASE_URL : ‘http://XXXXX/api’, // API地址 VERSION : ‘1.0.0’}在根目录下新增build文件夹,在文件夹中新增webpack.config.js config文件const fs = require(‘fs’)const chalk =require(‘chalk’)const webpack = require(‘webpack’)const path = require(‘path’)const defaultConfig = require(’@ionic/app-scripts/config/webpack.config.js’)const env = process.env.IONIC_ENV/* * 获取配置文件 * @param {} env /function configPath(env) { const filePath = ./src/config/config.${env}.ts if (!fs.existsSync(filePath)) { console.log(chalk.red(’\n’ + filePath + ’ does not exist!’)); } else { return filePath; }}// 定位当前文件const resolveDir = filename => path.join(__dirname, ‘..’, filename)// 其他文件夹别名let alias ={ “@”: resolveDir(‘src’), “@components”: resolveDir(‘src/components’), “@directives”: resolveDir(‘src/directives’), “@interface”: resolveDir(‘src/interface’), “@pages”: resolveDir(‘src/pages’), “@service”: resolveDir(‘src/service’), “@providers”: resolveDir(‘src/providers’), “@theme”: resolveDir(‘src/theme’)}console.log(“当前APP环境为:"+process.env.APP_ENV)let definePlugin = new webpack.DefinePlugin({ ‘process.env’: { APP_ENV: ‘”’+process.env.APP_ENV+’"’ }})// 设置别名defaultConfig.prod.resolve.alias = { “@config”: path.resolve(configPath(‘prod’)), // 配置文件 …alias}defaultConfig.dev.resolve.alias = { “@config”: path.resolve(configPath(‘dev’)), …alias}// 其他环境if (env !== ‘prod’ && env !== ‘dev’) { defaultConfig[env] = defaultConfig.dev defaultConfig[env].resolve.alias = { “@config”: path.resolve(configPath(env)) }}// 删除sourceMapsmodule.exports = function () { return defaultConfig}tsconfig.json配合,配置中新增如下内容 这个地方很扯 这个path相关的需要放在tsconfig.json的最上面"baseUrl": “./src”, “paths”: { “@app/env”: [ “environments/environment” ] }修改package.json。配置末尾新增如下内容"config": { “ionic_webpack”: “./config/webpack.config.js”}使用配置变量import {CONFIG} from “@app/env"如果过我们想修改Ionic中其他的webpack配置, 那么可以像上面那种形式来进行修改。// 拿到webpack 的默认配置 剩下的还不是为所欲为const defaultConfig = require(’@ionic/app-scripts/config/webpack.config.js’);// 像这样去修改配置defaultConfig.prod.resolve.alias = { “@config”: path.resolve(configPath(‘prod’))}defaultConfig.dev.resolve.alias = { “@config”: path.resolve(configPath(‘dev’))}Ionic路由首页设置有时候我们需要设置我们第一次显示得页面。那这样我们就需要使用NavController来设置// app.component.tspublic rootPage: any = StartPage; // 路由跳转href方式跳转:直接在dom中指定要跳转的页面,以tabs中的代码为例<!– 单个跳转按钮 [root]=“HomeRoot” 是最重要的 –><ion-tab [root]=“HomeRoot” tabTitle=“Home” tabIcon=“home”></ion-tab>import { HomePage } from ‘../home/home’export class TabsPage { // 声明变量地址 HomeRoot = HomePage constructor() { }}编程式导航:编程式导航我们可能会用的更多,下面是一个基础的例子编程式导航是由NavController控制NavController是Nav和Tab等导航控制器组件的基类。 您可以使用导航控制器导航到应用中的页面。 在基本级别,导航控制器是表示特定历史(例如Tab)的页面数组。 通过推送和弹出页面或在历史记录中的任意位置插入和删除它们,可以操纵此数组以在整个应用程序中导航。当前页面是数组中的最后一页,如果我们这样想的话,它是堆栈的顶部。 将新页面推送到导航堆栈的顶部会导致新页面被动画化,而弹出当前页面将导航到堆栈中的上一页面。除非您使用NavPush之类的指令,或者需要特定的NavController,否则大多数时候您将注入并使用对最近的NavController的引用来操纵导航堆栈。// 引入NavControllerimport { NavController } from ‘ionic-angular’;import { NewsPage } from ‘../news/news’export class HomePage { // 注入NavControllerconstructor(public navCtrl: NavController) { // this.navCtrl.push(LoginPage)}goNews () { this.navCtrl.push(NewsPage, { title : ‘测试传参’ }) }}相关常用APInavCtrl.push(OtherPage, param): 跳转页面navCtrl.pop(): Removing a view 移除当前View,相当于返回上一个页面路由中参参数相关push(Page, param)传参: 这个很简单也很明白this.navCtrl.push(NewsPage, { title : ‘测试传参’})[navParams]属性:和HTML配合进行传参import {LoginPage } from’./login’;@Component()class MyPage { params; pushPage: any; constructor(){ this.pushPage= LoginPage; this.params ={ id:123, name: “Carl” } }}<button ion-button [navPush]=“pushPage” [navParams]=“params”> Go</button><!– 同理在root page上传递参数就是下面这种方式 –><ion-tab [root]=“tab1Root” tabTitle=“home” tabIcon=“home” [rootParams]=“userInfo”></ion-tab获取参数//NavController就是用来管理和导航页面的一个controllerconstructor(public navCtrl: NavController, public navParams: NavParams) { //1: 通过NavParams get方法获取到单个对象 this.titleName = navParams.get(’name’) //2: 直接获取所有的参数 this.para = navParams.data}provider(service)使用当重复的需要一个类中的方法时,可封装它为服务类,以便重复使用,如http。provider,也叫service。前者是ionic的叫法,后者是ng的叫法。建议仔细得学一下Angular创建ProviderIonic提供了创建指令ionic g provider http 自动创建的Provider会自主动在app.module中导入注意这个需要在app.module中注入首先导入装饰器,再用装饰器装饰,这样,该类就可以作为提供者注入到其他类中以使用:import { Injectable } from ‘@angular/core’;@Injectable()export class StorageService { constructor() { console.log(‘Hello StorageService’); } myAlert(){ alert(“服务类的方法”) }}使用provider如果是顶级的服务(全局通用服务),需要在app.module.ts的providers中注册后然后使用import { StorageService } from ‘./../../service/storage.service’;export class LoginPage { userName: string = ‘demo’ password: string = ‘123456’ constructor( public storageService: StorageService ) { } doLogin () { const para = { userName: this.userName, password: this.password } console.log(para) if (para.userName === ‘demo’ && para.password === ‘123456’) { this.storageService.setStorage(‘user’, para) } setTimeout(() => { console.log(this.storageService.getStorage(‘user’)) }, 3000) }}Ionic事件系统Events是一个发布-订阅样式事件系统,用于在您的应用程序中发送和响应应用程序级事件。这个是不同页面之间交流的核心。主要用于组件的通信。你也可以用events传递数据到任何一个页面。Events实例方法publish(topic, eventData): 发布一个eventsubscribe(topic, handler): 订阅一个eventunsubscribe(topic, handler) 取消订阅一个event// 发布event login.ts// 发布event事件submitEvent (data) { console.log(1) this.event.publish(‘user:login’, data)}// 订阅页面 message.tsconstructor(public event: Events ) { // 订阅event事件 event.subscribe(‘user:login’, (data) => { console.log(data) let obj = { url: ‘assets/imgs/logo.png’, name: data.username } this.messages.push(obj) })}注意点: <font color=“red”>1: 订阅必须再发布之前,不然接收不到。打个比喻:比如微信公众号,你要先关注才能接收到它的推文,不然它再怎么发推文,你也收不到。2: subscribe中得this指向是有点问题的,这里需要注意一下。</font>用户操作事件Basic gestures can be accessed from HTML by binding to tap, press, pan, swipe, rotate, and pinch events.Ionic对手势事件的解释基本是一笔带过。组件间通信组件之间的通信:要把一个组件化的框架给玩6了。组件之前的通信搞明白了是个前提。在Ionic中,我们使用Angular中的方式来实现。父 => 子: @Input()通过输入型绑定把数据从父组件传到子组件:这个用途最广泛和常见,和recat中的props非常相似// 父组件定义值(用来传递)export class NewsPage { father: number = 1 // 父组件数据 /* * Ionic生命周期函数 / ionViewDidLoad() { // 父组件数据更改 setTimeout(() => { this.father ++ }, 2000) }}// 子组件定义属性(用来接收)@Input() child: number // @Input装饰器标识child是一个输入性属性<!– 父组件使用 –><backtop [child]=“father”></backtop><!– 子组件定义 –><div class=“backtop”> <p (click)=“click()">back</p> father数据: {{child}}</div>通过get, set在子组件中对父组件得数据进行拦截来达到我们想要得结果// 拦截父组件得值private _showContent: string @Input()// set valueset showContent(name: string) { if (name !== ‘4’) { this._showContent = ’no’ } else { this._showContent = name }}// get valueget showContent () :string { return this._showContent}通过ngOnChanges监听值得变化// 监听所有属性值得变化ngOnChanges(changes: SimpleChange): void { /* * 从旧值到新值得一次变更 * class SimpleChange { constructor(previousValue: any, currentValue: any, firstChange: boolean) previousValue: any // 变化前得值 currentValue: any // 当前值 firstChange: boolean isFirstChange(): boolean // 检查该新值是否从首次赋值得来的。 } / // changes props集合对象 console.log(changes[‘child’].currentValue) // }父组件与子组件通过本地变量互动父组件不能使用数据绑定来读取子组件的属性或调用子组件的方法。但可以在父组件模板里,新建一个本地变量来代表子组件,然后利用这个变量来读取子组件的属性和调用子组件的方法.通过#childComponent定义这个组件。然后直接使用childComponent.XXX去调用。这个的话就有点强大了,但是这个交流时页面级别的。仅限于在html定义本地变量然后在html中进行操作和通信。也就是父组件-子组件的连接必须全部在父组件的模板中进行。父组件本身的代码对子组件没有访问权。<!– 父组件 –><button ion-button color=“secondary” full (click)=“childComponent.fromFather()">测试本地变量</button><backtop #childComponent [child]=“father” [showContent] = “father” (changeChild)=“childCome($event)"></backtop>// 子组件// 父子组件通过本地变量交互fromFather () { console.log(I am from father) this.show = !this.show}父组件调用@ViewChild()互动如果父组件的类需要读取子组件的属性值或调用子组件的方法,可以把子组件作为 ViewChild,注入到父组件里面。也就是说@ViewChild()是为了解决上面的短板而出现的。// 父组件import { Component, ViewChild } from ‘@angular/core’;export class NewsPage { //定义子组件数据 @ViewChild(BacktopComponent) private childComponent: BacktopComponent ionViewDidLoad() { setTimeout(() => { // 通过child调用子组件方法 this.childComponent.formChildView() }, 2000) }}子 => 父: @Output(): 最常用的方法子组件暴露一个 EventEmitter 属性,当事件发生时,子组件利用该属性 emits(向上弹射)事件。父组件绑定到这个事件属性,并在事件发生时作出回应。// 父组件// 接收儿子组件得来得值 并把儿子得值赋给父亲childCome (data: number) { this.father = data}// 字组件// 子向父传递得事件对象@Output() changeChild: EventEmitter<number> = new EventEmitter() // 定义事件传播器对象// 执行子组件向父组件通信click () { this.changeChild.emit(666)}<!– 父组件 –><backtop [child]=“father” [showContent] = “father” (changeChild)=“childCome($event)"></backtop>获取父组件实例有的时候我们也可以暴力一点获取父组件的实例去使用它(未验证)。constructor( // 注册父组件 @Host() @Inject(forwardRef(() => NewsPage)) father: NewsPage ) { this.text = ‘Hello World’; setTimeout(() => { // 直接通过对象来修改父组件 father.father++ }, 3000) }父 <=> 子:父子组件通过服务来通信如果我们把一个服务实例的作用域被限制在父组件和其子组件内,这个组件子树之外的组件将无法访问该服务或者与它们通讯。父子共享一个服务,那么我们可以利用该服务在家庭内部实现双向通讯。// serviceimport { Injectable } from ‘@angular/core’; // 标记元数据// 使用service进行父子组件的双向交流@Injectable()export class MissionService { familyData: string = ‘I am family data’}// father componentimport { MissionService } from ‘./../../service/mission.service’;export class NewsPage { constructor( public missionService: MissionService) { } ionViewDidLoad() { // 父组件数据更改 setTimeout(() => { // 调用修改service中的数据 这个时候父子组件中的service都会改变 this.missionService.familyData = ‘change familyData’ }, 2000) }}// child componentimport { Component} from ‘@angular/core’;import { MissionService } from ‘./../../service/mission.service’;@Component({ selector: ‘backtop’, templateUrl: ‘backtop.html’})export class BacktopComponent { constructor( public missionService:MissionService ) { console.log(missionService) this.text = ‘Hello World’; } // 执行子组件向父组件通信 click () { // 修改共享信息 this.missionService.familyData = ‘change data by child’ }}<!– 父组件直接使用 –>{{missionService.familyData}}<!– 子组件 –><div> servicedata: {{missionService.familyData}}</div>在service中使用订阅也可以同样的实现数据的通信// mission.service.tsimport { Subject } from ‘rxjs/Subject’;import { Injectable } from ‘@angular/core’; // 标记元数据// 使用service进行父子组件的双向交流@Injectable()export class MissionService { familyData: string = ‘I am family data’ // 订阅式的共享数据 private Source = new Subject() Status$=this.Source.asObservable() statusMission (msg: string) { this.Source.next(msg) }}// 父组件// 通过service的订阅提交信息emitByService () { this.missionService.statusMission(’emitByService’)}// 子组件// 返回一个订阅器this.subscription = missionService.Status$.subscribe((msg:string) => { this.text = msg})ionViewWillLeave(){ // 取消订阅 this.subscription.unsubscribe()}高级通信我们可以使用ionic-angular中的Events模块来进行 父 <=> 子 , 兄 <=> 弟的高级通信。Events模块在通信方面具有得天独厚的优势。具体可以看上面的示例使用EventEmitter模块// serviceimport { EventEmitter } from ‘@angular/core’; // 标记元数据// 使用service进行父子组件的双向交流@Injectable()export class MissionService { // Event通信 来自angular serviceEvent = new EventEmitter()}// 父组件// 通过Events 模块高级通信 接收信息this.missionService.serviceEvent.subscribe((msg: string) => { this.messgeByEvent = msg})// 子组件// 通过emit 进行高级通信 发送新emitByEvent () { this.missionService.serviceEvent.emit(’emit by event’)}Shared组件公共组件设置,Angular倡导的是模块化开发,所以公共组件的注册可能稍有不同。在这里我们根据Angular提供的CommonModule共享模块,我们要知道他干了什么事儿:它导入了 CommonModule,因为该模块需要一些常用指令。它声明并导出了一些工具性的管道、指令和组件类。它重新导出了 CommonModule 和 FormsModuleCommonModule 和 FormsModule可以代替BrowserModule去使用定义在shared文件夹下新建shared.module.tsimport { NgModule } from ‘@angular/core’;import { CommonModule } from ‘@angular/common’;import { FormsModule } from ‘@angular/forms’; // 通过重新导出 CommonModule 和 FormsModule,任何导入了这个 SharedModule 的其它模块,就都可以访问来自 CommonModule 的 NgIf 和 NgFor 等指令了,也可以绑定到来自 FormsModule 中的 [(ngModel)] 的属性了。// 自定义的模块和指令import { ComponentsModule } from ‘./../components/components.module’;import { DirectivesModule } from ‘./../directives/directives.module’;@NgModule({ declarations: [], imports: [ CommonModule, FormsModule ], exports:[ // 导出模块 CommonModule, FormsModule, ComponentsModule, DirectivesModule ], entryComponents: [ ]})export class SharedModule {}注意: 服务要通过单独的依赖注入系统进行处理,而不是模块系统使用了shared模块仅仅需要在xxx.module.ts中引用即可,然后又就可以使用shared中所有引入的公共模块。import { NgModule } from ‘@angular/core’;import { IonicPageModule } from ‘ionic-angular’;import { XXXPage } from ‘./findings’;import { SharedModule } from ‘@shared/shared.module’;@NgModule({ declarations: [ XXXPage, ], imports: [ SharedModule, IonicPageModule.forChild(FindingsPage), ]})export class XXXPageModule {}http部分Ionic中的http模块是直接采用的HttpClient这个模块。这个没什么可说的,我们只需要根据我们的需求对service进行修改即可,例如可以把http改成了更加灵活的Promise模式。你也可以用Rxjs的模式来实现。下面这个是个简单版本的实现:import { TokenServie } from ‘./token.service’;import { StorageService } from ‘./storage.service’;import { HttpClient, HttpHeaders, HttpParams } from ‘@angular/common/http’import { Injectable, Inject } from ‘@angular/core’import {ReturnObject, Config} from ‘../interface/index’ // 返回数据类型和配置文件/Generated class for the HttpServiceProvider provider./@Injectable()export class HttpService{ /* * @param CONFIG * @param http * @param navCtrl / constructor( @Inject(“CONFIG”) public CONFIG:Config, public storage: StorageService, public tokenService: TokenServie, public http: HttpClient ) { console.log(this.CONFIG) } /* * key to ’name=‘qweq’’ * @param key * @param value / private toPairString (key, value): string { if (typeof value === ‘undefined’) { return key } return ${key}=${encodeURIComponent(value === null ? '' : value.toString())} } /* * objetc to url params * @param param / private toQueryString (param, type: string = ‘get’) { let temp = [] for (const key in param) { if (param.hasOwnProperty(key)) { let encodeKey = encodeURIComponent(key) temp.push(this.toPairString(encodeKey, param[key])) } } return ${type === 'get' ? '?' : ''}${temp.join('&amp;')} } /* * set http header / private getHeaders () { let token = this.tokenService.getToken() return new HttpHeaders({ ‘Content-Type’: ‘application/x-www-form-urlencoded’, ’tokenheader’: token ? token : ’’ }) } /* * http post请求 for promise * @param url * @param body / public post (url: string, body ? : any): Promise<ReturnObject> { const fullUrl = this.CONFIG.BASE_URL + url console.log(this.toQueryString(body, ‘post’)) return new Promise<ReturnObject>((reslove, reject) =>{ this.http.post(fullUrl, body, { // params, headers: this.getHeaders() }).subscribe((res: any) => { reslove(res) }, err => { // this.handleError(err) reject(err) }) }) } /* * get 请求 return promise * @param url * @param param / public get(url: string, params: any = null): Promise<ReturnObject> { const fullUrl = this.CONFIG.BASE_URL + url let realParams = new HttpParams() for (const key in params) { if (params.hasOwnProperty(key)) { realParams.set(${key}, params[key]) } } // add time map realParams.set( ’timestamp’, (new Date().getTime()).toString() ) return new Promise<ReturnObject>((reslove, reject) =>{ this.http.get(fullUrl, { params, headers: this.getHeaders() }).subscribe((res: any) => { console.log(res) reslove(res) }, err => { // this.handleError(err) reject(err) }) }) }}Cordova插件使用Ionic提供了丰富的基于cordova的插件,官网介绍,使用起来也很简单。下载Cordova插件cordova add plugin plugin-name -Dnpm install @ionic-native/plugin-name使用插件(从@ionic-native/plugin-name中导入)import { StatusBar } from ‘@ionic-native/status-bar’;constructor(private statusBar: StatusBar) { //沉浸式并且悬浮透明 statusBar.overlaysWebView(true); // 设置状态栏颜色为默认得黑色 适合浅色背景 statusBar.styleDefault() // 浅色状态栏 适合深色背景 // statusBar.styleLightContent() }优化部分项目写完了,不优化一下 心里怪难受的。App启动页体验优化Ionic App毕竟是个混合App,毕竟还没有达到秒开级别。所以这个时候我们需要启动页来帮助我们提升用户体验,首先在config.xml种配子我们的启动页相关配置<preference name=“ShowSplashScreenSpinner” value=“false” /> <!– 隐藏加载时的loader –><preference name=“ScrollEnabled” value=“false” /> <!– 禁用启动屏滚动 –><preference name=“SplashMaintainAspectRatio” value=“true” /> <!– 如果值设置为 true,则图像将不会伸展到适合屏幕。如果设置为 false ,它将被拉伸 –><preference name=“FadeSplashScreenDuration” value=“1000” /><!– fade持续时长 –><preference name=“FadeSplashScreen” value=“true” /><!– fade动画 –><preference name=“SplashShowOnlyFirstTime” value=“false” /><!– 是否只第一次显示 –><preference name=“AutoHideSplashScreen” value=“false” /><!– 自动隐藏SplashScreen –><preference name=“SplashScreen” value=“screen” /><platform name=“android”> <allow-intent href=“market:” /> <icon src=“resources/android/icon/icon.png” /> <splash src=“resources/android/splash/screen.png” /><!– 启动页路径 –> <!– 下面是各个分辨率的兼容 –> <splash height=“800” src=“resources/android/splash/screenh.png” width=“480” /> <splash height=“1280” src=“resources/android/splash/screenm.png” width=“720” /> <splash height=“1600” src=“resources/android/splash/screenxh.png” width=“960” /> <splash height=“1920” src=“resources/android/splash/screenxxh.png” width=“1280” /> <splash height=“2048” src=“resources/android/splash/screenxxxh.png” width=“1536” /></platform>我在这里关闭了自动隐藏SplashScreen,因为她的判定条件是一旦App出事还完毕就隐藏,这显然不符合我们的要求。我们需要的是我们的Ionic WebView程序启动之后再隐藏。所以我们在app.component.ts中借助@ionic-native/splash-screen来进行这个操作.platform.ready().then(() => { // 延迟1s隐藏启动屏幕 setTimeout(() => { splashScreen.hide() }, 1000) })这样一来我们就可以完美的欺骗用户,体验能好点。打包优化新增–prod参数"build:android”: “ionic cordova build android –prod –release”,预(AOT)编译:预编译 Angular 组件的模板。生产模式:启用生产模式部署到生产环境。打捆(Bundle):把这些模块串接成一个单独的捆文件(bundle)。最小化:移除不必要的空格、注释和可选令牌(Token)。混淆:使用短的、无意义的变量名和函数名来重写代码。消除死代码:移除未引用过的模块和未使用过的代码.App打包我认为打包APK对于一些不了解服务端和Android的前端工程师来说还是比较费劲的。下面我们来仔细的说一说这个部分。环境配置第一步进行各个环境的配置Node安装/配置环境变量(我相信这个你已经弄完了)jdk安装 (无需配置环境变量)jdk是java的开发环境支持,你可以在这里下载, 提取码:9p74。下载完成后,解压,直接按照提示安装,全局点确定,不出意外,最后的安装路径为:C:\Program Files\Javajdk安装完成,在cmd中,输入java -version验证是否安装成功。我这边是修改了安装路径,如果你不熟悉的话还是不要修改安装路径。出现了下面的log表示安装成功SDK安装/配置环境变量:这一部分是重点,稍微麻烦一些。先下载。解压后将重命名的文件夹,跟jdk放在一个父目录,便于查找:C:\Program Files\SDK接着配置环境变量,我的电脑——右键属性——-高级系统设置——-环境变量。在下面的系统变量(s)中,新建,键值对如下:name: ANDROID_HOMEkey: C:\Program Files\SDK新建完系统变量之后在path中加入全局变量。在控制台中输入android -h,出现下面的日志,表示sdk安装成功接下来我们使用Android Studio进行SDK下载,Adnroid Studio下载地址,studio安装完之后就要安装Android SDK Tools,Android SDK platform-tools,Android SDK Build-tools这些工具包和SDK platformgradle安装/配置环境变量在SDK都安装完了之后我们再进行gradle的安装和配置。先在官网或者在这里下载然后同样安装在JDK,SDK的目录下,便于查找。和SDK同样的配置环境变量:GRADLE_HOME=C:\Program Files\SDK\gradle-4.1;%GRADLE_HOME%\bin测试命令(查看版本):gradle -v 出现下面的日志,表示安装成功进行打包打包之前的环境准备工作都已经做完了,接下来我们进行打包apk。安装cordovanpm i cordova -g在项目中创建Android工程,在Ionic项目中执行下面命令ionic cordova platform add android这可能是一个很漫长的过程,你要耐心等待,毕竟曙光就在眼前了。创建完Android项目之后项目的platform文件夹下会多出来一个android文件夹。这下接着执行打包命令。ionic cordova build android然后你会看到控制台疯狂输出,最后出现下图表明你已经打包出来一个未签名的安装包APK签名APK不签名是没法发布的。这个有两种方法使用jdk签名,这里不多说,想了解的可以看这篇文章使用Android Studio打签名包。在AS上方工具栏build中选取Generate Signed APK首先创建一个签名文件生成完之后可以直接用AS打签名包点击locate就能看到我们的apk包了~ 至此我们的Android就ok了,IOS的之后再补上。简单APP服务器更新(简单示例)由于Android的要求不如苹果那么严,我们也可以通过自己的服务器进行程序的更新。下面就是实现一个比较简单的更新Service更新我们主要是使用到下面几个Cordova插件cordova-plugin-file-transfer / @ionic-native/file-transfer: 线上文件的下载和存储(官方推荐使用XHR2,有兴趣的可以看一看)cordova-plugin-file-opener2 / @ionic-native/file-opener: 用于打开APK文件cordova-plugin-app-version / @ionic-native/app-version: 用于获取app的版本号cordova-plugin-file / @ionic-native/file:操作app上的文件系统cordova-plugin-device / @ionic-native/device:获取当前设备信息,主要用于平台的区分在下载完插件之后我们来实现一个比较简陋的版本更新service,具体解释我会写在代码注释中,主要分成两部分,一部分是具体的更新操作update.service.ts, 另一部分是用于存放数据的data.service.tsdata.service.ts/* * @Author: etongfu * @Description: 设备信息 * @youWant: add you want info here */import { Injectable } from '@angular/core';import { Device } from '@ionic-native/device';import { File } from '@ionic-native/file';import { TokenServie } from './token.service';import { AppVersion } from '@ionic-native/app-version';@Injectable()export class DataService { /******************************APP数据模块******************************/ // app 包名 private packageName: string = '' // app 版本号 private appCurrentVersion: string = '---' // app 版本code private appCurrentVersionCode:number = 0 // 当前程序运行平台 private currentSystem: string // 当前userId // app 下载资源存储路径 private savePath: string // 当前app uuid private uuid: string /******************************通用数据模块******************************/ constructor ( public device: Device, public file: File, public app: AppVersion, public token: TokenServie, public http: HttpService ) { // 必须在设备准备完之后才能进行获取 document.addEventListener("deviceready", () =&gt; { // 当前运行平台 this.currentSystem = this.device.platform // console.log(this.device.platform) // app版本相关信息 this.app.getVersionNumber().then(data =&gt; { //当前app版本号 data,存储该版本号 if (this.currentSystem) { // console.log(data) this.appCurrentVersion = data } }, error =&gt; console.error(error)) this.app.getVersionCode().then((data) =&gt; { //当前app版本号数字代码 if (this.currentSystem) { this.appCurrentVersionCode = Number(data) } }, error =&gt; console.error(error)) // app 包名 this.app.getPackageName().then(data =&gt; { //当前应用的packageName:data,存储该包名 if (this.currentSystem) { this.packageName = data; } }, error =&gt; console.error(error)) // console.log(this.currentSystem) // file中的save path 根据平台进行修改地址 this.savePath = this.currentSystem === 'iOS' ? this.file.documentsDirectory : this.file.externalDataDirectory; }, false); } /** * 获取app 包名 */ public getPackageName () { return this.packageName } /** * 获取当前app版本号 * @param hasV 是否加上V标识 */ public getAppVersion (hasV: boolean = true): string { return hasV ? V${this.appCurrentVersion} : this.appCurrentVersion } /** * 获取version 对应的nuamber 1.0.0 =&gt; 100 */ public getVersionNumber ():number { const temp = this.appCurrentVersion.split('.').join('') return Number(temp) } /** * 获取app version code 用于比较更新使用 */ public getAppCurrentVersionCode (): number{ return this.appCurrentVersionCode } /** * 获取当前运行平台 */ public getCurrentSystem (): string { return this.currentSystem } /** * 获取uuid */ public getUuid ():string { return this.uuid } /** * 获取存储地址 */ public getSavePath ():string { return this.savePath }}update.service.ts/* * @Author: etongfu * @Email: 13583254085@163.com * @Description: APP简单更新服务 * @youWant: add you want info here */import { HttpService } from './../providers/http.service';import { Injectable, Inject } from '@angular/core'import { AppVersion } from '@ionic-native/app-version';import { PopSerProvider } from './pop.service';import { DataService } from './data.service';import {Config} from '@interface/index'import { FileTransfer, FileTransferObject } from '@ionic-native/file-transfer';import { FileOpener } from '@ionic-native/file-opener';import { LoadingController } from 'ionic-angular';@Injectable()export class AppUpdateService { constructor ( @Inject("CONFIG") public CONFIG:Config, public httpService: HttpService, public appVersion: AppVersion, private fileOpener: FileOpener, private transfer: FileTransfer, private popService: PopSerProvider, // 这就是个弹窗的service private dataService: DataService, private loading:LoadingController ) { } /** * 通过当前的字符串code去进行判断是否有更新 * @param currentVersion 当前app version * @param serverVersion 服务器上版本 */ private hasUpdateByCode (currentVersion: number, serverVersion:number):Boolean { return serverVersion &gt; currentVersion } /** * 查询是否有可更新程序 * @param noUpdateShow 没有更新时显示提醒 */ public checkForUpdate (noUpdateShow: boolean = true) { // 拦截平台 return new Promise((reslove, reject) =&gt; { // http://appupdate.ymhy.net.cn/appupdate/app/findAppInfo?appName=xcz&amp;regionCode=370000 // 查询app更新 this.httpService.get(this.CONFIG.CHECK_URL, {}, true).then((result: any) =&gt; { reslove(result) if (result.succeed) { const data = result.appUpload const popObj = { title: '版本更新', content: `` } console.log(当前APP版本:${this.dataService.getVersionNumber()}) // 存在更新的情况下 if (this.hasUpdateByCode(this.dataService.getVersionNumber(), data.versionCode)) { // if (this.hasUpdateByCode(101, data.versionCode)) { let title = 新版本<b>V${data.appVersion}</b>可用,是否立即下载?<h5 class=“text-left”>更新日志</h5> // 更新日志部分 let content = data.releaseNotes popObj.content = title + content // 生成弹窗 this.popService.confirmDIY(popObj, data.isMust === '1' ? true: false, ()=&gt; { this.downLoadAppPackage(data.downloadPath) }, ()=&gt; { console.log('取消'); }) } else { popObj.content = '已是最新版本!' if(!noUpdateShow) { this.popService.confirmDIY(popObj, data.isMust === '1' ? true: false) } } } else { // 接口响应出现问题 直接提醒默认最新版本 if(!noUpdateShow) { this.popService.alert('版本更新', '已是最新版本!') } } }).catch((err) =&gt; { console.error(err) reject(err) }) }) } /** * 下载新版本App * @param url: string 下载地址 */ public downloadAndInstall (url: string) { let loading = this.loading.create({ spinner: 'crescent', content: '下载中' }) loading.present() try { if (this.dataService.getCurrentSystem() === 'iOS') { // IOS跳转相应的下载页面 // window.location.href = 'itms-services://?action=download-manifest&amp;url=' + url; } else { const fileTransfer: FileTransferObject = this.transfer.create(); fileTransfer.onProgress(progress =&gt;{ // 展示下载进度 const present = new Number((progress.loaded / progress.total) * 100); const presentInt = present.toFixed(0); if (present.toFixed(0) === '100') { loading.dismiss() } else { loading.data.content =已下载 ${presentInt}%` } }) const savePath = this.dataService.getSavePath() + ‘xcz.apk’; // console.log(savePath) // 下载并且保存 fileTransfer.download(url,savePath).then((entry) => { // this.fileOpener.open(entry.toURL(), “application/vnd.android.package-archive”) .then(() => console.log(‘打开apk包成功!’)) .catch(e => console.log(‘打开apk包失败!’, e)) }).catch((err) => { console.error(err) console.log(“下载失败”); loading.dismiss() this.popService.alert(‘下载失败’, ‘下载异常’) }) } } catch (error) { this.popService.alert(‘下载失败’, ‘下载异常’) // 有异常直接取消dismiss loading.dismiss() } }}以上我们就可以根据直接调用service去进行更新app.component.ts// 调用更新this.appUpdate.checkForUpdate()App真机调试说实在的,Hybird真机调试是真的痛苦。目前比较流行的方式是以下两种调试方式Chrome Inspect调试依靠chrome的强大能力,我们可以把App中的WebView中的内容完全的显示在chrome端。可以在web端控制我们的app中的网页,还是先当的炫酷的。以下是操作步骤在chrome中打开chrome://inspect/#devices连接设备,注意第一次连接的话,是需要fan墙的,否则会出现404等等的问题在连接的设备中安装需要调试的App,接着Chrome会自动找到需要调试的WebView愉快的开始调试使用VConsole进行调试这个就更简单了,直接npm install vconsole这个库, 然后在app.component.ts进行引用import VConsole from ‘vconsole’export class MyApp {constructor() { platform.ready().then(() => { console.log(APP_ENV) // 调试程序 APP_ENV === ‘debug’ && new VConsole() }) }}效果如下Ionic中的特殊部分(坑)静态资源路径问题如果在打完包之后静态路径出来问题,没有加载出来的话要注意以下情况<!– html中的img标签直接引用图片处理 –><img src=”./assets/xxx.jpg”/><!– 或者这样 –><img src=“assets/imgs/timeicon.png” style=“width: 1rem;">/scss文件中要使用绝对路径/.bg{ background-image: url(”../assets/xxx.jpg”)}Android API版本修改Ionic中现在默认的SDK版本太高了,有些低版本的机器没发安装需要修改的有以下这么几个部分<!– platforms/android/project.properties –>target=android-26<!– 和platforms/android/CordovaLib/project.properties –>target=android-26关于SDK和cordova插件中的坑(暂时不写)这个东西真的是坑的一塌糊涂,以cordova-plugin-file-opener2为例AS3.0打包之后Android7.0以下的手机无法安装这个不能算是Ionic的坑,要算也得是Android Studio3.0的坑,之前因为不了解在打包的时候下面的选项并没有勾选上不加上的时候一直在Android7.0以下都没法安装,一直以为是项目代码的问题,没想到是设置的问题,加上了V1选项之后打也就可以了,查了一下原因如下。上图中提供的选项其实是签名版本选择,在AS3.0的时候新增的选项。Android 7.0中引入了APK Signature Scheme v2,v1呢是jar Signature来自JDKV1:应该是通过ZIP条目进行验证,这样APK 签署后可进行许多修改 - 可以移动甚至重新压缩文件。V2:验证压缩文件的所有字节,而不是单个 ZIP 条目,因此,在签名后无法再更改(包括 zipalign)。正因如此,现在在编译过程中,我们将压缩、调整和签署合并成一步完成。好处显而易见,更安全而且新的签名可缩短在设备上进行验证的时间(不需要费时地解压缩然后验证),从而加快应用安装速度。如果不勾选V1,那么在7.0以下会直接安装完显示未安装,7.0以上则使用了V2的方式验证。如果勾选了V1,那么7.0以上就不会使用更加安全的快速的验证方式。也可以在app目录下的build.gradle中进行配置signingConfigs { debug { v1SigningEnabled true v2SigningEnabled true } release { v1SigningEnabled true v2SigningEnabled true }}总结这么一番折腾下来,越到了不少坑。但是也都一一解决了。使用Ionic最大的感触就是TS+Angular的模块化开发模式很舒服。而且开发速度上也不至于太慢,对Angular感兴趣的朋友我认为还是可以一试的。示例代码请稍后春节马上到了,祝各位开发者春节快乐远离BUG????????????原文地址 如果觉得有用得话给个⭐吧 ...

January 30, 2019 · 14 min · jiezi