componentWillUpdate
方法中判断一下当前state
/props
有没有改变, 这个判断其实跟PureRenderMixin
干的是完全一样的事情, 但是PureRenderMixin
是在shouldComponentUpdate
这个life cycle method里执行的, 所以并不能直接拿来用. 跑去看了一下PureRenderMixin
的实现, 发现它对state
/props
的比较是依赖于一个shallowEqual
方法, 而这个方法在fbjs
里面, fbjs是Facebook维护的一个实用类库的集合, 所以今天决定读一读这个项目, 希望能从中学到一些新的东西.
那先来分享一下学到的第一个新东西吧.
String.replace()
之前对这个函数的认识比较简单, 只知道可能将指定的字符串或pattern替换成另外一个字符串, 如:
1 2 |
|
或者是:
1 2 3 4 |
|
需要注意的是, 正则表达式没有使用g
选项的时候, replace
只会替换第一处匹配.
今天学到的高级用法是replace
函数还可以用一个函数来返回替换量, 而且这个函数的参数是整个匹配还有这个匹配中的所有捕获组, 比如fbjs
中的camelize函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
这里 _hyphenPattern
用来匹配连字符写法, 并将连字符写法替换成驼峰写法, 我们无法通过简单将-
替换成空白字符来实现, 因为我们还需要将-
后面的字母大写, 这个时候可以接受捕获组的函数上可以出场了. 在 _hyphenPattern
的定义里面, -
后面的那个字母是被定义成一个捕获组, 它会作为第二个参数传入replacement function(第一个参数是匹配的字符串), replacement function将这个字母大写之后返回, 就实现了将-
+ 任意字母 替换成了一个大写之后的字母的.
Backbone
+ React
作为前端框架, Backbone主要是用来作为Model
来与后台的Restful API进行交互, React则负责View
, 也算是发挥了各自的优势: Backbone的Model和Collection只需要指定对应Resource的url, 就可以直接读取/更新/删除记录, 而不需要手动去发送对应的AJAX请求, 另外借助于寄生在Model/Collection上的underscore
方法, 操作数据也很方便; React接收Backbone的Model/Collection作为state
或props
, 通过Virtual DOM
来高效的DOM更新, 与其他的View技术相比较, React功能更单一, 概念更少更易理解, 而且有更高的性能. 这一切看下来都是那么的美好, 但是使用React还是在它的代价的, 代价就是JSX
.
简单讲, JSX
也就是一段写在JavaScript中的XML, 它是由React提供的一种更简洁的生成Virtual DOM的方法, 通过使用JSX
我们可以像书写HTML
一样来编写React Component, 然后再使用对应的编译器将JSX
定义的Component Tree翻译成不易读的React.createElement(...)
(Virtual DOM就是由React.createElemnt()
创建出来的)
正是因为JSX
代码会最终被翻译成对应的Javascript代码, 所以在书写JSX
代码的时候, 虽然我们说他的形式像是HTML
一样, 但是我们并不能完全遵照HTML规范来书写, 在JSX
中使用HTML的属性还是有一些限制(坑)的:
因为JSX
会被编译成JS代码供浏览器执行, 所以如果在JSX
中使用了Javascript的关键字的话, 那么很可能导致生成的代码无法被正确解释.(在IE上这个问题格外严重, 但在强大的Chrome上, 这种情况可以被很智能地处理).
一看下面的例子就明白了:
这段JSX
代码生成一个表单, 其中有一个Label和对应的Input用来输入用户的名字,
1 2 3 4 |
|
根据它编译生成的Javascript代码长这个样子:
1 2 3 4 5 6 7 8 |
|
通过比较我们基本上可以总结出这个翻译的过程, 对于一个JSX Tag
:
1 2 3 |
|
它生成的Javascript应该是:
1
|
|
可以看出其中TagName
是最安全的, 因为它在生成的JS中是一个字符串, 但是HTML Attribute就没有这么幸运了, 如果它碰巧是一个Javascript关键字, 那么恭喜你, 你掉到坑里去了. 比如上例中label
上面的for
属性在被编译后的Javascript代码中就很可能被浏览器当作是循环的那个for, 从而报出一个语法错误出来.
为了解决类似这种问题, React为这些”不幸”的属性定义了对应的”替身”, 它们中的一些是(不完全版):
Native HTML Attribute | React Version |
---|---|
for |
htmlFor |
class |
className |
inline style估计是最让React的初学者头痛的问题了, 如果我们按照HTML规范来使用的话, 是无法将想要的样式apply到元素上的, 至于为什么, 我们看一下由JSX编译成的JS代码就明白了:
1
|
|
我们在JSX中定义了一个DIV元素, 并且想通过inline style将它里面的文字颜色设为yellow, 然而这个DIV甚至都无法被渲染到页面中, 因为React认为它是不合法的:
1
|
|
在上面这段生成的JS里面, 用于设置style
的值是一个字符串, 这显然是无法工作的, 因为style
这个属性与其他属性不一样, 它比较特殊, 一般的属性的值都是一个普通的字符串, 比如type="text"
, name="fieldName"
, 这些属性值都是可以被浏览器直接使用的, 而style它传递的是一组样式属性, 它在这里已经不是一般的字符串, 而是可以被解析的一个样式对象, 说到这里, 估计你也猜到了, 我们不能直接使用一个普通字符串给style, 我们要传递的应该是一个JS对象(JSON)才对:
1
|
|
但是将代码改成这样之后还是无法将这个DIV渲染出来, 再次查看生成的JS代码:
1
|
|
style的值还是一个字符串! 这里正好引出了JSX的一条很重要的规则, 即被放在{}
中的都作为Javascript代码来处理, 但是如果{}
被引号包围起来, 那么它当作一个字符串处理. 这可能是JSX让人感觉比较困惑的地方, 当在JSX中使用{}
来引用JS变量的时候, 很容易让人感觉JSX
似乎是模板引擎, 而对于模板引擎来说(比如Freemarker, JSP等), 它们会将所模板中所有的变量都替换成对应的值, 不管它们是不是在一对双引号中:
如在Freemarker中, 如果变量 userName
的值是 ‘Erdong FENG’, 使用的模板是
1
|
|
那么最终生成的文件是这样的:
1
|
|
然而在JSX中并不是这样的, 因为JSX不是模板引擎
(当然了, 大多数时候JSX表现的还是像一个模板引擎, 比如在一个DIV的文字结点中, 但是在一个Tag的属性值中使用变量的时候, 情况完全不一样)
1
|
|
可以看到, 当你将变量名作为属性值的时候, 而且给这个属性值加上了引号的话, 它会被当作一个字符串被处理:
1
|
|
1
|
|
当然了, 我们在innerHTML的位置还是可以将JSX当作模板引擎来使用的:
1
|
|
最终的JS跟HTML如下:
1
|
|
1 2 3 4 5 |
|
一下子跑得有点偏了, 再回顾一下, 在JSX中, {}
中的东西是当作JS来处理的, 因为在生成的HTML中这部分会被对应的JS表达式的值替换, 但是如果是在属性值中使用{}
, 那么就一定不能用引号将它括起来了, 不然的话它就会当作字符串来处理!
再回到inline style的问题上, 基于上面的分析, 我们这次不给属性值加引号了
1
|
|
再编译一次, 这次直接就语法错误了, 因为{}
里面的部分是被当作Javscript来处理的, 而color: yellow
根本不是合法的JS语句, 再回顾一下, 我们这里需要的属性值是一个JSON, 那么应该是{color: yellow}
才对, 再将这个JSON放到{}
中去:
1
|
|
1
|
|
怎么还是有问题?! 原来是因为yellow被当作一个JS变量来解析, 但是却没有对应的变量存在, 所以这时候又引出了在JSX中使用inline style的另一条规则, 对于这些非字面量的值, 需要将他们用引号括起来, 表示他们是字符串:
1
|
|
这样就可以了!
除此之外, 还有另一个需要注意的地方, 如果style的属性名中含有连字符-
, 也需要做适当转换的, 因为属性名在编译后的JS代码中是JSON中的key, key要适合JS的变量名的定义, 因此是不能使用连字符-
的, React的解决办法是使用它的camelPattern, 好的, 讲完了.
1
|
|
双向绑定
, 无须显式声明Model
, 模块管理
, 依赖注入
等特点都给Web应用开发带来了极大的便利, 另外, 借助于它众多强大的原生directive
, 我们几乎可以避免麻烦的DOM操作了, 除了这些, Angular还有一个很大的亮点, 那就是高度的可测试性.
今天的Web开发已经不同往日, 更多的交互与逻辑都需要在前端完成, 有时候, 前端的代码量甚至在后端之上. 怎么去保证如此多的前端逻辑不被破坏, 依赖于功能测试? 这显示不现实, 功能测试很耗时, 而且它的创建成本较高, 所以通常只用它来覆盖最基本的那部分逻辑, 另一方面, 功能测试是依赖于流程的, 如果你想验证购买页面上的某个前端逻辑, 那么你就不得不一路从产品详情页面老老实实点过来, 反馈时间太长了, 可能你要等一分多钟才知道某个功能出错了, 我们自然不想把宝贵的开发时间浪费在等待上.
我在过去一段比较长的时候里都在项目上使用Angular, 在感受到Angular带来的便利的同时, 也饱受了Angular测试的折磨, 因为我一直觉得Angular的单元测试很难写, 跟JUnit + Mockito比起来, Angular代码的单元测试真是感觉写起来不得心应手, 更别说用TDD的方式来驱动开发.
我一直在思考为什么Angular社区说Angular的测试性很高, 但是在项目上实现用起来却是另一番境地. 经过分析项目上的代码, 我觉得要想驱动测试开发Angular代码, 那么其实是对你的Angular代码提出了比较高的要求, 你要遵循Angular的风格来开发你的应用, 只有你了解了其中的思想, 你的测试写起来才会轻松.
如果你已经使用Angular有一段时间了, 但是还没有读过这篇文章, 那么我强烈推荐你去读一下: Thinking in Angular.
先来看一看怎么样的Angular代码才是苗正根红的Angular代码.
像DOM操作这样的脏活累活都应该交给Angular的原生directive
去做, 我们的Angular代码应该只处理与DOM无关的业务逻辑. 来看一个简单的例子, 我们想创建一个简单的邮箱地址验证的directive, 它要实现的功能是, 当焦点从邮箱地址输入框移出的时候, 对输入框中的邮箱地址进行验证, 如果验证失败, 则向输入框添加一个样式表示输入的地址不合法, 比较糟糕的实现可能是这样的
1 2 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
上面的代码应该可以满足我们的要求(验证逻辑因为不是我们关注的重点, 所以并不完善), 而且这个directive实现起来也挺简单的, 但是现在让我们一起来分析一下为什么我们认为这种写法是比较糟糕的.
最简单的办法就是在你的directive里面去找所有与DOM操作相关的代码.
首先看到的就是on()
这个事件监听器. 完全没有必要自己去监听发生在被directive修饰的元素上的事件, angular有一整套的原生directive来干这个事情, 这里正确的做法应该是使用ng-blur
来处理blur
事件.
下一个有问题的地方就是addClass()
, angular除了提供了事件监听相关的directive外, 也提供了操作元素本身属性的directive, ng-class
就可以用来替换addClass()
方法.
按照这个思路修改后的代码:
1 2 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
比较一下这两个版本的实现, 是不是修改后的版本更简短, 更容易理解一些. 在新的版本里面, 我们只处理了业务逻辑, 即判断一个邮箱地址是否合法, 至于何时触发验证, 验证失败或成功之后应该有怎样的样式, 我们都统统交给了angular原生directive去处理了.
从测试的角度来看, 如果想给第一个版本的实现写单元测试, 那么要准备和验证的东西都很多, 我们需要设法去触发对应元素的blur
事件, 然后再验证这个元素上是否添加了error-box
这个class, 根据我的经验, 有时候为了验证这些DOM更新, 你还不得不创建真实的DOM结构添加到DOM tree上去, 又增加了一部分工作量.
而版本二就简单多了, 只定义了一个Model值isValid
来标识当前的邮箱地址是否合法, validate()
方法会在每次失焦之后自动执行, 要为它添加单元测试, 则只需要调用一下它的validate()
方法, 然后验证isValid
的值就可以了. SO EASY!~
一个Web项目中总是无法避免地要使用一些第三方的服务, 这里讨论的主要是前端的一些第三方服务, 比如在线客服, 站点统计等, 这些代码都在我们的控制之外, 大多数时候下都是从服务提供商的服务器上下载下来的, 而我们需要在业务代码中调用这些代码.
如果我们每次都是赤裸裸地以全局变量的形式来使用这些服务, 那么造成的问题就是这样的代码很难测试, 因为这些代码是不存在于我们的代码库中的, 而且内容应该也是不定时更新的, 大多数情况很多人会因为这些原因放弃到对这类操作的测试.
假设我们现在需要在某些动作发生之后调用一个第三方服务, 这个第三方服务叫做serviceLoadedFromExternal
, 它提供了一个API叫做makeServiceCall
, 如果直接使用这个API, 那么在测试中很难去验证这个服务被执行了(因为在单元测试环境中这个服务根本不存在), 但是如果我们将这个服务包装成一个angular service, 那么就可以在测试中轻易地将它替换成一个mock对象, 然后验证这个mock对象上的方法被调用了就可以了.
比较下面的两段代码:
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Angular是高度模块化的, 它希望通过这种模块的形式来解决JS代码管理上的混乱, 并且使用依赖注入来自动装配, 这一点与Spring IOC很像, 带来的好处就是你的依赖是可以随意替换的, 这就极大的增加了代码的可测试性.
Angular中使用service来组织那些可被复用的逻辑, 除此之外, 我们也可以将service理解为是对应一个领域对象的操作的集合, 因此, 通常会将一组Ajax操作放在一个service中去统一管理.
当然了,你也可以通过向你的directive或是controller中注入$http
, 但是我个人不喜欢这种做法. 首先, $http
是一个比较初级的依赖, 与其实注入的业务服务不是一个抽象层级, 如果在你的业务代码中直接操作http请求, 给人的一种感觉就像是在Spring MVC的request method中直接使用HttpServeletRequest
一样, 有点突兀, 另外会让整个方法失衡, 因为这些操作的抽象层次是不一样的. 其次就是给测试带来的麻烦, 我们不得不使用$httpBackend
来模拟一个HTTP请求的发送.
我们应该设法让测试更简单,通过将Ajax请求封装到service中, 我们只需要让被mock的service返回我们期望的结果就可以了. 只有这样大家才会喜欢写测试, 甚至是做到测试驱动开发, 要去mock$http
这样的东西, 显然是增加了测试的负担.
Angular中所有的Ajax请求默认都返回一个Promise对象, 不建议将处理Ajax返回值的逻辑通过回调函数的形式传递给发送http请求的service, 而应该是在调用service的地方利用返回的promise对象来决定如何处理.
让我们通过下面的例子来感受一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
这里的处理办法是将快递地址验证失败或成功之后的处理函数都传给了deliveryService
, 当验证结果从服务器端返回之后, 相应的处理函数会被执行. 这做写法其实是比较常见的, 但是问题出在哪里呢?
其实, 作为一个service的接口, validateAddress
应该只接收一个待验证的地址, 验证完成之后返回一个验证结果就可以了, 本来应该是一个很干净的接口, 我们之所以丑陋把对应的处理函数也传进去, 原因就在于这是一个异步的请求, 所以需要在发请求的时候就将对处理函数绑定
上去.
你应该已经猜到了第二个问题我会说一说对它的测试, 通常来说, 如果一个service创建成本较高或是存在外部依赖/请求的话, 我们会将这个service mock掉, 通过让mocked service直接返回我们想要的结果来让我们只关注被验证的业务逻辑.我们回头看一下上面的实现, 如果我们把deliveryService
的validateAddress()
方法mock掉, 那么我们根本没有办法去验证acceptAddress()
和rejectAddress()
里面的逻辑!
所以, 如果你的处理函数是传递给service中的API的话, 那么你的测试其实就已经跟这个API的实现绑定了, 你只有去创建一个真实的service并且让它发送HTTP请求, 你的处理函数才会被执行到.
经过这一番折腾, 你一定要说, 这测试比实现代码难写多了. 正确的打开方式应该是这样的: service的API只需要返回promise, 对应的处理函数的绑定
在这个返回的promise上, 这样我们只需要mock那个service的接口让它返回一个我们期望的promise, 然后控制promise的结果让对应的处理函数被执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
本来打算接下来介绍一下Angular代码的单元测试的各种模式的, 写着写着篇幅有点多了, 期待下一篇吧.
]]>function1
是定义在obj1
上的, 但是它还可以被obj2
所调用, 怎么做到的? 有下面的三种办法:
基本语法: fnc.call(thisArg, arg1, arg2... argN)
thisArg
将作为this
对象传入到函数fnc
中arg1, arg2... argN
作为参数列表传入到函数中通过call
方法可以让一个函数被任何的对象所使用, call()
会立即返回使用thisArg
作为this
对象, 余下部分作为参数列表来调用原函数fnc
的结果, 如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
上面的例子里toString
只是返回了身高与体重的数值, 但是只有数值是没有意义的, 我们还需要单位, 出于演示的目的我们打算将单位传为参数传入toString
方法, 修改后的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
现在在调用call
的时候需要传入三个参数(除去thisArg不算), 还不错, 不过接下来我们打算将更进一步, 我们希望在dog对象上添加一个方法justDoIt
, 不管这个justDoIt
接收什么参数, 都要将对justDoIt
函数的调用代理到toString
方法上去, 先试试看call
能不能帮我们解决这个问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
可以看到arguments
这个数组被赋值给了第一个参数delimiter
, 而heightUnit
, weightUnit
都没有被正确的赋值, 所以他们都是undefined
.
要解决上面的问题, 就需要apply
出场了, apply
与call
非常的相似, 唯一的区别就是call
需要明确地为每一个参数传值, 而apply
只需要一个参数数组就可以了, 当原函数被调用的时候, apply
会将这个参数数组unpack
并给对应位置上的参数赋值.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
如果你对函数式编程有所了解, 那么你应该知道柯里化(currying function)
与偏函数(partial function)
, 它们有个共同之处就是都是从一个函数上产生出的另一个新的函数.
偏函数可以视为是针对某种情况简化之后的函数, 比如说现在有一个函数awesomeFunction(arg1, arg2, arg3)
, 而对这个函数的调用过程中, 第一个参数几乎都是相同的, 作为眼睛里容不得重复的程序员, 我们应该有一种办法可以消除这种重复, 这个时候偏函数就伴着掌声入场了, 我们在偏函数的声明中已经预置了第一个参数arg1=mostFrequentlyUsedValue
, 这样我们调用偏参数的时候只需要传入(arg2, arg3
)就可以了.
那么现在问题来了, JavaScript中怎么实现偏函数呢?
这时候就需要使用bind
了, bind
的基本语法与call
一致:
fnc.bind(thisArg, arg1, arg2... argN)
但是它们有一个重要的区别, 那就是bind
不会立即执行原函数, 而是返回一个新的函数, 并且这个新的函数在被调用的时候会自动将已经bind
上去的那些参数值赋给对应位置的上的参数, 新的函数只需要关心余下的那部分参数列表.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
上面的例子中greet
这个函数多数情况下都是使用”Morning”作为第一个参数, 何不创建一个更简短的函数来专门处理这种场景呢:
1 2 3 4 5 6 7 8 9 10 11 |
|
了解了bind
其实是返回一个新的函数之后, 那么其实想通过它来实现上面call
/apply
的功能其实就非常简单了, 只需要调用一下生成的新的函数就可以了.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
上面只是从函数可以被任意对象调用的角度来分析了call
, apply
, bind
的用法, 其实实际使用的过程中, 这些方法还有另一个非常重要的用途, 那就是给回调函数绑定正确的上下文环境(this对象).
简单讲, View
就是用来展示Model
里面的数据的.
这样可以简单回顾一下我用过的一些前端JS框架是怎么处理数据展示的, 从最原始的jQuery开始:
只使用jQuery的时候, 当我们已经通过AJAX拿到数据之后, 我们可能需要先定位到需要显示数据的元素然后通过el.val('...')
或者el.text('...')
将数据手动设置上去. 当元素的值被更新之后需要将修改反应到后端时, 我们可能需要取到所有的元素的最新值然后发送到后台.
如上面那样通过操作对应的单个的DOM元素去设置值的方式写起来很繁琐, 如果返回的数据里面有十几甚至几十个条目的话, 写出来的代码更是毫无美感可言, 于是人们想到了使用JS模板
, 通过一段通常是写在<head></head>
中<script type="text/template" id="myTemplate">...</script>
来表示最终要后面的HTML的结构, 在其中使用占位符来标识哪里部分需要被真正传入的数据替换. 模板定义好之后, 当拿到AJAX的返回结果时, 我们只需要将数据填充到这个模板, 然后将生成的HTML添加到页面中就可以了, 一句话就可以搞定, 比如 $container.html(_.template($('#myTemplate'), ajaxReponseData))
. 比较常用的模板引擎有mustache
, underscore template
事情发展到这里还没完, 现在数据显示的问题解决的差不多了, 但是如果用户将某些DOM元素的值修改之后, 我们还需要将所有的元素的值读取一遍, 然后将这些值发送到后台去更新这条记录. 我们通过模板技术解决了繁琐的DOM设值, 但是数据的更新还是一个问题.
一起来看看Backbone的View
是如何解决这个问题.
首先我们需要定义一个Backbone View
, 定义的方法与Model
非常的类似, 只需要继承’View`就可以了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
与上面提到的方式不同, Backbone View
实现了细粒度的数据更新, 通过对对应的组件进行事件监听, 当某个组件的值被修改时, 只需要将被修改的属性更新到Model
中.
下面介绍一下View
中比较重要的几个属性(钩子):
initialize
通常在这个属性对应的方法里面去生成最初的视图, 如上例中所求, 在initialize
方法中将模板填充之后添加到View
对应的HTML容器中.
el
这个属性通常是一个css selector, 对应页面中这个View
将被添加到HTML元素, 可以理解成是这个View
的container, 个人理解是在创建Backbone Collection
, View
, Model
的实例的时候, 都可以向其中传入一个对象, 这个对象的属性会与定义时的那些属性进行merge. 所以el
既可以声明在View
类的定义中, 也可以通过参加传入, 如 var todoItemView = new TodoItemView({el: jQuery('.todoItem'});
在View
内部使用的时候, 通常是通过this.$el
来操作这个容器, this.$el
将会引用你配置的el
对应的jQuery对象.
除了el
之外, 还有一些其他的属性也是跟View
的容器相关的:
tagName
className
如果没有配置View
的el
属性, 那么默认将使用一个DIV
来包装View
的内容, 这时候如果配置了tagName
, 那么就会使用<tagName></tagName>
来包装, 如果还有className
的话, 那就变成了<tagName class="valueOfClassNameProp"></tagName>
this.$('css selector')
一开始我也比较疑惑this.$()
与$()
有什么区别, 看了源码之后才发现原来this.$()
是在当时View
的范围内查找元素, 是个挺实用的方法.
1 2 3 |
|
events
这个属性应该是View
所特有的, 它里面定义了一组元素上可能发生的事件以及对应的处理方法, 如:
1 2 3 4 5 6 7 |
|
如在.toggle
元素上点击时会执行handleToggle
方法. 这种事件监听方式其实与jQuery.on()
非常的类似:
jQuery.on(eventType, selector, handler)
"eventType selector": "handler"
在Backbone
的理念中, 一个Model
应该算是存在于客户端的与服器端resource
/entity
对应的一个JS对象, 定义一个Model
很简单:
1 2 3 4 5 6 7 8 9 |
|
创建一个Model
的实例就是创建一个对象 var todoItem = new TodoItem()
, 下面对定义在Model
中的常用属性做一下说明(这些属性其实是一些钩子, Backbone
希望我们去重写这些属性)
defaults
通过这个属性来指定Model
的默认值
当创建Model实例的时候没有传入对象来初始化, 那么这个实例的属性就与defaults
指定的属性值是一致的, 如:
1 2 3 |
|
当创建Model实例时传入了对象, 那么这个对象会跟defaults
进行merge
1 2 3 |
|
validate
将对Model
的验证逻辑放在这个属性对应的方法中, 一般情况下, 我们不会直接调用这个validate
方法, 它扮演的角色有点像实现
定义在父类中的抽象方法
, 这个方法会在Model
的其他方法中被用到, 比如以下的这些方法:
isValid()
我们可以调用Model
上的isValid()
方法来判断这个Model
上的属性值满足验证条件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
save()
or set()
默认情况下调用Model
的save()
方法会触发validate()
, 可以通过在save()
的时候使用option {validate: false}
来跳过验证. 类似的, 也可以在set()
时通过option {validate: true}
来触发验证.
validate()
方法的返回值比较有趣, 如果验证通过了, 则什么都不用返回, 如果验证失败了, 则需要返回点什么(字符中或是对象都可以).
validate()
方法验证失败之后有两件事件会发生:
validate()
返回的结果添加到Model
中, 可以通过model.validationError
来访问Model
上触发invalid
事件, 并像model和error绑定对回调函数上接收邮件服务器:imap.qq.com,使用SSL,端口号993
发送邮件服务器:smtp.qq.com,使用SSL,端口号465或587
账户名:您的QQ邮箱账户名(如果您是VIP帐号或Foxmail帐号,账户名需要填写完整的邮件地址)
密码:您的QQ邮箱密码
电子邮件地址:您的QQ邮箱的完整邮件地址
兴高采烈地把这些配置写入程序, 执行一下代码, 没想到竟然报错了:
1
|
|
一开始我以为需要在本机生成什么SSL证书之类的,不过上网搜了一圈发现,原来与程序本身无法,而是由于国内的这些邮件服务商(如网易, 腾讯)使用的SSL协议与python smtplib
不兼容(真是坑爹呀). 无奈,只好搭上梯子去连Gmail了.
但接着还是报错了 :(
不过这次报的错信息量多多了:
1
|
|
谷歌不愧是谷歌, 居然还在response里面给出了解决这个问题的网页,虽然是报错的,但是心里还是踏实多了,至少有响应了.
点开错误提示中的那个链接一看,里面列出了多个可能导致无法连接的原因, 一一分析了一下, 觉得这条嫌疑最大:
您的邮件应用可能不支持最新的安全标准。了解如何让安全性较低的应用访问 Gmail 帐户。
转到”允许不够安全的应用”网页, 开启了让不够安全的应用访问Gmail的开关, 再次运行程序, 谢谢谷歌,邮件终于可以发出去了.
]]>因为目前的这个项目需要每个迭代(两周)都发布一次, 并且有很多的story都是需要做A/B testing的 (简单讲,A/B testing就是对同一个功能有两种不同实现或是设计,发布之后通过用户反馈来判断某种设计更受欢迎),因此就有了这样的需求: 如果一个story在当前迭代中无法完成,那样需要给它加上toggle, 这样只需在生产环境将toggle关闭,不为担心未完成的功能被release出去;另一方面,如果发现新的实现、体验不被欢迎,那么只需将toggle关闭就可以快速地返回到旧的实现了。为了满足上面的需求,我们选择了togglz。
但是这种方式进行了一段时间后,问题开始出现了。功能测试中的有些测试方法是针对旧的体验书写的,另外还有一些测试方法是对同一功能的新的体验书写的,我们想同时保留针对新旧两种体验的测试方法,但是想这样做的话就需要手动来打开或是关闭对应的测试方法,比如说现在需要对AwesomeFeature
做A/B testing, 我们添加了一个toggle叫 NEW_DESIGN_FOR_AWESOME_FEATURE
, 对应的就有两种情况
NEW_DESIGN_FOR_AWESOME_FEATURE = true
, 对应的测试需要修改为1 2 3 4 5 6 |
|
NEW_DESIGN_FOR_AWESOME_FEATURE = false
, 对应的测试需要修改为1 2 3 4 5 6 |
|
我们需要根据toggle的配置来动态来决定哪些测试方法需要被执行!
于是求助google, 发现了有这么个东西 junit-ext, 它很可能就是我们想想找的东西!
1 2 3 4 5 6 7 8 9 10 11 12 |
|
我们只需要在@RunIf里面使用我们自己的Checker就可以很方便地控制测试方法的执行与否,于是脑海中出现了这样片段:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
感觉不错,似乎已经成功一大半了,但是。。。
当我尝试给现有的测试类加上@RunWith(JunitExtRunner.class)
时,才注意到他上面已经指定了一个Runner了(@RunWith(Theories.class)
), 再问google后得到一个令人绝望的消息,一个测试类不能指定多个Runner!!!
看来只能另辟蹊径了, 接着找可以控制测试方法执行的办法。
assumeThat似乎可以,它会在条件不满足的时候中止当前的测试方法并且ignore之, 而且这是junit自带的,不需要使用额外的runner。
应该通过类似下面的代码就可以如愿了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
现在形式一片大好,接下来要做的就是想办法读取到toggle的状态。当我们执行 NEW_DESIGN_FOR_AWESOME_FEATURE.isActive()
的时候,实际上是去一个FeatureManager那里查询指定的toggle的状态,togglz在web应用里有对应的特定实现,即有现成的FeatureManager可以使用。而我们的功能测试是独立于web应用的另外一个module, 因此我们需要提供自己的FeatureManager才可以: 第一步,在mvn中通过copy-resource将定义在webapp module中的toggle配置信息 togglz.properties文件复制到功能测试模块中;第二步,自定义一个FeatureManager来读取toggle信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
在我们自己实现的这个togglzConfig中我们主要做了两件事情,一是指定了从哪里读取toggle配置信息,还有就是指定了Feature Enum
.
1 2 3 4 5 |
|
在上面的这段代码里,我们使用FeatureManagerBuilder
构建了一个FeatureManager, 并且把它注册/绑定到ThreadLocalFeatureManagerProvider
, 这两行代码我们是放在了最外层的TestSuite类的BeforeClass
方法中,这样保证了在所有测试方法被执行之前toggle信息已经被初始化好了。
Done!
]]>Project Settings
Libraries
section+
button and choose Scala SDK
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Now comes the final one, we have a reference to the validation method in isolated scope, we want to verify this reference has been called, and with the right arguments.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
Not like verify data change in isolated scope, we don’t need to track method on isolated scope, track on method in default scope is enough. One thing interesting is in the implementation, we need to pass a json object as argument(scope.isValid({name: scope.newFruit})
), the json object is used internally in angular, in the test, what we need to verify is only values(expect(isValid.mostRecentCall.args[0]).toBe('apple');
).
Check out the implementation below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 7 8 9 |
|
This time, we want to validate the fruit name use typed in first, if it only contains letter, then it’s eligible to add in, otherwise nothing happen. The validation logic is defined in controller, we need to test the validation method is called inside our directive.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
It’s very similar with testing manipulate data on default scope, the only thing different is we spy on the validation method to verify it has been called, also we let the spy object return the corresponding result to execute each branch.
]]>1 2 3 4 5 6 7 8 9 |
|
The above directive manipulate data in default scope, now we create a isolated scope for the directive, the test point change to verify the data on isolated scope.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
In the test, we setup the surrounding scope, then verify both the default scope and isolated scope are updated.(we use element.scope()
to access the isolated scope).
1 2 3 4 5 6 7 8 9 |
|
Let’s try something advanced, we have a input box to type in a fruit name and a button, after click the button, the value of the input box will added into the fruit list.
How to write test for this directive? If the fruit list on the scope will have the fruit we passed in after we click the button, then we think it’s working well.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Not like the first test code, this one needs to verify the data on scope, we need a real scope, also we need to initialize the state of the scope, after we compile the html fragment using the scope, interact with the DOM will affect the scope.
]]>1 2 3 4 5 |
|
We’re going to write a directive for the button, after click it, the search box wil be clear.
To demonstrate DOM manipulation, we’ll not set ngModel
for the search box. Let’s write out the test first.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
To be honest, directive which only manipulate DOM doesn’t need scope, so we can remove scope in test, the test still can pass.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
hg purge
hg revert [FILE]
hg rollback
hg incoming
hg outgoing
reset --hard
in git)hg strip -r commit_hash
hg update [BRANCH_NAME]
hg branch
hg merge [ANOTHER_BRANCH_NAME]
hg log --only-branch my_branch
Let’s check out the below examples form easy to hard.
]]>After writing a lot of directives, I’ve decided to use less isolated scope. Even though it is cool and you encapsulate the data and be sure not to leak data to the parent scope, it severely limits the amount of directives you can use together. So,
If the directive you’re going to write is going to behave entirely on its own and you are not going to share it with other directives, go for isolated scope. (like a component you can just plug it in, with not much customization for the end developer) (it gets very trickier when you try to write sub-elements which have directives within)
If the directive you’re going to write is going to just make dom manipulations which has needs no internal state of scope, or explicit scope alterations (mostly very simple things); go for no new scope. (such as ngShow,ngMouseHover, ngClick, ngRepeat)
If the directive you’re going to write needs to change some elements in parent scope, but also needs to handle some internal state, go for new child scope. (such as ngController)
So keep in mind that don’t use isolated scope unless you have to.
]]>I was blocked for whole afternoon by a very weird phenomenon, two radio buttons(with the same name), one of them can never be checked after you click it, at the last, I found out the root cause is the browser’s default behavior is prevented by the return false
statement in directive.
Let’s see what a normal radio button group should be:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
check the demo, click a radio button can make it checked.
Now let’s make some trouble, if we click a text input field, we want to show a console log say ‘input field clicked’, if other type input component is clicked, do nothing. let’s write a directive to handle this.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
Check the demo, you’ll see the radion button group is not functional well, one of them can never be checked, this is all because we use return false
in directive.
After we replace return false
with return
, everything back to normal, check again here
If your own directive has a isolated scope, then it will impact native angular directive, which means, sometime, ngModel, ngDisabled suddenly doesn’t work, because they’re impacted by your directive. take below as an example:
We have a input field to type in a programming language, click the ‘Add’ button will add it into a list(as it’s a simple demo, so data validation is not concerned)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
we put a directive addLanuage
on the button, which will get the value in the input field and add it to language list, due to we need to operate the language list, so we use a isolated scope to access it inside the directive.
Try it yourself. demo
Now the new requirement comes, a user only allow to fill up to five programming languages, we need to disable the Add
button after user have input 5 languages.
Seems a small change will fit the new requirement, ngDisabled should solve this.
1 2 3 4 5 6 7 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Play with the updated code you’ll find out ngDisabled
is not working!
demo
What can we do to save the ngDisabled
damaged by isolated scope.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
Now ngDisabled
works, demo
As you can see from the code, we declare a new attribute needs-disabled
which use reachThreshold()
as it’s value, then we set needsDisabled
to ng-disabled
, the last thing is to declare the new attribute in directive’s scope, in this way, ngDisabled
back again.
If you put more than one directive on a element, and each of them has a isolated scope, angular will fail and complain multiple isolated scope on one element.
]]>So I decide to write a series of articles to record what I’ve learned in the past months, these articles are not for beginners, I assume you have a basic concept with AngularJS, but still don’t know how to write AngularJS in the right way. Let’s start with directives .
A directive usually appears as a element/tag name or attribute, it’s used to add additional functionality to the element, like, <a toggle-background/>
, the directive toggleBackground will switch the
background color of the current page once you click it, it’s more clear than implement the same logic with jQuery, you will understand what will happen when you see the directive on the link.
Let’s take a closer look to directives, below is a simple directive defination.
1 2 3 4 5 6 7 8 |
|
we can ignore the restrict
property, by default a directive can appear as a attribute, let’s goes into the most important part in a directive: the link function .
The link function take three parameters: scope
, elm
, attr
, the last two are easy to understand:
elm
the jQuery object representation of the element which this directve blongs toTake below code as an example, then most common use of directive is to do something against the element it decorated, in below example, we add autocomplete functionality for a input field.
1 2 3 4 5 6 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
See demo here
attr
all the attributes on this element, it’s a map of attribute name and value, given <a type="text" some-directive/>
, attr.type
will return text
attr
are used to pass additional information to directive, it has the same functionallity with jQuery(element).attr('attrName')
, but more convenient.In below exmaple, we config the autocomplete dropdown is triggered at least user input 3 characters.
1 2 3 4 5 6 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
see demo here
Why we need scope for directive ? First, let’s devide all kinds of directves into two groups:
without scope
If what we do in this the directive is only DOM manipulation, then elm
and attr
is enough for use, we don’t need to declare a isolated scope.
with scope
If we need to manipulate angular model in directive, then we need to declare a scope.
directive which don’t need to use scope is easy to understand, usually we bind event listener on the link function, but directive which need scope requires more practice to master it, we also can
devide this kind of directives into two groups: manipulate model in controller
and call method in controller
access model in controller need to establish a isolated scope, there are two ways:
@[attributeName]
return the value of that attributeName, the value is a plain string, it’s the same as using attr.attributeName
=[attributeName]
two way binding, first get the value of that attributeName, then evaluate the value in controller scope, changing the value in directive will reflect in controller scope1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
check the demo here.
In the above code, we establish a isolateds scope to setup a bridge with languages
and newLanguage
in controller’s scope, then we can manipulate them.
&[attributeName]
return the value of that attributeName, the value is a function reference which points to the a method whose name same as the value in controller.we are two type of invocation of controller method: without parameters
and with parameters
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
see demo here.
we want to call a methhod in controller which send some signal to server from our directive, we declare an attribute send-signal-to-server
whose value is the method name is controller, as a result, scope.sendSignalToServer
hold a reference to method sendSignalToServer()
.
Continue with the above example, we change the controller method sendSignalToServer()
to accept two parameters, pass parameters from directive to controller is a lillte tricky.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
see demo
as you can see, we need to define placeholder for the argument list in the attribute, and in the directive, when we are going to call that method, we need to construct a object which use the place holder as keys, and your real parameters as values.
If you’re patient enough to read to here, i belieave you’ve got a basic concept how to write directive in the right way. But keep in mind, don’t use too many directives on one element, it’s difficult to understand which directive is responsible for which functionality, thus increase the effort to maintain the code, also your directive could impact the native angular directive, so first try to use angular native directives(e.g. ng-click, ng-init), if it can not fit your requirements, write you own.
]]>