近日来我有幸主导了一个典型的web app开发。该项目从产品层次来说是个典型的CRUD应用,故而我毫不犹豫地采用了grunt + boilerplate + angularjs + bootstrap + D3 + requirejs 的架构来实现它。angularjs早在去年6月份我就有所接触,将它应用在实验室项目的个别页面中,11月份在新浪的时候也将其推荐给了所在云事业部项目组。项目组老大程辉等人都是很有技术敏感性的人,大胆地采纳了我的建议,将之应用于原本使用dojo开发的项目前端模块上。然而,由于前端模块很大,并且已经用dojo开发了很长时间了,故而angularjs一开始也是应用在一些页面和子模块上,至我离开新浪之前还完成全部的重构。虽然之后我也做过一些angularjs的插件之类的事,但终究没有用angularjs完整地搞一个大型项目的经验。故而这次实验室交予我一整个项目,且不管所得利益几何,对于一个对前端有爱的人而言,这是一次从头开始完整地进行angularjs项目实战的机会,我怎么可能放过。期间种种我都想用博客记录下来,以备后用。
开发大型javascript应用和做几个demo页面玩一玩最大的区别在于,要对一开始的文件组织和模块划分要有更清晰的认识。这项工作前期花了我较多的时间去处理。故而第一篇文章就写模块划分和目录组织,这方面本人经验不多,不足之处敬请指出。
项目需求和资源评估
没有什么模块划分是必须的和通用的,模块划分都是在对项目需求、手头资源(员工资源、时间资源和物质资源)有充分的评估,同时参考大量范型的基础上作出决定。所以先大致介绍一下我接手的项目需求。这是一个典型的B/S架构的呈报系统软件。要求有用户-服务台-办事人员-系统管理员四级的角色架构,针对不同的角色,显示不同界面,每种界面内含子页面大约20个不等,子页面互相有重复。需求要求系统有足够的稳定性,一般的并发性。前端要求有设计感,足够美观,快速响应,对一些特定统计内容有相应的可视化呈现。不需支持IE6。项目成型后,作为软件安装部署,二次开发可能性较小。人力资源方面,加上我干活的有三个,其中前端熟练工一个(俺),有一定开发经验的一个,编写代码一般可视化程序操作优良踏实肯干的一个。时间资源大约3人*60个工作日。
根据项目需求可知,项目从用户角色可划分为四个大模块:用户、服务台、办事人员、系统管理员。大部分页面逻辑较为单一,易于划分。对于重复子页面可另建立通用模块。考虑到人力方面,2/3是非熟练工,美工设计人员基本没有(本来我是,但是现在的工作量不允许我做这个了),工程量大时间紧,故而要求将设计的工作量降到最低。使用的技术和模块划分要尽量易上手,减少重复劳动,提高并发效率。由于前端人员较多,后台较为简单,故而整体架构上,后台只须提供REST风格的API,将工作重心前移,前台负责主要逻辑模块的实现。
前端初始架构设计
在确认了需求和资源评估之后,本人接下来的工作是在github上遍览使用了angularjs的各种项目,以及google相关文档。其中github上的几个project给我了我较大启发,它们分别是:
- https://github.com/angular/angular-seed
- https://github.com/zhangdiwaa/angular-coffee-AMD-seed
- https://github.com/elsom25/angular-requirejs-html5boilerplate-seed
- https://github.com/mz121star/NJBlog (一个使用 Mongodb+Nodejs+express+angularjs写的博客程序)
- https://github.com/zhangdiwaa/ng-grid(一个基于angularjs写的grid插件)
根据这些参考内容,我设计了初始的技术路线,如图所示:
简要说明一下:
web server使用的是node.js。同时配以自动化工具grunt做一些端到端测试、代码压缩和合并之类的工作。
framwork当然用的是angularjs做MVVM框架。同时用了一些UI小插件,如angular-ui-bootstrap, angular-ui和我自己写的表格插件angrid(https://github.com/zhangdiwaa/anGrid)。因为表格插件对于本项目来说比较重要,这里采用自己写的表格插件主要是为了便于定制(想当年为了定制dojo的表格插件改得要吐)。在模块划分上,一开始是模仿angular-seed的方式,就用一个myApp作为程序入口点,direcvtives、filters、service作为文件夹,存放对应的js文件。
基础的工具集采用jquery以及bootstrap。bootstrap这个东西的好处就在于可以快速地让不懂设计的人也能快速开发出能看的界面,搞定不挑剔的用户足够了。何况国内还有很多软件系统的界面远不如bootstrap好看。这里bootstrap我们没有使用其开发包而是直接使用它的工程包,并且摒弃了less。这样做因为考虑到需求我们并不需要对bootstrap做多少定制化处理。直接使用原始设计,将样式设计的工作量降到最低。less这个东西,只在需要非常频繁变更样式设计的时候方显本事,其他时候还不如直接用CSS。
其他工具集主要是前端数据可视化工具包D3,D3的强大无需说明。另外把boilerplate单独说一下是因为这东西我确实很喜欢,它是制作符合html5+CSS3规范的网页程序的快捷方式
初始架构设计的问题1:angularjs没有诸如AMD或者CMD的机制。
当我企图以现有方案继续的时候,发现在从myApp划分子模块开始就进行不下去了。问题如下:angularjs没有诸如AMD或者CMD的机制。从myApp开始,myApp所有的依赖都必须先行加载。虽然angularjs可以使用路由按需加载模板,但是控制器却要先行加载(如下面的代码所示)。
我的项目里有可分4个角色模块(用户、服务台、办事人员、系统管理员),对于一个入口点压力已经很大了,每个模块还有20多个页面,难道所有的控制器、以来模块都要全部加载吗?这显然不科学。必须按需加载。
于是我在网上搜了半天,首先找到的方法如下:
这个办法相当于使用angularjs内置的ajax方法,延迟加载了js文件。然而此法并不好用。在每个控制函数后面都写个延迟加载属性方法显得很笨,当然,你可以把它写到prototype去或者改angularjs源码,但那样做很有可能是给另外两个非熟练开发人员埋地雷。总之延迟加载controller问题多多,尤其是在显示的时候,具体你试试就知道了。
于是我想到了require.js这个AMD工具。之所以用require.js而不是国产的seajs并非我歧视国产(事实上我上一个项目就用的是seajs),而是因为require.js文档丰富,在github上有很多例程,适合教其他两个开发人员使用。除此以外require.js的AMD模式跟dojo的AMD按需加载方式如出一辙,因此使用过dojo开发的我对require.js更有好感。于是我最终还是使用了require.js做AMD方式的按需加载。
更进一步地,github上还有人基于RequireJS写了在angular路由时动态加载templete,controller,和directives的插件。在模块较少较清晰的情况下,这样已经够用了。 https://github.com/matys84pl/angularjs-requirejs-lazy-controllers
初始架构设计的问题2:我们的程序真的只需要一个app做入口点吗?
在angularjs的教程中,总是使用一个入口模块(通常是叫myApp)来组织整个程序。不论是angular-seed,phonecat这样的示例程序还是像NJBlog这样的比较大的应用都是一个入口点。但是,我们的项目跟这些都不同,是一个比他们都大的多、复杂的多的项目。由于本项目中的角色之间功能是严格分离的,所以产生了四大角色模块用户、服务台、办事人员、系统管理员,它们之间是不会串门的。所以这4个模块完全可以用4个app做入口点来组织程序。最后再加上一个登陆模块,做判定和跳转即可。使用requirejs最怕的问题就是依赖关系太多从而产生混乱,而这样划分5个入口的方式决定了每个app的依赖都不是太多,整个程序的功能逻辑一目了然。
虽然事实上,采用一个入口点,然后通过内部机制判断,从而在各个模块之间跳转也是可行的,但是那样程序复杂度就要高的多,还要给另外两个搭档解释都要解释半天,所以还是算了吧。
初始架构设计的问题3:按模块组织文件
一开始的时候,我的文件目录完全是照书抄书仿照angular-seed进行组织的。考虑到文件较多较复杂,于是就设置了几个direcvtives、filters、service作为文件夹,存放对应的js文件。于是文件目录看起来就像下面这个样子:
- controllers/
- LoginController.js
- RegistrationController.js
- ProductDetailController.js
- SearchResultsController.js
- directives.js
- filters.js
- models/
- CartModel.js
- ProductModel.js
- SearchResultsModel.js
- UserModel.js
- services/
- CartService.js
- UserService.js
- ProductService.js
但是这样真的好用吗?
看起来文件排列的是很整齐,但是叫我的搭档来看,他依然不清楚这些对象的依赖关系。尤其当要重用某些模块的时候,他必须从各个文件夹中搜集相关文件,而且常常会遗漏某些文件夹中的对象。
事实上,在快速开发中确实很少会在新项目中重用很多代码(重复代码倒有很多),但很可能需要重用登陆系统这样的整个模块。
所以,不如按照功能模块去组织文件夹。最终,我的目录是这么排列的:
- build/(工程目录)
- css/
- img/
- js/
- appAdmin/ (独立模块,以app名字开头,各app模块内容近似)
- controller/ (相关的子模板的controller.js存放在这里)
- directives/ (相关的directive.js存放在这里)
- admin_app.js (app模型定义和路由配置文件)
- admin_main.js (requirejs的入口和配置文件)
- admin_services.js (app的相关服务配置文件)
- appCustomer/
- appHelpdesk/
- appRepairer/
- appLogin/
- common/ (通用模块库)
- angular_filter/ (一些通用的过滤器)
- common_plugin.js (一些非基于angular的但比较重要组件,例如console plugin)
- utils/ (其他组件)
- appAdmin/ (独立模块,以app名字开头,各app模块内容近似)
- lib/ (包含所有第三方类库)
- templete/ (子模板文件夹,其内容按模块类型分类)
- common/
- tplAdmin/
- tplCustomer/
- tplHelpdesk/
- tplRepairer/
- 404.html
- admin.html (admin模块的入口html)
- customer.html (customer模块的入口html)
- helpdesk.html (helpdesk模块的入口html)
- login.html (login模块的入口html)
- index.html (程序的总入口点,用以根据配置跳转到各个模块入口)
- repairer.html (repairer模块的入口html)
- node_modules/ (grunt)
- src/ (未经grunt处理的源文件)
- test/ (端到端测试)
- gruntfile.js (grunt)
- human.txt
- package.json (grunt)
- README.md
显然,这里我将js和templete里面的文件都按模块划分为一个个子文件夹,在build的根目录下留下了数个模块入口点。这样已经足够清晰了。
还有更为激进的做法,就是将属于同一个模块的templete、css、js全放在一个模块目录下,我上一个项目就是这么做的。但是上一个项目未能清晰地划分通用模块和功能模块,造成了一定混乱。我暂时不好比较这两种划分的优劣。至少以目前的划分,已经足够可用了。