前端测试进化论

前端测试进化论

第一篇web 发展史

这篇主要是帮助我们qa 了解前端的历史与现状。

测试工程师大部分时间可能关注的是feature正确性,而对于前端代码级别的测试基本没有关心过,随着各种前端框架在web和移动端大行其道,绽放各种高大上的功能时,我们渐渐的发现前端代码级别问题逐渐暴露出来,甚至超越后端之势。那么问题来了,为什么以前这种现象出现呢?
我们现来看看前端技术发展史。

##石器时代——开荒阶段

带有简单逻辑的界面

最早期的Web界面基本都是在互联网上使用,而且基本都是静态页面展示,人们浏览某些内容,填写几个表单并且提交。当时的界面以浏览为主,基本都是HTML代码,我们来看一个最简单的HTML文件:

1
2
3
4
5
6
7
8
9
<html>
 <head> 
<title>开荒时代</title> 
</head>
 <body> 
<h1>静态页面</h1>
 <p>这是个测试</p>
 </body>
 </html>

带有简单逻辑的界面

接下来我们发现使用一些工具可以帮助我们做一些业务处理,这时javascript到来了,开始大放异彩。这时的代码的组织比较简单,而且CSS的运用也是比较少的。譬如:下面这个文档将带有一段JavaScript代码,用于拼接两个输入框中的字符串,并且弹出窗口显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
    <head>
        <title>javascript时代</title>
    </head>
    <body>
        <input id="firstNameInput" type="text" />
        <input id="lastNameInput" type="text" />
        <input type="button" onclick="greet()" />
        <script language="JavaScript">
        function greet() {
            var firstName = document.getElementById("firstNameInput").value;
            var lastName = document.getElementById("lastNameInput").value;
            alert("Hello, " + firstName + "." + lastName);
        }
        </script>
    </body>
</html>

结合了服务端技术的混合编程

由于静态界面不能实现保存数据等功能,出现了很多服务端技术,早期的有CGI(Common Gateway Interface,多数用C语言或者Perl实现的),ASP(使用VBScript或者JScript),JSP(使用Java),PHP等等,Python和Ruby等语言也常被用于这类用途。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<head>
<title>Using GET Method to Read Form Data</title>
</head>
<body>
<center>
<h1>Using GET Method to Read Form Data</h1>
<ul>
<li><p><b>First Name:</b>
<%= request.getParameter("first_name")%>
</p></li>
<li><p><b>Last Name:</b>
<%= request.getParameter("last_name")%>
</p></li>
</ul>
</body>
</html>

铁器时代—Ajax席卷全世界

AJAX

由于Ajax的出现,规模更大,效果更好的Web程序逐渐出现,在这些程序中,JavaScript代码的数量迅速增加。出于代码组织的需要,“JavaScript框架”这个概念逐步形成。

JavaScript基础库—jquery

Prototype框架主要是为JavaScript代码提供了一种组织方式,对一些原生的JavaScript类型提供了一些扩展,比如数组、字符串,又额外提供了一些实用的数据结构,如:枚举,Hash等,除此之外,还对dom操作,事件,表单和Ajax做了一些封装。

Mootools框架的思路跟Prototype很接近,它对JavaScript类型扩展的方式别具一格,所以在这类框架中,经常被称作“最优雅的”对象扩展体系。
从这两个框架的所提供的功能来看,它们的定位是核心库,在使用的时候一般需要配合一些外围的库来完成。

jQuery与这两者有所不同,它着眼于简化DOM相关的代码。

DOM的选择

jQuery提供了一系列选择器用于选取界面元素,在其他一些框架中也有类似功能,但是一般没有它的简洁、强大。

1
2
3
4
5
$("*")              //选取所有元素
$("#lastname")      //选取id为lastname的元素
$(".intro")         //选取所有class="intro"的元素
$("p")              //选取所有&lt;p&gt;元素
$(".intro.demo")    //选取所有 class="intro"且class="demo"的元素

链式表达式

在jQuery中,可以使用链式表达式来连续操作dom,比如下面这个例子:

1
$("p.neat").addClass("ohmy").show("slow");

除此之外,jQuery还提供了一些动画方面的特效代码,也有大量的外围库,比如jQuery UI这样的控件库,jQuery mobile这样的移动开发库等等。

模块代码加载方式–Requirejs

以上这些框架提供了代码的组织能力,但是未能提供代码的动态加载能力。动态加载JavaScript为什么重要呢?因为随着Ajax的普及,jQuery等辅助库的出现,Web上可以做很复杂的功能,因此,单页面应用程序(SPA,Single Page Application)也逐渐多了起来。

单个的界面想要做很多功能,需要写的代码是会比较多的,但是,并非所有的功能都需要在界面加载的时候就全部引入,如果能够在需要的时候才加载那些代码,就把加载的压力分担了,在这个背景下,出现了一些用于动态加载JavaScript的框架,也出现了一些定义这类可被动态加载代码的规范。

在这些框架里,知名度比较高的是RequireJS,它遵循一种称为AMD(Asynchronous Module Definition)的规范。
例如下列是一个js代码保存为adder.js文件

1
2
3
4
5
6
7
define(["math"], function(math) {
    return {
        addTen : function(x) {
            return math.add(x, 10);
        }
    };
});

当我们需要使用这个模块时,我们只需引入就好

1
2
3
4
5
6
<script src="require.js"></script>
<script>
    require(["adder"], function(adder) {
        //使用这个adder
    });
</script>

RequireJS除了提供异步加载方式,也可以使用同步方式加载模块代码。AMD规范除了使用在前端浏览器环境中,也可以运行于nodejs等服务端环境,nodejs的模块就是基于这套规范定义的。(修订,这里弄错了,nodejs是基于类似的CMD规范的)

工业革命—前端框架

这个时期,随着Web端功能的日益复杂,人们开始考虑这样一些问题:

• 如何更好地模块化开发

• 业务数据如何组织

• 界面和业务数据之间通过何种方式进行交互

在这种背景下,出现了一些前端MVC、MVP、MVVM框架,我们把这些框架统称为MV*框架。这些框架的出现,都是为了解决上面这些问题,具体的实现思路各有不同,主流的有Backbone,AngularJS,Ember,Spine等等,本文主要选用Backbone和AngularJS来讲述以下场景。

数据模型

在这些框架里,定义数据模型的方式与以往有些差异,主要在于数据的get和set更加有意义了,比如说,可以把某个实体的get和set绑定到RESTful的服务上,这样,对某个实体的读写可以更新到数据库中。另外一个特点是,它们一般都提供一个事件,用于监控数据的变化,这个机制使得数据绑定成为可能。
在一些框架中,数据模型需要在原生的JavaScript类型上做一层封装,比如Backbone的方式是这样:

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
var Todo = Backbone.Model.extend({
    // Default attributes for the todo item.
    defaults : function() {
        return {
            title : "empty todo...",
            order : Todos.nextOrder(),
            done : false
        };
    },
 
    // Ensure that each todo created has `title`.
    initialize : function() {
        if (!this.get("title")) {
            this.set({
                "title" : this.defaults().title
            });
        }
    },
 
    // Toggle the 'done' state of this todo item.
    toggle : function() {
        this.save({
            done : !this.get("done")
        });
    }
});

控制器

在Backbone中,是没有独立的控制器的,它的一些控制的职责都放在了视图里,所以其实这是一种MVP(Model View Presentation)模式,而AngularJS有很清晰的控制器层。

还是以这个todo为例,在AngularJS中,会有一些约定的注入,比如$scope,它是控制器、模型和视图之间的桥梁。在控制器定义的时候,将$scope作为参数,然后,就可以在控制器里面为它添加模型的支持。

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
function TodoCtrl($scope) {
    $scope.todos = [{
        text : 'learn angular',
        done : true
    }, {
        text : 'build an angular app',
        done : false
    }];
 
    $scope.addTodo = function() {
        $scope.todos.push({
            text : $scope.todoText,
            done : false
        });
        $scope.todoText = '';
    };
 
    $scope.remaining = function() {
        var count = 0;
        angular.forEach($scope.todos, function(todo) {
            count += todo.done ? 0 : 1;
        });
        return count;
    };
 
    $scope.archive = function() {
        var oldTodos = $scope.todos;
        $scope.todos = [];
        angular.forEach(oldTodos, function(todo) {
            if (!todo.done)
                $scope.todos.push(todo);
        });
    };
}

视图

在这些主流的MV*框架中,一般都提供了定义视图的功能。在Backbone中,是这样定义视图的:

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
36
37
38
39
40
// The DOM element for a todo item...
var TodoView = Backbone.View.extend({
    //... is a list tag.
    tagName : "li",
 
    // Cache the template function for a single item.
    template : _.template($('#item-template').html()),
 
    // The DOM events specific to an item.
    events : {
        "click .toggle" : "toggleDone",
        "dblclick .view" : "edit",
        "click a.destroy" : "clear",
        "keypress .edit" : "updateOnEnter",
        "blur .edit" : "close"
    },
 
    // The TodoView listens for changes to its model, re-rendering. Since there's
    // a one-to-one correspondence between a **Todo** and a **TodoView** in this
    // app, we set a direct reference on the model for convenience.
    initialize : function() {
        this.listenTo(this.model, 'change', this.render);
        this.listenTo(this.model, 'destroy', this.remove);
    },
 
    // Re-render the titles of the todo item.
    render : function() {
        this.$el.html(this.template(this.model.toJSON()));
        this.$el.toggleClass('done', this.model.get('done'));
        this.input = this.$('.edit');
        return this;
    },
 
    //......
 
    // Remove the item, destroy the model.
    clear : function() {
        this.model.destroy();
    }
});

参考文献:

http://any9.com/1524.html
http://blog.jobbole.com/45169/
http://blog.jobbole.com/45170/
http://blog.jobbole.com/41988/
我毕竟不是一个前端dev,有所疏漏尽请原谅,还有转载和抄录了一些人的blog 也请大家谅解,只是想写一些前端的文章给qa用。