分享 使用 Ruby 写 Node && 如何使用 Opal 包装 Javascript 库到 Ruby

zw963 · 2017年06月15日 · 最后由 lithium4010 回复于 2017年06月19日 · 1842 次阅读

原文地址, 我只是使用我自己当时理解的方式,分步骤讲了出来。

使用 node 启动一个 http server.

Node 自带 http 模块,启动一个 http_server 很简单,新建 node.js:

// node.js
http = require('http')
var port = process.env.port || 1337;
http.createServer(function(req, res) {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello World\n');
}).listen(port);

启动 server:

$:  node node.js

浏览器访问:http://127.0.0.1:1337, 看到 hello world, 成功。

最简单的 wrap 方式,node.rb 第一版:

# Opal 自带这个库.
require 'nodejs'

# 这是 Opal 新增的 node_require 方法, 用来  require 一个 node 模块.
http = node_require('http')
port = 1337

# 直接把上面的 js 原样拷贝过来,  放到  X-strings 里面执行, 就像 MRI 执行外部程序一样.
`
http.createServer(function(req, res) {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello World\n');
}).listen(port);
`

浏览器访问:http://127.0.0.1:1337, 看到 hello world, 成功。

让它看起来更 Rubyify 一点点,第二版。

如果你足够熟悉 Ruby, 你可能遇到了接触 Opal 以来的第一个大坑 (至少对我来说是的), 变量名的 string interpolation 在哪里呢?这是因为 http = node_require('http') 为 JS 的 runtime 定义了 JS 的本地变量,因此不需要 interpolation, 不过,为了让看起来更像 Ruby, 这样做也不无不可。

require 'nodejs'
http = node_require('http')
port = 1337

# 加上字符串插值, 现在更像 Ruby 了.

`
#{http}.createServer(function(req, res) {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('Hello World\n');
}).listen(#{port});
`

仍然成功。

更进一步,X-strings 的方式太丑了,我可以不可以用更 Ruby 的方式来写这段 JS 代码?第三版。

require 'nodejs'
http = node_require('http')
port = 1337

http.JS.createServer(->(_req, res) {
    res.JS.writeHead(200, {'Content-Type': 'text/plain'}.to_n)
    res.JS.end("Hello World\n")
  }).JS.listen(port)

值得一提的是:这里的 .JS 是 Opal 0.9 开始支持的一个特殊的词法,而不是方法调用。 to_n 方式是要将 js native 对象类型转化为 Opal 支持的 Native::Object 类型 Ruby 对象. 但是我们 JS 调用了三次,太丑了,换个写法如何?

使用 Opal 的 Native 模块,第四版

require 'nodejs'
http = node_require('http')
port = 1337

Native(http).createServer(->(_req, res) {
    opal_res = Native(res)
    opal_res.writeHead(200, {'Content-Type': 'text/plain'})
    opal_res.end("Hello World\n")
  }).listen(port)

看起来又好了一点点,这里的 Native(http) 使用 method_missing 实现了 createServer 到 JS native 对象的委托. 可是写起来感觉还是挺烂的,我们定义一个接口,把它 wrap 起来如何?

我们要用我们自己的接口。第五版

我需要定义一个 HTTP 模块,再定义一个 Server 类,然后创建一个 listen 方法,用这个方法来 wrap 我们不想见到的 JS 方法,下面的接口就看起来不错:

HTTP::Server.listen(port) do |req, res|
   # ...
end

下面的代码看起来想那么回事儿。

require 'nodejs'

module HTTP
  class Server
    def self.listen(port, &block)
      # 这里使用 Native 模块封装下 http
      http = Native(node_require('http'))
      # 直接把 JS 传入的 block 和 port 委托给原始的 createServer 方法.
      http.createServer(&block).listen(port)
    end
  end
end

HTTP::Server.listen(1337) do |req, res|
  res.writeHead(200, {'Content-Type': 'text/plain'})
  res.end "Hello World\n"
end

看起来不错,快去运行下!

很不幸,他不工作。

res.$writeHead(200, $hash2(["Content-Type"], {"Content-Type": "text/plain"}));
    ^

这因为 res 对象没有 writeHead 可用!他是一个 JS 的 native 对象,你不能在 Ruby 中 直接调用 JS 的原生方法。

因此,不得不改成这样:

HTTP::Server.listen(1337) do |req, res|
  res.JS.writeHead(200, {'Content-Type': 'text/plain'})
  res.JS.end "Hello World\n"
end

哈,终于 OK 了。

既然要 wrap, 那就彻底一点,我们的目标是完全的 Rubyify, 你不需要知道我其实 wrap 的是 JS.

module HTTP
  class Server
    def self.listen(port)
      Native(node_require('http')).createServer(->(req, res) {
          # 在这里把要返回的参数先 wrap 一下, 然后再传到 block 中.
          yield(Native(req), Native(res))
        }).listen(port)
    end
  end
end

HTTP::Server.listen(1337) do |req, res|
  res.writeHead(200, {'Content-Type': 'text/plain'})
  res.end "Hello World\n"
end

终于彻底消除了 JS 的痕迹,虽然,仍然暴露了几个 node 的内部实现方法. 不如改成 rack 兼容的实现,具体实现,留给读者自己去做吧。(原文内有答案)

有没有办法不改已有代码就把代码用 node 跑?

需要 登录 后方可回复, 如果你还没有账号请 注册新账号