依照昨天的構想,把傳送binary資料的功能也實作出來了,就把它用在資源分享上。
分成兩個部分來傳送資料,基本上問題不大,但是要實作,需要在使用方法上制定一些規則,才能讓傳送一般資訊與binary資料使用同樣的方法。
這些規則及限制主要是因為內部需要傳送額外的資訊,才可能將binary資料與其他資訊一同傳送。為此,先定義一個內部使用的事件名稱,叫做blob,另外,在傳送的資料中,也必須記錄原本的事件,所以也不能在傳送的資料物件中,使用'name'做為屬性的名稱。還有一個限制,不過這是偷懶的緣故,就是程式中會做判斷哪個屬性存放了Blob或是ArrayBuffer物件,但是我只檢查一層的屬性,如果屬性中還有其他物件,物件中有Blob,就不管他了。為了讓從放Blob/ArrayBuffer的屬性名稱一致,還佔用了一個欄位,叫做blobfield。所以目前的限制是:
只要符合這個規則,就可以像平常使用Socket.IO的方式來傳送binary資料。其實做更多判斷、調整設計的話,還是可以拿掉這些規則限制,不過目前先這樣吧。
接下來看目前的程式碼,既然功能差不多了,我就把比較完整的程式放上來。
首先是測試的程式,伺服器端(test844.js):
var fs = require('fs'),
url = require('url'),
app = require('http').createServer(function(req, res) {
var filename = '',
resource = url.parse(req.url).pathname;
switch(resource) {
case '/ws.io/ws.io.js':
console.log(resource);
filename = __dirname + resource;
res.setHeader('Content-Type', 'text/javascript');
break;
default:
filename = __dirname + '/test844.html';
res.setHeader('Content-Type', 'text/html');
break;
}
fs.readFile(filename, function(err, data) {
if(err) {
res.writeHead(500);
return res.end('Error reading resource.');
} else {
res.writeHead(200);
res.end(data);
}
});
}),
io = require('./ws.io').listen(app);
io.sockets.on('connection', function(socket) {
socket.on('share', function(data) {
socket.broadcast.emit('share', data);
});
});
app.listen(8443);
網頁(test844.html):
<meta charset='utf-8'>
<style>
#panel {
border: solid 1px #336699;
width: 240px;
line-height: 20px;
vertical-align: middle;
padding: 5px;
border-radius: 5px;
}
</style>
<script src='/ws.io/ws.io.js'></script>
<input type='file' id='files'><br>
<div id='panel'>
<ul id='list'>
</ul>
</div>
<script>
var files = document.getElementById('files');
var socket = io.connect('ws://localhost:8443');
function getUrl() {
if(!!window.URL) {
return window.URL;
}
if(!!window.webkitURL) {
return window.webkitURL;
}
}
files.addEventListener('change', function(e) {
var URL = getUrl();
if(files.files.length>0) {
var file = files.files[0];
var src = URL.createObjectURL(file);
var a = document.createElement('a');
a.href = src;
a.innerHTML = file.name;
a.target = '_blank';
var li = document.createElement('li');
li.appendChild(a);
document.getElementById('list').appendChild(li);
socket.emit('share', {filename: file.name, type: file.type, file:file});
}
});
var fileinfo;
socket.on('share', function(data) {
var URL = getUrl();
var a = document.createElement('a');
var file = new Blob([data.file], {type:data.type});
a.href = URL.createObjectURL(file);
a.innerHTML = data.filename;
a.target = '_blank';
var li = document.createElement('li');
li.appendChild(a);
document.getElementById('list').appendChild(li);
});
</script>
使用'node test844'執行後,會監聽8443埠。另外,ws.io模組放在ws.io子目錄中,裡面有幾個檔案。首先是模組的定義(package.json):
{
"name": "ws.io",
"version": "0.0.1",
"author": "fillano <fillano.feng@gmail.com>",
"contributors": [
{
"name": "Fillano Feng",
"email": "fillano.feng@gmail.com"
}
],
"main": "./lib/ws.io",
"description": "a simple wrap for ws to make it acts more like Socket.IO",
"keywords": [
"WebSocket",
"ws",
"Socket.IO"
],
"dependencies": [
{"ws": "0.4.21"}
],
"license": "MIT",
"engines": [
{"node": ">=0.8"}
]
}
以及模組的進入點(index.js):
module.exports = require('./lib/ws.io');
需要在網頁中載入的靜態js檔也在這個目錄中(ws.io.js),這裡定義了client的基本操作方法,包含io.connect()、socket.on()、socket.emit():
(function(window, undefined) {
var io = window.io = {
connect: function(addr) {
if(addr.indexOf('http')==0) addr = addr.replace('http','ws');
return new Socket(new WebSocket(addr));
}
};
function Socket(ws) {
var handlers = {};
var blobinfo = null;
this.on = function(name, handler) {
if(!!handlers[name]) {
handlers[name].push(handler);
} else {
handlers[name] = [];
handlers[name].push(handler);
}
};
this.emit = function(name, data) {
var isblob = false;
for(var i in data) {
if(checkBinary(data[i])) {
isblob=true;
var blob = data[i];
delete data[i];
data['name'] = name;
data['blobfield'] = i;
break;
}
}
if(isblob) {
var tosent = {name:'blob',data:data};
ws.send(JSON.stringify(tosent));
ws.send(blob);
} else {
ws.send(JSON.stringify({name:name,data:data}));
}
};
ws.onmessage = function(msg) {
switch(typeof msg.data) {
case 'object':
if(!!blobinfo) {
if(!!handlers[blobinfo.name]) {
var name = blobinfo.name;
delete blobinfo.name;
blobinfo[blobinfo.blobfield] = msg.data;
delete blobinfo.blobfield;
handlers[name].forEach(function(fn) {
fn.call(this, blobinfo);
});
}
blobinfo = null;
}
break;
case 'string':
if(!!msg.data) {
var data = JSON.parse(msg.data);
switch(data.name) {
case 'blob':
blobinfo = data.data;
break;
default:
if(!!handlers[data.name]) {
handlers[data.name].forEach(function(fn) {
fn.call(this, data.data);
});
}
break;
}
}
break;
}
}
function checkBinary(obj) {
return (checkBlob(obj) || checkArrayBuffer(obj));
function checkBlob(obj) {
return (typeof obj==='object' &&
!!obj.slice && typeof obj.slice==='function');
}
function checkArrayBuffer(obj) {
return (typeof obj==='object' && !!obj.byteLength && obj.byteLength>0);
}
}
}
})(window);
接下來是ws.io/lib目錄,所有套件實作的程式都放在這裡。首先是模組的主檔(ws.io.js):
var server = require('http').createServer();
var Manager = require('./Manager');
exports.listen = function(arg) {
if(typeof arg === 'number') {
server.listen(arg);
return new Manager(server);
} else {
return new Manager(arg);
}
};
接下來是Manager.js,這裡定義了Manager物件:
var Namespace = require('./Namespace');
var Manager = module.exports = function(server) {
this.server = server;
this.namespaces = {};
this.store = {};
this.rooms = {};
this.handlers = {};
this.blobinfo = {};
};
Manager.prototype.of = function(ns) {
var namespace = new Namespace(this.server, this, ns);
return namespace;
};
Manager.prototype.__defineGetter__('sockets', function() {
return this.of('/');
});
Namespace.js,ws模組的事件都在這裡處理,然後轉發到使用者定義的事件處理函數:
var Socket = require('./Socket');
var wss = require('ws').Server;
var Namespace = module.exports = function(server, manager, ns) {
this.namespace = ns;
this.socket = Socket;
this.wss = new wss({server: server});
this.manager = manager;
this.server = server;
this.manager.handlers[ns] = {};
this.manager.namespaces[this.namespace] = {};
this.manager.blobinfo[this.namespace] = {};
}
Namespace.prototype.on = function(conn, handler) {
var self = this;
if(conn == 'connection') {
this.wss.on('connection', function(ws) {
var id = Math.random()*10000 + '-' + new Date().getTime();
var socket = new Socket(self.server, self.manager, self, id, ws);
if(!!self.manager.namespaces[self.namespace]['all']) {
self.manager.namespaces[self.namespace]['all'][id] = socket;
} else {
self.manager.namespaces[self.namespace]['all'] = {};
self.manager.namespaces[self.namespace]['all'][id] = socket;
}
if(!self.manager.handlers[self.namespace]) {
self.manager.handlers[self.namespace] = {};
}
if(!self.manager.handlers[self.namespace][id]) {
self.manager.handlers[self.namespace][id] = {};
}
if(!self.manager.blobinfo[self.namespace][id]) {
self.manager.blobinfo[self.namespace][id] = null;
}
ws.on('message', function(data, flags) {
if(typeof data === 'string') {
var args = JSON.parse(data);
var msg = true;
var tmp = ['open', 'close', 'error', 'ping', 'pong'];
tmp.forEach(function(n) {
if(n===args.name) msg = false;
});
if(!msg) return;
if(args.name==='blob') {
if(!!self.manager.blobinfo[self.namespace]) {
self.manager.blobinfo[self.namespace][id] = args.data;
}
return;
}
if(!!self.manager.handlers[self.namespace][id][args.name]) {
self.manager.handlers[self.namespace][id][args.name].call(self, args.data);
ev = null;
} else {
console.log('no handler: ');
}
} else {
if(!!self.manager.blobinfo[self.namespace][id]) {
var name = self.manager.blobinfo[self.namespace][id].name;
if(!!self.manager.handlers[self.namespace][id][name]) {
var msg = self.manager.blobinfo[self.namespace][id];
msg[msg.blobfield] = data;
delete msg.blobfield;
self.manager.handlers[self.namespace][id][name].call(self, msg);
self.manager.blobinfo[self.namespace][id] = null;
}
}
}
});
ws.on('open', function() {
if(!!self.manager.handlers[self.namespace][id]['open']) {
self.manager.handlers[self.namespace][id]['open'].call(self);
}
});
ws.on('close', function(code, message) {
if(!!self.manager.handlers[self.namespace][id]['close']) {
self.manager.handlers[self.namespace][id]['close'].call(self, code, message);
}
});
ws.on('error', function(error) {
if(!!self.manager.handlers[self.namespace][id]['error']) {
self.manager.handlers[self.namespace][id]['error'].call(self, error);
}
});
ws.on('ping', function(data, flags) {
if(!!self.manager.handlers[self.namespace][id]['ping']) {
self.manager.handlers[self.namespace][id]['ping'].call(self, data, flags);
}
});
ws.on('pong', function(data, flags) {
if(!!self.manager.handlers[self.namespace][id]['pong']) {
self.manager.handlers[self.namespace][id]['pong'].call(self);
}
});
handler.call(socket, socket);
});
}
}
最後是Socket.js物件,大部分使用者操作在這裡實作:
var Socket = module.exports = function(server, manager, namespace, id, ws) {
this.id = id;
this.server = server;
this.manager = manager;
this.ws = ws;
this.namespace = namespace.namespace;
this.flags = {"allnotme":false,"inroom":false};
};
Socket.prototype.on = function(name, handler) {
this.manager.handlers[this.namespace][this.id][name] = handler;
};
Socket.prototype.emit = function(name, data, opt) {
var room = '';
if(!this.flags.inroom) {
room = 'all';
} else {
room = this.flags.inroom;
}
var targets = [];
if(this.flags['allnotme']) {
for(var i in this.manager.namespaces[this.namespace][room]) {
if(this.flags.allnotme) {
if(i.indexOf(this.id)<0) {
targets.push(this._findSocket(this.namespace, room, i));
}
} else {
targets.push(this._findSocket(this.namespace, room, i));
}
}
} else {
targets.push(this);
}
var self = this;
//data['name'] = name;
var isblob = false;
for(var i in data) {
if(Buffer.isBuffer(data[i])) {
var blob = data[i];
delete data[i];
data['name'] = name;
data['blobfield'] = i;
isblob = true;
break;
}
}
targets.forEach(function(socket) {
if(isblob) {
socket.ws.send(JSON.stringify({name:'blob',data:data}));
socket.ws.send(blob, {binary:true,mask:false});
} else {
socket.ws.send(JSON.stringify({name:name,data:data}));
}
});
this.flags['allnotme'] = false;
this.flags['inroom'] = false;
};
Socket.prototype.__defineGetter__('broadcast', function() {
this.flags['allnotme'] = true;
return this;
});
Socket.prototype._findSocket = function(ns, room, id) {
if(!!this.manager) {
if(!!this.manager.namespaces[ns]) {
if(!!this.manager.namespaces[ns][room]) {
if(!!this.manager.namespaces[ns][room][id]) {
return this.manager.namespaces[ns][room][id];
}
}
}
}
};
Socket.prototype.join = function(room) {
if(!!this.manager.namespaces[this.namespace][room]) {
this.manager.namespaces[this.namespace][room][this.id] = this;
} else {
this.manager.namespaces[this.namespace][room] = {};
this.manager.namespaces[this.namespace][room][this.id] = this;
}
};
Socket.prototype.leave = function(room) {
if(!!this.manager.namespaces[this.namespace][room]) {
if(!!this.manager.namespaces[this.namespace][room][this.id]) {
delete this.manager.namespaces[this.namespace][room][this.id];
}
}
var i = 0;
for(var a in this.manager.namespaces[this.namespace][room]) {
i++
}
if(i===0) {
delete this.manager.namespaces[this.namespace][room];
}
}
Socket.prototype.in = function(room) {
var check = false;
if(!!this.manager.namespaces[this.namespace][room]) {
if(!!this.manager.namespaces[this.namespace][room][this.id]) {
check = true;
}
}
if(check) {
this.flags['inroom'] = room;
}
return this;
}
Socket.prototype.set = function(name, value, cb) {
if(!this.manager.store[this.id]) {
this.manager.store[this.id] = {};
}
this.manager.store[this.id][name] = value;
cb();
};
Socket.prototype.get = function(name, cb) {
if(!!this.manager.store[this.id]) {
if(!!this.manager.store[this.id][name]) {
cb(false, this.manager.store[this.id][name]);
} else {
cb(true);
}
} else {
this.manager.store[this.id] = {};
cb(true);
}
};
Socket.prototype.has = function(name, cb) {
if(!!this.manager.store[this.id]) {
if(!!this.manager.store[this.id][name]) {
cb(false, true);
} else {
cb(true, true);
}
} else {
this.manager.store[this.id] = {};
cb(true, false);
}
};
function checkBinary(obj) {
return Buffer.isBuffer(obj);
}
在package.json已經定義好模組依賴性,所以進入ws.io目錄後,執行'npm rebuild'就會把ws安裝在ws.io/node_modules目錄中。
接下來看上述測試test844.html執行的結果:
可以看到在三個網頁中分別選擇三個檔案,這些檔案會出現在所有網頁的列表中,點選以後就可以打開檔案。
OK,就先把資源分享的主題告一段落,明天開始測試WebRTC,並使用他做出視訊會議的功能。
(2012-11-15 22:56補充)
今天中午把ws.io調整了一下,讓namespace能真正運作。這個部分還是繞了幾圈,主要是因為對於ws不夠了解,文件也不太清楚。本來以為他的WebSocket物件的url屬性,就是client請求時的url的pathname(host之後的資源位置),結果這是WebSocket物件當做WebSocket client來使用時才會用到的。實際上在當Server使用時,ws是在configuration中傳給他要handle的pathname,所以一個path可以用一個WebSocket物件來handle,共享一個http伺服器。
了解了之後,其實要調整沒有花很多時間。主要調整的有三支程式首先是Manager.js:
var Namespace = require('./Namespace');
var Manager = module.exports = function(server) {
this.server = server;
this.namespaces = {};
this.store = {};
this.handlers = {};
this.blobinfo = {};
};
Manager.prototype.of = function(ns) {
var namespace = new Namespace(this.server, this, ns);
this.namespaces[ns] = namespace;
return namespace;
};
Manager.prototype.__defineGetter__('sockets', function() {
return this.of('/');
});
還有Namespace.js:
var Socket = require('./Socket'),
wss = require('ws').Server;
var Namespace = module.exports = function(server, manager, ns) {
this.manager = manager;
this.namespace = ns;
this.manager.handlers[ns] = {};
this.manager.namespaces[this.namespace] = {};
this.manager.blobinfo[this.namespace] = {};
this.rooms = {all:{}};
this.wss = new wss({
server: server,
path: ns
});
};
Namespace.prototype.on = function(conn, handler) {
if(conn == 'connection') {
//---------------------
var self = this;
this.wss.on('connection', function(ws) {
if(!!self.manager.namespaces[self.namespace]) {
var id = Math.random()*10000 + '-' + new Date().getTime();
var socket = self.createSocket(id, ws);
} else {
console.log('namespace object not exists.');
return;
}
ws.on('message', function(data, flags) {
console.log('message received to '+id);
console.log(data);
if(typeof data === 'string') {
var args = JSON.parse(data);
var msg = true;
var tmp = ['open', 'close', 'error', 'ping', 'pong'];
tmp.forEach(function(n) {
if(n===args.name) msg = false;
});
if(!msg) return;
if(args.name==='blob') {
if(!!self.manager.blobinfo[self.namespace]) {
self.manager.blobinfo[self.namespace][id] = args.data;
}
return;
}
if(!!self.manager.handlers[self.namespace][id][args.name]) {
self.manager.handlers[self.namespace][id][args.name].call(self, args.data);
ev = null;
} else {
console.log('no handler: ');
}
} else {
if(!!self.manager.blobinfo[self.namespace][id]) {
var name = self.manager.blobinfo[self.namespace][id].name;
if(!!self.manager.handlers[self.namespace][id][name]) {
var msg = self.manager.blobinfo[self.namespace][id];
msg[msg.blobfield] = data;
delete msg.blobfield;
self.manager.handlers[self.namespace][id][name].call(self, msg);
self.manager.blobinfo[self.namespace][id] = null;
}
}
}
});
ws.on('open', function() {
if(!!self.manager.handlers[self.namespace][id]['open']) {
self.manager.handlers[self.namespace][id]['open'].call(self);
}
});
ws.on('close', function(code, message) {
if(!!self.manager.handlers[self.namespace][id]['close']) {
self.manager.handlers[self.namespace][id]['close'].call(self, code, message);
}
delete self.manager.handlers[self.namespace][id];
if(!!self.manager.store[id]) {
delete self.manager.store[id];
}
if(!!self.manager.blobinfo[self.namespace][id]) {
delete self.manager.blobinfo[self.namespace][id];
}
self.manager.namespaces[self.namespace].destroySocket(id);
});
ws.on('error', function(error) {
if(!!self.manager.handlers[self.namespace][id]['error']) {
self.manager.handlers[self.namespace][id]['error'].call(self, error);
}
});
ws.on('ping', function(data, flags) {
if(!!self.manager.handlers[self.namespace][id]['ping']) {
self.manager.handlers[self.namespace][id]['ping'].call(self, data, flags);
}
});
ws.on('pong', function(data, flags) {
if(!!self.manager.handlers[self.namespace][id]['pong']) {
self.manager.handlers[self.namespace][id]['pong'].call(self);
}
});
handler.call(socket, socket);
});
//---------------------
}
};
Namespace.prototype.createSocket = function(id, ws) {
var socket = new Socket(this.manager, this, id, ws);
if(!this.manager.handlers[this.namespace]) {
this.manager.handlers[this.namespace] = {};
}
if(!this.manager.handlers[this.namespace][id]) {
this.manager.handlers[this.namespace][id] = {};
}
if(!this.manager.blobinfo[this.namespace][id]) {
this.manager.blobinfo[this.namespace][id] = null;
}
if(!this.rooms['all']) {
this.rooms['all'] = {};
}
this.rooms['all'][id] = socket;
return socket;
};
Namespace.prototype.destroySocket = function(id) {
for(var i in this.rooms) {
if(!!this.rooms[i][id]) {
delete this.rooms[i][id];
}
}
if(!!this.manager.handlers[this.namespace]) {
if(!!this.manager.handlers[this.namespace][id]) {
delete this.manager.handlers[this.namespace][id];
}
}
if(!!this.manager.blobinfo[this.namespace]) {
if(!!this.manager.blobinfo[this.namespace][id]) {
delete this.manager.blobinfo[this.namespace][id];
}
}
};
最後是大部分操作所在的Socket.js
var Socket = module.exports = function(manager, namespace, id, ws) {
this.id = id;
this.manager = manager;
this.ws = ws;
this.namespace = namespace;
this.flags = {"allnotme":false,"inroom":false,"to":false};
if(!this.manager.handlers[this.namespace.namespace]) {
this.manager.handlers[this.namespace.namespace] = {};
this.manager.handlers[this.namespace.namespace][id] = {};
}
};
Socket.prototype.on = function(name, handler) {
this.manager.handlers[this.namespace.namespace][this.id][name] = handler;
};
Socket.prototype.emit = function(name, data, opt) {
var room = '';
if(!this.flags.inroom) {
room = 'all';
} else {
room = this.flags.inroom;
}
var targets = [];
if(this.flags['allnotme']) {
for(var i in this.namespace.rooms[room]) {
if(this.flags.allnotme) {
if(i.indexOf(this.id)<0) {
targets.push(this._findSocket(room, i));
}
} else {
targets.push(this._findSocket(room, i));
}
}
} else {
targets.push(this);
}
if(this.flags['to']) {
targets = [];
targets.push(this._findSocket(room, this.flags['to']));
}
var self = this;
//data['name'] = name;
var isblob = false;
for(var i in data) {
if(checkBinary(data[i])) {
var blob = data[i];
delete data[i];
data['name'] = name;
data['blobfield'] = i;
isblob = true;
break;
}
}
targets.forEach(function(socket) {
console.log('message reply to '+socket.id);
console.log(data);
if(isblob) {
socket.ws.send(JSON.stringify({name:'blob',data:data}));
socket.ws.send(blob, {binary:true,mask:false});
} else {
socket.ws.send(JSON.stringify({name:name,data:data}));
}
});
this.flags['allnotme'] = false;
this.flags['inroom'] = false;
this.flags['to'] = false;
};
Socket.prototype.__defineGetter__('broadcast', function() {
this.flags['allnotme'] = true;
return this;
});
Socket.prototype.to = function(id) {
this.flags.allnotme = false;
this.flags.inroom = false;
this.flags.to = id;
return this;
}
Socket.prototype._findSocket = function(room, id) {
if(!!this.namespace.rooms[room]) {
if(!!this.namespace.rooms[room][id]) {
return this.namespace.rooms[room][id];
}
}
};
Socket.prototype.join = function(room) {
if(!!this.namespace.rooms[room]) {
this.namespace.rooms[room][this.id] = this;
} else {
this.namespace.rooms[room] = {};
this.namespace.rooms[room][this.id] = this;
}
};
Socket.prototype.joined = function(room) {
return (!!this.namespace.rooms[room] && !!this.namespace.rooms[room][this.id]);
}
Socket.prototype.leave = function(room) {
if(!!this.namespace.rooms[room]) {
if(!!this.namespace.rooms[room][this.id]) {
delete this.namespace.rooms[room][this.id];
}
}
var i = 0;
for(var a in this.namespace.rooms[room]) {
i++
}
if(i===0) {
delete this.namespace.rooms[room];
}
}
Socket.prototype.in = function(room) {
var check = false;
if(!!this.namespace.rooms[room]) {
if(!!this.namespace.rooms[room][this.id]) {
check = true;
}
}
if(check) {
this.flags['inroom'] = room;
}
return this;
}
Socket.prototype.set = function(name, value, cb) {
if(!this.manager.store[this.id]) {
this.manager.store[this.id] = {};
}
this.manager.store[this.id][name] = value;
cb();
};
Socket.prototype.get = function(name, cb) {
if(!!this.manager.store[this.id]) {
if(!!this.manager.store[this.id][name]) {
cb(false, this.manager.store[this.id][name]);
} else {
cb(true);
}
} else {
this.manager.store[this.id] = {};
cb(true);
}
};
Socket.prototype.has = function(name, cb) {
if(!!this.manager.store[this.id]) {
if(!!this.manager.store[this.id][name]) {
cb(false, true);
} else {
cb(true, true);
}
} else {
this.manager.store[this.id] = {};
cb(true, false);
}
};
function checkBinary(obj) {
if(Buffer.isBuffer(obj)) {
if(!!obj.copy) {
return true;
}
}
return false;
}
使用方式沒有改變,只是可以利用
io.of(pathname).on('connection', function(socket){
socket.on('client send event', function(data) {
socket.emit('server send event', data);
});
});
的方式,同時服務更多的子目錄。
(2012-11-17 1:45 補充)
把ws.io放到github了:https://github.com/fillano/ws.io
有 git 下來了,但是 npm rebuild 有錯誤!!
% npm rebuild
npm ERR! TypeError: Object #<Object> has no method 'trim'
npm ERR! at /usr/local/Cellar/node/0.8.14/lib/node_modules/npm/node_modules/read-package-json/read-json.js:494:39
npm ERR! at Array.forEach (native)
npm ERR! at depObjectify (/usr/local/Cellar/node/0.8.14/lib/node_modules/npm/node_modules/read-package-json/read-json.js:493:22)
npm ERR! at objectifyDep_ (/usr/local/Cellar/node/0.8.14/lib/node_modules/npm/node_modules/read-package-json/read-json.js:484:30)
npm ERR! at /usr/local/Cellar/node/0.8.14/lib/node_modules/npm/node_modules/read-package-json/read-json.js:471:33
npm ERR! at Array.forEach (native)
npm ERR! at objectifyDeps (/usr/local/Cellar/node/0.8.14/lib/node_modules/npm/node_modules/read-package-json/read-json.js:470:26)
npm ERR! at final (/usr/local/Cellar/node/0.8.14/lib/node_modules/npm/node_modules/read-package-json/read-json.js:307:17)
npm ERR! at /usr/local/Cellar/node/
看起來像是package.json的問題,我測試一下看看。
請git pull一次,我改過package.json了。如果之前沒有安裝過ws,可以在目錄中npm intall,之前安裝過,則可npm rebuild。
OK了! 讚! :-)