Angular-组件库-NGNEST-源码解析项目结构

前言从本文开始将逐步介绍 NG-NEST UI 库的项目源码结构和组件是如何设计制作的。 环境准备通过以下命令来下载 ng-nest 源码: $ git clone https://github.com/NG-NEST/ng-nest.git$ cd ng-nest$ npm install核心目录介绍| 文件夹名称 | 说明 | | docs | 非组件文档,项目简介、快速上手、教程等 || lib | 组件文件夹,包含框架组件源码 || scripts | ts 脚本,主要用来生成文档页面组件以及相关的路由配置 || src | 文档项目,生成的文档会自动放到 src/main/docs 下 |package.json 文件中的 scripts 说明..."scripts": { "ng": "ng", "postinstall": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points", // 加快 ngcc 编译 "start": "ng serve", // 启动文档项目 "start:en": "ng serve --configuration=en", "start:zh": "ng serve --configuration=zh", "build": "node --max_old_space_size=81920 ./node_modules/@angular/cli/bin/ng build --prod ", // 打包文档项目 "build:en": "ng build --prod --configuration=production-en", "build:zh": "ng build --prod --configuration=production-zh", "build:ng-nest-ui": "node --max_old_space_size=81920 ./node_modules/@angular/cli/bin/ng build ng-nest-ui --prod && npm run build:scss", // 打包组件库并拷贝相关scss样式文件 "build:docs": "npm run build:scripts && node ./scripts/build/generate/docs", // 生成文档页面 "build:scripts": "tsc -p scripts", // ts 脚本编译成 js "build:scss": "cpx ./lib/ng-nest/ui/style/**/* ./dist/ng-nest/ui/style", // 拷贝组件库样式文件 "test": "ng test ng-nest-site", // 测试文档项目 "test:ng-nest-ui": "ng test ng-nest-ui", // 组件测试,通过此处开发测试单个组件 "lint": "ng lint", "e2e": "ng e2e", "extract": "ng xi18n --output-path=locale" // 文档项目的本地化配置},...组件结构我们以 Button 组件来介绍一下组件的结构,所有组件的结构都是遵循此格式。 ...

June 7, 2020 · 2 min · jiezi

记一次-Angular-基于-STOMP-over-WebSocket-实现流文本传输

本文成文时间是 2019-08-18 ,文中提到的最新版本号是以 2019-08-18 为基准的。 前情摘要在介绍正文之前需要先简单了解几个概念: STOMP 协议、STOMP over WebSocket 以及 RxJS。(关于这三点本文不会进行详细介绍) 什么是 STOMP?STOMP 即 Simple or Streaming Text Orientated Messageing Protocal ,是一种简单(流)文本定向传输协议。 STOMP 是 WebSocket 更高级的子协议,它使用一个基于帧的格式来定义消息,与 HTTP 的 Request 和 Response 类似。 STOMP 提供可互操作的连接格式,允许 STOMP 客户端与任意代理进行交互。STOMP 是一个非常简单易用的协议, 服务器端实现起来会相对困难一些,编写客户端非常容易。 STOMP over WebSocketSTOMP over Websocket 即通过 WebSocket 建立 STOMP 连接,也就是说是在 WebSocket 连接的基础上再建立 STOMP 连接。 WebSocket 协议定义了两种类型的消息,文本和二进制,但它们的内容是未定义的。 如果说 Socket 是 C/S 架构 的 TCP 编程,那么同理 WebSocket 就是 B/S架构的 TCP 编程,所以需要在客户端与服务端之间定义一个机制去协商一个子协议 - 更高级别的消息协议,将它使用在 WebSocket 之上去定义每次发送消息的类别、格式和内容,等等。 ...

June 1, 2020 · 3 min · jiezi

记一次解决错误ExpressionChangedAfterItHasBeenCheckedError

问题的出现在写本周的实验时又发现了一个以前没有注意到的angular的报错 ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'ngForOf: [object Object]'. Current value: 'ngForOf: undefined'. 网上找了找出现这个的原因,大概就是视图层显示的数据和c层的数据不一致造成的。 为何会出现视图都成功的渲染出来了,为什么还会报错呢? 先来了解一下angular的渲染流程 angular的渲染流程Angular 程序其实是一个组件树,在变更检测期间,Angular 会按照以下顺序检查每一个组件(注:这个列表称为列表 1): 更新所有子组件/指令的绑定属性调用所有子组件/指令的三个生命周期钩子:ngOnInit,OnChanges,ngDoCheck更新当前组件的 DOM为子组件执行变更检测(注:在子组件上重复上面三个步骤,依次递归下去)为所有子组件/指令调用当前组件的ngAfterViewInit生命周期钩子在每一次操作后,Angular 会记下执行当前操作所需要的值,并存放在组件视图的oldValues属性里(注:Angular Compiler 会把每一个组件编译为对应的 view class,即组件视图类)。在所有组件的检查更新操作完成后,Angular 并不是马上接着执行上面列表中的操作,而是会开始下一次digest cycle,即 Angular 会把来自上一次 digest cycle 的值与当前值比较(注:这个列表称为列表 2): 检查已经传给子组件用来更新其属性的值,是否与当前将要传入的值相同检查已经传给当前组件用来更新 DOM 值,是否与当前将要传入的值相同针对每一个子组件执行相同的检查(注:就是如果子组件还有子组件,子组件会继续执行上面两步的操作,依次递归下去。)所以我的代码开始时的渲染是没问题,问题出在后面的变更检测,方向有了,就去找为啥吧。 问题所在经过排查发现问题是来自这里众所周知,pop()是弹出,而angular不是双向绑定的,这就造成视图层的数据发生了改变,而c层的数据没有改变,也就造成了该错误的出现。 参考文章[译] 关于 `ExpressionChangedAfterItHasBeenCheckedError` 错误你所需要知道的事情

November 2, 2019 · 1 min · jiezi

Nginx部署Angular项目

1.创建前端项目目录进入/var文件夹 cd /var在/var目录下创建www文件夹 mkdir www进入/var/www目录,创建test文件夹 cd www #进入www目录mkdir test2.将前端编译后的文件上传到/var/www/test目录3.配置nginx下test目录及选项进入/etc/nginx/conf.d/目录 cd /etc/nginx/conf.d/打开default.conf,写入test的相关配置 server { listen 80; # nginx监听端口 server_name localhost; #若有域名则将localhost替换为域名 #charset koi8-r; #access_log /var/log/nginx/host.access.log main; location / { root /var/www/test; #test项目路径 index index.html index.htm; #默认起始页 try_files $uri $uri/ /index.html; #spa前端项目路由配置 } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 # #location ~ \.php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \.php$ { # root html; # fastcgi_pass 127.0.0.1:9000; # fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; # include fastcgi_params; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #}}4.测试部署curl localhost #假设项目端口为80则为localhost如若返回的html字符串中没有异常报错的状态码则为成功 ...

October 17, 2019 · 1 min · jiezi

AngularVueReact-和前端的未来

最近社区针对框架的争论,从发文互怼再到粉丝站队再到大漠穷秋准备离职,令人唏嘘不已。不知从何而起,前端圈已经逐步变成了前端娱乐圈。越来越多的人开始站队 Angular、Vue、React,仅仅围绕这些库或者框架进行前端技术讨论,这实在不是什么好的现象。其实我想基于我个人的经验聊下前端的演进和未来,希望可以贡献微薄的力量,消除一些我个人认为的前端社区不太好的风气。 注意:以下只是我个人对于前端和业务的理解和感悟,不代表任何其他人和我所在公司、团队的观点,意见不同欢迎一起讨论。 ======== 以史为鉴,想要知道前端的未来,必须知道前端的过去,抽象前端发展的规律。 前端的历史前端的发展始终伴随着端的发展。 PC 端的兴起06 年左右国内互联网公司开始有了前端工程师的概念,原因很简单,是因为上网访问网页的人数增多,大型互联网公司为了提升用户体验专门剥离了这样一个岗位来解决相关问题。这是第一批专业前端工程师的起源。 在这几年中的发展,进行了很多轮的技术方案、框架、浏览器的演进。比如 jQuery 兼容性库,再到 Require.js 异步加载,再到现在 React、Vue、Angular 等附带编程思想的前端库以及前后端分离、前端构建器、样式预处理器等。这些演进都是随着 PC 端的用户量的增多和业务复杂度的提升,为了用户体验和开发者体验而进化的。 移动端的兴起09 年左右,智能手机的兴起导致了移动端开发的热潮。人人拥有智能手机,这种特殊的端的特性,也产生了新的业务形态。因此无线前端相关需求开始爆发,无线前端开发、iOS/Android 工程师等需求量非常大。 这几年中的发展,先从最初把 PC 端页面放在手机上渲染,再到出来响应式设计的概念,再到专门做无线端页面,再到独立客户端和 Weex、React Native 这些跨终端的方案。也是出现了非常多的技术演进,这些演进不难看出也是因为用户量的增多和业务复杂度的提升,为了用户体验和开发者体验而进行优化的。 PC 端的衰落14 年左右,其实 PC 端颓废之势早已显现,但在双十一下被放大。因此阿里系前端在这个时间点附近就开始弱化 PC 端前端的投入。 以前 PC 端业务,在无线端流量更大的直接被下掉,核心链路的 PC 端业务能用就可以了,不再做效果、功能迭代优化。甚至很多业务直接不做 PC 端只做无线端。业务指标也从 PC 的 PV、UV 变成了客户端的 DAU 等指标。 在这个时间,只做 PC 端的前端,毫无无线端经验的前端,将会慢慢丧失竞争力。PC 兼容库 jQuery 之流也渐渐被替换废弃,因为 PC 的业务很少花费精力做兼容性测试,甚至目前我们团队的业务从来都只测试最新版 Chrome。可以看到,随着端和业务形态的变化,很多前端演进的产物会逐步被替换废掉。 移动端的衰落移动端目前还没有衰落,但一个端只要兴起,就会有衰落的时候。总会有新的、更好的、更高效的端来替代老的端。但这个时机是难以预测的。 前端的未来回顾前端的历史,前端总是伴随的端的变化而变化: 端的出现 -> 业务场景的落地 -> 需要端的开发者 -> 端开发者学习、演进 -> 端的开发效率提升 -> 新的端出现 -> 端的没落 -> 端开发者转领域或者被淘汰 ...

October 15, 2019 · 1 min · jiezi

Angular国际化方案ngxtranslate

效果图: 步骤一. 安装依赖 npm install @ngx-translate/core @ngx-translate/http-loader rxjs --save@ngx-translate/core包含供翻译的核心程序:TranslateService和一些管道。 @ngx-translate/http-loader从网络服务器加载翻译文件 步骤二. 在app.module.ts中初始化TranslateModule: import { BrowserModule } from '@angular/platform-browser';import { NgModule } from '@angular/core';import { AppComponent } from './app.component';// import ngx-translate and the http loaderimport {TranslateLoader, TranslateModule} from '@ngx-translate/core';import {TranslateHttpLoader} from '@ngx-translate/http-loader';import {HttpClient, HttpClientModule} from '@angular/common/http';@NgModule({ declarations: [ AppComponent ], imports: [ BrowserModule, // ngx-translate and the loader module HttpClientModule, TranslateModule.forRoot({ loader: { provide: TranslateLoader, useFactory: HttpLoaderFactory, deps: [HttpClient] } }) ], providers: [], bootstrap: [AppComponent]})export class AppModule { }// required for AOT compilationexport function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http);}步骤三. 在app.component.ts组件中设置默认语言 ...

October 9, 2019 · 1 min · jiezi

免费angular8高级实战教程网易云音乐pc端

自制angular8实战教程,先上链接: 网易云课堂:https://study.163.com/course/...B站:https://www.bilibili.com/vide...历时个把月,本想出个单一功能的教程,没想一开始就控制不住了,到最后时长竟高达30多个小时, 为什么是angular?angular是我的第一个框架,所谓先入为主,即使工作中怕是再难用上,也不会把它丢掉,而且angular用户是痛苦的,至少在国内,不论是文档、生态、百度、教程等都全面被vue和react压制,并非angular技不如人,只因google太任性。本教程也算是为推进angular做点贡献吧,这应该是前端框架中,最给力的免费教程了。 做什么?用angular8仿造网易云音乐pc端的部分功能,包括:歌单、歌曲、歌手和会员的登录注册等,并实现网易云核心的播放器功能。 能学到什么?主技术栈:angular8 + ngrx8 + ng-zorro + material/cdk,包括但不限于: ng-template,ng-content,ng-containerng模块化设计proxy,http拦截器依赖注入自定义指令和管道自定义表单控件动态组件各种rxjs操作符material/cdk变更检测策略ngrx8...课程特色?本课程全程都在实战,在开发过程中会尽力覆盖ng的各种api,项目的模块化、目录设计和组件化等都是以真实项目标准来做的,可复用到日常工作的各项目中去,代码极度精简,期间更有徒手造轮子的过程,是一门学习框架和提升基本功的双休课程。源码也分好了章节上传到github:master分支是最终完成的代码https://github.com/lycHub/ng-wyy 需要的基本知识?typescriptrxjsangular基本api的使用(重要)学完后能达到什么水平?由于本课程会尽可能多的使用angular高级api,如果能完全掌握,那在使用层面已经非常优秀了。完全可以独自用angular胜任网易云音乐官网这种难度的项目 教授方式?手写每行ts和html,样式部分复制做好的。 很遗憾无法上传到慕课网,因为实现没有做好功课,推荐大家去网易云课堂或51cto(待上)观看,记的好评哟: 网易云课堂:https://study.163.com/course/...B站:https://www.bilibili.com/vide...

October 8, 2019 · 1 min · jiezi

angular依赖注入

一 基本流程服务或组件中注册服务提供商-->配置-->注入器-->把服务注入到-->组件中

October 5, 2019 · 1 min · jiezi

代码美化的艺术

Ng-Matero 中文版文档已经发布 点击查看前言原本只是想简单的聊一下代码格式化的问题,无奈本文拖沓了很久,在此期间,我又思考了很多,我越来越觉得代码格式化是一门艺术。为了衬托“艺术”二字,可能叫“代码美化”更贴切一点,但是本文的深度远没有标题那么宏大。 在我看来,代码质量不仅体现在逻辑上,也要体现在形式上。尤其前端代码,在日渐复杂的单页面开发中,代码格式化不仅是为了美观,也是为了更好的阅读。关于代码的格式化并没有统一的标准,每个人都有自己的见解,所以本文的目的以探讨和推荐为主。 可能很少有人会去考虑这方面的问题,毕竟美化插件都是现成的,比如常用的 Prettier,只要一个快捷键就可以迅速格式化,但是代码格式化插件的标准并不一定是最好的。 本文范例主要以 Angular 为主,但是代码美化的建议同样适用于 React 和 Vue。 每行代码多少字符合适?关于代码字符数一直是一个争论不休的问题。在 Python编码风格指导(PEP8) 规定了每行不超过 80 个字符。Prettier 默认也是 80 个字符。 赞成这条规范的人认为 80 个字符紧凑美观,在大屏显示器也可以分多栏显示。如下图所示: 我最开始也是赞成 80 个字符的建议,但是当我遵循这条规范写了近一年的 Angular 代码之后,我发现这条规范有一些缺陷。 首先这条规范是 Python 编码风格的建议,而 Python 的代码是以缩进代表代码块,类、函数等在定义时也没有大括号及小括号,算上括号前的空格,这就比一般的代码少几个字符。 其次现代的编程模式大多是面向对象的风格,类的继承、接口实现等都可能导致代码很长,在 Angular 中可能还会实现多个钩子函数的接口。 另外,Angular 的风格指南建议不要为了精简变量命名而损失易读性,所以很多时候函数命名可能很长,再加上类型系统及链式调用等,单行代码很容易超过 80 个字符,这样会造成过多的折行。 下面是一段使用 80 字符宽度格式化的 TS 代码: 我们再看一下扩大到 100 字符之后的效果: 这段代码或许还不是最典型的例子,但是也能看出两者的不同,在实际的业务当中,类似的折行可能更多,而从我个人的角度来看,过多的折行反而破坏了代码的完整度。目前常用的代码宽度有三种,分别是 80、100、120,很显然,80 太短,120 太长,以中庸之道,取 100 刚好。 模板格式化代码宽度对模板(html)的影响也很大,下面我们重点聊一下关于模板的格式化问题。以下是使用 Prettier 的默认设置格式化的效果。 上面这种格式化方案非常普遍,但是我个人并不喜欢这种格式化的效果,原因有以下几点: 开始标签末尾的尖括号看上去有点突兀。所有属性全部换行,整体有些松散,模板代码可能变得很长。标签和属性的区分度不高。我比较喜欢下面的格式化方案,整齐紧凑,属性之间对齐,标签一目了然。 简单说一下上面这种格式化效果的方法:需要使用 VSCode 默认的 HTML 格式化插件。在 首选项-设置-扩展-HTML,设置 Wrap Attributes 属性,选择 preserve-aligned(保留属性的包装,但对齐),这个选项允许多个标签单行显示。 ...

September 20, 2019 · 1 min · jiezi

不再-封装-组件

世事总不能尽如人意。 就算强如 NG-ZORRO,虽然 99% 的情况下已经足够丰富,足够强大,足够恰当,但总有 %1 的情况,你会希望它能这样、或者那样就好了。 很多项目组都有 “封装” 组件的优良传统。理由有技术层面的,也有非技术层面的。我以前也没少干这事儿。有的时候是 nz-select 变成了 my-select、nz-date-picker 变成了 my-date-picker;有的时候是它们统统变成了 my-form-edit-control。 最后,我们获得了好处,也付出沉重的代价: 组件使用起来更加简单,但更加不灵活;组件 API 更加通用,但到最后程序往往不得不学习两套API;组件功能更加强大,但与原来的组件有哪些微妙的差别又很让人迷惑,到底该用哪个又总要纠结;有时候能有效屏蔽破坏性更新,但慢慢就开始阻碍进步……怎么破? 轻量级、装饰模式、单一职责 这是软件领域最美妙的几个词儿之一,而且它们很适合用来形容 Angular 的指令。 例如,如果我们希望卡片组件能自适应页面的高度,就可以写一个名为 nsAutoHeightCard 的指令 源码 demo,将其 “装饰” 到 nz-card 组件上,就实现了自适应页面高度的卡片。 <nz-card nsAutoHeightCard nzTitle="自适应高度卡片"> <p>Card content</p> </nz-card> DRY NG-ZORRO 作为组件库,保持组件模型的简单、直接没有问题。但我们在实际的工程实践中,需要更大程度地遵循 DRY(Don’t Repeat Yourself) 原则,以表单验证为例: 对于必填项等常规表单验证显示错误信息的功能,应该统一处理,而不是在每个表单中将错误信息重复一遍。既然已经在 FormGroup 对象中配置了验证条件,表单组件最好能根据这些条件自动配置验证视觉反馈,而不用再在表单组件中重复配置 nzRequired 和 nzHasFeedback。我们可以通过无脑统一给每个组件增加 nsAutoFeedback 和 nsErrorTip 指令 源码1 源码2 demo,就可以将上述那些繁琐枯燥还易错的工作全部自动完成,甚至还能自动为标签和编辑组件增加 nzFor 和 id 属性: <nz-form-item *nsAutoFeedback> <nz-form-label [nzSpan]="8">用户名</nz-form-label> <nz-form-control nsErrorTip nzSpan="16"> <input nz-input formControlName="userText" /> </nz-form-control></nz-form-item> ...

September 9, 2019 · 1 min · jiezi

Angular-路由

概述路由所要解决的核心问题就是通过建立URL和页面的对应关系,使得不同的页面可以用不同的URL来表示。Angular路由的核心工作流程图 首先,当用户在浏览器上输入URL后,Angular将获取该URL并将其解析生成一个UrlTree实例其次,在路由配置中寻找并激活与UrlTree实例匹配的配置项再次,为配置项中指定的组件创建实例最后,将该组件渲染于路由组件的模板中<router-outlet>指令所在的位置基本用法Angular路由最为基本的用法是将一个URL所对应的组件在页面中显示出来。要做到这一点,有三个必不可少的步骤,分别是创建根路由模块、定义路由配置、添加<router-outlet>指令标签。 创建根路由模块根路由模块包含了路由所需要的各项服务,是路由工作流程得以正常执行的基础。 下面的代码以路由配置rootRouterConfig为参数,通过调用RouterModule.forRoot()方法来创建根路由模块,并将其导入到应用的根模块AppModule中。 app-routing.module.ts const rootRouterConfig: Routes = [ { path: 'add', component: AddComponent, }, { path: 'list', component: ListComponent, }, { path: '', redirectTo: 'add', pathMatch: 'full', }, { path: '**', redirectTo: 'add', pathMatch: 'full', }];@NgModule({ imports: [RouterModule.forRoot(rootRouterConfig)], exports: [RouterModule]})export class AppRoutingModule { }path 不能以斜杠 / 开头** 通配符路由,不满足以上路径时,选择此路由路由策略HashLocationStrategyhttp://localhost:4200/#/addHashLocationStrategy是Angular路由最为常见的策略,其原理是利用了浏览器在处理hash部分的特性 浏览器向服务器发送请求时不会带上hash部分的内容,更换URL的hash部分不会向服务器重新发送请求,这使得在进行跳转的时候不会引发页面的刷新和应用的重新加载使用该策略,只需要在注入路由服务时使用useHash属性进行显示指定即可 app-routing.module.ts @NgModule({ imports: [RouterModule.forRoot(rootRouterConfig, { useHash: true })], exports: [RouterModule]})HashLocationStrategy路由跳转Web应用中的页面跳转,指的是应用响应某个事件,从一个页面跳转到另一个页面的行为。对于Angular构建的单页面而言,页面跳转实质上就是从一个配置项跳转到另一个配置项的行为。 指令跳转指令跳转通过使用RouterLink指令来完成,该指令接收一个链接参数数组,Angular将根据该数组生成UrlTree实例进行跳转 <div [routerLink]="['/add']" routerLinkActive="add" >add</div><div [routerLink]="['/list']" routerLinkActive="list" >list</div>第一个参数名可以使用 /、./ 或 ../ 前缀 ...

July 26, 2019 · 1 min · jiezi

一步一步搭建前端监控系统如何定位前端线上问题

摘要: 记录用户行为,排查线上BUG。 作者:一步一个脚印一个坑原文:如何定位前端线上问题(如何排查前端生产问题)Fundebug经授权转载,版权归原作者所有。 一直以来,前端的线上问题很难定位,因为它发生于用户的一系列操作之后。错误的原因可能源于机型,网络环境,复杂的操作行为等等,在我们想要去解决的时候很难复现出来,自然也就无法解决。 当然,这些问题并非不能克服,让我们来一起看看如何去定位线上的问题吧。 所谓,工欲善其事必先利其器,你不能撸起袖子蛮干,所以,我们需要一个工具。我们曾经尝试用过很多监控工具去统计这些错误,比如,听云、OneApm、sentry、Fundebug、growingIo 等等。 每家工具都各有所长,但也都各有所短,而且要花不少的钱(感觉是痛点,哈哈)。 一、统计前端错误(Demo)众所周知,我们有办法去统计前端的错误,那就是大名鼎鼎的 window.onerror 方法, 用法如下: // 重写 onerror 进行jsError的监听window.onerror = function(errorMsg, url, lineNumber, columnNumber, errorObj) { var errorStack = errorObj ? errorObj.stack : null; // 分类处理信息 siftAndMakeUpMessage(errorMsg, url, lineNumber, columnNumber, errorStack);};window.onerror 方法中参数的意义我就不一一介绍了,我相信大家也已经耳熟能详了。 总之它能够为我们记录下线上的很多错误,以及一些额外的信息。我将window.onerror方法收集到的错误信息进行分析统计后的结果如下: 如上图所见: 我们统计出了每天的错误量,每个小时的错误量,每天的错误率变化,来鉴定我们线上环境是否健康。我们按照JS错误数量进行分类排序,按照页面进行错误分类。通过上边的数据分析,我们能够清晰地观察到线上项目的报错情况。 二、分析错误详情线上的错误日志统计出来了, 如何解析这些错误日志呢。如下图,解析出用户的机型,版本,系统平台,影响范围,以及具体的错误位置, 从而提高我们解决问题的效率。 疑问?window.onerror 方法能够利用的地方都已经用的差不多了,但是它真的可以帮我们定位和解决前端线上的问题吗? 线上能够修复的问题我已经尽量修复了,但是线上的问题频发。 当客服反馈一个问题, 你发现没有测试机型,无法复现用户错误的时候,让你来修复这个问题,只能两眼一抹黑,无能为力。 例如:线上用户进过了复杂的链接跳转而发生了错误;用户调用的接口发生了异常或者超时;线上的用户反馈异常根本就跟实际情况不符,等等。这些非直观型的问题该如何解决? 所以,我们需要用户的行为记录。 三、记录用户的访问行为(Demo)有些错误是前端页面经过复杂的跳转、回退之后才发生的,就算测试人员也很难测试出这种问题,因为线上的用户的任何行为都有可能出现。往往我们知道的只是他在最后停留的页面发生了错误。 如此,我们记录下用户的跳转日志, 就能够复现出用户的行为, 从而复现BUG。 四、记录用户的接口行为接口请求是一个前端项目涉及最多的行为,接口的异常包括:后台报错,响应超时,网络环境较差,重复接口数据覆盖等等。这些错误也只有在真实的用户环境中才会发生,是典型的线上问题。我们可以记录下用户的请求时间,参数,响应时间,响应状态等等,可以具体分析出来接口对页面的影响。 五、记录用户的点击行为用户经过一系列复杂的行为操作之后(主要是点击行为),页面的样子和保存的数据都经过了很多变化,此时此刻最容易发生数据错乱的现象,导致修复bug的时候无从入手,是复现用户行为中重要的一环。 六、记录用户的页面截图即使你记录下所有的行为,但是你依然需要看到页面的样子,才能够分析出问题所在,那么我们依然可以通过js截图来看看用户设备上的样子。 七、分析用户的场外信息当用户所有的行为都被我们掌握之后,我们能够复现出用户的行为,甚至能够复现出用户的问题,也许我们还需要一些场外信息才能精准定位问题,比如,用户的机型,地理位置,系统版本,当时的网络环境(这个不准确,我是依据用户当时首次加载页面的时间来判断,只能作为参考依据) ...

July 15, 2019 · 1 min · jiezi

响应式编程的思维艺术-5Angular中Rxjs的应用示例

本文是【Rxjs 响应式编程-第四章 构建完整的Web应用程序】这篇文章的学习笔记。示例代码托管在:http://www.github.com/dashnowords/blogs 博客园地址:《大史住在大前端》原创博文目录 华为云社区地址:【你要的前端打怪升级指南】 [TOC] 一. 划重点RxJS-DOM原文示例中使用这个库进行DOM操作,笔者看了一下github仓库,400多星,而且相关的资料很少,所以建议理解思路即可,至于生产环境的使用还是三思吧。开发中Rxjs几乎默认是和Angular技术栈绑定在一起的,笔者最近正在使用ionic3进行开发,本篇将对基本使用方法进行演示。 冷热Observable 冷Observable从被订阅时就发出整个值序列热Observable无论是否被订阅都会发出值,机制类似于javascript事件。涉及的运算符bufferWithTime(time:number)-每隔指定时间将流中的数据以数组形式推送出去。 pluck(prop:string)- 操作符,提取对象属性值,是一个柯里化后的函数,只接受一个参数。 二. Angular应用中的Http请求Angular应用中基本HTTP请求的方式: import { Injectable } from '@angular/core';import { Observable, of } from 'rxjs';import { MessageService } from './message.service';//某个自定义的服务import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http';@Injectable({ providedIn: 'root'})export class HeroService { private localhost = 'http://localhost:3001'; private all_hero_api = this.localhost + '/hero/all';//查询所有英雄 private query_hero_api = this.localhost + '/hero/query';//查询指定英雄 constructor(private http:HttpClient) { } /*一般get请求*/ getHeroes(): Observable<HttpResponse<Hero[]>>{ return this.http.get<Hero[]>(this.all_hero_api,{observe:'response'}); } /*带参数的get请求*/ getHero(id: number): Observable<HttpResponse<Hero>>{ let params = new HttpParams(); params.set('id', id+''); return this.http.get<Hero>(this.query_hero_api,{params:params,observe:'response'}); } /*带请求体的post请求,any可以自定义响应体格式*/ createHero(newhero: object): Observable<HttpResponse<any>>{ return this.http.post<HttpResponse<any>>(this.create_hero_api,{data:newhero},{observe:'response'}); } }在express中写一些用于测试的虚拟数据: ...

July 13, 2019 · 2 min · jiezi

nzform-inline-模式下多类型控件打乱布局的问题

nzForm 布局被打乱nz-form 布局被打乱的原因表单样式使用行内:[nzLayout]="'inline'"表单中使用多中类型的控件(input、datepicker、select...)会出现日期选择控件没有充满,同时 select 控件会被挤到下一行,看看下面的效果 使用官方的样式类设置统一宽度解决在模板对应的CSS中使用下面的样式设置统一宽度 /* 通过设置下面两个样式的宽度解决 nz-select 打乱 form 布局的问题*//* nz-form-control 外围类 */.ant-form-item-control-wrapper{ width: 152.16px;}/* nz-form-item 样式类*/.ant-form-item{ width: 221.2px;}设置后的效果是:其他解决方法实际不该使用inline模式,而应该使用horizontal模式,一个 nz-form-item 中放置4个 label 和 control因为一个 nz-form-item 是一行

July 12, 2019 · 1 min · jiezi

Material-Datepicker自定义日期格式

前言在企业级开发中,我们经常会使用到日期控件。在以前的jquery框架项目中,我们通常会选择mydate 97。现在我们使用Angular框架,Material官网也为我们提供了Datepicker组件。对于日期的显示的格式,有很多种,比如下面的: 2019/07/112019-07-11等...面对这种需求,就需要我们可以自定义Datepicker的显示格式。 自定义日期格式官网为我们提供了一个自定义格式的范例: <mat-form-field> <input matInput [matDatepicker]="dp" placeholder="Verbose datepicker" [formControl]="date"> <mat-datepicker-toggle matSuffix [for]="dp"></mat-datepicker-toggle> <mat-datepicker #dp></mat-datepicker></mat-form-field>import {Component} from '@angular/core';import {FormControl} from '@angular/forms';import {MomentDateAdapter} from '@angular/material-moment-adapter';import {DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE} from '@angular/material/core';// Depending on whether rollup is used, moment needs to be imported differently.// Since Moment.js doesn't have a default export, we normally need to import using the `* as`// syntax. However, rollup creates a synthetic default module and we thus need to import it using// the `default as` syntax.import * as _moment from 'moment';// tslint:disable-next-line:no-duplicate-importsimport {default as _rollupMoment} from 'moment';const moment = _rollupMoment || _moment;// See the Moment.js docs for the meaning of these formats:// https://momentjs.com/docs/#/displaying/format/export const MY_FORMATS = { parse: { dateInput: 'LL', }, display: { dateInput: 'LL', monthYearLabel: 'MMM YYYY', dateA11yLabel: 'LL', monthYearA11yLabel: 'MMMM YYYY', },};/** @title Datepicker with custom formats */@Component({ selector: 'datepicker-formats-example', templateUrl: 'datepicker-formats-example.html', styleUrls: ['datepicker-formats-example.css'], providers: [ // `MomentDateAdapter` can be automatically provided by importing `MomentDateModule` in your // application's root module. We provide it at the component level here, due to limitations of // our example generation script. {provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE]}, {provide: MAT_DATE_FORMATS, useValue: MY_FORMATS}, ],})export class DatepickerFormatsExample { date = new FormControl(moment());}效果如下: ...

July 11, 2019 · 2 min · jiezi

成为优秀Angular开发者所需要学习的19件事

一款to-do app基本等同于前端开发的“Hello world”。虽然涵盖了创建应用程序的CRUD方面,但它通常只涉及那些框架或库也能做到的皮毛而已。 Angular看起来似乎总是在改变和更新 - 但实际上,还是有一些事情仍然保持不变。以下是关于Angular所需要学习的核心概念的概述,以便大家可以正确地利用JavaScript框架。 说到Angular,我们需要学习很多东西,很多人被困在初学者的圈子里,仅仅是因为不知道去哪里搜索或者应该搜索什么关键词。下面我们会说到的这个指南(也是对Angular本身的一个快速摘要),我其实很希望我在第一次开始使用Angular 2+时就已经有了。 1.模块化Angular架构理论上,你可以将所有Angular代码放在一个页面上并放入一个大型函数中,但不建议这样做,这也不是一种有效的方法来构造代码,并且违背了Angular存在的目的。 Angular将模块的概念作为框架架构的重要组成部分,这是指只有一个存在理由的代码集合。你的Angular app基本上由模块组成 - 有些是独立的,有些是共享的。 有多种方法可以在您的应用程序中构造模块,深入了解不同的体系结构也可以帮助确定如何在应用程序增长时扩展应用程序,它还可以帮助隔离代码并防止产生代码耦合。 搜索关键词: Angular架构模式可扩展的Angular应用架构2.单向数据流和不可变性早在Angular 1中,双向绑定就俘获了许多前端开发人员的心。这实际上是Angular最初的卖点之一。然而,随着时间的推移,当应用程序开始变得更加复杂时,它开始在性能方面产生问题。 事实证明,并不是在任何地方都需要双向绑定的。 双向绑定在Angular 2+中仍然是可实现的,但只有在开发人员明确请求时才能进行双向绑定 – 这样就迫使代码背后的人员考虑数据方向和数据流,它还允许应用程序通过确定数据的流动方式来更加灵活地处理数据。 搜索关键词: Angular数据流最佳实践Angular中的单向流单向绑定的优点3.属性型和结构型指令指令是HTML通过自定义元素的扩展。属性型指令允许您更改元素的属性,结构型指令通过在DOM中添加或删除元素来更改布局。 例如,ngSwitch和ngIf是结构型指令,因为它评估参数并确定DOM的某些部分是否应该存在。 属性型指令是附加到元素、组件或其他指令的自定义行为。 学习如何使用这两个指令可以扩展应用程序的功能,并减少项目中重复代码的数量。属性型指令还可以帮助集中在应用程序不同部分使用的某些行为。 搜索关键词: Angular属性型指令Angular结构型指令Angular结构型指令模式4.组件生命周期钩子每个软件都有自己的生命周期,决定了如何创建、渲染和删除某些内容。Angular的组件生命周期是这样的:create → render → render children → check when data-bound properties change → destroy → remove from DOM 我们能够在这个周期内抓住关键时刻,并在特定时刻或事件中锁定他。这允许我们根据组件存在的不同阶段创建适当的响应并配置行为。 例如,在呈现页面之前可能需要加载一些数据,你可以通过ngOnInit()来实现这一点,或者你可能需要断开与数据库的连接,这可以通过ngOnDestroy()来实现。 搜索关键词: Angular生命周期钩子组件生命周期5.Http和可观察对象服务这并不是Angular特有的功能,而是来自ES7。Angular只是碰巧将其作为框架支持功能的一部分来实现,并且恰好理解了这一点,它也可以很好地转换为React、Vue和任何JavaScript相关的库或框架。 可观察对象服务是允许你有效处理数据的模式 - 允许你在基于事件的系统中解析、修改和维护数据。你无法完全逃避Http和可观察对象,因为一切都是数据。 搜索关键词: JavaScript可观察对象模式Angular HTTP和可观察对象ES7可观察功能6.Smart/Dumb组件架构在编写Angular应用程序时,我们倾向于将所有内容都放入组件中。但是,这并不是最佳做法。Angular中Smart/Dumb组件的概念需要更多的讨论,尤其是在初学者圈子里。 组件是否Smart/Dumb决定了它在应用程序的总体规划中扮演的角色。Dumb组件通常是无状态的,其行为易于预测和理解。因此,尽可能使你的组件变得Dumb。Smart组件更难掌握,因为它会涉及到输入和输出。要正确利用Angular的功能,请研究Smart/Dumb组件架构,它将为你提供有关如何处理代码及其相互关系的模式和思维方式。 搜索关键词: Smart/Dumb Angular 组件无状态的Dumb组件演示组件Angular中的Smart组件7.应用程序结构和最佳实践在结构和最佳实践方面,CLI只能带您到目前为止。构建Angular应用程序(或任何一般应用程序)就像构建一个房子。社区多年来一直在优化设置流程,从而实现最有效和最有效的应用。 Angular也不例外。 那些试图学习Angular的人对Angular的大多数抱怨往往是由于缺乏结构知识;语法是很容易上手的,而且清晰明确。然而,应用程序的结构知识需要理解上下文背景、需求以及它们如何在概念和实践层面组合到一起。了解Angular不同的潜在应用程序结构及其最佳实践,将会让你对如何构建应用程序拥有一个全新的视角。 搜索关键词: ...

July 2, 2019 · 1 min · jiezi

DevOps-基于Walle的小型持续集成实战六基于Walle发布前端ReactAngular应用

本章用于讲解如何在walle下构建和运行前端应用。主要包含React,Angular应用,以Nginx+Docker运行(Vue方式不讲,大家自行研究)新建项目项目中心 > 项目管理 > 新建项目以下是一份配置好的项目表 分组项目参考备注基本配置项目名称dev-我的JavaDemo项目随便填写,名称不要太长(不好看),最好把环境卸载最前,例如dev(开发环境)基本配置环境开发环境提前在环境管理配置好即可基本配置Git Repogit@gitlab.xxx.com:xxx/react-demo.gitGit仓库地址目标集群目标集群192.168.0.122提前配置服务器管理目标集群目标集群部署路径/data/walle-build/react-demo实际运行的环境目标集群目标集群部署仓库/data/walle-run会存放多个版本编译后的项目目标集群目标集群部署仓库版本保留数5可以回滚的版本数配置脚本前端生态下基本脚本区别较大,但拥有共通性,此处以Angular为例 基本脚本任务配置 - 部署包含文件包含方式 docker-compose.ymlnginx.confdist该方式用于描述从源码包到发布包中,排除/包含的内容。一般java使用target即可 任务配置 - 自定义全局变量# 运行目录# 运行目录NG_PATH=/data/walle-tool/node-v10.5.0-linux-x64/lib/node_modules/@angular/cli/binPORT=2222ENV=testSERVER_NAME=idp-server-ui-test【Angular to Docker】任务配置 - 高级任务-Deploy前置任务pwdecho "开始初始化"npm install \@angular/cli\@6.0.8 -g || echo "安装失败"任务配置 - 高级任务-Deploy后置任务pwdunzip -q node_modules.zipecho "${NG_PATH}/ng -v"${NG_PATH}/ng -v || echo "环境检查失败"${NG_PATH}/ng build --prod --configuration=${ENV}sed -i 's/${container_port}/'${PORT}'/g' docker-compose.yml sed -i 's/${container_name}/'${SERVER_NAME}'/g' docker-compose.yml 任务配置 - 高级任务-Release前置任务docker-compose -p ${SERVER_NAME} -f ${WEBROOT}/docker-compose.yml down || echo "服务不存在"docker stop ${SERVER_NAME} || echo "服务不存在"docker rm ${SERVER_NAME} || echo "服务不存在"rm -rf ${WEBROOT}任务配置 - 高级任务-Release后置任务docker-compose -p ${SERVER_NAME} up -decho "服务启动完成"项目 - nginx.conf 配置# gzip设置gzip on;gzip_vary on;gzip_comp_level 6;gzip_buffers 16 8k;gzip_min_length 1000;gzip_proxied any;gzip_disable "msie6";#gzip_http_version 1.0;gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript application/javascript;server { listen 80; server_name localhost; #charset koi8-r; #access_log /var/log/nginx/host.access.log main; location / { root /usr/share/nginx/html; index index.html index.htm; # 其作用是按顺序检查文件是否存在,返回第一个找到的文件或文件夹(结尾加斜线表示为文件夹),如果所有的文件或文件夹都找不到,会进行一个内部重定向到最后一个参数。 try_files $uri /index.html; } #error_page 404 /404.html; # redirect server error pages to the static page /50x.html # error_page 500 502 503 504 /50x.html; location = /50x.html { root /usr/share/nginx/html; } # proxy the PHP scripts to Apache listening on 127.0.0.1:80 # #location ~ \.php$ { # proxy_pass http://127.0.0.1; #} # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 # #location ~ \.php$ { # root html; # fastcgi_pass 127.0.0.1:9000; # fastcgi_index index.php; # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; # include fastcgi_params; #} # deny access to .htaccess files, if Apache's document root # concurs with nginx's one # #location ~ /\.ht { # deny all; #}}项目 - docker-compose.yml配置version: '2'services: # 服务名称 nginx: # 镜像:版本 image: nginx:latest container_name: ${container_name} # 映射容器80端口到本地80端口 ports: - "${container_port}:80" # 数据卷 映射本地文件到容器 volumes: # 映射nginx.conf文件到容器的/etc/nginx/conf.d目录并覆盖default.conf文件 - ./nginx.conf:/etc/nginx/conf.d/default.conf # 映射build文件夹到容器的/usr/share/nginx/html文件夹 - ./dist/idp-server-ui:/usr/share/nginx/html # 覆盖容器启动后默认执行的命令。 command: /bin/bash -c "nginx -g 'daemon off;'"环境准备参考文档: https://segmentfault.com/a/11... ...

June 17, 2019 · 2 min · jiezi

强烈推荐-GitHub-上值得前端学习的开源实战项目

强烈推荐 GitHub 上值得前端学习的开源实战项目。 Vue.jsvue-element-admin 是一个后台前端解决方案,它基于和 element-ui 实现基于 iView 的 Vue 2.0 管理系统模板基于 vue2 + vuex 构建一个具有 45 个页面的大型单页面应用基于 vue + element-ui 的后台管理系统基于Vue.js + Element UI 的后台管理系统解决方案基于 Vue(2.5) + vuex + vue-router + vue-axios +better-scroll + Scss + ES6 等开发一款移动端音乐 WebAppSpring Boot 后端 + Vue 管理员前端 + 微信小程序用户前端 + Vue 用户移动端高仿网易云音乐的 webapp,只实现了 APP 的核心功能Vue + TypeScript + Element-Ui 支持 markdown 渲染的博客前台展示更多...React.js一套优秀的中后台前端解决方案网易云音乐第三方一个 react + redux 的完整项目 和 个人总结 react 后台管理系统解决方案这是一个用来查看GitHub最受欢迎与最热项目的App,它基于React Native支持Android和iOS双平台RN写的饿了么,还原度相当高,实现了各类动效仿知乎日报一个商城类的RN项目react + Ant Design + 支持 markdown 的博客前台展示基于 pro.ant.design 的 react + Ant Design 的博客管理后台项目使用 react hooks + koa2 + sequelize + mysql 搭建的前后台的博客 基于typescript koa2 react的个人博客更多...Angular基于angular.js,weui和node.js重写的新闻客户端 管理仪表板模板基于Angular 7+,Bootstrap 4Node.js基于 node.js + Mongodb 构建的后台系统Nodeclub 是使用 Node.js 和 MongoDB 开发的社区系统基于Node.js+MySQL开发的开源微信小程序商城(微信小程序)NideShop 开源微信小程序商城服务端 API(Node.js + ThinkJS)基于react, node.js, go开发的微商城(含微信小程序) React+Express+Mongo ->前后端博客网站 基于 node + express + mongodb 的博客网站后台最后笔者博客首更地址 :https://github.com/biaochenxuying/blog ...

June 15, 2019 · 1 min · jiezi

Angular-CDK-Overlay-弹出覆盖物

为什么使用Overlay?Overlay中文翻译过来意思是覆盖物,它是Material Design components for Angular中针对弹出动态内容这一场景的封装,功能强大、使用方便,尤其在开发自己的组件库时,可以让你少写许多代码,可以说只要是弹出内容的场景基本都可以使用Overlay.我们自己的组件库中弹出场景基本都已经使用Overlay,如自定义Select、Cascader、Tree Select、Tooltip、Dialog等,总结最重要的的两点好处: 让使用者不再进行繁琐的位置计算,而简单通过参数配置就实现内容的定位,而且关于位置的各种情况都有考虑到.组件的弹出内容都是用Overlay实现,避免了各自实现的产生的不兼容,如相互遮盖问题.简单示例 - 连结位置源的弹出下面通过一个示例代码来展示Overlay的使用,这种弹出场景类似于Tooltip,弹出的overlay内容是基于一个参照的位置源origin元素. 安装并且导入模块项目中如果没有安装CDK,要先安装 npm install @angular/cdk导入OverlayModuleimport {OverlayModule} from '@angular/cdk/overlay';@NgModule({ imports: [ OverlayModule, // ... ]})export class AppModule {}示例模板内容<div class="demo-trigger"> <!--触发位置源--> <button mat-raised-button cdkOverlayOrigin type="button" [disabled]="overlayRef" (click)="openWithConfig()">Open</button></div><!--弹出动态内容模板--><ng-template #overlay> <div class="demo-overlay"> <div style="overflow: auto;"> <ul><li *ngFor="let item of itemArray; index as i">{{itemText}} {{i}}</li></ul> </div> </div></ng-template>除了弹出模板,上面模板中还有一个Open按钮,后面要用到它作为位置源origin 注入Overlay服务在组件的constructor构造函数中注入Overlay服务,下面代码包括组件的定义 @Component({ selector: 'overlay-demo', templateUrl: 'connected-overlay-demo.html'})export class ConnectedOverlayDemo { @ViewChild(CdkOverlayOrigin, {static: false}) _overlayOrigin: CdkOverlayOrigin; @ViewChild('overlay', {static: false}) overlayTemplate: TemplateRef<any>; /** * 注入Overlay服务 */ constructor( public overlay: Overlay) { } openWithConfig() { }}处理注入服务,上面代码还通过ViewChild取到模板中的两个对象,后面用到的时候再解释. ...

June 13, 2019 · 2 min · jiezi

ngalain动态表单SF表单项设置必填和正则校验

在使用动态表单时对表单项进行非空校验及正则校验。使用手机号进行校验,示例如下:动态表单的基本使用:https://ng-alain.com/form/get... 基于基本示例,增加手机号必填与正则校验的例子: @Component({ selector: 'app-home', template: ` <sf [schema]="schema" [ui]="ui" (formSubmit)="submit($event)"></sf> `})export class HomeComponent { schema: SFSchema = { properties: { email: { type: 'string', title: '邮箱', format: 'email', maxLength: 20 }, name: { type: 'string', title: '姓名', minLength: 3 }, mobileNumber: { type: 'string', title: '手机号', pattern: '^1[0-9]{10}$' }, }, }; ui: SFUISchema = { '*': { spanLabelFixed: 100, grid: { span: 24 }, }, $mobileNumber: { widget: 'string', errors: { 'pattern': '请输入11位手机号码' } } }; submit(value: any) { }}

June 11, 2019 · 1 min · jiezi

从观察者模式到迭代器模式系统讲解-RxJS-Observable一

RxJS 是 Reactive Extensions for JavaScript 的缩写,起源于 Reactive Extensions,是一个基于可观测数据流 Stream 结合观察者模式和迭代器模式的一种异步编程的应用库。RxJS 是 Reactive Extensions 在 JavaScript 上的实现。 Reactive Extensions(Rx)是对 LINQ 的一种扩展,他的目标是对异步的集合进行操作,也就是说,集合中的元素是异步填充的,比如说从 Web或者云端获取数据然后对集合进行填充。LINQ(Language Integrated Query)语言集成查询是一组用于 C# 和Visual Basic 语言的扩展。它允许编写 C# 或者 Visual Basic 代码以操作内存数据的方式,查询数据库。RxJS 的主要功能是利用响应式编程的模式来实现 JavaScript 的异步式编程(现前端主流框架 Vue React Angular 都是响应式的开发框架)。 RxJS 是基于观察者模式和迭代器模式以函数式编程思维来实现的。学习 RxJS 之前我们需要先了解观察者模式和迭代器模式,还要对 Stream 流的概念有所认识。下面我们将对其逐一进行介绍,准备好了吗?让我们现在就开始吧。 RxJS 前置知识点观察者模式观察者模式又叫发布订阅模式(Publish/Subscribe),它是一种一对多的关系,让多个观察者(Obesver)同时监听一个主题(Subject),这个主题也就是被观察者(Observable),被观察者的状态发生变化时就会通知所有的观察者,使得它们能够接收到更新的内容。 观察者模式主题和观察者是分离的,不是主动触发而是被动监听。 举个常见的例子,例如微信公众号关注者和微信公众号之间的信息订阅。当微信用户关注微信公众号 webinfoq就是一个订阅过程,webinfoq负责发布内容和信息,webinfoq有内容推送时,webinfoq的关注者就能收到最新发布的内容。这里,关注公众号的朋友就是观察者的角色,公众号webinfoq就是被观察者的角色。 示例代码: // 定义一个主题类(被观察者/发布者)class Subject { constructor() { this.observers = []; // 记录订阅者(观察者)的集合 this.state = 0; // 发布的初始状态 } getState() { return this.state; } setState(state) { this.state = state; // 推送新信息 this.notify(); // 通知订阅者有更新了 } attach(observer) { this.observers.push(observer); // 对观察者进行登记 } notify() { // 遍历观察者集合,一一进行通知 this.observers.forEach(observer = { observer.update(); }) }}// 定义一个观察者(订阅)类class Observer { constructor(name, subject) { this.name = name; // name 表示观察者的标识 this.subject = subject; // 观察者订阅主题 this.subject.attach(this); // 向登记处传入观察者实体 } update() { console.log(`${this.name} update, state: ${this.subject.getState()}`); }}// 创建一个主题let subject = new Subject();// 创建三个观察者: observer$1 observer$2 observer$3let observer$1 = new Observer("observer$1", subject);let observer$2 = new Observer("observer$2", subject);let observer$3 = new Observer("observer$3", subject);// 主题有更新subject.setState(1);subject.setState(2);subject.setState(3);// 输出结果// observer$1 update, state: 1// observer$1 update, state: 1// observer$1 update, state: 1// observer$2 update, state: 2// observer$2 update, state: 2// observer$2 update, state: 2// observer$3 update, state: 3// observer$3 update, state: 3// observer$3 update, state: 3迭代器模式迭代器(Iterator)模式又叫游标(Sursor)模式,迭代器具有 next 方法,可以顺序访问一个聚合对象中的各个元素,而不需要暴露该对象的内部表现。 ...

June 10, 2019 · 5 min · jiezi

angular使用md5CryptoJS-des加密

在业务系统中,通常需要对用户的密码进行加密,再时行http的请求。加强系统登录的安全验证。 常用的加密方式有MD5, Base64, CryptoJS的 AES DES等。下面介绍我常用的几种加密方法的使用: MD5加密1. 安装模块 ts-md5$ npm install ts-md5 --save2. 使用md5进行加密import { Md5 } from 'ts-md5';// ...// 密码password: string = "12345";// 加密方法 - md5加密decode() { const passwordMd5 = Md5.hashStr(this.password).toString(); // 结果:827ccb0eea8a706c4c34a16891f84e7b}Base64加密1.安装模块 js-base64$ npm install js-base64 --save2.使用md5进行加密import { Base64 } from 'js-base64';// ...// 密码password: string = "12345";// 加密方法 - Base64加密decode() { const passwordBase64 = Base64.encode(password); // 结果:MTIzNDU=}DES加密DES对称加密,是一种比较传统的加密方式,其加密运算、解密运算使用的是同样的密钥key,信息的发送者和信息的接收者在进行信息的传输与处理时,必须共同持有该密码(称为对称密码),是一种对称加密算法。crypto-js Github: https://github.com/brix/crypt... 1.安装模块 crypto-js$ npm install crypto-js --save2.使用DES进行加密import CryptoJS from 'crypto-js';// ...// 密钥key: string = "abcdefg";// 密码password: string = "12345";// 加密方法 - des加密decode() { // key编码 const keyHex = CryptoJS.enc.Utf8.parse(this.key); console.log(keyHex.toString()); // 结果:61626364656667 // 加密 const passwordDES = CryptoJS.DES.encrypt(this.password, keyHex, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 }).toString(); console.log(passwordDES); // 结果:zYGeIdaZpEM=}3. 使用AES进行加密加密用法基本与des一致。 ...

June 3, 2019 · 1 min · jiezi

angular依赖注入

依赖注入是angular中一个十分常用的功能,但由于自己以前写的代码并没有用到依赖注入的高级功能,所以也就没有系统的学习过,正好这段时间看angular的官方文档看到了这里,就简单的总结一下。 什么是依赖注入关于什么是依赖注入,实际上还是很好理解的,依赖注入通常和控制反转联系在一起 依赖注入(DI)和控制反转(IOC)基本是一个意思,因为说起来谁都离不开谁。简单来说,类A依赖类B,但A不控制B的创建和销毁,仅使用B,那么B的控制权则交给A之外处理,这叫控制反转(IOC)。由于A依赖于B,因此在A中必然要使用B的instance,我们可以通过A的构造函数将B的实例注入,比如:下面是一个简单的,用ts实现的依赖注入的例子: class B { }class A { constructor(b: B) { console.log(b); }}const b = new B();// 将B的实例注入到a中const a = new A(b);angular的依赖注入angular 有自己的 DI 框架,所以在实现依赖注入的过程被隐藏了,我们只需要很简单的步骤就可以使用其强大的依赖注入功能。 angular的依赖注入可以分为三个步骤: 得到依赖项查找依赖项所对应的对象执行时注入下面是一个例子 使用依赖注入组件 @Component({ selector: 'app-test', template: ``})export class TestComponent { constructor(userService: UserService) { }}创建将被注入的UserService // @Injectable() 装饰器把它标记为可供注入的服务@Injectable({// 指定把被装饰类的提供商放到 root 注入器中,也就是一个全局的对象 providedIn: 'root',})export class UserService { constructor() { }}这样就算把一个UserService注入到了TestComponent中,并且因为提供商被放到了 root 注入器中,因此你可以在整个应用中使用该对象——在某个注入器的范围内,服务是单例的。也就是说,在指定的注入器中最多只有某个服务的最多一个实例 三种提供商angular一共有三种提供商,@Injectable,@NgModule,@Component @Injectable@Injectable的提供方式就是上面所提到的那样。 @Injectable() 装饰器会标出每个服务类。服务类的元数据选项 providedIn 会指定一个注入器(通常为 root 来用被装饰的类作为该服务的提供商。 当可注入的类向 root 注入器提供了自己的服务时,任何导入了该类的地方都能使用这个服务。同理,你可以向某个特定 NgModule 的注入器提供自己的服务。这种注入方式也是使用angular-cli生成服务时默认的方式,也是最常用的方式,它同另外两种注入方式相比,有一个显著的优点:如果 NgModule 没有用到该服务,那么这个服务就会被摇树优化掉。 ...

May 31, 2019 · 1 min · jiezi

angular路由学习

最近比较忙,抽空看了angular官网上的路由与导航,其实还是有很多想了解的地方,可惜目前没有时间一一验证学习,只能以后有空再说了,在这里只是记录一下学习是遇到的混淆的点。 路由配置(Route) 路由器(Router) 激活路由(ActivatedRoute) 路由状态(RouterState)先给出一份路由模块配置: const routers: Routes = [ { path: '', children: [ { path: '', component: MainComponent, children: [ {path: 'son', component: SonComponent} ] }, {path: 'modal', component: ModalComponent} ] }];@NgModule({ imports: [ RouterModule.forRoot(routes) ], exports: [RouterModule]})export class AppRoutingModule {}在这里routers变量的类型是Routers,其实就是一个Route数组: 所以Route的意思就显而易见了,就是一个简单的路由配置 Router路由器(我看的时候老是和Route看混),提供导航和操纵 URL 的能力。 ActivatedRoute包含与当前组件相关的路由信息,可以从上面获取参数,url,父子路由信息等。 路由状态RouterState,我们都知道路由为一个树,由我们配置的路由信息构建而成,而RouteState就代表当前路由的这棵树,可以通过RouteState获取当前的路由树。参考链接这是我按照上边的 路由配置打印的结果,可以看到RouteState最主要的为一个Root属性,类型是ActivatedRoute,一般为AppComponent,而在他的Children属性下一步步可以看到我们的MainCoponemt和SonComponent,我浏览器路由为http://localhost:4200/son;这说明当前MainComponet和SonComponent,AppComponent都是被路由激活的组件,他们都记录在RouteState这个当前的路由状态上。 路由快照(Snapshot)在ActivatedRoute,RouteState等路由信息中,都有一个带有XXXSnapshot字段的属性,这个为快照,也就是当前瞬间的路由信息。 默认情况下,如果没有访问过其它组件就导航到了同一个组件实例,那么路由器倾向于复用组件实例。如果复用的话,路由的参数可以变化,这也是ActivatedRoute里的参数需要订阅的原因,而快照则是保存每一次路由的信息。例如我们修改上边son路由,接受一个id参数,一个方法跳转,一个方法显示快照参数: <p> id:{{id}}</p><button (click)="show()">显示id</button><button (click)="go()">跳转</button>id: number; constructor(private active: ActivatedRoute, private router: Router) { } ngOnInit() { this.id = +this.active.snapshot.paramMap.get('id'); console.log('创建'); } show() { this.id = +this.active.snapshot.paramMap.get('id'); } go() { const id = this.id + 1; this.router.navigateByUrl('/son/' + 10); }输入网址http://localhost:4200/son/9,可以看到id显示9,并且控制台打印了‘创建’: ...

May 31, 2019 · 1 min · jiezi

使用路由复用来实现返回上一页面内容不刷新

问题:最近遇到的问题是我们在一个查询列表页面上对该页面的的某条内容进行编辑或者新增之后再跳回的话,我们的原页面便会重新加载,这个不是很人性化,为此我们想要实现的效果是,在编辑和修改完成之后,返回原界面时原内容保留。实现效果如下所示。 编辑前 编辑后 在这里我们使用了一种叫做路由复用的机制来实现。 解决方式:大概的原理是,一般情况下,在路由离开时就会销毁该路由的组件,再次跳转时会重新初始化,调用nginit()方法,但是我们现在是需求是保留路由的原状态,为此我们需要重写RouteReuseStrategy接口,该接口实现路由复用策略。 shouldDetach 是否允许复用路由store 当路由离开时会触发,存储路由shouldAttach 是否允许还原路由retrieve 获取存储路由shouldReuseRoute 进入路由触发,是否同一路由时复用路由 在该项目中我们的具体实现代码如下 export class SimpleReuseStrategy implements RouteReuseStrategy { _cacheRouters: { [key: string]: any } = {}; shouldDetach(route: ActivatedRouteSnapshot): boolean { // 默认对所有路由复用 可通过给路由配置项增加data: { keep: true }来进行选择性使用 // {path: 'search', component: SearchComponent, data: {keep: true}}, if (route.data.keep) { return true; } else { return false; } } store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { // 在懒加载的时候使用,data.key存储路由快照&组件当前实例对象 // path等同RouterModule.forRoot中的配置 this._cacheRouters[route.data.key] = { snapshot: route, handle: handle }; } shouldAttach(route: ActivatedRouteSnapshot): boolean { // 在缓存中有的都认为允许还原路由 return !!route.routeConfig && !!this._cacheRouters[route.data.key]; } retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { // 从缓存中获取快照,若无则返回null if (!route.routeConfig || route.routeConfig.loadChildren || !this._cacheRouters[route.data.key]) { return null; } return this._cacheRouters[route.data.key].handle; } shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean { // 同一路由时复用路由 return future.routeConfig === curr.routeConfig; }}对于要服用的路由我们如下使用 ...

May 30, 2019 · 1 min · jiezi

Angular代码风格

写在前面自身的良好编码风格只能律己,而无法律人;我喜欢 Angular 其中主要一个因素是有一整套的工具及风格指南,它可以极大的简化团队开发沟通成本,但是有些小缺失例如在编码风格上官方只提供 TypeScript 的部分,对于其他文件并没有一套指南以及智能化。 VSCode 是我开发 Angular 应用的首选,本文也将以此 IDE 为基准;任何提到的扩展都可以通过市场来获取。 Angular 应用是由组件树组成,一个组件从文件来看包含:TypeScript、HTML、Less(或其他 CSS 预处理器),其中 HTML 可能被包含至 ts 文件里。 当然除此之外还包含一些 JSON 文件、Bash 文件等,当此部分不在本文讨论内。TSLintAngular 创建后就已经包含 tslint.json(它是 TSLint 的配置文件),并且所有默认规则都按官方风格指南具体践行。 而 TSLint 的配置文件,默认使用内置预设 tslint:recommended 版本,并在此基础上加入 Angular 质量检查工具 codelyzer,所有这些规则你可以通过 tslint rules、codelyzer 找到每项规则的说明。 规则的写法要么是 boolean 类型,或者使用数组对该规则指定额外参数。运行 ng lint 命令时,当你某个字符串变量使用双引号,它会提示: ERROR: /src/app/app.component.ts[9, 16]: " should be '我们也可以安装 TSLint 扩展让这个触发机制放在正在编码过程中实时反馈: 当有不符合风格指南会出现一个绿色的波浪线,按 command+. > Fix: " Should be '通过终端 PROBLEMS 面板查看所有已打开文件且不符合风格指南的明细嗯,让你按五次 command+. 快捷键,我一定会疯掉;TSLint 扩展支持在保存文件时自动修复,只需要在项目根目录 .vscode/settings.json 配置: ...

May 29, 2019 · 3 min · jiezi

对子路由的进一步理解

在以前的时候一直以为angular的子路由就是下一次跳转的位置,虽然如果你不用 router-outlet导出的话他会显示不出来,但也仅仅这样了,也没有细细研究过,直到遇到了这周的问题,果然错误让人进步。 No provider for照着老师以前写的弹窗抄了一下,可却报了错 该引入的也已经引入了 当时找了一会,也没找到问题所在,就报着死马当活马医的心态,不是说缺少provider吗,我就给他个provider 这样一弄,显示倒是能显示出来了。 功能却没法使用 不过这一搞,倒是让我感觉基础相当不扎实,顺便学习了一下provides的具体用途 服务模块提供了一些工具服务,比如数据访问和消息。理论上,它们应该是完全由服务提供商组成的,不应该有可声明对象。Angular 的 HttpClientModule 就是一个服务模块的好例子。根模块 AppModule 是唯一的可以导入服务模块的模块。 说的很清楚,不应该有可声明对象,我上面的用法显然是不对的。更详细的内容请看官方文档 问题所在当时由于马上就要期中考试了,简单的问了问张喜硕组长后就去了,考试的就一直在想这个问题,明明用法和潘老师的一模一样,我的却有这种问题,唯一的不同就是没用router-outlet,而router-outlet是给子路由使用的,我感觉自己可能发现了真相,但却不知道为啥,考完之后回来一试。果然就成了。 配置子路由 const routes: Routes = [ { path: '', component: GetLoginPrivilegesComponent, children: [ { path: 'register', loadChildren: '../get-login-privileges/register-index/register-index.module#RegisterIndexModule', }, { path: 'forgetPassword', loadChildren: '../get-login-privileges/forget-password/forget-password.module#ForgetPasswordModule', }, ] },];@NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule],})export class GetLoginPrivilegesRoutingModule {}界面功能全有 经过尝试,是因为子路由的原因。 对原因的猜测子路由与父路由是否会让组件之间有子路由对应的组件已经成为了父组件的一部分,所以可以自由调用父组件的方法如果有大神解答相关疑惑,感激不尽。 一点感想不同的阶段对同样的问题,还是会有不一样的理解的,就像潘老师常说的有的时候找到一个让自己信服的理由就够了,虽然在编程的道路上,自己还是很菜,很多问题还是知其然,但却不知其所以然,但也确实是渐渐的成长了。慢慢来吧。

May 24, 2019 · 1 min · jiezi

cwikiusangularapp-导入后如何添加到自己的项目

将 https://stackblitz.com/github... 导入到界面后,如何将这个项目添加到自己的项目里面。 然后再自己的项目里面进行编辑,修改后提交? 你可以在编辑界面中 Fork 到本地后进行修改。

May 18, 2019 · 1 min · jiezi

Angular-架构设计

引言Alice测试上线,发现包体积太大,加载太慢。决定启用懒加载与预加载加速加载速度。 整三天,课也没去上。改得时候特别痛苦,哭了,为什么没有早点发现惰性加载这个东西。 星期一,重新设计前台架构,重构前台代码。 星期二,分模块加载,启用惰性加载与预加载。 星期三,修改单元测试,添加provide。 星期四,写PPT。 星期五,.NET考试。 重构前台之后,觉得自己当前设计的架构很合理,遂分享出来,供大家学习交流。 架构理论架构理论主要参考外国老哥的一篇文章,Angular (2+): Core vs Shared Modules CoreModule:核心模块,只被AppModule引用,保证全局单例。 ShareModule:共享模块,被各业务模块引用,存储各模块必备的组件、管道以及模板。 实践CoreModule核心Module,全局只导入一次。 称之为核心,因为没有它应用跑不起来。 核心模块存放拦截器和服务,不过与正常的有些区别。 拦截器 @Injectable()export class YunzhiInterceptor implements HttpInterceptor {}@NgModule({ imports: [ NgZorroAntdModule, RouterModule ], providers: [ {provide: HTTP_INTERCEPTORS, useClass: YunzhiInterceptor, multi: true} ]})export class CoreModule {}服务 @Injectable({ providedIn: CoreModule})export class CollegeService {}现在不往root里注入了,因为发现有的时候写root有人会搞不清楚模块的层级关系,然后就懵圈了。 为了规避这种问题,直接注入到核心模块中,防止有人误解。 norm其实是想起一个规范的英文的,但是spec却被测试给用了,所以就去百度翻译了个放这了。 这个包主要是存储数据规范的。 entity存储实体,对应后台实体。 target存储自定义的规范对象,历史的教训告诉我们,如果把所有都放到实体包里,这很糟糕。 page这个是向小程序抄来的,小组件可以复用,大组件就需要单建目录了,都放一起看着混乱。 分模块加载,每个功能一个单独的模块,模块职责划分清晰。 @NgModule({ declarations: [ SetupComponent ], imports: [ SetupRouteModule, ShareModule ]})export class SetupModule {}模块中就这几行,什么废话都不要写,就声明本模块的组件,并导入本模块的路由和Share模块。其他的都不要写,第三方的导入交给ShareModule去处理。本模块只负责业务,不负责代码。 ...

May 18, 2019 · 1 min · jiezi

angular多语言配置

angular的国际化方案,采用ngx-translate来实现。 安装模块: npm install @ngx-translate/core --save在根模块中导入: // other moduleimport {TranslateModule} from '@ngx-translate/core';@NgModule({ declarations: [ AppComponent, ], imports: [ // other module TranslateModule.forRoot(), ], providers: [ ], bootstrap: [AppComponent]})export class AppModule {}我们希望可以在一个固定的文件里面配置对应的翻译文件,然后在每个用到的组件里面使用它,随意我们需要借助TranslateHttpLoader来加载翻译文件。首先安装TranslateHttpLoader: npm install @ngx-translate/http-loader --save翻译文件可以放在/assets/i18n/[lang].json中,[lang]代表使用的语言文件名称。然后我们可以在跟组件中添加配置对应的加载项: // other moduleimport {TranslateModule} from '@ngx-translate/core';// 自定义加载方法export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './assets/i18n/', '.json?');}@NgModule({ declarations: [ AppComponent, ], imports: [ // other module TranslateModule.forRoot({ loader: { provide: TranslateLoader, useFactory: HttpLoaderFactory, deps: [HttpClient], } }), ], providers: [ ], bootstrap: [AppComponent]})export class AppModule {}然后我们在翻译文件中配置一个简单的示例: ...

May 16, 2019 · 1 min · jiezi

ngalain重置表单cascader不生效

ng-alain 使用SF formReset重置搜索表单时,级联组件cascader不生效,文字没被清空?在使用ng-alain生成的列表模板中,上面是由动态表单组件SF构成。下面是由ST表格组件构成。下面说说ST表格的上方的搜索表单组件SF。 在使用过程中,SF动态表单可以随意配置已有的小部件,大多小部件都很实用并且常用,但有个别小部分存在缺陷问题,如cascader级联组件。你可能会遇到以下的小问题: 当表单都存在数据时,进行重置操作,使用SF的(formReset)方法,但出来的效果除了级联组件cascader内容没清空外,其他小部件都清空了原有的值。此时再调用SF的(formSubmit)方法,发现原来cascader的值是空的,而组件所显示文字却没有被清空。 此时解决办法如下:在(formReset)方法中刷新SF的schema来实现清空。 html文件 <page-header [action]="phActionTpl" [breadcrumb]="breadcrumb"></page-header><nz-card> <sf #sf mode="search" [schema]="searchSchema" [ui]="uiSchema" [button]="sfButton" (formSubmit)="search($event)" (formReset)="reset($event)"></sf> <st #st [data]="url" [columns]="columns"></st></nz-card>ts文件 /** * 搜索 * @param event 搜索表单数据对象 */search(event) { console.log(event) this.st.load(undefined, event)}/*** 搜索重置 * @param event 搜索表单数据对象 */reset(event) { this.st.reset(event); // 针对级联组件cascader不能清空的问题,重新refresh一次表单 this.sf.refreshSchema();}

May 16, 2019 · 1 min · jiezi

antdesign-奇技淫巧

如果你在 vscode 中编写Angular,那么安装 ng-zorro-vscode 代码片断,对开发效率很有帮助。

May 12, 2019 · 1 min · jiezi

angular-的坑

在angular.cli中使用cnpm包管理用具 ng config --global cli.packageManager cnpm安装全局,这样允许直接在CMD命令行中使用 ng 命令。cnpm install -g @angular/cli@latest创建项目ng new democd demo安装包npm install --save ng-zorro-antd

May 12, 2019 · 1 min · jiezi

Alice-上线小记

引言Alice学生管理系统昨日正式上线测试,上线遇到的问题不少,但最后都完美解决了。 特此分享,一起爬坑。 项目优化登录页美化原来的登录页采用的是黑背景,经过大家的充分讨论,我们需要换一个登录页,黑背景看着压抑。 然后就在晨澍和潘佳琦的帮助下开始找各种登录的模板,发现都特别丑,后来发现当前系统的登录风格和微信的登录风格很像,顺手就抄过来了,感觉效果还不错。 上线问题打包问题前台ng build --prod之后,发现样式不一致。 期待: 实际结果: 查看元素发现是Bootstrap的样式在打包之后没有了。 后来发现:黄庭祥在style.less中引用了一个在线的Bootstrap,打包肯定打不进去啊。 引用的所有包,都不能引在线的,需要使用npm安装。npm install之后的包才能被打包进去。 远程文件拷贝本地打完包,需要将文件上传到服务器,查了一下,需要使用scp命令。 scp:secure copy,安全拷贝,将文件加密传输,安全的远程文件拷贝命令。 scp -r /Users/panjie/github/yunzhiclub/alice/web/webApp/dist/webApp root@xxx.xxx.xxx.xxx:/root/将本地打包的webApp目录上传到服务器的/root目录下。 -r代表目录,xxx.xxx.xxx.xxx请替换成相应的服务器IP地址。 nginx 403前台上线,浏览器端访问服务器却得到了403,查看相关日志后发现是nginx访问文件时遭到了拒绝。 在nginx的配置文件中,有一行配置用户的。 原配置是user nginx;,所以启动时nginx进程的用户是nginx,但是webApp文件的用户所有者是root,所以就403了。 解决方案是把用户配置改成root(有权限的用户),然后重新加载配置文件即可。 刷新404问题当用户访问127.0.0.1:8100时,根据路由的重定向,''重定向为'setup'登录界面。 但是用户刷新或直接访问127.0.0.1:8100/setup时,报404错误。 猜想,nginx转发出现了问题,应该是/转给了Angular,但是把/setup当成了文件夹。 这个只是我的猜测,如果您有什么意见,欢迎在评论区中指出我的错误,感激不尽。 华软就没有该类问题,对比两个项目,发现华软中默认配置了hash路由。 hash路由特意去官网学习了一下hash路由,感觉应该能给大家讲明白。 两个路由: 127.0.0.1:8100/setup127.0.0.1:8100/#/setup普通的路由是不带#的,hash路由是带#的。 #号,我们是不是在哪里见过?大家还记得Spring的官方文档吗? 用a标签实现页面内跳转。hash路由与之类似。 #之后的路由变化不会被发送给服务器,也就是说:127.0.0.1:8100/setup,后台nginx获取到的路径是/setup,而使用hash路由,对于路由127.0.0.1:8100/#/setup,后台获取到的路径就是/。 注入hash路由策略,即可启用hash路由。 再访问,后台获取到的就是/,然后把angular应用返回回来,然后angular应用再去处理#之后的路由,不会出现404。 以上的论述,是我结合官方文档和我的经验得出的解决,如果有不正确之处,欢迎您批评指正。 不足当时忙着上线,ng alain中默认也启用了hash路由,就以为hash路由是正统的解决方案。 但是今天看官方文档,却看到了这样的描述: 几乎所有的Angular项目都会使用默认的HTML 5风格。它生成的URL更易于被用户理解,它也为将来做服务端渲染预留了空间。 在服务器端渲染指定的页面,是一项可以在该应用首次加载时大幅提升响应速度的技术。那些原本需要十秒甚至更长时间加载的应用,可以预先在服务端渲染好,并在少于一秒的时间内完整呈现在用户的设备上。 默认的路由(不带#)的,支持服务器端渲染,而hash路由则不支持。除非你有强烈的理由不得不使用hash路由,否则就应该坚决使用默认的HTML 5路由风格。 hash路由不推荐,不支持SSR。另外,我觉得应该是当前nginx的转发配置写得不好,以后再研究研究。 总结对技术怀着一颗敬畏之心,努力地寻找着最佳实践。

May 10, 2019 · 1 min · jiezi

Angular学习资料

Angular是一款面向企业级应用开发的前端框架,掌握好Angular相关技术,有助于我们提升开发效率,编写高质量的前端代码。 Angular 1.x版本AngularJS 诞生于2009年,由Misko Hevery 等人创建,后为Google所收购。是一款优秀的前端JS框架,已经被用于Google的多款产品当中。AngularJS有着诸多特性,最为核心的是:MVW(Model-View-Whatever)、模块化、自动化双向数据绑定、语义化标签、依赖注入等等。目前稳定在1.7.x,进入了"Long Term Support"版本状态,谷歌表示会长期进行支持。整体来说新项目采用1.x版本的好处不大,可用于旧项目的维护和升级。 1、Angualr菜鸟教程1.x版本的入门教程。有调试环境,可以边做边学,也可以在这上面尝试各种指令。 2、谷歌官方文档,和官方API比较权威,涵盖了各个方面,英文略有难读。 3、官方文档译文和官方API译文中文译文,内容详实,缺点是版本可能略有滞后,准确度稍差。 4、大漠穷秋的教程视频大漠以前是谷歌的官方布道者,讲的比较权威,通俗易懂。 Angular 2+版本Angular 是谷歌开发的一款MVVM(Model-View-ViewModel) 类型的Web 框架,具有优越的性能和绝佳的跨平台性。通常结合 TypeScript 开发,也可以使用 JavaScript 或 Dart,提供了无缝升级的过渡方案。于 2016 年 9 月正式发布,目前已发布到6.0版本。Angular 2+比较大而全,上手容易,开发效率高,适合做企业级的内部管理系统。推荐新项目使用,最新稳定版本6.0+ 1、官方文档和官方文档译文首推文档,文档中包含一个实例教程,按照教程流程演练一遍,即可上手开发项目。中文翻译与Angular主站同步的非常及时。 2、Angular修仙之路某技术同学编写的成套的Angular入门文档,全中文,通俗易懂。版本基于Angular4+的,篇幅较多,内容详实,内容相互独立,适合碎片时间阅读。 3、Angular从0到1《Angular从零到一》图书作者写的手把手的入门教程,书的电子精简版。 4、Angular News 和 Angular 话题知乎上维护的Angular新闻,包括最新的Angular动态和相关更新。 5、技术博客某技术人员的关于Angular的博客,99+篇的干货。 6、速成班博客 和 Angular Material完全攻略某技术人员的博客,比较厉害。 7、点灯坊台湾Angular开发者的博客,深度剖析和给入门者看的文章都有,内容详实。 8、Angular 2 之 30 天迈向神乎其技之路繁体中文社区的高质量全套教程。 UI界面及组件库Angular自带的命令行工具十分强大,它可以创建项目、添加文件以及执行一大堆开发任务,比如测试、打包和发布。更细致的操作可以参考Cli命令指南 ,Angular本身不带UI界面,业内有很多第三方优秀的组件库。 1、NG-ZORROAnt Design 的 Angular实现,支持Angular4.0~6.0,注意Angular版本与对应版本的选择。 2、NG ALAIN基于 ng-zorro-antd 的 Admin 系统解决方案,是一个企业级中后台前端/设计解决方案脚手架。 3、Angular MaterialAngular 官方自己维护的UI组件库。 4、NG-bootstrapBootstrap官方实现UI组件库。 5、ngx-bootstrapBootstrap非官方实现,组件略有差异,文件的引入方式不一样。 6、Clarity Designvmware公司的Angular组件库,新款。 7、ngx-echarts图表echarts的angular实现。 ...

May 7, 2019 · 1 min · jiezi

Angualr-8-事件绑定

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

May 6, 2019 · 1 min · jiezi

Angular-8-组件

创建一个组件独立模板ng g c user-list内联模板ng g c user-list -it显示值插值法 {{}}绑定值从数据流向划分为 3 种: 数据源 -> Template{{expression}}[target]="expression"bind-target="expression"Template -> 数据源(target)="statement"on-target="statement"Template <-> 数据源[(target)]="expression"bindon-target="expression"绑定理解的误区 <button [disabled]="isUnchanged">Save</button> 这并不是将 isUnchanged 的值绑定到了 button 的 disabled attribute 上,而是设置在 该 DOM 元素的 property disabled 上。 HTML attribute 与 DOM property 的对比 HTML attribute 用来初始化 DOM propertyattribute 一旦设置则不可修改,property 可以修改attribute 可以理解为初始值,property 理解为当前值attribute 与 property 并不是完全对应的模板绑定是通过 property 和 event 来工作的,而不是 attribte 绑定目标绑定的目标可以是 property、event、property+event(双向) 绑定目标汇总表属性绑定(property)[property] 形式<img [src]="url">bind-property 形式<img bind-src="url">不应该绑定一个会修改其他任何值的表达式(属性、函数),这可能会导致意料之外的问题 [hero]="currentHero" 与 hero="currentHero" 是不同的 ...

April 29, 2019 · 2 min · jiezi

angular-请求本地json数据

本周写一个前台模块时,发现需要的数据依赖其他模块的支持,但其他模块暂时还不能用,所以需要手动添加模拟数据,然后参考模板demo,发现很有意思的写法,于是就拿来借鉴了。 1. 添加json测试数据在assets/demo/data目录下创建checkRecord.json文件,如图添加数据: 2. 请求数据在check-record.service.ts文件中 getAll(): Observable<CheckRecord[]> { return this.http.get<CheckRecord[]>('assets/demo/data/checkRecord.json');}在check-record.component.ts文件中: // 获取检定记录getAllCheckRecords(): void { this.checkRecordService.getAll().subscribe((checkRecords: CheckRecord[]) => { this.checkRecords = checkRecords; console.log(checkRecords); }, () => { console.log('network error'); });}结果发现写完后,控制台报404 发现请求路径上多了'api',于是就去改拦截器 intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { // 如果请求的是本地资源,则不进行拦截 if (req.url.slice(0, 6) !== 'assets') { const apiReq = req.clone({ url: '/api' + req.url }); return next.handle(apiReq); } else { return next.handle(req); }}再次请求,请求数据成功: 总结发现平时开发还是太依赖前后台相互配合,不能做到完全的前后台分离开发,这次参考别人的写法,学到了一手。

April 27, 2019 · 1 min · jiezi

angular-指令的学习

前言在开发alice项目中,使用了阿里的ng-zorro框架,所以会经常遇到很多框架自己写的属性,例如下面的栅格: <div nz-row> <div nz-col [nzSpan]="4"> xxx </div></div>其中的nz-row,nz-col,以及nzSpan就是框架自定义的html属性,但他们究竟是什么,又是怎么实现的,我却从来没有想过,所以,趁着本周学习时间比较充裕,就去探索了一下。首先,在webstorm通过Ctrl + 鼠标点击nz-col查看它的源码,如图: 发现nz-col其实是一个指令,就去angular上搜索了指令的内容 一、angular指令概览在 Angular 中有三种类型的指令: 组件: 拥有模板的指令,例如:<yunzhi-data-list></yunzhi-data-list>属性型指令: 改变元素、组件或其它指令的外观和行为的指令,上面的nz-row,nz-col就是这类指令结构型指令: 通过添加和移除 DOM 元素改变 DOM 布局的指令,例如:*ngFor,*ngIf这三种指令中最常用的就是组件,所以我重点学习了后两种指令。 二、属性型指令自定义属性型指令目标:使用指令实现给html元素绑定背景色: <button appHighlight = "yellow">高亮</button> 生成指令类文件,同生成组件一样,使用CLI命令即可 ng generate directive highlight然后就会多了这两个文件: 在指令的逻辑部分: export class HighlightDirective implements OnInit {// 使用@input绑定传给指令的值 @Input('appHighlight') highlightColor: string; constructor(private el: ElementRef) { // ElementRef就是该指令绑定的宿主元素 } ngOnInit(): void { // 设置该元素的css样式的背景色为输入值 this.el.nativeElement.style.backgroundColor = this.highlightColor; }}使用该指令: <button appHighlight="yellow">高亮</button>效果:绑定多个属性: export class HighlightDirective implements OnInit { @Input('appHighlight') highlightColor: string; @Input() defaultColor: string; // 默认颜色 constructor(private el: ElementRef) { } ngOnInit(): void { // 如果highlightColor有值,则绑定highlightColor,没有则绑定defaultColor this.el.nativeElement.style.backgroundColor = this.highlightColor || this.defaultColor; }}使用: ...

April 27, 2019 · 2 min · jiezi

特性模块

本周边写项目边进一步学习angular,主要侧重看了模块这一部分,之前没怎么看懂,这里写下自己的学习理解。 特性模块概念特性模块提供了聚焦于特定应用需求的一组功能,比如用户工作流、路由或表单。 虽然你也可以用根模块做完所有事情,不过特性模块可以帮助你把应用划分成一些聚焦的功能区。特性模块通过它提供的服务以及共享出的组件、指令和管道来与根模块和其它模块合作。特性模块其实就是把一个大的系统分成一份份的模块,分别专注与服务,路由,共享组件,还有特定的领域这些方面,把他们组合在一起就足以构成一个功能完善的webapp,之所以把它划分,是因为能够让我们专注与各个领域详细开发,方便维护。 特性模块的分类特性模块分为五个部分: 领域特性模块。带路由的特性模块。路由模块。服务特性模块可视部件特性模块。领域特性模块领域特性模块就是用来提供应用程序中某个领域独特的用户体验,就好比教师模块专注与教师,学院模块专注与管理学院.通常它们会有跟多declarations的组件,但应该只有一个顶级组件来充当根组件并把他导出,供外部使用。比如我现在创建一个TeacherModule,声明如下: @NgModule({ declarations: [TeacherListComponent, TeacherSearchComponent, TeacherIndexComponent], imports: [ CommonModule ]})export class TeacherModule { }这个模块包含teacher列表组件用来展示教师数据,还有一个TeacherSearch组件来搜索教师,用index组件来做根组件,在teacherIndex组件中使用teacherList和teacherSearch组件: <p> teacher-index works! <app-teacher-search></app-teacher-search> <app-teacher-list></app-teacher-list></p>再将teacherIndex组件导出,作为teacherModule的根组件: @NgModule({ declarations: [TeacherListComponent, TeacherSearchComponent, TeacherIndexComponent], imports: [ CommonModule ], exports: [TeacherIndexComponent]})export class TeacherModule {}在AppModule导入我们的领域模块TeacherModule,并在AppComponent里使用teacher-index组件: @NgModule({ declarations: [ AppComponent, MainComponent ], imports: [ BrowserModule, AppRoutingModule, HttpClientModule, TeacherModule ], providers: [], bootstrap: [AppComponent]})export class AppModule {}<app-teacher-index></app-teacher-index>效果如图:我们只暴露了一个teacherIndex组件给根模块使用,关于教师的展示,搜索组件都是在我们自己的teacherModule中定义的,也就是TeacherModule专注于Teacher领域,而根模块并不需要知道怎么实现的,这就把应用划分为了一个个的领域模块了.特性模块不应该有自己的服务提供商,除非需要,服务提供商应该注册在根模块上. 带路由的特性模块带路由的特性模块是一种特殊的领域特性模块,但它的顶层组件会作为路由导航时的目标组件。所有的惰性加载模块都是带路由的特性模块.在上面那个例子中,我们的模块直接导入了AppModule中,这个就是急性加载了,我们可以通过配置模块的路由实现惰性加载。我们先配置AppRoutingModule的路由: const routes: Routes = [ { path: 'teacher', loadChildren: './teacher/teacher.module#TeacherModule' }];我们配置这样一条路由,当路由为teacher路径时,去加载./teacher/teacher.module文件的TeacherModule模块,因此当路径为teacher时angular就会将我们的TeacherModule加载进来了,为此,我们不需要导入TeacherModule,并且TeacherModue也不需要导出任何的根组件: ...

April 27, 2019 · 1 min · jiezi

RxJS原来应该这样用

引言最近帮潘佳琦解决了一个诡异的问题,然后突然发现⾃己对观察者感到迷茫了。 需求是⼀个注销按钮,如果是技术机构登陆,就调用技术机构的注销⽅法,如果是器具用户登陆,就调⽤器具⽤户的注销方法。当然,最优的解决⽅案并不是我下⽂所列的,既然功能不同,那就应该是两个对象。看来我们的⾯向对象运用得还不不够灵活。 原问题解决问题描述注销代码如下,只表达思想,别去深究具体的语法: logout(): void { this.departmentService.isLogin$.subscribe((isDepartmentLogin) => { if (isDepartmentLogin) { this.departmentLogout(); } }); this.systemService.isLogin$.subscribe((isTechnicalLogin) => { if (isTechnicalLogin) { this.technicalLogout(); } });}看着好像没啥错误啊?订阅获取当前登录用户状态,如果departmentService.isLogin$为true,表示是器具用户登录,则调用器具用户的注销方法;如果是systemService.isLogin$为true,表示是技术机构登录,则调用技术机构的注销方法。 然而,诡异的事情发生了: 首次打开系统,登录,登录成功。点击注销,也能注销成功。但是再登录系统,就登不进去了。也就是说,这个注销方法影响了后续的登录,当时就很懵圈,为什么呢? 后来多打了几条日志,才发现了问题所在: 原因分析根据Spring官方的Spring Security and Angular所述,官方做法是用一个boolean值来判断当前是否登录,从而进行视图的展示。 潘老师在新系统中也是根据官方的推荐进行实现的: let isLogin$ = new BehaviorSubject<boolean>();login() { ...... this.isLogin$.next(true);}logout() { ...... this.isLogin$.next(false);}一个可观察的boolean对象来判断当前用户是否登录,然后main组件订阅这个值,根据是否登录并结合ngIf来判断当前应该显示登录组件还是应用组件。 看这个图大家应该就明白了,问题出在了对subscribe的理解上。 点击注销,发起订阅,当isLogin$为true的时候就注销,注销成功。 下次登录时,这个订阅还在呢!然后点击登录,执行登录逻辑,将isLogin$设置为true,观察者就通知了订阅者,又执行了一遍注销逻辑。肯定登不上去了。 执行订阅之后,应该获取返回值,再执行取消订阅。 迷茫所以,这个问题出在本该订阅一次,但是subscribe是只要订阅过了,在取消订阅之前,我一直是这个可观察对象的观察者。 想到这我就迷茫了,就是我一订阅,一直到我取消订阅之前,这个可观察对象都要维护着观察者的列表。 那我们的网络请求用的不也是Observable吗?只是此Observable是由HttpClient构建好返回给我们的。那我们订阅了也没取消,那它是不是一直维护着这个关系,会不会有性能问题?难道我们之前的用法都错了吗? 这个问题一直困扰了我好多天,知道今天才在Angular官网上看到相关的介绍,才解决了我的迷茫。 HttpClient.get()方法正常情况下只会返回一个可观察对象,它或者发出数据,或者发出错误。有些人说它是“一次性完成”的可观察对象。 The HttpClient.get() method normally returns an observable that either emits the data or an error. Some folks describe it as a "one and done" observable. ...

April 26, 2019 · 1 min · jiezi

搜索时自动排除angularjs

由于历史原因在搜索angular的文章时老是有一大堆的angularjs的文章在其中 有的时候甚至大半都是,虽然我们可以使用-angularjs来排除,但能否自动添加呢? google搜索语法突然想到可能有人还不知道-angularjs是啥,这代表排除掉含有angularjs的搜索项,是google的基本语法之一(百度,必应等也有相应的语法,规则上大同小异)实际上Google搜索可以比我们使用的更强大,引用一句话 Google是一款十分强大的搜索引擎,黑客们常常借助它搜索网站的一些敏感目录和文件,甚至可以利用它的搜索功能来自动攻击那些有漏洞的网站;而有些人可以通过搜索把某个个人的信息,包括住址、电话号码、出生年月等都可以搜索出来;当然我们在日常的生活中正确的借助Google搜索也可以更加高效的找到我们需要的东西。当然,这不是本篇文章的重点,如果想要了解一下,可以看看——Google Hacking————你真的会用Google吗? 偶然发现的插件也想过自己去写一个插件来自动添加-angularjs,思路实际上很简单 但是该怎么实现呢?有点懵逼,这时候直接上框架干活的坏处就显现出来了,虽然也算会一些js了,但对浏览器的各种基础接口却不熟悉,这么一个简单的功能却不知如何开始。这些方面的知识也得一点点补上啊,不过也还好,还年轻,一切都不用着急。 不过互联网嘛,资源丰富,找找肯定有相关功能的,果然,我发现了这个 想得真周到,还会在完成搜索以后自动隐藏,体验又上一个台阶,果然自己不光能力不够,境界也不够。 有了源代码了,改改的能力还是有的。 站在大佬的肩旁上下面就来简单看看这段代码首先便是获取相关的信息并判断搜索引擎 const host = location.host const url = new URL(location.href) const searchParams = url.searchParams const paramsIt = searchParams.keys() let search = '?' // 判断搜索引擎 (~这个符号的作用是啥呢?去掉以后效果也一样) if (~host.indexOf('google')) { removeFromGoogle() } else if (~host.indexOf('bing')) { removeFromBing() } else if (~host.indexOf('baidu')) { removeFromBaidu() }需要修改的地方很少, 效果展示 插件源码及安装地址 一点完善思路可以做个界面出来,自己输入要排除的搜索项。这样一点计算机基础都没用的人也同样可以排除自己不想看到的东西。还发现一个可以拦截域名的,是否可以让这两结合一下?有时间的话可以尝试一下

April 26, 2019 · 1 min · jiezi

angular-Ts文件操作dom元素打包报错问题

error TS2339: Property 'style' does not exist on type 'Element'. ts文件操作dom元素打包时报错 error TS2339: Property 'style' does not exist on type 'Element'. 但是并不影响功能 看度娘上的解决办法都不好使 (可能是个人原因 (;´`)ゞ) 问了公司大佬后 const nav: any = document.querySelect('....');就完事了 ヾ(✿゚▽゚)ノ

April 25, 2019 · 1 min · jiezi

使用ng-zorro图标库部分图标不能正常显示

在ng-alain中,使用ng-zorro图标库,发现部分能正常显示,部分并不能显示,在控制台同时发现出错报错。ERROR Error: [@ant-design/icons-angular]: the icon redo-o does not exist or is not registered.at IconNotFoundError (ant-design-icons-angular.js:159)at MapSubscriber.project (ant-design-icons-angular.js:343)...出现以上问题是没有对相对的图标进行导入,并导出。ng-alain默认只导入了图标库的几十个图标,在 style-icons-auto.ts可进行查看。 因此可以参考style-icons-auto.ts,把你所需要的图标进行import and exportng-zorro图标库:https://ng.ant.design/compone...

April 22, 2019 · 1 min · jiezi

都 9012了,该选择 Angular、React,还是Vue?

转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。尽管 Web开发的典型应用场景除了将服务器用作平台、浏览器用作客户端之外,几乎很少活跃于其他业务领域,但不可否认JavaScript 语言和框架的使用已经成为了主流。Angular、React 和 Vue,作为 JavaScript 的三大框架已逐步发展成熟,三者的差异性也越发明显,假设JavaScript还会继续流行十年左右,未来十年,你将会作何选择?AngularAngularJS 自2009年诞生,至今已有十年历史。在这短短十年中,其对 Web 社区的发展产生了十分深远的影响。作为一款优秀的 JavaScript 框架,在其推出一年后,便在全球引起了广泛关注,如今更是在Google的 600 多款产品中得以成功运用,如Firebase控制台、谷歌分析、谷歌快车、谷歌云等。AngularJS有着诸多核心特性,包含:MVC(Model–view–controller)、模块化、自动化双向数据绑定、语义化标签、依赖注入等。Angular 最新版 7.0.0 已于2018年10月发布,下一版本预计将于今年第二季度正式上线。以下是Angular 7 针对性能、命令行工具和Material Design组件的优化项:性能方面:Angular 7 新增的虚拟滚动优化了单页面的呈现方式,对于那些吸引访问者继续向下滚动的clickbait网站来说,这将派上大用场。Angular 7 的另一个性能亮点被称为Bundle Budgets,它用于预警开发人员当前使用的JavaScript包的大小,当JavaScript 包超过 2MB 时开始预警,在达到 5MB 后直接中断生成。命令行提示:当在CLI中键入某些命令,如 ng new 或者 ng add @angular/material 时,Angular 7 会提示用户,让你找到像路由或SCSS支持之类的内置特性,从而简化编码体验,帮助开发者发现新功能或提供灵感。视觉风格:谷歌在Angular 7之前已于2018年更新了Material.io,用户更新后会出现细微的视觉差异:如,UI结构层次更为大胆、形状的边角更加圆滑,五种全新的 Icon 样式,以及一个非常时尚且现代化的拖放模块。Angular 7 拖放效果ReactAngular的出现,在Web社区引发了强烈轰动。两年后,Facebook 也推出了一款同样具备丰富功能的JavaScript UI组件库——React。使用React,意味着您将用一种更简约的方式开始前端开发,这也是大部分开发人员所期待的:没有依赖注入使用JSX(一种基于JavaScript构建的类似XML的语言),而非经典模板,创建虚拟DOM使用状态管理setState和Context APIXSS保护用于单元测试组件的实用程序不多,却正好够用,您完全可以根据自己的需要,自由添加任何组件库,它们包括:路由:React-router获取 HTTP请求:Fetch(或axios)各种各样的CSS封装技术用于单元测试的EnzymeGoogle 和 Facebook 作为 Web 社区开源项目的主要发起者,彼此之间从未停止过竞争,尤其是关于 Angular 和 React 之间的辩论已经持续了四年之久。但严格来说,将Angular与React进行比较并不完全公平,因为Angular是一个功能齐全、组件丰富的框架,而React只是一个UI组件库。为了解决这个问题,我们将就 Angular 框架中的一些常用组件库与 React 进行对比。组件功能:React VS AngularAngular提供了比React更多开箱即用的功能,如:依赖注入基于HTML的扩展模板由 @angular / router 提供的路由使用 @angular / common / http 的Ajax请求用于构建 @angular /forms 的表单组件CSS封装XSS保护用于单元测试组件的实用程序其中,依赖注入等功能作为 Angular 的核心,您无法选择不使用它们,这好像一把双刃剑,在带来强大功能模块的同时,也使得Angular 变得越来越笨重。当然,Google工程师已经意识到了这个问题,也在一定程度上致力于简化Angular框架的复杂性,希望在 Angular 8 中能让人耳目一新。Vue在React 与 Angular孰优孰劣的讨论逐步升温的时候,另一个JavaScript框架Vue抵达了现场,使得这场最优Web开发框架的角逐变得更加白热化。Vue.js 是由Google的核心开发工程师——尤雨溪(Evan You)所创建的框架,作为一个比 React 和 Angular 都更年轻的框架,Vue 从它们那里借鉴了好的部分,即函数式和面向对象编程的混合体。2014年2月(在微软收购GitHub平台四年之前),Evan You在GitHub上发布了第一个稳定版本的Vue,标志着一个构建数据驱动的 Web UI的渐进式框架就此诞生。尽管没有得到谷歌和Facebook等科技巨头的支持,但自2018 年以来,Vue一直受到开发者的广泛关注。从去年几大主流前端开发框架的热度来看,大多数知晓 Vue 的开发者都表示有兴趣学习它。也许,那些已经熟练掌握Angular和React前端框架的开发人员也应该花些时间去了解一下这个简单、小巧、省心的前端框架,希望下面的内容能对你有所帮助。学习曲线:React VS Vue如果前端框架的学习不包含TypeScript(即便 TypeScript 通常被认为是JavaScript的增集,但要完全掌握仍需要学习额外的类处理过程),那么 React和Vue的学习速率都高于Angular。相对于 React,许多初学者认为Vue的学习成本更低,因为它提供了更加丰富的资源文档和中文支持。事实上,Vue和React学习速率的实际情况是大致相同的,由于大部分Vue的学习资料直接以单个Web应用程序的开发实践开始,直观且清晰的代码逻辑的确可以帮助初学者更快入门,但是,随着学习内容的深入,当您需要开发复杂的Web应用程序时,花哨灵活的指令和逻辑反而会让人觉得Vue比React更难掌控。技术社区:React VS VueReact是一个已经存在近十年的Facebook开源项目,因此它拥有更加成熟的技术社区支持。尽管 Vue 已经成功地在短短几年间吸引了相当多的追随者,但在它真正建立出一整套完善且丰富的生态系统之前,仍需要更多人和时间的打磨。当你看到许多使用Vue完成的项目时,你会注意到,其整体的设计理念更趋向现代化,这是因为 Vue 仍是一个相对较新的框架,比如,这个示例。众多周知,React所包含的工具、组件库和代码包的数量更多,但Vue灵巧、精致和简单却更加令人印象深刻。安全性:React VS Vue前端几乎无安全可言!当然,这里所指的安全性,仅仅是 React 和 Vue 这两个框架之间的对比,相对于React,Vue更为小众且不同,因此在面对大规模黑客攻击的时候,React更容易成为目标。Vue和React同样都容易受到跨站点脚本(XSS)攻击,这也是Web应用程序中最为常见的安全漏洞。XSS攻击允许攻击者将客户端脚本注入到其他用户查看的网页中,以影响其关联的任何JavaScript Web应用程序。PS:缓解此问题的最佳方法是将数据保存在脚本之外,加入黑名单机制并从白名单中进行数据验证。灵活性:React VS Vue这也是争议最大的地方。React 专注于 UI,所以在构建 UI 组件时可以从它那里获得很好的支持。Vue作为一个渐进式框架,只允许使用最基本的功能来构建应用程序,但同时也提供了一些开箱即用的东西:如,用于状态管理的 Vuex、用于应用程序 URL 管理的 Vue Router、Vue 服务器端渲染。Vue剥离了许多元素,相比之下React更加全面。但如果您正在寻找一种精简、新颖、简单易学、样板代码少、高性能、灵活且完整的前端框架,Vue更加适合;当然,如果您打算使用低版本jQuery代码,Vue也同样支持。React的灵活性则更多依赖于其背后强大的技术社区,在 Facebook 的强力支撑下(Facebook 的 React 团队包括了 10 名专职开发人员),提供了更多工具、UI库和教程。如果您的开发理念更趋向全栈文化、跨平台、保持独特、引领潮流而不是跟随,那么您一定会喜欢Vue;但如果您的项目需要大量熟练使用该框架的前端开发者、大量的工具及第三方库,那么您最好使用React。不过小孩子才做选择,您最需要的应该是一个全面兼容 Angular、React和Vue的前端开发工具包——WijmoJS。Vue的未来截至2019年初,Angular、React和Vue之间的竞争持续升温,越来越多的开发人员开始抛弃Google项目,就商业开发工具的提供者而言,Vue的未来一片光明。为开发速度更快的 Web 应用程序而选择了Vue的人有明显的增长,Vue 很有趣,开发起来也很简单。虽然,React依托于其庞大的生态圈,在目前为止,处理更复杂的 Web 项目时占据优势,但随着前端社区内大量 Vue 追随者的出现、Vue 社区稳定增长的良好氛围,都在暗示着 Vue 很快就会变得像 React 一样受欢迎。Angular、React,还是Vue?作者尽量保持着公正的态度,客观地分析了上述三个前端框架,而作为前端开发者,面对 Angular、React 和 Vue,你会作何选择?为什么?如果您有任何问题或意见,欢迎在文末回复讨论。 ...

April 18, 2019 · 1 min · jiezi

Fundebug支持浏览器报警

摘要: 除了邮件报警和第三方报警,我们新增了浏览器报警功能。邮件报警与第三方报警Fundebug是专业的应用BUG监控服务,当您的线上应用,比如网页、小程序、Java等发生BUG时,我们会第一时间发送邮件报警,这样可以帮助您及时发现BUG,快速修复BUG。另外,我们还支持各种第三方报警方式,如下:钉钉Slack倍洽简聊Worktile零信自定义Webhook浏览器报警为了帮助用户第一时间发现BUG,我们支持了浏览器报警。默认情况下,如果您保持Fundebug控制台打开,我们会每隔1个小时检查是否有新的错误出现,并且通过浏览器提醒告诉您:您也可以在项目设置页面对该功能进行配置,选择开启或者关闭浏览器提醒,或者配置浏览器提醒的时间间隔(取值为60到3600秒之间)。最后,感谢Fundebug用户大宝的反馈。关于FundebugFundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和Java线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了9亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用!版权声明转载时请注明作者Fundebug以及本文地址:https://blog.fundebug.com/2019/02/25/fundebug-javascript-1-6-0/

April 10, 2019 · 1 min · jiezi

Angular CLI 使用教程指南参考

Angular CLI 使用教程指南参考安装要安装Angular CLI你需要先安装node和npm,然后运行以下命令来安装最新的Angular CLI:注意:Angular CLI 需要Node 4.X 和 NPM 3.X 以上的版本支持。npm install -g angular-cli在 Mac 或 Linux 平台上,你可能需要添加sudo前缀提权进行全局安装:sudo npm install -g angular-cli基本用法你可以通过 Angular CLI 的 help 命令来获取相关的命令信息.ng helpAngular CLI的命令关键字为 ngng new命令描述ng new <project-name> [options]创建一个新的 Angular 项目,默认在当前所在目录下参数描述–dry-run -d只输出要创建的文件和执行的操作,实际上并没有创建项目–verbose -v输出详细信息–skip-npm在项目第一次创建时不执行任何npm命令–name指定创建项目的名称ng serveng new PROJECT_NAMEcd PROJECT_NAMEng serve将会自动在浏览器中打开默认地址 http://localhost:4200/. 运行之后如果你修改了程序源代码.应用将会自动重载.你也可以自定义配置 IP, 端口和实时重载端口号ng serve –host 0.0.0.0 –port 4201 –live-reload-port 49153ng init命令描述ng init <project-name> [options]在当前所在目录下初始化一个新的 Angular 项目参数描述–dry-run -d只输出要创建的文件和执行的操作,实际上并没有创建项目–verbose -v输出详细信息–skip-npm在项目第一次创建时不执行任何npm命令–name指定创建项目的名称ng completion命令描述ng completion将自动完成功能添加到ng命令的shell中ng doc命令描述ng doc <keyword>在浏览器中打开Angular文档并搜索当前关键字ng e2e命令描述ng e2e使用protractor在当前应用中运行e2e测试ng format命令描述ng format使用clang-format格式化当前项目代码ng generate命令描述ng generate <type> [options]在项目中构建新代码ng g <type> [options]简写支持的类型用法Componentng g component my-new-componentDirectiveng g directive my-new-directivePipeng g pipe my-new-pipeServiceng g service my-new-serviceClassng g class my-new-classInterfaceng g interface my-new-interfaceEnumng g enum my-new-enumModuleng g module my-moduleRouteng g route my-route当前已禁用构建的组件都会使用自用目录,除非 –flat 单独指定.参数描述–flat不在自用目录内创建代码–route=<route>指定父路由.仅用于生成组件和路由.默认为指定的路径.–skip-router-generation跳过生成父路由配置。只能用于路由命令。–default指定路由应为默认路由。–lazy指定路由是延迟的。 默认为true。ng get命令描述ng get <path1, path2, …pathN> [options]从Angular CLI配置获取值pathN是一个有效的JavaScript参数路径,例如“users[1].userName”。 如果未设置该值,将显示“undefined”。 此命令默认情况下仅在项目目录中工作。参数描述–global返回全局配置值,而不是本地配置值(如果都设置). 此选项还可以使命令在项目目录外工作ng set命令描述ng get <path1=value1, path2=value2, …pathN=valueN> [options]在Angular CLI配置中设置值默认情况下,如果在项目内部运行,则设置项目配置中的值,如果不在项目内部,则失败。 pathN参数是一个有效的JavaScript路径,如“users [1] .userName”。 该值将被强制转换为正确的类型,或者如果类型无法强制,则会抛出错误。参数描述–global设置全局配置值,而不是本地配置值。 这也使ng set可以在项目之外工作。ng build构建工件将存储在/dist目录中。ng build可以指定构建目标(–target = production或–target = development)和要与该构建一起使用的环境文件(–environment = dev或–environment = prod)。 默认情况下,使用开发构建目标和环境。# 这是生产构建ng build –target=production –environment=prodng build –prod –env=prodng build –prod# 这是开发构建ng build –target=development –environment=devng build –dev –e=devng build –devng buildng github-pages:deploy命令描述ng github-pages:deploy [options]构建生产应用程序,设置GitHub存储库,然后发布应用程序。参数描述–message=<message>构建并提交信息.默认为 “new gh-pages version”–environment=<env>angular 环境构建。 默认为“production”–branch=<branch-name>推送页面的git分支。 默认为“gh-branch”–skip-build在发布之前跳过构建项目–gh-token=<token>用于部署的API令牌,必须.–gh-username=<username>使用的Github用户名,必须.ng lint命令描述ng lint在项目上运行codelyzer linterng test命令描述ng test [options]使用 karma 运行单元测试参数描述–watch继续运行测试. 默认为true–browsers, –colors, –reporters, –port, –log-level这些参数直接传递给karmang version命令描述ng version输出cli版本, node 版本和操作系统信息参数描述–watch继续运行测试. 默认为true ...

April 10, 2019 · 1 min · jiezi

Angular 8 配置 oidc

配置相对较为繁琐,最后会放上 Github 源码地址新建一个 ng 项目ng new angular-oidc进入目录 cd angular-oidc安装 oidc-clientnpm i oidc-client –save配置 oidc-client 参数打开 environment.ts 将下面的代码覆盖原来的内容import { WebStorageStateStore } from “oidc-client”;export const environment = { production: false, authConfig: { authority: “http://localhost:57001”, client_id: “query”, redirect_uri: “http://localhost:4200/login-callback”, response_type: “id_token token”, scope: “openid profile”, post_logout_redirect_uri: “http://localhost:4200”, accessTokenExpiringNotificationTime: 4, filterProtocolClaims: true, silentRequestTimeout: 10000, loadUserInfo: true, userStore: new WebStorageStateStore({ store: window.localStorage }), },};需要修改的几个参数:authority: 认证服务器,需要修改为自己的认证服务器client_id: 客户端 id ,按照约定修改即可redirect_uri: 认证服务器回调的客户端页面post_logout_redirect_uri: 登出回调链接模块划分这里我们把模块划分为2块: 1) 游客模块 2) 用户模块默认的壳组件所在的 module 作为游客模块, 另外还需要构建一个用户模块游客模块为了方便理解, 游客模块创建一个欢迎页, 点击继续按钮访问用户模块.1. 创建一个欢迎页没什么特别的作用, 就是为了方便理解单独设立的一个交互页面.ng g c public/index修改 index.component.html<h3>WELLCOME TO ANGULAR OIDC</h3><input type=“button” value=“visit” (click)=“visitAuth()">修改 index.component.tsimport { Component, OnInit } from “@angular/core”;import { Router } from “@angular/router”;@Component({ selector: “app-index”, templateUrl: “./index.component.html”, styleUrls: [”./index.component.less"],})export class IndexComponent implements OnInit { constructor(private _router: Router) {} ngOnInit() {} public visitAuth(): void { this._router.navigate([“auth”]); }}2. 创建一个回调页回调页是用户 oidc 认证结束后的回调, 起到一个过度的作用(目前先空着)ng g c public/login-callback3. 配置路由打开 app-routing.module.ts, 对照修改import { NgModule } from “@angular/core”;import { Routes, RouterModule } from “@angular/router”;import { IndexComponent } from “./public/index/index.component”;import { LoginCallbackComponent } from “./public/login-callback/login-callback.component”;const routes: Routes = [ { path: “”, pathMatch: “full”, component: IndexComponent, }, { path: “login-callback”, component: LoginCallbackComponent, },];@NgModule({ imports: [RouterModule.forRoot(routes)], exports: [RouterModule],})export class AppRoutingModule {}启动程序 ng s -o, 这时候已经能看到一点点信息了, 不过还没有 home 路由, 下面来配置一下用户模块1. 添加一个 auth 模块ng g m auth/auth –flat–flat:不在一个单独的文件夹创建2. 将 auth 模块添加到壳组件打开 app-module.ts, 主要修改一下内容import { AuthModule } from “./auth/auth.module”;…imports: […, AuthModule],3. 添加 auth “壳组件"ng g c auth/auth4. 添加 auth 模块的路由ng g m auth/auth-routing –flat修改 auth-routing.module.ts 内容如下:import { NgModule } from “@angular/core”;import { RouterModule, Routes } from “@angular/router”;import { AuthComponent } from “./auth/auth.component”;const routes: Routes = [ { path: “home”, component: AuthComponent, },];@NgModule({ exports: [RouterModule],})export class AuthRoutingModule {}5. 修改 app-routing.module.ts 添加 home 路由const routes: Routes = [ { path: “”, pathMatch: “full”, component: IndexComponent, }, { path: “login-callback”, component: LoginCallbackComponent, }, { path: “home”, component: AuthComponent, },];ctrl + c -> y 停止之前启动项目的终端, ng s 重新启动项目此时的项目已经可以从游客路由跳转至用户路由,但我们是不允许游客默认访问用户路由的, 这时候就应该 守卫(Guard) 登场了。配置守卫(Guard)1. 添加 auth.service (认证相关的函数)ng g s auth/auth –flat替换 auth.service.ts 内容:import { Injectable, EventEmitter } from ‘@angular/core’;import { environment } from ‘src/environments/environment’;import { UserManager, User } from ‘oidc-client’;import { Observable, from } from ‘rxjs’;@Injectable({ providedIn: ‘root’})export class AuthService { // 大多数 oidc-client 操作都在其中 private manager: UserManager = new UserManager(environment.authConfig); // private manager: UserManager = undefined; // 登录状态改变事件 public loginStatusChanged: EventEmitter<User> = new EventEmitter(); // localStorage 中存放用户信息的 Key private userKey = oidc.user:${environment.authConfig.authority}:${environment.authConfig.client_id}; // private userKey = oidc.user:${this._conf.env.authConfig.authority}:${this._conf.env.authConfig.client_id}; constructor() { // 如果访问用的 token 过期,调用 login() this.manager.events.addAccessTokenExpired(() => { this.login(); }); } login() { this.manager.signinRedirect(); } logout() { this.manager.signoutRedirect(); } loginCallBack() { return Observable.create(observer => { from(this.manager.signinRedirectCallback()) .subscribe((user: User) => { this.loginStatusChanged.emit(user); observer.next(user); observer.complete(); }); }); } tryGetUser() { return from(this.manager.getUser()); } get type(): string { return ‘Bearer’; } get user(): User | null { const temp = localStorage.getItem(this.userKey); if (temp) { const user: User = JSON.parse(temp); return user; } return null; } get token(): string | null { const temp = localStorage.getItem(this.userKey); if (temp) { const user: User = JSON.parse(temp); return user.access_token; } return null; } get authorizationHeader(): string | null { if (this.token) { return ${this.type} ${this.token}; } return null; }}2. 添加 auth.guardng g g auth/auth –flat选择 CanActivate替换 auth.guard.ts 内容:import { Injectable } from “@angular/core”;import { CanActivate, CanActivateChild, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree,} from “@angular/router”;import { Observable } from “rxjs”;import { map } from “rxjs/operators”;import { AuthService } from “./auth.service”;import { User } from “oidc-client”;@Injectable({ providedIn: “root”,})export class AuthGuard implements CanActivate { constructor(private _auth: AuthService) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean> { return this.mapper(this._auth.tryGetUser()); } private mapper = map((user: User) => { if (user) return true; this._auth.login(); return false; });}3. 修改 app-routing.module.tsimport { NgModule } from “@angular/core”;import { RouterModule, Routes } from “@angular/router”;import { AuthComponent } from “./auth/auth.component”;import { C1Component } from “./test/c1/c1.component”;import { C2Component } from “./test/c2/c2.component”;const routes: Routes = [ { path: “home”, component: AuthComponent, children: [ { path: “c1”, component: C1Component }, { path: “c2”, component: C2Component }, ], },];@NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule],})export class AuthRoutingModule {}4. 修改 login-callback.component.ts 页回到成功后,导航到 home 页,你也可以写更多的其他逻辑。import { Component, OnInit } from “@angular/core”;import { Router } from “@angular/router”;import { User } from “oidc-client”;import { AuthService } from “src/app/auth/auth.service”;@Component({ selector: “app-login-callback”, templateUrl: “./login-callback.component.html”, styleUrls: [”./login-callback.component.less"],})export class LoginCallbackComponent implements OnInit { constructor(private _router: Router, private _auth: AuthService) {} ngOnInit() { this._auth.loginCallBack().subscribe((user: User) => { this._router.navigate([“home”]); }); }}顺便美化一下下样式login-callback.component.html:<div class=“callback-bar”> <span style=“margin-left: 10px;">登录成功,跳转中…</span></div>login-callback.component.less(我这里使用的是 less,你的可能是 css/scss/sass):.callback-bar { margin: 0px 0px 0px 0px; padding: 8px 0px 0px 0px; font-size: 24px; font-weight: 600px; color: white; background-color: #3881bf; box-shadow: 0px 3px 5px #666; height: 50px;}再此重启一下程序(往往一些奇奇怪怪的问题重新启动后会被解决)。这时候就已经实现了一个认证的过程,不过 auth 模块(用户模块)只有一个组件,总感觉不够直观,因此,我们需要在 auth 模块添加更多的组件,形成子路由,在观察功能。添加 auth 子组件、子路由修改 auth.component 组件1. auth.component.html<div> <input type=“button” value=“c1” (click)=“goC1()"> <input type=“button” value=“c2” (click)=“goC2()"></div><div> <router-outlet></router-outlet></div>2. auth.component.tsimport { Component, OnInit } from “@angular/core”;import { Router } from “@angular/router”;@Component({ selector: “app-auth”, templateUrl: “./auth.component.html”, styleUrls: [”./auth.component.less”],})export class AuthComponent implements OnInit { constructor(private _router: Router) {} ngOnInit() {} public goC1(): void { this._router.navigate([“home/c1”]); } public goC2(): void { this._router.navigate([“home/c2”]); }}新建子路由2. 添加 c1、c2 子组件ng g c auth/test/c1ng g c auth/test/c2保持默认内容即可。3. 修改 auth-routing.module.tsimport { NgModule } from “@angular/core”;import { RouterModule, Routes } from “@angular/router”;import { AuthComponent } from “./auth/auth.component”;import { C1Component } from “./test/c1/c1.component”;import { C2Component } from “./test/c2/c2.component”;const routes: Routes = [ { path: “home”, component: AuthComponent, children: [ { path: “c1”, component: C1Component }, { path: “c2”, component: C2Component }, ], },];@NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule],})export class AuthRoutingModule {}重启项目,这时候得到一个错误信息:Error: Template parse errors:‘router-outlet’ is not a known element:这表示 auth 模块没有引入 RouterModule,其实是我们的 auth.module.ts 没有引入 auth-routing.module.ts 导致的(routing 中有引入 RouterModule)修改 auth.module.ts:…import { AuthRoutingModule } from ‘./auth-routing.module’;@NgModule({ … imports: […, AuthRoutingModule],})重启项目,可以看到现在基本功能都已经实现了,不过还差一个退出功能。退出登录1. 修改 auth.component.html<div> <input type=“button” value=“c1” (click)=“goC1()"> <input type=“button” value=“c2” (click)=“goC2()"> <input type=“button” value=“exit” (click)=“exit()"></div><div> <router-outlet></router-outlet></div>2. 修改 auth.component.tsimport { Component, OnInit } from “@angular/core”;import { Router } from “@angular/router”;import { AuthService } from “../auth.service”;@Component({ selector: “app-auth”, templateUrl: “./auth.component.html”, styleUrls: [”./auth.component.less”],})export class AuthComponent implements OnInit { constructor(private _router: Router, private _auth: AuthService) {} ngOnInit() {} public goC1(): void { this._router.navigate([“home/c1”]); } public goC2(): void { this._router.navigate([“home/c2”]); } public exit(): void { this._auth.logout(); }}重启测试,退出成功!访问 /home 自动跳转登录,没问题。访问 /home/c1 居然跳过了认证,直接进来了!造成这个问题的原因是但是我们的守卫添加的方式是 canActivate,canActivate只会保护本路由,而不会保护其子路由。因此,我们还需要保护子路由!保护子路由1. 修改 auth.guard.tsimport { Injectable } from “@angular/core”;import { CanActivate, CanActivateChild, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree,} from “@angular/router”;import { Observable } from “rxjs”;import { map } from “rxjs/operators”;import { AuthService } from “./auth.service”;import { User } from “oidc-client”;@Injectable({ providedIn: “root”,})export class AuthGuard implements CanActivate, CanActivateChild { constructor(private _auth: AuthService) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean> { return this.mapper(this._auth.tryGetUser()); } canActivateChild( next: ActivatedRouteSnapshot, state: RouterStateSnapshot ): | Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree { return this.mapper(this._auth.tryGetUser()); } private mapper = map((user: User) => { if (user) return true; this._auth.login(); return false; });}2. 修改 auth-routing.module.ts主要修改代码如下:import { AuthGuard } from “./auth.guard”; // <- hereconst routes: Routes = [ { path: “home”, component: AuthComponent, canActivateChild: [AuthGuard], // <- here children: [ { path: “c1”, component: C1Component }, { path: “c2”, component: C2Component }, ], },];重启项目,再此访问 ‘/home/c1’,成功跳转,访问 ‘/home’,同样成功跳转。Githubangular-oidc ...

April 8, 2019 · 6 min · jiezi

Angular脚手架开发

简介写一份自定义的angular脚手架吧写之前我们先解析一下antd的脚手架前提先把 Angular Schematic这篇文章读一遍,确保了解了collection等基础antd脚手架克隆项目git clone https://github.com/NG-ZORRO/ng-zorro-antd.git开始打开项目在schematics下的collection.json为入口,查看内容一共定了了4个schematic,每个schema分别指向了各文件夹的子schema.json,factory指向了函数入口,index.tsng-add/schema.json{ // 指定schema.json的验证模式 “$schema”: “http://json-schema.org/schema", “id”: “nz-ng-add”, “title”: “Ant Design of Angular(NG-ZORRO) ng-add schematic”, “type”: “object”, // 包含的属性 “properties”: { “project”: { “type”: “string”, “description”: “Name of the project.”, “$default”: { “$source”: “projectName” } }, // 是否跳过package.json的安装属性 “skipPackageJson”: { // 类型为布尔 “type”: “boolean”, // 默认值为false “default”: false, // 这是个描述,可以看到,如果在ng add ng-zorro-antd时不希望自动安装可以加入–skipPackageJson配置项 “description”: “Do not add ng-zorro-antd dependencies to package.json (e.g., –skipPackageJson)” }, // 开始页面 “bootPage”: { // 布尔 “type”: “boolean”, // 默认为true “default”: true, // 不指定–bootPage=false的话,你的app.html将会被覆盖成antd的图标页 “description”: “Set up boot page.” }, // 图标配置 “dynamicIcon”: { “type”: “boolean”, “default”: false, “description”: “Whether icon assets should be add.”, “x-prompt”: “Add icon assets [ Detail: https://ng.ant.design/components/icon/en ]” }, // 主题配置 “theme”: { “type”: “boolean”, “default”: false, “description”: “Whether custom theme file should be set up.”, “x-prompt”: “Set up custom theme file [ Detail: https://ng.ant.design/docs/customize-theme/en ]” }, // i18n配置,当你ng add ng-antd-zorro 的时候有没有让你选择这个选项呢? “i18n”: { “type”: “string”, “default”: “en_US”, “enum”: [ “ar_EG”, “bg_BG”, “ca_ES”, “cs_CZ”, “da_DK”, “de_DE”, “el_GR”, “en_GB”, “en_US”, “es_ES”, “et_EE”, “fa_IR”, “fi_FI”, “fr_BE”, “fr_FR”, “is_IS”, “it_IT”, “ja_JP”, “ko_KR”, “nb_NO”, “nl_BE”, “nl_NL”, “pl_PL”, “pt_BR”, “pt_PT”, “sk_SK”, “sr_RS”, “sv_SE”, “th_TH”, “tr_TR”, “ru_RU”, “uk_UA”, “vi_VN”, “zh_CN”, “zh_TW” ], “description”: “add locale code to module (e.g., –locale=en_US)” }, “locale”: { “type”: “string”, “description”: “Add locale code to module (e.g., –locale=en_US)”, “default”: “en_US”, “x-prompt”: { “message”: “Choose your locale code:”, “type”: “list”, “items”: [ “en_US”, “zh_CN”, “ar_EG”, “bg_BG”, “ca_ES”, “cs_CZ”, “de_DE”, “el_GR”, “en_GB”, “es_ES”, “et_EE”, “fa_IR”, “fi_FI”, “fr_BE”, “fr_FR”, “is_IS”, “it_IT”, “ja_JP”, “ko_KR”, “nb_NO”, “nl_BE”, “nl_NL”, “pl_PL”, “pt_BR”, “pt_PT”, “sk_SK”, “sr_RS”, “sv_SE”, “th_TH”, “tr_TR”, “ru_RU”, “uk_UA”, “vi_VN”, “zh_TW” ] } }, “gestures”: { “type”: “boolean”, “default”: false, “description”: “Whether gesture support should be set up.” }, “animations”: { “type”: “boolean”, “default”: true, “description”: “Whether Angular browser animations should be set up.” } }, “required”: []}schema.ts当你进入index.ts时首先看到的是一个带options:Schema的函数,options指向的类型是Schema interface,而这个interface 恰好是schema.json中的properties,也就是cli的传入参数类.我们可以通过自定义传入参数类来完成我们需要的操作.export type Locale = | ‘ar_EG’ | ‘bg_BG’ | ‘ca_ES’ | ‘cs_CZ’ | ‘da_DK’ | ‘de_DE’ | ’el_GR’ | ’en_GB’ | ’en_US’ | ’es_ES’ | ’et_EE’ | ‘fa_IR’ | ‘fi_FI’ | ‘fr_BE’ | ‘fr_FR’ | ‘is_IS’ | ‘it_IT’ | ‘ja_JP’ | ‘ko_KR’ | ’nb_NO’ | ’nl_BE’ | ’nl_NL’ | ‘pl_PL’ | ‘pt_BR’ | ‘pt_PT’ | ‘sk_SK’ | ‘sr_RS’ | ‘sv_SE’ | ’th_TH’ | ’tr_TR’ | ‘ru_RU’ | ‘uk_UA’ | ‘vi_VN’ | ‘zh_CN’ | ‘zh_TW’;export interface Schema { bootPage?: boolean; /** Name of the project to target. / project?: string; /* Whether to skip package.json install. */ skipPackageJson?: boolean; dynamicIcon?: boolean; theme?: boolean; gestures?: boolean; animations?: boolean; locale?: Locale; i18n?: Locale;}ng-add/index.tsimport { Rule, SchematicContext, Tree } from ‘@angular-devkit/schematics’;import { NodePackageInstallTask, RunSchematicTask } from ‘@angular-devkit/schematics/tasks’;import { addPackageToPackageJson } from ‘../utils/package-config’;import { hammerjsVersion, zorroVersion } from ‘../utils/version-names’;import { Schema } from ‘./schema’;// factory指向的index.ts必须实现这个函数,一行一行看代码// 我们的函数是一个更高阶的函数,这意味着它接受或返回一个函数引用。// 在这种情况下,我们的函数返回一个接受Tree和SchematicContext对象的函数。// options:Schema上面提到了export default function(options: Schema): Rule {// tree:虚拟文件系统:用于更改的暂存区域,包含原始文件系统以及要应用于其的更改列表。// rule:A Rule是一个将动作应用于Tree给定的函数SchematicContext。 return (host: Tree, context: SchematicContext) => { // 如果需要安装包,也就是–skipPackageJson=false if (!options.skipPackageJson) { // 调用addPackageToPackageJson,传入,tree文件树,包名,包版本 addPackageToPackageJson(host, ’ng-zorro-antd’, zorroVersion); // hmr模式包 if (options.gestures) { addPackageToPackageJson(host, ‘hammerjs’, hammerjsVersion); } } const installTaskId = context.addTask(new NodePackageInstallTask()); context.addTask(new RunSchematicTask(’ng-add-setup-project’, options), [installTaskId]); if (options.bootPage) { context.addTask(new RunSchematicTask(‘boot-page’, options)); } };}addPackageToPackageJson// 看function名字就知道这是下载依赖的函数// @host:Tree 文件树// @pkg:string 包名// @vserion:string 包版本// @return Tree 返回了一个修改完成后的文件树export function addPackageToPackageJson(host: Tree, pkg: string, version: string): Tree { // 如果文件树里包含package.json文件 if (host.exists(‘package.json’)) { // 读取package.json的内容用utf-8编码 const sourceText = host.read(‘package.json’).toString(‘utf-8’); // 然后把package.json转化为对象,转为对象,转为对象 const json = JSON.parse(sourceText); // 如果package.json对象里没有dependencies属性 if (!json.dependencies) { // 给package对象加入dependencies属性 json.dependencies = {}; } // 如果package对象中没有 pkg(包名),也就是说:如果当前项目没有安装antd if (!json.dependencies[pkg]) { // 那么package的dependencies属性中加入 antd:version json.dependencies[pkg] = version; // 排个序 json.dependencies = sortObjectByKeys(json.dependencies); } // 重写tree下的package.json内容为(刚才不是有package.json对象吗,现在在转回去) host.overwrite(‘package.json’, JSON.stringify(json, null, 2)); } // 把操作好的tree返回给上一级函数 return host;}现在在回过头去看 ng-add/index.ts// 给context对象增加一个安装包的任务,然后拿到了任务idconst installTaskId = context.addTask(new NodePackageInstallTask());// context增加另一个任务,然后传入了一个RunSchematicTask对象,和一个id集合 context.addTask(new RunSchematicTask(’ng-add-setup-project’, options), [installTaskId]);RunSchematicTask(’ng-add-setup-project’)任务ng-add-setup-project定义在了schematic最外层的collection.json里,记住如下4个schematic,后文不再提及{ “$schema”: “./node_modules/@angular-devkit/schematics/collection-schema.json”, “schematics”: { “ng-add”: { “description”: “add NG-ZORRO”, “factory”: “./ng-add/index”, “schema”: “./ng-add/schema.json” }, // 在这里 “ng-add-setup-project”: { “description”: “Sets up the specified project after the ng-add dependencies have been installed.”, “private”: true, // 这个任务的函数指向 “factory”: “./ng-add/setup-project/index”, // 任务配置项 “schema”: “./ng-add/schema.json” }, “boot-page”: { “description”: “Set up boot page”, “private”: true, “factory”: “./ng-generate/boot-page/index”, “schema”: “./ng-generate/boot-page/schema.json” }, “add-icon-assets”: { “description”: “Add icon assets into CLI config”, “factory”: “./ng-add/setup-project/add-icon-assets#addIconToAssets”, “schema”: “./ng-generate/boot-page/schema.json”, “aliases”: [“fix-icon”] } }}ng-add/setup-project// 刚才的index一样,实现了一个函数export default function (options: Schema): Rule { // 这里其实就是调用各种函数的一个集合.options是上面的index.ts中传过来的,配置项在上文有提及 return chain([ addRequiredModules(options), addAnimationsModule(options), registerLocale(options), addThemeToAppStyles(options), options.dynamicIcon ? addIconToAssets(options) : noop(), options.gestures ? hammerjsImport(options) : noop() ]);}addRequiredModules// 模块字典const modulesMap = { NgZorroAntdModule: ’ng-zorro-antd’, FormsModule : ‘@angular/forms’, HttpClientModule : ‘@angular/common/http’};// 加入必须依赖模块export function addRequiredModules(options: Schema): Rule { return (host: Tree) => { // 获取tree下的工作目录 const workspace = getWorkspace(host); // 获取项目 const project = getProjectFromWorkspace(workspace, options.project); // 获取app.module的路径 const appModulePath = getAppModulePath(host, getProjectMainFile(project)); // 循环字典 for (const module in modulesMap) { // 调用下面的函数,意思就是:给appModule引一些模块,好吧,传入了tree,字典key(模块名称),字典value(模块所在包),project对象,appModule的路径,Schema配置项 addModuleImportToApptModule(host, module, modulesMap[ module ], project, appModulePath, options); } // 将构建好的tree返回给上层函数 return host; };}function addModuleImportToApptModule(host: Tree, moduleName: string, src: string, project: WorkspaceProject, appModulePath: string, options: Schema): void { if (hasNgModuleImport(host, appModulePath, moduleName)) { console.log(chalk.yellow(Could not set up "${chalk.blue(moduleName)}" + because "${chalk.blue(moduleName)}" is already imported. Please manually + check "${chalk.blue(appModulePath)}" file.)); return; } addModuleImportToRootModule(host, moduleName, src, project);}未完待续 ...

April 8, 2019 · 4 min · jiezi

手动实现“低配版”IOC

引言IOC,全称Inversion of Control,控制反转。也算是老生常谈了。老生常谈:原指老书生的平凡议论;今指常讲的没有新意的老话。同时另一个话题,依赖注入,用什么就声明什么,直接就声明,或者构造函数或者加注解,控制反转是实现依赖注入的一种方式。通过依赖注入:我们无需管理对象的创建,通过控制反转:我们可以一键修改注入的对象。最近在做Android实验与小程序相关的开发,发现用惯了IOC的我们再去手动new对象的时候总感觉心里不舒服,以后改起来怎么办呢?既然没有IOC,我们就自己写一个吧。实现就像我在标题中描述的一样,我先给大家讲解一下标配IOC的原理,使大家更清晰明了,但是受Android与小程序相关的限制,我具体的实现,是低配版IOC。个人扯淡不管是Spring还是Angular,要么是开源大家,要么是商业巨头,具体的框架实现都是相当优秀。我还没水平也没精力去研读源码,只希望分享自己对IOC的理解,帮到更多的人。毕竟现在小学生都开始写Python,以后的框架设计会越来越优秀,学习成本越来越低,开发效率越来越高。可能是个人报个培训班学个俩月也会设计微服务,也能成为全栈工程师。所以我们应该想的是如何设计框架,而不是仅停留在使用的层面,渐渐地被只会写增删改查天天搬砖的人取代。996加班的工程师都开始挤时间写框架扩大影响力,让社会听到程序员的呐喊,我们还有什么不努力的理由?“标配”IOC不管是Spring还是Angular,它们的核心是什么呢?打上mvn spring-boot:run后台就Started Application in xxx seconds了,它到底干什么了呢?容器Spring与Angular就是一个大的IOC容器,所以应用启动的过程,其实就是构造容器的过程。容器,肯定是装东西的啊?IOC容器里装的是什么?装的是对象。控制器Controller,服务Service,自定义的组件Component,所有被Spring管理的对象都将被放进IOC容器里。所以,大家应该能明白,为什么IOC是依赖注入的一种实现方式?因为这个对象不是你自己new的,是从容器中拿的,容器初始化的时候,就已经把这个对象构造好了,该注的都注进来了。思考从上面大家可以看到,依赖注入的前提是什么?是要求这个对象必须是从容器中拿的,所以才能依赖注入成功。Spring Boot中没问题,Tomcat转发的路由直接交给容器中相应的对象去处理,同理,Angular也一样。Android呢?手机调用的并不是我们构造的Activity,而是它自己实例化的,小程序也与之类似,Page的实例化不归我们管。所以“标配”IOC在这里不适用,所以我设计了“低配”IOC容器。“低配”IOC找点自己能管得了的对象放在IOC容器里,这样再需要对象就不用去new了,Service有变更直接修改注入就行了。受我们管理的只有Service,计划设计一个管理所有Service的IOC容器,然后Activity或Page里用的时候,直接从容器中拿。(低配在这里,不能依赖注入了,得自己拿)。Android端一个单例的Configuration负责注册Bean和获取Bean,存储着一个context上下文。看着挺高级的,其实就是一个HashMap存储着接口类型到容器对象的映射。/** * 全局配置类 /public class Configuration { private static Map<Class<?>, Object> context = new HashMap<>(); private static final class Holder { private static final Configuration INSTANCE = new Configuration(); } public static Configuration getInstance() { return Holder.INSTANCE; } public Configuration registerBean(Class<?> clazz, Object bean) { context.put(clazz, bean); return this; } public <T> T getBean(Class<?> clazz) { return (T) context.get(clazz); }}写一个静态方法,更加方便配置。/* * 云智,全局配置辅助类 */public class Yunzhi { … public static <T> T getBean(Class<?> clazz) { return Configuration.getInstance().getBean(clazz); }}一个Application负责容器中所有对象的创建。public class App extends Application { @Override public void onCreate() { super.onCreate(); Yunzhi.init() .setApi(“http://192.168.2.110:8888”) .setTimeout(1L) .registerBean(AuthService.class, new AuthServiceImpl()) .registerBean(LetterService.class, new LetterServiceImpl()); }}使用方法和原来就一样了,一个接口,一个实现类。这里用到了RxJava,看上去倒是类似我们的Angular了。public interface AuthService { Observable<Auth> login(String username, String password);}public class AuthServiceImpl implements AuthService { private static final String TAG = “AuthServiceImpl”; @Override public Observable<Auth> login(String username, String password) { Log.d(TAG, “BASIC 认证”); String credentials = username + “:” + password; String basicAuth = “Basic " + Base64.encodeToString(credentials.getBytes(), Base64.NO_WRAP); Log.d(TAG, “请求登录”); return HttpClient.request(AuthRequest.class) .login(basicAuth) .subscribeOn(Schedulers.io()) // 在IO线程发起网络请求 .observeOn(AndroidSchedulers.mainThread()); // 在主线程处理 }}因为Activity我们管不着,所以在Activity里用不了依赖注入,需要手动从容器里拿。@Overrideprotected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); this.authService = Yunzhi.getBean(AuthService.class);}这里有一处没考虑到的问题,就是手机端的性能问题,手机和服务器的处理能力肯定是比不了的,服务器在初始化的时候把所有对象都创建处理无可厚非,但是感觉手机端这样做还是会对性能产生一定影响的。应该是某些对象用到的时候再创建,实验要求时间很紧,这个就先这样吧,不改了。小程序端小程序是在Android之后写的,想到了之前设计的部分缺陷,对容器使用了另一种思想进行实现。export class YunzhiService { private static context = new Map<string, object>(); public static getBean(beanName: string): object { // 从context里拿对象 let bean = this.context.get(beanName); // 如果没有,构造一个,并放进context if (!bean) { bean = this.createBeanByName(beanName); this.context.set(beanName, bean); } // 返回 return bean; } public static createBeanByName(beanName: string): object { // 根据不同名称构造不同的Bean switch (beanName) { case Bean.AUTH_SERVICE: return new AuthServiceImpl(); case Bean.SCORE_SERVICE: return new ScoreServiceImpl(); case Bean.SEMESTER_SERVICE: return new SemesterServiceImpl(); default: throw ‘错误,未注册的bean’; } }}总结Spring Boot真厉害,什么时候我们也能写出如此优秀的框架? ...

April 3, 2019 · 2 min · jiezi

名称验重

本周写的项目需要对名称进行验重,采用的是angular的异步验证器的方式.后台接口首先要查询数据库表中是否存在相同名称,需要一个验证重复的后台接口:/** * @description 是否存在姓名 * @param name 姓名 * @return java.lang.Boolean * @author htx * @date 下午7:19 19-3-28 / @GetMapping(“existByName/{name}”) public Boolean existByName(@PathVariable String name) { return majorService.existsByName(name); }只需要查询数据库中是否存在该名称,返回true或false即可.前台验证因为在编辑和添加时都需要验证,于是就把获取验证方法写在了服务里,从服务里获取验证方法:/ * @description 获取名称是否重复的验证器 * @param name 当name存在时 默认跳过name(用于编辑可能保留原名称) * @return AsyncValidatorFn 异步验证器 * @author htx * @date 下午7:53 19-3-28 / getValidatorNameExistFn(): AsyncValidatorFn { return (control: AbstractControl): Observable<ValidationErrors | null> => { return this.existByName(control.value).pipe( map(exist => (exist ? {exist: true} : null)), catchError(() => null) ); }; } / * @description 是否存在相同编号名称 * @param name 名称 * @author htx * @date 下午7:45 19-3-28 **/ existByName(name: string): Observable<boolean> { return this.http.get<boolean>(this.baseUrl + ‘/existByName/’ + name); }执行getValidatorNameExistFn()方法就会获取验证名字是否重复的异步验证方法,因为AsyncValidatorFn接口要求返回的是Observable<ValidationErrors | null>,但我们后台接口返回的是true或false,因此需要用map操作符进行转换.在建立控件的时候将验证方法加入异步验证即可:this.validateForm = this.fb.group({ name: [null, [Validators.required], [this.majorService.getValidatorNameExistFn()]], number: [null, [Validators.required], [this.majorService.getValidatorNumberExistFn()]], college: [null], teacherList: [null] });v层中增加提示信息:<nz-form-explain *ngIf=“validateForm.get(’name’).dirty && validateForm.get(’name’).errors?.exist”>专业名称已存在! </nz-form-explain>效果: ...

March 30, 2019 · 1 min · jiezi

在 Angular 中引入 Jest 进行单元测试

在 Angular 中引入 Jest 进行单元测试为什么要从 Karma 迁移到 Jest用 Karma 在项目中遇到了坑最近新换了一个项目,去的时候项目已经做了两个月了,因为前期赶功能,没有对单元测试做要求,CI/CD 的时候也没有强制跑单元测试。所以虽然有用 Angular CLI 自动生成的测试文件,但是基本上都是测试不通过。项目做久了,人员变动多,新来的成员对之前的业务逻辑不清不楚,稍不注意就会破坏之前的功能;业务复杂了,随便增加或者修改一点点功能都可能引起不易被察觉的 BUG。作为一个敬业的开发,不上单元测试怎么行。所以,就有了一个修复已有单元测试的任务。修复已有测试文件的思路很简单:写个 TestingModule 把常用的依赖 mock 掉,再引入到需要的文件中就行了;不常用的依赖,在各自的文件中 mock 掉就好了。然而实际操作起来的时候,Karma 早早挖好坑等这了。有些测试文件单跑没有问题,整体跑得时候就报错,测试结果及其不稳定;karma 的报错信息又特别难读懂,很多时候根本定位不到到底是哪里出了问题。再加上 Karma 需要先把 Angular 应用编译之后再在浏览器中跑测试,整体时间也比较慢,修复的过程一直处于抓狂的边缘。整体测试跑起来的时候难以定位测试出错的定位,怎么办呢,那就让跑整个测试的时候各个文件之间也没有依赖可以单独跑好了,所以就想到了 Jest。实践证明,在 Angular 中, Jest 大法也非常好使。Karma 和 Jest 的对比前面也说过了,在修复测试的过程中,karma 遇到了各种各样的问题。归结起来大概就是:Karma 需要先把 Angular 应用整体编译之后再在浏览器中跑测试,跑测试的时间比较长;Karma 测试结果不稳定(很可能是因为异步操作引起的),单个文件和整体测试时的测试结果不一致;报错信息模糊不清,无法定位问题。特别是在有大量测试需要修复的情况下,难以定位问题的根本原因。那么对比而言,Jest 在上面这些方面都有很好的表现:不需要整体编译,可以单文件测试测试结果稳定报错清楚,易于定位问题除了这些,Jest 还有的好处有:开箱即用,基本算是全家桶,包含了测试需要的大部分工具:测试结构、断言、spies、mocks直接提供了测试覆盖率报告快照测试非常强大的模块级 mock 功能watch 模式仅仅测试和被修改文件相关的测试,速度非常快迁移第一步,你需要相关依赖包:npm install –save-dev jest jest-preset-angular @types/jest其中:jest – Jest 测试框架jest-preset-angular – jest 对于 angular 的一些通用的预设置@types/jest – Jest 的 typings第二步,你需要在 package.json 中对 Jest 进行配置:“jest”: { “preset”: “jest-preset-angular”, “setupFilesAfterEnv”: ["<rootDir>/src/setupJest.ts"]}其中,preset 声明了预设,setupFilesAfterEnv 配置了 Jest setup 文件的地址,可以包含多个文件,这里设置的是项目根目录下的 src/setupJest.ts。第三步,在 src 目录下创建上一步中设置的 setup 文件 setupJest.tsimport ‘jest-preset-angular’; // jest 对于 angular 的预配置import ‘./jestGlobalMocks’; // jest 全局的 mock第四步,在 src 目录下创建 jestGlobalMocks.ts 文件,并加入相关的全局的 mock,以下是一个例子:const mock = () => { let storage = {}; return { getItem: key => key in storage ? storage[key] : null, setItem: (key, value) => storage[key] = value || ‘’, removeItem: key => delete storage[key], clear: () => storage = {}, };};Object.defineProperty(window, ’localStorage’, {value: mock()});Object.defineProperty(window, ‘sessionStorage’, {value: mock()});Object.defineProperty(window, ‘getComputedStyle’, { value: () => [’-webkit-appearance’]});可以看到这个例子中 mock 了 window 上的对象,这是因为 jsdom 并没有实现所有的 window 上的对象和方法,所以有时我们需要自己给 window 打个补丁。在这里 mock localStorage 是可选的,如果我们在代码中并没有使用。但是 mock getComputedStyle 是必须的,因为 Angular 会检查它在哪个浏览器中执行。如果没有 mock getComputedStyle,我们的测试代码将无法执行。接下来,我们就可以在 package.json 的 script 中配置 test 的命令了:“test”: “jest”,“test:watch”: “jest –watch”,其中 test 只跑一次测试,test:watch 可以检测文件变化,跑当前有修改的文件的相关测试。此时,在命令行中运行测试命令,就应该能够顺利把测试跑起来并通过了。如果没有通过,可能是因为我们在 src/tsconfig.spec.json 中的 file 配置中有 test.js 的配置,这是 Karma 的 setup 文件,删掉这行配置并删除对应的文件,(src/tsconfig.app.json 中出现的 test.js 也可一并删除),重新跑一遍测试命令:npm run test至此,Jest 测试环境就算顺利搭建好了。如果你对代码有洁癖,接下来,你还可以删除 Karma 的相关代码,将测试全部转为 Jest。删除 Karma 相关代码删除相关依赖包(@types/jasmine @types/jasminewd2 jasmine-core jasmine-spec-reporter 因为在 e2e 测试中有使用所以不能删除):npm uninstall karma karma-chrome-launcher karma-coverage-istanbul-reporter karma-jasmine karma-jasmine-html-reporter删除文件 src/karma.config.js删除 angular.json 中 test 的配置src/tsconfig.spec.json 中 compilerOptions.type 的配置移除 jasmine, 加上 jest。至此,你已经删除了所有与 Karma 相关的代码。你甚至还能将测试断言换成 jest 的风格。查看最后生成的代码库和相关文件配置参考:Angular 6: “ng test” with Jest in 3 minutesTESTING ANGULAR FASTER WITH JEST ...

March 30, 2019 · 2 min · jiezi

angular单元测试遇到the icon user-o does not exist or is not registered

这篇博客本不应由我写,但由于团队要求每周最少一篇,而这周又实在不知道写啥,又正好我也遇到了这个问题。所以我就“抢”了过来……在此,感谢潘哥!团队在上周才启用前台的单元测试,对于解决前台的单元测试的错误还十分不成熟,遇到错误时经常一脸懵逼,这次的这个问题也是在潘老师的帮助下才解决的。错误描述在前台运行单元测试时出现了如下错误the icon user-o does not exist or is not registered…看意思是图标没有找到,但是运行的时候却是能够看到图标的,应该怎么解决呢?解决办法之所以找不到,是因为找的地方不对测试时默认是去/src/assets寻找,但是我们引用的图片不在那,自然找不到。比如在本项目中修改成下面这样就行了解决问题很重要,但更重要的是要知道为什么要这样解决,这也是为什么别人能解决一个bug,而自己对此却束手无策的原因。只有真正理解了大佬debug的思路,自己才能获得真正的提升,下面就让我们来看看潘老师的网上教学。为什么要这样?两点疑惑不是太懂这个单元测试的思路,为什么以前的单元测试就能通过,新写的功能也完全没调用与之相关的模块,却无法通过了。不知道前台的单元测试的意义是什么,也没有测出你写的方法的正确与否,对此应该是因为我们前台单元测试还使用的相当浅薄,并且前后台都写,不像公司里面前台就只有前台吧。

March 28, 2019 · 1 min · jiezi

通过超级直观的图表学习合并Rxjs

内容来自于Max Koretskyi aka Wizard的《Learn to combine RxJs sequences with super intuitive interactive diagrams》在足够复杂的应用程序上工作时,通常会有来自多个数据源的数据。它可以是一些多个外部数据点。序列合成是一种技术,通过将相关流组合成一个流,可以跨多个数据源创建复杂的查询。RxJs提供了各种各样的操作符,可以帮助你做到这一点,在本文中,我们将看看最常用的操作符。下面是我将在接下里的文章里将会用到的图表的样例:同时合并多个序列我们将要看到的第一个操作符是merge。该运算符将多个可观察流组合在一起,同时从每个给定的输入流中发出所有值。当所有组成这个流的输入流产生值的时候,这些值都会作为合成流的结果被发出。这个过程在文档中经常被称为扁平化。当所有输入流都结束了,那这个流就结束了。任何一个输入流引发了错误,则这个流引发错误。只要有一个流没有完成,则这个流就不会完成。如果您不关心排放顺序,只关心来自多个组合流的所有值,就像它们是由一个流产生的一样,请使用此运算符。在下图中,你可以看到merge合并了A,B两个流,每一个流都产生了3个值,当值被发出的时候,值会落入合成流中,最终由合成流发出。下面是演示代码:const a = stream(‘a’, 200, 3, ‘partial’);const b = stream(‘b’, 200, 3, ‘partial’);merge(a, b).subscribe(fullObserver(‘merge’));// can also be used as an instance operatora.pipe(merge(b)).subscribe(fullObserver(‘merge’));顺序连接多个序列接下来我们要讲到的操作符是concat。它将所有的输入流串联起来,顺序的订阅并发送每一个流的值。一旦当前流完成,它会订阅下一个流,并将输入流发出的值传递到结果流中。当所有输入流完成时,该流完成,如果某些输入流引发错误,将引发错误。如果一些输入流没有完成,它将永远不会完成,这也意味着一些流将永远不会被订阅。如果排放顺序很重要,并且您希望首先看到由您首先传递给操作符的流发送的值,请使用此运算符。例如,您可能有一个从缓存传递值的可观察序列和另一个从远程服务器传递值的序列。如果您想要合并它们并确保首先传递来自缓存的值,请使用concat。在下图中,您可以看到concat运算符将两个流A和B组合在一起,每个流产生3个值,值首先从A开始,然后从B开始,一直到结果流。下面是演示代码:const a = stream(‘a’, 200, 3, ‘partial’);const b = stream(‘b’, 200, 3, ‘partial’);concat(a, b).subscribe(fullObserver(‘concat’));// can also be used as an instance operatora.pipe(concat(b)).subscribe(fullObserver(‘concat’));多个流竞争接下来我们要讲到的这个操作符race,相当的有趣。它并不是将多个输入流合成一个流输出,而是多个流竞争,一旦有一个输入流最先发出值,那其他流将被取消订阅并完全忽略。当选定的输入流完成时,结果流完成,如果这个流出错,将抛出一个错误。如果内部流不完成,它也永远不会完成。如果你有多个可以提供价值的资源,例如世界各地的服务器,该运算符可能会很有用,但是由于网络条件的原因,延迟是不可预测的,并且变化很大。使用这个运算符,你可以将同一个请求发送到多个数据源,并使用第一个响应的结果。在下图中,您可以看到race操作符将两个流A和B组合在一起,每个流产生3个项目,但是只有流A中的值被发出,因为这个流首先开始发出值。下面是演示代码:const a = intervalProducer(‘a’, 200, 3, ‘partial’);const b = intervalProducer(‘b’, 500, 3, ‘partial’);race(a, b).subscribe(fullObserver(‘race’));// can also be used as an instance operatora.pipe(race(b)).subscribe(fullObserver(‘race’));组合为止数量的流和高阶可观察对象之前讲到的操作,都只能组合已知数量的流。但是如果您事先不知道所有的流,并且想要合并可以在运行时延迟评估的流,会怎么样呢?事实上,这是使用异步代码时非常常见的情况。例如,对某些资源的网络调用可能会导致由原始请求的结果值决定的许多其他请求。RxJs有我们在上面看到的操作符的变体,这些操作符采用一系列序列,被称为高阶Observable或Observable。MergeAll该运算符组合所有发出的内部流,就像普通合并一样,同时从每个流中生成值。在下图中,你将看到一个高阶流H,它发出两个内部类A和B。mergeAll运算符将这两个流中的值组合起来,然后在它们发出值时将它们传递给结果流。下面是演示代码:const a = stream(‘a’, 200, 3);const b = stream(‘b’, 200, 3);const h = interval(100).pipe(take(2), map(i => [a, b][i]));h.pipe(mergeAll()).subscribe(fullObserver(‘mergeAll’));ConcatAll该运算符将所有发出的内部流组合起来,就像普通concat一样,从每个流中顺序生成值。在下图中,您可以看到产生两个内部流A和B的高阶流H。串联运算符首先从A流中获取值,然后从流B中获取值,并将它们传递给结果序列。下面是演示代码:const a = stream(‘a’, 200, 3);const b = stream(‘b’, 200, 3);const h = interval(100).pipe(take(2), map(i => [a, b][i]));h.pipe(concatAll()).subscribe(fullObserver(‘concatAll’));SwitchAll有时从所有内部Observable中接收值不是我们需要的。在某些情况下,我们可能只对最新内部序列中的值感兴趣。搜索功能是一个很好的例子。当用户在输入框输入一些值后,我们想服务器发送一些网络请求,但这些网络请求是异步的。如果用户在返回结果之前又更新了输入框中的值,会发生什么?第二个网络请求被发送了出去,所以现在我们已经向服务器发送了两个搜索的网络请求。然而,我们对第一次搜索的结果已经不感兴趣了,并且,如果将两次搜索结果都显示给用户,这将不符合我们的设想。所以我们使用switchAll操作符,它只会订阅最新的内部流并产生值,并忽略之前的流。在下图中,您可以看到产生两个内部流A和B的高阶流H。开关操作符首先从A流中获取值,然后从B流中获取值,并将它们传递给结果序列。下面是演示代码:const a = stream(‘a’, 200, 3);const b = stream(‘b’, 200, 3);const h = interval(100).pipe(take(2), map(i => [a, b][i]));h.pipe(switchAll()).subscribe(fullObserver(‘switchAll’));concatMap,mergeMap,switchMap有趣的是,这些映射操作符concatMap,mergeMap,switchMap的使用频率比和他们相对应的concatAll,‘mergeMap’,switchAll要高得多。然而,如果你仔细想想,它们几乎是一样的。所有的*Map操作符都是由两个parts — producing流通过映射和使用组合逻辑,在由高阶Observable产生的内部流上进行观察。让我们来看看下面熟悉的代码,它演示了mergeAll运算符是如何工作的:const a = stream(‘a’, 200, 3);const b = stream(‘b’, 200, 3);const h = interval(100).pipe(take(2), map(i => [a, b][i]));h.pipe(mergeAll()).subscribe(fullObserver(‘mergeAll’));这里的map操作符产生Observable流,mergeAll合并这些Observable流。所以我们可以使用mergeMap轻松替代mergeAll。const a = stream(‘a’, 200, 3);const b = stream(‘b’, 200, 3);const h = interval(100).pipe(take(2), mergeMap(i => [a, b][i]));h.subscribe(fullObserver(‘mergeMap’));这两个结果是完全一样的。concaMap和switchMap操作也是如此。你可以自己尝试一下。配对序列组合前面的操作符允许我们展平多个序列,并通过结果流不变地传递来自这些序列的值,就好像它们都来自这个序列一样。接下来我们要看的这组运算符仍然将多个序列作为输入,但不同之处在于它们将每个序列的值配对,为输出序列产生一个组合值。每个运算符都可以选择一个所谓的投影函数作为最后一个参数,该参数定义了结果序列中的值应该如何组合。在我的示例中,我将使用默认的投影函数,该函数简单地使用逗号作为分隔符来连接值。在这一节的最后,我将展示如何提供一个定制的投影函数。CombineLatest我们要看到的第一个操作符是combineLatest。它允许您从输入序列中获取最新的值,并将这些值转换为结果序列的一个值。RxJs缓存每个输入序列的最后一个值,一旦所有序列产生了至少一个值,它就使用从缓存中获取最新值的投影函数来计算结果值,然后通过结果流发出该计算的输出。如果任何一个内部流不完成,它将永远不会完成。另一方面,如果任何一个流不发出值而是完成了,则结果流将在同一时刻完成而不发出任何信号,因为现在不可能在结果序列中包含来自完成的输入流的值。此外,如果某个输入流不发出任何值并且永远不会完成,combineLatest也永远不会发出并且永远不会完成,因为它将再次等待所有流发出某个值。如果您需要评估一些状态组合,而这些状态组合需要在部分状态发生变化时保持最新,那么这个运算符会很有用。一个简单的例子就是监控系统。每个服务都由一个返回布尔值的序列表示,该值指示所述服务的可用性。如果所有服务都可用,则监控状态为绿色,因此投影功能只需执行逻辑“与”。在下图中,你可以看到combineLatest操作组合了两个流A和B。一旦所有流都发射了至少一个值,每个新发射通过结果流产生一个组合值。下面是实例代码:const a = stream(‘a’, 200, 3, ‘partial’);const b = stream(‘b’, 500, 3, ‘partial’);combineLatest(a, b).subscribe(fullObserver(’latest’));Zip这个操作符也是一个非常有趣的合并操作符,它在某种程度上类似于衣服或袋子上拉链的机械结构。它将两个或多个相应值的序列集合成一个元组(在两个输入流的情况下是一对)。它等待从所有输入流中发出相应的值,然后使用投影函数将它们转换成单个值并发出结果。只有当每个源序列中有一对新值时,它才会发布,因此如果其中一个源序列发布值的速度快于另一个序列,发布速率将由两个序列中较慢的一个决定。当任何内部流完成并且相应的匹配对从其他流发出时,结果流完成。如果任何内部流没有完成,它将永远不会完成,如果任何内部流出错,它将抛出一个错误。该运算符可方便地用于实现一个流,该流产生一系列具有间隔的值。以下是投影函数仅从range流返回值的基本示例:zip(range(3, 5), interval(500), v => v).subscribe();在下图中,您可以看到zip运算符将两个流A和B组合在一起。一旦对应的流对匹配,结果序列就会产生一个组合值:以下是示例代码:const a = stream(‘a’, 200, 3, ‘partial’);const b = stream(‘b’, 500, 3, ‘partial’);zip(a, b).subscribe(fullObserver(‘zip’));forkjoin有时,您有一组流,只关心每个流的最终发射值。通常这种序列只有一次发射。例如,您可能希望发出多个网络请求,并且只希望在收到所有请求的响应后采取措施。在某种程度上,它类似于Promise.all的功能。但是,如果您有一个发出多个值的流,除了最后一个值之外,这些值将被忽略。当所有内部流完成时,生成的流只发出一次。如果任何内部流没有完成,它将永远不会完成,如果任何内部流出错,它将抛出一个错误。在下图中,您可以看到forkJoin运算符将两个流A和b组合在一起。一旦对应的流对匹配,结果序列就会产生一个组合值:下面是示例代码:const a = stream(‘a’, 200, 3, ‘partial’);const b = stream(‘b’, 500, 3, ‘partial’);forkJoin(a, b).subscribe(fullObserver(‘forkJoin’));WithLatestFrom我们在本文中最后要看的运算符是withLatestFrom。当您有一个引导流,但还需要来自其他流的最新值时,使用该运算符。在某种程度上,它类似于combineLatest操作符,每当任何输入流有新的排放时,都会发出新的值。withLatestFrom只有在引导流发出值后,才会发出新值。正如combineLatest一样,它仍然等待来自每个流的至少一个发射值,并且当引导流完成时,可以在没有单个发射的情况下完成。如果引导流没有完成,它将永远不会完成,如果任何内部流出错,它将抛出一个错误。在下图中,您可以看到withLatestFrom运算符将两个流A和流B组合在一起,其中流B是引导流。每当流B发出一个新值时,产生的序列使用来自流A的最新值产生一个组合值。下面是示例代码:const a = stream(‘a’, 3000, 3, ‘partial’);const b = stream(‘b’, 500, 3, ‘partial’);b.pipe(withLatestFrom(a)).subscribe(fullObserver(’latest’));Projection function(投影函数)如本节开头所述,通过配对组合值的所有运算符都采用可选的投影函数。该函数定义结果值的转换。使用此函数,您可以选择只从特定的输入序列中发出一个值,或者以任何您想要的方式连接值:// return value from the second sequencezip(s1, s2, s3, (v1, v2, v3) => v2)// join values using dash as a separatorzip(s1, s2, s3, (v1, v2, v3) => ${v1}-${v2}-${v3})// return single boolean resultzip(s1, s2, s3, (v1, v2, v3) => v1 && v2 && v3) ...

March 28, 2019 · 2 min · jiezi

ng6表单动态拖拽demo

github[图片]

March 26, 2019 · 1 min · jiezi

Angular变化检测的简介

内容来自于Max Koretskyi aka Wizard的《A gentle introduction into change detection in Angular》初次相遇让我们从一个简单的Angular组件开始。他表现应用程序的变化检测。这个时间戳的精度为毫秒。点击triggers按钮触发检测:@Component({ selector: ‘my-app’, template: &lt;h3&gt; Change detection is triggered at: &lt;span [textContent]="time | date:'hh:mm:ss:SSS'"&gt;&lt;/span&gt; &lt;/h3&gt; &lt;button (click)="0"&gt;Trigger Change Detection&lt;/button&gt; })export class AppComponent { get time() { return Date.now(); }}如你所见,这是相当基本的。有一个名为time的getter返回当前时间戳。并且,我将它绑定到HTML中的span元素。当Angular运行变化检测时,它获取time属性的值,通过日期管道传递它,并使用结果更新DOM。这一切都很正常,但当我打开控制台的时候,我看到了一个错误:ExpressionChangedAfterItHasBeenCheckedError。事实上,这让我们感到非常惊讶。通常这个错误出现在更加复杂的程序上。但为什么一个如此简单的功能会导致这个错误呢?别担心,我们现在就来查看他的原因。让我们先从错误消息开始:Expression has changed after it was checked. Previous value: “textContent: 1542375826274”. Current value: “textContent: 1542375826275”.它告诉我们,textContent绑定的值是不同的。的确,毫秒不相同。因为Angular通过表达式time | date:‘hh:mm:ss:SSS’计算了两次,并比较了结果。它检测到了两次值的差异,这就是导致错误的原因。但Angular为什么要这样做?或者它什么时候做的?在我们了解这些问题的答案之前,我们还需要了解另外一些东西。组件视图和绑定Angular的变化检测主要有两个部分:组件视图相关绑定每一个Angular的组件都有一个HTML元素。当Angular创建DOM节点并将内容渲染到屏幕上,它需要一个地方来储存DOM节点的引用。为了实现这一目标,Angular内部有一个被称为View的数据结构。它还用于存储对组件实例的引用和绑定表达式之前的值。并且视图和组件之间的关系是一一对应的。下图展示了该关系:当编译器分析模板时,它会辨识在变化检测期间可能需要更新的DOM元素属性。每一个这样的属性,编译器都会创建一个绑定。绑定定义要更新的属性名和Angular用来获取新值的表达式。在我们的例子当中,time属性用于textContent的表达式中。所以,Angular会创建绑定来连接它和span元素。实际上,绑定不是包含所有必要信息的单个对象。viewDefinition定义模板元素和要更新的属性的实际绑定。用于绑定的表达式在updateRenderer方法中。*检查组件视图如你所知,Angular会对每一个组件执行变化检测。现在我们知道每个组件在Angular内部被称为视图(view),我们可以说Angular对每个视图执行了变化检测。当Angular检查视图时,它只需运行编译器为视图生成的所有绑定。它计算表达式并将它们的结果与视图上旧值数组中存储的值(oldValues)进行比较。这就是脏检查这个名字的由来。如果检测到差异,它会更新与绑定相关的DOM属性。它还需要将新值放入视图的旧值数组中。就这样。您现在有了更新的用户界面。一旦完成当前组件的检查,它将对子组件重复完全相同的步骤。在我们的应用程序中,在App组件中span元素的属性textContent只有一个绑定。所以在变化检测期间,Angular会读取组件time属性的值,再使用date管道,并将它与视图中存储的先前值进行比较。如果检测到不同,Angular会更新span旧值(oldValues)数组中的textContent属性.但是错误又从哪里出来的呢?在开发模式下,每个变化检测周期之后,Angular会同步运行另外一个检查,已确保表达式产生的值与之前变化检测运行期间的值相同。该检查不是原始检查的一部分,它在对整个组件树的检查完成后运行,并执行完全相同的步骤。然而,当这一次变化检测期间,如果检测到不同那个的值,Angular不会去更新DOM,相反的,它会直接抛出错误ExpressionChangedAfterItHasBeenCheckedError。但是Angular为什么要这样做?现在我们知道什么时候抛出错误了。但是为什么Angular需要这个检测。假设在变化检测运行期间,又有一些组件的一些属性被更新。此时,表达式产生的新值与用户界面中呈现的值不一样。这个时候Angular应该怎么做?它当然也可以另外再运行一个变化检测周期来使应用程序状态与用户界面同步。但如果在这期间,又有一些属性被更新了呢?看到问题了吗?实际上Angular可能会在变化检测的无限循环中结束。这种情况在AngularJS中经常发生。为了避免这种事情,Angular强制让数据单向流动。这种在变更检测和结果表达式变更后运行的检查是强制机制。一旦Angular处理了当前组件的绑定,就不能再更新绑定表达式中使用的组件属性。

March 26, 2019 · 1 min · jiezi

使用Angular CLI时的6个最佳实践和专业技巧 学习总结

原文地址组织项目文件结构三个不同的模块: Core(核心模块), Shared(共享模块) 和 Feature(特性模块);核心模块(CoreModule):所有服务都应该在核心模块实现。典型的例子比如认证服务或用户服务。共享模块(ShareModule):所有的简单组件和管道都应该在这里实现。共享模块没有任何依赖于我们的应用程序的其余部分。比如Angular Material 等其他UI组件库的按需导入然后重导入;特性模块(FeatureModule):我们将为应用程序的每一个独立特性创建多个特性模块。特性模块应该只能从核心模块导入服务。如果特性模块A需要从特性模块B导入服务,可以考虑将该服务迁移到CoreModule。在某些情况下,需要只是某些功能模块共享的服务,将它们移动到核心是没有意义的。在这种情况下,我们可以创建特殊的共享功能模块,如本文后面所述。 遵循的经验是: 我们创建的功能模块尽量不依赖其他功能模块,仅仅服务由CoreModule提供,组件由SharedModule提供这将保持我们的代码干净,易于维护和扩展的新功能。它还减少了重构所需的工作量。如果遵循得当,我们将确信对一个功能的更改不会影响或破坏我们的应用程序的其余部分。延迟加载:我们应该尽可能延迟加载我们的功能模块。理论上,只有一个功能模块应该在应用程序启动时同步加载以显示初始内容。每个其他功能模块应该在用户触发导航后缓慢加载。只有设计作为着陆的模块是需要立即加载的,其他模块应该使用懒加载app 和 environments 的别名使用避免导入路径过长 例如import { SomeService } from ‘../../../core/subpackage1/subpackage2/some.service’。解决方案在tsconfig.json中使用别名路径;#tsconfig.json { “compilerOptions”: { “…”: “reduced for brevity”, “baseUrl”: “src”, “paths”: { “@app/”: [“app/”], “@env/”: [“environments/”] } } }SASS默认情况下不添加stylePreprocessorOptions includePaths,我们可以自己设置成根目录 “./” 和 可选的 “./themes”。#angular.json{ “apps”: [ { “…”: “reduced for brevity”, “stylePreprocessorOptions”: { “includePaths”: ["./", “./themes”] } } ]}构建阶段Target Production 这是一个标志能使代码压缩,以及还有很多默认的有用的构建标志。使用如下:–environment prod —使用 environment.prod.ts 文件设置环境变量–aot —预编译,提前编译. 这将在未来的Angular CLI是默认设置,但是现在我们必须手动启动。–output-hashing all — 将生成的文件的散列内容添加到文件名中,以方便浏览器缓存破坏(文件内容的任何更改都会导致不同的哈 希值,因此浏览器被迫加载新版本的文件)–extract-css true — 将所有的css提取到到单独的样式表文件–sourcemaps false — 禁用source maps的生成–named-chunks false — 禁用使用可读的名字,用数字替代Other useful flags–build-optimizer — 新的功能,导致更小的捆绑,但更长的构建时间,所以慎用!(将来也应该默认启用)–vendor-chunk — 将所有第三方(库)代码提取到单独的块中去官方文档检查其他有用的配置项,也许在个人项目中用得上。使用标准的提交信息 & 自动生成更新日志standard-version这个工具根据 [Conventional-Commits-specification][3] 把所有提交自动生成和更新changelog.md文件并且正确地确定我们项目的新版本。如何使用npm install -D standard-version, 然后添加脚本"release": “standard-version"到package.json文件中。例如:“release”: “standard-version && git push — follow-tags origin master && npm publish” ...

March 18, 2019 · 1 min · jiezi

项目debug记录

在写前台获取数据时,报了如下错误:看了一下好像是拦截器那报的错,去看拦截器报错的地方:发现也没啥问题,就后台一步一步debug,发现确实有返回值,但为啥前台接受不到呢?然后我重新启动了一次项目,查看后台日志,发现了如下情况:意思是json序列化了,去检查控制器,发现果然没有加jsonView,加上jsonView后,问题解决但为什么json序列化,前台却是拦截器报错呢?后来朴世超组长告诉我说json序列化的数据太长了,后台没有给响应,而前台拦截器长时间接收不到响应,就报错了。虽然听懂了一些,但还是一知半解。总结感觉自己还是有好多知识盲区,即使知道怎样解决问题,但是具体原理还是不清楚,就是凭经验解决。

March 16, 2019 · 1 min · jiezi

labmda表达式和普通函数

在使用ng-zorro的上传文件组件时,因为要自定义上传方式,找到他的一个属性,需要穿对应的方法进去让他执行,于是就写了一个上传方法:/** * 导入学生信息 */ importItems(item: UploadXHRArgs) { const formData = new FormData(); formData.append(‘multipartFile’, item.file as any); return this.studentService.batchImport(formData).subscribe(() => { this.reloadData(); this.message.create(‘success’, ‘上传成功!’); }, () => { this.message.create(’error’, ‘上传失败!’); }); }结果一上传,居然报了个batchImport’ of undefined的错.我把this对象打出来后,发现this对象是这样的:而我们组件对象应该是这样的:打出来后明白了,应该是将函数传递过去后执行环境变了,this对象也就不同了,但是这要怎么解决呢?回文档仔细查看,发现了忽略的字:他是要求用labmda表达式的,改成labmda表达式后,果然成功了。上网一搜,发现了labmda表达式与普通函数的不同:箭头函数和普通函数有一个微妙的区别,那就是箭头函数没有它们自己的 this,箭头函数中的 this 值始终来自闭包所在的作用域。如果你在箭头函数引用了this、arguments或者参数之外的变量,那它们一定不是箭头函数本身包含的,而是从父级作用域继承的原来如此,箭头函数本身就为一个闭包操作,我们使用箭头函数之后就不用写闭包,也能保留我们想要的作用域了。

March 16, 2019 · 1 min · jiezi

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

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

March 15, 2019 · 1 min · jiezi

angular组件样式不生效

在本周的项目中有一个需求,对处于两种不同状态的计算机列表赋予不同的颜色方便更加醒目的区分,完工后的效果如下。解决思路——rowClassName项目使用了ng-alain,作者提供了许多方便的方法与属性,所以解决这个问题并不太难,张喜锁学长直接给我说了使用rowClassName这个属性。0开始的时候我以为这个类名是对象那个类,还在思考应该怎么写,后来听张喜硕学长一说才明白,这是css的那个类,那就简单了。再写出相应的判断方法/** * @Description: 设置行背景 * 返回class名 */ public setRowColor(record: Host, index: number) { if (record.status === 1) { return ‘greenRow’; } }css.greenRow { background-color: #52c41a;}大功告成,查看测试效果——没有效果……怎么回事?难道思路是错的?打开开发者工具,class已经附上了,但是却没有相应的css失败的原因我猜测产生上面这种现象的原因是,是因为组件的样式进行了封装,而这种给他css的方式时,它没有引用相应的css的文件如果猜测有误,请指出,不胜感激。关于封装封装模式有三种,分别是:Native 原先浏览器Shadow DOM行为。Emulated 仿真模式,通过Angular来模拟类似Shadow DOM的行为。None 无任何封装行为。想更加深入的了解,可以看这篇文章。解决办法难道我们没有办法侵犯第三方组件了吗?好在 Angular 提供了一种对未来工具更好兼容性的命令 ::ng-deep 来强制样式允许侵入子组件。css修改如下:::ng-deep .greenRow { background-color: #52c41a;}成功了(写到这里不得不提一句,实际上要完成需求根本不需要这样麻烦,ng-alain内部已经提供了颜色的样式, 直接使用即可。)Shadow DOMangualr的实际上是Shdow Dom.Shadow DOM它允许在文档(document)渲染时插入一棵DOM元素子树,但是这棵子树不在主DOM树中。因此开发者可利用Shadow DOM 封装自己的 HTML 标签、CSS 样式和 JavaScript 代码。关于Shadow DOM的具体部分可以看这篇文章神奇的Shadow DOM

March 15, 2019 · 1 min · jiezi

angular 实现下拉列表组件

需求:方案一最开始就是用最简单的方法,前台请求数据,然后通过select和option在页面上显示,但是写了一会儿发现出现了许多类似下面的重复的代码:// 初始化年级选项initGradeOptions() { this.gradeService.getAll().subscribe((res) => { this.gradeOptions = res; }, () => { console.log(‘get gradeOption error’); });}<nz-select nzPlaceHolder=“请选择所属年级” formControlName=“grade”> <nz-option *ngFor=“let grade of gradeOptions” [nzLabel]=“grade.name” [nzValue]=“grade”></nz-option></nz-select>每写一个列表都要写请求它的数据的方法和模板中的内容,非常繁琐。方案二因为在项目中,不止一个地方用到了这样的列表,所以就想着把这些列表单独拿出来,写成组件。这里就参考了朴世超组长的angular的输入与输出写了这个组件思路大概如下:ts:@Input() defaultValue: Grade; // 选中的值@Output() selected = new EventEmitter<number>(); // 输出属性datas: Grade[]; // 所有数据constructor(private gradeService: GradeService) {}// 请求所有的数据ngOnInit() { this.gradeService.getAll().subscribe((res) => { this.datas = res; }, () => { console.log(’error’); });}// 当则内容更改时,将已选中对象的id弹射到父组件绑定的事件上dataChange() { this.selected.emit(this.defaultValue);}html:<nz-select nzPlaceHolder=“所属年级” class=“wide” [(ngModel)]=“defaultValue” (ngModelChange)=“dataChange()"><nz-option *ngFor=“let data of datas” [nzLabel]=“data.name” [nzValue]=“data”></nz-option> </nz-select>ps: 默认选中的功能还在完善,待更新思考当我照着上面的套路继续写collegeList,majorList,klassList,以后还会有teacherList,studentList等等,这样不也形成了很多重复的代码吗?于是我就想能不能设计一个组件:我让它是什么列表,它就是什么列表。然后我就寻找这几个组件的共性,发现它们请求数据的的特点:都是使用get请求返回的数据都是数组url只有最后一项不同那么,我只要传给组件一个url数组,就能根据url请求对应的数据,再生成相应的模板方案三(失败)子组件ts:@Input() urls: String[][] = []; // 保存传递过来的urldatas: String[][] = []; // 保存查询结果@Input() titles: String[][] = []; // 保存提示语句@Output() selectItems = new EventEmitter(); // 已选中的对象index = 0;items = [];constructor(public dataService: DataService) {}ngOnInit() { this.getData(this.index);}getData(index: number): void { if (index < this.urls.length) { const url = this.urls[index]; this.dataService.getAllData(url).subscribe((res) => { this.datas[index] = res; console.log(this.datas); }, () => { console.log(’error’); }); }}dataChange(i: number) { console.log(this.items); this.selectItems.emit(this.items); this.getData(i + 1);}子组件html:<nz-select [nzPlaceHolder]=“titles[i]” style=“width: 150px;” (ngModelChange)=“dataChange(i)” [(ngModel)]=“items[i]” *ngFor=“let url of urls,let i = index”><nz-option *ngFor=“let item of datas[i]” [nzValue]=“item” [nzLabel]=“item.name”> </nz-option></nz-select>父组件ts:url = [‘Grade’, ‘College’, ‘Major’];titels = [‘年级’, ‘学院’, ‘专业’];getSelectItems(event) { console.log(event);}父组件html: <app-grade-list [urls]=“url” [titles]=“titels” (selectItems)=“getSelectItems($event)"> </app-grade-list>效果:看起来还能用,但是再往后写就发现这样写有致命的缺陷。每一个下拉框都是根据url生成的,使用时需要查找url传给父组件的数据不知道数据与实体的对应关系当存在级联时,使用该方案难度高,且不易维护。总结虽然这些下拉列表有一定的共性,并且可以抽象出一些公共的功能来实现,但本身设计略复杂,且使用效果并不好,最后还是放弃了第三个方案。 ...

March 9, 2019 · 1 min · jiezi

前台Ng-Alain

对与Ng-Alain这个前段框架,用了也有一短时间了,对于框架而言确实时很强大,很好,但对于我们而言文档少,就寸步难行,边研究编写,时不时的就在坑里爬半天,还不一定爬出来。问题描述点击多选框时,选择是正确的,但是点击表格的其他地方值就变成了null,感到很奇怪,发现官方的就正常,不会出现我这种情况。想法首先想到的是去看文档,但是很遗憾,没有找到文档还好有github可以看到示例代码,比较哪里与官方的不一样。解决发现官方上的 (change)事件是这样写的:stChange(e: STChange) { switch (e.type) { case ‘checkbox’: this.selectedRows = e.checkbox; this.totalCallNo = this.selectedRows.reduce((total, cv) => total + cv.callNo, 0); this.cdr.detectChanges(); break; case ‘filter’: this.getData(); break; } }思考:为什么要这样写,这样写的原因是什么?在控制台打印了一下e,发现原来鼠标点击checkbox和点击表格还有在表格中双击所触发的事件不同:修改后: public selectHost(e: STChange) { switch (e.type) { // 如果是多选框事件,选中主机 case ‘checkbox’: this.selectedHosts = e.checkbox; } }完美,没有问题!!!总结在需要这个样式的时候就Ctrl+C、Ctrl+V过来了也没有多看,没用的代码就直接删掉了,但是在发现问题是往往可能是你感觉没用的哪些代码,所有在使用样式的时候大概看一下,这次没用到下次就可能用到,要多看,多总结,到在用的时候就会顺手许多。

March 8, 2019 · 1 min · jiezi

一周总结

又到了一周写汇报的时候,细细想来,这周的状态其实不太好,然后又因为报了驾校,课余时间又得分出去一部分,所以这周的项目进展主要是在靠学长和潘哥的推动,自己只写了几个小功能。就简单的总结一下这周遇到的问题。observable未订阅在项目中写了一个http请求的函数,但是无论如何这个函数就是没有网络请求,后来偶然发现没有订阅observable,订阅一下就可以了。通过这件事牢牢记住了obervable的对象必须订阅。使用timer()遇到的坑timer定时器是很好用的,但由于开始对angular的生命周期和timer不太熟悉,费了好长时间才在学长和潘哥的帮助下解决遇到的一个bug。在说bug之前先简单的说说timer怎么使用。timner的简单用法想要完整的学习,自然得通过官方文档的方式,但只是想简单使用,可以参照下面:先定义一个Subscription定义定时器的操作一个简单的定时器就完成了。突然出现的报错在计算列表为空的情况下会发出带数据的请求。开始怎么检查都觉得代码没问题,找不到他产生的原因,研究了半天,发现他是几个一出现几个一出现,而且几个任务执行的间隔绝对不到定时器执行的时间,学长突然想到了是不是timer不会随着组件销毁而销毁,经过测试果然是这样。angular生命周期既然定时器不能自动销毁,只能靠我们自己销毁了,这时候就要用到ngOnDestroy,当组件销毁时,主动去取消timer的订阅。 /** * 组件销毁方法 */ public ngOnDestroy() { // 取消定时器订阅 this.yunzhiTimer.unsubscribe(); }

March 8, 2019 · 1 min · jiezi

Angular 使用 ControlValueAccessor 创建自定义表单控件

在 Angular 自定义表单控件,有时你想要的输入不是标准的文本输入、选择或复选框。通过实现ControlValueAccessor 接口并将组件注册为 NG_VALUE_ACCESSOR,您可以将自定义表单控件无缝地集成到模板驱动或响应表单中,就像它是本地表单一样!ControlValueAccessorControlValueAccessor 是一个接口,充当Angular API 和 DOM 元素之间的桥梁export interface ControlValueAccessor { writeValue(obj: any) : void registerOnChange(fn: any) : void registerOnTouched(fn: any) : void}writeValue(obj:any)是将表单模型中的值写入视图中。writeValue(value: any): void { this._renderer.setProperty(this.elementRef.nativeElement, ‘value’, value);}registerOnChange(fn:any)是一个方法,用于注册在视图中的某些内容发生更改时应调用的处理程序。它获取一个函数,告诉其他表单指令和表单控件更新其值。registerOnChange(fn: (: any) => void): void { this._onChange = fn;}registerOnTouched(fn:any)与registerOnChange()此类似,它专门为控件接收触摸事件时注册一个处理程序。registerOnTouched(fn: any): void { this._onTouched = fn;}setDisabledState?(isDisabled: boolean): void; 是一个可选的方法,设置自定义表单的状态setDisabledState(isDisabled: boolean): void { this._renderer.setProperty(this._elementRef.nativeElement, ‘disabled’, isDisabled);}AbstractValueAccessor我们可以把 ControlValueAccessor 中的方法写在一个抽象类中,不同的组件可以实现这个基类export abstract class AbstractValueAccessor implements ControlValueAccessor { private _value: any = ‘’; get value(): any { return this._value; } set value(v: any) { if (v !== this._value) { this.value = v; this.onChange(v); this.onTouched(); } } writeValue(value: any) { this.value = value; } onChange = () => {}; onTouched = () => {}; registerOnChange(fn: (: any) => void): void { this.onChange = fn; } registerOnTouched(fn: () => void): void { this.onTouched = fn; }}export function MakeProvider(type: any): { provide: any, useExisting: any, multi: boolean} { return { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => type), multi: true };}Example自定义一个 list 控件,可以选择年级在线预览git仓库 ...

March 7, 2019 · 1 min · jiezi

同样做前端,为何差距越来越大?

阿里妹导读:前端应用越来越复杂,技术框架不断变化,如何成为一位优秀的前端工程师,应对更大的挑战?今天,阿里前端技术专家会影结合实际工作经验,沉淀了五项重要方法,希望能对你的职业发展、团队协作有所启发。过去一年,阿里巴巴新零售事业群支撑的数据相关业务突飞猛进,其中两个核心平台级产品代码量急速增长,协同开发人员增加到数十人。由于历史原因,开发框架同时基于 React 和 Angular,考虑到产品的复杂性、人员的短缺和技术背景各异,我们尝试了各种方法打磨工具体系来提升开发效率,以下分享五点。一、基于 Redux 的状态管理从2013年React发布至今已近6个年头,前端框架逐渐形成 React/Vue/Angular 三足鼎立之势。几年前还在争论单向绑定和双向绑定孰优孰劣,现在三大框架已经不约而同选择单向绑定,双向绑定沦为单纯的语法糖。框架间的差异越来越小,加上 Ant-Design/Fusion-Design/NG-ZORRO/ElementUI 组件库的成熟,选择任一你熟悉的框架都能高效完成业务。那接下来核心问题是什么?我们认为是状态管理。简单应用使用组件内 State 方便快捷,但随着应用复杂度上升,会发现数据散落在不同的组件,组件通信会变得异常复杂。我们先后尝试过原生 Redux、分形 Fractal 的思路、自研类 Mobx 框架、Angular Service,最终认为 Redux 依旧是复杂应用数据流处理最佳选项之一。庆幸的是除了 React 社区,Vue 社区有类似的 Vuex,Angular 社区有 NgRx 也提供了几乎同样的能力,甚至 NgRx 还可以无缝使用 redux-devtools 来调试状态变化。无论如何优化,始终要遵循 Redux 三原则:这三个问题我们是通过自研 iron-redux 库【1】来解决,以下是背后的思考:如何组织 Action?action type 需要全局惟一,因此我们给 action type 添加了 prefix,其实就是 namespace 的概念;为了追求体验,请求(Fetch)场景需要处理 3 种状态,对应 LOADING/SUCCESS/ERROR 这 3 个action,我们通过 FetchTypes 类型来自动生成对应到 3 个 action。如何组织 Store/Reducer?reducer 和 view 不必一一对应,应用中同时存在组件树和状态树,按照各自需要去组织,通过 connect 来绑定状态树的一个或多个分支到组件树;通过构造一些预设数据类型来减少样板代码。对于 Fetch 返回的数据我们定义了 AsyncTuple 这种类型,减少了样板代码;明确的组织结构,第1层是 ROOT,第2层是各个页面,第3层是页面内的卡片,第4层是卡片的数据,这样划分最深处基本不会超过5层。最终我们得到如下扁平的状态树。虽庞大但有序,你可以快速而明确的访问任何数据。如何减少样板代码?使用原生 Redux,一个常见的请求处理如下。非常冗余,这是 Redux 被很多人诟病的原因:使用 iron-redux 后:代码量减少三分之二!!主要做了这2点:引入了预设的 AsyncTuple 类型,就是 {data: [], loading: boolean, error: boolean} 这样的数据结构;使用 AsyncTuple.handleAll 处理 LOADING/SUCCESS/ERROR 这 3 种 action,handleAll 的代码很简单,使用 if 判断 action.type 的后缀即可,源码【2】。曾经 React 和 Angular 是两个很难调和的框架,开发中浪费了我们大量的人力。通过使用轻量级的 iron-redux,完全遵循 Redux 核心原则下,我们内部实现了除组件层以外几乎所有代码的复用。开发规范、工具库达成一致,开发人员能够无缝切换,框架差异带来的额外成本降到很低。二、全面拥抱 TypeScriptTypeScript 目前可谓大红大紫,根据 2018 stateofjs【3】,超过 50% 的使用率以及 90% 的满意度,甚至连 Jest 也正在从 Flow 切换到 TS【4】。如果你还没有使用,可以考虑切换,绝对能给项目带来很大提升。过去一年,我们从部分使用 TS 变为全面切换到 TS,包括我们自己开发的工具库等。TS 最大的优势是它提供了强大的静态分析能力,结合 TSLint 能对代码做到更加严格的检查约束。传统的 EcmaScript 由于没有静态类型,即使有了 ESLint 也只能做到很基本的检查,一些 typo 问题可能线上出了 Bug 后才被发现。下图是一个前端应用常见的4层架构。 代码和工具全面拥抱 TS 后,实现了从后端 API 接口到 View 组件的全链路静态分析,具有了完善的代码提示和校验能力。除了上面讲的 iron-redux,我们还引入 Pont 【5】实现前端取数,它可以自动把后端 API 映射到前端可调用的请求方法。Pont 实现原理:(法语:桥) 是我们研发的前端取数层框架。对接的后端 API 使用 Java Swagger,Swagger 能提供所有 API 的元信息,包括请求和响应的类型格式。Pont 解析 API 元信息生成 TS 的取数函数,这些取数函数类型完美,并挂载到 API 模块下。最终代码中取数效果是这样的:Pont 实现的效果有:根据方法名自动匹配 url、method,并且对应到 prams、response 类型完美,并能自动提示;后端 API 接口变更后,前端相关联的请求会自动报错,再也不担心后端悄悄改接口前端不知晓;再也不需要前后端接口约定文档,使用代码保证前端取数和后端接口定义完全一致。另外 iron-redux 能接收到 Pont 接口响应数据格式,并推导出整个 Redux 状态树的静态类型定义,Store 中的数据完美的类型提示。效果如下:最终 TS 让代码更加健壮,尤其是对于大型项目,编译通过几乎就代表运行正常,也给重构增加了很多信心。三、回归 Sass/Less2015 年我们就开始实践 CSS Modules,包括后来的 styled-components 等,到 2019 年 css-in-js 方案依旧争论不休,虽然它确实解决了一些 CSS 语言天生的问题,但同时增加了不少成本,新手不够友好、全局样式覆盖成本高涨、伪类处理复杂、与AntD等组件库结合有坑。与此同时 Sass/Less 社区也在飞速发展,尤其是 Stylelint 【6】的成熟,可以通过技术约束的手段来避免 CSS 的 Bad Parts。全局污染:约定每个样式文件只能有一个顶级类,如 .home-page{ .top-nav {//}, .main-content{ // } }。如果有多个顶级类,可以使用 Stylelint rule 检测并给出警告。依赖管理不彻底。借助 webpack 的 css-loader,已够用。JS 和 CSS 变量共享。关于 JS 和 Sass/Less 变量共享,我们摸索出了自己的解法:在 scss 文件中,可以直接引用变量:四、开发工具覆盖全链路2019 年,你几乎不可能再开发出 React/Angular/Vue 级别的框架,也没必要再造 Ant-Design/Fusion-Design/Ng-Zorro 这样的轮子。难道就没有机会了吗?当然有,结合你自身的产品开发流程,依旧有很多机会。下面是常规项目的开发流程图,任何一个环节只要深挖,都有提升空间。如果你能通过工具减少一个或多个环节,带来的价值更大。单拿其中的【开发】环节展开,就有很多可扩展的场景:一个有代表性的例子是,我们开发了国际化工具 kiwi【7】。它同样具有 TS 的类型完美,非常强大的文案提示,另外还有:VS Code 插件 kiwi linter【8】,自动对中文文案标红,如果已有翻译文案能自动完成替换;Shell 命令全量检查出没有翻译的文案,批量提交给翻译人员;Codemod 脚本自动实现旧的国际化方案向 Kiwi 迁移,成本极低。除了以上三点,未来还计划开发浏览器插件来检查漏翻文案,利用 Husky 在 git 提交前对漏翻文案自动做机器翻译等等。未来如果你只提供一个代码库,那它的价值会非常局限。你可以参照上面的图表,开发相应的扩展来丰富生态。如果你是新手,推荐学习下编译原理和对应的扩展开发规范。五、严格彻底的 Code Review过去的一年,我们一共进行了 1200+ 多次 Code Review(CR),很多同事从刚开始不好意思提 MR(GitLab Merge Request,Code Review 的一种方式) 到后来追着别人 Review,CR 成为每个人的习惯。通过 CR 让项目中任何一行代码都至少被两人触达过,减少了绝大多数的低级错误,提升了代码质量,这也是帮助新人成长最快的方式之一。![[其中一个项目MR截图]](https://upload-images.jianshu…Code Review 的几个技巧:No magic;Explicit not implicit;覆盖度比深度重要,覆盖度追求100%;频率比仪式感重要,坐公交蹲厕所打开手机都可以 Review 别人代码,不需要专门组织会议;粒度要尽可能小,一个组件一个方法均可,可以结合 Git Flow;24h 小时内处理,无问题直接 merge,有问题一定要留 comment,并且提供 action;对于亟待上线来不及 Review 的代码,可以先合并上线,上线后再补充 Review;需要自上而下的推动,具有完善的规范,同时定期总结 Review 经验来丰富开发规范;CR 并不只是为了找错,看到好的代码,不要吝啬你的赞美;本质是鼓励开发者间更多的沟通,互相学习,营造技术文化氛围。总结以上5点当然不是我们技术的全部。除此之外我们还实践了移动端开发、可视化图表/WebGL、Web Worker、GraphQL、性能优化等等,但这些还停留在术的层面,未来到一定程度会拿出来分享。如果你也准备或正在开发复杂的前端应用,同时团队人员多样技术背景各异,可以参考以上5点,使用 Redux 实现规范清晰可预测的状态管理,深耕 TypeScript 来提升代码健壮性和可维护性,借助各种 Lint 工具回归简单方便的 CSS,不断打磨自己的开发工具来保证开发规范高效,并严格彻底实行 Code Review 促进人的交流和提升。本文作者:会影阅读原文本文来自云栖社区合作伙伴“阿里技术”,如需转载请联系原作者。 ...

March 7, 2019 · 2 min · jiezi

Angular开发技巧

由于之前有幸去参加了ngChina2018开发者大会,听了will保哥分享了Angular开发技巧,自己接触Angular也有差不多快一年的时间了,所以打算对Angular开发中的一些技巧做一个整理工具篇所谓 “工欲善其事,必先利其器”,下面我会介绍 如何打磨 VS Code 这把利器抛弃资源管理器 ,使用快捷键 Commd + P 来查找文档,默认会展示最近打开的文档,并且支持模糊搜索文件快速打开最近文档:前进 Ctrl+➕ 后退 Ctrl+➖灵活使用VS Code重构功能,可以通过快捷键 Command + 对代码进行重构 安装插件 Angular Extension Pack (认准will保哥出品),这个插件集成了很多提升Angular开发效率的插件,比如: 在 TS中实用 ng-import-* 导入常见的类 模板编辑的时候实用 a-*** 快速使用Angular的组件和指令 (Angular v7 Snippets) 实用 ng-* 来生成常用的ng代码段,创建 Component,Directive 等 (Angular Snippets)通过快捷键把JSON转换成TS类Ctrl+Alt+V 把粘贴板中的JSON 转为 TypescriptCtrl+Alt+S 将选中的JSON 生成对应的 Typescript还有一个值得一提的一个比较实用的功能,通过快捷键来 快速切换组件对应的不同的文件 (Angular2-switcher)还有很多其他功能,插件中有详细介绍 `Angular Extension Pack安装插件 Clipboard History , 这个插件会存储你最近的拷贝的记录,方便记录和粘贴最近几次的拷贝内容安装插件 Local History ,这个插件用于维护文件的本地历史记录。每次修改文件时,旧内容的副本都会保留在本地历史记录中,你可以随时将文件与历史记录中的任何旧版本进行比较,如果发生意外时,可以帮助我们恢复丢失的内容,需要注意的是它会生成一个 .history 的文件夹进行本地修改的备份,所以我们需要再 .gitignore 排除这个文件夹,避免将其提交到git仓储。安装插件 Prettier - Code formatter,这是一个代码格式化的插件,用过几个格式化的插件,个人感觉最好用的一个,更适合Angular开发安装 Chrome 插件 Angular Angury 进行调试工作,可以查看 Component 的 State,Router Tree,NgModules的一些状态 (这个插件在复杂项目中并不是特别好用,包括对一些动态组件的支持比较差,但是在一些简单的项目中,或者新手在学习的时候安装这个插件比较方便调试排错)开发篇下面会介绍一些Angular开发中的技巧1.使用模板语言 as , 使用 as 对一些嵌套结构深的属性进行重命名改进前:<div *ngFor=“let queue of fileUploadQueues”> <div class=“icon” *ngIf=“queue.result.file.icon”>{{ queue.result.file.icon }}</div> <div class=“name” *ngIf=“queue.result.file.name”>{{ queue.result.file.name }}</div> <div class=“size” *ngIf=“queue.result.file.size”>{{ queue.result.file.size }}</div></div>改进后:<div *ngFor=“let queue of fileUploadQueues”> <ng-container *ngIf=“queue.result.file as file”> <div class=“icon” *ngIf=“file.icon”>{{ file.icon }}</div> <div class=“name” *ngIf=“file.name”>{{ file.name }}</div> <div class=“size” *ngIf=“file.size”>{{ file.size }}</div> </ng-container></div>2.灵活使用 ngIfElse,很多人其实一直在写ngIf 并不知道其实Angular支持 else 的写法 *ngIf=“条件 ; else 模板” ,看看下面这两段代码改进前:<div *ngIf="(data$ | async).length > 0"> …</div><div *ngIf="!(data$ | async).length > 0"> 没有数据</div>改进后:<div *ngIf="(data$ | async).length > 0; else emptyTemplate;"> …</div><ng-template #emptyTemplate> 没有数据</ng-template>改进前的写法,也能实现同样的效果,但是因为数据是通过async 订阅的,第一种写法相当于进行了两次订阅,当然也可以用 as 来解决,这里只是一个示例。还有一种情况,在条件多的时候,通过第一种方式写的话,如果条件有修改的话,必须要对取反后的条件进行维护, 而用 ngIfElse 的方式则只需要进行一次维护。3.使用 ng-container 对代码进行整理,使代码更清晰,提升代码的可读性<ng-container *ngIf=“type === 1”> …</ng-container><ng-container *ngIf=“type === 2”> …</ng-container><ng-container *ngIf=“type === 3”> …</ng-container>4.@ViewChild 读取指定类型的实例<input #input thyInput [thyAutofocus]=“true” />上面这行代码有三个实例 ElementRef 、ThyInputComponent、ThyAutoFocusDirective,在某些情况下如果我们要获取指定类型的实例应该怎么做呢?@ViewChild(‘input’, { read:ThyInputComponent }) inputComponent : ThyInputComponent ;5.使用 async 管道,直接在模板中订阅流,而不必将结果存储在中间属性中,当组件被销毁时,Angular将会自动取消订阅。<div *ngFor=“let item of data$ | async”> …</div>在一些情况下,我们可能需要重复使用订阅的数据,但是我们又不能每次使用的时候都用 async 去订阅,所以我们可以通过刚才说的 as 对齐进行重命名。<div *ngFor=“let item of data$ | async as data”> <span>一共有{{data.length}}条数据</span></div>6.使用 takeUntil 来管理订阅在某些复杂的业务中,我们可能需要订阅多个流,一个一个去取消订阅又繁琐,又会产生很多冗余代码,不利于代码的维护。这时候我们可以takeUntil 来管理多个订阅,统一取消订阅。private _ngUnsubscribe$ = new Subject();ngOnInit() { this.students$.pipe( takeUntil(_ngUnsubscribe$) ).subscribe(() => { … }); this.books$.pipe( takeUntil(_ngUnsubscribe$) ).subscribe(() => { … });}ngOnDestroy() { this._ngUnsubscribe$.next(); this._ngUnsubscribe$.complete();}7.合理使用 ngZone runOutsideAngular 来提升应用性能我们知道Angular可以自动处理变化检测,这是因为它使用了 zone.js ,简单的来说,zone.js 就是通过打补丁的方式来拦截浏览器的事件,然后进行变化检测,但是变化检测是极其消耗资源的,如果绑定了大量的事件,那么就会造成性能问题,所以我们可以使用 runOutsideAngular 来减少不必要的变化检测。 this.ngZone.runOutsideAngular(() => { this.renderer.listen(this.elementRef.nativeElement, ‘keydown’, event => { const keyCode = event.which || event.keyCode; if (keyCode === keycodes.ENTER) { event.preventDefault(); this.ngZone.run(() => { this.thyEnter.emit(event); }); } }); });上面这段代码是绑定一个回车事件,如果不使用 runOutsideAngular 的话,只要触发键盘输入事件,就会执行变化检测,这时候我们可以用 runOutsideAngular 在只有为enter事件的时候,去调用 ngZone.run() 主动触发变化检测8.灵活使用 ngTemplateOutlet 来实现递归<ng-container *ngFor=“let node of treeNodes;” [ngTemplateOutlet]=“nodeTemplate” [ngTemplateOutletContext]="{node: node}"></ng-container><ng-template #nodeTemplate let-node=“node”> <div class=‘title’>{{node.title}}</div> <ng-container *ngFor=“let child of node?.children;” [ngTemplateOutlet]=“nodeTemplate” [ngTemplateOutletContext]="{node: child}"> </ng-container></ng-template>在我们实际开发的过程中,经常会展示一些树形结构的数据,如果业务场景比较简单,可以通过Angular的 ngTemplateOutlet 来实现递归展示,如果业务复杂,建议还是通过组件的方式来实现。写在最后上面是我这一年Angular开发的过程中积累的一些小技巧(可能还有没想起来的,我想起来会慢慢的往上补),大家如果发现有错误的地方,请指正。其实去年就写好这篇文章,但是总感觉缺点什么,不过无所谓了~~ 希望能给Angular学习者提供帮助~本文作者:王凯文章来源:Worktile技术博客欢迎访问交流更多关于技术及协作的问题。文章转载请注明出处。 ...

March 7, 2019 · 2 min · jiezi

Angular 响应式表单之下拉框

1、问题分析下拉框绑定的 options 改变时,但是 value 值却没有改变,导致检查错误。在线预览git仓库2、代码分析项目中遇到一个问题,选择司机,是一个下拉列表。这个司机列表是一个动态列表,当你选择的司机不再列表中,可以添加司机,然后再选择;也有可能你选择了司机,还未提交时,这个司机就被删除了,需要给出错误提示。简化一下需要,性别选择,选中了保密后,删除这个选项。性别下拉框的 optionssexOptions: Option[] = [ { value: ‘man’, display: ‘男’ }, { value: ‘woman’, display: ‘女’ }, { value: ‘secret’, display: ‘保密’ }, ];点击删除保密 delete() { this.sexOptions = [ { value: ‘man’, display: ‘男’ }, { value: ‘woman’, display: ‘女’ }, ]; }此时就尴尬了,options 中已经没有保密选项了,但是下拉框绑定的value 却还是 secret。表单值:{ “sex”: “secret” }现在在删除事件中添加一个重置表单控件的条件,即可解决问题delete() { this.sexOptions = [ { value: ‘man’, display: ‘男’ }, { value: ‘woman’, display: ‘女’ }, ]; this.formGroup.get(‘sex’).reset(); }表单的 reset() 方法只是让表单的控件置为 null, 很不友好。如果没有选择 保密选择 ,点击删除按钮,值也会被置为空;项目中的选择司机是批量上传,控件有很多,如果重置,会让用户重新输入,不是很好。如果这个下拉控件是自定义的,就很好解决了,在 ngOnChanges 时检查当前控件的 value 在不在 options 的 value 中,如果不再则 value 置为 null,并且控件使用 updateValueAndValidity 方法,重新计算控件的值和校验状态。// 自定义控件中ngOnChanges() { // 当 optionList 更新后, value 不再 optionList 中,重置 value 为 null const isReset = this.optionList.some(item => item.value === this.value); if (!isReset) { this.value = null; } }// 删除后使用 updateValueAndValiditydelete() { this.sexOptions = [ { value: ‘man’, display: ‘男’ }, { value: ‘woman’, display: ‘女’ }, ]; this.formGroup.get(‘sex’).updateValueAndValidity(); } ...

March 4, 2019 · 1 min · jiezi

Alain 菜单权限控制

问题描述动态菜单管理,用户对应角色,角色对应菜单。为用户进行设置角色,登陆系统后,用户可使用其拥有角色对应的所有菜单。功能实现很简单,这里就不进行代码的讲解了,直接讲一下我所实现的思路。实现原设计系统设置中,前台菜单遵循如下格式:menus: [ { text: ‘主导航’, group: true, children: [ { text: ‘首页’, link: ‘/main’, icon: ‘anticon anticon-compass’ }, { text: ‘系统设置’, link: ‘/setting’, icon: ‘anticon anticon-setting’ } ], }]所以最开始的思路也很简单,后台的Menu实体中存储菜单所有相关的信息。后台直接就查出当前登录用户所有的菜单,前台根据返回来的菜单数据构建前台菜单。问题能实现肯定是能实现,但我们进行设计时,考虑的不应仅仅是实现,考虑的更多的是我这么实现,效率高不高?以后好不好改?能不能被以后维护的人员快速理解?斟酌之后,断然抛弃了这种实现,因为,不能把所有的数据都放在后台。就拿icon字段来说,如果我们采用了上述实现:那当我们以后想修改前台菜单图标的时候,需要去修改后台的数据初始化。这显然不合理,以后维护的人员肯定会存在一个疑问,这是谁设计的菜单?我改个前台的图标为什么要动后台?新设计既然不能讲数据都放在后台,那前后台就各司其职。前台:包含菜单名称,菜单图标,菜单路由等信息。负责前台菜单的格式显示。后台:只保留,菜单名,菜单路由,父菜单三项信息。负责后台用户的菜单授权。核心思想就是:前台配置好所有的菜单,但默认将菜单隐藏。应用启动时,查询后台接口,获取当前用户的所有授权菜单,授权一个,前台就显示一个。前台菜单:写菜单时将hide置为true,默认隐藏。menus: [ { text: ‘主导航’, group: true, children: [ { text: ‘首页’, link: ‘/main’, hide: true, icon: ‘anticon anticon-compass’ }, { text: ‘系统设置’, link: ‘/setting’, hide: true, icon: ‘anticon anticon-setting’ } ] }]然后就是具体的逻辑,先获取前台的菜单,即所有菜单。获取当前用户授权菜单列表,以路由表示该菜单唯一,如果路由被授权,就把hide置为false。/** * 获取所有被授权的菜单 */getAllAuthMenu(): Observable<Array<Menu>> { // 获取前台菜单 const menus = AppConfig.menus as Array<Menu>; return this.httpClient.get(’/menu/allAuthMenu’) .pipe(map((allAuthMenus: Array<WebAppMenu>) => { // 对菜单进行处理 menus.forEach((menu: Menu) => { menu.children.forEach((childMenu: Menu) => { childMenu.hide = !WebAppMenuService.checkMenuAuthOrNot(childMenu, allAuthMenus); }); }); return menus; }));}总结先完成,再完美。这里仅实现了菜单的隐藏,需要再编写权限控制逻辑,使我们的系统更安全,但那是我们以后要考虑的事情。现在先加个TODO。先把客户想要的功能先实现了,至于你实现得如何,代码如何,客户统统不关心,我们在先满足客户对开发速度需求的前提下,以后再抽出时间将程序的某些功能完美。 ...

February 28, 2019 · 1 min · jiezi

spring + angular 实现导出excel

需求描述要求批量导出数据,以excel的格式。选择方式前台 + 后台之前在别的项目中也遇到过导出的问题,解决方式是直接在前台导出将表格导出。这次没有选择前台导出的方式,是由于需要导出所有的数据,所以考虑直接在后台获取所有的数据,然后就直接导出,最后前台触发导出API。后台实现导出使用的是POI,在上一篇文章中,我已做了基本的介绍,这里就不做介绍配置了,参照:POI实现将导入Excel文件创建表格首先先建立一张表,这里要建立.xlsx格式的表格,使用XSSFWorkbook:Workbook workbook = new XSSFWorkbook();Sheet sheet = workbook.createSheet(“new sheet”);接着创建表格的行和单元格:Row row = sheet.createRow(0);row.createCell(0);然后设置表头:row.createCell(0).setCellValue(“学号”);row.createCell(1).setCellValue(“姓名”);row.createCell(2).setCellValue(“手机号码”);最后获取所有的数据,对应的填写到单元格中:int i = 1;for (Student student : studentList) { row = sheet.createRow(i); row.createCell(0).setCellValue(student.getStudentNumber()); row.createCell(1).setCellValue(student.getName()); row.createCell(2).setCellValue(student.getPhoneNumber()); i++;}输出这部分是纠结比较久的,反复试了很多次。一开始是直接以文件输出流的形式输出的:FileOutputStream output = new FileOutputStream(“test.xlsx”);workbook.write(output);这样可以正确生成文件,但是问题是,它会生成在项目的根目录下。而我们想要的效果是,下载在本地自己的文件夹中。要解决这个问题,需要添加相应信息,返回给浏览器:OutputStream fos = response.getOutputStream();response.reset();String fileName = “test”;fileName = URLEncoder.encode(fileName, “utf8”);response.setHeader(“Content-disposition”, “attachment;filename="+ fileName+".xlsx”);response.setCharacterEncoding(“UTF-8”);response.setContentType(“application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”);workbook.write(fos);fos.close();后台完成代码:public void batchExport(HttpServletResponse response) { logger.debug(“创建工作表”); Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet(“new sheet”); logger.debug(“获取所有学生”); List<Student> studentList = (List<Student>) studentRepository.findAll(); logger.debug(“建立表头”); Row row = sheet.createRow(0); row.createCell(0).setCellValue(“学号”); row.createCell(1).setCellValue(“姓名”); row.createCell(2).setCellValue(“手机号码”); logger.debug(“将学生信息写入对应单元格”); int i = 1; for (Student student : studentList) { row = sheet.createRow(i); row.createCell(0).setCellValue(student.getStudentNumber()); row.createCell(1).setCellValue(student.getName()); row.createCell(2).setCellValue(student.getPhoneNumber()); i++; } OutputStream fos; try { fos = response.getOutputStream(); response.reset(); String fileName = “test”; fileName = URLEncoder.encode(fileName, “utf8”); response.setHeader(“Content-disposition”, “attachment;filename="+ fileName+".xlsx”); response.setCharacterEncoding(“UTF-8”); response.setContentType(“application/vnd.openxmlformats-officedocument.spreadsheetml.sheet”);// 设置contentType为excel格式 workbook.write(fos); fos.close(); } catch (Exception e) { e.printStackTrace(); }}前台实现在前台调用的时候,也经历了多次失败,google了很多篇文章,各种各样的写法都有,自己也是试了试,前台后台都对照做了很多尝试,但基本都是有问题的。这里我值给出我最后选择配套后台的方法。// 后台导出路由const exportUrl = ‘/api/student/batchExport’;// 创建a标签,并点击let a = document.createElement(‘a’);document.body.appendChild(a);a.setAttribute(‘style’, ‘display:none’);a.setAttribute(‘href’, exportUrl);a.click();URL.revokeObjectURL(exportUrl);最后的实现还是一种比较简单的方法,创建了一个a标签,然后隐式点击。注意到这里我没有使用http请求,主要是他并不能触发浏览器的下载,在发起请求后,并没有正确的生成文件,具体是什么还不清楚。后面弄明白后我会再更新这篇文章。总结我们在google的时候,很多时候,我们并不能一下子就找到我们想要的东西,但是并不是说这在做无用功,因为我们往往会在一些类似的文章中找到灵感。所以,当我们没有直接找到我们想要的结果的时候,不妨大胆的做一些尝试,因为我们会在一次又一次失败的尝试中,慢慢的了解问题的原理到底是怎么回事。相关参考:https://my.oschina.net/u/3644…https://blog.csdn.net/LUNG108… ...

February 26, 2019 · 1 min · jiezi

angular 拦截器

需求描述前台需要请求后台的API,然后在请求时API的样式均有如下格式:/api/…所以依照不写重复代码的原则,一种方法我们可以定义一个公共变量,另一种就是可以定义一个拦截器,然后在请求API之前,为路径添加公共部分,再去请求。angular中的拦截器angular中要定义拦截器,需要实现HttpInterceptor接口,然后实现intercept()方法。@Injectable()export class MyInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler) { return next.handle(req); }}该方法接受了两个参数。第一个我们通过名字一下子就看出来这个是什么意思:这是一个http请求,他是我们主要操作的对象。另一个参数是next,他表示拦截器链表中的下一个拦截器。也就是说他会将http请求传递给下一个拦截器。这个链表的最后一项就是后端处理器,所以最后用过next将请求传给后台处理。然后我们的需求是在请求之前修改请求路径,所以需要这么写:intercept(req: HttpRequest<any>, next: HttpHandler) { const cloneRequest = req.clone({ url: api/${req.url} // 添加默认api访问路径 }); return next.handle(cloneRequest);}在angular中,如果想要修改请求,就需要调用它的clone方法,然后修改这个克隆体,最后将克隆体通过next对象,传递给下一个对象,或传给后台。这里我只对url进行了修改,当然还可以处理请求中的其他方面。官方参考:https://angular.cn/guide/http…https://angular.cn/api/common...https://angular.cn/api/common…

February 22, 2019 · 1 min · jiezi

Spring Security and Angular 实现用户认证

引言度过了前端框架的技术选型之后,新系统起步。ng-alain,一款功能强大的前端框架,设计得很好,两大缺点,文档不详尽,框架代码不规范。写前台拦截器的时候是在花了大约半小时的时间对代码进行全面规范化之后才开始进行的。又回到了最原始的问题,认证授权,也就是Security。认证授权认证,也就是判断用户是否可登录系统。授权,用户登录系统后可以干什么,哪些操作被允许。本文,我们使用Spring Security与Angular进行用户认证。开发环境Java 1.8Spring Boot 2.0.5.RELEASE学习这里给大家介绍一下我学习用户认证的经历。官方文档第一步,肯定是想去看官方文档,Spring Security and Angular - Spring.io。感叹一句这个文档,实在是太长了!!!记得当时看这个文档看了一晚上,看完还不敢睡觉,一鼓作气写完,就怕第二天起来把学得都忘了。我看完这个文档,其实我们需要的并不是文档的全部。总结一下文档的结构:引言讲解前后台不分离项目怎么使用basic方式登录前后台不分离项目怎么使用form方式登录,并自定义登录表单讲解CSRF保护(这块没看懂,好像就是防止伪造然后多存一个X-XSRF-TOKEN)修改架构,启用API网关进行转发(计量项目原实现方式)使用Spring Session自定义token实现Oauth2登录文档写的很好,讲解了许多why?,我们为什么要这么设计。我猜想这篇文章应该默认学者已经掌握Spring Security,反正我零基础看着挺费劲的。初学建议结合IBM开发者社区上的博客进行学习(最近才发现的,上面写的都特别好,有的作者怕文字说不明白的还特意录了个视频放在上面)。学习 - IBM中国这是我结合学习的文章:Spring Security 的 Web 应用和指纹登录实践实现引入Security依赖<!– Security –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency>基础配置继承配置适配器WebSecurityConfigurerAdapter,就实现了Spring Security的配置。重写configure,自定义认证规则。注意,configure里的代码不要当成代码看,否则会死得很惨。就把他当成普通的句子看!!!@Componentpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // 使用Basic认证方式进行验证进行验证 .httpBasic() // 要求SpringSecurity对后台的任何请求进行认证保护 .and().authorizeRequests().anyRequest().authenticated(); }}如此,我们后台的接口就被Spring Security保护起来了,当访问接口时,浏览器会弹出登录提示框。用户名是user,密码已打印在控制台:自定义认证这不行呀,不可能项目一上线,用的还是随机生成的用户名和密码,应该去数据库里查。实现UserDetailsService接口并交给Spring托管,在用户认证时,Spring Security即自动调用我们实现的loadUserByUsername方法,传入username,然后再用我们返回的对象进行其他认证操作。该方法要求我们根据我们自己的User来构造Spring Security内置的org.springframework.security.core.userdetails.User,如果抛出UsernameNotFoundException,则Spring Security代替我们返回401。@Componentpublic class YunzhiAuthService implements UserDetailsService { @Autowired private UserRepository userRepository; private static final Logger logger = LoggerFactory.getLogger(YunzhiAuthService.class); @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { logger.debug(“根据用户名查询用户”); User user = userRepository.findUserByUsername(username); logger.debug(“用户为空,则抛出异常”); if (user == null) { throw new UsernameNotFoundException(“用户名不存在”); } // TODO: 学习Spring Security中的role授权,看是否对项目有所帮助 return new org.springframework.security.core.userdetails.User(username, user.getPassword(), AuthorityUtils.createAuthorityList(“admin”)); }}基础的代码大家都能看懂,这里讲解一下最后一句。return new org.springframework.security.core.userdetails.User(username, user.getPassword(), AuthorityUtils.createAuthorityList(“admin”));构建一个用户,用户名密码都是我们查出来set进去的,对该用户授权admin角色(暂且这么写,这个对用户授予什么角色关系到授权,我们日后讨论)。然后Spring Security就调用我们返回的User对象进行密码判断与用户授权。用户冻结Spring Security只有用户名和密码认证吗?那用户冻结了怎么办呢?这个无须担心,点开org.springframework.security.core.userdetails.User,一个三个参数的构造函数,一个七个参数的构造函数,去看看源码中的注释,一切都不是问题。Spring Security设计得相当完善。public User(String username, String password, Collection<? extends GrantedAuthority> authorities) { this(username, password, true, true, true, true, authorities);}public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) { if (((username == null) || “".equals(username)) || (password == null)) { throw new IllegalArgumentException( “Cannot pass null or empty values to constructor”); } this.username = username; this.password = password; this.enabled = enabled; this.accountNonExpired = accountNonExpired; this.credentialsNonExpired = credentialsNonExpired; this.accountNonLocked = accountNonLocked; this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));}启用密码加密忘了当时是什么场景了,好像是写完YunzhiAuthService之后再启动项目,控制台中就有提示:具体内容记不清了,大体意思就是推荐我采用密码加密。特意查了一下数据库中的密码需不需要加密,然后就查到了CSDN的密码泄露事件,很多开发者都批判CSDN的程序员,说明文存储密码是一种非常不服责任的行为。然后又搜到了腾讯有关的一些文章,反正密码加密了,数据泄露了也不用承担过多的法律责任。腾讯还是走在法律的前列啊,话说是不是腾讯打官司还没输过?既然这么多人都推荐加密,那我们也用一用吧。去Google了一下查了,好像BCryptPasswordEncoder挺常用的,就添加到上下文里了,然后Spring Security再进行密码判断的话,就会把传来的密码经过BCryptPasswordEncoder加密,判断和我们传给它的加密密码是否一致。@Configurationpublic class BeanConfig { /** * 密码加密 / @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }}然后一些User的细节就参考李宜衡的文章:Hibernate实体监听器。Help, How is My Application Going to Scale?其实,如果对技术要求不严谨的人来说,上面已经足够了。如果你也有一颗崇尚技术的心,我们一起往下看。嘿!我的应用程序怎么扩大规模?这是Spring官方文档中引出的话题,官方文档中对这一块的描述过于学术,什么TCP,什么stateless。说实话,这段我看了好几遍也没看懂,但是我非常同意这个结论:我们不能用Spring Security帮我们管理Session。以下是我个人的观点:因为这是存在本地的,当我们的后台有好多台服务器,怎么办?用户这次请求的是Server1,Server1上存了一个seesion,然后下次请求的是Server2,Server2没有session,完了,401。所以我们要禁用Spring Security的Session,但是手动管理Session又太复杂,所以引入了新项目:Spring Session。Spring Session的一大优点也是支持集群Session。引入Spring Session<!– Redis –><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId></dependency><!– Session –><dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId></dependency>这里引入的是Spring Session中的Session-Redis项目,使用Redis服务器存储Session,实现集群共享。禁用Spring Security的Session管理@Componentpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // 使用Basic认证方式进行验证进行验证 .httpBasic() // 要求SpringSecurity对后台的任何请求进行认证保护 .and().authorizeRequests().anyRequest().authenticated() // 关闭Security的Session,使用Spring Session .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER); }}关闭Spring Security的Session管理,设置Session创建策略为NEVER。Spring Security will never create an HttpSession, but will use the HttpSession if it already existsSpring Security不会创建HttpSession,但是如果存在,会使用这个HttpSession。启用Redis管理SessionMac下使用Homebrew安装redis十分简单,Mac下安装配置Redis。@EnableRedisHttpSession@Configurationpublic class BeanConfig { /* * 设置Session的token策略 / @Bean public HeaderHttpSessionIdResolver httpSessionIdResolver() { return new HeaderHttpSessionIdResolver(“token”); }}@EnableRedisHttpSession启用Redis的Session管理,上下文中加入对象HeaderHttpSessionIdResolver,设置从Http请求中找header里的token最为认证字段。梳理逻辑很乱是吗?让我们重新梳理一下逻辑。使用HttpBasic方式登录,用户名和密码传给后台,Spring Security进行用户认证,然后根据我们的配置,Spring Security使用的是Spring Session创建的Session,最后存入Redis。以后呢?登录之后,就是用token的方式进行用户认证,将token添加到header中,然后请求的时候后台识别header里的token进行用户认证。所以,我们需要在用户登录的时候返回token作为以后用户认证的条件。登录方案登录方案,参考官方文档学来的,很巧妙。以Spring的话来说:这个叫trick,小骗术。我们的login方法长成这样:@GetMapping(“login”)public Map<String, String> login(@AuthenticationPrincipal Principal user, HttpSession session) { logger.info(“用户: " + user.getName() + “登录系统”); return Collections.singletonMap(“token”, session.getId());}简简单单的四行,就实现了后台的用户认证。原理因为我们的后台是受Spring Security保护的,所以当访问login方法时,就需要进行用户认证,认证成功才能执行到login方法。换句话说,只要我们的login方法执行到了,那就说明用户认证成功,所以login方法完全不需要业务逻辑,直接返回token,供之后认证使用。怎么样,是不是很巧妙?注销方案注销相当简单,直接清空当前的用户认证信息即可。@GetMapping(“logout”)public void logout(HttpServletRequest request, HttpServletResponse response) { logger.info(“用户注销”); // 获取用户认证信息 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); // 存在认证信息,注销 if (authentication != null) { new SecurityContextLogoutHandler().logout(request, response, authentication); }}单元测试如果对整个流程不是很明白的话,看下面的单元测试会有所帮助,代码很详尽,请理解整个认证的流程。@RunWith(SpringRunner.class)@SpringBootTest@AutoConfigureMockMvc@Transactionalpublic class AuthControllerTest { private static final Logger logger = LoggerFactory.getLogger(AuthControllerTest.class); private static final String LOGIN_URL = “/auth/login”; private static final String LOGOUT_URL = “/auth/logout”; private static final String TOKEN_KEY = “token”; @Autowired private MockMvc mockMvc; @Test public void securityTest() throws Exception { logger.debug(“初始化基础变量”); String username; String password; byte[] encodedBytes; MvcResult mvcResult; logger.debug(“1. 测试用户名不存在”); username = CommonService.getRandomStringByLength(10); password = “admin”; encodedBytes = Base64.encodeBase64((username + “:” + password).getBytes()); logger.debug(“断言401”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(“Authorization”, “Basic " + new String(encodedBytes))) .andExpect(status().isUnauthorized()); logger.debug(“2. 用户名存在,但密码错误”); username = “admin”; password = CommonService.getRandomStringByLength(10); encodedBytes = Base64.encodeBase64((username + “:” + password).getBytes()); logger.debug(“断言401”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(“Authorization”, “Basic " + new String(encodedBytes))) .andExpect(status().isUnauthorized()); logger.debug(“3. 用户名密码正确”); username = “admin”; password = “admin”; encodedBytes = Base64.encodeBase64((username + “:” + password).getBytes()); logger.debug(“断言200”); mvcResult = this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(“Authorization”, “Basic " + new String(encodedBytes))) .andExpect(status().isOk()) .andReturn(); logger.debug(“从返回体中获取token”); String json = mvcResult.getResponse().getContentAsString(); JSONObject jsonObject = JSON.parseObject(json); String token = jsonObject.getString(“token”); logger.debug(“空的token请求后台,断言401”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(TOKEN_KEY, “”)) .andExpect(status().isUnauthorized()); logger.debug(“加上token请求后台,断言200”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(TOKEN_KEY, token)) .andExpect(status().isOk()); logger.debug(“用户注销”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGOUT_URL) .header(TOKEN_KEY, token)) .andExpect(status().isOk()); logger.debug(“注销后,断言该token失效”); this.mockMvc.perform(MockMvcRequestBuilders.get(LOGIN_URL) .header(TOKEN_KEY, token)) .andExpect(status().isUnauthorized()); }}前台方法和这么复杂的后台设计相比较,前台没有啥技术含量,把代码粘贴出来大家参考参考即可,没什么要说的。前台Service:@Injectable({ providedIn: ‘root’,})export class AuthService { constructor(private http: _HttpClient) { } /* * 登录 * @param username 用户名 * @param password 密码 / public login(username: string, password: string): Observable<ITokenModel> { // 新建Headers,并添加认证信息 let headers = new HttpHeaders(); headers = headers.append(‘Content-Type’, ‘application/x-www-form-urlencoded’); headers = headers.append(‘Authorization’, ‘Basic ’ + btoa(username + ‘:’ + password)); // 发起get请求并返回 return this.http .get(’/auth/login?_allow_anonymous=true’, {}, { headers: headers }); } /* * 注销 / public logout(): Observable<any> { return this.http.get(’/auth/logout’); }}登录组件核心代码:this.authService.login(this.userName.value, this.password.value) .subscribe((response: ITokenModel) => { // 清空路由复用信息 this.reuseTabService.clear(); // 设置用户Token信息 this.tokenService.set(response); // 重新获取 StartupService 内容,我们始终认为应用信息一般都会受当前用户授权范围而影响 this.startupSrv.load().then(() => { // noinspection JSIgnoredPromiseFromCall this.router.navigateByUrl(‘main/index’); }); }, () => { // 显示错误信息提示 this.showLoginErrorInfo = true; });注销组件核心代码:// 调用Service进行注销this.authService.logout().subscribe(() => { }, () => { }, () => { // 清空token信息 this.tokenService.clear(); // 跳转到登录页面,因为无论是否注销成功都要跳转,写在complete中 // noinspection JSIgnoredPromiseFromCall this.router.navigateByUrl(this.tokenService.login_url); });前台拦截器有一点,headers.append(‘X-Requested-With’, ‘XMLHttpRequest’),如果不设置这个,在用户名密码错误的时候会弹出Spring Security原生的登录提示框。还有就是,为什么这里没有处理token,因为Ng-Alain的默认的拦截器已经对token进行添加处理。// noinspection SpellCheckingInspection/* * Yunzhi拦截器,用于实现添加url,添加header,全局异常处理 /@Injectable()export class YunzhiInterceptor implements HttpInterceptor { constructor(private router: Router) { } intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { /* * 为request加上服务端前缀 / let url = req.url; if (!url.startsWith(‘https://’) && !url.startsWith(‘http://’)) { url = environment.SERVER_URL + url; } let request = req.clone({ url }); /* * 设置headers,防止弹出对话框 * https://stackoverflow.com/questions/37763186/spring-boot-security-shows-http-basic-auth-popup-after-failed-login / let headers = request.headers; headers = headers.append(‘X-Requested-With’, ‘XMLHttpRequest’); request = request.clone({ headers: headers }); /* * 数据过滤 */ return next.handle(request).pipe( // mergeMap = merge + map mergeMap((event: any) => { return of(event); }), // Observable对象发生错误时,执行catchError catchError((error: HttpErrorResponse) => { return this.handleHttpException(error); }), ); } private handleHttpException(error: HttpErrorResponse): Observable<HttpErrorResponse> { switch (error.status) { case 401: if (this.router.url !== ‘/passport/login’) { // noinspection JSIgnoredPromiseFromCall this.router.navigateByUrl(’/passport/login’); } break; case 403: case 404: case 500: // noinspection JSIgnoredPromiseFromCall this.router.navigateByUrl(/${error.status}); break; } // 最终将异常抛出来,便于组件个性化处理 throw new Error(error.error); }}解决H2控制台看不见问题Spring Security直接把H2数据库的控制台也拦截了,且禁止查看,启用以下配置恢复控制台查看。@Componentpublic class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http // 使用Basic认证方式进行验证进行验证 .httpBasic() // 要求SpringSecurity对后台的任何请求进行认证保护 .and().authorizeRequests().anyRequest().authenticated() // 关闭Security的Session,使用Spring Session .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.NEVER) // 设置frameOptions为sameOrigin,否则看不见h2控制台 .and().headers().frameOptions().sameOrigin() // 禁用csrf,否则403. 这个在上线的时候判断是否需要开启 .and().csrf().disable(); }}总结一款又一款框架,是前辈们智慧的结晶。永远,文档比书籍更珍贵! ...

February 22, 2019 · 4 min · jiezi

Angular 响应式表单之表单分组

1、案例需求表单提交,表单全部校验成功才能提交,当表单校验错误,表单边框变红,同时有错误提示信息,有重置功能在线预览git仓库2、名词解释在分析代码之前,首先明确 FormControl、formControl、formControlName、FormGroup、formGroup、formGroupName、FormArray、formArray、formArrayName 都是什么意思以及它们的用法。2.1、FormControlFormControl:跟踪独立表单控件的值和验证状态。它和 FormGroup 和 FormArray 是 Angular 表单的三大基本构造块之一。它扩展了 AbstractControl 类,并实现了关于访问值、验证状态、用户交互和事件的大部分基本功能。当使用响应式表单时,FormControl 类是最基本的构造块。要注册单个的表单控件,在组件中导入 FormControl 类,并创建一个 FormControl 的新实例,把它保存在类的某个属性中。export class AppComponent implements OnInit { const control = new FormControl(’’, Validators.required); console.log(control.value); // ’’ console.log(control.status); // ‘INVALID’}在组件类中创建了控件之后,还要把它和模板中的一个表单控件关联起来,为表单控件添加 formControl 绑定。<label> Name: <input type=“text” [formControl]=“name”></label>formControl:是一个输入指令,接受 FormControl 的实例,在模版中使用。formControlName: 也是输入指令,但是它接受的是一个字符串,同 formGroup 指令配合使用。<div> <input type=“text” [formControl]=“myForm.controls.firstName”/> <input type=“text” [formControl]=“myForm.controls.lastName”/> <input type=“text” [formControl]=“myForm.controls.email”/> <input type=“text” [formControl]=“myForm.controls.title”/></div>//等同于<div [formGroup]=“myForm”> <input type=“text” formControlName=“firstName”/> <input type=“text” formControlName=“lastName”/> <input type=“text” formControlName=“email”/> <input type=“text” formControlName=“title”/></div>2.2、FormGroupFormGroup:跟踪一组 FormControl 实例的值和有效性状态FormGroup 把每个子 FormControl 的值聚合进一个对象,它的 key 是每个控件的名字。它通过归集其子控件的状态值来计算出自己的状态。如果组中的任何一个控件是无效的,那么整个组就是无效的。2.3、FormArrayFormArray:跟踪一个控件数组的值和有效性状态FormArray 聚合了数组中每个表单控件的值。它会根据其所有子控件的状态总结出自己的状态。如果 FromArray 中的任何一个控件是无效的,那么整个数组也会变成无效的。FormControl、FormGroup、FormArray 类 用法一致formControl、formGroup、formArray 输入指令 值为对应类的实例 用法一致formControlName、formGroupName、formArrayName 输入指令 值为字符串 用法一直3、代码分析fromGroup 可以然我们对表单内容进行分组,方便我们在语义上区分不同类型的输入,本例中,地址细分为“省”、“市”、“区”。this.formGroup = this.fb.group({ name: [’’, nameValidator()], age: [’’, ageValidator()], sex: [’’, sexValidator()], address: this.fb.group({ province: [’’, requiredValidator(‘请输入省’)], city: [’’, requiredValidator(‘请输入市’)], district: [’’, requiredValidator(‘请输入区’)] }) });address 此时不是 fromControl 而是 formGroup。<div class=“form-group” formGroupName=“address”> <label>地址:</label> <div> <label>省:</label> <input type=“text” formControlName=“province”> <p>{{errorMessage(‘province’)}}</p> </div> <div> <label>市:</label> <input type=“text” formControlName=“city”> <p>{{errorMessage(‘city’)}}</p> </div> <div> <label>区:</label> <input type=“text” formControlName=“district”> <p>{{errorMessage(‘district’)}}</p> </div> </div>在获取 省市区的 formControl 时,可以通过这样获取// 太复杂了this.formGroup.controls[‘address’].controls[‘province’];// 同样复杂this.formGroup.get(‘address’).controls[‘province’];// 还好this.formGroup.get([‘address’, ‘province’]);第三种方式虽然简单,但是不够完美,get方法不能一步到位,必须同时传入 formGroupName 和 formControlName。因此在查看单个表单是否有错误信息时,必须先判断 formControlName 是子组件还是孙子组件。errorMessage(formControlName: string): string { let control: AbstractControl; if (this.formGroup.contains(formControlName)) { control = this.formGroup.get(formControlName); } else { control = this.formGroup.get([‘address’, formControlName]); } return ((control.touched || control.dirty) && control.invalid) ? control.errors.message : ‘’; }contains方法:检查组内是否有一个具有指定名字的已启用的控件,存在返回 true,不存在返回 false。 ...

February 22, 2019 · 1 min · jiezi

rxjs switchMap操作符

在写分页数据请求时,将页数大小和当前页传入后台,就能获取到分页数据了,一开始写了一个方法,将页数和大小传入,返回可观察数据。service:/** 获取分页数据 / getMajorByPage(page: number, size: number): Observable<Page<Major>> { const params = { page: page.toString(), size: size.toString() }; return this.http.get<Page<Major>>(this.baseUrl + ‘/page’, {params: params}); }组件:queryPage() { this.majorService.getMajorByPage(this.page - 1, this.size) .subscribe((data) => { this.majorList = data.content; this.total = data.totalElements; this.page = data.number + 1; this.size = data.size; }); }顺利获取到了数据,但在写删除时,删除最后一条信息时,会出现空数据.在删除第二页的最后一条数据时,删除后重新请求时还是以第二页请求,就没数据了.一开始是在删除时判断当前内容是否只有一条,如果只有一条重新请求数据时请求上一页的数据.deleteMajor(majorId: number) { this.majorService.deleteMajor(majorId).subscribe(() => { this.allChecked = false; this.indeterminate = false; if (this.majorList.length === 1) { this.page–; } this.reload(); }); }这样解决了问题,但在多选删除时又没有用了,于是想写一个一劳永逸的方法,在获得请求数据时,判断内容是否为空,如果内容为空,并且请求的不是第一页就重新请求:queryPage() { this.majorService.getMajorByPage(this.page - 1, this.size) .subscribe((data) => { if (data.content.length === 0 && data.number !== 0) { this.page–; this.majorService.getMajorByPage(this.page - 1, this.size); } else { this.majorList = data.content; this.total = data.totalElements; this.page = data.number + 1; this.size = data.size; } }); }每一次调用这个方法,都要对请求回来的数据判断,写完之后感觉太麻烦了,想到这是一个可观察数据,有很多操作符可以对数据进行操作,如果有操作符能在数据返回时进行判断,再返回我们想返回的可观察数据就好了.去搜了一下,找到一个操作符,switchmap:switchMap 和其他打平操作符的主要区别是它具有取消效果。在每次发出时,会取消前一个内部 observable (你所提供函数的结果) 的订阅,然后订阅一个新的 observable 。看了下大致用法,就是能更换observable,可以满足我的需求,修改了一下服务层的代码,也能完成需求:/ *获取分页数据 */ getCollegeByPage(page: number, size: number): Observable<Page<Major>> { const params = { page: page.toString(), size: size.toString() }; return this.http.get<Page<Major>>(this.baseUrl + ‘/page’, {params: params}) .pipe(switchMap((majorPage) => { if (majorPage.content.length === 0 && majorPage.number !== 0) { return this.getCollegeByPage(page - 1, size); } else { return of(majorPage); } })); }这样一来,在调用的时候就不用担心返回数据为空了,也成功的将逻辑和调用分离了,好维护。 ...

February 22, 2019 · 1 min · jiezi

Angular 响应式表单 基础例子

1、案例需求表单提交,表单全部校验成功才能提交,当表单校验错误,表单边框变红,同时有错误提示信息,有重置功能2、代码分析在线预览git仓库 (clone npm install ng serve)本案例中使用了响应式表单,响应式表单在表单的校验方面非常方便2.1、注册 ReactiveFormsModule要使用响应式表单,就要从 @angular/forms 包中导入 ReactiveFormsModule 并把它添加到你的 NgModule 的 imports 数组中。app.module.tsimports: [ BrowserModule, AppRoutingModule, ReactiveFormsModule ]2.2、使用 FormBuilder 来生成表单控件当需要与多个表单打交道时,手动创建多个表单控件实例会非常繁琐。FormBuilder 服务提供了一些便捷方法来生成表单控件。FormBuilder 在幕后也使用同样的方式来创建和返回这些实例,只是用起来更简单。注入 FormBuilder 服务constructor( private fb: FormBuilder ) { }生成表单控件FormBuilder 提供了一个语法糖,以简化 FormControl、FormGroup 或 FormArray 实例的创建过程。它会减少构建复杂表单时所需的样板代码的数量(new FormControl)。formGroup: FormGroup;this.formGroup = this.fb.group({ name: ‘’, age: ‘’, sex: ’’ });2.3、FormGroupDirectiveformGroup 是一个输入指令,它接受一个 formGroup 实例,它会使用这个 formGroup 实例去匹配 FormControl、FormGroup、FormArray 实例,所以模版中的 formControlName 必须和 formGroup 中的 name 匹配。<form [formGroup]=“formGroup” (ngSubmit)=“submit()” novalidate> <div class=“form-group”> <label>姓名:</label> <input type=“text” formControlName=“name”> <p>{{nameErrorMessage}}</p> </div></form>2.4、表单状态每个表单控件都有自己的状态,共五个状态属性,都是布尔值。valid 表单值是否有效pristine 表单值是否未改变dirty 表单值是否已改变touched 表单是否已被访问过untouched 表单是否未被访问过以输入姓名的表单为例,只验证该表单的必填项时。表单先获取焦点并且输入姓名,最后移除焦点,每一步表单的状态如下:初始状态 状态值validfalsepristinetruedirtyfalsetouchedfalseuntouchedtrue输入状态 状态值validtruepristinefalsedirtytruetouchedfalseuntouchedtrue失去焦点后状态 状态值validtruepristinefalsedirtytruetouchedtrueuntouchedtrue2.5、表单校验表单验证用于验证用户的输入,以确保其完整和正确。Angular内置的了一些校验器,如 Validators.required, Validators.maxlength,Validators.minlength, Validators.pattern,但是不能自定义错误提示信息,所以实用性不强,满足不了业务场景的需求,于是我们可以自定义表单校验器。自定义表单校验器name-validator.tsimport { AbstractControl, ValidatorFn } from ‘@angular/forms’;export function nameValidator(): ValidatorFn { return (control: AbstractControl): { [key: string]: any } => { if (!control.value) { return { message: ‘请输入必选项’ }; } if (control.value.length > 10) { return { message: ‘名称大于10位了’ }; } return null; };}使用自定义验证器app.component.tsthis.formGroup = this.fb.group({ name: [’’, nameValidator()], age: [’’, ageValidator()], sex: [’’, sexValidator()] });显示错误提示信息当页面初始化的时候不应该显示错误信息,也就是表单touched为true// Errorprivate errorMessage(name): string { const control = this.formGroup.controls[name]; return (control.touched && control.invalid) ? control.errors.message : ‘’; }然而touched只有失去焦点才为true,在输入的时候一直为false。导致在输入的时候,表单校验错误,却显示不了错误信息。因此需要再次判断 dirty 状态,只要表单值改变,则为falseprivate errorMessage(name): string { const control = this.formGroup.controls[name]; return ((control.touched || control.dirty) && control.invalid) ? control.errors.message : ‘’; }2.6、markAsTouched未对表单操作时,点击提交按钮时,则模拟表单被touched,显示提示信息markFormGroupTouched(formGroup: FormGroup) { Object.values(formGroup.controls).forEach(item => { if (item.controls) { // 当 formGroup中存在 formGroup 时,递归 this.markFormGroupTouched(item.controls); } else { item.markAsTouched(); } }); } ...

February 21, 2019 · 1 min · jiezi

angular中绑定如如何iframe中src

需求: 页面中有一个网页组件(由iframe编写),此iframe显示在一个输入框中,当修改输入框中地址的时候,要求改变网页组件中的内容网页组件中的代码(html的部分) <iframe #Iframe [src]=“testUrl” frameborder=“0” width=“100%” height=“100%"> </iframe>网页组件中的代码(ts的部分)…省略export class DesignWebInputComponent implements OnInit{ testUrl ;}此时问题出现了,页面无法显示内容不要慌,有办法可以解决constructor( private sanitizer:DomSanitizer) {}导入DomSanitizer 这个类 并使用其中的bypassSecurityTrustResourceUrl() 转换url的格式 如下 trustUrl(url: string) { if(url){ return this.sanitizer.bypassSecurityTrustResourceUrl(url); } }html中 <iframe #Iframe [src]=“trustUrl(testUrl)” frameborder=“0” width=“100%” height=“100%"> </iframe>在这里写了个trustUrl()转换 testUrl 这样就可以显示了总结: 使用 DomSanitizer 类中的 bypassSecurityTrustResourceUrl() 来转换url

January 31, 2019 · 1 min · jiezi

使用Angular自定义字段校验指令

Angular中,提供的表单验证不能用于所有应用场景,就需要创建自定义验证器,比如对IP、MAC的合法性校验 这里是根据官网实例自定义MAC地址的正则校验,环境为Angular: 7.2.0 , NG-ZORRO:v7.0.0-rc3添加指令/shared/validator.directive.ts注册到 NG_VALIDATORS 提供商中providers: [ {provide: NG_VALIDATORS, useExisting: ValidatorDirective, multi: true} ]Angular 在验证流程中的识别出指令的作用,是因为指令把自己注册到了 NG_VALIDATORS 提供商中,该提供商拥有一组可扩展的验证器。实现 Validator 接口import {Directive, Input} from ‘@angular/core’;import {Validator, AbstractControl, NG_VALIDATORS} from ‘@angular/forms’;@Directive({ selector: ‘[appValidator]’, providers: [ {provide: NG_VALIDATORS, useExisting: ValidatorDirective, multi: true} ]})export class ValidatorDirective implements Validator { @Input(‘appValidator’) value: string; validate(control: AbstractControl): { [key: string]: any } | null { const validateMac = /^(([A-Fa-f0-9]{2}[:]){5}[A-Fa-f0-9]{2}[,]?)+$/; switch (this.value) { case ‘mac’: return validateMac.exec(control[‘value’]) ? null : {validate: true}; break; } }}ValidatorDirective写好后,只要把 appValidator 选择器添加到输入框上就可以激活这个验证器。在模板中使用<nz-form-item> <nz-form-control> <nz-input-group> <input formControlName=“mac” nz-input type=“text” placeholder=“mac” appValidator=“mac”> </nz-input-group> <nz-form-explain *ngIf=“validateForm.get(‘mac’).dirty && validateForm.get(‘mac’).errors”> 请输入正确的Mac地址! </nz-form-explain> </nz-form-control> </nz-form-item>在mac地址校验不通过时,错误信息便会显示。如果想在失去焦点时显示错误信息可以使用validateForm.get(‘mac’).touched,如下:<nz-form-explain *ngIf=“validateForm.get(‘mac’).dirty && validateForm.get(‘mac’).errors&&validateForm.get(‘mac’).touched”> 请输入正确的Mac地址! </nz-form-explain>到此,自定义字段验证指令就完成了,更多请查看Angular官网表单验证自定义验证器部分。 ...

January 31, 2019 · 1 min · jiezi

【Angular】Angula6中的组件通信

Angula6_组件通信本文主要介绍 Angular6 中的组件通信一、父子组件通信1.1 父组件向子组件传递信息方法一 在父组件上设置子组件的属性父组件绑定信息<app-child childTitle=“可设置子组件标题”></app-child>子组件接收消息import { Component, OnInit, Input } from ‘@angular/core’;@Input childTitle: string;方法二 父组件调用子组件的方法父组件触发消息<app-child #child></app-child> <button (click)=“child.childPrint()"></button>子组件接收消息childPrint() { alert(“来自子组件的打印”);}1.2 子组件向父组件传递信息方法一 使用 EventEmitter子组件使用 EventEmitter 传递消息import { Component, OnInit, Output, EventEmitter } from ‘@angular/core’;…@Output() initEmit = new EventEmitter<string>();ngOnInit() { this.initEmit.emit(“子组件初始化成功”);}…父组件接收消息<app-child (initEmit)=“accept($event)"></app-child>accept(msg:string) { alert(msg);}方法二 使用 ViewChild子组件提供传递参数的函数sendInfo() { return ‘Message from child 1.’;}父组件使用 ViewChild 触发并接收信息<button (click)=“getInfo()">获取子组件1号的信息</button><h2>{{ info }}</h2>import { Component, OnInit, ViewChild } from ‘@angular/core’;…@ViewChild(ChildFirstComponent) private childcomponent: ChildFirstComponent;getInfo() { this.info = this.childcomponent.sendInfo();}二、非父子组件通信方法一 service缺点:需要双向的触发(发送信息 / 接收信息)service.tsimport { Component, Injectable, EventEmitter } from “@angular/core”;@Injectable()export class myService { public info: string = “”; constructor() {}}组件 1 向 service 传递信息import { myService } from ‘../../service/myService.service’;…constructor( public service: myService) { }changeInfo() { this.service.info = this.service.info + “1234”;}…组件 2 从 service 获取信息import { myService } from ‘../../service/myService.service’;…constructor( public service: myService) { }showInfo() { alert(this.service.info);}…方法二 使用 BehaviorSubject优点:真正的发布订阅模式,当数据改变时,订阅者也能得到响应serviceimport { BehaviorSubject } from ‘rxjs’;…public messageSource = new BehaviorSubject<string>(‘Start’);changemessage(message: string): void { this.messageSource.next(message);}组件调用 service 的方法传信息和接收信息changeInfo() { this.communication.changemessage(‘Message from child 1.’);}ngOnInit() { this.communication.messageSource.subscribe(Message => { window.alert(Message); this.info = Message; });}三、其他的通信方式路由传值cookie、session、storage参考文献《Angular6.x 学习笔记——组件详解之组件通讯》《angular6 组件间的交流方式》 ...

January 31, 2019 · 1 min · jiezi

使用angular框架离线你的应用(pwa指南)

简介关于service worker,网络上已经有了较多的文章。总的来说它依靠缓存资源,拦截http请求,来帮助我们达到离线使用网站的效果。而angular cli为了让使用service worker更加便利,为开发者提供了一个配置窗口(ngsw-config.json)和一个与service worker通讯的服务(SwUpdate)。启用新建项目后在项目根目录下键入ng add @angular/pwa。会为你的项目添加一些文件,其中包括上文提到的ngsw-config.json,为了即刻感受离线应用的魅力,现暂不需配置。安装http-server npm install http-server -g将用http-server启用的服务器来加载应用,这么做是为了模拟真实的生产环境,因为ng serve环境下无法启用service worker。键入ng build –prod进行打包。 打包完成后进入dist下的项目根目录下,键入http-server -p <port>将打包后的应用部署在http-server指定的端口上。打开浏览器进入控制台,以chrome为例,在network选项卡上勾选offline模拟离线使用。此时重新刷新网页发现页面依然能够在离线状态下显示,说明service worker已经工作了。配置以上并没有手动配置ngsw-config.json,然而初始的配置还有许多不足,比如无法拦截缓存api请求。因此需要对该文件的配置参数做一个大概了解(点击浏览官方配置说明)。参考官方的说明,我们了解到可以配置静态资源的缓存策略(配置项中的assetGroups)以及动态资源的缓存策略(配置项中的dataGroups)。静态资源配置(assetGroups)interface AssetGroup { name: string; installMode?: ‘prefetch’ | ’lazy’; updateMode?: ‘prefetch’ | ’lazy’; resources: { files?: string[]; /** @deprecated As of v6 versionedFiles and files options have the same behavior. Use files instead. / versionedFiles?: string[]; urls?: string[]; };}这是该配置项的接口,下面对各个属性做一个简要的说明:resources属性下可配置本地的静态资源(resources.files)和通过cdn来的静态资源(resources.urls)name是需要编写的该资源集合的唯一的名字installMode配置的是你的网站应用第一次在当前浏览器加载后,service worker应该进行的缓存策略。选择’prefetch’会将resources列出的资源一股脑儿的预先缓存起来,不管当前是否有访问到。选择’lazy’则不会预先缓存,而是在用到时才会进行缓存。updateMode配置的是当检测到资源的版本改变之后,所进行的资源缓存策略。如何得知资源的版本发生了变化呢?angular service worker会对比资源内容的hash值。如果hash值不同则版本不同。选择’prefetch’会立即缓存更新的资源,选择’lazy’会在用到时在进行缓存。不过,这里要注意如果在installMode的配置中没有选择’lazy’模式,则这里的’lazy’模式也不会生效。动态资源配置(dataGroups)export interface DataGroup { name: string; urls: string[]; version?: number; cacheConfig: { maxSize: number; maxAge: string; timeout?: string; strategy?: ‘freshness’ | ‘performance’; };}这是缓存动态资源的配置项,其实就是缓存的ajax、fetch的response,将这些api请求的响应体进行缓存后,就可以在离线状态下使用。其中:urls配置api的urlcacheConfig配置具体的缓存策略:maxSize 缓存的最大条目数或响应数,太多则会暂用系统资源maxAge 过期时间,该项与下面提到的strategy策略配合,如果设置过长,容易呈现老资源给用户。timeout是指的应用发起真实网络请求后的等待时间,如果超时将会配合下面提到的strategy进行动作strategy策略,选择’performance’会直接拦截网络请求,返回缓存(前提是有缓存,并且没有超过maxAge的时间),选择’freshness’会在timeout超时的时候返回缓存。与service worker通讯与service worker通讯可以让我们主动做很多事情,而不是仅仅依赖于ngsw-config.json配置,通过依赖注入一个SwUpdate服务,我们可以主动要求查询、更新、激活应用的版本,(这部分内容笔者还未投入应用,详见官网描述)总结这篇文章我们分享了如何在angular里面使用service worker 进行离线场景的增强,其中包括引入@angular/pwa安装http-server,模拟生产环境配置ngsw-config.json 缓存策略简单描述了与service worker通讯的概念相信今后angular框架能够在pwa应用方便给我们更多的方便。 ...

January 31, 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

使用 ale.js 制作一个小而美的表格编辑器(4)

今天来教大家如何使用 ale.js 制作一个小而美的表格编辑器,首先先上 gif:是不是还是有一点非常 cool 的感觉的?那么我们现在开始吧!这是我们这篇文章结束后完成的效果(如果想继续完成请访问第五篇文章):ok,那继续开始吧(本篇文章是表格编辑器系列的第四篇文章,如果您还没有看过第一篇,请访问 第一篇文章(开源中国)):首先我们需要先添加一个 Sreach 按钮(在 handleTemplateRender 函数里)://把 定义DOM基本结构 的 returnValvar returnVal = “<table><thead><tr>”//改为var returnVal = “<table><thead><button class=‘a-btn’ onclick=‘this.methods.trigSearch()’>Search</button><tr>“之后我们需要在 methods 里面添加一个 trigSearch 函数:trigSearch(){ if (this.data.isOpenSearch) { this.data.data = this.staticData.preData; this.data.isOpenSearch = false; } else { this.data.isOpenSearch = true; }}接下来,我们需要在 data 里添加一个 isOpenSearch 变量,默认为 false:isOpenSearch: false还要在 staticData 里添加一个 preData,用来存储 bookData 数据:preData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”]]之后我们要在 handleTemplateRender 函数中增加一个判断,判断是否 openSearch 开启了://把//循环遍历bookHeader数据并输出this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”;})returnVal += “</thead></tr><tbody>”;//改为//循环遍历bookHeader数据并输出this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”;})var cellId = -1;if (this.data.isOpenSearch) { //这里增加判断 returnVal += “</tr><tr>”; for (var i = 0; i < this.data.bookHeader.length; i++) { cellId++; returnVal += “<th><input data-cell=’” + cellId + “’ type=‘text’ oninput=‘this.methods.handleSearch(this, event)’ placeHolder=‘Search…’></th>”; }}returnVal += “</thead></tr><tbody>";然后我们要继续在 methods 里面添加一个名叫 handleSearch 的函数:handleSearch(el, e) { var newData = [], elVal = el.value; this.staticData.preData.forEach(function(val, i, arr) { //判断是否拥有输入的字段 if (val[e.target.dataset.cell].indexOf(elVal) !== -1) { //添加到返回对列中 newData.push(val); } }); this.data.bookData = newData;}现在我们就已经实现了搜索功能,恭喜!这是我们目前全部的 js 代码:Ale(“excel”, { template() { return this.methods.handleTemplateRender(); }, methods: { handleTemplateRender() { //定义DOM基本结构 var returnVal = “<table><thead><button class=‘a-btn’ onclick=‘this.methods.trigSearch()’>Search</button><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1, edit = this.data.edit; //循环遍历bookHeader数据并输出 this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”; }) var cellId = -1; if (this.data.isOpenSearch) { returnVal += “</tr><tr>”; for (var i = 0; i < this.data.bookHeader.length; i++) { cellId++; returnVal += “<th><input data-cell=’” + cellId + “’ type=‘text’ oninput=‘this.methods.handleSearch(this, event)’ placeHolder=‘Search…’></th>”; } } returnVal += “</thead></tr><tbody>”; //循环遍历bookData数据并输出 this.data.bookData.forEach(function(thisBook, i, arr) { var cellId = -1; rowId++; //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { cellId++; if (rowId === edit.row && cellId === edit.cell) { returnVal += “<td><form data-cell=’” + cellId + “’ data-row=’” + rowId + “’ onsubmit=‘this.methods.save(event)’><input type=‘text’ value=’” + val + “’></form></td>”; } else { returnVal += “<td data-cell=’” + cellId + “’ data-row=’” + rowId + “’ ondblclick=‘this.methods.handleBlockOndblclick(event)’>” + val + “</td>”; } }) returnVal += “</tr>”; }) returnVal += “</tbody></table>”; //返回DOM结构 return returnVal; }, handleTheadOnclick(e) { this.methods.changeSortType(e); this.methods.sortList(e); }, changeSortType(e) { this.staticData.sortBy = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.staticData.sortType = “down”; } else { this.staticData.sortType = “up”; } }, sortList(e) { var index = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) > b[index].charCodeAt(0) ? 1 : -1; }) } else { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) < b[index].charCodeAt(0) ? 1 : -1; }) } this.data.bookData = this.data.bookData; }, getSortSign() { if (this.staticData.sortType === “up”) { return ‘\u2191’; } else { return ‘\u2193’; } }, handleBlockOndblclick(e) { if (!this.staticData.isOpenEdit) { this.staticData.isOpenEdit = true; this.data.edit = { row: parseInt(e.target.dataset.row), cell: parseInt(e.target.dataset.cell) } } }, save(e) { e.preventDefault(); var input = e.target.firstChild; this.staticData.isOpenEdit = false; this.data.edit = { row: -1, cell: -1 } this.data.bookData[e.target.dataset.row][e.target.dataset.cell] = input.value; this.data.bookData = this.data.bookData; }, trigSearch() { if (this.data.isOpenSearch) { this.data.bookData = this.staticData.preData; this.data.isOpenSearch = false; } else { this.data.isOpenSearch = true; } }, handleSearch(el, e) { var newData = [], elVal = el.value; this.staticData.preData.forEach(function(val, i, arr) { if (val[e.target.dataset.cell].indexOf(elVal) !== -1) { newData.push(val); } }); this.data.bookData = newData; } }, data: { bookHeader: [ “Book”, “Author”, “Language”, “Published”, “Sales” ], bookData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”] ], edit: { row: -1, cell: -1 }, isOpenSearch: false }, staticData: { sortBy: -1, sortType: ‘down’, isOpenEdit: false, preData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”] ] } }) Ale.render(“excel”, { el: “#app” })如果想了解更多,欢迎关注我在明天推出的第五篇教程,同时也关注一下 alejs 哦,感谢各位!(非常重要:如果有能力的话不妨去 Github 或 码云 上 star 一下我们吧!不过如果您特别喜欢 alejs 的话也可以 watch 或 fork 一下哦!十分感谢!) ...

January 21, 2019 · 4 min · jiezi

angular组件双向绑定

在写项目时,需要编写一个组件,根据用户选择的单选框返回值,就像组件的双向绑定。组件的双向绑定就是子组件接受父组件的数据,父组件监听子组件的事件来修改自己的值.子组件定义事件发射器@Output(‘stateChange’) instrumentStateChange = new EventEmitter<number>();暴露一个stateChange属性,当state值变化时,就把state值发射给父组件。定义输入属性@Input(‘state’) set instrumentState(state: number) { this.instrumentStateChange.emit(state); this._instrumentState = state; }定义一个输入属性,当他的值变化时,就用时间发射器将值发射出去父组件<app-instrument-state [state]=“state” (stateChange)=“changeState($event)"></app-instrument-state> <p>当前状态:{{state}}</p>定义一个属性传输数据,一个方法修改属性值changeState(event: any) { this.state = event; }效果:双向绑定语法糖双向绑定语法 [(state)]=state 等价于 => [state]=state (stateChange)=“state=$event"采用双向绑定语法,就不用定义监听的函数了,方便使用.<app-instrument-state [(state)]=“state”></app-instrument-state> <p>当前状态:{{state}}</p>这样写效果也是一样的.

January 19, 2019 · 1 min · jiezi

angualr异常处理

在前两天的时候本打算对一个angualr的项目的前台进行统一的异常处理,虽然最后放弃了这个打算,但通过这个还是学到了不少东西,感觉还是值得写篇文章加强一下记忆的。Errorhandler通过这次我发现自己以前对异常的理解也是有问题的,以前一直以为各种错误都算是异常,http的错误也是异常,然后一谷歌angualr全局异常处理,好多文章都是介绍Errorhanler的,官方文档介绍如下:提供用于集中异常处理的挂钩。看意思,很符合自己需求(在http错误也算异常的情况下),而且使用方法也很简单。先定义一个异常处理类,并在其中写下应该如何处理异常:class MyErrorHandler implements ErrorHandler { handleError(error) { // do something with the exception }}接着配置一下 Provider:@NgModule({ providers: [{provide: ErrorHandler, useClass: MyErrorHandler}]})class MyModule {}angular的简单全局异常处理就完成了,如果想要更完善的异常处理,比如把异常信息发送到服务器可以看这篇文章在配置完成后,发现自己的http错误完全没有触发这个方法,但是在某种情况下又可以触发,说明自己的配置并没错,只能理解为http错误不算异常了。用专门的servic处理那对于http异常又该如何呢? angular官方文档中是先把信息交给一个服务,然后再在服务中处理这个异常错误处理showConfig() { this.configService.getConfig() .subscribe( (data: Config) => this.config = { …data }, // success path error => this.error = error // error path );}获取错误详情private handleError(error: HttpErrorResponse) { if (error.error instanceof ErrorEvent) { // A client-side or network error occurred. Handle it accordingly. console.error(‘An error occurred:’, error.error.message); } else { // The backend returned an unsuccessful response code. // The response body may contain clues as to what went wrong, console.error( Backend returned code ${error.status}, + body was: ${error.error}); } // return an observable with a user-facing error message return throwError( ‘Something bad happened; please try again later.’);};retry()有时候,错误只是临时性的,只要重试就可能会自动消失。 比如,在移动端场景中可能会遇到网络中断的情况,这种情况我们可以让他自动重试几次,angualr为我们提供了这种方法getConfig() { return this.http.get<Config>(this.configUrl) .pipe( retry(3), // retry a failed request up to 3 times catchError(this.handleError) // then handle the error );}如果想要这样,所有已写方法都得改写,并且感觉并没有优雅多少。感觉没有必要。拦截器再之后就是用拦截器来处理,这个方法倒是简单,也不需要对已写方法进行什么改动,import { Injectable } from ‘@angular/core’;import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor, HttpResponse, HttpErrorResponse,} from ‘@angular/common/http’; import { Observable } from ‘rxjs/Observable’;import ‘rxjs/add/operator/do’; @Injectable()export class RequestInterceptor implements HttpInterceptor { constructor() {} intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(request).pipe(tap((event: HttpEvent<any>) => {}, (err: any) => { if (err instanceof HttpErrorResponse) { // do error handling here } })); }} ...

January 18, 2019 · 1 min · jiezi

使用 ale.js 制作一个小而美的表格编辑器(3)

今天来教大家如何使用 ale.js 制作一个小而美的表格编辑器,首先先上 gif:是不是还是有一点非常 cool 的感觉的?那么我们现在开始吧!这是我们这篇文章结束后完成的效果(如果想继续完成请访问第四篇文章):ok,那继续开始吧(本篇文章是表格编辑器系列的第三篇文章,如果您还没有看过第一篇,请访问 第一篇文章(开源中国)):首先让我们把每一个列表项都添加一个他们的行数和列数作为 dataset 数据吧!先创建一个 rowId 变量://在 handleTemplateRender 函数里,我们把:var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy;//改为var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1;然后再在 “循环遍历bookData数据并输出” 这行注释所对应的forEach函数的里面创建一个名叫 cellId 的变量:(就是输出td标签的forEach)//原来的代码this.data.bookData.forEach(function(thisBook, i, arr) { //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { //输出一列 returnVal += “<td>” + val + “</td>”; }) returnVal += “</tr>”;})//改为this.data.bookData.forEach(function(thisBook, i, arr) { var cellId = -1; //这里增加了一行代码 //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { //输出一列 returnVal += “<td>” + val + “</td>”; }) returnVal += “</tr>”;})当然这样还没完,我们还需要改为这样:this.data.bookData.forEach(function(thisBook, i, arr) { var cellId = -1; //这里让rowId++ rowId++; returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { //这里让cellId++ cellId++; //注意这里写了 dataset returnVal += “<td data-row=’” + rowId + “’ data-cell=’” + cellId + “’>” + val + “</td>”; }) returnVal += “</tr>”;})这样你就可以看到在控制台上已经输出了它们的 dataset:接下来,让我们往 data 里面添加一个名叫 edit 的对象,用来指定我们点击的到底是哪个表格:edit: { row: -1, //默认为-1,因为没有选中表格 cell: -1}然后,我们把下面这行代码,给他添加一个 ondblclick:returnVal += “<td data-row=’” + rowId + “’ data-cell=’” + cellId + “’>” + val + “</td>”;//改为newVal += “<td data-cell=’” + cellId + “’ data-row=’” + rowId + “’ ondblclick=‘this.methods.handleBlockOndblclick(event)’>” + val + “</td>";然后我们在 methods 对象里面添加一个 handleBlockOndblclick 的函数:handleBlockOndblclick(e) { if (!this.staticData.isOpenEdit) { //判断是否开启了edit this.staticData.isOpenEdit = true; //获取并设置目标格位置 this.data.edit = { row: parseInt(e.target.dataset.row), cell: parseInt(e.target.dataset.cell) } }}因为在 handleBlockOndblclick 函数里面,我们用到了静态数据的 isOpenEdit,所以我们需要定义一个:isOpenEdit: falseok,那么之后我们需要再改进一下输出 book 数据的那一行代码,把他改成这样:thisBook.forEach(function(val, i, arr) { cellId++; if (rowId === edit.row && cellId === edit.cell) { returnVal += “<td><form data-cell=’” + cellId + “’ data-row=’” + rowId + “’ onsubmit=‘this.methods.save(event)’><input type=‘text’ value=’” + val + “’></form></td>”; } else { returnVal += “<td data-cell=’” + cellId + “’ data-row=’” + rowId + “’ ondblclick=‘this.methods.handleBlockOndblclick(event)’>” + val + “</td>”; }})接下来让我们在上方定义一个名叫 edit 的变量吧://把var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1;//改为var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1, edit = this.data.edit;之后我们还需要在 methods 里添加一个 save 函数,用来保存修改后的结果:save(e) { e.preventDefault(); var input = e.target.firstChild; this.staticData.isOpenEdit = false; this.data.edit = { row: -1, cell: -1 } this.data.bookData[e.target.dataset.row][e.target.dataset.cell] = input.value; this.data.bookData = this.data.bookData;}好了,那么现在我们的编辑器就可以正式运作了,我们已经实现了本篇文章最开始时所做的功能!(按回车可以保存修改结果)这是我们目前全部的 js 代码:Ale(“excel”, { template() { return this.methods.handleTemplateRender(); }, methods: { handleTemplateRender() { //定义DOM基本结构 var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy, rowId = -1, edit = this.data.edit; //循环遍历bookHeader数据并输出 this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”; }) returnVal += “</thead></tr><tbody>”; //循环遍历bookData数据并输出 this.data.bookData.forEach(function(thisBook, i, arr) { var cellId = -1; rowId++; //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { cellId++; if (rowId === edit.row && cellId === edit.cell) { returnVal += “<td><form data-cell=’” + cellId + “’ data-row=’” + rowId + “’ onsubmit=‘this.methods.save(event)’><input type=‘text’ value=’” + val + “’></form></td>”; } else { returnVal += “<td data-cell=’” + cellId + “’ data-row=’” + rowId + “’ ondblclick=‘this.methods.handleBlockOndblclick(event)’>” + val + “</td>”; } }) returnVal += “</tr>”; }) returnVal += “</tbody></table>”; //返回DOM结构 return returnVal; }, handleTheadOnclick(e) { this.methods.changeSortType(e); this.methods.sortList(e); }, changeSortType(e) { this.staticData.sortBy = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.staticData.sortType = “down”; } else { this.staticData.sortType = “up”; } }, sortList(e) { var index = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) > b[index].charCodeAt(0) ? 1 : -1; }) } else { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) < b[index].charCodeAt(0) ? 1 : -1; }) } this.data.bookData = this.data.bookData; }, getSortSign() { if (this.staticData.sortType === “up”) { return ‘\u2191’; } else { return ‘\u2193’; } }, handleBlockOndblclick(e) { if (!this.staticData.isOpenEdit) { this.staticData.isOpenEdit = true; this.data.edit = { row: parseInt(e.target.dataset.row), cell: parseInt(e.target.dataset.cell) } } }, save(e) { e.preventDefault(); var input = e.target.firstChild; this.staticData.isOpenEdit = false; this.data.edit = { row: -1, cell: -1 } this.data.bookData[e.target.dataset.row][e.target.dataset.cell] = input.value; this.data.bookData = this.data.bookData; } }, data: { bookHeader: [ “Book”, “Author”, “Language”, “Published”, “Sales” ], bookData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”] ], edit: { row: -1, cell: -1 } }, staticData: { sortBy: -1, sortType: ‘down’, isOpenEdit: false }})Ale.render(“excel”, { el: “#app”})如果想了解更多,欢迎关注我在明天推出的第四篇教程,同时也关注一下 alejs 哦,感谢各位!(非常重要:如果有能力的话不妨去 Github 或 码云 上 star 一下我们吧!不过如果您特别喜欢 alejs 的话也可以 watch 或 fork 一下哦!十分感谢!) ...

January 17, 2019 · 4 min · jiezi

[WIP] 一个基于Angular最新版搭建的博客类小项目

前言:因为项目需要学习了一下Angular,按照官网的教程做了一个简单粗糙的小项目。大致的功能有博客的新建,删除,查看详情,列表展示。使用内存Web API模拟远程服务器,进行数据操作。临时安排的变动,可能没有太多的时间持续研究,所以现在这里记录一下。以下是我边学习边记的一些笔记,方便日后回来可以快速回忆起相关技术点。路由一个配置了路由的Angular应用有一个路由服务的单例。当浏览器的url改变的时候,路由就会去配置里找到相应的路由信息,根据该路由就可以决定展示哪一个组件。路由器会把类似URL的路径映射到视图而不是页面。浏览器本应该加载一个新页面,但是路由器拦截了这一行为。<router-outlet>标签官网上说它起到了placeholder的作用,当路由根据url找到要展示的组件时,就会把这个组件填充到<router-outlet>中去。获取路由中的参数信息: +this.route.snapshot.paramMap.get(‘id’);route.snapshot 是一个路由信息的静态快照,抓取自组建刚刚创建完毕之后paramMap是一个从URL中提取的路由参数值的字典。javascript(+)操作符会把字符串转为数字数据绑定用指令 [(ngModel)]=“data.content"可实现遇到报错 在app.module中导入import { FormsModule } from ‘@angular/forms’;HTTP获取资源主要是使用了common包下的HttpClientModule这一个模块,使用它则需要把它先导入进app.module中,然后在需要用到的service中导入相应的模块,例如HttpClient,HttpHeaders等。HttpClient提供了很丰富的Http协议的请求方法。实际使用时可参考源码。请求失败处理:catchError() 操作符会拦截失败的 Observable。 它把错误对象传给错误处理器,错误处理器会处理这个错误。因此异常被处理后,浏览器的控制台不会报错。状态管理 ngrx与Redux的关系:ngrx/store的灵感来源于Redux,是一款集成RxJS的Angular状态管理库,由Angular的布道者Rob Wormald开发。它和Redux的核心思想相同,但使用RxJS实现观察者模式。它遵循Redux核心原则,但专门为Angular而设计。Actions(行为)是信息的载体,它发送数据到reducer(归约器),然后reducer更新store(存储)。Actions是store能接受数据的唯一方式。组件化思想把所有特性都放在同一个组件中,将会使应用变得难以维护。因此需要把大型组件分成小一点的子组件,每个子组件都要集中经历处理某个特定的任务或者工作流。依赖注入DI,是一种重要的应用设计模式。实现方式:类从外部源中请求获取依赖,无需自己创建。DI框架会在实例化该类时向其提供这个类所声明的依赖项。@Injectable()装饰器是每个Angular服务定义中的基本要素,把当前类标志为可注入的服务。注入器:负责创建服务实例提供商:告诉注入器如何创建实例服务组件不直接获取或者保存数据,而是把这一块的功能放在service中,让组件专注于如何展示数据,如此开发使得项目的每个模块功能更加明确。service里的方法应该都是异步的。Observable(可观察对象)的版本可以实现。ES6相关一个模块从另一个模块import进来,若是一个值 :直接取值取到的是一个会被缓存的结果(例如获取WebStorage中更新的值,必须要刷新页面)函数,获取一个值的引用,会调用该函数,取到最新的值控制台输出与Debug有时候会发现控制台输出的结果与直接在js文件里进行debug所显示的结果不一致,那是因为,console.log()采用了懒加载的机制,它展示的是一个引用的值,当你点击三角箭头的时候,它会去加载当前变量的值。但debug的数据具有“实时性”。不一致的情况经常出现在一些异步代码中。如果你看到这里,觉得内容有误或有可改进之处,欢迎你的提出~

January 17, 2019 · 1 min · jiezi

2019 年值得学习的顶级 JavaScript 框架与主题

图:Jon Glittenberg Happy New Year 2019 (CC BY 2.0)又到了一年的这个时候:JavaScript 年度技术生态回顾。我们的目标是找出最有职业投资回报率的主题和技术。在实际生产中大家都在用什么呢?现在的趋势是什么样的呢?我们不会试图去找出最佳,但是会使用数据驱动的方法,来帮助大家着重关注那些能帮助你在求职面试中回答“你知道 __ 吗?”的主题与技术。我们不会去分析哪些是最快的,哪个有最好的代码质量。我们会假设它们都是速度恶魔(speed demons),并且它们都很棒,足以完成你的工作。而主要的关注点在于:什么是被大规模使用的?组件框架我们要关注的大问题是当前组件框架的状况。我们会主要关注三巨头:React、Angular 和 Vue.js,主要因为在工作中,它们的使用远远超过了其他的框架。去年,我注意到了 Vue.js 的(使用量)增长并提到了它可能在 2018 年赶上 Angular。事实上它没有发生,但 Vue.js 的增长仍然非常快。我也预测了将 React 用户转化为其他框架用户将会更加困难,因为 React 比 Angular 有更高的用户满意度 — React 用户并不会有充分的理由去切换框架。与我对 2018年的预期一致。React 在 2018 年牢牢占据了头把交椅。但有趣的是,三个框架每年仍持续着指数级的增长。预测:React 在 2019 将继续领先在我们关注 React 的第三年,它 相比 Angular 仍有更高的满意度,而且对于挑战者,它不会放弃任何优势。目前看来我认为在 2019 没有能够挑战它地位的框架。除非有超级强大的东西出现并且扰乱了 React(社区),React 将会在 2019 年底继续领先。说到 React,它一直在变得更好。最新的 React hooks API 取代了我从 0.14 版本开始就几乎不能忍受的 class API。(class API 仍然可以继续使用,但是 hooks API 真的 更好)。React 的 API 改进如更好的代码分割和并发渲染(详情)将使它在 2019 年更难被打败。不用怀疑,React 现在是目前对开发者最友好的前端框架。我没有理由不推荐它。数据来源我们会关注一些关键点来评估在(这些框架)实际生产中的兴趣和使用情况:Google 搜索的趋势。这并不是我最喜欢的指标,但是它是个不错的宏观视角。包下载量。这里的目的是获取使用框架的真实用户(数据)。Indeed.com 上的招聘广告。用和去年相同的方法论来保持结果的一致性。Google 搜索趋势框架搜索趋势:2014 年 1 月 — 2018 年 12 月在搜索趋势上,React 在 2018 年 1 月超越了 Angular,并且在这一整年剩余的时间里保持了领先的位置。Vue.js 在图里保持了一个可见的位置,但是仍然是搜索趋势中的一个小因子。对比:去年的趋势图:框架搜索趋势:2014 年 1 月 — 2017 年 12 月包下载量包下载量是一个衡量实际使用情况的公平指标,因为开发者在工作是会频繁地下载那些他们需要的包。睿智的读者会发现有时候他们从他们公司内部源的下载包,对于这种情况,我的回答是:“那确实会发生 — 对于这三个框架来说。”它们都可以在企业中立足,而我对这个大规模的数据的平均能力有信心。React 每月下载量:2014–2018Angular 每月下载量:2014–2018Vue 每月下载量:2014–2018让我们看一下下载份额的快速可视化比较:“但你忘记了 Angular 1.0!它在企业中仍然很重要。”不,我没有。Angular 1.0 仍然在企业中被广泛使用,这和 Windows XP 在企业中仍被广泛使用是相似的。这个数量绝对足够引起注意,但是新版本的 Angular 早已使 Angular 1.0 相形见绌,Angular 1.0 的重要性已经不如其他的框架了。为什么?因为整个软件行业和 所有部门(包括企业) 的 JavaScript 的使用增长得很快,新的框架会使旧的框架变得很渺小,即使它是 永不升级 的遗产应用。证据就是,看看这些下载量统计图。2018 年单年的下载量就比之前几年的 总和 都要多。招聘广告投放数Indeed.com 集合了许多招聘部门的招聘广告。每年 我们都会统计提到每个框架的招聘广告¹ 来给大家提供关于企业在招什么样的人的更好的观点。这是今年的形势:2018 年 12 月有关每个框架的招聘广告统计React:24,640Angular:19,032jQuery:14,272Vue:2,816Ember(不在图中):2,397再说一次,今年投放的职位总数比去年要多。我把 Ember 剔除了,因为它显然没有像其他框架一样按比例增长。我不推荐为了未来找工作而去学它。jQuery 和 Ember 相关的岗位并没有多大的变化,但其他的岗位都有很大的增长。令人感激的是,加入软件工程领域的新人在 2018 年也增长了很多,但这也意味着我们也需要持续聘用并培训初级开发者(意味着我们需要 合格的高级开发者来指导他们),否则我们将无法跟上爆炸性的就业增长。作为对比,这里有去年的图表:平均薪资在 2018 年也攀升了,从每年 $110k 到每年 $111k。有传闻说,薪资列表落后于新员工的预期,并且如果招聘经理不去适应开发者的市场,不给出更多的加薪,他们会更难雇佣和留住开发者。留人和物色人才在 2018 仍然会是一个巨大的问题,因为雇员们会跳槽到别处有更高工资的职位。方法论:职位搜索是在 Indeed.com 上进行的。为了去除误报,我把它们和搜索词 “software” 组合在一起来加强相关度,然后乘以 1.5(粗略地说,就是使用关键词 “software” 和不用这个关键词搜索到的编程岗位列表的区别)。所有 SERPS 都按照日期排序并检查相关性。结果数据并不是 100% 准确的,但它们对于在本文中使用的相对近似值足够好了。JavaScript 基础我每年都在说:关注基础。今年你会得到更多的帮助。所有的软件开发都是这样组合的过程:把复杂的问题拆解成多个小问题,并将那些小问题组合起来,组成你的应用。但当我问 JavaScript 的面试者那些软件工程最基本的问题,如“什么是函数组合”和“什么是对象组合”,他们几乎总是回答不出这些问题,尽管他们每天都在做这些事。我一直认为这是一个需要解决的严重问题,所以我写了这个主题:“Composing Software”。如果你在 2019 年没有要学的了,那么就去学组合式编程吧。On TypeScriptTypeScript 在 2018 年持续增长,并且它会被持续高估,因为 类型安全并不是什么大问题(并没有很好地减少产品的 bug 密度),并且在 JavaScript 中,类型推断 不需要 TypeScript 的帮助也可以做得很好。你甚至可以在使用 Visual Studio Code 时,通过 TypeScript 引擎在普通的 JavaScript 中进行类型推断。或者为你喜爱的编辑器安装 Tern.js 插件。对于大部分高阶函数而言,TypeScript 会继续一败涂地。大概是因为我不知道怎样正确使用它(在与它日常相伴多年后 — 在这种情况下,他们真的需要提高可用性或者文档,或者两者都要),但我仍然不知道在 TypeScript 中如何定义 map 操作的类型,而它似乎在 transducer 中很清晰明了。捕获错误经常失败,并且经常报明明不是错误的错误。可能对于支持我所认为的软件,它仅仅是不够灵活或者功能不够完善。但我仍然对有一天它会加入我们需要的功能抱有希望,因为它的缺点在我尝试在真实项目中使用它时令我失望,但我仍然喜欢它在有用的时候能够合适地(并且可选择地)定义类型的潜力。我目前的评价:非常酷的选择,有限的使用场景,但被高估了,笨拙,并且在大型生产应用中的投资回报率很低。这非常讽刺,因为 TypeScript 自称是 “JavaScript 的超集”。可能他们要加入一个词:“笨拙的 JavaScript 超集”。在 JavaScript 里我们需要的是一个比 Haskell 更强大但是比 Java 更轻量的类型系统。(PS:这句翻译不确定,麻烦校对看下)其他值得学习的 JavaScript 技术用于请求服务端的 GraphQL用于管理应用状态的 Redux用于独立管理副作用的 redux-sagareact-feature-toggles 来简化持续交付和测试RITEway 来编写美观、可阅读的单元测试加密行业的崛起去年我预测区块链和金融会计将会成为 2018 年值得观察的重要技术。这个预测是正确的。2017 - 2018 的一个主要的主题是加密行业的崛起和构建价值网络的基础。记住这个阶段。你很快将会多次听到它。如果你和我一样自从 P2P 爆炸性增长后关注那些去中心化应用,这已经持续很久了。由于比特币点燃了导火索,并展示了去中心化应用通过加密货币自我维持的方式,这种爆炸性增长是不可阻挡的了。比特币在几年内增长了若干个量级。你可能听说过 2018 年是“加密寒冬”,并且有“加密行业处于挣扎中”的想法。这完全是无稽之谈。实际的情况是,在 2017 年底,比特币以史诗般的指数增长曲线增长到之前的 10 倍,但市场有所回落,这种回落会发生在每次比特币增长到之前的 10 倍。比特币 10 倍拐点在这个图表中,每个箭头始于 10 倍点,指向价格修正后的最低点。加密货币的 ICO(首次代币发行)的资金募集在 2018 年初达到顶峰。2017-2018 的资金泡沫带来了生态系统中大量新的职位空缺,在 2018 年 1 月达到了顶峰,有超过 10k 的职位空缺。这种趋势已经回落到大概 2400 个职位空缺了(根究 Indeed.com 的数据),但是我们现在仍处于(这个行业的)早期阶段,这场派对才刚开始。关于迅猛增长的加密行业有很多可以讨论的地方,但是这可以另写一篇博文了。如果你感兴趣的话,可以阅读:“Blockchain Platforms and Tech to Watch in 2019”。其他值得观察的技术和去年预测的一样,这些技术在 2018 持续爆炸性增长:人工智能/机器学习 正如火如荼,在 2018 年末有 30k 的职位空缺。deep fakes,令人难以置信的生成艺术,来自 Adobe 这样的公司的研究团队研发的令人惊讶的视频编辑能力 — 从来没有更激动人心的去探索人工智能时刻。渐进式 Web 应用(PWA) 迅速成为了构建现代应用的方式 — 增加的新特性与有 Google、Apple、Microsoft、Amazon 等公司的支持。令我难以置信的是,我将手机上的 PWA 视为理所当然。例如,我在我的手机上不再需要安装 Twitter 的原生应用。我仅仅使用 Twitter 的 PWA 来替代它。AR(增强现实)、VR(虚拟现实)、MR(混合现实)像战神金刚一样合体成 XR(eXtended Realty)。未来的全时 XR 沉浸即将到来。我预测在 5-10 年内会出现大规模的消费级 XR 眼镜产品。隐形眼镜会在 20 年内推出。这个行业在 2018 年有数以千计的新职位空缺,并且在 2019 仍会持续爆炸性增长。YouTube 视频链接:https://youtu.be/JaiLJSyKQHk机器人、无人机和自动驾驶汽车:在 2018 年末,自动飞行的无人机已经被研发出来了,自动机器人仍在持续优化中,并且有更多自动驾驶汽车上路了。2019 年,以及未来的 20 年,这些技术会持续增长并重塑我们周围的世界。量子计算 和预期的一样在 2018 发展得极好,并且和预期的一样,它仍然没有成为主流。事实上,我的预测“它会在 2019 或者在真正中断之前成为主流”可能太乐观了。加密领域的研究者已经集中更多的注意力在量子安全加密算法上(量子计算会打破今天的计算成本昂贵的假设,而加密正是依赖于这些成本昂贵的计算),但尽管在 2018 年不断涌现出有趣的研究进展,最近有一篇报道 换了个角度看待这个问题:“在 2000 到 2017 年间,量子计算已经 11 次上了 Gartner 的 hype list,每次都在 hype cycle 的最早阶段就被列出,并且每次都说已经距离我们有十年之遥。”这让我想起了早期人工智能的努力,它在 1950 年代开始升温,在 1980 和 1990 年代有了有限的但是有趣的成果,但是在 2010 年左右的成果才开始变得令人兴奋。我们正在构建未来的名人数字藏品:cryptobling。Eric Elliott 是 “编写 JavaScript 应用”(O’Reilly)以及“跟着 Eric Elliott 学 Javascript” 两书的作者。他为许多公司和组织作过贡献,例如 Adobe Systems、Zumba Fitness、The Wall Street Journal、ESPN 和 BBC 等,也是很多机构的顶级艺术家,包括但不限于 Usher、Frank Ocean 以及 Metallica。大多数时间,他都在 San Francisco Bay Area,同这世上最美丽的女子在一起。感谢 JS_Cheerleader。原文地址:Top JavaScript Frameworks and Topics to Learn in 2019原文作者:Eric Elliott译文出自:掘金翻译计划本文永久链接:https://github.com/xitu/gold-miner/blob/master/TODO1/top-javascript-frameworks-and-topics-to-learn-in-2019.md译者:ElizurHz校对者:KarthusLorin, wuzhengyan2015 ...

January 16, 2019 · 2 min · jiezi

使用 ale.js 制作一个小而美的表格编辑器(2)

今天来教大家如何使用 ale.js 制作一个小而美的表格编辑器,首先先上 gif:是不是还是有一点非常 cool 的感觉的?那么我们现在开始吧!这是我们这篇文章结束后完成的效果(如果想继续完成请访问第三篇文章):ok,那继续开始吧(本篇文章是表格编辑器系列的第二篇文章,如果您还没有看过第一篇,请访问 第一篇文章(开源中国)):首先我们写一个名叫 staticData 的 object,里面添加2个属性,分别是 sortBy 和 sortType:(关于 staticData 这里不做阐述,如果有需要请访问 cn.alejs.org)staticData: { sortBy: -1, //排序列索引,默认没有,所以为-1 sortType: ‘down’ //排序类型,默认为降序}之后我们在 th 标签里面增加一个 onclick 属性,指向 methods 里面的 handleTheadOnclick 函数,并传递一个 event 作为参数:(之前的代码)this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th>” + val + “</th>”;})(改进后的代码)this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + “</th>”;})为了让他显示排序时的小箭头,我们需要再改进这行代码为这样:this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”;})由于 sortBy 变量和 getSortSign 函数变量还未定义,所以我们要在之前的代码里引用一下:(原来的代码)var returnVal = “<table><thead><tr>";(改进后的代码)var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy;其中,sortBy 变量指向的是静态 data 里的 sortBy 变量,这个我们已经定义了,所以先不管他。而另一个 getSortSign 函数还没有定义,所以我们在 methods 里面定义一下他:getSortSign() { if (this.staticData.sortType === “up”) { return ‘\u2191’; } else { return ‘\u2193’; }}其功能主要就是判断是正序还是倒叙,并分别输出正反小箭头。之后我们就需要完成 handleTheadOnclick 函数了。它分别引用了 changeSortType 和 sortList 函数:handleTheadOnclick(e) { this.methods.changeSortType(e); this.methods.sortList(e);}其中 changeSortType 函数是用来改变排序类型的,而 sortList 函数使用来排序的。那么我们先完成 changeSortType 函数吧:changeSortType(e) { this.staticData.sortBy = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.staticData.sortType = “down”; } else { this.staticData.sortType = “up”; }}ok,这个函数的功能和实现都非常简单,其中 cellIndex 是用来获取这是属于表格中那一列的。那么 sortList 函数的实现则稍微有些复杂:sortList(e) { //获取列索引值 var index = e.target.cellIndex; //判断排序类型 if (this.staticData.sortType === “up”) { //使用数组的 sort 函数进行排序,分别按 charCode 进行排序 this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) > b[index].charCodeAt(0) ? 1 : -1; }) } else { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) < b[index].charCodeAt(0) ? 1 : -1; }) } this.data.bookData = this.data.bookData;}这是我们目前的全部 js 代码:Ale(“excel”, { template() { return this.methods.handleTemplateRender(); }, methods: { handleTemplateRender() { //定义DOM基本结构 var returnVal = “<table><thead><tr>”, getSortSign = this.methods.getSortSign, sortBy = this.staticData.sortBy; //循环遍历bookHeader数据并输出 this.data.bookHeader.forEach(function(val, i, arr) { returnVal += “<th onclick=‘this.methods.handleTheadOnclick(event)’>” + val + (sortBy === i ? getSortSign() : ‘’) + “</th>”; }) returnVal += “</thead></tr><tbody>”; //循环遍历bookData数据并输出 this.data.bookData.forEach(function(thisBook, i, arr) { //输出一行 returnVal += “<tr>”; thisBook.forEach(function(val, i, arr) { //输出一列 returnVal += “<td>” + val + “</td>”; }) returnVal += “</tr>”; }) returnVal += “</tbody></table>”; //返回DOM结构 return returnVal; }, handleTheadOnclick(e) { this.methods.changeSortType(e); this.methods.sortList(e); }, changeSortType(e) { this.staticData.sortBy = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.staticData.sortType = “down”; } else { this.staticData.sortType = “up”; } }, sortList(e) { var index = e.target.cellIndex; if (this.staticData.sortType === “up”) { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) > b[index].charCodeAt(0) ? 1 : -1; }) } else { this.data.bookData.sort(function(a, b) { return a[index].charCodeAt(0) < b[index].charCodeAt(0) ? 1 : -1; }) } this.data.bookData = this.data.bookData; }, getSortSign() { if (this.staticData.sortType === “up”) { return ‘\u2191’; } else { return ‘\u2193’; } } }, data: { bookHeader: [ “Book”, “Author”, “Language”, “Published”, “Sales” ], bookData: [ [“The Lord of the Rings”, " J. R. R. Tolkien”, “English”, “1954-1955”, “150 million”], [“The Little Prince”, “Antoine de Saint-Exupéry”, “French”, “1943”, “140 million”], [“Dream of the Red Chamber”, “Cao Xueqin”, “Chinese”, “1791”, “100 million”] ] }, staticData: { sortBy: -1, sortType: ‘down’ }})Ale.render(“excel”, { el: “#app”})然后效果就如下图所示啦:如果想了解更多,欢迎关注我在明天推出的第三篇教程,同时也关注一下 alejs 哦,感谢各位!(非常重要:如果有能力的话不妨去 Github 或 码云 上 star 一下我们吧!不过如果您特别喜欢 alejs 的话也可以 watch 或 fork 一下哦!十分感谢!) ...

January 16, 2019 · 3 min · jiezi

用Angular脚手架搭建项目

安装全局angular-cli首先你得安装好node和npm的环境。接着用命令:npm install -g @angular/cli(这个命令可以安装最新版)然后用 ng version命令检验是否成功用脚手架生成项目你可以选择在你想要新建项目的文件夹下打开命令行窗口输入下面这个命令 ng new your-project-name最后项目创建成功后,会有提示的(我没有截全)有一个快捷键的方式打开当前文件夹的cmd窗口:在目标文件夹下按住shift然后鼠标右键,就会有如下这个选项。启动项目我直接用npm start这个命令了项目启动完成这个项目版本算是挺老的了,后来自己又跑去升级了脚手架,把项目升到最新版(如图1所示)

January 12, 2019 · 1 min · jiezi

Angular6+ webpack自定义扩展

Angular6+ webpack自定义扩展背景在项目开发过程中,发现生产模式下console.log()日志文件依然存在,通过百度得出的结果是在生产模式下console.xx一系列方法全部重写window.console.log = ()=>{}这种方法表示一看就不舒服,无法接受。所以想着想着@angular/cli底层是webpack,而且代码压缩用的是UglifyJs,所以想着能不能扩展一配置项,让我把console全部给我过滤掉,最后去Issues上找了许久,发现angular6+不支持eject,最后有人推荐了一个工具库ngx-build-plus,不需要改很多东西就能在现有项目进行集成。接下来教大家如何使用,具体详情可以去github上找文档。如何使用1.运行 ng add ngx-build-plus,在angular7版本会自动一键配置好,但是6版本中可能会出现安装不成功,这时候请直接npm install ngx-build-plus –save-dev,然后angular.json文件中更改以下两处地方:“build”: { - “builder”: “@angular-devkit/build-angular:browser” + “builder”: “ngx-build-plus:build” …},“serve”: { - “builder”: “@angular-devkit/build-angular:dev-server” + “builder”: “ngx-build-plus:dev-server” …}2.接下来根目录下新建webpack.extra.js文件const UglifyJsPlugin = require(‘uglifyjs-webpack-plugin’);module.exports = { optimization: { minimizer: [new UglifyJsPlugin({ uglifyOptions: { compress: { drop_console: true } } })] }};记得npm install uglifyjs-webpack-plugin --save-dev3.进行生产环境编译ng build –extraWebpackConfig webpack.extra.js –prod4.好了就这么简单。写的比较简陋,有问题可以留言,实在没弄懂我就弄个示例出来。

January 6, 2019 · 1 min · jiezi

angular实现皮肤主题切换的方案

分配了个给现有angular6项目实现主题切换的效果,实现light dark明暗两种显示效果,我使用scss css预处理器方便开发。效果(因为项目需要保密,只截了一点点):切换样式 就是 实现css 的 变化,主要思路是通过在body 上控制自定义属性的值实现样式的控制,在body上 添加 data-theme-style=”dark”, 像这样:<body data-theme-style=“dark”> <app-root></app-root></body>我们通过data-theme-style的值 来控制样式,本项目中 值有 light,dark 分别代表明暗两套主题首先把切换主题 需要变化的 样式 抽离出来 定义成mixin,如:背景颜色,字体颜色,阴影,边框等,,这里以背景颜色 和 字体颜色举例:@mixin mixin-bg-color($dark-color,$light-color){ [data-theme-style=“dark”] & { background-color: $dark-color; } [data-theme-style=“light”] & { background-color: $light-color; }}@mixin mixin-font-color($dark-color,$light-color){ [data-theme-style=“dark”] & { color: $dark-color; } [data-theme-style=“light”] & { color: $light-color; }}在需要使用相关样式的选择器里应用mixin: @include mixin-font-color(blue,red)举个使用的例子,在data-theme-style=dark时 让 .title 类的字体颜色变为蓝色 在data-theme-style=light时 让 .title 类的字体颜色变为红色:@mixin mixin-font-color($dark-color,$light-color){ [data-theme-style=“dark”] & { color: $dark-color; } [data-theme-style=“light”] & { color: $light-color; }}.main{ .title{ @include mixin-font-color(blue,red) }}上方的scss 编译成css后的结果:[data-theme-style=“dark”] .main .title { color: blue; }[data-theme-style=“light”] .main .title { color: red; }这样一看 就很明白了。接下来就是实现切换到效果,我这里是点击一个按钮 实现 dark 和light 的来回切换,在html模板上:<button class=“switch-theme-btn” (click)=“changeTheme()">{{theme | uppercase}}</button>ts 代码如下,注释很清楚了: /** * 使用localStorage 存储主题的名称 * @param theme / saveTheme(theme): void { localStorage.setItem(theme, theme); } /* * 获取主题名称并设置到body / getTheme(): void { let theme = localStorage.getItem(theme); // 获取主题名称 if (!theme) { theme = dark; // 本地存储中没有theme的话 使用dark主题 } const body = document.getElementsByTagName(‘body’)[0]; body.setAttribute(‘data-theme-style’, theme); // 设置data-theme-style 属性 this.theme = theme; // 用于界面显示 } /* * 点击按钮 触发改变主题的方法 */ changeTheme(): void { const body = document.getElementsByTagName(‘body’)[0]; if (body.getAttribute(data-theme-style) === ‘dark’) { this.saveTheme(light); // 保存 this.getTheme(); // 更新获取 } else { this.saveTheme(dark); // 保存 this.getTheme(); // 更新获取 } }在组件的 ngOnInit() 生命周期 调用下 this.getTheme() 初始化。。做完这些 已经可以实现主题的切换了 ,但是 上方的 样式 写在公共的样式表里才有效,因为组件的样式只对对应的组件生效,使用[data-theme-style=”dark”]属性选择器无法匹配到对应的元素,该属性是定义在body上的,组件上肯定是没有的。 如何在组件的样式里生效呢,这个问题困扰了我一阵子,还是在官网文档找到答案::host-context 选择器有时候,基于某些来自组件视图外部的条件应用样式是很有用的。 例如,在文档的 元素上可能有一个用于表示样式主题 (theme) 的 CSS 类,你应当基于它来决定组件的样式。这时可以使用 :host-context() 伪类选择器。它也以类似 :host() 形式使用。它在当前组件宿主元素的祖先节点中查找 CSS 类, 直到文档的根节点为止。在与其它选择器组合使用时,它非常有用。参考:https://www.angular.cn/guide/…根据文档的说明,我们把前面的mixin改一下:@mixin mixin-font-color($dark-color,$light-color){ :host-context([data-theme-style=“dark”]) & { color: $dark-color; } :host-context([data-theme-style=“light”]) & { color: $light-color; }}.main{ .title{ @include mixin-font-color(blue,red) }}生成 的css 是这样的::host-context([data-theme-style=“dark”]) .main .title { color: blue; }:host-context([data-theme-style=“light”]) .main .title { color: red; }至此 大功告成,对你有帮助的话记得点个赞哦~~ ...

January 3, 2019 · 2 min · jiezi

【Angular】Angular 与 AngularJs 之间的纠缠不清

Angular 与 AngularJs 之间的纠缠不清市场关系Angular 和 AngularJS 是两个独立的产品:AngularJS 的官网是 Superheroic JavaScript MVW Framework;Angular 的官网是 One framework. Mobile & desktop.。官方的名称用法指导在 PRESS KIT - Angular Docs。Marketing/Branding 部分其实只是单纯的设定,不需要逻辑,所以记住就好。历史关系最初 Google 提供了 AngularJS 和 AngularDart 两个框架(或者叫一个框架的两个实现?),分别用于 JavaScript 和 Dart 的 Web 开发。(关于 AngularJS 的名称问题在 AngularJS - FAQ 中有明确阐述,整个官网中都使用的是 AngularJS 这个正式名称)Google 曾经计划基于 Web Components 技术重写 AngularJS 1.x,从而推出全新的 AngularJS 2.0,不过这个想法只在早期文档中存在,从未真正实现过。早在开发过程中,这个设计就已经被完全推翻掉了,新的框架也已经逐步不再使用 AngularJS 这个 Brand。在一段时间内,谷歌曾经试图默许使用 Angular 这个新 Brand 来包含已有的 AngularJS Brand(即 AngularJS 1.x 也可以被成为 Angular 1.x,而 2+ 仅称为 Angular。注意是包含而非取代,即原框架正式名称始终为 AngularJS),来实现无缝过度。事实上民间很早就使用 Angular 作为 AngularJS 的简称了,所以这个做法影响并不大,毕竟不是所有人都天天在看官方博客。随着名称混淆带来的各种问题,之后谷歌开始重新强调使用框架正式名称,即https://github.com/angular/an… 中的项目称为 AngularJS,https://github.com/angular/an… 中的项目称为 Angular。Angular 的核心目标就是替代 AngularJS,Google 官方也多次表示,只有等 Angular 的用户数量全面超过 AngularJS 之后才会停止对 AngularJS 的维护。其他的:Dart 是什么?最初设计 Dart,是 Google 的一帮程序员出于对 JavaScript 的不满,决定自己搞一个新语言用来替换 JavaScript 的,所以刚开始 Dart 也就是用来作为浏览器脚本运行在浏览器中的。Google 的这帮程序员万万没想到,看似并无大用的 JavaScript 居然因为 NodeJS 而焕发了第二春。于是 Dart 被这股浪潮遮掩了它的光芒,但是谷歌作为它的亲爸爸仍旧对它非常关照,在 Google 的未来操作系统 Fuchsia 中,Dart 被指定为官方的开发语言。参考文献《Angular 和 AngularJS 之间的关系?》《你想了解的 Dart》 ...

January 2, 2019 · 1 min · jiezi

Angular借助指令传递模板

上一篇中使用@Host() @Optional() public hc: HelloComponent感觉不够优雅,也不符合正常数据传递流程。下面是改造后的实现逻辑。在HelloComponent中使用ContentChildren获取所有HelloDirective。@Component({ selector: ‘hs-hello’, template: &lt;div&gt; &lt;ng-container *ngFor="let helloDirective of helloTemplates"&gt; &lt;p&gt;this is hello-for&lt;/p&gt; &lt;ng-container *ngTemplateOutlet="helloDirective.template"&gt;&lt;/ng-container&gt; &lt;/ng-container&gt; &lt;/div&gt; })export class HelloComponent { @ContentChildren(HelloDirective) helloTemplates: QueryList<HelloDirective>;}HelloDirective中将template向外暴露,借助指令传递TemplateRef,同时指令也起到了分类模板的作用@Directive({ selector: ‘[hsHello]’})export class HelloDirective { constructor( public template: TemplateRef<any> ) {}}示例<hs-hello> <ng-template hsHello> <p>this is hello-1</p> </ng-template> <ng-template hsHello> <p>this is hello-2</p> </ng-template> <ng-template hsHello> <p>this is hello-3</p> </ng-template></hs-hello>

December 28, 2018 · 1 min · jiezi

理解Angular2中的ViewContainerRef

原文链接:https://netbasal.com/angular-…作者:Netanel Basal译者:而井译者注:虽然文章标题写的是Angular2,但其实泛指的是Angular2+,读者可以将其运用到最新的Angular版本中。如果你曾经需要用编程的方式来插入新的组件或模版,你可能已经用过了ViewContainerRef服务了。在阅读了(许多)文章和问题后,我发现了许多(人)对于ViewContainerRef的疑惑,所以让我试着向你解释什么是ViewContainerRef。注意:本文不是关于如何用编程的方式来创建组件的(文章)。(译者注:只是为了阐述什么是ViewContainerRef)让我们回归到纯JavaScript上来开始(教程)。根据下面的标记,你的任务是添加一个新段落来作为当前(节点)的一个兄弟(节点)。<p class=”one”>Element one</p>为了简化(操作),让我们使用JQuery:$(’<p>Element two</p>’).insertAfter(’.one’);当你需要添加新的DOM元素(即:组件、模版)时,你需要一个可以插入这个元素的位置。Angular也没有什么黑魔法。它也只是JavaScript。如果你想插入新的组件或模版,你需要告诉Angular,哪里去放置这个元素。所以ViewContainerRef就是:一个你可以将新的组件作为其兄弟(节点)的DOM元素(容器)。(译者注:即如果你以某个元素或组件作为视图容器ViewContainerRef,对其新增的组件、模版,将成为这个视图容器的兄弟节点)用依赖注入来获取ViewContainerRef@Component({ selector: ‘vcr’, template: &lt;template #tpl&gt; &lt;h1&gt;ViewContainerRef&lt;/h1&gt; &lt;/template&gt; ,})export class VcrComponent { @ViewChild(’tpl’) tpl; constructor(private _vcr: ViewContainerRef) { } ngAfterViewInit() { this._vcr.createEmbeddedView(this.tpl); }}@Component({ selector: ‘my-app’, template: &lt;vcr&gt;&lt;/vcr&gt; ,})export class App {}我们在这个组件中注入了服务。在这个样例中,容器将指向你的宿主元素(vcr 元素),并且模版将作为vcr元素的兄弟(节点)被插入。用ViewChild来获取ViewContainerRef@Component({ selector: ‘vcr’, template: &lt;template #tpl&gt; &lt;h1&gt;ViewContainerRef&lt;/h1&gt; &lt;/template&gt; &lt;div&gt;Some element&lt;/div&gt; &lt;div #container&gt;&lt;/div&gt; ,})export class VcrComponent { @ViewChild(‘container’, { read: ViewContainerRef }) _vcr; @ViewChild(’tpl’) tpl; ngAfterViewInit() { this._vcr.createEmbeddedView(this.tpl); }}@Component({ selector: ‘my-app’, template: &lt;div&gt; &lt;vcr&gt;&lt;/vcr&gt; &lt;/div&gt; ,})export class App {}我们可以使用ViewChild装饰器来收集任何我们视图上的元素,并将其当作ViewContainerRef。在这个例子中,容器元素就是div元素,模版将作为这个div元素的兄弟(节点)被插入。你可以将ViewContainerRef用日志输出,来查看它的元素是什么:你可以在这里试玩这些代码。好了本文到此结束。译者附虽然作者已经说得很透彻了,但是由于动态插入组件、模版有很多种排列组合,我(译者)做了一些样例代码来辅助你理解,目前代码已经上传到GitHub上了,地址是:https://github.com/RIO-LI/ang…这个参考项目目前包含6的目录,每一个都是单独的Angular项目,每一个目录具体演示内容如下:component-insert-into-component-viewcontainer: 用来演示以组件作为视图容器ViewContainerRef,将另外一个组件插入视图容器的效果。component-insert-into-dom-viewcontainer: 用来演示以DOM元素为视图容器ViewContainerRef,将一个组件插入视图容器的效果。component-insert-into-self-viewcontainer: 用来演示以组件自身作为视图容器ViewContainerRef,将组件中的模版插入视图容器的效果。ngtemplate-insert-into-component-viewcontainer: 用来演示以一个组件作为视图容器ViewContainerRef,将一个<ng-template>插入视图容器的效果。ngtemplate-insert-into-dom-viewcontainer: 用来演示以一个DOM元素为视图容器ViewContainerRef,将一个<ng-template>插入视图容器的效果。ngtemplate-insert-into-ngcontainer-viewcontainer:用来演示以一个<ng-container>元素为视图容器ViewContainerRef,将一个<ng-template>插入视图容器的效果。 ...

December 26, 2018 · 1 min · jiezi

RouteReuseStrategy angular路由复用策略详解,深度刨析路由复用策略

关于路由复用策略网上的文章很多,大多是讲如何实现tab标签切换历史数据,至于如何复用的原理讲的都比较朦胧,代码样例也很难适用各种各样的路由配置,比如懒加载模式下多级嵌套路由出口网上的大部分代码都会报错。我希望能通过这篇文章把如何复用路由的原理讲明白,让小伙伴能明明白白的实用路由复用策略,文字中有不详实和错误的地方欢迎小伙伴批评指正对路由复用策略的理解路由复用策略的是对路由的父级相同节点的组件实例的复用,我们平时看到的多级嵌套路由切换时上层路由出口的实例并不会从新实例化就是因为angular默认的路由复用策略在起作用,而我们从写路由复用策略能实现很多事情,其中之一就是实现历史路由状态(数据)的存储,即jquery时代的tab页签和iframe实现操作历史的切换。我一开始认为路由复用策略就是对历史路由数据的复用策略,这个错误的观念导致我对路由复用策略接口方法理解起来异常困难,不知小伙伴和我犯没犯同样的错误。观念正确了,下面就理解起来比较方便了,写路由复用策略也就比较顺手了。下面是angular默认路由复用策略,每切换一下路由,下面代码都再默默的执行。export class DefaultRouteReuseStrategy { shouldDetach(route) { return false; } store(route, detachedTree) { } shouldAttach(route) { return false; } retrieve(route) { return null; } shouldReuseRoute(future, curr) { return future.routeConfig === curr.routeConfig; }}每个活动路由是一棵树,每个节点都有一个ActivatedRouteSnapshot,root节点是RouterModule.forRoot(Routes)Routes之上的一个默认路由,也无法配置路由复用策略解析路由复用策略方法调用顺序shouldReuseRoute(future, curr)retrieve(route)shouldDetach(route)store(route, detachedTree)shouldAttach(route)retrieve,取决一上一步的返回值store(route, detachedTree),取决第五步shouldReuseRouteshouldReuseRoute()决定是否复用路由,根据切换的前后路由的节点层级依次调用,返回值为true时表示当前节点层级路由复用,然后继续下一路由节点调用,入参为切换的下一级路由(子级)的前后路由,返回值为false时表示不在复用路由,并且不再继续调用此方法(当前路由不再复用,其子级路由也不会复用,所以不需要再询问下去),root路由节点调用一次,非root路由节点调用两次这个方法,第一次比较父级节点,第二次比较当前节点,retrieveretrieve()接上一步奏,当当前层级路由不需要复用的时候,调用一下retrieve方法,其子级路由也会调用一下retrieve方法,如果返回的是null,那么当前路由对应的组件会实例化,这种行为一直持续到末级路由。shouldDetachshouldDetach是对上一路由的数据是否实现拆离,其调用开始是当前层级路由不需要复用的时候,即shouldReuseRoute()返回false的时候,如果这时候反回false,将继续到上一路由的下一层级调用shouldDetach,直到返回true或者是最末级路由后才结束对shouldDetach的调用,当返回true时就调用一次store 方法,请看下一步奏storestore存储路分离出来的上一路由的数据,当 shouldDetach返回true时调用一次,存储应该被分离的那一层的路由的DetachedRouteHandle。注意:无论路由树上多个含有组件component路由节点,能分离出来的只能有一个,被存储的也只能有一个,感觉这种机制对使用场景有很大限制。shouldAttachshouldAttach是对当前路由的数据是否实现恢复(附加回来),其调用开始是当前层级路由不需要复用的时候,即shouldReuseRoute()返回false的时候,这和shouldDetach的调用时机很像,但是,并不是所有的路由层级都是有组件实例的,只有包含component的route才会触发shouldAttach,如果反回false,将继续到当前路由的下一带有component的路由层级调用shouldAttach,直到返回true或者是最末级路由后才结束对shouldAttach的调用,当返回true时就调用一次retrieve 方法,如果retrieve方法去获取一下当前路由的DetachedRouteHandle,返回一个DetachedRouteHandle,就再调用一次store,再保存一下retrieve返回的DetachedRouteHandle。注意注意:无论路由树上多个含有组件component路由节点,能恢复数据的只能有一个节点,这和shouldDetach是一个套路,对使用场景有很大限制。总结·这个还是实验性的路由复用策略还是不够强大路由复用策略这种调用机制对使用场景限制很大 ,比如多级路由出口嵌套就无法实现路由数据缓存。因为多级路由出口嵌套的应用切换路由时,前后路由会包含多个带component的路由节点,而每次对路由的存储和恢复只能存储和恢复某一个节点的component的DetachedRouteHandle,其他路由节点上的component就是被从新实例化。明白这一点后我就放弃了想写一个可以适用任何场景的路由复用策略的想法,如果有小伙伴能解决好这一业务场景,欢迎赐教。 如果这个路由复用策略可以存储一个路由上多个节点的DetachedRouteHandle,和恢复多个节点的DetachedRouteHandle,应该能解决上面是的多级路由出口嵌套场景,但不知道会不会带来别的问题。一个路由复用策略用例下面贴一个路由复用策略用例,应该是满足大部分人的业务要求,注意事项:只能是末级路由的缓存,且路由切换的时候路由节点上的component不能超过两个。import {ActivatedRouteSnapshot, DetachedRouteHandle, Route, RouteReuseStrategy} from “@angular/router”;export class CustomerReuseStrategy implements RouteReuseStrategy { static handlers: Map<Route, DetachedRouteHandle> = new Map(); shouldDetach(route: ActivatedRouteSnapshot): boolean { return !route.firstChild; } shouldAttach(route: ActivatedRouteSnapshot): boolean { return !!CustomerReuseStrategy.handlers.has(route.routeConfig); } shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot) { return curr.routeConfig === future.routeConfig; } retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle { return CustomerReuseStrategy.handlers.get(route.routeConfig); } store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { CustomerReuseStrategy.handlers.set(route.routeConfig, handle); }}很精简,但是很好用,小伙伴可以根据自己的业务逻辑进行改造。如果感觉这篇文章对你有帮助,请点个赞吧 ???????????????????????????????????????????????????????????????????????? ...

December 24, 2018 · 1 min · jiezi

在Angular中操作DOM:意料之外的结果及优化技术

【翻译】在Angular中操作DOM:意料之外的结果及优化技术原文链接:https://blog.angularindepth.c… 作者:Max Koretskyi 译者:而井我最近在NgConf的一个研讨会上讨论了Angular中的高级DOM操作的话题。我从基础知识开始讲起,例如使用模版引用和DOM查询来访问DOM元素,一直谈到了使用视图容器来动态渲染模版和组件。如果你还没有看过这个演讲,我鼓励你去看看。通过一系列的实践,你将可以快速地学会新知识,并加强认知。关于这个话题,我在NgViking 也有一个简单地谈话。然而,如果你觉得那个版本太长了(译者注:指演讲视频)不想看,或者比起听,你更喜欢阅读,那么我在这篇文章总结了(演讲的)关键概念。首先,我会介绍在Angular中操作DOM的工具和方法,然后再介绍一些我在研讨会上没有说过的、更高级的优化技术。你可以在这个GitHub仓库中找到我演讲中使用过的样例。窥探视图引擎假设你有一个要将一个子组件从DOM中移除的任务。这里有一个父组件,它的模块中有一个子组件A需要被移除:@Component({ … template: &lt;button (click)="remove()"&gt;Remove child component&lt;/button&gt; &lt;a-comp&gt;&lt;/a-comp&gt; })export class AppComponent {}解决这个任务的一个错误的方法就是使用Renderer或者原生的DOM API来直接移除<a-comp> DOM 元素:@Component({…})export class AppComponent { … remove() { this.renderer.removeChild( this.hostElement.nativeElement, // parent App comp node this.childComps.first.nativeElement // child A comp node ); }}你可以在这里看到整个解决方案(译者注:样例代码)。如果你通过Element tab来审查移除节点之后的HTML结果,你将看到子组件A已经不存在DOM中了。然而,如果你接着检查一下控制台,Angular依然报道子组件的数量为1,而不是0。并且关于对子组件A及其子节点的变更检测还在错误的运行着。这里是控制台输出的日志:为什么?发生这种情况是因为,在Angular内部中,使用了通常称为View或Component View的数据结构来代表组件。这张图显示了视图和DOM之间的关系:每个视图都由持有对应DOM元素的视图节点所组成。所以,当我们直接修改DOM的时候,视图内部的视图节点以及持有的DOM元素引用并没有被影响。这里有一张图可以展示在我们从DOM中移除组件A后,DOM和视图的状态:并且由于所有的变更检测操作和对子视图的包含,都是运行在视图中而不是DOM上,Angular检测与组件相关的视图,并且报告(译者注:组件数量)为1,而不是我们期望的0。此外,由于与组件A相关的视图依旧存在,所以对于组件A及其子组件的变更检测操作依然会被执行。要正确地解决这个问题,我们需要一个能直接处理视图的工具,在Angular中它就是视图容器View Container。视图容器View Container视图容器可以保障DOM级别的变动的安全,在Angular中,它被所有内置的结构指令所使用。在视图内部有一种特别的视图节点类型,它扮演着其他视图容器的角色:正如你所见的那样,它持有两种类型的视图:嵌入视图(embedded views)和宿主视图(host views)。在Angular中只有这些视图类型,它们(视图)主要的不同取决于用什么输入数据来创建它们。并且嵌入视图只能附加(译者注:挂载)到视图容器中,而宿主视图可以被附加到任何DOM元素上(通常称其为宿主元素)。嵌入视图可以使用TemplateRef通过模版来创建,而宿主视图得使用视图(组件)工厂来创建。例如,用于启动程序的主要组件AppComponent,在内部被当作为一个用来附加挂载组件宿主元素<app-comp>的宿主视图。视图容器提供了用来创建、操作和移除动态视图的API。我称它们为动态视图,是为了和那些由框架在模版中发现的静态组件所创建出来的静态视图做对比。Angular不会对静态视图使用视图容器,而是在子组件特定的节点内保持一个对子视图的引用。这张图可以表明这个想法:正如你所见,这里没有视图容器,子视图的引用是直接附加到组件A的视图节点上的。操控动态视图在你开始创建一个视图并将其附加到视图容器之前,你需要引入组件模版的容器并且将其进行实例化。模版中的任何元素都可以充当视图容器,不过,通常扮演这个角色的候选者是<ng-container>,因为在它会渲染成一个注释节点,所以不会给DOM带来冗余的元素。为了将任意元素转化成一个视图容器,我们需要对一个视图查询使用{read: ViewContainerRef} 配置:@Component({ … template: &lt;ng-container #vc&gt;&lt;/ng-container&gt;})export class AppComponent implements AfterViewChecked { @ViewChild(‘vc’, {read: ViewContainerRef}) viewContainer: ViewContainerRef;}一旦Angular执行对应的视图查询并将视图容器的的引用赋值给一个类的属性,你就可以使用这个引用来创建一个动态视图了。创建一个嵌入视图为了创建一个嵌入视图,你需要一个模版。在Angular中,我们会使用<ng-template> 来包裹任意DOM元素和定义模版的结构。然后我们就可以简单地用一个带有 {read: TemplateRef} 参数的视图查询来获取这个模版的引用:@Component({ … template: &lt;ng-template #tpl&gt; &lt;!-- any HTML elements can go here --&gt; &lt;/ng-template&gt; })export class AppComponent implements AfterViewChecked { @ViewChild(’tpl’, {read: TemplateRef}) tpl: TemplateRef<null>;}一旦Angular执行这个查询并且将模版的引用赋值给类的属性后,我们就可以通过createEmbeddedView方法使用这个引用来创建和附加一个嵌入视图到一个视图容器中:@Component({ … })export class AppComponent implements AfterViewInit { … ngAfterViewInit() { this.viewContainer.createEmbeddedView(this.tpl); }}你需要在ngAfterViewInit生命周期中实现你的逻辑,因为视图查询是那时完成实例化的。而且你可以给模版(译者注:嵌入视图的模版)中的值绑定一个上下文对象(译者注:即模版上绑定的值隶属于这个上下文对象)。你可以通过查看API文档来了解更多详情。你可以在这里找到创建嵌入视图的整个样例代码。创建一个宿主视图要创建一个宿主视图,你就需要一个组件工厂。如果你需要了解Angular中动态组件的话,点击这里可以学习到更多关于组件工厂和动态组件的知识。在Angular中,我们可以使用componentFactoryResolver这个服务来获取一个组件工厂的引用:@Component({ … })export class AppComponent implements AfterViewChecked { … constructor(private r: ComponentFactoryResolver) {} ngAfterViewInit() { const factory = this.r.resolveComponentFactory(ComponentClass); } }}一旦我们得到一个组件工厂,我们就可以用它来初始化组件,创建宿主视图并将其视图附加到视图容器之上。为了达到这一步,我们只需简单地调用createComponent方法,并且传入一个组件工厂:@Component({ … })export class AppComponent implements AfterViewChecked { … ngAfterViewInit() { this.viewContainer.createComponent(this.factory); }}你可以在这里找到创建宿主视图的样例代码。移除视图一个视图容器中的任何附加视图,都可以通过remove和detach方法来删除。两个方法都会将视图从视图容器和DOM中移除。但是remove方法会销毁视图,所以之后不能重新附加(译者注:即从缓存中获取再附加,不用重新创建),detach方法会保持视图的引用,以便未来可以重新使用,这个对于我接下来要讲的优化技术很重要。所以,为了正确地解决移除一个子组件或任意DOM元素这个问题,首先有必要创建一个嵌入视图或宿主视图,并将其附加到视图容器上。然后你才有办法使用任何可用的API方法来将视图从视图容器和DOM中移除。优化技术有时你需要重复地渲染和隐藏模版中定义好的相同组件或HTML。在下面这个例子中,通过点击不同的按钮,我们可以切换要显示的组件:如果我们把之前学过的知识简单地应用一下,那代码将会如下所示:@Component({…})export class AppComponent { show(type) { … // 视图被销毁 this.viewContainer.clear(); // 视图被创建并附加到视图容器之上 this.viewContainer.createComponent(factory); }}最终,我们会得一个不想要的结果:每当按钮被点击、show方法被执行时,视图都会被销毁和重新创建。在这个例子中,宿主视图会因为我们使用组件工厂和createComponent方法,而销毁和重复创建。如果我们使用createEmbeddedView方法和TemplateRef,那嵌入视图也会被销毁和重复创建:show(type) { … // 视图被销毁 this.viewContainer.clear(); // 视图被创建并附加到视图容器之上 this.viewContainer.createEmbeddedView(this.tpl);}理想状况下,我们只需创建视图一次,之后在我们需要的时候复用它。有一个视图容器的API,它提供了将已经存在的视图附加到视图容器之上、移除视图却不销毁视图的办法。ViewRefComponentFactory和TemplateRef都实现了用来创建视图的创建方法。事实上,当你调用createEmbeddedView 和 createComponent 方法并传入输入数据时,视图容器在底层内部使用了这些创建方法。有一个好消息就是我们可以自己调用这些方法来创建一个嵌入或宿主视图、获取视图的引用。在Angular中,视图可以通过ViewRef及其子类型来引用。创建一个宿主视图所以通过这样,你可以使用一个组件工厂来创建一个宿主视图和获取它的引用:aComponentFactory = resolver.resolveComponentFactory(AComponent);aComponentRef = aComponentFactory.create(this.injector);view: ViewRef = aComponentRef.hostView;在宿主视图情况下,视图与组件的关联(引用)可以通过ComponentRef调用create方法来获取。通过一个hostView属性来暴露。一旦我们获得到这个视图,它就可以通过insert方法附加到一个视图容器之上。另外一个你不想显示的视图可以通过detach方法来从视图中移除并保持引用。所以可以通过这样来解决组件切换显示问题:showView2() { … // 视图1将会从视图容器和DOM中移除 this.viewContainer.detach(); // 视图2将会被附加于视图容器和DOM之上 this.viewContainer.insert(view);}注意,我们使用detach方法来代替clear或remove方法,为之后的复用保持视图(的引用)。你可以在这里找到整个实现。创建一个嵌入视图在以一个模版为基础来创建一个嵌入视图的情况下,视图(引用)可以直接通过createEmbeddedView方法来返回:view1: ViewRef;view2: ViewRef;ngAfterViewInit() { this.view1 = this.t1.createEmbeddedView(null); this.view2 = this.t2.createEmbeddedView(null);}与之前的例子类似,有一个视图将会从视图容器移除,另外一个视图将会被重新附加到视图容器之上。你可以在这里找到整个实现。有趣的是,视图容器(译者注:ViewContainerRef类型)的createEmbeddedView和createComponent这两个创建视图的方法,都会返回被创建的视图的引用。 ...

December 23, 2018 · 1 min · jiezi

【前端芝士树】SPA 网站 SEO 初级优化指南(MVVM)

SPA 网站 SEO 初级优化指南(MVVM)百度 Baidu百度搜索资源平台 https://ziyuan.baidu.com/?cas…链接提交地址 https://ziyuan.baidu.com/link…百度爬虫 UAMozilla/5.0 (Linux;u;Android 4.2.2;zh-cn;) AppleWebKit/534.46 (KHTML,like Gecko) Version/5.1 Mobile Safari/10600.6.3 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/s…Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/s…)谷歌 GoogleGoogle Search Console https://search.google.com/sea…谷歌爬虫 UAMozilla/5.0 (Linux; Android 6.0.1; Nexus 5X Build/MMB29P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.96 Mobile Safari/537.36 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)SEO 优化重点(一)三个基本注意点信息架构信息架构要明晰,这个对 SEO 也非常重要,包括网站扁平化设计,友好的 URL 设计,标题书写,页面相关度信息聚合和融合,这也是影响用户体验的一大部分。网站主题为网站确定一个主题(核心关键词),一旦确定,那么全站都围绕这个关键词进行扩展和相关性来做外链和友链权重网站外链就是在别的网站导入自己网站的链接,高质量高 PR 的外链能迅速提高自身网站的权重。友链则是自己的网站包含了其他网站的链接,效果与外链类似。(二)利用好 Meta 信息标签Meta 标签用于给搜索引擎提供关于网页的信息,这些信息对于访问者而言是不可见的。参考淘宝网的做法<header> <title>淘宝网 - 淘!我喜欢</title> <meta name=“description” content=“淘宝网 - 亚洲较大的网上交易平台,提供各类服饰、美容、家居、数码、话费/点卡充值… 数亿优质商品,同时提供担保交易(先收货后付款)等安全交易保障服务,并由商家提供退货承诺、破损补寄等消费者保障服务,让你安心享受网上购物乐趣!” /> <meta name=“keyword” content=“淘宝,掏宝,网上购物,C2C,在线交易,交易市场,网上交易,交易市场,网上买,网上卖,购物网站,团购,网上贸易,安全购物,电子商务,放心买,供应,买卖信息,网店,一口价,拍卖,网上开店,网络购物,打折,免费开店,网购,频道,店铺” /> …</header>Titletitle 网站标题Meta - Descriptiondescription 给搜索引擎提供了关于这个网页的简短的描述Meta - Keywordskeywords 关于网页内容的几个关键词Meta - Robotsrobots 管理着搜索引擎是否可以进入网页如下面一段代码,禁止搜索引擎获取网页,并且阻止其进入链接。<meta name="”robots”" content="”noindex," nofollow” />SPA 网站 SEO 优化指南(一) 三种解决方案:服务器端渲染,较为复杂页面缓存服务,prerender.io,涉及收费第三方服务,或者中间层启用渲染静态页替换,phantom.js、puppeteer 或者浏览器 Copy outerHTML 就能完成,需要 nginx 配合(二)如何判断是爬虫访问还是浏览器访问爬虫访问时,会使用特殊的 user agent,以百度蜘蛛的 UA 为例,它会使用“Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/s…)”这样的UA,所以我们可以判断 UA 中含有“Baiduspider”字样则意味着是百度蜘蛛来访问了(三)如何在百度爬虫来访问时返回静态页先把静态页放置到网站的 /assets/static/ 下,配置 nginx 的配置文件 nginx.conf: location / { root C:\projects\bzbclub-dist; index index.html index.htm; if ( $http_user_agent ~* “Baiduspider”){ rewrite ^/index.html$ /assets/static/index.html last; } }保存配置文件后要使用 nginx -s reload 重新加载网站,然后使用 curl 命令的“-A”参数模拟百度蜘蛛访问首页:curl -A “Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/s…)” http://localhost:17082 > z:tempbzbclub.html打开 z:tempbzbclub.html 确认是否已经返回了静态页的内容。(四)如何生成静态页并优化Phantom.jsvar fs = require(“fs”);var page = require(“webpage”).create();phantom.outputEncoding = “utf-8”; //指定编码方式page.open(“http://localhost:17082”, function(status) { if (status === “success”) { fs.write(“z:\temp\index.html”, page.content, “w”); } else { console.log(“网页加载失败”); } phantom.exit(0); //退出系统});将此脚本保存为“phantom-static-page.js”,在命令行执行此脚本:phantomjs phantom-static-page.jsPuppeteerconst fs = require(“fs”);const puppeteer = require(“puppeteer”);(async () => { const browser = await puppeteer.launch({ headless: false // executablePath: “C:/Program Files (x86)/Google/Chrome” }); const page = await browser.newPage(); page.setViewport({ width: 1920, height: 1800 }); await page.goto(“http://localhost:3333”); await page.screenshot({ path: “example.png” }); const content = await page.content(); fs.writeFileSync("./index.html", content); // await page.close(); // await browser.close();})();将此脚本保存为“pupp-static-page.js”,在命令行执行此脚本:node pupp-static-page.js从浏览器获取静态页内容(推荐)与前两者相比,看上去没那么极客,但是非常的简单好用。首先需要新建一个static.html然后在浏览器打开需要生成静态页的页面按 F12 打开 DevTool鼠标选中<html>标签,右键 Copy > Copy OuterHTML将内容粘贴至static.html保存即可静态页压缩优化用编辑器打开static.html,删除掉所有的<script></script>以及其中的内容浏览器打开静态页,按 F12 打开 DevTool 确保没有报错体积大小优化的程序视页面的复杂度而定,一般能压缩到原有大小的十分之一参考文章链接《Meta 标签与搜索引擎优化》《SEO 网站优化的步骤和技巧有哪些?》《Angular2 网站 SEO 攻略》 ...

December 21, 2018 · 2 min · jiezi

Angular5升级至Angular7

根据升级建议,应该先从Angular 5升级至Angular 6,再从Angular 6升级至Angular 7。 本文内容“升级前-升级”适用于想从Angular 5升级至Angular 6的小伙伴;本文内容“升级后”适用于想从Angular 6升级至Angular 7的小伙伴;ng –version可查看当前版本升级前1.HttpModule和Http Service 切换到 HttpClientModule和HttpClient Service如果项目中有用到HttpModule和Http Service,把它们切换到HttpClientModule和HttpClient Service。HttpClient有几个特性:不需要调用.json()来映射返回的数据到json格式,默认就是json格式支持拦截器具体可以参考:HttpClient2.如果有从@ angular / core导入任何动画服务或工具,则应从@ angular / animations导入它们3.ngOutletContext替换为ngTemplateOutletContext4.CollectionChangeRecord替换为IterableChangeRecord升级1.确保使用的是Node 8或更高版本2.升级Angular CLI(全局和本地)npm install -g @angular/clinpm install @angular/cling update @angular/cli可在每行后面加@你想要的版本号,例如:npm install -g @angular/cli@6.0.0,以升级至Angular6。3.迁移配置到当前版本的angular.jsonng update @angular/cli –migrate-only –from=1ps:Angular5以下的配置文件名是angular-cli.json4.升级Angular框架包到v6,以及升级RxJS和TypeScript。ng update @angular/core5.使用ng update查看更新情况如果仍有需要更新的内容,会显示当前版本号和最新版本号的对比。更新后使用rxjs-tslint自动更新规则删除已弃用的RxJS 6功能对于大多数应用程序,这将意味着运行以下两行命令:npm install -g rxjs-tslintrxjs-5-to-6-migrate -p src/tsconfig.app.json【参考资料】Angular官方升级指南刘敏的码经笔记:Angular 5升级到Angular 6

December 20, 2018 · 1 min · jiezi

【Angular6+】事件绑定

Angular6+ 事件绑定临近毕业开始了第二段实习,因为项目需求,技术栈从Vue转到Angular,才发现Angular已经到7了?????,我两年前还是2来着,看来VAR(Vue、Angular、React)老大哥的压力还是很大的,这系列文章也记录一下我重拾Angular的过程。1. 事件绑定的基础语法<li *ngFor=“let hero of heroes” (click)=“onSelect(hero)"></li>或者可以使用带 on-前缀的形式<button on-click=“onSave()">On Save</button>添加如下 onSelect() 方法,它会把模板中被点击的英雄赋值给组件的 selectedHero 属性。selectedHero: Hero;onSelect(hero: Hero): void { this.selectedHero = hero;}2. 为事件起别名(不推荐)@Directive({outputs: [‘clicks:myClick’] // propertyName:alias})<div (myClick)=“clickMessage=$event” clickable>click with myClick</div>3. &dollar;event和事件处理语句事件对象的形态取决于目标事件。如果目标事件是原生 DOM 元素事件, $event就是 DOM 事件对象,它有像 target 和 target.value 这样的属性。<input [value]=“hero.name” (input)=“hero.name=$event.target.value” />input 中的值发生改变后,会对 hero.name中的值进行更新。是不是似曾相识?是的,这就是 Angular 中的双向绑定。<div> <label>name: <input [(ngModel)]=“hero.name” placeholder=“name” /> </label></div>4. 通过@HostListenr 把事件绑定在宿主监听器上把一个事件绑定到一个宿主监听器,并提供配置元数据。 当宿主元素发出特定的事件时,Angular 就会执行所提供的处理器方法,并使用其结果更新所绑定到的元素。 如果该事件处理器返回 false,则在所绑定的元素上执行 preventDefault。Click 事件import {…, HostListener} from ‘@angular/core’;…@Directive({ selector: “button[counting]” })class CountClicks { numberOfClicks = 0; @HostListener(“click”, ["$event.target”]) onClick(btn) { console.log(“button”, btn, “number of clicks:”, this.numberOfClicks++); }}@Component({ selector: “app”, template: “<button counting>Increment</button>”})class App {}Scroll 事件@HostListener(‘window:scroll’, [’$event’]) onScroll() { this.pageYOffset = window.pageYOffset; }5. 使用 EventEmitter 实现自定义事件(父子组件间事件传递)后面再起一章单独讲 Angular 的组件通信子组件触发事件 Output<div> <img src=”{{heroImageUrl}}" /> <span [style.text-decoration]=“lineThrough”> {{prefix}} {{hero?.name}} </span> <button (click)=“delete()">Delete</button></div>deleteRequest = new EventEmitter<Hero>(); delete() { this.deleteRequest.emit(this.hero); }父组件接受事件 Input<app-hero-detail (deleteRequest)=“deleteHero($event)” [hero]=“currentHero”></app-hero-detail> ...

December 17, 2018 · 1 min · jiezi

angular源码分析之StaticInjector

上一篇说到了平台实例在初始化的时候会创建根注入器,那现在就一起看看注入器是如何创建的,又是如何工作的.(所有引用的代码都被简化了)创建注入器程序初始化时调用的创建根注入器的静态方法:abstract class Injector{ static create(options: StaticProvider[]|{providers: StaticProvider[], parent?: Injector, name?: string},parent?: Injector): Injector { if (Array.isArray(options)) { return new StaticInjector(options, parent); } else { return new StaticInjector(options.providers, options.parent, options.name || null); }}调用此方法会返回一个StaticInjector类型的实例(也就是注入器).StaticInjector类export class StaticInjector implements Injector { readonly parent: Injector; readonly source: string|null; private _records: Map<any, Record>; constructor(providers: StaticProvider[], parent: Injector = NULL_INJECTOR, source: string|null = null) { this.parent = parent; this.source = source; const records = this._records = new Map<any, Record>(); records.set(Injector, <Record>{token: Injector, fn: IDENT, deps: EMPTY, value: this, useNew: false}); records.set(INJECTOR, <Record>{token: INJECTOR, fn: IDENT, deps: EMPTY, value: this, useNew: false}); recursivelyProcessProviders(records, providers); }}注入器的构造函数在初始化过程中的操作:设置当前注入器的父注入器设置注入器的源新建注册表(_records属性,是一个Map类型)将参数providers全部添加到注册表中向注册表中添加服务调用了recursivelyProcessProviders函数const EMPTY = <any[]>[];const MULTI_PROVIDER_FN = function (): any[] { return Array.prototype.slice.call(arguments) };function recursivelyProcessProviders(records: Map<any, Record>, provider: StaticProvider) { if (provider instanceof Array) { for (let i = 0; i < provider.length; i++) { recursivelyProcessProviders(records, provider[i]); } } else (provider && typeof provider === ‘object’ && provider.provide) { let token = resolveForwardRef(provider.provide);// 方法resolveForwardRef的作用可能是向前兼容,可以忽略 const resolvedProvider = resolveProvider(provider); if (provider.multi === true) { let multiProvider: Record | undefined = records.get(token); if (multiProvider) { if (multiProvider.fn !== MULTI_PROVIDER_FN) { throw multiProviderMixError(token); } } else { records.set(token, multiProvider = <Record>{ token: provider.provide, deps: [], useNew: false, // 这个值在后面获取依赖实例的时候会用到,当做判断条件 fn: MULTI_PROVIDER_FN, value: EMPTY // 这个值在后面获取依赖实例的时候会用到,当做判断条件 }); } token = provider; multiProvider.deps.push({ token, options: OptionFlags.Default }); } records.set(token, resolvedProvider); }}recursivelyProcessProviders函数具体的执行过程:如果provider是个数组,那就遍历后依次调用此方法.如果provider是个对象:1 获取token let token = resolveForwardRef(provider.provide);2 调用resolveProvider方法处理服务中可能出现的属性和依赖,返回一个Record对象,此对象会作为token的值<!– (useValue,useClass,deps,useExisting,useFactory) –>function resolveProvider(provider: SupportedProvider): Record { const deps = computeDeps(provider); let fn: Function = function (value) { return value }; let value: any = []; // useUew用来标识是否需要 new let useNew: boolean = false; let provide = resolveForwardRef(provider.provide); if (USE_VALUE in provider) { value = provider.useValue; } else if (provider.useFactory) { fn = provider.useFactory; } else if (provider.useExisting) { //do nothing } else if (provider.useClass) { useNew = true; fn = resolveForwardRef(provider.useClass); } else if (typeof provide == ‘function’) { useNew = true; fn = provide; } else { throw staticError(‘StaticProvider does not have [useValue|useFactory|useExisting|useClass] or [provide] is not newable’, provider); } return { deps, fn, useNew, value }; // provider中不同的属性会返回包含不同值的对象}这个方法会先调用computeDeps函数处理服务需要的依赖,它将useExisting类型的服务也转换成deps,最后返回[{ token, OptionFlags }]形式的数组(OptionFlags是枚举常量) function computeDeps(provider: StaticProvider): DependencyRecord[] { let deps: DependencyRecord[] = EMPTY; const providerDeps: any[] = provider.deps; if (providerDeps && providerDeps.length) { deps = []; for (let i = 0; i < providerDeps.length; i++) { let options = OptionFlags.Default; let token = resolveForwardRef(providerDeps[i]); deps.push({ token, options }); } } else if ((provider as ExistingProvider).useExisting) { const token = resolveForwardRef((provider as ExistingProvider).useExisting); deps = [{ token, options: OptionFlags.Default }]; } return deps; }resolveProvider函数最终返回的Record对象有一个缺省值:{ deps:[], // 包含依赖时 [{ token, options },{ token, options }] fn:function(value) { return value }, useNew:false, value:[]}执行过程中会根据provider不同的属性修改Record对象的变量为不同的值:useValue : 修改value为useValue的值useFactory : 修改fn为对应的函数useClass 或 typeof provide == ‘function’(令牌为一个函数时) : 修改fn为对应的函数,并设置useNew为trueuseExisting : 不做修改,直接使用默认值3 如果是多处理服务(multi:ture)且为首次注册,那么在注册表中额外注册一个占位的Recordrecords.set(token, multiProvider = <Record>{ token: provider.provide, deps: [], useNew: false, fn: MULTI_PROVIDER_FN, value: EMPTY});4 非多处理服务以token为键,多处理服务以provider对象为键,返回的Record对象为值,存入注册表records中从注入器中获取实例服务注册完,下一步就是怎么从注入器中获取服务的实例了,这会调用StaticInjector的get方法export class StaticInjector implements Injector { get(token: any, notFoundValue?: any, flags: InjectFlags = InjectFlags.Default): any { // 获取token对应的record const record = this._records.get(token); return resolveToken(token, record, this._records, this.parent, notFoundValue, flags);}get方法调用了resolveToken函数,这个函数会返回token对应的实例(就是被注入的对象)const EMPTY = <any[]>[];const CIRCULAR = IDENT;const IDENT = function <T>(value: T): T { return value };function resolveToken(token: any, record: Record | undefined, records: Map<any, Record>, parent: Injector, notFoundValue: any, flags: InjectFlags): any { let value; if (record && !(flags & InjectFlags.SkipSelf)) { value = record.value; if (value == CIRCULAR) { throw Error(NO_NEW_LINE + ‘Circular dependency’); } else if (value === EMPTY) { record.value = CIRCULAR; let obj = undefined; let useNew = record.useNew; let fn = record.fn; let depRecords = record.deps; let deps = EMPTY; if (depRecords.length) { deps = []; for (let i = 0; i < depRecords.length; i++) { const depRecord: DependencyRecord = depRecords[i]; const options = depRecord.options; const childRecord = options & OptionFlags.CheckSelf ? records.get(depRecord.token) : undefined; deps.push(tryResolveToken( depRecord.token, childRecord, records, !childRecord && !(options & OptionFlags.CheckParent) ? NULL_INJECTOR : parent, options & OptionFlags.Optional ? null : Injector.THROW_IF_NOT_FOUND, InjectFlags.Default)); } } record.value = value = useNew ? new (fn as any)(…deps) : fn.apply(obj, deps); } } else if (!(flags & InjectFlags.Self)) { value = parent.get(token, notFoundValue, InjectFlags.Default); } return value;}函数中会先判断当前请求的token是否存在,如果不存在则去当前注入器的父注入器中寻找,如果存在:获取token对应的record判断record.value是否为:ture : 非useValue类型的服务/多处理服务或此服务没有被创建过查看是否包含依赖,包含则优先创建依赖的实例,也是调用这个函数根据record.fn创建当前token对应的实例并更新record.value(这里需要根据record.useNew来判断是否需要用new来实例化,比如useFactory类型就不需要new,而useExisting更是直接返回了deps)返回这个值false : useValue类型的服务或此服务已经被创建过直接返回这个值 ...

December 14, 2018 · 4 min · jiezi

Angular表单验证器

为什么使用验证器用户输入的数据各式各样,并不总是正确的,如果用户输入了错误的数据格式,那么我们希望给他们提供实时正确的反馈,并且阻止表单的提交.因此,我们需要验证器来处理这些情况.表单验证Angular支持的内置validate属性:required- 设置表单控件值是非空的email - 设置表单控件的格式是emailminlength - 设置表单控件值的最小长度maxlength - 设置表单控件长度的最大值pattern - 设置表单控件的值需匹配 pattern 对应的模式通过表单控件的.valid判断验证结果,其结果状态:valid - 有效invalid - 无效pristine - 表单值未改变dirty - 表单值已改变touched - 表单控件已被访问过untouched- 表单控件未被访问过Angular验证器 简单实例1.验证器由Validators模块提供,该模块从’@angular/forms’中导出import { Validators }from’@angular/forms'2.使用验证器的方法(实例)export class AppComponent { constructor(private fb: FormBuilder) { } profileForm = new FormGroup({ userName: new FormControl(’’, Validators.required), password: new FormControl(’’, Validators.required), });3.效果最后,提及一下我们团队的黄庭祥,虽然跟我说的时候云里雾里的,但是还是感谢!!!

December 13, 2018 · 1 min · jiezi