原文地址, 我只是使用我自己当时理解的方式,分步骤讲了出来。
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
, 成功。
# 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});
`
仍然成功。
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 调用了三次,太丑了,换个写法如何?
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 了。
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 兼容的实现,具体实现,留给读者自己去做吧。(原文内有答案)