这里有新鲜出炉的 AngularJS 教程, 程序狗速度看过来!
AngularJS 前端 JS 框架
AngularJS 诞生于 Google 是一款优秀的前端 JS 框架, 已经被用于 Google 的多款产品当中 AngularJS 有着诸多特性, 最为核心的是: MVC 模块化自动化双向数据绑定语义化标签依赖注入, 等等
本篇文章主要介绍了浅谈 angular4 实际项目搭建总结, 小编觉得挺不错的, 现在分享给大家, 也给大家做个参考一起跟随小编过来看看吧
前言
接到一个 pc 端后台项目, 还会加入两个安卓同事一起学习和做这个项目, 需要带一下他们 既 ng1 之后, 我就没怎么有过其它后台框架的实际项目经验了, 期间用的移动端框架也并非 vueangular 系列 react, 包括 es6webpack 等也都并不熟悉 公司一个其它后台项目使用了 vue, 偶尔我会维护一下但是总体来说对体系不熟悉 一直比较喜欢 angular, 应该是有过 ng1 框架搭建的经验的原因(前面也有写过博客), 学习过 ng2 和 ng4 的官方 demo, 总的来说照着抄写一遍意义不大, 必须要用于实际项目才能真正吸收 目前 ng1 肯定不要用了, 我要重新熟悉和搭建一个架子, 从自我喜好和前期一些基础来讲, ng4 是最好的选择, 刚好 typescript 的语法对安卓同事也比较友好 向领导请示了之后, 也得到了允许
入坑之初, 问题一般比较多, 使用的是官方 angular-cli 创建项目 中途完善 cli 的功能, 理解整个框架是比较费精力的事情 总的来说这次就花了两三天时间, 梳理好了框架, 配置好了一些基本功能和依赖, 比如环境部署, 路由嵌套, 主页面布局(侧边栏, 导航栏, 内容, 底部), 公共服务(loading,http 请求封装, 全局服务 title, 用户信息存取) 而后就直接和同事一起开发了 最开始还有点没底, 不知道搭架子要花多久时间, 现在来看这个进度和时间还是很满意的, 当然过程中参考了一些网上的同行分享的资源和代码 非常感谢两位同事的积极支持, 他们学习的也很快
问题细节点
sCSS
angular-cli.json 设置 styleExt 为 scss 之后, 在组件里写 Styles, 并不会编译把 scss 编译成 css, 必须要写在独立的 scss, 然后通过 styleUrls 引入
routes path
routes path 前面不能加 / 否则会报错
- const routes: Routes = [
- { path: '', redirectTo:'/main', pathMatch:'full' },
- {
- path: 'main',
- component: MainComponent
- },
- ]
- <router-outlet>
<router-outlet> 的意思是当通过路由访问 component 的时候, component 的 selector 会找到 <router-outlet></router-outlet> ,@Component 定义的 selector 会直接生成一个标签, 载入到 < router-outlet></router-outlet > 下方, 如果不设置 selector 标签名就是默认的 ng-component
- RouterModule
- 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 去注册路由
routes 的 loadChildren 属性
ng4 的提供了路由懒加载, 如下, 我们有个一级路由 / main,/main 有个二级路由 / home 对应 home 组件, 我们可以通过, 下面的方法来定义子模块的路由和组件, 但是这种写法就需要把路由写在一起, 而我们希望 home 组件的路由是一个单独的 home.routes.ts 文件存在于 home 文件夹中, 通过在类似父模块中引入子模块的方式注册 home 路由
- {
- 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, 这样就重复注入了, 而且还是一次和父模块同级的注册, 这一点要小心
- {
- 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 { }
总结一下上面的 router 相关内容 :
假设一个场景, 根模块注册两个路径, 一个是 / login, 一个是 / main/login 这个路由访问就是单纯的一个登陆页面,/main 下面的路由都将是对应核心页面和业务, 因为在 main 组件里包含了公用的侧边栏导航栏内容容器和底部栏, 所以 /main 路由加载的 main 组件的内容容器里需要嵌套子模块 举个实例, 当我访问 / main/home 的时候 main 组件会加载 home 组件到 content 容器中, 当我访问 / main/manager,manager 组件又会替换 content 中的 home 这样我们的公共页面部分就是不变的 main 组件, 根据子路由的变化, 去加载不同的组件到 content 里
以下是 main 组件的 html 大致代码和实际页面截图:
这里也有一个知识点是 < router-outlet > 标签的嵌套, 上面的代码中 < div > 下面有一个 < router-outlet > 标签, home 等二级组件会被载入到父组件 main 的 < router-outlet > 标签下而 main 组件的父组件是引导组件 AppCompontent, 所以 main 组件是通过一级路由被载入到 AppCompontent 的 html 模板的 < router-outlet > 下方, 以下是 AppCompontent 组件部分代码:
那么打开调试器, 我们就能从 dom 节点上看清楚, router-outlet 的嵌套关系:
目录结构
再看下 src 的目录结构, component 文件夹是存放一些公共的组件, login 和 main 组件是注册的一级路由, home 和另一个马赛克组件是注册为 main 的二级路由, 实际后面会注册很多组件到 main 下, 但是他们的文件夹划分都是同级
使用 hash 路由
RouterModule.forRoot(routes, { useHash: true }); 使用 hash 路由, 后端不用修改配置, 这样比较方便, 省去很多麻烦
title
引入了 platform-browser 的 Title, 使用它的 setTitle 方法改标题
APP_BASE_HREF
在非 hash 路由情况下, 有时候环境的原因布置静态资源路径的时候可能不是根域名, 同时还要删除 index.html 的 < base > 标签, 否则会有问题
import { APP_BASE_HREF } from '@angular/common';
在 app.module 里注册 providers: [{provide: APP_BASE_HREF, useValue: environment.publicBase}],
http
使用 http 相关 API, 需要注入 HttpModule, 否则会报错: No provider for Http
import { HttpModule } from '@angular/http';
引入了三方 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
- bootstrap4
考虑引入 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 值
NG-ZORRO
想了想, 需要快速开发, 还是需要一个 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 个多月没写博客了, 这之间学的新东西不多, 但是回头补了一下基础的一些知识, 也算是有很多收获和新的理解 设计模式的博客写了一部分, 后面会抽时间一步步完成, 是一篇希望大家都能看懂的博客, 尽请期待!
PS: 这篇博客不定期更新, 更新的写到下面吧
[ngClass]="item.fromAccount == webimS.userId ?'me':'other'" ,ngClass 可以这样写, 官网没有这种示例
按照缩写规则, 指令 directive 我应该写成 xx.d.ts 的, 但是 d.ts 这个格式的文件似乎会被框架其他程序处理, 就会报错, 所以指令的命名我就没用 d.ts 这样的缩写了
指令在父模块 declarations 之后, 发现在子模块里使用指令, 根本没有反应折腾了很久, 发现 declarations 申明的只在当前模块才能使用, 而我的懒加载的子模块无效, 所以为指令定义一个独立的 module, 在需要使用指令的地方 import 这个 module, 如下图
在指令 directive 中要拿到当前使用指令的 dom, 需要使用 ViewContainerRef
- import { Directive, EventEmitter, ViewContainerRef, AfterViewInit, OnDestroy} from '@angular/core';
- constructor(public viewContainerRef: ViewContainerRef) { }
如果我们需要拿到当前控制器下某个 dom 节点, 需要使用 @ViewChild
- //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(); 方法, 所以目前开发环境会报错, 但是也不影响逻辑, 所以这个报错暂时忽略
formreset:
一个页面可能会有一个弹窗来填写表单, 填写的时候有两种状态, 编辑和新增, 但是都是用的同一个弹窗对象, 表单做了验证错误条件达到并且 dirty 属性为 true 的时候, 就会展示错误提醒在新增和编辑切换的时候如果直接修改表单的值, dirty 就会一直是 true, 就可能一直有错误提示所以需要在一定情况下使用 formGroup 的 reset 来重置表单, dirty 就会是 false 了每个表单自己也有 reset 方法当使用 formGroup 重置表单的时候, textarea 有可能并不会被重置, 如果没有被重置, 需要单独处理下 textarea, 给 textarea 的 formControl 对象单独 reset 一下
正式环境打包的检查:
使用 ng build --prod 命令时, 打包的检查比较严格开发时候使用的 private 定义可以在模板里使用对象, 在开发环境就会报错一些模板里绑定的对象数据, 是需要后端返回了数据才会传值给对象, 在打包的时候就会检测到当前对象属性不存在就无法通过, 这时就不能用 {{xxx.atrr}} 这种形式输出数据, 得用 {{xxx['attr']}} 这种方式, 才能跳过检查
某些情况使用 this 编译会报错:
直接上图, 图一会报错编译无法通过, 代码逻辑是正确的图二能编译通过
ng5 出来后, 我会把当前项目升级, 到时候项目也比较完善了, 我会抽出核心部分, 分享到 github 上, 敬请期待
来源: http://www.phperz.com/article/18/0312/361572.html