这个题目有点大, 因为对于开发SPA(单页面应用, Single Page Application), 一个前端工程师需要掌握太多太多知识和工具了. 这里只说我自己最近半年在摸索和研究前端SPA相关技术的过程中的一些经验和总结.

选择合适的开发IDE

一个好的IDE可以让你敲键盘的次数大大减少, 我个人推荐使用Sublime Text, 速递快, 功能强大, 可扩充, 这也是我目前正在使用的IDE.

选择合适的开发语言

呃, 难道还有别的选择? 开发前端应用不都是用Javascript么? 对的, 因为现在有了CoffeeScript. 相比于Javascript的类C++/Java静态语言语法, CoffeeScript提供了更贴近自然语言的动态语言语法, 更少的代码, 更少的语法漏洞, 更好的代码可读性.

选择合适的Javascript前端MVC框架

Javascript程序很难调试, 特别是当代码行数超过一定量级之后. 为了让程序逻辑更有条例, 与服务器端的web框架类似, 前端也有很多Javascript库或者框架提供了MVC或类MVC架构. 按照MVC架构进行设计, 可以很清晰地将Module(数据模块), View(页面展示), Controller(用户行为响应控制)进行分离. 对于小型web应用来说, 这可能无关紧要, 甚至MVC带来的弊端会掩盖它的优点, 但对于一个超过上千行的web应用来说, 一个清晰的程序架构可以极大地减少模块间的耦合性.

大家可以仔细看一下Top 10 Javascript MVC框架的对比. 我个人感觉, Backbone的代码更紧凑一些, 开发社区也比较活跃.

模块定义与加载管理

Javascript语言自身不提供代码的加载管理, 当JavaScript代码分成多个模块, 处于不同文件中时, 如何管理模块之间的依赖关系以及文件的加载顺序, 就会变成一个非常棘手的事情. 并且JavaScript自身不提供名字空间的管理, 不同模块, 不同的JavaScript库之间的都有可能出现命名冲突.

AMD(Asynchronous Module Definition)是用来规范如何定义模块及其依赖项, 以及如何异步加载模块. 大家可以参考Addy Osmani的博文Writing Modular JavaScript With AMD, CommonJS & ES Harmony来了解具体的细节.

目前前端使用最广泛的前端AMD库应当是RequireJS.

代码质量静态检查

Javascript代码的书写格式非常自由, 甚至带着错都能运行下去, 这也是为什么JavaScript代码很难调试的原因之一.

进行代码质量的静态检查可以极大减少由于语法漏洞或者拼写疏忽带来的这些额外错误, 推荐使用jshint. 如果使用CoffeeScript, 这步可以省略了, CoffeeScript编译后的代码都能通过jshint的检测.

Read on →

Contact(源代码)示例项目使用了bbb作为项目构建工具, 作为gruntjs的扩展, bbb能很方便地完成:

  • Coffeescript的编译
  • 文件的清除和复制
  • 源代码lint
  • 编译LESS
  • 优化requirejs模块
  • js/css文件的合并和优化
  • 文件的压缩, 打包
  • 内置调试http服务器

并且项目中还使用bbb进行jasmine单元测试代码的编译. 所有的操作都可以通过定义bbb任务并以命令行方式进行运行, 唯一的一个例外是jasmine测试执行需要启动浏览器执行. 如果要在项目中实施持续集成, 就必须不能依赖浏览器而以命令行方式执行测试.

曾经尝试过envjs, 但在当前的实现中, envjs还不能顺利运行requirejs的异步模块加载(issue). Phantomjs是另外一种方案, 它可以很顺利地集成jasmine以及requirejs, 但是如果需要集成得很好, 必须得实现一个jasminereporter用来和Phantomjs进行通讯, 并且还得实现一个gruntjs任务插件, 用来调用Phantomjs执行测试, 以及生成测试报告. 这个工作量不大, 我也曾经完成了一个最简单的gruntjs任务来调用Phantomjs. 正在想着怎样进行重构的时候, 突然发现最新的bbb已经悄然导入了grunt-jasmine-task, 一个利用Phantomjs执行jasmine测试的gruntjs任务插件.

Ok, 那一切就简单了. 现在仅仅需要修改grunt.js配置文件来定义项目的jasmine任务. 当然, 之前必须安装Phantomjs(gruntjs网站上有关于如何安装的faq).

1
2
3
4
5
6
7
8
9
10
11
  grunt.initConfig({
    // jasmine task is to run specs using phantom, before running it, you must
    // make sure you have installed phantom following instruction on
    // https://github.com/cowboy/grunt/blob/master/docs/faq.md#why-does-grunt-complain-that-phantomjs-isnt-installed
    jasmine: {
      all: {
        src:['http://localhost:8000/tests/SpecRunner.html'],
        timeout: 300000 //in milliseconds
      }
    }
  });

启动http服务:

1
2
3
$ ./node_modules/bbb/bin/bbb server
Running "server" task
Listening on http://127.0.0.1:8000

然后在另一个终端运行jasmine任务:

1
2
3
4
5
6
7
$ ./node_modules/bbb/bin/bbb jasmine
Running "jasmine:all" (jasmine) task
Running specs for SpecRunner.html
.............
>> 31 assertions passed in 13 specs (1451ms)

Done, without errors.
Read on →

我们必须为自己的代码写自动测试代码, 并且需要持续地对自己的代码进行回归测试, 因为:

  • 项目成员对模块间接口的理解必须一致, 测试代码是最好的文档.
  • 谁都没十足的把握保证自己的修改不影响别人的代码.
  • 没有快速而充分的测试作为保障, 就很难提倡快速重构.
  • 让代码成为资产, 而不是债务, 减少代码的维护成本.
  • 每个人都必须为自己的代码负责.

那么基于backbone+requirejs的前端项目应当如何实施测试?

当前已经有很多非常优秀的javascript测试框架, 包括jasmine, QUnit, mocha. backbonerequirejs的前端项目可以使用上述任何一种测试框架. 技术经理可以根据团队成员的技术背景, 喜好来决定选择哪一种测试框架.

下面就以Contacts应用为例, 简单demo如何在项目中实现基于jasmine+sinon的测试用例.

工程的目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|-coffee                # web应用coffeescript源代码
|-app                   # coffee编译之后的web应用js文件
|-dist
   |-debug              # concat所有js文件到一个js文件, 但未作minize
   |-release            # concat所有js文件到一个js文件, 并且minize
|-assets                # jquery/requirejs/underscore/backbone等库文件
|-tests
   |-coffee             # 测试程序的coffeescript源代码
      |-config.coffee   # requirejs配置文件
      |-runner.coffee   # 按照requirejs模块定义规范定义的jasmine测试执行模块
   |-js                 # coffee编译之后的测试程序js文件
   |-lib                # jasmine/sinon等测试用库文件
   |-SpecRunner.html    # 测试html文件, 用来执行broqser端测试代码
|-index.html            # app html文件
|-favicon.ico
|-grunt.js              # grunt任务配置文件

按照requirejs的模块定义方式定义测试模块

由于被测模块是由requirejs进行加载的, 因此, 我们也可以遵循requirejs的模块定义方式定义测试模块, 确保被测模块测试模块前加载完成:

model_spec.coffee
1
2
3
4
define [use!underscore', 'use!backbone', 'model/under/test'], (_, Backbone, model) ->
  describe "suite description...", ->
    it "spec description...", ->
      # test code here ...

下面是测试用例的代码(collection, model, view各实现了一个模块的测试, 仅供demo):

Read on →

Backbone.sync是Backbone用来和服务器进行数据交换的方法. 每当Collection或者Model的数据发生变化, Backbone就会调用Backbone.sync进行数据的CRUD操作, 这个同步方法的缺省实现是使用(jQuery/Zepto).ajax向服务器发送RESTful JSON请求, 并返回一个jqXHR. 你可以通过重载这个方法来定义不同的持续化策略, 例如WebSocket, XML, 或者本地存储.

Backbone.sync的函数定义是function(method, model, [options]):

  • medhod: CRUD名称, 可以是create, read, update, delete.
  • model: 需要保存的model, 可以是Backbone.Model或者Backbone.Collection.
  • options: 所有jQuery请求选项, 包括success和error回掉函数.

Backbone.sync方法的缺省实现是通过标准的RESTful风格的CRUD进行数据的操作. CRUD方法对应的REST接口分别是:

  • create –> POST /collection
  • read –> GET /collection[/id]
  • update –> PUT /collection/id
  • create –> DELETE /collection/id

你可以通过重载全局的Backbone.sync, 或者Collection/Model的sync方法来改变其缺省实现.

localStorage方式实现Backbone.sync

地址本示例中, 通过重载全局的Backbone.sync方法, 将Collection/Model的CRUD操作转化为localStorage的对象CRUD操作.

下面是Backbone.sync方法的定义(源码), 首先获得传递进来的model对象或其集合对象的localStorage属性对象, 并将CRUD操作转化为localStorage属性对象的数据CRUD操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  Backbone.sync = (method, model, options) ->
    store = model.localStorage or model.collection.localStorage

    switch method
      when "read"
        resp = if model.id then store.find(model) else store.findAll()
      when "create"
        resp = store.create(model)
      when "update"
        resp = store.update(model)
      when "delete"
        resp = store.destroy(model)

    if resp
      options.success resp
    else
      options.error "Record not found"
Read on →

JavaScript是标准的函数式动态语言, 但却有Java的语法. 用Java的语法写JavaScript的代码, 就好比穿着西装进行散打, 的确很让人别扭.

CoffeeScript对JavaScript的改造刚好切中要害, 用Ruby/Python的语法重新塑造了JavaScript, 抛弃掉Java语法上繁琐的要求, 让人可以用更简洁的方式写出优雅, 可读性更好的语句.

简洁

简洁意味着花更少的时间进行coding. 难道不是么? 我相信程序员都愿意去做真正意义的编程, 而不是枯燥地敲键盘coding.

大家可以对比一下CoffeeScript和JavaScript的语法, 看看CoffeeScript的简洁:

CoffeeScript的平均代码量估计不超过等价JavaScript代码量的50%.

Read on →

什么是 mixin ?

In object-oriented programming languages, a mixin is a class that provides a certain functionality to be inherited or just reused by a subclass, while not meant for instantiation (the generation of objects of that class). Mixins are synonymous with abstract base classes. Inheriting from a mixin is not a form of specialization but is rather a means of collecting functionality. A class or object may “inherit” most or all of its functionality from one or more mixins, therefore mixins can be thought of as a mechanism of multiple inheritance.

简单的说, mixin 是一种类的多继承的机制.

什么时候需要 mixin ?

就如stackoveflow 上的回答, 有两个主要的使用 mixin 的场景:

  • 你希望给一个类提供很多可选的特征(feature).
  • 你希望在很多不同的类中使用一个特定的特征(feature).
Read on →

什么是 metaclass ?

In object-oriented programming, a metaclass is a class whose instances are
classes. Just as an ordinary class defines the behavior of certain objects, a
metaclass defines the behavior of certain classes and their instances.

简单的说, metaclass 就是 class 类型对象的 class, metaclass 的实例对象是 class 类型对象.

metaclass 能用来做什么 ?

我们就可以用 metaclass 来动态生成或者更改 class 的定义.

下面分别是通过传统 class 方式以及 metaclass 动态定义类 A

传统 class 方式定于类 A
1
2
class A(object):
    a = 'I am a string.'
metaclass 动态定义类 `A`
1
A = type('A', (object,), {'a': 'I am a string.'})

上面我们通过type类的实例化来生成类A, 如果我们想自定义类的生成, 我们可以以type类为基 类派生出自定义的 metaclass.

注: 本文所有代码在 python2.6 下能执行通过, 不能确保在 python3 下无误.

Read on →

AMD项目的构建和发布

在 Javascript 前端项目中, 为了提高页面的响应速度, 通常需要对 js/css 文件进行:

  • concat, 将多个 js 或者 css 文件合成一个文件, 这样能减少浏览器给服务器发送请求的此次数, 减少在网络通讯握手上花费的时间;
  • minify, 去除 js 或者 css 文件中多余的字符, 例如注释, 换行, 空格等, 并将变量的可读的长名称改为不可读的短名称, 缩短 js 和 css 文件的长度, 减少网络传输的时间;

目前已经有很多进行 Javascript 构建和发布管理的工具, 包括Jake, Grunt, js-build-tools (ant tasks collection). RequireJS也已经提供了一个很好的优化 js 文件的工具r.js, 可以将多个 AMD 模块合并成一个文件. 这里主要介绍 Grunt 的一个 plugin 集合: Grunt-bbb

Grunt-bbb

bbb 主要包含下面这些 tasks:

  • init 初始化一个空的项目模板.
  • lint 确保所有的 js 文件符合 JSHint.
  • less 编译 LESS 生成 css 文件.
  • mincss Minify 所有的 css 文件,并合并成一个 css 文件.
  • min grunt 内置 task, Minify js 文件.
  • concat 合并多个文件到一个文件.
  • requirejs 利用r.js合并RequireJS模块到一个 js 文件.
  • server 静态文件的 HTTP 服务, 用于开发调试.

安装

1
$ npm install -g bbb

新项目初始化

进入空的项目目录,然后运行:

1
$ bbb init

根据命令行的提示输入相应的内容, 然后会生成一个空的 grunt.js 文件.

目录结构

bbb 要求按照如下目录结构组织源代码:

1
2
3
4
5
6
7
8
9
10
|-app
   |-项目 js 文件
|-dist
   |-build生成的发布文件
|-assets
   |-require.js
   |-backbone.js
   |-underscore.js 等需要的 js 库文件
|-index.html
|-favicon.ico

css 和 img 文件缺省目录在 assets/ 目录中. 我们可以更改 grunt.js 来增加/更改/删除任务(task), 以及更改任务的设置, 包括缺省目录.

Read on →

为什么使用AMD/RequireJS?

还记得C语言是怎么解决引用冲突的么?

1
2
3
4
5
6
#ifndef _MY_MODULE_H_
#define _MY_MODULE_H_

/*my code here*/

#endif  /*_MY_MODULE_H_*/

Backbone解决的是将用户数据,页面显示,以及流程控制模块化,而AMD解决的是将不同功能的代码封装到小的代码单元,代码单元功能的注册,以及代码依赖。

What are JavaScript modules? What is their purpose?
- Definition: how to encapsulate a piece of code into a useful unit, and how to register its capability/export a value for the module.
- Dependency References: how to refer to other units of code.

如何使用AMD/RequireJS?

下面javascript代码定义了一个新的模块module1,其依赖jquery模块:

module1.js
1
2
3
define('module1', ['jquery'] , function ($) {
    return function () {};
});

define函数接受三个参数:

  • 第一个参数是个字符串,定义了本模块的名称,其他模块可以用这个名称来引用本模块曝露出来的对象;可以省略该参数,缺省以文件名来命名该模块;
  • 第二个参数是个数组,定义了本模块需要引用的其他模块的列表,例如jquery或者其他用户自定义模块;
  • 第三个参数是个函数,该函数的参数列表分别对应第二个参数里的模块列表曝露的对象;该函数的返回值即为本模块曝露的对象;

官网可以查到详细的说明和用例。

Read on →

相对于传统的服务器端动态页面技术,SPA(单页面应用)在浏览器端实现了所有的页面逻辑,避免了页面跳转而造成的操作中断,提供更为平滑的用户体验。要实现SPA,浏览器端必然需要一个javascript库来管理数据模块,用户操作,以及界面刷新,类似于服务器端MVC结构。在这里可以找到目前流行的浏览器端类MVC实现。

前些日子先后学习了BackboneSpine,并参阅了双方的不少例子程序,其中包括Spine实现的Spine.Contacts(demo, source)。为了更好地理解Backbone的原理,就利用空闲时间使用Backbone重写了该地址本应用(demo, source)。

Read on →