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

zw963 · June 15, 2017 · Last by lithium4010 replied at June 19, 2017 · 1844 hits

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

使用 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 跑?

You need to Sign in before reply, if you don't have an account, please Sign up first.