天天看点

《AngularJS深度剖析与最佳实践》一1.4 实现第一个页面:注册

本节书摘来自华章出版社《angularjs深度剖析与最佳实践》一书中的第1章,第1.4节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区“华章计算机”公众号查看

接下来,我们开始实现第一个迭代的第一个功能:10.注册。

我们把能够通过url独立访问的一项功能简称为“一个路由”,这里为注册功能分配一个叫作/reader/create的路由。

之所以不使用/register的形式,是希望在各个url之间保持统一,这也是我们在整个项目中将贯穿的一个约定。

如同后端开发一样,我们将reader称为controller,create称作action,中间还可以有一个id,所以,典型的url是这样的:/$controller/:id?/$action,其中的id字段是可以省略的,取决于具体的action。

这样,我们在url和文件所在的路径之间就可以建立一个简单的映射关系:拿到一个url,如/reader/1/edit,其中reader是$controller,edit是$action,于是我们知道它的代码位于app/controllers/reader目录下,其模板为edit.html,控制器为edit.js,样式为edit.scss。

这有什么好处呢?比如测试人员报告说一个页面存在bug,其url是/book/1/preview,我们一看bug报告,判断出其错误位于控制器中,于是,我们直接开始修改app/controllers/book/preview.js。

如果使用的是intellij/webstorm,那么我们只要按下cmd-shift-n(navigate|file...,mac下)组合键,输入preview.js,然后回车,就可以直接开始编辑了。其他ide也有类似的功能。

不要小看这一点点约定,它可以节省很多不必要的时间浪费,特别是在多人协作开发时,你的代码可能会被很多人修改,如果连这个都需要沟通或者阅读代码,那么浪费的时间和精力也是很可观的。

分配完url,我们还需要把这个url和控制器、模板对应起来。虽然我们有了一个约定,但是程序并不知道,我们还需要找个地方声明一下,这个地方就是app/configs/router.js。

范例工程会给它生成一个代码骨架,为了方便加注释,我对一些语句进行了额外换行,实际代码中是不需要这么多换行的:

定义完路由,我们仍然不能直接通过url访问它,如果访问则会在浏览器控制台中发现一个错误信息:angular.js:10126 error: [ng:areq] argument 'readercreatectrl' is not a function, got undefined(angular.js:10126 错误:[ng:areq]参数'readercreatectrl'不是函数,而是未定义。),其原因是没有找到名为readercreatectrl的控制器。

接下来,我们就对它所引用的模板和控制器进行实现。

首先我们要创建一个app/controllers/reader/create.js文件,和一个app/controllers/reader/create.html文件,我们来看一个空白的create.js文件:

接下来,我们就要开始实现它们了。

如果已经有ux(用户体验设计师)或ba(业务分析师)给出的原型图,那么建议从设计model的数据结构开始,这样有助于更深入的理解angular开发中最显著的特点:模型驱动。如果是从零开始,也可以先设计html。我们这个项目的开发是单人项目,所以我们先直接设计html。到本节的最后,我再来讲解根据原型图做模型驱动开发的过程。

我们要设计一个html,但不用设计一个“漂亮的”html。注意,在一个项目组中,不同的角色是需要分工的,要把ux擅长的工作留给ux;即使是单人项目,也需要把不同类型的工作分开完成。

在注册页,我们需要一个表单,它具有如下业务意义上的字段:邮箱、昵称、密码、确认密码;还需要一些技术和法律意义上的字段:图形验证码(captcha)、网站服务协议、“同意服务协议”复选框。

由于我们的业务并不需要手机号、年龄之类的字段,那么我们就不要收集它。这种“最小信息”原则,可以帮助你在受到安全攻击的时候把损失控制在最小。同时,把需要填写的内容控制在最小范围内,也有利于提升用户体验。

我们的第一个html页面如下:

注意,用来在input和label之间建立关联的id字段都是用下划线开头的,这并不是随意为之,而是要把id留给写“端到端测试”的人员。我们把这些不能不用的id全用下划线开头,有助于防止潜在的冲突。

现在,我们切到浏览器中,会看到一个很难看的表单,如图1-1所示。

《AngularJS深度剖析与最佳实践》一1.4 实现第一个页面:注册

虽然简陋,但已经足够表现我们的html骨架了。

接下来,我们需要把它修到可正常交互的级别。

首先,我们需要给每个输入型字段(input/textarea/select等)绑定一个model变量。仅以邮箱字段为例,我们把它修改为:

这里的ng-model就是angular中一系列“魔法”的关键。它是一个angular指令,其作用是把所在的input元素和ng-model=""中的表达式建立双向绑定,这种双向绑定意味着,当表达式的值发生变化时,input的value会跟着变化,反过来,当input中的value 由于用户操作而发生变化时,绑定表达式的值也会相应跟着变化。而且这两者的数据格式并不需要保持一致,ng-model指令提供了一系列机制在两者之间进行转换。

ng-model并不限于用在input/textarea/select元素中,事实上,它几乎可以用在任何元素中。但只有这几个元素可以直接使用ng-model,用于其他元素时需要自己写自定义指令来实现双向绑定。这是因为angular自带了对input/textarea/select的重写指令,重定义了它们的行为,使其可以支持ng-model。在后面的章节,我们会看到如何在自定义指令中支持ng-model。

这里还涉及另一项约定和最佳实践:把当前表单的所有字段绑定到一个叫作vm.form的对象中,这样我们就可以很方便的把表单数据作为一个整体进行处理,比如提交或重置。

除了“确认密码”和“同意协议”之外的其他的字段可以以此类推,不再摘引源码。

“确认密码”字段之所以特殊,是因为它并不需要最终提交给服务端,我们只是为了防止用户输入错误,靠前端来校验就已经足够了。我们设想一下攻击场景,发现对它只做前端校验并不会构成安全漏洞。所以,我们不需要把它绑定到vm.form对象的属性中去,用个独立的vm.retypedpassword变量就可以了。

而“同意协议”字段也同样可以依靠纯前端验证。在法律上,我们只要尽到了提醒和询问的义务即可。如果用户通过非正规手段绕过前端直接访问服务端,不能作为未曾同意协议的借口。

注意,前面我们只是修改了html就已经完成了双向绑定,我们并没有在控制器中定义vm.form.email变量,甚至连vm.form对象都没有定义。这是因为在angular中做了容错处理,发现一个变量没有定义时,它会自动帮我们定义一下,而不会触发错误,这个特性在写模板时非常有用。

“图形验证码”的字段也比较特殊,我们把它留到下一节去处理。现在,我们的界面就已经到了最初的可交互级别。

接下来我们有两个分支可以走:套用bootstrap类进行初步美化(ux分支),或者开始实现表单提交代码(程序员分支),两者可以同时进行。

为了尽快看到“可行走骨架”,我们选择程序员的分支:实现表单提交代码。要在收集了表单数据的基础上进一步实现表单提交,我们就要借助另一个指令了,这个指令叫作ng-submit。

直觉上,我们可能希望在提交按钮上绑定一个事件来完成表单提交,但更好的方式是在form上绑定一个ng-submit事件。这是因为触发表单提交并不是只有点击“提交”按钮这一种方式,用户还可以在input中敲回车键来直接提交表单,在大多数场景下,这是更为友好的方式。但更重要的是,这样的html,其表意性更强一些。

这个步骤对代码的影响很小,在html中,我们只要把

改为`javascript

-submit="vm.submit(vm.form)">即可,在javascript中增加几句指令即可:

vm.submit = function(form) {

};

module.exports = function (config) {

angular.module('com.ngnice.app').factory('reader', function readerfactory ($resource) {

});

angular.module('com.ngnice.app').controller('readercreatectrl', function readercreatectrl($resource) {

angular.module('com.ngnice.app').controller('readercreatectrl', function readercreatectrl(reader) {

reader.save(form,

);

{

}

$$validitystate: validitystate

$dirty: false

$error: object

$formatters: array[2]

$invalid: true

$isempty: function (value) {...

$modelvalue: undefined

$name: "email"

$parsers: array[2]

$pristine: true

$render: function () {...

$setpristine: function () {...

$setvalidity: function (validationerrorkey, isvalid) {...

$setviewvalue: function (value) {...

$valid: false

$viewchangelisteners: array[0]

$viewvalue: undefined

angular.module('com.ngnice.app').directive(

// 指令名称,它会按照约定转换成减号分隔的标识符后才能在模板中使用:<code>bf-field-error</code>,这里的bf是book-forum的缩写,用这个前缀来防止和其他指令冲突。

'bffielderror', function bffielderror($compile) {

var hint = $compile('

{{name}}')(subscope);

{{name | error}}')(subscope);

angular.module('com.ngnice.app').filter('error', function () {

1)app/constants/errors.js:

angular.module('com.ngnice.app').constant('errors', {

2)app/filters/error.js:

angular.module('com.ngnice.app').filter('error', function (errors) {

angular.module('com.ngnice.app').directive('bfassertsameas', function bfassertsameas() {

...

angular.module('com.ngnice.app').directive('bfcaptcha', function bfcaptcha() {

继续阅读