Feng erdong's Blog

Life is beautiful

在JS string.replace() 中使用捕获组

| Comments

上周在写代码的时候要在componentWillUpdate方法中判断一下当前state/props有没有改变, 这个判断其实跟PureRenderMixin干的是完全一样的事情, 但是PureRenderMixin是在shouldComponentUpdate这个life cycle method里执行的, 所以并不能直接拿来用. 跑去看了一下PureRenderMixin的实现, 发现它对state/props的比较是依赖于一个shallowEqual方法, 而这个方法在fbjs里面, fbjs是Facebook维护的一个实用类库的集合, 所以今天决定读一读这个项目, 希望能从中学到一些新的东西.

那先来分享一下学到的第一个新东西吧.

String.replace()

之前对这个函数的认识比较简单, 只知道可能将指定的字符串或pattern替换成另外一个字符串, 如:

1
2
console.log('AlphaGo vs Lee Sedol'.replace('Go', 'Google'))
// "AlphaGoogle vs Lee Sedol"

或者是:

1
2
3
4
console.log('188-0000-0000'.replace(/-/, '+'))
// "188+0000+0000"
console.log('188-0000-0000'.replace(/-/g, '+'))
// "188+0000+0000"

需要注意的是, 正则表达式没有使用g选项的时候, replace只会替换第一处匹配.

今天学到的高级用法是replace函数还可以用一个函数来返回替换量, 而且这个函数的参数是整个匹配还有这个匹配中的所有捕获组, 比如fbjs中的camelize函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const _hyphenPattern = /-(.)/g;

/**
 * Camelcases a hyphenated string, for example:
 *
 *   > camelize('background-color')
 *   < "backgroundColor"
 *
 * @param {string} string
 * @return {string}
 */
function camelize(string) {
  return string.replace(_hyphenPattern, function(_, character) {
    return character.toUpperCase();
  });
}

这里 _hyphenPattern 用来匹配连字符写法, 并将连字符写法替换成驼峰写法, 我们无法通过简单将-替换成空白字符来实现, 因为我们还需要将-后面的字母大写, 这个时候可以接受捕获组的函数上可以出场了. 在 _hyphenPattern 的定义里面, -后面的那个字母是被定义成一个捕获组, 它会作为第二个参数传入replacement function(第一个参数是匹配的字符串), replacement function将这个字母大写之后返回, 就实现了将- + 任意字母 替换成了一个大写之后的字母的.

JSX里的那些个坑

| Comments

最近在项目上使用Backbone + React作为前端框架, Backbone主要是用来作为Model来与后台的Restful API进行交互, React则负责View, 也算是发挥了各自的优势: Backbone的Model和Collection只需要指定对应Resource的url, 就可以直接读取/更新/删除记录, 而不需要手动去发送对应的AJAX请求, 另外借助于寄生在Model/Collection上的underscore方法, 操作数据也很方便; React接收Backbone的Model/Collection作为stateprops, 通过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的属性还是有一些限制(坑)的:

避免出现Javascript关键字

因为JSX会被编译成JS代码供浏览器执行, 所以如果在JSX中使用了Javascript的关键字的话, 那么很可能导致生成的代码无法被正确解释.(在IE上这个问题格外严重, 但在强大的Chrome上, 这种情况可以被很智能地处理).

一看下面的例子就明白了:

这段JSX代码生成一个表单, 其中有一个Label和对应的Input用来输入用户的名字,

1
2
3
4
<form>
   <label for="name">Your name:</label>
   <input type="text"/>
</form>

根据它编译生成的Javascript代码长这个样子:

1
2
3
4
5
6
7
8
React.createElement("form", null,
    React.createElement("label", {
        for: "name"
    }, "Your name:"),
    React.createElement("input", {
        type: "text"
    })
);

通过比较我们基本上可以总结出这个翻译的过程, 对于一个JSX Tag:

1
2
3
<tagName attr1=val1 attr2=val2>
  [childrenComponents]
</tagName>

它生成的Javascript应该是:

1
React.createElement("tagName", {attr1: val1, attr2: val2}, [childrenReactComponents]);

可以看出其中TagName是最安全的, 因为它在生成的JS中是一个字符串, 但是HTML Attribute就没有这么幸运了, 如果它碰巧是一个Javascript关键字, 那么恭喜你, 你掉到坑里去了. 比如上例中label上面的for属性在被编译后的Javascript代码中就很可能被浏览器当作是循环的那个for, 从而报出一个语法错误出来.

为了解决类似这种问题, React为这些”不幸”的属性定义了对应的”替身”, 它们中的一些是(不完全版):

Native HTML Attribute React Version
for htmlFor
class className

另类的inlineStyle

inline style估计是最让React的初学者头痛的问题了, 如果我们按照HTML规范来使用的话, 是无法将想要的样式apply到元素上的, 至于为什么, 我们看一下由JSX编译成的JS代码就明白了:

1
<div style="color: yellow">Hello World</div>

我们在JSX中定义了一个DIV元素, 并且想通过inline style将它里面的文字颜色设为yellow, 然而这个DIV甚至都无法被渲染到页面中, 因为React认为它是不合法的:

1
React.createElement("div", {style: "color: yellow"}, "Hello World");

在上面这段生成的JS里面, 用于设置style的值是一个字符串, 这显然是无法工作的, 因为style这个属性与其他属性不一样, 它比较特殊, 一般的属性的值都是一个普通的字符串, 比如type="text", name="fieldName", 这些属性值都是可以被浏览器直接使用的, 而style它传递的是一组样式属性, 它在这里已经不是一般的字符串, 而是可以被解析的一个样式对象, 说到这里, 估计你也猜到了, 我们不能直接使用一个普通字符串给style, 我们要传递的应该是一个JS对象(JSON)才对:

1
<div style="{color: yellow}">Hello World</div>

但是将代码改成这样之后还是无法将这个DIV渲染出来, 再次查看生成的JS代码:

1
React.createElement("div", {style: "{color: yellow}"}, "Hello World");

style的值还是一个字符串! 这里正好引出了JSX的一条很重要的规则, 即被放在{}中的都作为Javascript代码来处理, 但是如果{}被引号包围起来, 那么它当作一个字符串处理. 这可能是JSX让人感觉比较困惑的地方, 当在JSX中使用{}来引用JS变量的时候, 很容易让人感觉JSX似乎是模板引擎, 而对于模板引擎来说(比如Freemarker, JSP等), 它们会将所模板中所有的变量都替换成对应的值, 不管它们是不是在一对双引号中:

如在Freemarker中, 如果变量 userName的值是 ‘Erdong FENG’, 使用的模板是

1
<div data-name="${userName}">Welcome to our website</div>

那么最终生成的文件是这样的:

1
<div data-name="Erdong FENG">Welcome to our website</div>

然而在JSX中并不是这样的, 因为JSX不是模板引擎 (当然了, 大多数时候JSX表现的还是像一个模板引擎, 比如在一个DIV的文字结点中, 但是在一个Tag的属性值中使用变量的时候, 情况完全不一样)

1
<div data-name="{userName}">Welcome to our website</div>

可以看到, 当你将变量名作为属性值的时候, 而且给这个属性值加上了引号的话, 它会被当作一个字符串被处理:

1
React.createElement("div", {"data-name": "{userName}"}, "Welcome to our website");
1
<div data-name="{userName}">Welcome to our website</div>

当然了, 我们在innerHTML的位置还是可以将JSX当作模板引擎来使用的:

1
<div>Welcome to our website, "{userName}"</div>

最终的JS跟HTML如下:

1
React.createElement("div", null, "Welcome to our website, \"", userName, "\"");
1
2
3
4
5
<div>
    <span>Welcome to our website, "</span>
    <span>Erdong FENG</span>
    <span>"</span>
</div>

一下子跑得有点偏了, 再回顾一下, 在JSX中, {}中的东西是当作JS来处理的, 因为在生成的HTML中这部分会被对应的JS表达式的值替换, 但是如果是在属性值中使用{}, 那么就一定不能用引号将它括起来了, 不然的话它就会当作字符串来处理!

再回到inline style的问题上, 基于上面的分析, 我们这次不给属性值加引号了

1
<div style={color: yellow}>Welcome to our website</div>;

再编译一次, 这次直接就语法错误了, 因为{}里面的部分是被当作Javscript来处理的, 而color: yellow根本不是合法的JS语句, 再回顾一下, 我们这里需要的属性值是一个JSON, 那么应该是{color: yellow}才对, 再将这个JSON放到{}中去:

1
<div style={ {color: yellow} }>Welcome to our website</div>
1
React.createElement("div", {style: {color: yellow}}, "Welcome to our website");

怎么还是有问题?! 原来是因为yellow被当作一个JS变量来解析, 但是却没有对应的变量存在, 所以这时候又引出了在JSX中使用inline style的另一条规则, 对于这些非字面量的值, 需要将他们用引号括起来, 表示他们是字符串:

1
<div style={ {color: 'yellow'} }>Welcome to our website</div>

这样就可以了!

除此之外, 还有另一个需要注意的地方, 如果style的属性名中含有连字符-, 也需要做适当转换的, 因为属性名在编译后的JS代码中是JSON中的key, key要适合JS的变量名的定义, 因此是不能使用连字符-的, React的解决办法是使用它的camelPattern, 好的, 讲完了.

1
<div style={ {color: 'yellow', backgroundColor: 'green'} }>Welcome to our website</div>

为什么你的Angular代码很难测试

| Comments

Angular推出有好几年的时候了, 跟其他的MV*框架相比, 它的双向绑定, 无须显式声明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操作

像DOM操作这样的脏活累活都应该交给Angular的原生directive去做, 我们的Angular代码应该只处理与DOM无关的业务逻辑. 来看一个简单的例子, 我们想创建一个简单的邮箱地址验证的directive, 它要实现的功能是, 当焦点从邮箱地址输入框移出的时候, 对输入框中的邮箱地址进行验证, 如果验证失败, 则向输入框添加一个样式表示输入的地址不合法, 比较糟糕的实现可能是这样的

1
2
<label for="email">Your email:</label>
<input id="email" type="text" ng-model="email" validate-on-blur/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
angular.module("TheNG", [])
        .directive("validateOnBlur", function () {
            return {
                link: function (scope, element, attrs) {
                    var validate = function (email) {
                        return email.indexOf('@gmail.com') !== -1;
                    };

                    element.on('blur', function () {
                        if (!validate(scope.email)) {
                            element.addClass('error-box');
                        }
                    });
                }
            };
        }
);

上面的代码应该可以满足我们的要求(验证逻辑因为不是我们关注的重点, 所以并不完善), 而且这个directive实现起来也挺简单的, 但是现在让我们一起来分析一下为什么我们认为这种写法是比较糟糕的.

最简单的办法就是在你的directive里面去找所有与DOM操作相关的代码.

首先看到的就是on()这个事件监听器. 完全没有必要自己去监听发生在被directive修饰的元素上的事件, angular有一整套的原生directive来干这个事情, 这里正确的做法应该是使用ng-blur来处理blur事件.

下一个有问题的地方就是addClass(), angular除了提供了事件监听相关的directive外, 也提供了操作元素本身属性的directive, ng-class就可以用来替换addClass()方法.

按照这个思路修改后的代码:

1
2
<label for="email">Your email:</label>
<input id="email" type="text" ng-model="email" ng-blur="validate()" ng-class="{'error-box': !isValid}" validate-on-blur/>
1
2
3
4
5
6
7
8
9
10
11
12
angular.module("TheNG", [])
    .directive("validateOnBlur", function () {
        return {
            link: function (scope, element, attrs) {
                scope.isValid = true;
                scope.validate = function () {
                    scope.isValid = scope.email.indexOf('@gmail.com') !== -1;
                };
            }
        };
    }
);

比较一下这两个版本的实现, 是不是修改后的版本更简短, 更容易理解一些. 在新的版本里面, 我们只处理了业务逻辑, 即判断一个邮箱地址是否合法, 至于何时触发验证, 验证失败或成功之后应该有怎样的样式, 我们都统统交给了angular原生directive去处理了.

从测试的角度来看, 如果想给第一个版本的实现写单元测试, 那么要准备和验证的东西都很多, 我们需要设法去触发对应元素的blur事件, 然后再验证这个元素上是否添加了error-box这个class, 根据我的经验, 有时候为了验证这些DOM更新, 你还不得不创建真实的DOM结构添加到DOM tree上去, 又增加了一部分工作量.

而版本二就简单多了, 只定义了一个Model值isValid来标识当前的邮箱地址是否合法, validate()方法会在每次失焦之后自动执行, 要为它添加单元测试, 则只需要调用一下它的validate()方法, 然后验证isValid的值就可以了. SO EASY!~

  • 将所有第三方服务封装成Service

一个Web项目中总是无法避免地要使用一些第三方的服务, 这里讨论的主要是前端的一些第三方服务, 比如在线客服, 站点统计等, 这些代码都在我们的控制之外, 大多数时候下都是从服务提供商的服务器上下载下来的, 而我们需要在业务代码中调用这些代码.

如果我们每次都是赤裸裸地以全局变量的形式来使用这些服务, 那么造成的问题就是这样的代码很难测试, 因为这些代码是不存在于我们的代码库中的, 而且内容应该也是不定时更新的, 大多数情况很多人会因为这些原因放弃到对这类操作的测试.

假设我们现在需要在某些动作发生之后调用一个第三方服务, 这个第三方服务叫做serviceLoadedFromExternal, 它提供了一个API叫做makeServiceCall, 如果直接使用这个API, 那么在测试中很难去验证这个服务被执行了(因为在单元测试环境中这个服务根本不存在), 但是如果我们将这个服务包装成一个angular service, 那么就可以在测试中轻易地将它替换成一个mock对象, 然后验证这个mock对象上的方法被调用了就可以了.

比较下面的两段代码:

  • 直接使用第三方服务
1
2
3
4
5
6
7
angular.module("TheNG", [])
    .controller("AController", function ($scope) {
        $scope.someAction = function () {
            // handle some logic here
            serviceLoadedFromExternal.makeServiceCall();
        };
    });
  • 使用封装成service的第三方服务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
angular.module("TheNG", [])
    .factory("wrappedService", function () {
        return {
            makeServiceCall: function () {
                serviceLoadedFromExternal.makeServiceCall();
            }
        };
    })
    .controller("AController", function ($scope, wrappedService) {
        $scope.someAction = function () {
            // handle some logic here
            wrappedService.makeServiceCall();
        };
    });

Angular是高度模块化的, 它希望通过这种模块的形式来解决JS代码管理上的混乱, 并且使用依赖注入来自动装配, 这一点与Spring IOC很像, 带来的好处就是你的依赖是可以随意替换的, 这就极大的增加了代码的可测试性.

  • 尽量将Ajax请求放到service中去做

Angular中使用service来组织那些可被复用的逻辑, 除此之外, 我们也可以将service理解为是对应一个领域对象的操作的集合, 因此, 通常会将一组Ajax操作放在一个service中去统一管理.

当然了,你也可以通过向你的directive或是controller中注入$http, 但是我个人不喜欢这种做法. 首先, $http是一个比较初级的依赖, 与其实注入的业务服务不是一个抽象层级, 如果在你的业务代码中直接操作http请求, 给人的一种感觉就像是在Spring MVC的request method中直接使用HttpServeletRequest一样, 有点突兀, 另外会让整个方法失衡, 因为这些操作的抽象层次是不一样的. 其次就是给测试带来的麻烦, 我们不得不使用$httpBackend来模拟一个HTTP请求的发送.

我们应该设法让测试更简单,通过将Ajax请求封装到service中, 我们只需要让被mock的service返回我们期望的结果就可以了. 只有这样大家才会喜欢写测试, 甚至是做到测试驱动开发, 要去mock$http这样的东西, 显然是增加了测试的负担.

  • 使用Promise处理Ajax的返回值, 而不是传递回调函数

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
angular.module("TheNG", [])
    .service("deliveryService", function ($http) {
        this.validateAddress = function (address, success, failure) {
            $http.post('/unknown', address).then(success, failure);
        };
    })
    .controller("DeliveryController", function ($scope, deliveryService) {
        var acceptAddress = function () {
            console.log('address accepted');
        };
        var rejectAddress = function () {
            console.log('address rejected');
        };
        $scope.validateAddress = function () {
            deliveryService.validateAddress($scope.address, acceptAddress, rejectAddress);
        };
    });

这里的处理办法是将快递地址验证失败或成功之后的处理函数都传给了deliveryService, 当验证结果从服务器端返回之后, 相应的处理函数会被执行. 这做写法其实是比较常见的, 但是问题出在哪里呢?

其实, 作为一个service的接口, validateAddress应该只接收一个待验证的地址, 验证完成之后返回一个验证结果就可以了, 本来应该是一个很干净的接口, 我们之所以丑陋把对应的处理函数也传进去, 原因就在于这是一个异步的请求, 所以需要在发请求的时候就将对处理函数绑定上去.

你应该已经猜到了第二个问题我会说一说对它的测试, 通常来说, 如果一个service创建成本较高或是存在外部依赖/请求的话, 我们会将这个service mock掉, 通过让mocked service直接返回我们想要的结果来让我们只关注被验证的业务逻辑.我们回头看一下上面的实现, 如果我们把deliveryServicevalidateAddress()方法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.module("TheNG", [])
    .service("deliveryService", function ($http) {
        this.validateAddress = function (address) {
            return $http.post('/unknown', address);
        };
    })
    .controller("DeliveryController", function ($scope, deliveryService) {
        var acceptAddress = function () {
            console.log('address accepted');
        };
        var rejectAddress = function () {
            console.log('address rejected');
        };
        $scope.validateAddress = function () {
            deliveryService.validateAddress($scope.address).then(acceptAddress, rejectAddress);
        };
    });

本来打算接下来介绍一下Angular代码的单元测试的各种模式的, 写着写着篇幅有点多了, 期待下一篇吧.

Call vs Apply vs Bind

| Comments

在JavaScript中, 函数是一等公民, 可以作为参数传入另一个函数, 也可以作为返回值从另一个函数中返回, 不仅如此, 在JavaScript这门动态语言中, 函数可以被任意对象调用, 比如说function1是定义在obj1上的, 但是它还可以被obj2所调用, 怎么做到的? 有下面的三种办法:

  • Call

基本语法: 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
var person = {
  toString: function(delimiter) {
    return "height=" + this.height + delimiter + " weight=" + this.weight;
  }
};

person.height = "180";
person.weight = "80";

console.log(person.toString(","));
//"height=180, weight=80"

var dog = {};
dog.height = "40";
dog.weight = "30";

console.log(person.toString.call(dog, ":"));
//"height=40: weight=30"
  • Apply

上面的例子里toString只是返回了身高与体重的数值, 但是只有数值是没有意义的, 我们还需要单位, 出于演示的目的我们打算将单位传为参数传入toString方法, 修改后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = {
  toString: function(delimiter, heightUnit, weightUnit) {
    return "height=" + this.height + heightUnit + delimiter + " weight=" + this.weight + weightUnit;
  }
};

person.height = "180";
person.weight = "80";

console.log(person.toString(",", "cm", "kg"));
//"height=180cm, weight=80kg"

var dog = {};
dog.height = "40";
dog.weight = "30";

console.log(person.toString.call(dog, ":", "cm", "kg"));
//"height=40cm: weight=30kg"

现在在调用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
var person = {
  toString: function(delimiter, heightUnit, weightUnit) {
    return "height=" + this.height + heightUnit + delimiter + " weight=" + this.weight + weightUnit;
  }
};

person.height = "180";
person.weight = "80";

console.log(person.toString(",", "cm", "kg"));
//"height=180cm, weight=80kg"

var dog = {
  justDoIt: function() {
    person.toString.call(this, arguments);
  }
};
dog.height = "40";
dog.weight = "30";

console.log(dog.justDoIt(":", "cm", "kg"));
//"height=40undefined[object Arguments] weight=30undefined"

可以看到arguments这个数组被赋值给了第一个参数delimiter, 而heightUnit, weightUnit都没有被正确的赋值, 所以他们都是undefined.

要解决上面的问题, 就需要apply出场了, applycall非常的相似, 唯一的区别就是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
var person = {
  toString: function(delimiter, heightUnit, weightUnit) {
    return "height=" + this.height + heightUnit + delimiter + " weight=" + this.weight + weightUnit;
  }
};

person.height = "180";
person.weight = "80";

console.log(person.toString(",", "cm", "kg"));
//"height=180cm, weight=80kg"

var dog = {
  justDoIt: function() {
    return person.toString.apply(this, arguments);
  }
};
dog.height = "40";
dog.weight = "30";

console.log(dog.justDoIt(":", "cm", "kg"));
//"height=40cm: weight=30kg"
  • Bind

如果你对函数式编程有所了解, 那么你应该知道柯里化(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
function greet(timeFrame, msg) {
  return "Good " + timeFrame + ", " + msg;
}

console.log(greet("Morning", "What can I do for you?"));
console.log(greet("Morning", "This is your ticket"));
console.log(greet("Morning", "welcome back"));
console.log(greet("Morning", "How was the trip"));
console.log(greet("Afternoon", "Can I borrow you some time?"));
// "Good Morning, What can I do for you?"
// "Good Morning, This is your ticket"
// "Good Morning, welcome back"
// "Good Morning, How was the trip"
// "Good Afternoon, Can I borrow you some time?"

上面的例子中greet这个函数多数情况下都是使用”Morning”作为第一个参数, 何不创建一个更简短的函数来专门处理这种场景呢:

1
2
3
4
5
6
7
8
9
10
11
function greet(timeFrame, msg) {
  return "Good " + timeFrame + ", " + msg;
}

var morningGreet = greet.bind(undefined, "Morning");

console.log(morningGreet("What can I do for you?"));
console.log(morningGreet("This is your ticket"));
console.log(morningGreet("welcome back"));
console.log(morningGreet("How was the trip"));
console.log(greet("Afternoon", "Can I borrow you some time?"));

了解了bind其实是返回一个新的函数之后, 那么其实想通过它来实现上面call/apply的功能其实就非常简单了, 只需要调用一下生成的新的函数就可以了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var person = {
  toString: function(delimiter) {
    return "height=" + this.height + delimiter + " weight=" + this.weight;
  }
};

person.height = "180";
person.weight = "80";

console.log(person.toString(","));
//"height=180, weight=80"

var dog = {};
dog.height = "40";
dog.weight = "30";

console.log(person.toString.bind(dog, ":")());
//"height=40: weight=30"

上面只是从函数可以被任意对象调用的角度来分析了call, apply, bind的用法, 其实实际使用的过程中, 这些方法还有另一个非常重要的用途, 那就是给回调函数绑定正确的上下文环境(this对象).

Backbone体验 – View篇

| Comments

View

简单讲, 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
TodoItemView = Backbone.View.extend({
    template: _.template($("#todoItemTemplate").html()),
    initialize: function () {
        this.render();
    },
    render: function () {
        return this.$el.html(this.template(this.model.attributes));
    },
    events: {
        "click .toggle": "handleToggle",
        "click .destroy": "handleDestroy"
    },
    handleToggle: function () {
        this.model.set('completed', this.$('.toggle').prop('checked'));
    }
}

与上面提到的方式不同, 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

如果没有配置Viewel属性, 那么默认将使用一个DIV来包装View的内容, 这时候如果配置了tagName, 那么就会使用<tagName></tagName>来包装, 如果还有className的话, 那就变成了<tagName class="valueOfClassNameProp"></tagName>

  • this.$('css selector')

一开始我也比较疑惑this.$()$()有什么区别, 看了源码之后才发现原来this.$()是在当时View的范围内查找元素, 是个挺实用的方法.

1
2
3
$: function(selector) {
  return this.$el.find(selector);
},
  • events

这个属性应该是View所特有的, 它里面定义了一组元素上可能发生的事件以及对应的处理方法, 如:

1
2
3
4
5
6
7
events: {
    "click .toggle": "handleToggle",
    "click .destroy": "handleDestroy"
},
handleToggle: function () {
    this.model.set('completed', this.$('.toggle').prop('checked'));
}

如在.toggle元素上点击时会执行handleToggle方法. 这种事件监听方式其实与jQuery.on()非常的类似:

  • jQuery.on(eventType, selector, handler)
  • "eventType selector": "handler"

Backbone体验 – Model篇

| Comments

Model

Backbone的理念中, 一个Model应该算是存在于客户端的与服器端resource/entity对应的一个JS对象, 定义一个Model很简单:

1
2
3
4
5
6
7
8
9
TodoItem = Backbone.Model.extend({
    defaults: {
        title: '',
        completed: false
    },
    validate: function (attributes) {
      // validation goes here
    }
})

创建一个Model的实例就是创建一个对象 var todoItem = new TodoItem(), 下面对定义在Model中的常用属性做一下说明(这些属性其实是一些钩子, Backbone希望我们去重写这些属性)

  • defaults

    通过这个属性来指定Model的默认值

    当创建Model实例的时候没有传入对象来初始化, 那么这个实例的属性就与defaults指定的属性值是一致的, 如:

1
2
3
var todoItem = new TodoItem();
console.log(JSON.stringify(todoItem.toJSON()));
// {"title":"","completed":false}

当创建Model实例时传入了对象, 那么这个对象会跟defaults进行merge

1
2
3
var todoItem = new TodoItem({"newAttr": "valueOfNewAttr", "title": "Build a time machine"});
console.log(JSON.stringify(todoItem.toJSON()));
// {"newAttr":"valueOfNewAttr","title":"Build a time machine","completed":false}
  • validate

    将对Model的验证逻辑放在这个属性对应的方法中, 一般情况下, 我们不会直接调用这个validate方法, 它扮演的角色有点像实现定义在父类中的抽象方法, 这个方法会在Model的其他方法中被用到, 比如以下的这些方法:

    isValid()

    我们可以调用Model上的isValid()方法来判断这个Model上的属性值满足验证条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
TodoItem = Backbone.Model.extend({
defaults: {
    title: '',
    completed: false
},
validate: function (attributes, options) {
    if (attributes.title.indexOf("<") != -1) {
        return "html tag is now allowed is title"
    }
}
});
var todoItem = new TodoItem({
    "title": "<script>...</script>"
});
console.log(todoItem.isValid());
// false

save() or set()

默认情况下调用Modelsave()方法会触发validate(), 可以通过在save()的时候使用option {validate: false}来跳过验证. 类似的, 也可以在set()时通过option {validate: true}来触发验证.

validate()方法的返回值比较有趣, 如果验证通过了, 则什么都不用返回, 如果验证失败了, 则需要返回点什么(字符中或是对象都可以).

validate()方法验证失败之后有两件事件会发生:

  • validate()返回的结果添加到Model中, 可以通过model.validationError来访问
  • Model上触发invalid事件, 并像model和error绑定对回调函数上

如何解决无法使用Python发送邮件的问题

| Comments

这两天在玩Flask Web开发,其中讲到使用Flask Mail来发送邮件, 例子里面用的是Gmail的SMTP (Simple Mail Transter Protocal) 服务器, 我想国内的话还是用QQ邮箱方便一些吧,于是打开QQ邮箱的设置面板,找到如下的配置信息:

接收邮件服务器:imap.qq.com,使用SSL,端口号993
发送邮件服务器:smtp.qq.com,使用SSL,端口号465或587
账户名:您的QQ邮箱账户名(如果您是VIP帐号或Foxmail帐号,账户名需要填写完整的邮件地址)
密码:您的QQ邮箱密码
电子邮件地址:您的QQ邮箱的完整邮件地址

兴高采烈地把这些配置写入程序, 执行一下代码, 没想到竟然报错了:

1
ssl.SSLError: [Errno 1] _ssl.c:507: error:140770FC:SSL routines:SSL23_GET_SERVER_HELLO:unknown protocol

一开始我以为需要在本机生成什么SSL证书之类的,不过上网搜了一圈发现,原来与程序本身无法,而是由于国内的这些邮件服务商(如网易, 腾讯)使用的SSL协议与python smtplib不兼容(真是坑爹呀). 无奈,只好搭上梯子去连Gmail了.

但接着还是报错了 :(

不过这次报的错信息量多多了:

1
smtplib.SMTPAuthenticationError: (534, '5.7.14 <https://accounts.google.com/ContinueSignIn?sarp=1&scc=1&plt=AKgnsbu_a\n5.7.14 jp4-nDDfH6fUTS8dQUeQfvPLhoMiYxCCT4CThSQuhcOJbzMUYI_QQZ44jUTB6FbPnYp8gv\n5.7.14 DEXJItt2gSz6WypV6cr7cZv9rpEprruo_JHXIBw6ZK3wjDKeSKKUKYMtpKoJUmARwONwcJ\n5.7.14 D3v5xypcAJtcazDB_WIUNCP8b3ZQl94GOTIKRvr7ASIgNuyD-rud8doBOTRnKpnLHbuc9B\n5.7.14 lZEGyzw> Please log in via your web browser and then try again.\n5.7.14 Learn more at\n5.7.14 https://support.google.com/mail/bin/answer.py?answer=78754 hv7sm4775459pdb.86 - gsmtp')

谷歌不愧是谷歌, 居然还在response里面给出了解决这个问题的网页,虽然是报错的,但是心里还是踏实多了,至少有响应了.

点开错误提示中的那个链接一看,里面列出了多个可能导致无法连接的原因, 一一分析了一下, 觉得这条嫌疑最大:

您的邮件应用可能不支持最新的安全标准。了解如何让安全性较低的应用访问 Gmail 帐户。

转到”允许不够安全的应用”网页, 开启了让不够安全的应用访问Gmail的开关, 再次运行程序, 谢谢谷歌,邮件终于可以发出去了.

在功能测试中使用Togglz

| Comments

Togglz

因为目前的这个项目需要每个迭代(两周)都发布一次, 并且有很多的story都是需要做A/B testing的 (简单讲,A/B testing就是对同一个功能有两种不同实现或是设计,发布之后通过用户反馈来判断某种设计更受欢迎),因此就有了这样的需求: 如果一个story在当前迭代中无法完成,那样需要给它加上toggle, 这样只需在生产环境将toggle关闭,不为担心未完成的功能被release出去;另一方面,如果发现新的实现、体验不被欢迎,那么只需将toggle关闭就可以快速地返回到旧的实现了。为了满足上面的需求,我们选择了togglz

但是这种方式进行了一段时间后,问题开始出现了。功能测试中的有些测试方法是针对旧的体验书写的,另外还有一些测试方法是对同一功能的新的体验书写的,我们想同时保留针对新旧两种体验的测试方法,但是想这样做的话就需要手动来打开或是关闭对应的测试方法,比如说现在需要对AwesomeFeature做A/B testing, 我们添加了一个toggle叫 NEW_DESIGN_FOR_AWESOME_FEATURE, 对应的就有两种情况

  • 当这个toggle开启的时候 NEW_DESIGN_FOR_AWESOME_FEATURE = true, 对应的测试需要修改为
1
2
3
4
5
6
@Ignore
@Test
public void shouldDoSomethingForAwesomeFeature() {}
      
@Test
public void shouldDoSomethingForNewDesignForAwesomeFeature() {}
  • 当这个toggle关闭的时候 NEW_DESIGN_FOR_AWESOME_FEATURE = false, 对应的测试需要修改为
1
2
3
4
5
6
@Test
public void shouldDoSomethingForAwesomeFeature() {}
      
@Ignore
@Test
public void shouldDoSomethingForNewDesignForAwesomeFeature() {}

RunIf

我们需要根据toggle的配置来动态来决定哪些测试方法需要被执行!

于是求助google, 发现了有这么个东西 junit-ext, 它很可能就是我们想想找的东西!

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(JunitExtRunner.class)
public class TestCasesOnDifferentOS {
    @Test
    @RunIf(value = OSChecker.class, arguments = OSChecker.MAC)
    public void shouldRunOnMac() throws Exception {
    }

    @Test
    @RunIf(value = OSChecker.class, arguments = OSChecker.WINDOWS)
    public void shouldRunOnWindows() throws Exception {
  }
}

我们只需要在@RunIf里面使用我们自己的Checker就可以很方便地控制测试方法的执行与否,于是脑海中出现了这样片段:

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(JunitExtRunner.class)
public class TestCasesOnDifferentDesign {
    @Test
    @RunIf(value = ToggleChecker.class, arguments = {"NEW_DESIGN_FOR_AWESOME_FEATURE", "false"})
    public void shouldDoSomethingForAwesomeFeature() throws Exception {
    }

    @Test
    @RunIf(value = ToggleChecker.class, arguments = {"NEW_DESIGN_FOR_AWESOME_FEATURE", "true"})
    public void shouldDoSomethingForNewDesignForAwesomeFeature() throws Exception {
  }
}

感觉不错,似乎已经成功一大半了,但是。。。

当我尝试给现有的测试类加上@RunWith(JunitExtRunner.class)时,才注意到他上面已经指定了一个Runner了(@RunWith(Theories.class)), 再问google后得到一个令人绝望的消息,一个测试类不能指定多个Runner!!!

assumeThat

看来只能另辟蹊径了, 接着找可以控制测试方法执行的办法。

assumeThat似乎可以,它会在条件不满足的时候中止当前的测试方法并且ignore之, 而且这是junit自带的,不需要使用额外的runner。

应该通过类似下面的代码就可以如愿了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestCasesOnDifferentDesign {
    @Test
    public void shouldDoSomethingForAwesomeFeature() throws Exception {
      assumeThat(NEW_DESIGN_FOR_AWESOME_FEATURE.isActive(), is(false));
      
      //...
    }

    @Test
    public void shouldDoSomethingForNewDesignForAwesomeFeature() throws Exception {
      assumeThat(NEW_DESIGN_FOR_AWESOME_FEATURE.isActive(), is(true));
      
      ///...
  }
}

FeatureManagerProvider

现在形式一片大好,接下来要做的就是想办法读取到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信息。

TogglzConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyTogglzConfig implements TogglzConfig {
  @Override
  public Class<? extends Feature> getFeatureClass() {
    return MyFeature.class;
  }

  @Override
  public StateRepository getStateRepository() {
    URL toggleProperties = getClass().getClassLoader().getResource("togglz.properties");
    assert toggleProperties != null;
    try {
      return new FileBasedStateRepository(new File(toggleProperties.toURI()));
    } catch (URISyntaxException e) {
      e.printStackTrace();
      return null;
    }
  }

  @Override
  public UserProvider getUserProvider() {
    return new NoOpUserProvider();
  }
}

在我们自己实现的这个togglzConfig中我们主要做了两件事情,一是指定了从哪里读取toggle配置信息,还有就是指定了Feature Enum.

FeatureManagerProvider

1
2
3
4
5
@BeforeClass
public static void beforeClass() {
  FeatureManager featureManager = new FeatureManagerBuilder().togglzConfig(new MyTogglzConfig()).build();
  ThreadLocalFeatureManagerProvider.bind(featureManager);
}

在上面的这段代码里,我们使用FeatureManagerBuilder构建了一个FeatureManager, 并且把它注册/绑定到ThreadLocalFeatureManagerProvider, 这两行代码我们是放在了最外层的TestSuite类的BeforeClass方法中,这样保证了在所有测试方法被执行之前toggle信息已经被初始化好了。

Done!

Attach Scala Source Code in IntelliJ

| Comments

Following below steps to attach source code for Scala in IntelliJ.

  1. Open your Project Settings
  2. Choose Libraries section
  3. Click + button and choose Scala SDK
  4. Specify the location of your downloaded SDK.

Testing Directive - Call Controller Method via Isolated Scope

| Comments

Call controller method via isolated scope

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div ng-controller="FruitController" ng-init="init()">
    <h4>Call controller method via isolated scope</h4>
    <label>What's your favorite fruit(name can only contains letter)</label>
    <input type="text" ng-model="newFruit"/>
    <button type="button"
      add-fruit-method-isolated
      is-valid="isValid(name)"
      new-fruit="newFruit"
      fruits="fruits">
        validate and add
    </button>
    <ul>
        <li ng-repeat="fruit in fruits track by $index"></li>
    </ul>
</div>

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
describe('directives', function () {
    beforeEach(module('myApp.directives'));

    describe('addFruitMethodIsolated', function () {
        var $scope, element;
        beforeEach(inject(function ($rootScope, $compile) {
            $scope = $rootScope;
            $scope.fruits = [];
            $scope.newFruit = 'apple';
            $scope.isValid = angular.noop;

            element = angular.element('<input type="text" ng-model="newFruit"/>' +
                '<button type="button" ' +
                'add-fruit-method-isolated ' +
                'is-valid="isValid(name)" ' +
                'new-fruit="newFruit" ' +
                'fruits="fruits">' +
                'validate and add</button>');

            $compile(element)($scope);
        }));

        it('should add valid fruit to fruit list when click button', function () {
            var isValid = spyOn($scope, 'isValid').andReturn(true);

            element.filter('button').trigger('click');

            expect(isValid).toHaveBeenCalled();
            expect(isValid.mostRecentCall.args[0]).toBe('apple');
            expect($scope.fruits[0]).toBe('apple');
        });
    });
});

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
angular.module('myApp.directives', [])
    .directive('addFruitMethodIsolated', function () {
        return {
            scope: {
                fruits: '=',
                newFruit: '=',
                isValid: '&'
            },
            link: function (scope, element, attr) {
                element.click(function () {
                    if (scope.isValid({name: scope.newFruit})) {
                        scope.fruits.push(scope.newFruit);
                        scope.$apply();
                    }
                });
            }
        };
    });