AngularJS 使用 AngularJS 从零构建大型应用

seem · 2015年02月04日 · 最后由 seem 回复于 2015年08月05日 · 25704 次阅读
本帖已被管理员设置为精华贴
  • 0、导言
  • 1、准备工作
  • 2、构建框架
  • 3、丰富你的 directives
  • 4、公用的 services
  • 5、用 controllers 组织业务

导言

纵览线上各种 AngularJS 教程,大部分都是基础与一些技巧分析。 如果你已经能运行你的 ng-app,但又找不到实际案例可以参考。那么本文应该对您有所帮助。 本文将以电商产品:友好速搭 其中的 店铺后台 作为的实际案例,裸奔展示如何从零构建 “自以为大型的” AngularJS 应用。 应用基于 AngularJS 1.2.24 版本。

准备工作

1、我们使用了以类型优先的目录结构。 ├── js │   ├── app.js │   ├── directives.js │   ├── services.js │   ├── controllers │   │   ├── BaseController.js // controller 基类 │   │   ├── Customer.js // 顾客管理 │   │   ├── Product.js // 商品管理 │   │   ├── Order.js // 订单管理 │   │   ├── Domain.js // 域名管理 │   │   ├── Payment.js // 收款方式 │   │   └── ... // 其他各种 controller │   ├── directives │   │   ├── ysBtn.js // 按钮组件 │   │   ├── ysCalendar.js // 日历组件 │   │   ├── ysImgeditor.js // 图片编辑器 │   │   ├── ysPopWindow.js // 模态弹窗组件 │   │   └── ... // 其他各种组件 │   └── services │   │   ├── Graphic.js // 图片文件处理 service │   │   ├── Popup.js // 弹层 service │   │   ├── Uri.js // 与 Uri 相关操作 service ($http 等 ajax 操作封装于此) │   │  └── ... // 其他各种 service │   ├── config │   │   ├── Navigation.js // 主导航配置 │   │   ├── Route.js // 路由配置 │   │   ├── RouteProvider.js // 配置$routeProvider │   │   ├── OnRootScope.js // 为$rootScope 配置$onRootScope 方法 │   │   ├── SceDelegate.js // 配置$sceDelegateProvider │   │   ├── Uri.js // 后端服务 API 配置 │   │   └── ... // 其他各种 config ├── lib │   ├── bower_components │   │   ├── angular │   │   │   ├── angular.min.js │   │   │   └── ... │   │   ├── angular-route │   │   ├── angular-sanitize │   │   ├── angular-route │   │   └── ... // 其他 bower 管理的 lib │   ├── URI.min.js │   ├── class.js │   └── ... // 其他手动管理的 lib ├── img ├── css └── html // controllers 对应的 views 的模板

2、使用 less 作为 css 的预处理器。 3、使用 bower 管理依赖的 JS 库。 4、使用 grunt 作为项目打包工具。 5、使用 fiddler4 作为 http 请求调试工具。 6、为了可以使用庞大的 jquery 插件库,我们也引入了 jquery。 7、controllers、directives、services 等部分的设计参考自: http://trochette.github.io/Angular-Design-Patterns-Best-Practices/

准备了半天,访问如下的 index 页面,angular 的应用是时候跑起来了。 应用代码展示

一个实际的案例看起来总是要比教程复杂些。下面是项目启动后友好速搭店铺后台的概貌。 店铺后台

什么?不是说好的从零开始构建吗?怎么就跑起来了? 好吧,请把上面这个预览图当做设计稿。我们开始构建框架。

构建框架

根据设计,从结构上将页面划分为

顶部

导航 内容 导航的点击会改变浏览器当前的 url,内容区域渲染对应模块的内容。

下面开始配置 angularJS,来达到上面的目标。 在 js/app.js 配置 YeeshopManager 这个 module,并将它的引用赋值到全局变量 YeeshopManagerModule,方便后续继续对其进行配置。 配置module

为了方便管理众多 directives 与 services。我们分别创建了 js/directives.js 集合所有 directives 的 module - YeeshopManager.directives 配置directives

js/services.js 集合所有 services 的 module - YeeshopManager.services 配置services

接下来,配置应用为不同路径的请求调用对应的 controller 与模板 在 config/Route.js 中先定义好的规则 配置Route.js

在 config/RouteProvider.js 中配置 $routeProvider 配置$routeProvider

当我们使用 #/guide 路径访问友好速搭店铺后台的新手引导页面的时候, ngview ngRoute 将为这个 index 文件上带有 ng-view 的节点渲染对应的模板 guide.html 并且运行对应的 GuideController。

结构上划分的三个区域对应的 controller 的关系 顶部 -> TopbarController 导航 -> ysNavigationController 内容 -> 由 ngRoute 动态的根据当前 url 和配置,加载对应模块的内容。 至此,大体的结构已经完成。

每个 Controller 创建的$scope 都能独立很好的运行,但有时$scope 之间也需要通讯。这时,我们需要为$rootScope 配置一个方法,来完成这个工作。

在 config/OnRootScope.js 中配置 $rootScope 配置rootScope 使用$provide 的 decorator 方法在$rootScope 注册的时候,注入一个 $onRootScope 的方法。

需要被通知的$scope 调用 $onRootScope 来监听事件'notification', 发出通知的 controllers, directives,或 services 中,只需要注入 $rootScope 服务。就可以很方便的进行通知。 收到通知

友好速搭 作为一款 SAAS 产品,支持商家自行绑定域名,让用户更好的记住您的域名和品牌。

angularJS 构建的应用程序,需要将静态资源部署在 CDN 上,来保证用户访问的快速流畅。 那么,如果我绑定了 http://myshop.com/ ,店铺后台的地址就会是 http://myshop.com/admin, 这时 CDN 静态资源的地址会是形如 http://cdn.com/js/app.js。 当应用的 host http://myshop.com/ 和引用资源的 host http://cdn.com/ 不一致时, angular 会告诉你 Error: [$sce:insecurl] ,资源因安全策略而加载失败。

配置资源白名单$sceDelegateProvider.resourceUrlWhitelist,允许 angular 跨域请求指定的 url 的资源。 白名单

至此,我们完成对 YeeshopManagerModule 的配置。

丰富你的 directives

框架构建完成了,参考第二节中的设计稿,接下来我们需要为系统添加各种 UI 组件。比如图标、按钮、下拉菜单、弹窗。依照 angularJS 创始人 Misko Hevery 设计的初衷: “构建 UI 应该是声明式的。” 那么,我们也设计自己的规则,来声明 UI 组件:

图标 按钮 下拉菜单 弹窗 ……

完成了以上的思 (yi) 考 (yin),我们着手来声明图标组件。 声明图标组件

声明完成后,我们在模板里面进行调用。

<ys-ico type="trade"></ys-ico>

经过 angularJS 的编译之后输出的节点与实际效果。 图标效果

对于这个通用的自定义标签,我们习惯用“组件”来称呼他。在 angularJS 中,称为一个 directive。官方文档中的定义请越过山丘 虽然已白了头~~

使用强大的 directive。我们可以将系统中需要复用的所有组件、甚至一个复用的行为全部抽象出来。当你需要使用的时候,你只是需要声明他。 例如,一个旋转的图标:

<ys-ico keepRotating type="trade"></ys-ico>

友好速搭项目早期规划的 directive 展示

按照需求增加了水印编辑器 directive 水印编辑器

方便商户编辑图片,增加了图片编辑器 directive 图片编辑器

这时候,前端工程师的问题就来了:“还有多少 directives?” 暴走 答:纵观整个项目的生命周期,所有的 directive 不可能在初始便全部设计并构建好。请在项目进行的过程中,按需的增加或修改、丰富你的 directive,不断提升你构建 view 的效率。

依照上文构建与调用 directive 的经验。在实际项目中,我们在模块中需要 10 个 ico,那么我们便在其模板中调用十次的标签。

那么前端工程师的问题又来了:“如果模块的交互需要弹窗,难不成我要先算好有多少个?然后全部先在模块里面声明,并使用 ng-show="false"全部隐藏起来?” 暴走

Don't worry.不要被 Thinking-in-AngularJs 限制了您的思维。换一个姿势 Thinking in jquery。 当我们需要一个弹窗的时候,按照往常的做法,便是在你页面架构预设好的位置(例如页面底部),插入弹窗的 dom 结构。 没错,弹窗还是这样实现,不同的是。我们插入的是弹窗的 directive。 类似以下(简化代码未测试,仅为示例):

$('body').append($compile('<ys-popwindow data="PopupModalData">#{content}</ys-popwindow>')($scope));

我们通过动态插入 directive 解决了上面这个问题。但我总不能每个模块都写弹窗 dom 的插入吧。 这个时候,我们需要 service。

公用的 services

各种 services 为友好速搭店铺后台提供了统一的 api 接口调用,图像处理,弹层处理,实用工具等。 通过上面的目录,我们可以看到 services 文件夹下,有定义$Popup 的 Popup.js。 (简化代码未测试,仅为示例) service代码

注入到您所需的模块后,愉快的调用吧!

$Popup.modal({text : '弹窗标题'});

用 controllers 组织业务

一切就绪,开始堆业务代码! 用controllers图片 用controllers代码

裸奔展示到此结束~有劳看官,若有任何错漏~烦请指正~

请教,如何优化 ui 闪烁问题?如何优化首页的加载?

我是来打酱油的.. 😋

#1 楼 @flowerwrong 1、可以通过 ng-bind,ng-show 等避免因为变量拼接造成的闪烁问题。 2、目前通过将基础数据直接输出到页面,避免启动时多次请求 api 来优化加载。

没有用 CoffeeScript , Yeoman, Grunt, angular-ui-router, angular-bootstrap 么?多好的东西啊!

controllers 目录我会拆得更细,比如: │─ controllers │─│─ customes │─│─│─ index.js │─│─│─ new.js

对应的 views 也是如此

#4 楼 @miclle 我是这样拆分的:

root │─ app.js │─ common │─│─ services │─│─ directives │─│─ views(HTML) │─ foo │─│─ controllers │─│─ services │─│─ directives │─│─ views(HTML) │─ bar │─│─ controllers │─│─ services │─│─ directives │─│─ views(HTML)

#4 楼 @miclle 用了 grunt 和 angular-route~其他暂时还未应用至项目中~谢谢大大提醒~。 #5 楼 @xhj6 关于目录结构的组织,目前我个人是习惯/喜好/方便操作的角度出发,是否有其他角度的参考?

baseController 秀一下吧,参考参考,多谢~

2 跟 1 有多大变化。

#9 楼 @realwol 目前所知的都是“颠覆”的改变。不准备有迁移的计划

#6 楼 @seem 真正的 大型应用 应该是模块化的吧,我最初是用的 ngbp 起的步,但现在已经不记得是 ngbp 本身还是我参考了这篇笔记 之后把文件目录结构改成这个样子的

另外,ui-router 应该已经快成标配了,你值得拥有

#12 楼 @miclle 是的,后来在实施的过程中,发现完全独立的模块化其实也是蛮难的

#14 楼 @xhj6 一开始就用 Yeoman + Grunt 目录结果就是第一种,就保持下来了 Yeoman + Gulp 好像就是你那种

#15 楼 @miclle 我开始是用的 ngbp,后来也投入到了 Yeoman 的怀抱,但还是一直用的 Grunt,构建文件写好之后就再也不想动它了,Gulp 再简单也不如我不动啊,哈哈!

#7 楼 @nightire 那个里面主要用来 json resig 的继承,其实用我的这个更简单

https://github.com/i5ting/node-mini-obj/

支持 web 也支持 node,哈哈

#13 楼 @xhj6 thx~有机会来改进一下

控制器方法复用,我是这样做的

'use strict';

/**
 * @ngdoc function
 * @name ngblogApp.controller:BaseController
 * @description
 * # MainCtrl
 * Controller of the ngblogApp
 */
angular.module('ngblogApp')
  .controller('BaseCtrl', ['$scope', '$window', function ($scope, $window) {
    $scope.search = function() {
      $window.location.href = '/#/search/posts?keyword=' + $scope.keyword;
    };
  }]);

在其他控制器中

$controller('BaseCtrl', {$scope: $scope});

#7 楼 @nightire 这样该控制器就有了该功能。

为何不直接搞 ng2?

#20 楼 @mogodb ng2 还没出来吧。

ng 不适合大型应用

匿名 #24 2015年02月11日

:plus1:

@seem 你们在生产环境下 js 如何打包,压缩成一个文件还是各个文件不动?,如何解决 部署后浏览器缓存的问题?

弹窗那里ng-show不合适可以用ng-if呀,还是可以 think in angular

notification 一般是用在什么情况下呢?可以讲讲应用场景么?另外,为什么不考虑按 feature 组织目录结构呢?

:plus1:

#25 楼 @yakjuly

友好速搭的店铺后台在生产环境的 js 引用主要分为三个层次:

/lib/angular/1.2.28/angular.min.js 只有当升级 angular 的时候,用户才需要重新加载 /dist/lib-201502041711/lib.js 基础库,变动频率比业务代码低很多 /dist/201502121456/js/main.js 主要业务代码,每次版本更新都会重新打包 其他……

总结,在与业务层面无关的底层库,我们使用不同的版本号作为路径来避免缓存。 与业务层面相关的代码,在每次 grunt 打包项目的时候,使用时间戳作为路径来避免缓存。

使用时间戳将所有业务代码打包成 main.js 的方式,缺点是每次更新用户都需要重新下载所有代码(即使部分功能与上个版本完全一致)。但优点是在此时间戳下的资源完全独立,不用烦恼太多依赖问题,方便进行版本切换。

另,像友好速搭官网 https://youhaosuda.com/ 虽然我们不用 rails~~,但我们也使用了 (The Asset Pipeline)[http://guides.rubyonrails.org/asset_pipeline.html] 的方式,来避免缓存 /manage/dist/main.f36197098e.js

#27 楼 @darkbaby123

具体应用是在“添加商品模块” -> "展开商品分类选择器" -> "在商品分类选择器中点击‘商品分类管理 (弹窗)’ "

那么当前大概的$scope 树如下 root │─ 商品模块 (controller) │─│─ 商品分类选择器 (directive) │─ 弹窗 (directive) │─│─ 商品分类管理 (directive)

那么当商品分类管理中对数据进行修改,而商品模块及其分类选择器必须同时响应。我们在这种情况下使用了 notification。

至于目录结构的组织,请参见@miclle @xhj6 两位大神的讨论~~

#26 楼 @krazy 如果是已有弹窗“你确定要删除吗?”之上,再弹一个“你真的确定要删除吗?”。ng-if 的方式应该怎么组织代码?求赐教~~

网页的后退如何解决?看了些 angular 做的项目,这点做的都不好,比如我是个检索的页面,检索后就回不到之前选择的条件了。

#33 楼 @krazy 代码已阅~ 刚好就是文中描述的问题的示例: “那么前端工程师的问题又来了:“如果模块的交互需要弹窗,难不成我要先算好有多少个?然后全部先在模块里面声明,并使用 ng-show="false"全部隐藏起来?”(或者是 ng-if="false" 隐藏起来)

#34 楼 @seem 用 jquery append 不是手动把 ng-if 干的事情实现了一遍吗

#35 楼 @krazy 恩,从单个模块所需的弹窗来看,是这样。 但从多模块间的复用和代码管理上,直接罗列全部对话框应该会是一场噩梦吧? 或是有更好方案~~?

#32 楼 @Tim_Lang 恩,好问题。确实现在本系统还支持不好,跟进。

#36 楼 @seem 配合 ng-include? 正好请教下模板组织这块儿有无好的实践方案?

@seem 你们的权限系统复杂吗?验证都是在后台验证吗?你们是否把权限缓存在前段,来判断用户是否能够看到某链接?

有 rage comic 的博文 都是郝博文 我特别喜欢 raywenderlich.com 的博客

#40 楼 @zzz6519003 rage comic 能充分表现遇到问题刹那间一抹蛋蛋的忧伤

#39 楼 @yakjuly 1、不算复杂吧,具体你可以进我们产品看看咯。 2、API 当然有后台验证啦~ 3、目前是把当前版块权限输出在前端,所以更改权限后需要刷新页面才能生效。

加一层 routes 会不会好一些

友好苏打招人中!

#44 楼 @gazeldx 竟然收费的,看了好些是软文

#45 楼 @mogodb 哈哈。所以说啊,现在招人不容易的! 通过技术文章来招人,其实说到底,在我看来,对多方面都是有益的。

#43 楼 @mingle5566 恩 现在用的 angular 自带的 route,略弱。 就像 @xhj6 说的 ui-route 我值得拥有~

#44 楼 @gazeldx 友好速搭~扶老奶奶过马路~争取曝光度和招人 ing~

#45 楼 @mogodb 其实第一次听说友好速搭要收费我是拒绝的,因为,你不能让我付款,我就马上去交,第一我要试一下,因为我不愿意在试用版本加一些特技上去,店铺“duang”一下,很美、很快,这样其他用户出来一定会骂我,根本没有这样的服务,就证明上面那个是假的。后来我也经过证实他们确实是很好的,我用了大概一个月左右,感觉还不错,我就成为付费用户了。后来我在用的时候也要求他们不要给我特别装修,因为我要让其他用户看到,我用完之后是这个样子,你们用完之后也会是这个样子!

人艰不拆呀~duang~duang~

#49 楼 @seem 还是不明白,你们卖的是啥?

#50 楼 @mogodb 我们卖的是独立网店快速建站服务,youhaosuda.com 方便的话,占用您 5 分钟来试用一下。或许下次有电商项目需求,就不用重复开发基础模块,重复部署代码,重复优化服务器…专注定制界面与个性流程,提升开发效率与品质。

@seem 冒昧问问,不知道下次能开源下商品属性这部分的数据库设计和 JS 代码?可以的也做个类似的讲解!

#52 楼 @stephen ng 1.x 不是说要被 google 抛弃了么,那我们还用啥 ng 呢?

#54 楼 @mogodb 官方 Roadmap 还有 1.4 和 1.5。Angular 1: Version 1.4 will be out with a release candidate in a week or so with many exciting new features. The theme of 1.5 will be supporting integration with Angular 2. 原文链接

感觉还是 backbone 更棒点,rivets 可以 data binding 啊

想问一下后台 API 服务器是用 ruby on rails 吗,Grape 还是啥?

#57 楼 @gilbertz Rails 和 Grape 都可以,但我更喜欢 Rails

需要 登录 后方可回复, 如果你还没有账号请 注册新账号