Cordova+Phaser 简易APP开发日记(一)

这个年代, 想要开发简单的手机 APP, 需要根据你的定位选择合适的技术.

原生代码成本太高, iOS 上的 Objective-C 语法奇特, Android 上的 Java 也不是一般的 Java.

一些跨平台的解决方案扩展空间够了, 比如 Unity, 比如 Unreal, 但是这些都要你从零开始去学习他们的工具链.

有没有一种技术可以充分利用现有的技术积累, 开源, 并且能够将不同平台之间的沟壑填平的方法呢?

我的选择是 CordovaPhaser, 也就是当下炒的火热的 Hybrid app.

Cordova 是一个无头浏览器外壳, 启动时会用内嵌的 WebView 运行你的 HTML 代码, 同时使用 Hook 通过插件的形式在 JS 和操作系统之间建立一个桥梁, 应用的核心内容就是 HTML/JS/CSS 这些常规的网站开发技术. Cordova 是基于 Node.js 的 Npm 包, 整个工具链和平常开发网站时所用到的工具同属一个生态系统, Cordova 针对不同平台建立原生的项目文件夹, iOS 项目可以用 XCode 打开, Android 项目可以导入 Android Studio, 可以按需增加平台, 编译过程是在本地进行的, 所以开发环境要满足编译条件才行, 比如要编译 iOS 项目就必须在 OSX 系统下进行.

Phaser 是一个 2D 游戏引擎, 主要面向手机端, 内部的渲染用到了 PIXI.js, 支持加载和缓存图片 精灵 地图 音乐 物理等资源, 支持场景 世界 摄像机 发射器等游戏开发必需的组件. 图像渲染方面采用 WebGL 技术, 平台不支持的情况下使用 Cavans; 音乐部分采用最新的 HTML5 标准 WebAudio 技术, 平台不支持的情况下使用 Audio Tag.

Phaser 对 Cordova 具备最基本的支持, 当 Phaser 发现自己运行在 Cordova 环境下, 它会自动监听 deviceready 事件; 同时对程序进入后台时的 pause 和 resume 事件也做了相应的处理, 自动暂停和恢复游戏.

决定采用这一对组合, 下一步就是要慢慢磨合和适应了.

MacBook 466/467 固态硬盘

Air 的闪存速度让我印象深刻, 在做了许多功课以后, 终于决定从京东订了一块美光 M4 128G.

对于品牌的选择, 查了很多资料后发现美光比较激进, 而 Intel 比较保守. 同时美光的价格也更亲民.

关于 MacBook 466/467:

  • 更换硬盘只要打开电池后盖即可, 不必大动干戈.
  • SATA-2 接口, 传输带宽为 3Gb/s, 但内置的硬盘和光驱都工作在 1.5Gb/s 模式下.
  • 原装硬盘厚度为 9.5mm.
  • 当电池耗尽时会进入深度睡眠, 如果装在光驱位会出现一睡不醒的问题, 装在硬盘位则没有该问题.

关于美光 Crucial M4:

  • M4 是目前最新款.
  • 接口是 SATA-3 6Gb/s.

美光 M4 安装在 MacBook 上, 接口协商速度为 3Gb/s, 有点小浪费, 但毕竟可以发挥出最大效能.

具体的安装过程可以看 iFixit 的图文教程.

安装后的优化.

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
#!/usr/bin/env bash
#
# SSD 优化脚本.
#
# 用法:
# 保存该脚本为 ssd.sh
# 在终端里运行 sudo ssd.sh

# 关闭 Sudden motion sensor
# SMS 用于笔记本在突然移动时通知硬盘碰头复位, 以保护机械硬盘不受损伤, 对于固态硬盘没有用处.
pmset -a sms 0

# 删除 Hibernate
# 当电池耗尽时, 系统会将内存中的数据写入硬盘, 把固态硬盘安装在光驱位的必须关闭深度睡眠, 放在硬盘位的取消这个功能也可以节省与内存大小相同的硬盘空间.
# 如果想保留深度睡眠, 请删除下面这一节代码.
f=/var/vm/sleepimage
if [[ -e $f ]]; then
pmset -a hibernatemode 0
rm $f
fi

# 取消 atime
# Mac 系统有个文件最后访问时间, 当你查看文件时, 会将当前时间写到 atime 里, 取消这个功能可以减少硬盘写入的次数.
f=/Library/LaunchDaemons/com.apple.hfs.noatime.plist
if [[ ! -e $f ]]; then
echo '<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.apple.hfs.noatime</string>
<key>ProgramArguments</key>
<array>
<string>mount</string>
<string>-vuwo</string>
<string>noatime</string>
<string>/</string>
</array>
<key>RunAtLoad</key>
<true />
</dict>
</plist>' > $f

fi


# 打开 TRIM
# 从 Lion 开始, 系统已经支持 SSD TRIM 指令, 但只对 Apple SSD 生效, 下面这段代码会对驱动打补丁, 将 "Apple SSD" 替换为空白, 借此激活 TRIM 功能.
f=/System/Library/Extensions/IOAHCIFamily.kext/Contents/PlugIns/IOAHCIBlockStorage.kext/Contents/MacOS/IOAHCIBlockStorage
if [[ ! -e $f.original ]]; then
cp $f $f.original
perl -pi -e 's|(\x52\x6F\x74\x61\x74\x69\x6F\x6E\x61\x6C\x00{1,20})[^\x00]{9}(\x00{1,20}\x51)|$1\x00\x00\x00\x00\x00\x00\x00\x00\x00$2|sg' $f
kextcache -system-prelinked-kernel
kextcache -system-caches
fi

Ruby Koans

学习 Ruby 语言的途径有太多太多, 你可以选择一本经典书籍慢慢看, 也可以找在线的资源(类似那种十五分钟入门的), 甚至有很多 Ruby 语言使用者是从 Ruby On Rails 学起的(例如我).

不知道从什么时候开始, Ruby 官网文档里出现一篇Ruby Koans, 采用单元测试填空题的方式强化基础知识. 网站访问不了可以直接访问源代码库.

说真的, 做完这套题目以后, 我对 Ruby 有了重新的认识, 这样说有点太官方了, 其实是我的自信心受到了粉碎性打击然后重组.

将代码下载回来, 运行 ruby path_to_enlightenment.rb, 就会开始单元测试过程, 因为题目的答案还没有填写, 所以测试肯定是通不过的, 碰到问题就会停下来, 然后提醒你哪个文件的第几行出了问题.

最先出现问题的文件会是 about_asserts.rb, 这里是练习一下断言的最基本用法, 以及几道 1+1=2 之类的问题. __处就是要你填空的地方, 如果不填会提醒你 <"FILL ME IN">.

about_nil.rb
1
2
3
4
5
6
def test_nil_has_a_few_methods_defined_on_it
# nil对应空字符串, 所以 "#{nil}" 才不会影响输出.
assert_equal '', nil.to_s
# 这个就很新鲜, 之前没有注意到啊.
assert_equal 'nil', nil.inspect
end
about_objects.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def test_some_system_objects_always_have_the_same_id
# false, true, nil 三者都有固定的 object_id, 为什么会取 0 2 4 这三个值呢? 请看下文.
assert_equal 0, false.object_id
assert_equal 2, true.object_id
assert_equal 4, nil.object_id
end

def test_small_integers_have_fixed_ids
# 整型的 object_id 等于 2n+1, 所以负数的 object_id 也是负的.
# 从 0 开始闲置的 object_id 就只有 0 2 4 等等. 所以之前特殊的 object_id 由此得来.
assert_equal 1, 0.object_id
assert_equal 3, 1.object_id
assert_equal 5, 2.object_id
assert_equal 201, 100.object_id
end

# 其它对象的 object_id 是随机产生并且绝不重复的(包括 Float), object_id 也是判断对象是否为同一个对象的依据.
about_arrays.rb
1
2
3
4
5
6
7
8
9
10
11
12
def test_slicing_arrays
array = [:peanut, :butter, :and, :jelly]

# 这种用法对应于 Array#slice(start, length)
# 当 length 超过数组长度时忽略.
assert_equal [:and, :jelly], array[2,20]
# 当 length 为 0 时返回空数组
assert_equal [], array[4,0]
assert_equal [], array[4,100]
# 当 start 超过数组长度时返回 nil.
assert_equal nil, array[5,0]
end

array[4,0] 返回 [], 而 array[5,0] 却返回 nil. 为什么? Array 文档把这个称为 special cases. 从源代码里顺着 rb_ary_aref 方法继续下去就是 rb_ary_subseq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
rb_ary_subseq(VALUE ary, long beg, long len)
{
VALUE klass;

// 如果长度大于数组长度, 返回 nil.
if (beg > RARRAY_LEN(ary)) return Qnil;
if (beg < 0 || len < 0) return Qnil;

if (RARRAY_LEN(ary) < len || RARRAY_LEN(ary) < beg + len) {
len = RARRAY_LEN(ary) - beg;
}
klass = rb_obj_class(ary);
// 如果长度等于 0, 返回空数组.
if (len == 0) return ary_new(klass, 0);

return ary_make_partial(ary, klass, beg, len);
}

不知道 Matz 当年是不是晃神了, 还是有意为之, 进而造成了下标 4 和 5 的不一致性, 反正这已经是即定事实了.

about_strings.rb
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
61
62
63
64
65
def test_flexible_quotes_can_handle_multiple_lines
long_string = %{# \n
It was the best of times,# \n
It was the worst of times.# \n
}

assert_equal 54, long_string.size
end

def test_here_documents_can_also_handle_multiple_lines
long_string = <<EOS
It was the best of times,# \n
It was the worst of times.# \n
EOS
assert_equal 53, long_string.size
end

# += 操作不会改变原始字符串.
def test_plus_equals_also_will_leave_the_original_string_unmodified
original_string = "Hello, "
hi = original_string
there = "World"
hi += there
assert_equal "Hello, ", original_string
end

# << 操作会修改源始字符串的.
def test_the_shovel_operator_modifies_the_original_string
original_string = "Hello, "
hi = original_string
there = "World"
hi << there
assert_equal "Hello, World", original_string
end

# 单引号字符串里也会发生转义.
def test_single_quotes_sometimes_interpret_escape_characters
string = '\\\''
assert_equal 2, string.size
assert_equal %{\\'}, string
end

def test_you_can_get_a_substring_from_a_string
string = "Bacon, lettuce and tomato"
# 可以像数组那样截取字符串分片
assert_equal 'let', string[7,3]
assert_equal 'let', string[7..9]
# 但是如果只取一个字符的话, 返回的是字符的 ASCII 码.
assert_equal 97, string[1]
end

def test_strings_can_be_split
string = "Sausage Egg Cheese"
words = string.split
# 字符串分割的默认参数是 $;
assert_equal ["Sausage", "Egg", "Cheese"], words
end

def test_strings_are_not_unique_objects
a = "a string"
b = "a string"
# 值相等.
assert_equal true, a == b
# 但却不是同一个对象.
assert_equal false, a.object_id == b.object_id
end

这里比较有意思的是 String#split 的默认值 $; . 它是一个全局变量, 它的唯一用途就是作为 split 方法的默认值出现, 它的初始值是 nil. 你可以通过修改这个全局变量来改变 split 方法的行为.

1
2
3
>> $; = 'b'
>> "abaabbbccdcbba".split
=> ["a", "aa", "", "", "ccdc", "", "a"]
about_symbols.rb
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
def test_identical_symbols_are_a_single_internal_object
symbol1 = :a_symbol
symbol2 = :a_symbol
# 符号是独一无二的.
assert_equal true, symbol1 == symbol2
assert_equal true, symbol1.object_id == symbol2.object_id
end

# all_symbols 方法可以返回所有符号, 这基中就包括方法名.
def test_method_names_become_symbols
symbols_as_strings = Symbol.all_symbols.map { |x| x.to_s }
assert_equal true, symbols_as_strings.include?("test_method_names_become_symbols")
end

# 在 MRI 版本里, 常量也是符号.
in_ruby_version("mri") do
RubyConstant = "What is the sound of one hand clapping?"
def test_constants_become_symbols
all_symbols = Symbol.all_symbols

assert_equal true, all_symbols.include?(:RubyConstant)
end
end

# 符号可以长的不太一样.
def test_symbols_with_spaces_can_be_built
symbol = :"cats and dogs"

assert_equal symbol, symbol.to_sym
end

# 符号也可以在内部插入值.
def test_symbols_with_interpolation_can_be_built
value = "and"
symbol = :"cats #{value} dogs"

assert_equal symbol, "cats and dogs".to_sym
end

# 符号不是"不可变的"字符串, 它没有字符串的方法.
def test_symbols_do_not_have_string_methods
symbol = :not_a_string
assert_equal false, symbol.respond_to?(:each_char)
assert_equal false, symbol.respond_to?(:reverse)
end

# 连最基本的连接操作都不允许.
def test_symbols_cannot_be_concatenated
assert_raise(NoMethodError) { :cats + :dogs }
end

# 符号可以动态创建, 但是不推荐这样做.
def test_symbols_can_be_dynamically_created
assert_equal :catsdogs, ("cats" + "dogs").to_sym
end
about_regular_expressions.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 字符串可以像这样查找内容.
def test_a_regexp_can_search_a_string_for_matching_content
assert_equal "match", "some matching content"[/match/]
end

# 找不到会返回 nil, 这种方式更像是条件判断.
def test_a_failed_match_returns_nil
assert_equal nil, "some matching content"[/missing/]
end

# 带星号的正则表达的十分贪婪.
def test_asterisk_means_zero_or_more
assert_equal "", "abbcccddddeeeee"[/z*/]
end

为什么会发生的情形? 看一下 Ruby 源代码里都做了什么.

1
2
3
4
5
6
7
8
9
10
# 当 String#slice 的第一个参数类型为 Regexp 时, 调用下面这个函数.
rb_str_subpat(VALUE str, VALUE re, VALUE backref)
{
if (rb_reg_search(re, str, 0, 0) >= 0) {
VALUE match = rb_backref_get();
int nth = rb_reg_backref_number(match, backref);
return rb_reg_nth_match(nth, match);
}
return Qnil;
}

事实上, 是 "abbcccddddeeeee".match(/z*/)[0] 返回了空字符串. 如果换成 "abbcccddddeeeee".match(/missing/) 就返回 nil 了.

1
2
3
4
# 这会儿又表现的十分容易满足, 这简直就是陷阱.
def test_the_left_most_match_wins
assert_equal "a", "abbccc az"[/az*/]
end

此处的表现十分怪异, 事实上 Ruby 正则也是遵从正常的贪婪原则的.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
>> "abb".match /ab*/
=> #<MatchData "abb">
>> "abb".match /ab*?/
=> #<MatchData "a">
>> "abb".match /ab+?/
=> #<MatchData "ab">
>> "abb".match /ab{0,9}/
=> #<MatchData "abb">

# 只是当出现在字符串末尾时就犯糊涂
>> "aabb".match /ab*/
=> #<MatchData "a">
>> "aabb".match /ab{0,9}/
=> #<MatchData "a">

看来以后星号正则要慎重使用了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# \w 里的连字符是下划线, 不包括减号.
def test_slash_w_is_a_shortcut_for_a_word_character_class
assert_equal "variable_1", "variable_1 = 42"[/[a-zA-Z0-9_]+/]
assert_equal "variable_1", "variable_1 = 42"[/\w+/]
end

# 不但能匹配, 还能选择返回第几个匹配结果.
def test_parentheses_also_capture_matched_content_by_number
assert_equal "Gray", "Gray, James"[/(\w+), (\w+)/, 1]
assert_equal "James", "Gray, James"[/(\w+), (\w+)/, 2]
end

# 每次匹配, 结果都会被记入全局变量中.
def test_variables_can_also_be_used_to_access_captures
assert_equal "Gray, James", "Name: Gray, James"[/(\w+), (\w+)/]
assert_equal "Gray", $1
assert_equal "James", $2
end
about_constants.rb
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
class Animal
LEGS = 4
end

class MyAnimals
LEGS = 2

class Bird < Animal
def legs_in_bird
LEGS
end
end
end

# 嵌套常量赢过继承常量.
def test_who_wins_with_both_nested_and_inherited_constants
assert_equal 2, MyAnimals::Bird.new.legs_in_bird
end

class MyAnimals::Oyster < Animal
def legs_in_oyster
LEGS
end
end

# 如果定义成这样, 继承变量就赢了.
def test_who_wins_with_explicit_scoping_on_class_definition
assert_equal 4, MyAnimals::Oyster.new.legs_in_oyster
end
about_exceptions.rb
1
2
3
4
5
6
7
8
9
10
class MySpecialError < RuntimeError
end

# 自定义异常完整的继承树.
def test_exceptions_inherit_from_Exception
assert_equal RuntimeError, MySpecialError.ancestors[1]
assert_equal StandardError, MySpecialError.ancestors[2]
assert_equal Exception, MySpecialError.ancestors[3]
assert_equal Object, MySpecialError.ancestors[4]
end
about_blocks.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
# 块中的代码甚至会给局部变量带来副作用.
def test_block_can_affect_variables_in_the_code_where_they_are_created
value = :initial_value
method_with_block { value = :modified_in_a_block }
assert_equal :modified_in_a_block, value
end

def test_blocks_can_be_assigned_to_variables_and_called_explicitly
add_one = lambda { |n| n + 1 }
# 有两种方法调用 lambda 表达式.
assert_equal 11, add_one.call(10)
assert_equal 11, add_one[10]
end
about_scope.rb
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
class AboutScope
class String
end

# 当前范围中的类名拥有更高的优先级.
def test_bare_bones_class_names_assume_the_current_scope
assert_equal true, AboutScope::String == String
end

# 当前范围中的类名如果与 Ruby 库中的类名重复, 会造成很多不必要的麻烦.
def test_nested_string_is_not_the_same_as_the_system_string
assert_equal false, String == "HI".class
end

def test_use_the_prefix_scope_operator_to_force_the_global_scope
assert_equal true, ::String == "HI".class
end

MyString = ::String

# 类名其实只是常量而已.
def test_class_names_are_just_constants
assert_equal true, MyString == ::String
assert_equal true, MyString == "HI".class
end

def test_constants_can_be_looked_up_explicitly
assert_equal true, MyString == AboutScope.const_get("MyString")
end
end
about_class_methods.rb
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
class Dog
end

# 实例也可以定义方法
def test_you_can_define_methods_on_individual_objects
fido = Dog.new
def fido.wag
:fidos_wag
end
assert_equal :fidos_wag, fido.wag
end

# 这个方法只属于这个实例, 不影响类的其它实例.
def test_other_objects_are_not_affected_by_these_singleton_methods
fido = Dog.new
rover = Dog.new
def fido.wag
:fidos_wag
end

assert_raise(NoMethodError) do
rover.wag
end
end

LastExpressionInClassStatement = class Dog; 21; end

# 你总能拿到最后一个表达式的结果.
def test_class_statements_return_the_value_of_their_last_expression
assert_equal 21, LastExpressionInClassStatement
end

SelfInsideOfClassStatement = class Dog; self; end

# 甚至是 self.
def test_self_while_inside_class_is_class_object_not_instance
assert_equal true, Dog == SelfInsideOfClassStatement
end
about_to_str.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class CanBeTreatedAsString
def to_s
"string-like"
end

def to_str
to_s
end
end

# to_str 有更进一步的作用.
def test_to_str_allows_objects_to_be_treated_as_strings
assert_equal false, File.exist?(CanBeTreatedAsString.new)
end

抑制 Ruby 警告

在 irb 命令行里输入

1
2
3
4
5
>> X = 1
=> 1
>> X = 2
(irb):28: warning: already initialized constant X
=> 2

你会看到上面那行警告, 已经初始化过常量 X, ruby 是在提醒你常量是不应该被改变的, 不过 ruby 只是在抱怨一句罢了, 实际上 X 的值已经由 1 变成 2 了.

警告是对的, 常量的再次赋值有违原则. 但是人总会碰到情非得已的时候.

ruby 论坛上有篇贴子也提到了这一问题, 沙发搞笑的说忽略事情最简单的方式就是闭上眼睛. 不过五楼是正经人, 给出了解决问题的思路. 顺着思路就想到了这个方法.

ruby 接受 -W 作为参数.

1
2
ruby -h
-W[level] set warning level; 0=silence, 1=medium, 2=verbose (default)

运行时对应的是全局变量 $-v

  • nil -W0
  • false -W1
  • true -W2

这样事情就变得简单了.

1
2
3
4
5
6
7
def suppress_warning &block
old_v = $-v
$-v = nil
yield
ensure
$-v = old_v
end

试试效果

1
2
3
4
>> X = 1
=> 1
>> suppress_warning { X = 2 }
=> 2

嗯, 感觉不错, 你也是不清理不舒服斯基吗?