Opal是一个 Ruby2Js 编译器,从语言的角度来说实现得相当不错,例如你可以在浏览器里用 method_missing 等等。
虽然 opal 没什么文档,但是它本身算是一个 less suprise 的东西,这里主要介绍一下怎样 wrap js 库到 ruby。
最小环境
一个最简单的 opal rack server 在https://github.com/opal/opal/tree/0-10-stable/examples/rack 。
git clone https://github.com/opal/opal.git
git checkout 0-10-stable
cd opal/examples/rack
bundle
bundle exec rackup
修改 application.rb 后刷新就能看到改动。在 application.rb 里可以用 require 引入其他 rb 文件,和普通 ruby 是一样的,详见http://opalrb.org/docs/guides/v0.10.1/compiler_directives.html 。
内联 Js
puts `window.title` #=> 'opal example'
`console.log(1)`
使用反引号会把语句直接插入到生成的 js 中,不进行编译。
第一次用到这个的时候很容易迷惑,
a = `{a: 1}`
puts a.class
这里的 a 是什么呢?
当我们打印 a 的 class 时,会提示Uncaught TypeError: a.$class is not a function
。
这里的 a 是一个原生 js 对象,所以并没有 ruby 对象的方法。从报错也可以发现,class 的调用是翻译成$class
的。所以 opal 对象就是特殊的 js 对象,包含了很多以$为前缀命名的 ruby 方法。记住这一点,理解之后的内容就非常简单了。
Native 模块
puts Native(`{a: 1}`).a #=> 1
puts Native(`{a: 1}`).class #=> Native::Object
Native 可以把 js 对象直接包装成 opal 对象,并且将 ruby 的方法调用直接代理到 js 对象上。
对于更严谨的封装,可以使用 include Native。
class B
include Native
def val
`#@native.v` #tips: #@native 等同 #{@native}
end
end
class A
include Native
alias_native :b, as: B
end
a = A.new(`{b: {v: 1}}`)
puts a.b.val #=> 1
include Native
的类,initialize 会接受一个 js 对象,并存放在@native,如果自己定义了 initialize,需要 super 一下。
alias_native
可以将一个 ruby 方法调用转发到 js 对象上,as: B
的意思是用 B 类去初始化返回值(可选)。
Native 模块的例子:opal stdlib 对 console 的封装。详细的 Native 和 Native::Object 可参阅Native 的实现。
.JS 调用
opal 中.JS 是一个关键字,用于调用 js 对象方法。
a = `{a: function(){return 1}, b: 2}`
puts a.JS.a #=> 1
puts a.JS[:b] #=> 2
同名冲突
opal 中的局部变量是按同名翻译成 js 的,所以同名的 ruby 变量有可能会和 js 中的冲突。 例如:
document = Native(`document`)
看上去是把 js 的 native document 赋给局部变量 document,但实际上编译出来的是:
var document = Opal.nil;
document = self.$Native(document);
不难想象 document 会为空。
闭包作用域
opal 中的 class 和 module 实际上都是 function 闭包,所以可以直接在 module 作用域里写内联 js,然后定义方法去调用,一个例子是 opal stdlib 的base64 实现。这个是不区分类作用域和实例作用域的,想想翻译的 js 就清楚了。
另外,opal 是不分 symbol 和 string 的,对于不看文档就开搞的,这个需要注意一下。