Python 面试问题

最近正在团队内部普及 Python 语言,有些刚接触 Python 语言的工程师在概念 上有很多混淆的地方,刚好看到这篇文章:Python面试问题, 里面列举的问题都是关于 Python 基本的常识或者容易混淆的知识点,因此推荐 给团队的 Python 初学者。

  1. 参数是如何传递的?传值还是传引用?(How are arguments passed – by reference of by value?)

    这个问题的有点c/c++风格,问题的实质是当一个传入的参数在函数体内被 更改,那么在函数返回后,函数体外的这个参数变量的值是否改变。最简短的回 答是,”都不是”,事实上 Python 里是传对象(call by object)。

    要弄明白这个变量的值是否在函数体外发生变化,需要明白两个概念:

    • Python 里的对象分为有可变(mutable)和不可变(immutable):数字(int, float),字符串,元组(tuple)是不可变的,列表(list),字典(dict)是可 变的。
    • 所有的变量都是一个对象的引用。无论对象是否可变,这个变量都可被赋 值为另外一个对象。

    因此,如果传入的参数变量指向一个不可变对象,那么在函数体外这个对象 的内容永远不会发生变化;如果当传入的参数变量指向一个可变对象,那么这个 对象是否发生变化,取决于函数体内部是否改变了这个可变对象的内容。

    下面的例子里,changed函数调用改变了传入对象的内容,unchanged函 数将l变量赋值为一个新生成的对象l = l + ["a"],但原始对象并未发生变 化:

  2. 什么是列表和字典推导(list and dict comprehensions)?你能给个例子吗?

    列表/字典推导是一种语法糖(syntax sugar),用来简化列表和字典的生成。 根据第三版的”Learning Python”,列表推导的执行速度快于通常的循环,但并 不确保以后的 Python 版本也是这个结果。

    列表推导是典型的 Pythonic 的代码书写方式。

  3. 什么是 PEP 8?

    PEP 8 定义的是 Python 代码规范,告诉你如何书写可读性强,更好维护的 代码。详细内容请参考 PEP 8 官方文档。Sublime Text编辑工具带有 PEP 8 格式检测插件,所存盘的文件都应当符合 PEP 8 的 规范。

  4. 你使用虚拟环境(virtual environment)吗?

    很多 Python 程序员认为,包括我自己也是这样的观点, virtual environment是一个非 常有用的工具,用来孤立你的开发和运行环境,特别是当需要同时开发多个项目, 基于不同的 Python 版本以及运行库的时候。每当新建一个项目,或者clone一 个已存在项目的时候,我都会按照下面的步骤建立和使用虚拟环境:

  5. 如何计算列表里所有元素的和?如何计算列表里所有元素的乘积?

    这个问题相当简单,但需要记住一点,你需要用 Pythonic 的方式来解答这 个问题:

  6. 你能列出列表(list)和元组(tuple)的区别吗?举例子说明用法的不同。

    列表和元组是 Python 里最基本的两个数据类型:

    • 首先,列表对象是可变的(mutable),但元组不是;
    • 其次,元组可被哈希(hashed),例如可以用作字典对象的键值(key);

    至于例子,地图上的地理坐标可以用二元组表示,而地图上的路径可以用坐标 点列表来表示。

  7. 你知道rangexrange的区别吗?

    range函数返回的是一个列表对象,xrange返回的是一个xrange对象。 他们的主要区别是:

    • 内存的占用不同。列表对象已经在内存中存在了,而xrange对象永远占 用同样的内存大小,无论需要生成的range有多大。
    • xrange对象不能使用列表对象的切片函数,也就是说 range(10)[3:5]能工作,但是xrange(10)[3:5]就不工作了。

    类似的问题还有:mapitertools.imap的区别?filteritertools.ifilter的区别?清楚这些区别,你就能知道怎样更有效地使用这 些函数。

  8. 请说出一些 python2.x 与 python3.x 之间的区别。

    如果你经常看网上的文章,你肯定能说出一些区别:

    • python3.x 的字符串都是unicode;
    • python3.x 中print是函数,而不是语句;
    • python3.x 中使用range代替了xrange,删除了xrange函数;
    • python3.x 中全部类(class)都是新类型(new style);
    • python3.x 支持更简单的unpack方式,如first, *middle, last = [0, 1, 2, 3, 4, 5, 6, 7]

    如果你一条区别也说不出来,至少说明你对 Python 的关注度不够。

  9. 什么是修饰器(Decorator)?你能说出它的用途吗?

    修饰器(Decorator)也是一种非常 Pythonic 的方式,它可以让你非常方便 地注入(inject)或者修改函数和类的代码。换句话说,修饰器允许你包装函数和 类方法,在执行原始代码之前和之后执行其他的代码。修饰器语法能带来很多非 常有意思的编码方式,例如:

    • 内存缓存

      • 参数检测

  10. with语句及其用法?

    简单地说,with语句允许你在进入和/或退出指定的代码块的时候,执行 特定的代码。最常用的例子是使用with语句打开文件。为了在你自己定义的对 象上使用with语句,你必须定义__enter____exit__方法。

    更多的信息你可以查看理解 Python 的 with 语句


除了原文列举上述问题,下面知识点也是 Python 面试中很可能被问到的:

  • Package/Module的定义,以及模块加载原则;
  • 如何构建你自己的类型,如列表(list),字典(dict),迭代器(iterator);
  • 生成器(generator)的概念以及使用方式;
  • built-in 类型和函数;
  • 对象属性的操作原理,如dictgetattrgetattribute,描述 器(descriptor);
  • 元类编程(metaclass)的概念,以及如何使用;
  • 如何进行Package/Module的打包和分发;
  • 什么是WSGI;
  • 字符串的处理和正则表达式;
  • 如何操作json和xml数据;

另外,还必须尽可能地多了解 Python 语言最为强大的 库函数

在Python中生成单体实例的方法

Python是很强力的编程语言,它即支持面向对象编程的特征,也支持很多函数式 编程的特征,同时还是一门动态语言。由于Python语言特征的多样性,对于同一 种设计需求,我们可以采用多种完全不同的方式进行实现。优秀的软件工程师总 能在这些实现方式中,根据他们的优劣,选择出最适合的方式。

下面的内容源自StackOverflow的一篇文章: Creating a singleton in python

这个问题不是为了讨论单体实例是否是有必要的,或者是反面模式,也不是为了挑起任何宗教式的战争,而是为了讨论如何用更Pythonic的方式,最佳地实现单体实例模式。

方法1:装饰器(Decorator)

1
2
3
4
5
6
7
8
9
10
11
12
13
def singleton(class_):
  instances = {}
  def getinstance(*args, **kwargs):
    if class_ not in instances:
      instances[class_] = class_(*args, **kwargs)
    return instances[class_]
  return getinstance

@singleton
class MyClass(BaseClass):
  pass

c = MyClass()

优点

  • 装饰器在通常情况下比其他任何方式都要直观。

缺点

  • 任何通过调用MyClass()能获得真的单体实例,但是MyClass本身却被装饰 成一个函数(function)了,也就是说不能使用任何类方法和与类相关的函数如 isinstanceissubclass等。
  • 还能从MyClass继承一个子类么?

方法2:基类继承

1
2
3
4
5
6
7
8
9
10
11
class Singleton(object):
  _instances = {}
  def __new__(class_, *args, **kwargs):
    if class_ not in class_._instances:
      class_._instances[class_] = super(Singleton, class_).__new__(class_, *args, **kwargs)
    return class_._instances[class_]

class MyClass(Singleton, BaseClass): #  这里Singleton必须放在其他基类前面
  pass

c = MyClass()

优点

  • MyClass是真的类(class)。

缺点

  • 多继承…得小心使用!

方法3:元类(Metaclass)

1
2
3
4
5
6
7
8
9
10
11
class Singleton(type):
  _instances = {}
  def __call__(cls, *args, **kwargs):
    if cls not in cls._instances:
      cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
    return cls._instances[cls]

class MyClass(BaseClass):
  __metaclass__ = Singleton

c = MyClass()

忧点

  • MyClass是真的类(class)
  • 魔术般地覆盖了继承的案例。
  • 有目的地使用__metaclass__,让阅读者明确地知道这个目的。

缺点

  • 如果有的话,就是很多程序员不熟悉元类(MetaClass)。

方法4:装饰器(Decorator)返回相同名称的类(Class)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def singleton(class_):
  class class_w(class_):
    _instance = None
    def __new__(class_, *args, **kwargs):
      if class_w._instance is None:
          class_w._instance = super(class_w, class_).__new__(class_, *args, **kwargs)
          class_w._instance._sealed = False
      return class_w._instance
    def __init__(self, *args, **kwargs):
      if self._sealed:
        return
      super(class_w, self).__init__(*args, **kwargs)
      self._sealed = True
  class_w.__name__ = class_.__name__
  return class_w

@singleton
class MyClass(BaseClass):
  pass

c = MyClass()

优点

  • MyClass是真的类(class)
  • 魔术般地覆盖了继承的案例。

缺点

  • 生成一个新类有额外的开销没?每定义一个类,实际上生成了两个类来实现单 体实例。通常情况下不是问题,但扩展的时候可能有麻烦。
  • 对于_sealed属性有什么观点了?
  • MyClass是另外一个具有同样名称的MyClass类的子类,是否有点怪诞?这 就意味着你不能在子类的方法中通过super方法调用基类的相同的方法, 因为这是递归调用。

上面列出了四种不同的方式实现了单体实例,分别用到了装饰器(Decorator), 元类(MetaClass),和多继承(Multiple Inheritance)等语言特征(实际上还有 很多其他实现方式)。如果程序中用不到类(class)相关的函数和方法,第一种 方法就已经足够了,否则我们只能考虑方法2、3或者4。

还有没有更简单的方法了???

抛开固有的思维模式,keep it simple!

stackoverflow上投票最 多的答案是:

Modules are imported only once, everything else is overthinking. Don’t use singletons and try not to use globals.

用代码解释就是:

  • 定义一个模块,如mymodule
1
2
3
4
5
6
# mymodule.py
class Foo(object):
  def somemethod(self):
    pass

some_global_variable = Foo()
  • 在其他模块中引入该实例:
1
2
3
from mymodule import some_global_variable as foo

foo.somemethod()

在 Markdown 中嵌入 UML 文档

Markdown是网络书写语言,特别适合程序员书写文档:

  • 全文本格式,方便进行diffpatch和版本的管理;
  • 格式直观,简单易学,便于书写和阅读;
  • 兼容 HTML,能方便地转换为 pdf,doc等格式;
  • 支持 Linux,Windows,Mac;
  • 支持内嵌代码和语法高亮;

估计只是方便版本管理,就能吸引很多程序员的兴趣,特别是需要团队一起参与书写文档的时候。 感兴趣的可以参考官方网站,或者以前写的一份使用 Markdown 的 slides

但是毕竟Markdown只是书写语言,不是程序设计语言,如果我们需要嵌入 UML 的时候,不可避免地需要其他专业软件的支持。 这里介绍几种利用网络服务,可以直接在Markdown文档中嵌入的 UML 建模图:


用例图

  • 角色(Actor)

    使用[角色名]表示角色。

      <img src="http://yuml.me/diagram/scruffy/usecase/[Customer]" >
    

  • 用例(Use Case)

    使用(用例名)表示用例,-表示角色和用例之间的关联。

      <img src="http://yuml.me/diagram/scruffy/usecase/[Customer]-(Login),[Customer]-(Logout)" >
    

  • 备注(Notes)

    如果用例名以note:开头,表明那是一个备注,可以用{bg:颜色名}定义备注的背景色。

      <img src="http://yuml.me/diagram/scruffy/usecase/[Customer]-(Login), [Customer]-(note: Cust can be registered or not{bg:beige})" >
    

  • 角色继承(Actor Inheritance)

    使用符号^表示角色之间的继承关系。

      <img src="http://yuml.me/diagram/scruffy/usecase/[Cms Admin]^[User], [Customer]^[User], [Agent]^[User]" >
    

  • 扩展和包含(Extends and Includes)

    使用>表示用例之间的包含关系,<表示用例的扩展。

      <img src="http://yuml.me/diagram/scruffy/usecase/(Login)<(Register),(Login)<(Request Password Reminder),(Register)>(Confirm Registration)" >
    

  • 完整示例

      <img src="http://yuml.me/diagram/nofunky/usecase/(note: figure 1.2{bg:beige}), [User]-(Login),[Site Maintainer]-(Add User),(Add User)<(Add Company),[Site Maintainer]-(Upload Docs),(Upload Docs)<(Manage Folders),[User]-(Upload Docs), [User]-(Full Text Search Docs), (Full Text Search Docs)>(Preview Doc),(Full Text Search Docs)>(Download Docs), [User]-(Browse Docs), (Browse Docs)>(Preview Doc), (Download Docs), [Site Maintainer]-(Post New Event To The Web Site), [User]-(View Events)" >
    

YUML支持3种图示风格,分别是:

  • plain
  • scruffy
  • boring

你可以对比下列图示的不同风格:

活动图

  • 动作(Action)

    (状态名)表示一个状态,其中(start)(end)分别表示开始状态和结束状态,箭头->表示状态的转换。

      <img src="http://yuml.me/diagram/nofunky/activity/(start)->(Boil Kettle)->(end)" >
    

  • 判断和限制(Decisions and Constraints)

    使用<判断名>表示一个条件判断,其后跟[条件]->表示满足条件后状态的转换;用不同的判断名来标识不同的判定位置。

      <img src="http://yuml.me/diagram/nofunky/activity/(start)-><a>[kettle empty]->(Fill Kettle)->(Boil Kettle),<a>[kettle full]->(Boil Kettle)->(end)" >
    

  • 分支合并(Fork/Join)

    使用||表示分支或者合并点。

      <img src="http://yuml.me/diagram/nofunky/activity/(start)-><a>[kettle empty]->(Fill Kettle)->|b|,<a>[kettle full]->|b|->(Boil Kettle)->|c|,|b|->(Add Tea Bag)->(Add Milk)->|c|->(Pour Water)->(end),(Pour Water)->(end)" >
    

  • 对象(Objects)

    符号[]表示一个对象。

      <img src="http://yuml.me/diagram/nofunky/activity/(start)->[Water]->(Fill Kettle)->(end)" >
    

  • 连接器名称(Connector Name)

    ->中加入名称,-名称>表示命名连接器。

      <img src="http://yuml.me/diagram/nofunky/activity/(start)-fill>(Fill Kettle)->(end)" >
    

类图

  • 关联(Association)

    类名用[]表示,->表示定向关联,-表明关联。

      <img src="http://yuml.me/diagram/nofunky/class/[Customer]->[Billing Address]" >
    

  • 基数(Cardinality)

    基数-基数>表明关联的基数,其中基数可以为010..**等任意定义的值。

      <img src="http://yuml.me/diagram/nofunky/class/[Customer]1-0..*[Address]" >
    

  • 定向关联(Directional Association)

    定向关联->可以定义名称:-名称>

      <img src="http://yuml.me/diagram/nofunky/class/[Order]-billing >[Address], [Order]-shipping >[Address]" >
    

  • 颜色和UTF8字符(Splash of Colour And UTF-8)

    类图可以用{bg:颜色名}定义显示的背景颜色。

      <img src="http://yuml.me/diagram/nofunky/class/[❝Customer❞{bg:orange}]❶- ☂>[Order{bg:green}]" >
    

  • 聚合(Aggregation)

    聚合表示比关联更强的关联关系,使用<>->或者+->来表示。

      <img src="http://yuml.me/diagram/nofunky/class/[Company]<>-1>[Location], [Location]+->[Point]" >
    

  • 组成(Composition)

    组成表示比聚合更强的关联关系,使用++->来表示。

      <img src="http://yuml.me/diagram/nofunky/class/[Company]++-1>[Location]" >
    

  • 备注(Notes)

    使用[note:注解内容]表示备注,同样备注可以自定义颜色{bg:颜色名}

      <img src="http://yuml.me/diagram/nofunky/class/[Customer]<>1->*[Order], [Customer]-[note: Aggregate Root{bg:cornsilk}]" >
    

  • 继承(Inheritance)

    使用^-表示类的继承,右边的类是子类。

      <img src="http://yuml.me/diagram/nofunky/class/[Wages]^-[Salaried], [Wages]^-[Contractor]" >
    

  • 接口继承(Interface Inheritance)

    接口继承用^-.-来表示。

      <img src="http://yuml.me/diagram/nofunky/class/[<<ITask>>]^-.-[NightlyBillingTask]" >
    

  • 依赖(Dependencies)

    类的依赖用-.->来表示,依赖是最弱的关联关系,一般用来表示类方法的参数或者实现用到了依赖类。

      <img src="http://yuml.me/diagram/nofunky/class/[HttpContext]uses -.->[Response]" >
    

  • 接口(Interface)

    和类名相比,接口的名称一般包含在<<>>中。

      <img src="http://yuml.me/diagram/nofunky/class/[<<IDisposable>>;Session]" >
    

  • 类定义(Class with Details)

    可以在类符号[]中定义类的所有成员。使用|表示类名与类成员变量和成员函数的分割符,不同的成员之间用;隔开,使用+-分别表示公开和私有成员。

      <img src="http://yuml.me/diagram/nofunky/class/[User|+Forename+;Surname;+HashedPassword;-Salt|+Login();+Logout()]" >
    

  • 完整的示例

      <img src="http://yuml.me/diagram/nofunky/class/[note: You can stick notes on diagrams too!{bg:cornsilk}],[Customer]<>1-orders 0..*>[Order], [Order]++*-*>[LineItem], [Order]-1>[DeliveryMethod], [Order]*-*>[Product], [Category]<->[Product], [DeliveryMethod]^[National], [DeliveryMethod]^[International]" >
    

时序图

首先在文件头加入如下 javascript 文件:

    <script type="text/javascript" src="http://www.websequencediagrams.com/service.js"></script>
  • 同步/异步/返回消息(Synchronous/Asynchronous/Return Message)

    一般使用实心箭头->表示同步消息或调用,开箭头->>表示异步消息或调用,虚箭头-->-->>表示返回消息。

      <div class=wsd wsd_style="rose" ><pre>
      # This is a comment.            
      Alice->Bob: Filled arrow
      Alice->>Bob: Open arrow
      Bob-->Alice: Dotted line
      Bob-->>Alice: Dotted Line, open arrow
      </pre></div>
    

      # This is a comment.
      Alice->Bob: Filled arrow
      Alice->>Bob: Open arrow
      Bob—>Alice: Dotted line
      Bob—>>Alice: Dotted Line, open arrow
      

  • 定义参与者的顺序

    通过participant可以定义角色在时序图中的显示顺序,而不是按照缺省的参与者被使用顺序来显示。并且可以定义参与者的别名。

      <div class=wsd wsd_style="rose" ><pre>
      participant Bob
      participant Alice
      participant "I have a really\nlong name" as L
      #
      Alice->Bob: Authentication Request
      Bob->Alice: Authentication Response
      Bob->L: Log transaction
      </pre></div>
    

      participant Bob
      participant Alice
      participant “I have a really\nlong name” as L
      #
      Alice->Bob: Authentication Request
      Bob->Alice: Authentication Response
      Bob->L: Log transaction
      

  • 自关联消息(Self-Message)

    参与者可以发送一个消息给自己。你可以用\n将文字切分成多行。

      <div class=wsd wsd_style="rose" ><pre>
      Alice->Alice: This is a signal to self.\nIt also demonstrates \nmultiline \ntext.
      </pre></div>
    

      Alice->Alice: This is a signal to self.\nIt also demonstrates \nmultiline \ntext.
      

  • 分组消息

    通过alt/elseoptloop,将消息分组,组头显示分组定义的文本信息,end关键字用来结束一个分组。分组可以嵌套。

      <div class=wsd wsd_style="rose" ><pre>
      Alice->Bob: Authentication Request
      alt successful case
          Bob->Alice: Authentication Accepted
      else some kind of failure
          Bob->Alice: Authentication Failure
          opt
              loop 1000 times
                  Alice->Bob: DNS Attack
              end
          end
      else Another type of failure
          Bob->Alice: Please repeat
      end
      </pre></div>
    

      Alice->Bob: Authentication Request
      alt successful case
          Bob->Alice: Authentication Accepted
      else some kind of failure
          Bob->Alice: Authentication Failure
          opt
              loop 1000 times
                  Alice->Bob: DNS Attack
              end
          end
      else Another type of failure
          Bob->Alice: Please repeat
      end
      

  • 备注(Notes)

    使用note left ofnote right ofnote over分别定义左/右/中显示的备注,可以包含多行,end note用来结束该段note

      <div class=wsd wsd_style="rose" ><pre>
      participant Alice
      participant Bob
      #
      note left of Alice 
      This is displayed 
      left of Alice.
      end note
      note right of Alice: This is displayed right of Alice.
      note over Alice: This is displayed over Alice.
      note over Alice, Bob: This is displayed over Bob and Alice.
      </pre></div>
    

      participant Alice
      participant Bob
      #
      note left of Alice
      This is displayed
      left of Alice.
      end note
      note right of Alice: This is displayed right of Alice.
      note over Alice: This is displayed over Alice.
      note over Alice, Bob: This is displayed over Bob and Alice.
      

  • 生命线的激活和终止(Activation/Destruction)

    +来表示激活被发送者-表示终止发送者destroy关键字可以将销毁该参与者。

      <div class=wsd wsd_style="rose" ><pre>
      User->+A: DoWork
      A->+B: <<createRequest>>
      B->+C: DoWork
      C-->B: WorkDone
      destroy C
      B-->-A: RequestCreated
      A->User: Done
      </pre></div>
    

      User->+A: DoWork
      A->+B: <>
      B->+C: DoWork
      C—>B: WorkDone
      destroy C
      B—>-A: RequestCreated
      A->User: Done
      


其他工具

使用GitHub进行团队合作

原文: Team Collaboration With GitHub


GitHub已经成为的一切开放源码软件的基石。开发人员喜欢它,基于它进行协作,并不断通过它开发令人惊叹的项目。除了​​代码托管,GitHub的主要吸引力是使用它作为一个协作开发工具。在本教程中,让我们来看看一些最有用的GitHub的功能,特别是使团队工作更有效率,更高生产力,非常重要的,好玩的那些功能!


GitHub和软件合作

有一件事我觉得非常有用的是,可以将GitHub的维基集成到项目的源代码主线上。

本教程假定您已经熟悉Git – 开放源码的分布式版本控制系统,由Linux的创世人Linus Torvalds在2005年创造的。如果您需要修改或查找有关Git,请访问我们以前的截屏教程,和一些文章。此外,你应该已经有一个Github上的帐户,并做了一些基本的功能,如创建一个存储库,并推送到GitHub上。如果没有,可以参照更多以前的教程

在这个世界上的软件项目,不可避免的是,我们必须和一个团队一起工作来交付软件。在本教程中,我们将探索一些软件开发团队最常用的工具。这些工具包括:

  • 添加团队成员 – 组织和合作者
  • Pull请求 – 发送代码变更和合并
  • 问题跟踪 – Github上的错误记录
  • 分析 – 图形与网络
  • 项目管理TrelloPivotal Tracker
  • 持续集成Travis CI
  • 代码审查 – 代码行评论与URL查询
  • 文档记录 – Wiki与Hubot

更喜欢截屏操作视频?

如果你倾向于观看截屏操作视频,可以观看下面的截屏操作视频,而将本教程作为旁注。


工具一:增加团队成员

有两种常用的方法在GitHub上建立团队合作:

  • 组织 – 组织的所有者可以针对不同的代码仓库建立不同访问权限的团队。
  • 合作者 – 代码仓库的所有者可以为单个仓库增加具备只读或者读写权限的协作者。

组织

如果您监管几个团队,想为每个团队设置不同的权限级别,或者为不同的代码仓库增加不同的成员组织(Organizations)将是最好的选择。任何GitHub用户帐户已经可以创建免费的开源代码库的组织。要创建一个组织,只需浏览您的组织设置页面

要访问组织的团队页面,你可以简单地去页面http://github.com/organizations/[组织名称]/teams来查看,或者访问页面https://github.com/organizations/[组织名称]/teams/new来创建新的具备3种不同的权限级别的团队成员,如:

  1. Pull Only:提取和合并另一个库或本地副本。只读访问权限。

  2. Push和Pull:(1)以及更新远程代码仓库。读+写访问权限。

  3. Pull, Push和管理:(1), (2),计费,建立团队,以及取消组织帐户。读+写+管理员权限

合作者

合作者主要用于读写访问个人账号所拥有的代码仓库。你可以通过https://github.com/[用户名]/[代码仓库名称]/settings/collaboration来增加合作者(其他github个人账号)。

一旦做到这一点,每个合作者将会看到代码库页面的访问状态的变化。在拥有对代码库的写访问权限后,我们可以做一个git克隆,进行代码变更,用git拉取和归并远程存储库中的任何变化,并最终将本地的变化git推送到远程代码库:


工具二:Pull请求(Pull request)

Pull请求是一个非常棒的方式,通过fork一个新的代码库用来独立开发,并将变更贡献回原始代码库。在一天结束的时候,如果我们愿意,我们可以发送一个pull请求给代码库所有者,来合并我们的代码更改。Pull请求本身可以引起合作者之间的评论,包括代码质量,功能,甚至总体战略等。

现在让我们浏览一个pull请求的基本步骤。

发起一个Pull请求

GitHub有两种Pull请求方式:

  1. Fork & Pull 方式 – 用于在公共库中,我们没有推送(push)权限。
  2. 共享库方式 – 用于私有代码仓库,我们有推送(push)权限。这种情况下没有必要进行fork。

下面的工作流程是在两个用户(原始代码库拥有者,和fork代码库拥有者)之间的fork-pull方式:

  1. 进入你想贡献修改的GitHub代码库,单击“Fork”按​​钮来创建自己的Github帐户上的代码库克隆:

  2. 这将在自己的帐户上创建一个该代码库的复制:

  3. 选择 SSH URL,那样它会自动使用你自己的SSH密钥,而不用每次在git pull或者push时询问你的用户名和密码。下一步,我们将克隆一份代码库到本地计算机:

    $ git clone [ssh-url] [folder-name]
    $ cd [folder-name]
    
  4. 一般情况下,每一个新的功能,我们将创建一个新的Git分支。这是一个很好的做法,因为在未来,如果经过一番讨论后我们需要进一步更新分支,Pull请求将被自动更新。让我们创建一个新的分支做一个非常简单的变化修改的readme.md文件:

    $ git checkout -b [new-feature]
    
  5. 在为这个新功能增加文件后,我们只需要将修改提交到这个新分支上,然后切换回master分支:

    $ git add .
    $ git commit -m "information added in readme"
    $ git checkout master
    
  6. 在这里,我们需要将新分支推送到远程代码仓库里。首先,我们需要检查这个新功能的分支名称以及其在远程仓库的别名,然后我们用git push [git-remote-alias] [branch-name]推送这个变更。

    $ git branch
    * master
    readme
    $ git remote -v
    origin  git@github.com:[forked-repo-owner-username]/[repo-name].git (fetch)
    origin  git@github.com:[forked-repo-owner-username]/[repo-name].git (push)
    $ git push origin readme
    
  7. 进入我们fork的代码库的GitHub页面,选择为这个新功能建立的分支,然后点击Pull Request按钮:

  8. 提交Pull请求后,页面将直接跳转到原始库的Pull请求页面,我们将看到我们提交的Pull请求,作为一个新的问题,以及作为一个新的pull请求。

  9. 在经过讨论后,fork的代码库的作者可能想为这个新功能增加一些新的改动。在这种场景下,我们需要在本地计算机上checkout这个同样的分支,修改,提交,并推送回GitHub。当我们再次访问原代码库的pull请求页面的时候,会发现上次提交的Pull请求已经自动更新了。

合并一个Pull请求

如果你是原始代码库的所有者,你将有两种方式来合并收到的Pull请求。

  1. 直接在GitHub上合并:如果我们想直接在GitHub上进行合并,必须确保没有冲突。原始库的所有者可以通过简单地点击Merge Pull Request按钮来进行合并:

  2. 在本地计算机上进行合并:另外一种情况,合并的时候可能会遇到冲突,点击上部的Info图标,GitHub有非常清晰的指导,怎么从贡献者的分支上下拉代码变更到本地,合并并解决冲突。

在软件开发团队中有很多不同的代码分支模型。这里有两种非常常用的工作流程模型:

至于采用何种分支模型,取决于团队,项目,以及当时的状态。


工具三:错误跟踪

在GitHub中,缺陷跟踪的中心是问题列表(Issues)。虽然问题列表主要是为了跟踪缺陷,但我们经常会用以下面的方式:

  • 缺陷:软件中显然坏了的行为,需要进行修正的地方。
  • 功能:需要实现的很酷很棒的新点子。
  • 待完成清单:待完成的检查清单。

让我们来探讨问题列表的一下特点:

  1. 标签:具有不同颜色的类别,用来帮助过滤问题。

  2. 里程碑:附加在每一个问题上的日期分类,可用于确定哪些问题需要在下一个版本解决。 此外,由于每个问题都定有里程碑,每当一个问题解决,它会自动更新进度条。

  3. 搜索:搜索时能自动列出匹配的问题列表和里程碑。

  4. 分配:每个问题都能分配一个人负责进行解决,同时这也能让我们知道目前我们需要工作在什么上面。

  5. 自动关闭:包含Fixes/Fixed/Close/Closes/Closed #问题编号的提交记录,将自动关闭该问题。

    $ git add .
    $ git commit -m "corrected url. fixes #2"
    $ git push origin master
    

    很显然,我们能将任务清单与代码提交紧密地耦合在一起。

  6. 提及或者引用:任何人在评论的时候在消息文本中包含#[问题编号],将自动生成该问题的链接,使得在讨论的过程中能非常容易地提及相关的问题。


工具四:分析

有两个工具-图形和网络,让我们能洞察存储库的变化。Github图 提供了代码库的合作者,以及代码提交的直观展现,而Github网络可视化直观地展现了每一个贡献者和他们在所有分支上的代码提交。这些分析和图形非常强大,尤其是当在团队中工作。

图(Graphs)

图提供了详细的分析,包括:

  • 贡献者:有哪些代码提交者?他们增加或者删除了多少代码行?
  • 代码提交活动:在过去的一年中,这些代码提交主要发生在哪些周?
  • 代码频率:在整个项目的生命周期的不同阶段,提交了多少代码行?
  • 记录卡:代码提交通常发生在每一天的什么时候?

网络(Network)

GitHub网络(Network)是一个非常强大的工具,让我们能看到每一个贡献者的代码提交,以及这些提交与其他的提交有什么关联。当我们作为一个整体观看这个网络的可视化展现时,我们能看到每一个库,每一个分支,和每一个提交,


工具五:项目管理

GitHub上的问题列表可以定义问题和里程碑,具有一定的项目管理能力。因为其他的某些功能或现有的工作流程,有些团队可能会更倾向于另外的工具。在本节中,我们将看到我们如何连接Github与其他流行的项目管理工具 – TrelloPivotal Tracker。使用GitHub的服务钩子(hooks,我们可以将代码提交,问题和许多其他活动自动更新到任务中。对于任何软件开发团队,这种自动化的帮助,不仅节省了时间,而且还可以提高更新的。

GitHub和Trello

Trello提供了一直简单而直观的方式管理任务。使用敏捷开发的方式, Trello任务卡能模拟简单,可视化的虚拟任务看版。作为实例, 当GitHub代码库收到一个Pull请求的时候,我们将利用GitHub的钩子(Hooks)服务,自动在Trello里生成一个任务卡。 让我们来看看实现这个功能的具体步骤:

  1. 首先注册一个Trello帐号,并建立一个新的Trello看版(Board)。

  2. 访问GitHub repository > Settings > Service Hooks > Trello

  3. 通过Install Note #1描述的方法获得认证用的令牌(Token)。

  4. 通过Install Note #2给的链接获得json格式的任务列表(list) id。BOARDID是我们访问网站看版的URL中的一部分https://trello.com/board/[BOARD-NAME]/[BOARDID]

  5. 回到GitHub的钩子服务,输入我们得到的list idtoken,激活这个hook,可以通过Test Hook按钮来测试每次收到新的Pull请求时,是否自动更新看版内容。

  6. 当下次收到新的Pull请求时,Trello看版就会自动生成一个Pull请求任务卡。

GitHub和Pivotal Tracker

Pivotal Tracker是另外一个轻量级的项目管理工具,通过基于故事(story)的计划,让组员能对任何变化和进度进行回应,非常容易进行协作开发。 基于项目当前的进度,能生成可视化的图表来分析团队的开发速度,迭代burn-up图,以及当前发布的burn-down图。在下面的例子中, 我们通过关联一个GitHub代码提交(commit)到故事(story),自动地交付一个故事。

  1. Pivotal Tracker上新建一个项目,并生成一个需要交付的故事(story)。

  2. 访问Profile > API Token,复制给定的API令牌(token)。

  3. 返回到GitHub页面访问repository > Settings > Service Hooks > Pivotal Tracker,粘贴刚才复制的token,激活,并保存设置。 这样我们就能够在提交代码的时候自动交付对应的故事(story)。

  4. 当我们最终提交代码修订的时候,按照格式git commit -m "message [delivers #tracker_id]" 将故事的id添加到提交记录里

    $ git add .
    $ git commit -m "Github and Pivotal Tracker hooks implemented [delivers #43903595]"
    $ git push
    
  5. 现在,返回到Pivotal Tracker页面,我们将发现这个指定的故事被自动的发布出去了,附着对应的GitHub上代码提交的链接。

通过TrelloPivotal Tracker实例,非常清楚的一点是我们能紧密地将我们的任务和代码提交绑定在一起。 当作为一个团队进行工作的时候,这将节省大量的时间,并且提高工作记录的精确度。非常好的消息是, 如果你已经使用了其他项目管理工具,例如AsanaBasecamp, 或者其他的,你能够用类似的方式添加hook。如果没有你目前正在使用的项目管理工具的hook, 自力更生自己做一个!


工具六:持续集成

对于团队软件开发来说,持续集成(CI)是一个非常重要的部分。CI确保当开发人员提交代码改动的时候,将触发自动的构建(build),包括测试,用来快速地检测软件集成的错误。这将毫无疑问地减少集成过程中的错误,提高快速开发迭代的效率。在下面的例子里,我们将看到如何与GitHub一起使用Travis CI,自动检测错误,并且在所有测试通过后进行代码合并。

设置 Travis CI

这里我们使用基于node.js服务器,基于[grunt.js][http://gruntjs.com/]作为构建工具的”Hello-World”应用,来设置Travis CI项目。下面是项目中的文件:

  1. hello.js文件是nodejs项目。我们有目的地漏写了一个分号,为了让这个文件不能通过grunt构建工具的lint(静态代码检测工具):

    var http = require('http');
    http.createServer(function (req, res) {
        res.writeHead(200, {'Content-Type': 'text/plain'});
        res.end('Hello World in Node!\n') // 这里没有分号,将不会通过linting
    }).listen(1337, '127.0.0.1');
    console.log('Server running at http://127.0.0.1:1337/');
    
  2. package.json定义依赖的包:

    {
      "name": "hello-team",
      "description": "A demo for github and travis ci for team collaboration",
      "author": "name <email@email.com>",
      "version": "0.0.1",
      "devDependencies": {
        "grunt": "~0.3.17"
      },
      "scripts": {
        "test": "grunt travis --verbose"
      }
    }
    
  3. 为了简化起见,gruntjs构建工具的配置文件仅仅包含一个任务(linting):

    module.exports = function(grunt) {
      grunt.initConfig({
        lint: {
          files: ['hello.js']
        }
      });
      grunt.registerTask('default', 'lint');
      grunt.registerTask('travis', 'lint');
    };
    
  4. .travis.yml是Travis的配置文件,确保Travis运行我们的测试:

    language: node_js
    node_js:
      - 0.8
    
  5. 接着,用GitHub帐号登录到Travis,在repository选项卡打开repository hook:

  6. 如果上述步骤还不能触发构建,我们将不得不手工配置hook,在Travis的profile栏复制token。

  7. 返回到GitHub代码库,使用复制的token设置Travis Hook:

  8. 第一次,我们必须手工做一次git push来触发Travis构建,如果一切ok,我们可以访问http://travis-ci.org/[用户名]/[repo名]查看构建的结果。

Travis CI 和 Pull 请求(Pull request)

以前没有持续集成的Pull请求流程,步骤大概是(1)提交pull请求(2)合并(3)测试来看是否通过或者失败。带有持续集成hook的Pull请求流程将反转(2)和(3)步骤,Travis CI将向我们汇报每一个Pull请求的持续集成结果,让我们能够知道这个Pull请求是否足够好,并快速作出判断是否合并进主线。下面我们来看这是怎样做到的:

  1. 提交一个附带通过构建结果的Pull请求。Travis将做所有的一切,让我们在合并前就能知道这个合并是否足够好。

  2. 如果这个Pull请求使得构建失败,Travis同样会警告你:

  3. 如果我们点击红色的警告链接,浏览器将跳转到Travis页面,显示这次构建的详细信息。

因为自动的构建和及时地通知,Travis CI对团队来说非常有帮助,它能极大地缩短我们更正错误的周期。如果你使用另外一个非常有名的持续集成工具Jenkins,你能用相似的步骤设置hook服务。


工具七:代码评审

对于每个提交(commit),GitHub有个干净的接口用来进行评论,甚至是对某行代码进行评论。在进行逐行代码评审的时候,针对单行代码提出评论和问题的功能就显得非常重要了。打开提交(commit)界面的顶部的检查框,就能显示行内评论。

下面探讨一些帮助我们进行代码评审,能快速显示不同提交之间差异的URL模式:

  1. 对比branch/tags/SHA1:使用URL模式https://github.com/[username]/[repo-name]/compare/[starting-SHA1]...[ending-SHA1]。 可以用分支或者标签名代替SHA1

  2. 去除空格进行对比:增加?w=1到对比的URL尾部。

  3. Diff:增加.diff到URL的后面能得到git diff输出的纯文本信息。在写脚本的时候,这个功能非常有用。

  4. Patch:增加.patch到URL的后面能得到电子邮件补丁提交格式git diff输出的信息。

  5. 行链接:在查看文件时,点击任何行号,GitHub将会在URL后面增加一个#行号,并且将该行的背景颜色置成黄色。这能干净利落地标识代码文件的某一行。我们同样能通过增加#开始行号-结束行号来指定一个范围。这里是行链接行范围链接的例子。


工具八:文档

在这段,我们将探讨两种文档方法:

  1. 正式文档:使用GitHub Wiki生成正式的项目文档。

  2. 非正式文档:使用GitHub Hubot来归档团队内部的讨论,以及与Hubot互动而自动获得的非常有趣的信息。

  3. 提及,快捷键和表情符号

GitHub维基(Wiki)

每个GitHub代码库都可以生成一个维基,这样非常方便地将代码和文档存放在同一个存储库中。要创建维基,访问主标题的维基选项卡,并设置创建页面的信息。其实维基也有自己的版本,并可以将数据复制到本地机器进行更新,甚至是离线访问。

有一件事我觉得非常有用的是可以将GitHub的维基整合到源代码中,这样我就不必维护两个独立的Git项目了。要做到这一点,我将Wiki作为git子模块增加到主分支上。如果您使用的是Travis CI或任何其他CI,必须确保构建工具会忽略wiki的子模块。在Travis的CI文件.travis.yml中,添加以下内容:

git:
  submodules: false

接着增加一个git子模块wiki到主代码库中:

$ git submodule add git@github.com:[username]/[repo-name].wiki.git
Cloning into 'hello-team.wiki'...
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 6 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (6/6), done.
$ git add .
$ git commit -m "added wiki as submodule"
$ git push origin master

现在,维基就作为一个子模块显示在代码库项目中。

GitHub Hubot

Hubot,总之,可以极大地增添了不少的乐趣记录,并通知小组讨论重要的提交。

Hubot是一个简单的聊天机器人,可以检索信息,或提供通知,每当GitHub有代码提交,问题,或活动时。在一个旨在减少,甚至完全消除会议的一个团队中,Hubot拥有所有团队成员的聊天接口,帮助您记录着每一个讨论。这当然促进灵活的工作时序,因为团队没有必要同时出席讨论。警告:Hubot是非常上瘾的!

有了这个,让我们开始在Heroku上设置Hubot,拥有Campfire聊天接口的聊天机器人![Heroku][]和Campfire,都有免费的版本供大家开始尝试。

  1. 我们将使用GitHub出品的支持Campfire的Hubot。如果你愿意,也可以使用其他如Skype,IRC,GTalk等聊天适配器

  2. 建立一个仅为HubotCampfire账号,这个账号将新建一个房间,并邀请其他人加入。

  3. 根据Hubot维基上给的指示,部署Hubot到Heroku上。如果Heroku的应用程序的URL返回了一个Cannot GET /,别惊慌,因为默认情况下不会得到任何返回

  4. 从Hubot Campfire账号上,邀请你自己的账号,现在,登录你自己的Campfire账号,然后执行Hubot help,你将得到所有Hubot支持的命令。

  5. 尝试几次,例如Hubot ship it或者Hubot map me CERN

  6. 下一步,我们将增加一个Hubot脚本,这里已经有大量的脚本,附带命令说明

  7. 作为实例,我们将增加一个github提交脚本,以至于每次有一个新的提交,Hubot将在聊天室通知大家。将文件github-commits.coffee放置到scripts目录。

  8. 更新package.json文件,根据每个脚本文件头的指示,加入新的依赖包。

  9. 使用下面命令再一次发布代码到Heroku:git push heroku master

  10. 浏览GitHub代码库,我们希望代码提交通知能显示在聊天室,在代码库设置下增加一个web hook,对github-commits脚本,webhook将是[HUBOT_URL]:[PORT]/hubot/gh-commits?room=[ROOM_ID]

  11. 下一次当代码库有新的代码提交,Hubot将在聊天室如此说:

检查其他Github相关的Hubot的脚本,或者如果您想自己写一个脚本,这里有一个很酷的教程!总之,Hubot可以极大地增添很多乐趣在文档记录、通知小组讨论代码库发生的重要提交,问题和活动。试试看吧!

关于和团队一起使用GitHub,最后要说明的是,这里有一些提高生产力的技巧:

  1. 提及(Mentions) – 在任何文本区域中,我们可以通过@用户名提到另外一个GitHub用户,并且该用户将得到通知。

  2. 快捷键 – 按SHIFT + ?可以查看Github上任何页面上的快捷键。

  3. 表情符号 – 通过使用表情符号,Github上的文本区域还支持插入的图标。来吧,与队友一起工作时有点情趣!


GitHub上非软件项目的合作

我们大多数人会认为使用Github只能为软件项目。毕竟,Github产生就是为了社交编程。但是,也有一些很酷的使用Github的库被用于非编码项目,和他们的合作和讨论同样非常棒。因为这些项目是开源的,任何人都可以作出贡献,这是快速修复错误,容易报告错误,与志同道合的人有效的合作。只是为了好玩,这里是其中的一些:

你能想象GitHub开发团队怎么认为这些项目?

“我们挖掘像这样一样使用GitHub的乐趣!”


更多的资源


更多合作的乐趣!

那些都是在GitHub上积攒的协作化工具。大部分都是作为分析工具,或者用于和团队工作时节省时间的自动化工具。你有更多GitHub团队合作的技巧吗?让我们一起分享!

介绍 Android DropBoxManager Service

什么是 DropBoxManager ?

Enqueues chunks of data (from various sources – application crashes, kernel log records, etc.). The queue is size bounded and will drop old data if the enqueued data exceeds the maximum size. You can think of this as a persistent, system-wide, blob-oriented “logcat”.

DropBoxManager 是 Android 在 Froyo(API level 8) 引入的用来持续化存储系统数据的机制, 主要用于记录 Android 运行过程中, 内核, 系统进程, 用户进程等出现严重问题时的 log, 可以认为这是一个可持续存储的系统级别的 logcat.

我们可以通过用参数 DROPBOX_SERVICE 调用 getSystemService(String) 来获得这个服务, 并查询出所有存储在 DropBoxManager 里的系统错误记录.

Android 缺省能记录哪些系统错误 ?

我没有在官方的网站上找到关于哪些系统错误会被记录到 DropBoxManager 中的文档, 但我们可以查看源代码来找到相关信息. 从源代码中可以查找到记录到 DropBoxManager 中各种 tag(类似于 logcat 的 tag).

crash (应用程序强制关闭, Force Close)

当Java层遇到未被 catch 的例外时, ActivityManagerService 会记录一次 crash 到 DropBoxManager中, 并弹出 Force Close 对话框提示用户.

ActivityManagerServicelink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void handleApplicationCrash(IBinder app, ApplicationErrorReport.CrashInfo crashInfo) {
    ProcessRecord r = findAppProcess(app, "Crash");
    final String processName = app == null ? "system_server"
            : (r == null ? "unknown" : r.processName);

    EventLog.writeEvent(EventLogTags.AM_CRASH, Binder.getCallingPid(),
            UserHandle.getUserId(Binder.getCallingUid()), processName,
            r == null ? -1 : r.info.flags,
            crashInfo.exceptionClassName,
            crashInfo.exceptionMessage,
            crashInfo.throwFileName,
            crashInfo.throwLineNumber);

    addErrorToDropBox("crash", r, processName, null, null, null, null, null, crashInfo);

    crashApplication(r, crashInfo);
}

anr (应用程序没响应, Application Not Responding, ANR)

当应用程序的主线程(UI线程)长时间未能得到响应时, ActivityManagerService 会记录一次 anr 到 DropBoxManager中, 并弹出 Application Not Responding 对话框提示用户.

ActivityManagerServicelink
1
2
3
4
5
6
7
final void appNotResponding(ProcessRecord app, ActivityRecord activity,
        ActivityRecord parent, boolean aboveSystem, final String annotation) {
    //......
    addErrorToDropBox("anr", app, app.processName, activity, parent, annotation,
            cpuInfo, tracesFile, null);
    //......
}

wtf (What a Terrible Failure)

‘android.util.Log’ 类提供了静态的 wtf 函数, 应用程序可以在代码中用来主动报告一个不应当发生的情况. 依赖于系统设置, 这个函数会通过 ActivityManagerService 增加一个 wtf 记录到 DropBoxManager中, 并/或终止当前应用程序进程.

ActivityManagerServicelink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public boolean handleApplicationWtf(IBinder app, String tag,
        ApplicationErrorReport.CrashInfo crashInfo) {
    ProcessRecord r = findAppProcess(app, "WTF");
    final String processName = app == null ? "system_server"
            : (r == null ? "unknown" : r.processName);

    EventLog.writeEvent(EventLogTags.AM_WTF,
            UserHandle.getUserId(Binder.getCallingUid()), Binder.getCallingPid(),
            processName,
            r == null ? -1 : r.info.flags,
            tag, crashInfo.exceptionMessage);

    addErrorToDropBox("wtf", r, processName, null, null, tag, null, null, crashInfo);

    if (r != null && r.pid != Process.myPid() &&
            Settings.Global.getInt(mContext.getContentResolver(),
                    Settings.Global.WTF_IS_FATAL, 0) != 0) {
        crashApplication(r, crashInfo);
        return true;
    } else {
        return false;
    }
}

strict_mode (StrictMode Violation)

StrictMode (严格模式), 顾名思义, 就是在比正常模式检测得更严格, 通常用来监测不应当在主线程执行的网络, 文件等操作. 任何 StrictMode 违例都会被 ActivityManagerService 在 DropBoxManager 中记录为一次 strict_mode 违例.

ActivityManagerServicelink
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
public void handleApplicationStrictModeViolation(
        IBinder app,
        int violationMask,
        StrictMode.ViolationInfo info) {
    ProcessRecord r = findAppProcess(app, "StrictMode");
    if (r == null) {
        return;
    }

    if ((violationMask & StrictMode.PENALTY_DROPBOX) != 0) {
        Integer stackFingerprint = info.hashCode();
        boolean logIt = true;
        synchronized (mAlreadyLoggedViolatedStacks) {
            if (mAlreadyLoggedViolatedStacks.contains(stackFingerprint)) {
                logIt = false;
                // TODO: sub-sample into EventLog for these, with
                // the info.durationMillis?  Then we'd get
                // the relative pain numbers, without logging all
                // the stack traces repeatedly.  We'd want to do
                // likewise in the client code, which also does
                // dup suppression, before the Binder call.
            } else {
                if (mAlreadyLoggedViolatedStacks.size() >= MAX_DUP_SUPPRESSED_STACKS) {
                    mAlreadyLoggedViolatedStacks.clear();
                }
                mAlreadyLoggedViolatedStacks.add(stackFingerprint);
            }
        }
        if (logIt) {
            logStrictModeViolationToDropBox(r, info);
        }
    }
    //......
}

lowmem (低内存)

在内存不足的时候, Android 会终止后台应用程序来释放内存, 但如果没有后台应用程序可被释放时, ActivityManagerService 就会在 DropBoxManager 中记录一次 lowmem.

ActivityManagerServicelink
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    public void handleMessage(Message msg) {
        switch (msg.what) {
        //...
        case REPORT_MEM_USAGE: {
            //......
            Thread thread = new Thread() {
                @Override public void run() {
                    StringBuilder dropBuilder = new StringBuilder(1024);
                    StringBuilder logBuilder = new StringBuilder(1024);
                    //......
                    addErrorToDropBox("lowmem", null, "system_server", null,
                            null, tag.toString(), dropBuilder.toString(), null, null);
                    //......
                }
            };
            thread.start();
            break;
        }
        //......
    }

watchdog

如果 WatchDog 监测到系统进程(system_server)出现问题, 会增加一条 watchdog 记录到 DropBoxManager 中, 并终止系统进程的执行.

Watchdoglink
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
/** This class calls its monitor every minute. Killing this process if they don't return **/
public class Watchdog extends Thread {
    //......
    @Override
    public void run() {
        boolean waitedHalf = false;
        while (true) {
            //......

            // If we got here, that means that the system is most likely hung.
            // First collect stack traces from all threads of the system process.
            // Then kill this process so that the system will restart.

            //......

            // Try to add the error to the dropbox, but assuming that the ActivityManager
            // itself may be deadlocked.  (which has happened, causing this statement to
            // deadlock and the watchdog as a whole to be ineffective)
            Thread dropboxThread = new Thread("watchdogWriteToDropbox") {
                    public void run() {
                        mActivity.addErrorToDropBox(
                                "watchdog", null, "system_server", null, null,
                                name, null, stack, null);
                    }
                };
            dropboxThread.start();
            try {
                dropboxThread.join(2000);  // wait up to 2 seconds for it to return.
            } catch (InterruptedException ignored) {}

            //......
        }
    }

    //......
}

netstats_error

NetworkStatsService 负责收集并持久化存储网络状态的统计数据, 当遇到明显的网络状态错误时, 它会增加一条 netstats_error 记录到 DropBoxManager.

BATTERY_DISCHARGE_INFO

BatteryService 负责检测充电状态, 并更新手机电池信息. 当遇到明显的 discharge 事件, 它会增加一条 BATTERY_DISCHARGE_INFO 记录到 DropBoxManager.

系统服务(System Serve)启动完成后的检测

系统服务(System Serve)启动完成后会进行一系列自检, 包括:

  • 开机

    每次开机都会增加一条 SYSTEM_BOOT 记录.

  • System Server 重启

    如果系统服务(System Server)不是开机后的第一次启动, 会增加一条 SYSTEM_RESTART 记录, 正常情况下系统服务(System Server)在一次开机中只会启动一次, 启动第二次就意味着 bug.

  • Kernel Panic (内核错误)

    发生 Kernel Panic 时, Kernel 会记录一些 log 信息到文件系统, 因为 Kernel 已经挂掉了, 当然这时不可能有其他机会来记录错误信息了. 唯一能检测 Kernel Panic 的办法就是在手机启动后检查这些 log 文件是否存在, 如果存在则意味着上一次手机是因为 Kernel Panic 而宕机, 并记录这些日志到 DropBoxManager 中. DropBoxManager 记录 TAG 名称和对应的文件名分别是:

    • SYSTEM_LAST_KMSG, 如果 /proc/last_kmsg 存在.
    • APANIC_CONSOLE, 如果 /data/dontpanic/apanic_console 存在.
    • APANIC_THREADS, 如果 /data/dontpanic/apanic_threads 存在.
  • 系统恢复(System Recovery)

    通过检测文件 /cache/recovery/log 是否存在来检测设备是否因为系统恢复而重启, 并增加一条 SYSTEM_RECOVERY_LOG 记录到 DropBoxManager 中.

System Server BootReceiverlink
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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
private void logBootEvents(Context ctx) throws IOException {
    final DropBoxManager db = (DropBoxManager) ctx.getSystemService(Context.DROPBOX_SERVICE);
    final SharedPreferences prefs = ctx.getSharedPreferences("log_files", Context.MODE_PRIVATE);
    final String headers = new StringBuilder(512)
        .append("Build: ").append(Build.FINGERPRINT).append("\n")
        .append("Hardware: ").append(Build.BOARD).append("\n")
        .append("Revision: ")
        .append(SystemProperties.get("ro.revision", "")).append("\n")
        .append("Bootloader: ").append(Build.BOOTLOADER).append("\n")
        .append("Radio: ").append(Build.RADIO).append("\n")
        .append("Kernel: ")
        .append(FileUtils.readTextFile(new File("/proc/version"), 1024, "...\n"))
        .append("\n").toString();

    String recovery = RecoverySystem.handleAftermath();
    if (recovery != null && db != null) {
        db.addText("SYSTEM_RECOVERY_LOG", headers + recovery);
    }

    if (SystemProperties.getLong("ro.runtime.firstboot", 0) == 0) {
        String now = Long.toString(System.currentTimeMillis());
        SystemProperties.set("ro.runtime.firstboot", now);
        if (db != null) db.addText("SYSTEM_BOOT", headers);

        // Negative sizes mean to take the *tail* of the file (see FileUtils.readTextFile())
        addFileToDropBox(db, prefs, headers, "/proc/last_kmsg",
                -LOG_SIZE, "SYSTEM_LAST_KMSG");
        addFileToDropBox(db, prefs, headers, "/cache/recovery/log",
                -LOG_SIZE, "SYSTEM_RECOVERY_LOG");
        addFileToDropBox(db, prefs, headers, "/data/dontpanic/apanic_console",
                -LOG_SIZE, "APANIC_CONSOLE");
        addFileToDropBox(db, prefs, headers, "/data/dontpanic/apanic_threads",
                -LOG_SIZE, "APANIC_THREADS");
    } else {
        if (db != null) db.addText("SYSTEM_RESTART", headers);
    }

    // Scan existing tombstones (in case any new ones appeared)
    File[] tombstoneFiles = TOMBSTONE_DIR.listFiles();
    for (int i = 0; tombstoneFiles != null && i < tombstoneFiles.length; i++) {
        addFileToDropBox(db, prefs, headers, tombstoneFiles[i].getPath(),
                LOG_SIZE, "SYSTEM_TOMBSTONE");
    }

    // Start watching for new tombstone files; will record them as they occur.
    // This gets registered with the singleton file observer thread.
    sTombstoneObserver = new FileObserver(TOMBSTONE_DIR.getPath(), FileObserver.CLOSE_WRITE) {
        @Override
        public void onEvent(int event, String path) {
            try {
                String filename = new File(TOMBSTONE_DIR, path).getPath();
                addFileToDropBox(db, prefs, headers, filename, LOG_SIZE, "SYSTEM_TOMBSTONE");
            } catch (IOException e) {
                Slog.e(TAG, "Can't log tombstone", e);
            }
        }
    };

    sTombstoneObserver.startWatching();
}

SYSTEM_TOMBSTONE (Native 进程的崩溃)

Tombstone 是 Android 用来记录 native 进程崩溃的 core dump 日志, 系统服务在启动完成后会增加一个 Observer 来侦测 tombstone 日志文件的变化, 每当生成新的 tombstone 文件, 就会增加一条 SYSTEM_TOMBSTONE 记录到 DropBoxManager 中.

DropBoxManager 如何存储记录数据 ?

DropBoxManager 使用的是文件存储, 所有的记录都存储在 /data/system/dropbox 目录中, 一条记录就是一个文件, 当文本文件的尺寸超过文件系统的最小区块尺寸后, DropBoxManager 还会自动压缩该文件, 通常文件名以调用 DropBoxManager 的 TAG 参数开头.

System Server BootReceiverlink
1
2
3
4
5
6
7
8
9
10
$ adb shell ls -l /data/system/dropbox
-rw------- system   system        258 2012-11-21 11:36 SYSTEM_RESTART@1353469017940.txt
-rw------- system   system         39 2012-11-21 11:40 event_data@1353469222884.txt
-rw------- system   system         39 2012-11-21 12:10 event_data@1353471022975.txt
-rw------- system   system         34 2012-11-21 18:10 event_log@1353492624170.txt
-rw------- system   system         34 2012-11-21 18:40 event_log@1353494424296.txt
-rw------- system   system         34 2012-11-22 10:10 event_log@1353550227432.txt
-rw------- system   system       1528 2012-11-21 22:54 system_app_crash@1353509648395.txt
-rw------- system   system       1877 2012-11-21 11:36 system_app_strictmode@1353469014395.txt
-rw------- system   system       3724 2012-11-21 11:36 system_app_strictmode@1353469014924.txt.gz

如何利用 DropBoxManager ?

  • 利用 DropBoxManager 来记录需要持久化存储的错误日志信息

    DropBoxManager 提供了 logcat 之外的另外一种错误日志记录机制, 程序可以在出错的时候自动将相关信息记录到 DropBoxManager 中. 相对于 logcat, DropBoxManager 更适合于程序的自动抓错, 避免人为因素而产生的错误遗漏. 并且 DropBoxManager 是 Android 系统的公开服务, 相对于很多私有实现, 出现兼容性问题的几率会大大降低.

  • 错误自动上报

    可以将 DropBoxManager 和设备的 BugReport 结合起来, 实现自动上报错误到服务器. 每当生成新的记录, DropBoxManager 就会广播一个 DropBoxManager.ACTION_DROPBOX_ENTRY_ADDED Intent, 设备的 BugReport 服务需要侦听这个 Intent, 然后触发错误的自动上报.

参考

Yeoman: 一个新的 Javascript 构建工具

Grunt 是一个非常优秀的 Javascript 构建工具, 虽然它的 Build-in 的任务非常有限, 但是它提供了一套非常灵活的插件机制, 可以进行任务扩展. 目前在官网上的第三方任务插件数目已经达到 160+, 并且在持续增长中… 对于通用的功能基本上都能找到对应的任务插件, 当然你也可以自己写扩展任务来满足特殊的构建需求.

我通常会直接使用 bbb, 因为 bbb 已经收集齐了我想用到的任务插件, 非常顺手.

对我而言 bbb 已经足够用了, 尽管这样, 在第一次尝试 Yeoman 之后, 我还是忍不住想向大家推荐这个新的工具.

Yeoman is a robust and opinionated client-side stack, comprised of tools and frameworks that can help developers quickly build beautiful web applications. We take care of providing everything needed to get started without any of the normal headaches associated with a manual setup.
With a modular architecture that can scale out of the box, we leverage the success and lessons learned from several open-source communities to ensure the stack developers use is as intelligent as possible.
As firm believers in good documentation and well thought out build processes, Yeoman includes support for linting, testing, minification and much more, so developers can focus on solutions rather than worrying about the little things.
Yeoman is fast, performant and is optimized to work best in modern browsers.

想对比其他 Javascript 构建工具, Yeoman 具有下面非常吸引人的特点:

根据定制模板快速生成程序框架

Yeoman 能根据你选择的框架初始化生成程序框架, 目前已经支持大量的程序模板, 看趋势似乎是想把所有框架的初始化模板都包含进去:

Yeoman:
  generator
  controller

Angular:
  angular:service
  angular:all
  angular:directive
  angular:view
  angular:route
  angular:filter
  angular:controller
  angular:app

Testacular:
  testacular:app

Quickstart:
  quickstart:all

Bbb:
  bbb:all

Ember:
  ember:controller
  ember:all
  ember:view
  ember:model
  ember:app

Chromeapp:
  chromeapp:all

Ember-starter:
  ember-starter:all

Backbone:
  backbone:model
  backbone:view
  backbone:router
  backbone:collection
  backbone:app
  backbone:all

虽然目前我只用到了 Bbb 模板, 但还是得赞一下其模板的齐全.

前端 Javascript 包管理

在目前, 我们都是手工增加和管理前端应用中用到的 Javascript 库, 例如说 jQuery, 每次当 jQuery 发布一个新版本, 我们都不得不手工地从官网上下载最新的发布文件, 然后将这个文件复制到自己的项目中.

Yemoman 可以让我们省掉这种麻烦的手工操作. 它集成了前端 Javascript 包管理应用 Bower, 我们只需要简单地运行 yeoman install jquery, 或者 yeoman update jquery 就能安装或者更新 jQuery 库文件.

监测文件更新并自动重新加载应用

每次当源文件有了修改, 无论是 HTML, CSS, Markdown 文件, 还是脚本文件, 或者其他需要预处理的 CoffeeScript, Sass 等文件, Yeoman 带来的活动加载监测进程会自动重新加载你的页面. 在调试的过程中, 这个功能的确能帮你省不少手工刷新的麻烦:–).

使用 yeoman server 就能运行一个本地 Web 服务器, 监测文件的变化并实时重新加载应用.

强大的构建系统

Yeoman 也是基于 grunt, 并且额外开发了一些高度定制的构建任务. 对我来说比较新鲜的构建任务是图片的优化, 使用 OptiPNG 和 JPEGTran 优化图片来减少图片的下载时间, 初步测试的结果是能减少大约 30% 图片大小, 这在以前的项目中还没尝试过.


最后, 下面是使用 Yeoman 重构后的地址本应用的演示源码.

在Backbone项目中使用backbone.layoutmanager来组织页面布局

bbbinit 命令生成的初始化模板工程中包含有 backbone.layoutmanager 插件, 该插件提供了一种页面的结构化组织方式, 将 Backbone Views 组装成页面布局 Layout.

backbone.layoutmanager 主要的目的是提供一种规则来管理 Backbone.View 的渲染, 程序员只需要遵循这套规则, 就能简化页面渲染的实现. backbone.layoutmanager 主页已经很详细地介绍了使用方法, 这里就不再赘述. 下面是个人感觉在学习使用过程中感觉应当注意的几点:

完全受管理的 render

Backbone.LayoutView 仅仅是一个将 manage 属性设置成 trueBackbone.View.

backbone.layoutmanager V0.6.6
1
2
3
Backbone.LayoutView = Backbone.View.extend({
  manage: true
});

只有当 manage 属性被设置为 true, LayoutManager 才会接管 Backbone.View 的渲染.

数据和模板

视图的渲染需要 templateserialize 属性, 分别对应HTML模板和数据模型. LayoutManager 缺省使用 underscoretemplate 函数进行页面的渲染.

嵌套视图

可以通过 setView, setViews, insertView, insertViews 等函数在视图中嵌套多个其他的子视图.

BeforeRender 和 AfterRender

render 完全处在 LayoutManager 的管理下, 因此, 如果你想在每次视图渲染前后做一些特殊的处理, 必须定义 beforeRenderafterRender 函数. 由于 LayoutManager 内部会在渲染视图前移除所有附加模式的子视图, 因此, 通常在 beforeRender 函数中调用 setViewinsertView 来增加和设置子视图.

如果你不想在每次渲染时移除子视图, 自己控制子视图的增删, 可以设置子视图的 keep 属性为 true.

Cleanup 函数

很多视图(View)都有数据模型(model)的事件绑定函数, 因此, 必须在视图被删除后解除绑定. 可以通过定义视图的 cleanup 函数来完成这个解绑操作:

cleanup函数
1
2
3
4
5
6
7
8
9
10
11
12
var MyView = Backbone.View.extend({
  // The constructor binds the model's "change" event to the view's render function. 
  initialize: function() {
    this.model.on("change", this.render, this);
  }

  // This is a custom cleanup method that will remove the model events owned by
  // this View.
  cleanup: function() {
    this.model.off(null, null, this);
  }
});

自定义获取模版

LayoutManager 缺省只支持 script 标签的模版, 但是在实际软件项目中, 通常会将不同的模版放置在不同的单独的 html 文件中, 这样便于模版文件的管理. LayoutManager 提供了 fetch 配置项让我们有机会来自己获得模版文件:

从script标签中获得模版内容
1
2
3
4
  Backbone.LayoutManager.configure
    # Default fetch implementation, get template from `script` tag
    fetch: (path)->
      _.template $(path).html()
同步方式获取模版文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  JST = window.JST = window.JST or {}
  Backbone.LayoutManager.configure
    manage: true
    paths:
      layout: "templates/layouts/"
      template: "templates/"

    fetch: (path) ->
      path = path + ".html"
      unless JST[path]
        $.ajax(
          url: app.root + path
          async: false
        ).then (contents) ->
          JST[path] = _.template(contents)
      JST[path]

下面是作者的教学视频, 初学者一定要看看.

backbone.layoutmanager from tbranyen on Vimeo.

开发SPA前端项目必须掌握什么知识?

这个题目有点大, 因为对于开发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的检测.

单元测试

单元测试是一个非常大的话题, 我没有办法用简短的一段话或者一篇文章将单元测试涉及到的方法和工具都介绍清楚.

技术大牛们都不愿意做单元测试, 当然, 很少有软件工程师愿意承认自己不是技术大牛:). 因为人员, 时间或者其他各种因素, 很多软件工程师们都愿意首先舍弃单元测试, 去保证功能的实现.

这里我不想挑起争论, 但我的经验的确是, 从个人角度上看, 单元测试能很大加深程序员对代码的理解程度, 加速个人能力的提升; 从项目角度上看, 技术经理需要权衡当前的开发, 回归测试和后继的代码维护成本之间的平衡, 然后决定是否实施单元测试, 以及多少程度的单元测试, 这是技术经理的职责.

在前端web应用中比较常用的单元测试框架包括:

Mock库可以采用SinonJS. 如果愿意, 还可以采用Should, Expect, Chai等Assertion库.

另外, 如果希望在持续集成中加入对测试的支持, 还需要加入Non-GUI浏览器的支持, 目前采用比较多的方案是:

由于Zombie当前的版本还不能支持RequireJS, 因此我更倾向于使用PhantomJS.

Minification & Concatenation

为了提高web应用的加载速度, 必须对Javascript文件, CSS文件进行Minification和Concatenation, 根据经验, 优化后的代码加载速度比优化前的要提高数倍.

常用的工具包括:

大部分开源的JavaScript构建工具都集成了对这些工具的支持, 因此没必要在这些工具上面花太多精力. 使用了RequireJS的应用, 可以直接使用其自带的r.js进行代码优化.

构建和部署

任何web应用工程都需要构建和部署, 一个自动化的构建和部署脚本能让编程人员更加专注于应用本身, 而不被繁琐的流程所困扰. 自动化的构建和部署是一个web前端项目最基本的要求, 当项目开始的时候, 项目组最好能在头两个迭代周期就完成自动化的构建和部署脚本的开发.

目前使用比较多的构建工具有:

我个人比较倾向于grunt, 如果工程中用了RequireJS, bbb(grunt扩展)是更好的选择.

网络地址本例子工程中, 我使用grunt/bbb开发了构建和部署脚本, 可以完成:

  • 文件的复制和清除
  • CoffeeScript的编译
  • JSHint
  • Minification & Concatenation
  • 在Phantom环境下运行Jasmine单元测试
  • Debug/Release Web服务器

感兴趣的人可以去看看上述例子工程的grunt.js配置文件, 也可以将这个例子工程作为web前端项目的模板.

下面的几点感触和具体的技术细节无关, 但是我个人觉得, 对于每个想以编程作为自己职业规划的程序员来说, 这几点尤其重要.

了解业界的技术发展动态

软件技术发展很快, 对于热爱技术的程序员来说, 必须经常更新自己的知识, 才能保证自己不会落伍. 因此, 每天都需要花一定时间去阅读, 了解行业最新的技术发展动态.

我的习惯是用Google Reader订阅几个经常更新的前端技术相关的RSS:

另外, 有些邮件周报也很不错:

多阅读开源代码, 最好参与感兴趣的开源项目

GitHub是一个非常有用的站点, 在上面能看到那些大牛们是怎么讨论问题, 如何思考问题, 以及如何实现或者修正的. 阅读开源项目的文档能了解概要, 如果需要了解细节, 最有效的方式是阅读代码, 特别是看针对问题的提交记录.

善于利用互联网工具

遇到问题的时候, 要知道如何去寻找问题的答案

StackOverflow是一个非常有名的编程问答站点, 我所遇到的大部分编程问题都能在上面找到答案, 即便没有答案, 也能找到其他人对这个问题的考虑. 另外, 别忘了强大的Google Search. 如果Google被墙, 可以考虑用Bing. 我不喜欢百度, 因为其搜索的命中率太低了.

善于使用云端的工具

选择合适的工具对于软件项目来说, 可以极大地提高工作效率, 起到事半功倍的效果. 好的商业软件大多需要支付巨额的费用, 并且搞不好还水土不服. 为了控制成本, 可以多尝试尝试云端的工具. 在知乎上有关于这些工具的讨论, 大家有兴趣可以去看看别人怎么做的.

多参与技术社区活动, 学会分享

多参加技术社区的活动, 包括线上的技术解答, 以及线下的技术讲座等. 分享越多, 获得也就越多.

Requirejs/Backbone/Jasmine前端项目和持续继承

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.

在CI系统中, 一种可行的做法是使用shell脚本或者Makefile, 先启动http服务, 然后异步执行测试. 这是一个可行的方案, 但是grunt.js本身就是一个命令行工具, 为什么还要用shell或者make了?

grunt.js支持alias task, 我们可以这样定义一个任务别名:

1
grunt.registerTask('test', 'default server jasmine');

执行test任务等价于按照顺序执行default, server, jasmine任务. 我们希望这样能工作, 可惜的是, bbbserver任务是阻塞式的, 不会退出, 也就是说, 在它后面的jasmine任务永远不会被执行.

我们希望执行一个server任务, 它能异步启动一个非阻塞的http服务, 后面的任务可以使用这个服务, 并且当grunt进程退出的时候, 该http服务能自动退出. 下面代码是用nodeconnect实现的满足这个要求的staticserver任务:

staticserver.js
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
41
42
43
44
45
46
/*
 * grunt
 * https://github.com/cowboy/grunt
 *
 * Copyright (c) 2012 "Cowboy" Ben Alman
 * Licensed under the MIT license.
 * http://benalman.com/about/license/
 */

module.exports = function(grunt) {

  // Nodejs libs.
  var path = require('path');

  // External libs.
  var connect = require('connect');

  // ==========================================================================
  // TASKS
  // ==========================================================================

  grunt.registerTask('staticserver', 'Start a static web server.', function() {
    // Get values from config, or use defaults.
    var port = grunt.config('server.port') || 8000;
    var base = path.resolve(grunt.config('server.base') || '.');

    var middleware = [
      // Serve static files.
      connect.static(base),
      // Make empty directories browsable. (overkill?)
      connect.directory(base)
    ];

    // If --debug was specified, enable logging.
    if (grunt.option('debug')) {
      connect.logger.format('grunt', ('[D] server :method :url :status ' +
        ':res[content-length] - :response-time ms').magenta);
      middleware.unshift(connect.logger('grunt'));
    }

    // Start server.
    grunt.log.writeln('Starting static web server on port ' + port + '.');
    connect.apply(null, middleware).listen(port);
  });

};

假如你看过gruntjs的代码, 你应当能看出来, 这其实就是gruntjs自带的server任务, 只是为了避免和bbbserver任务命名冲突, 这里将任务注册的名称从server换成了staticserver.

staticserver.js文件和其他gruntjs task文件一样存放在<工程目录>/tasks目录下, 确保这个任务能正确加载. 然后, 如下注册一个alias task命名为test:

1
2
3
  // Register test task, which will compile app and run the server and then do test.
  // Here we use 'staticserver' task (pure grunt static server) for testing.
  grunt.registerTask('test', 'default staticserver jasmine');

运行这个test任务就能到下面的输出:

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
41
$ ./node_modules/bbb/bin/bbb test
Running "clean:0" (clean) task
Removing: app
Removing: dist/debug
Removing: dist/release
Removing: tests/js

Running "clean:1" (clean) task
Removing: app
Removing: dist/debug
Removing: dist/release
Removing: tests/js

Running "clean:2" (clean) task
Removing: app
Removing: dist/debug
Removing: dist/release
Removing: tests/js

Running "clean:3" (clean) task
Removing: app
Removing: dist/debug
Removing: dist/release
Removing: tests/js

Running "coffee:app" (coffee) task

Running "lint:beforeconcat" (lint) task
Lint free.

Running "coffee:spec" (coffee) task

Running "staticserver" task
Starting static web server on port 8000.

Running "jasmine:all" (jasmine) task
Running specs for SpecRunner.html
.............
>> 31 assertions passed in 13 specs (2256ms)

Done, without errors.

参考

使用jasmine+sinon测试backbone+requirejs项目

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

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

那么基于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):

加载测试模块, 定义测试runner

通过定义模块依赖, 确保在执行runner方法前加载所有的测试模块.

SpecRunner (runner.coffee) download
1
2
3
4
5
6
7
8
9
10
11
12
# we should add all specs here to make sure all specs are loaded before execution.
define ["spec/models/contact"
        "spec/collections/contacts"
        "spec/views/contactitem"], ->
  runner = ->
    jasmineEnv = jasmine.getEnv()
    jasmineEnv.updateInterval = 1000
    trivialReporter = new jasmine.TrivialReporter()
    jasmineEnv.addReporter trivialReporter
    jasmineEnv.specFilter = (spec) ->
      trivialReporter.specFilter spec
    jasmineEnv.execute()

定义requirejs的配置文件, 执行测试runner

由于我们希望通过一个配置文件同时加载被测模块测试模块, 因此这里require.configbaseUrl项必须与web应用的该值保持一致.

require.config (config.coffee) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
require.config
  baseUrl: "../app"
  paths:
    libs: "../assets/js"
    jquery: '../assets/js/jquery/1.7.2/jquery'
    underscore: '../assets/js/underscore/1.3.2/underscore'
    backbone: '../assets/js/backbone/0.9.2/backbone'
    text: '../assets/js/require/plugins/text'
    templates: '../assets/templates'
    spec: "../tests/js/spec"

  shim:
    'backbone':
      deps: ['underscore', 'jquery'],
      exports: 'Backbone'

    'underscore':
      exports: '_'

require ["../tests/js/runner"], (runner) ->
  runner()

定义SpecRunner.html, 用来在浏览器端执行策测试代码

SpecRunner.html (SpecRunner.html) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
  "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
  <title>Jasmine Spec Runner</title>

  <link rel="stylesheet" type="text/css" href="lib/jasmine.css">

  <script type="text/javascript" src="lib/jasmine.js"></script>
  <script type="text/javascript" src="lib/jasmine-html.js"></script>
  <script type="text/javascript" src="lib/sinon.js"></script>
  <script data-main="js/config" type="text/javascript" src="../assets/js/require/require.js"></script>
</head>

<body>
</body>
</html>

下面是测试执行的结果:

遗留问题

  • 由于被测模块的加载是由requirejs完成的, 那么如何在加载时mock被测模块的依赖模块?
  • 如何和CI系统集成, 提供命令行方式的浏览器环境进行测试? 尝试过envjs, 但requirejs总在加载模块时出错, 还没能进一步研究出错细节.