接到一个pc端后台项目,还会加入两个安卓同事一块儿学习和作这个项目,须要带一下他们。 既ng1以后,我就没怎么有过其它后台框架的实际项目经验了,期间用的移动端框架也并不是vue、angular系列、react,包括es六、webpack等也都并不熟悉。 公司一个其它后台项目使用了vue,偶尔我会维护一下可是整体来讲对体系不熟悉。 一直比较喜欢angular,应该是有过ng1框架搭建的经验的缘由(前面也有写过博客),学习过ng2和ng4的官方demo,总的来讲照着抄写一遍意义不大,必需要用于实际项目才能真正吸取。 目前ng1确定不要用了,我要从新熟悉和搭建一个架子,从自我喜爱和前期一些基础来说,ng4是最好的选择,恰好typescript的语法对安卓同事也比较友好。 向领导请示了以后,也获得了容许。css
入坑之初,问题通常比较多,使用的是官方angular-cli建立项目。 中途完善cli的功能,理解整个框架是比较费精力的事情。 总的来讲此次就花了两三天时间,梳理好了框架,配置好了一些基本功能和依赖,好比环境部署,路由嵌套,主页面布局(侧边栏,导航栏,内容,底部),公共服务(loading,http请求封装,全局服务title,用户信息存取)。 然后就直接和同事一块儿开发了。 最开始还有点没底,不知道搭架子要花多久时间,如今来看这个进度和时间仍是很满意的,固然过程当中参考了一些网上的同行分享的资源和代码。 很是感谢两位同事的积极支持,他们学习的也很快。html
angular-cli.json 设置 styleExt为scss以后,在组件里写Styles,并不会编译把scss编译成css,必需要写在独立的scss,而后经过styleUrls引入。前端
routes path前面不能加 / 不然会报错vue
const routes: Routes = [ { path: '', redirectTo: '/main', pathMatch: 'full' }, { path: 'main', component: MainComponent }, ]
<router-outlet> 的意思是当经过路由访问component的时候,component的selector会找到 <router-outlet></router-outlet> ,@Component定义的selector会直接生成一个标签,载入到<router-outlet></router-outlet>下方,若是不设置selector标签名就是默认的ng-component。 react
forRoot
creates a module that contains all the directives, the given routes, and the router service itself.forChild
creates a module that contains all the directives and the given routes, but does not include the router service.总的来讲forRoot是定义一级路由,forChild是定义二级路由或者说是子路由。好比咱们的引导模块(命名通常是app.module.ts),也就是根模块,里面注册路由假设是 /main,就须要使用forRoot。根模块经过forRoot注册的路由/main,须要嵌套路由成为 /main/home ,那home的module就须要用forChild去注册路由。webpack
ng4的提供了路由懒加载,以下,咱们有个一级路由/main,/main有个二级路由/home对应home组件,咱们能够经过,下面的方法来定义子模块的路由和组件,可是这种写法就须要把路由写在一块儿,而咱们但愿home组件的路由是一个单独的home.routes.ts文件存在于home文件夹中,经过在相似父模块中引入子模块的方式注册home路由git
{ path: 'main',
compontent: MainComponent, children: [ { path: 'home', component: HomeComponent, } ] }
因此就要使用loadChildren,经过它来注册子模块和子路由,代码以下。 loadChildren会找到路径文件app/home/home.module,#隔开,后面是exports的模块名。 下面的HomeModule, 定义了home的路由和组件关系,由于是二级路由,因此这里用的是RouterModule.forChild,最后经过框架的处理,就达到了上面代码里的 children 属性的效果。 ps: 本来是访问 /main/home 就会载入main和home组件,可是发现直接访问路径 /home 能直接载入home组件,彷佛也注册了一个根域名。 原来是使用了loadChildren以后,HomeModule模块就已经被注册了到main模块下了,而我在AppModule里又经过imports注册了一次HomeModule,这样就重复注入了,并且仍是一次和父模块同级的注册,这一点要当心。es6
{ path: 'main',
compontent: MainComponet,//注意这里加载的是Main loadChildren: 'app/home/home.module#HomeModule', } //HomeModule const routes: Routes = [ { path: '', component: HomeCompontent} ] @NgModule({ declarations: [ HomeCompontent ], imports: [ RouterModule.forChild(routes) ], providers: [] }) export class HomeModule { }
假设一个场景,根模块注册两个路径,一个是/login,一个是/main。/login这个路由访问就是单纯的一个登录页面,/main下面的路由都将是对应核心页面和业务,由于在main组件里包含了公用的侧边栏、导航栏、内容容器和底部栏,因此 /main路由加载的main组件的内容容器里须要嵌套子模块。 举个实例,当我访问/main/home的时候main组件会加载home组件到content容器中,当我访问/main/manager,manager组件又会替换content中的home。这样咱们的公共页面部分就是不变的main组件,根据子路由的变化,去加载不一样的组件到content里。github
如下是main组件的html大体代码和实际页面截图:web
这里也有一个知识点是<router-outlet>标签的嵌套,上面的代码中<div class="main-content">下面有一个<router-outlet>标签,home等二级组件会被载入到父组件main的<router-outlet>标签下。而main组件的父组件是引导组件AppCompontent,因此main组件是经过一级路由被载入到AppCompontent的html模板的<router-outlet>下方,如下是AppCompontent组件部分代码:
那么打开调试器,咱们就能从dom节点上看清楚,router-outlet的嵌套关系:
目录结构
再看下src的目录结构,component文件夹是存放一些公共的组件,login和main组件是注册的一级路由,home和另外一个马赛克组件是注册为main的二级路由,实际后面会注册不少组件到main下,可是他们的文件夹划分都是同级。
RouterModule.forRoot(routes, { useHash: true });使用hash路由,后端不用修改配置,这样比较方便,省去不少麻烦
引入了platform-browser的Title,使用它的setTitle方法改标题
在非hash路由状况下,有时候环境的缘由布置静态资源路径的时候可能不是根域名,同时还要删除index.html的<base>标签,不然会有问题
import { APP_BASE_HREF } from '@angular/common';
在app.module里注册providers: [{provide: APP_BASE_HREF, useValue: environment.publicBase}],
使用http相关API,须要注入HttpModule,不然会报错: No provider for Http
import { HttpModule } from '@angular/http';
declare
引入了三方JS,三方JS定义的全局变量,在引入到代码里,编译会报错:没有定义。须要在前面加个申明 declare let thirdVar:any;
文件命名service,component,route,module,主要类型的文件种类很少,每次新建文件命名太长,引入的时候也麻烦,因此除了根目录命名保持xx.component.ts这种格式,其余文件统一为xx.c.ts,xx.s.ts。
xx.s.ts == xx.service.ts | export class xxS xx.r.ts == xx.routes.ts | export class xxR xx.m.ts == xx.module.ts | export class xxM xx.c.ts == xx.component.ts | export class xxC
考虑引入boostrap4来做为css库布局。
关于rem,咱们通常用rem做为单位的时候,是更但愿利用它的特性改变font-size达到自适应效果,会先定义一个font-size的范围和对应的屏幕宽度范围,根据设计稿的宽度获得一个基数,再用设计稿中元素的实际像素去除以基数获得rem,最后根据屏幕宽度动态设置font-size的相应值达到自适应效果。bootstrap4以浏览器字体默认大小16px,直接定义了元素的rem值,它的源码里没有任何计算,我想他们是参照16px来设置的元素大小,而后求出的rem值,当页面根font-size的值是16px的时候,全部的bootstrap4的元素大小就是标准大小,若是咱们想让页面的元素总体放大或者缩小,咱们只须要去改变font-size的大小,font-size设置为多少,须要咱们本身计算和定义规则。由于是三方库,因此这块的实现方法和咱们本身实际项目使用rem的时候,会有些反差。
若是项目中引入了它,咱们给自定义元素直接设置px值的话,就会出现问题,若是咱们须要改变font-size的大小,就必须统一使用rem,不然font-size改变的时候,自定义的px元素并不会改变。那么自定义元素须要设置为rem值。
想了想,须要快速开发,仍是须要一个UI插件库,本身去造轮子开发成本过高,经撸哥介绍,知道了蚂蚁金服的ng库ng-zorro,支持ng4,https://ng.ant.design。 看了下很全,还提供了栅格布局和按钮样式,转眼一想,若是用bootstrap4,相互之间可能有冲突,好比boostrap的reset相关的,并且用boot的按钮样式和蚂蚁的样式可能看起来不搭调。因此我在引入ng-zorro以后,先注释了对bootstrap4的引用,一些公用样式,后面能够考虑本身写。
src目录下有个environments文件夹,这里的文件是配置环境的,.angular-cli.json文件有配置两个默认环境,一个是开发一个是发布环境,在咱们开发的时候,默认选择的是dev环境
在src下的main.ts里有这么一段代码,这里的意思是切换到生产模式时禁用开发环境下特有的检查(双重变动检测周期)来让应用运行得更快。
咱们在开发项目的时候,也必然须要配置开发,测试和生产环境,不一样的环境的接口或者其余设置确定是不同的。 我须要配置一个apiBase变量,表明不一样环境的接口域名,在开发的时候ng4会运行ng serve在本地运行一个服务,域名是localhost,那么后端部署的接口确定不在咱们这个开发域名下,因此这个 apiBase 就是咱们后端接口的域名 apiBase='http://www.xxxx.com' (须要后端支持跨域)。 当咱们把打包好的代码部署到QA或者生产环境后,访问前端页面的url和后端接口都在同一域名,因此 apiBase='/' 。 那么dev和prod的environment代码分别以下:
//dev export const environment = { production: false, apiBase: 'http://www.xx.com/' };
//prod export const environment = { production: true, apiBase: '/' };
angular-cli 建立的 environment.ts 里有一段注释,以下图。 意思是若是咱们用 ng build 命令打包的时候,加上 --env=prod(若是是自定义environment文件,必须是ng build --environment=xxx命令),将会把 environment.ts 替代为 environment.prod.ts ,那么 main.ts 里的代码 import { environment } from './environments/environment'; 实际变成了 import { environment } from './environments/environment.prod'; 能够经过在 main.ts 打印日志查询当前环境变量是不是咱们须要的
所以,咱们就只须要把相应的环境变量配置好,以下API接口的代码和 main.ts 文件同样,我引入了 environment,在开发或者打包的时候,angular 配置的打包工具会自动载入相应的环境变量
由于业务需求缘由,过久没真正学习搭建新框架了,内心也一直不踏实,感受再不学点都跟不上时代了, 因此此次项目的机会也算是了却本身一个心愿。 对比ng4和ng1,开发模式有了很大的变化,给个人感受就是ng4的模块划分更干净,写法更舒服。 也多是由于有一些angualr系列的基础, 能力也应该比前年学习ng1的时候更强,此次入门很快。 es6和typescript语法有时候分不清谁是谁,落后的知识趁着此次尽快补起来。 由于新鲜感的缘由,写代码变得更有乐趣了,在页面细节和动画上,稍微多搞了一点东西进去(后台项目没有设计师,自由发挥)。转眼3个多月没写博客了,这之间学的新东西很少,可是回头补了一下基础的一些知识,也算是有不少收获和新的理解。 设计模式的博客写了一部分,后面会抽时间一步步完成,是一篇但愿你们都能看懂的博客,尽请期待!
在指令directive中要拿到当前使用指令的dom,须要使用ViewContainerRef
import { Directive, EventEmitter, ViewContainerRef, AfterViewInit, OnDestroy} from '@angular/core';
constructor(public viewContainerRef: ViewContainerRef) { }
//compontent @ViewChild('xxx') xxx: ElementRef; getXXX(){ console.log(this.xxx) } //html <div #xxx></div>
ng提供表单验证 FormGroup 能够定义每一个表单的验证条件,定义好以后,须要在表单下面写不少的ngIf dom来判断和展现当前表单的错误填写提醒,这样很很差的一点是提醒的文字是须要占位置的,在处理页面的时候须要兼容这些提醒文案,给他们作兼容布局(若是表单所有是独立的一排一排的还好,若是一行里有不少表单,每一个表单的宽度可能也不同,这时提醒文案就很差放了),并且每次写那么多条件和dom真的很麻烦。
写了一个指令组件,提醒文案做为弹出层展现出来,把当前表单的formControl对象传入指令,把全部的条件统一文案,好比说required 文案为‘’必填‘’。那须要作4件事情,1:动态为指令里加入组件(参考了官网 核心知识->模板与绑定->动态组件),2:让组件绝对定位到表单右上角,须要用一个div包裹一下表单,并获取表单的宽度,把宽度传给组件,组件给提示框设置绝对定位。3:传入formControl对象,指令组件须要判断显示隐藏,4:统一文案,条件知足后给显示框展现对应文案,由于formControl的errors是一个对象,因此须要配置一个管道pipe来把errors转换为对应文案。
ERROR Error: ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'undefined'. Current value: 'false'.
我写的每个指令都会有这个报错,通常报错在数据变化后触发,网上查了一下,说是没有使用 enableProdMode();方法就会触发这个报错,在main.ts中判断了环境,若是是开发环境的话就不使用enableProdMode();方法,因此目前开发环境会报错,可是也不影响逻辑,因此这个报错暂时忽略
一个页面可能会有一个弹窗来填写表单,填写的时候有两种状态,编辑和新增,可是都是用的同一个弹窗对象,表单作了验证、错误条件达到而且dirty属性为true的时候,就会展现错误提醒。在新增和编辑切换的时候若是直接修改表单的值,dirty就会一直是true,就可能一直有错误提示。因此须要在必定状况下使用 formGroup 的 reset来重置表单,dirty就会是false了。每一个表单本身也有reset方法。当使用formGroup重置表单的时候,textarea有可能并不会被重置,若是没有被重置,须要单独处理下textarea,给textarea的formControl对象单独reset一下。
使用ng build --prod 命令时,打包的检查比较严格。开发时候使用的private定义能够在模板里使用对象,在开发环境就会报错。一些模板里绑定的对象数据,是须要后端返回了数据才会传值给对象,在打包的时候就会检测到当前对象属性不存在就没法经过,这时就不能用{{xxx.atrr}}这种形式输出数据,得用{{xxx['attr']}}这种方式,才能跳过检查。
直接上图,图一会报错 编译没法经过,代码逻辑是正确的。图二能编译经过
ng5出来后,我会把当前项目升级,到时候项目也比较完善了,我会抽出核心部分,分享到github上,敬请期待。