这里有新鲜出炉的 Javascript 教程,程序狗速度看过来!
Javascript 是一种由 Netscape 的 LiveScript 发展而来的原型化继承的基于对象的动态类型的区分大小写的客户端脚本语言,主要目的是为了解决服务器端语言,比如 Perl,遗留的速度问题,为客户提供更流畅的浏览效果。
Node.js 可以用很少代码简单地实现一个 Web 服务,并且它有一个非常活跃的社区,通过 Node 出色的包管理机制(NPM)可以非常容易获得各种扩展支持。 对简单的应用场景 Node.js 实现 REST 是一个非常合适的选择。 本文介绍如何用 Node.js 实现 REST 服务。
RESTful 基础概念
REST(Representational State Transfer)描述了一个架构样式的网络系统,它首次出现在 2000 年 Roy Fielding 的博士论文中。在 REST 服务中,应用程序状态和功能可以分为各种资源。资源向客户端公开,客户端可以对资源进行增删改操作。资源的例子有:应用程序对象、数据库记录、算法等等。
REST 通过抽象资源,提供了一个非常容易理解和使用的 API,它使用 URI (Universal Resource Identifier) 唯一表示资源。REST 接口使用标准的 HTTP 方法,比如 GET、PUT、POST 和 DELET 在客户端和服务器之间传输状态。
狭义的 RESTful 关注点在于资源,使用 URL 表示的资源及对资源的操作。Leonard Richardson 和 Sam Ruby 在他们的著作 RESTful Web Services 中引入了术语 REST-RPC 混合架构。REST-RPC 混合 Web 服务不使用信封包装方法、参数和数据,而是直接通过 HTTP 传输数据,这与 REST 样式的 Web 服务是类似的。但是它不使用标准的 HTTP 方法操作资源。
和传统的 RPC、SOA 相比,RESTful 的更为简单直接,且构建于标准的 HTTP 之上,使得它非常快速地流行起来。
Node.js 可以用很少代码简单地实现一个 Web 服务,并且它有一个非常活跃的社区,通过 Node 出色的包管理机制(NPM)可以非常容易获得各种扩展支持。
对简单的应用场景 Node.js 实现 REST 是一个非常合适的选择(有非常多的理由选择这个或者那个技术栈,本文不会介入各种语言、架构的争论,我们着眼点仅仅是简单)。
应用样例场景
下面,就用一个 App 游戏排行榜后台服务来说明一下如何用 Node.js 快速地开发一个的 RESTful 服务。
当 App 游戏玩家过关时,会提交游戏过关时间(秒)数值到 REST 服务器上,服务器记录并对过关记录进行排序,用户可以查看游戏 TOP 10 排行榜。
游戏应用提交的数据格式使用 JSON 表示,如下:
- {
- "id": "aaa",
- "score": 9.8,
- "token": "aaa-6F9619FF-8B86-D011-B42D-00C04FC964FF"
- };
Id 为用户输入的用户名,token 用于区别不同的用户,避免 id 重名,score 为过关所耗费的时间(秒)。
可以使用 curl 作为客户端测试 RESTful 服务。
提交游戏记录的命令如下:
- curl -d "{\"cmd\":1,\"record\":{\"id\":\"test11\",\"score\":29.8,\"token\":\"aaa\"}}" http://localhost:3000/leaderboards
这个命令的语义不仅仅是狭义的 REST 增删改,我们为它添加一个 cmd 命令,实际上通过 POST 一个 JSON 命令,把这个服务实现为 REST-RPC。
删除游戏记录的命令格式如下:
- curl -X DELETE http://localhost:3000/leaderboards/aaa
或(使用 REST-RPC 语义)
- curl -d "{\"cmd\":2,\"record\":{\"id\":\"test11\"}}" http://localhost:3000/leaderboards
查看 TOP 10 命令如下:
- curl http://localhost:3000/leaderboards
标准 REST 定义中,POST 和 PUT 有不同含义,GET 可以区分单个资源或者资源列表。对这个应用我们做了简化,ADD 和 UPDATE 都统一使用 POST,对单个资源和列表也不再区分,直接返回 TOP 10 数据。
一些准备工作
安装 Node.js
本文使用的版本是 v5.5.0。
寻找一款方便的 IDE
本文作者使用 Sublime 敲打代码,eclipse+nodeclipse 生成框架代码和调试。
Node.js 中基础的 HTTP 服务器
在 Node 中,实现一个 HTTP 服务器是很简单的事情。在项目根目录下创建一个叫 app.js 的文件,并写入以下代码:
- var http = require("http");
- http.createServer(function(request, response) {
- response.writeHead(200, {
- "Content-Type": "text/plain"
- });
- response.write("Hello World");
- response.end();
- }).listen(3000);
用 Node.js 执行你的脚本:
- node server.js
打开浏览器访问 http://localhost: 3000/,你就会看到一个写着 "Hello World" 的网页。
即使完全不懂 Node,也可以非常直观的看到这里通过 require 引入了一个 http 模块,然后使用 createServer 创建 HTTP 服务对象,当收到客户端发出的 HTTP 请求后,将调用我们提供的函数,并在回调函数里写入返回的页面。
接下来,我们将把这个简单的应用扩展为一个 RESTful 服务。
简单直观的 RESTful 服务
现在需要超越 "hello world",我们将修改之前的 http 回调函数,根据请求类型返回不同的内容。
代码如下:
- var server = http.createServer(function(req, res) {
- var result;
- switch (req.method) {
- case 'POST':
- break;
- case 'GET':
- break;
- case 'DELETE':
- break;
- }
- });
通过 req.method,可以得到请求的类型。
1. 增加和修改
其中 POST 请求,是要求我们添加或更新记录,请求的数据可以通过回调得到。
代码如下:
- var item = '';
- req.setEncoding('utf8');
- req.on('data',
- function(chunk) {
- item += chunk;
- });
- req.on('end',
- function() {
- try {
- var command = JSON.parse(item);
- console.log(commandNaNd + ';' + command.record.id + ':' + command.record.score + '(' + command.record.token + ')');
- if (commandNaNd === CMD.UPDATE_SCORE) {
- addRecord(command.record, result);
- }
- else if (commandNaNd === CMD.DEL_USE) {
- db('leaderboards').remove({
- id: command.record.id
- });
- }
- res.end(JSON.stringify(result));
- }
- catch(err) {
- result.comment = 'Can\'t accept post, Error: ' + err.message;
- result.code = ErrCode.DataError;
- console.log(result.comment);
- res.end(JSON.stringify(result));
- }
- });
当框架解析读入数据时,会调用 req.on('data', function(chunk) 提供的回调函数,我们把请求的数据记录在 item 中,一有数据,就调用 item += chunk,直到数据读入完成,框架调用 req.on('end', function() 回调,在回调函数中,使用 JSON.parse 把请求的 JSON 数据还原为 Javascript 对象,这是一个命令对象,通过 commandNaNd 可以区分是需要添加或删除记录。
addRecord 添加或更新记录。
代码如下:
- function addRecord(record,result) {
- var dbRecord = db('leaderboards').find({ id: record.id });
- if (dbRecord){
- if (dbRecord.token !== record.token){
- result.code= ErrCode.DataError;
- result.comment= 'User exist';
- }
- else{
- db('leaderboards')
- .chain()
- .find({id:record.id})
- .assign({score:record.score})
- .value();
- result.comment= 'OK, New Score is '+ record.score;
- }
- }
- else{
- db('leaderboards').push(record);
- }
- }
命令执行结束后,通过 res.end(JSON.stringify(result)) 写入返回数据。返回数据同样是一个 JSON 字符串。
在这个简单的样例中,使用了 lowdb(https://github.com/typicode/lowdb#license?utm_source=ourjs.com)。
LowDB 是一个基于 Node 的纯 Json 文件数据库,它无需服务器,可以同步或异步持久化到文件中,也可以单纯作为内存数据库,非常快速简单。LowDB 提供 Lo-Dash 接口,可以使用类似. find({id:record.id}) 风格的方法进行查询。通过 chain(),可以把多个操作连接在一起,完成数据库的查找更新操作。
这个简单的数据库实现,如果游戏仅保存得分高的用户记录,实际上已经可以满足我们的应用了。对更复杂的应用,Node 也提供了各种数据库连接模块,比较常见的是 mongodb 或 mysql。
2. 返回 TOP 10
通过查询数据库里的数据,首先使用. sortBy('score'),取前 10 个,返回到记录集中,然后使用 JSON.stringify 转为字符串,通过 res 返回。
代码如下:
- var records = [];
- var topTen = db('leaderboards')
- .chain()
- .sortBy('score')
- .take(10)
- .map(function(record) {
- records.push(record);
- })
- .value();
- res.end(JSON.stringify(records));
3. 删除记录
RESTful 的删除资源 ID 一般带着 URL 里,类似 "http://localhost:3000/leaderboards/aaa",因此使用 var path = parse(req.url).pathname 解析出资源 ID"aaa"。
代码如下:
- case 'DELETE':
- result= {code:ErrCode.OK,comment: 'OK'};
- try {
- var path = parse(req.url).pathname;
- var arrPath = path.split("/");
- var delObjID= arrPath[arrPath.length-1];
- db('leaderboards').remove({id:delObjID});
- res.end(JSON.stringify(result));
- break;
- }
至此,我们实现了一个带基本功能,可真正使用的 RESTful 服务。
实际应用场合的 REST 服务可能会更复杂一些,一个应用或者会提供多个资源 URL 的服务;或者还同时提供了基本的 WEB 服务功能;或者 REST 请求带有文件上传等等。
这样,我们的简单实现就不够看了。
Express 框架
Express 是一个基于 Node.js 平台的 web 应用开发框架,它提供一系列强大的特性,帮助你创建各种 Web 应用。
可以使用 eclipse+nodeclipse 生成默认的 express 应用框架。一个 express 应用如下所示
- var express = require('express')
- , routes = require('./routes')
- , user = require('./routes/user')
- , http = require('http')
- , path = require('path');
- var app = express();
- // all environments
- app.set('port', process.env.PORT || 3000);
- app.set('views', __dirname + '/views');
- app.set('view engine', 'ejs');
- app.use(express.favicon());
- app.use(express.logger('dev'));
- app.use(express.bodyParser());
- app.use(express.methodOverride());
- app.use(app.router);
- app.use(express.static(path.join(__dirname, 'public')));
- // development only
- if ('development' == app.get('env')) {
- app.use(express.errorHandler());
- }
- app.get('/', routes.index);
- app.get('/users', user.list);
- http.createServer(app).listen(app.get('port'), function(){
- console.log('Express server listening on port ' + app.get('port'));
- });
Express 是一个 Web 服务器实现框架,虽然我们用不上页面和页面渲染,不过作为样例,还是保留了缺省生成的页面,并对其进行简单解释。
在这个生成的框架代码里,选择 view engine 模板为 ejs,这是一个类似 JSP 的 HTML 渲染模板引擎,app.get('/', routes.index) 表示把 HTTP 的 "/" 请求路由给 routes.index 处理,routes.index 对应于工程结构下的 index.js 文件处理,其内容如下:
- exports.index = function(req, res){
- res.render('index', { title: 'Express' });
- };
这个函数调用了对应 view 目录下的 index.ejs 模板,并把 {title:'Express'} 传递给 ejs 模板,在 ejs 模板中,可以使用 <%= title %> 得到传入的 json 对象。
Express 框架实现 RESTful 服务
首先我们实现一个自己的服务类,在 routes 子目录中,创建 leaderboards.js 文件,这个文件结构大致为定义 REST 对应的操作函数。
- exports.fnList = function(req, res) {
- };
- exports.fnGet = function(req, res) {
- };
- exports.fnDelete = function(req, res) {
- };
- exports.fnUpdate = function(req, res) {
- };
- exports.fnAdd = function(req, res) {
- };
在 app.js 文件中,需要把 HTTP 请求路由给对应函数。
- var leaderboards = require('./routes/leaderboards');
- …
- app.get('/leaderboards', leaderboards.fnList);
- app.get('/leaderboards/:id', leaderboards.fnGet);
- app.delete('/leaderboards/:id', leaderboards.fnDelete);
- app.post('/leaderboards', leaderboards.fnAdd);
- app.put('/leaderboards/:id', leaderboards.fnUpdate);
这样就把标准 Web 服务请求路由到 leaderboards 处理。因为请求中带有 POST 数据,可以使用
- var bodyParser = require('body-parser');
- // parse various different custom JSON types as JSON
- app.use(bodyParser.json({ limit: '1mb',type: 'application/*' }));
把请求的 JSON 结构解析后添加到 req.body 中。Limit 是为避免非法数据占用服务器资源,正常情况下,如果解析 JSON 数据,type 应该定义为'application/*+json', 在本应用里,为避免某些客户端请求不指明类型,把所有输入都解析为 JSON 数据了。
'body-parser'是一个很有用的库,可以解析各种类型的 HTTP 请求数据,包括处理文件上传,详细可以参见 https://www.npmjs.com/package/body-parser。
有了这个路由映射机制,我们不再需要考虑 URL 和数据的解析,仅仅指定路由,实现对应函数就可以了。
- exports.fnList = function(req, res) {
- var result = {
- code: ErrCode.OK,
- comment: 'OK'
- };
- try {
- var records = [];
- var topTen = db('leaderboards')
- .chain()
- .sortBy('score')
- .take(10)
- .map(function(record) {
- records.push(record);
- })
- .value();
- res.end(JSON.stringify(records));
- } catch(err) {
- result.comment = 'Can\'t get leaderboards, Error: ' + err.message;
- result.code = ErrCode.DataError;
- console.log(result.comment);
- res.end(JSON.stringify(result));
- }
- return;
- };
对类似 http://localhost:3000/leaderboards/aaa 的 URL,express 已经解析到 req.param 里了,可以通过 req.param('id') 得到。
- exports.fnDelete = function(req, res) {
- var result = {
- code: ErrCode.OK,
- comment: 'OK'
- };
- try {
- var resID = req.param('id');
- db('leaderboards').remove(resID);
- res.end(JSON.stringify(result));
- console.log('delete record:' + req.param('id'));
- }
- catch(err) {
- result.comment = 'Can\'t DELETE at ' + req.param('id') + ', Error: ' + err.message;
- result.code = ErrCode.DelError;
- console.log(result.comment);
- res.end(JSON.stringify(result));
- }
- };
使用了 bodyParser.json() 后,对 POST 请求中的 JSON 数据,已经解析好放到 req.body 里了,代码中可以直接使用。
- function processCmd(req, res) {
- var result = {
- code: ErrCode.OK,
- comment: 'OK'
- };
- try {
- var command = req.body;
- console.log(req.bodyNaNd + ';' + req.body.record.id + ':' + req.body.record.score + '(' + req.body.record.token + ')');
- if (commandNaNd === CMD.UPDATE_SCORE) {
- addRecord(command.record, result);
- console.log('add record:' + command.record.id);
- }
- else if (commandNaNd === CMD.DEL_USE) {
- db('leaderboards').remove({
- id: command.record.id
- });
- console.log('delete record:' + command.record.id);
- }
- res.end(JSON.stringify(result));
- }
- catch(err) {
- result.comment = 'Can\'t accept post, Error: ' + err.message;
- result.code = ErrCode.DataError;
- console.log(result.comment);
- res.end(JSON.stringify(result));
- }
- return;
- }
- exports.fnUpdate = function(req, res) {
- processCmd(req, res);
- };
- exports.fnAdd = function(req, res) {
- processCmd(req, res);
- };
使用 express 的好处是有一些细节可以扔给框架处理,代码结构上也更容易写得清晰一些。
来源: http://www.phperz.com/article/17/0812/343899.html