本文系原创,转载请注明出处
ES6 的出品为 JS 成为企业级语言扫清障碍,与之配套的,我们需要一个真正的企业级框架。Express 像一个精巧的微内核,不足以支撑起一个大项目。以下是 LoopBack 的一些入门知识,它是一个真正的企业级框架,随着使用的深入,读者将会发现它更多的用法和优秀的特性。本篇将对它的主要用法做一个详细的介绍 。
LoopBack 是建立在 Express 基础上的企业级 Node.js 框架,这个框架支持
- 只需编写少量代码就能创建动态端到端的 REST API
- 支持主流的数据源,例如 Mongodb、SOAP、MySQL 等和 REST API 的数据。
- 一致化的模型关系和对 API 访问的权限控制
- 可使用内置的用于移动应用场景下的地理定位、文件服务以及消息推送
- 提供 Android、iOS 和 JavaScript 的 SDK,轻松创建客户端应用程序
- 支持在云端或者本地部署服务
它可以像 Express 那样被使用。除此之外,LoopBack 作为一个面向企业级的 Web 框架,提供了更丰富的功能,这在我们添加模型,权限控制,连接数据源等操作时,极大的提升我们的效率。例如可以通过修改配置增加模型,并指定模型的数据源。它默认提供了一些基础模型,例如 User 这个模型包含了注册登录等逻辑。我们可以非常方便的继承这些内建模型,实现个性化的定制。它还提供了 Hook 编程的机制。它同时提供了可视化的调试页面,自动生成对应的前端 SDK 。这些功能在开发大型 Web 服务的时候,将帮助我们更容易查看和管理项目。本篇将会详细的介绍 LoopBack 的使用。
安装与运行
StrongLoop 是生成 LoopBack 框架的工具程序,我们首先安装它。运行
npm install -g strongloop
安装完成之后,可以运行 slc -v 查看是否安装成功(需要事先建立 slc 的软链接)。
紧接着,我们运行 slc loopback,这是一个交互式的命令,首先提示用户输入项目名称,这里就输入 loopback。接下来根据引导,按步骤填写相应信息即可。输入项目名称之后,接下来的步骤我们可以直接敲回车即可。最后 strongloop 会帮助我们创建 loopback 目录,并且在目录下创建默认的项目文件。我们进入 loopback 文件夹,运行 slc loopback:model,创建一个模型。我们可以随意输一个模块名,例如 cool 。接下来要求选择数据源,这里先选择默认值 db (memory),敲回车即可。下一步要求选择模型的基类,也选用默认值 PersistedModel,代表此模型与持久化数据源连接。接下来,会出现
Expose cool via the REST API? Yes
当我们选择 ‘Y’,LoopBack 会为我们的模型生成 REST API 的代码。之后直接点击回车完成步骤即可。我们查看一下 loopback 目录都包含哪些文件
LoopBack 符合模型(M)—视图(V)—控制器©的设计规范。上图中的 server 文件夹,包含了程序的启动代码,配置信息。路由部分的逻辑也在 server 目录下。Express 支持将路由分组,因此 server 目录可以对应 MVC 的 C。client 目录包含给用户展示的前端代码,也包含由后台处理的用于生成页面的模板。这个目录对应 MVC 的 V。common 目录下有一个 models 文件夹,这里的代码处理具体的业务逻辑和数据,对应 M。
我们进入 server 文件夹,运行 node server.js,可以看到如下信息
Web server listening at: http://0.0.0.0:3000 Browse your REST API at http://0.0.0.0:3000/explorer
在本地打开浏览器访问 http://0.0.0.0:3000/explorer, 可以看到如下界面
这是 LoopBack 集成的一个非常棒的功能,它列出了所有对外的模型和每一个模型的接口。LoopBack 默认生成的接口都是 REST API 风格。点击某一个接口,界面会展开,展开的界面提供了测试功能。我们可以将构造好的参数填入输入框,然后查看接口的返回结果。
LoopBack 为模型默认生成的接口包括
读系列
- exists - 模型的数据源中对应id项是否存在
- findById - 根据id返回数据源对应的项
- find - 返回所有满足匹配查询条件的项
- count - 返回满足匹配查询条件的项目个数
写系列
- create - 创建新项
- upsert - 更新项
- destroyById - 删除为指定id的项
默认这些 REST API 可以被访问。如果需要屏蔽某一个,可以在模型的 JS 文件内部,例如cool.js,内部增加调用
module.exports = function(Cool) {
Cool.disableRemoteMethod('findById', true);
// 省略...
这样就能屏蔽掉 findById 这个接口。
当 LoopBack 服务启动的时候,它会按照文件名的字符串顺序,加载位于 /server/root 里面的所有 后缀名为 .js 的文件。这提供了一个初始化整个系统的机会。例如我们可以利用这个机制挂载模块,或者将初始化数据库的代码放到这个目录。
在浏览器中打开 explorer 调试接口虽然方便,但在实际项目中,别人随意可以查看这个界面存在着一定的风险。这时候就可以利用 LoopBack 加载 server/root 里面 JS 文件的机制,为 explorer 的访问增加权限控制。接下来在 server/root 里新建一个文件,起名为 explorer.js,这个文件的内容是
module.exports = function mountLoopBackExplorer(server) {
var explorer;
try {
explorer = require('loopback-component-explorer');
} catch(err) {
// Print the message only when the app was started via `server.listen()`.
// Do not print any message when the project is used as a component.
server.once('started', function(baseUrl) {
console.error(
'Run `npm install loopback-component-explorer` to enable the LoopBack explorer'
);
});
return;
}
//用户名 test 密码 123456
server.use('/explorer', require('node-basicauth')({'test': '123456' }));
server.use('/explorer', explorer.routes(server, { basePath: server.get('restApiRoot') }));
server.once('started', function() {
var baseUrl = server.get('url').replace(/\/$/, '');
console.log('查看你的 REST API %s%s', baseUrl, '/explorer');
});
};
以上代码使用了一个新的模块 node-basicauth,因此在启动服务前需要先安装好,回到 loopback 目录运行
npm install node-basicauth
然后还需要修改 server/component-config.json 文件的内容,将默认的配置去除,或者直接删除这个文件。在 server 目录下重新启动服务,然后在本地用浏览器打开网址 http://0.0.0.0:3000/explorer ,出现提示,要输入用户名和密码。
mountLoopBackExplorer 函数的参数 server 是 LoopBack 传进来的,这个对象代表 LoopBack 程序本身。它在server.js文件开头创建
var app = module.exports = loopback();
app.models 包含了所有的模型,假如我们希望访问 cool 这个模型,可以通过如下形式
app.models.cool
得到此模型对象,之后便可以调用这个对象的函数。
路由与权限控制
LoopBack 添加路由的方式与 Express 一致。LoopBack 实现了 MVC 模型,在这个框架下,它提供了另外一种添加模块并导出 API 的方式。我们先来看 Express 添加路由的方法。默认生成的 server/server.js文件不大,大致内容为
var loopback = require('loopback');
var boot = require('loopback-boot');
var app = module.exports = loopback();
app.start = function() {
// start the web server
return app.listen(function() {
app.emit('started');
var baseUrl = app.get('url').replace(/\/$/, '');
console.log('Web server listening at: %s', baseUrl);
if (app.get('loopback-component-explorer')) {
var explorerPath = app.get('loopback-component-explorer').mountPath;
console.log('Browse your REST API at %s%s', baseUrl, explorerPath);
}
});
};
// Bootstrap the application, configure models, datasources and middleware.
// Sub-apps like REST API are mounted via boot scripts.
boot(app, __dirname, function(err) {
if (err) throw err;
// start the server if `$ Node server.js`
if (require.main === module)
app.start();
});
在 server.js 控制路由的逻辑中,应该将路由分类,以后方便管理。在 server 目录中新建一个文件夹,命名为 routes,然后新建一个 test.js 的文件,内容为
var router = module.exports.test_router = require('loopback').Router();
router.get('/name', function(req, res, next) {
res.send('visit test/name');
});
router.get('/', function(req, res) {
res.send('visit test root');
});
启动服务后,用户访问 /test/ 或者 /test/name 的时候要能正确返回。因此需要修改 server.js,建立 test 的路由,以下是修改之后的 server.js 内容
var loopback = require('loopback');
var boot = require('loopback-boot');
var path = require('path');
var app = module.exports = loopback();
app.start = function() {
// start the web server
return app.listen(function() {
app.emit('started');
var baseUrl = app.get('url').replace(/\/$/, '');
console.log('Web server listening at: %s', baseUrl);
if (app.get('loopback-component-explorer')) {
var explorerPath = app.get('loopback-component-explorer').mountPath;
console.log('Browse your REST API at %s%s', baseUrl, explorerPath);
}
});
};
app.use('/test',require(path.resolve(__dirname, './routes/test.js')).test_router);
// Bootstrap the application, configure models, datasources and middleware.
// Sub-apps like REST API are mounted via boot scripts.
boot(app, __dirname, function(err) {
if (err) throw err;
// start the server if `$ Node server.js`
if (require.main === module)
app.start();
});
process.on('uncaughtException', function (err){
console.error('uncaughtException: %s', err.message);
});
重新启动服务,使用浏览器访问 http://0.0.0.0:3000/test/name 和 http://0.0.0.0:3000/test/ 可以看到返回的结果。以上代码除了添加了一个 test 的路由,还监听了 uncaughtException 这个事件,后面的部分在讲解 cluster 模式的时候,我们将会看到对这个事件更合理的处理。
按照上述方式添加路由非常简单,但这些导出的 API 无法在 explorer 页面中查看和调试,也难以对API 进行权限控制等操作。好在 LoopBack 框架提供了一套机制,通过修改配置文件就能增加模型和导出 REST API,并且能够方便的对接口进行权限控制。之前在 common/models 文件夹里,我们用 slc 生成了一个模型 cool,这个目录下包含两个文件
cool.js cool.json
cool.json 是对这个模型的配置,这个文件包含的内容是
{
"name": "cool",
"base": "PersistedModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {},
"acls": [],
"methods": {}
}
这个文件定义了几个字段,base 代表 cool 模型的基类,acls 字段用于权限控制。relations 定义了模型之间的关系,properties 定义了模型对应的持久化字段。cool.js 中包含这个模型的处理逻辑,这个文件的初始内容是
module.exports = function(Cool) {
};
现在我们给 cool 添加一个 get 请求,并对这个 API 添加不同类型的权限。要添加新接口,需要在cool.js 中编写新接口的代码,例如我们添加一个名字为 test 的接口,这个接口接收一个字符串,然后返回这个字符串,代码大致如下
module.exports = function(Cool) {
Cool.test = function(content, cb){
cb(null, content);
};
Cool.remoteMethod(
'test'
,{
description: '输入一个字符串,返回它'
,accepts: [
{arg: 'content', type: 'string',required: true}
]
,http: {path:'/test', verb: 'get'}
,returns : { arg: 'ret', type:"string", root: true,required: true}
}
);
};
LoopBack 是一个优秀而易用的框架,代码就是最好的教科书。经过修改之后,我们重新启动服务,用浏览器打开 explorer,测试我们新添加的接口如下图
我们在输入框随意输入一个字符串,点击测试按钮,可以立即查看返回结果。可见 LoopBack 框架内,给模型添加一个接口非常方便,新接口添加完毕,浏览器打开页面就可以直接调试。下面我们修改cool.json 文件,来实现对这个接口的权限控制。我们为 acls 这个字段添加如下内容
"acls": [
{
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
}
]
保存文件之后重启服务,在 explorer 内重新测试,我们发现接口已经不可访问
{
"error": {
"name": "Error",
"status": 401,
"message": "Authorization Required",
"statusCode": 401,
"code": "AUTHORIZATION_REQUIRED",
"stack": "Error: Authorization Required"
}
}
principalId 是指对谁进行权限控制。在 LoopBack 中,我们常用的几个取值包括
$everyone $owner $authenticated 自定义角色,例如 admin
$everyone 按照字面意思比较好理解。$owner 和 $authenticated 以及自定义角色在启用用户Token 的情况下使用。例如一个登录用户,在访问 REST API 时会带上他的 Token 信息,$owner 代表这个用户只能访问自己的信息,而对其他用户的数据没有访问权限。如果换成 $authenticated,那么只要用户的 Token 信息合法,就可以调用这个接口。下面我们继续修改 acls 这个键,使得 test 接口重新可访问,我们添加一个针对 test 接口的访问控制项
"acls": [
{
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
}
,{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW",
"property": "test"
}
]
principalId 设置为对所有的人进行访问控制,permission 字段设置为允许。重启服务后,这个接口变得可访问。accessType 的取值有三个,分别是 READ,WRITE 和 EXECUTE。一般来讲,我们自定义的接口 accessType 使用 EXECUTE 修饰,principalId 使用 $everyone 或者 $authenticated 修饰。对于每一个模型,LoopBack 框架会自动生成一系列固定模式的 REST API,用于存取模型数据。这部分接口的 accessType 常会用到 READ 和 WRITE。接下来,我们基于 LoopBack 的一个内建模型 User,建立一个用户体系,允许使用者创建新用户,生成用户 Token,然后再进一步讨论 LoopBack 的权限控制,之后本章还将讨论 LoopBack 中模型之间的关系。
添加新模型
我们接下来添加一个模型 Ouser,并建立服务的用户体系。进入 server 目录,其中有一个配置文件 model-config.json,这个文件中记录了所有的模型。打开此文件,在 cool 之后,添加 Ouser 模型。
"cool": {
"dataSource": "db",
"public": true
},
"Ouser":{
"dataSource": "db",
"public": true
}
之后,在 common/models 目录下,新建两个文件
ouser.js ouser.json
下面我们编辑ouser.json文件的内容,如下
{
"name": "Ouser",
"plural": "ousers",
"base": "User",
"idInjection": true,
"properties": {
"nickname": {
"type": "string"
}
},
"validations": [],
"relations": {
},
"acls": [
{
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
},
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
},
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW"
}
],
"methods": {}
}
Ouser 继承自 User,对应的 JS 文件在 LoopBack 模块目录的 common/models 文件夹中。User 代表了对用户操作的模型,包含了注册,登录等逻辑。接着编写 ouser.js 文件内容,如下
module.exports = function(Ouser) {
};
因为模型 Ouser 继承自 User,因此源文件中可以调用 User 定义的方法,User 对外的接口也被 Ouser 模型继承。在 model-config.json 文件中,我们屏蔽掉 User,使这个模型的接口不对外。
"User": {
"dataSource": "db",
"public":false
}
接下来,我们重启服务,浏览器打开 explorer,可以看到,刚才新添加的模型 Ouser 已经存在,并且包含了一系列 REST API。这些 API 是 LoopBack 自动添加的,根据英文注释,不难理解每一个接口的含义。我们可以直接在 explorer 的界面中,创建一个新用户,先找到创建用户的接口
在输入框中填写如下内容
{
"nickname":"Json"
,"email":"[email protected]"
,"password":"12345"
}
nickname 是模型 Ouser 的一个属性。另外两个是基类 User 自带的属性。然后点击Try it out,返回如下内容
{
"nickname": "Json",
"email": "[email protected]",
"id": 1
}
这代表新创建了一个用户。接下来调用 Ouser 的 /ousers/login 方法,试着尝试使用邮箱和密码登录。在 credentials 输入框输入如下内容
{"email":"[email protected]"
,"password":"12345"
}
点击发送按钮,将返回如下数据
{
"id": "F7IliK3irck8ILWkAdEucYGoXw67j50GTYKIsurYx1EuZb61QcohEsAxcqLw0RMS",
"ttl": 1209600,
"created": "2016-07-24T06:20:28.436Z",
"userId": 1
}
这代表我们已经登录成功,并返回一个此用户的 Token 信息。目前服务使用的是基于 memory 存储方案,服务重启,数据丢失。复制这个 Token 信息,将他拷贝到如下图所示的输入框,然后点击 Set Access Token 按钮
接着,我们点开 get /ousers/{id} 这个接口,在 id 对应的输入框输入1,点击 Try it out 按钮,将返回这个 id 为1的用户对应的信息。
以上过程演示了注册,登录和根据有效 Token 访问用户信息的步骤。而真正用于实际的步骤比这个要复杂一些。现在回过头再来看看,模型是怎么添加的,在 model-config.json,我们添加了如下内容
"Ouser":{
"dataSource": "db",
"public": true
}
dataSource 字段的内容是 db,表示 Ouser 使用名称为 db 的数据源。这个数据源在同级目录的 datasources.json 中定义,我们看一下这个文件的内容
{
"db": {
"name": "db",
"connector": "memory"
}
}
connector 字段的值为 memory,它代表基于内存的持久化。刚才创建的新用户和对数据的任何修改,服务重启之后都将消失。在实际的使用中,服务的数据源应该来自可持久化的数据库。例如可修改为一个使用 mongodb 存储的数据源,为这个文件添加如下内容
{
"db": {
"name": "db",
"connector": "memory"
},
"mongods": {
"host": "localhost",
"port": 27017,
"url": "mongodb://name:pass@localhost:27017/dbname",
"database": "dbname",
"username": "name",
"password": "pass",
"name": "mongods",
"connector": "mongodb"
}
}
url 字段中的 name,pass 和 dbname 以实际的为准。database 字段代表数据库名称,username 代表 mongodb 的用户名,password 是数据库连接密码。使用 mongodb 做存储,需要先安装 mongodb 的连接器,在工程根目录下运行
npm install --save loopback-connector-mongodb
这样,在服务启动后,LoopBack 根据这个配置文件给出的连接 url,自动去连接 mongodb 数据库。我们希望所有的模型使用 mongodb 作为数据源,那就需要全面的修改 model-config.json。修改后如下
{
"_meta": {
"sources": [
"loopback/common/models",
"loopback/server/models",
"../common/models",
"./models"
],
"mixins": [
"loopback/common/mixins",
"loopback/server/mixins",
"../common/mixins",
"./mixins"
]
},
"User": {
"dataSource": "mongods",
"public":false
},
"AccessToken": {
"dataSource": "mongods",
"public": false
},
"ACL": {
"dataSource": "mongods",
"public": false
},
"RoleMapping": {
"dataSource": "mongods",
"public": false
},
"Role": {
"dataSource": "mongods",
"public": false
},
"cool": {
"dataSource": "mongods",
"public": true
},
"Ouser":{
"dataSource": "mongods",
"public": true
}
}
可见,修改数据源只需要将这些模型的 dataSource 都改为 mongods。
loopback-connector-mongodb 模块依赖 Mongodb 的官方 Node.js 驱动 mongodb 模块。在程序中,我们可以直接使用官方驱动操作数据库,这也极为方便。下例是使用 mongodb 模块连接数据库并创建集合的例子
// A simple example showing the creation of a collection.
var MongoClient = require('mongodb').MongoClient,
test = require('assert');
MongoClient.connect('mongodb://localhost:27017/test', function(err, db) {
test.equal(null, err);
// Create a capped collection with a maximum of 1000 documents
db.createCollection("a_simple_collection", {capped:true, size:10000, max:1000, w:1}, function(err, collection) {
test.equal(null, err);
// Insert a document in the capped collection
collection.insertOne({a:1}, {w:1}, function(err, result) {
test.equal(null, err);
db.close();
});
});
});
官方驱动原生支持 Promise 和 ES6 generator,其官网 API 文档对每一个接口的说明非常详尽。建议读者访问 http://mongodb.github.io/node-mongodb-native/2.1/api/ 了解更多。
初始化数据库
使用 mongodb 作为可持久化的数据源,最开始启动服务的时候,这个数据库为空。还记得 LoopBack 在启动时会到 server/root 目录下依次加载 JS 文件。因此也可以将初始化数据库的代码放入这个目录内。当启动服务时,JS 文件自动执行。但需要注意的是,这类初始化代码只需要执行一次,因此当数据库初始化完毕之后,要把文件名后缀的 js 去掉,防止以后重复执行。在 server/root 目录下,新添加一个文件 initmongo.js,内容为
module.exports = function(app) {
var mongoDs = app.dataSources.mongods;
mongoDs.automigrate('AccessToken', function(err){
if(err) throw err;
});
mongoDs.automigrate('Ouser', function(err){
if(err) throw err;
var Ouser = app.models.Ouser;
var Role = app.models.Role;
var RoleMapping = app.models.RoleMapping;
Ouser.create([
{username: 'admin', email: '[email protected]', password: '12345', emailVerified: true}
], function(err, users) {
if (err) throw err;
mongoDs.automigrate('Role', function(err){
if(err) throw err;
mongoDs.automigrate('RoleMapping', function(err){
if(err) throw err;
var userid = users[0].id;
Role.create({
name: 'admin'
}, function(err, role) {
console.log('Created role:', role);
role.principals.create({
principalType: RoleMapping.USER
, principalId: userid
}, function(err, principal) {
if (err) throw err;
console.log('Created principal:', principal);
});
});
});
});
});
});
};
上面这段代码创建了 AccessToken,Role,RoleMapping 和 Ouser 这几张表。前三个模型是 LoopBack 预定义的。AccessToken 用于保存用户登录后的 Token 信息。Role 和 RoleMapping 用于权限控制。上述代码创建了 Role 表,并添加了一个角色 admin。在 RoleMapping 中,将权限角色 admin 与 Ouser 表中新创建的用户关联起来。此用户登录成功之后,就可以访问用 admin 限定的接口。
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW",
"property": "test"
}
用户登录成功之后,服务端向浏览器返回其有效 Token,程序中可以将这个 Token 保存到域名所在的 cookie 中,这样以后的 http 访问请求就会自带这个 Token 信息,LoopBack 根据这个 Token 信息,从 AccessToken 中反查出用户 id,如果 Token 有效,此用户就拥有了 $authenticated 角色,可以访问被 $authenticated 限定的接口。例如可以修改 ouser.js,在登录成功后,把这个 cookie 植如用户浏览器。
module.exports = function(Ouser) {
Ouser.afterRemote('login', function (context, result, next) {
var res = context.res;
if ( result && result.id ) {
res.cookie('authorization', result.id, { maxAge: 1000*60*60*24*14*6, httpOnly: true
,signed: true, domain: '.domain.com' });
}
return next();
});
};
当然,处于安全考虑,应该对保存在用户本地的 cookie 信息加密。这可以使用 cookie-parser 这个中间件来完成。
钩子机制
上一节结尾的代码用到了 LoopBack 的钩子机制。LoopBack 的钩子分为两种
- 接口调用执行前和执行后,分别对应 beforeRemote 和 afterRemote;
- CRUD 操作前或后,注册的方法被执行。主要有
access before save after save before delete after delete
CRUD 是增加,读取查询,更新,删除的简称。这两种钩子使用起来都不复杂。上面的代码 afterRemote 就是使用第一种钩子的场景。在本章前面的部分,用例子演示了注册登录的过程。在实际的邮箱注册逻辑中,用户点击注册之后,应该给用户注册时填写的邮箱发送一封邮件。用户收到邮件后,点击连接,才能激活这个账户。而发送邮件的时机,应该是把用户的注册信息写到表 Ouser 之后。我们可以利用钩子的机制,在创建用户之后,执行一个函数,发送一封确认邮件。
Ouser.afterRemote('create', function (ctx, result, next) {
if(!ctx.result.emailVerified && !ctx.result.username){
let subject = '注册邮件';
let template = path.resolve(path.join(__dirname, '..', '..', 'client','templates', 'verify.ejs'));
ctx.result.verify({
type:'email',
from:'[email protected]', //发送邮箱
to:ctx.result.email, //用户邮箱
subject:subject,
template: template
}, function (err, data){
if(err){
console.error(err);
}
next();
});
}else{
next();
}
});
为了能够收发邮件,需要使用 LoopBack 的一个基础模型 Email 并增加相应的邮件配置,在 model-config.json 文件中增加
"Email": {
"dataSource": "emailds"
}
然后在datasources.json中增加邮件配置信息
"emailds": {
"name": "emailds",
"connector": "mail",
"transports": [
{
"type": "smtp",
"host": "the email host",
"secure": false,
"port": 25,
"auth": {
"user": "your email",
"pass": "your pass"
}
}
]
}
中间件
server 目录下有一个配置文件 middleware.json,LoobBack 增加了中间件执行序列的概念,这可以严格的定义中间件函数的调用顺序。LoopBack 预定义的阶段包含
initial - 中间件最早在这个阶段执行 session - 准备会话对象 auth - 权限认证 parse - 解析请求体 routes - 路由请求 files - 对静态文件的请求 final - 错误处理
每一个阶段又可分成三个子阶段,例如auth阶段,可分为
“auth:before”:{} “auth”:{} “auth:after”:{}
在一次请求中,这些阶段自上而下依次执行。我们可以举一个例子,来说明如何在这个文件中添加中间件。对于404错误,我们希望返回一个404页面。final 用来处理错误,因此可以在这个阶段,添加一个处理404错误的中间件。
"final": {
"./error404.js":{}
}
在同级目录下,新建这个文件,文件的内容为
module.exports = function(options) {
return function raiseUrlNotFoundError(req, res, next) {
var error = new Error('Cannot ' + req.method + ' ' + req.url);
error.status = 404;
//------------------- max custom 404 ------//
if (req.accepts('html, text/html')) {
console.log( "404 ERR! " );
return res.sendFile('404.html', { root: __dirname + './../client/public/html/' });
}
//---------------------------------------//
next(error);
};
}
如此做之后,不要忘了在 client/public/html 目录下包含一个 404.html 的文件。
再比如,在解析请求体阶段,可以添加自动对 json 或 urlencoded 编码的字符串进行解析的中间件
"parse": {
"body-parser#json": {},
"body-parser#urlencoded": {"params": { "extended": true }}
}
模型关系
在程序中可以定义很多模型,这些模型可能存在一些关系。例如一个用户可能在多处登录,因此可以存在多个有效的 Token信息。也就是说 User 模型的一个用户对应 AccessToken 模型的多份数据,而 AccessToken 中里面的任意一个元素只属于 User 中某一个用户。User 和 AccessToken 这两个模型是 LoopBack 自带的,我们可以进入 LoopBack 模块文件夹的 common/models 目录下,查看这两个模型的 json 文件,在 user.json 文件末尾,我们可以看到如下内容
"relations": {
"accessTokens": {
"type": "hasMany",
"model": "AccessToken",
"foreignKey": "userId",
"options": {
"disableInclude": true
}
}
}
type 字段的 hasMany 代表 User 与 AccessToken 是一对多的关系,User 是主模型。foreignKey 代表了这两个模型之间的关联键。也就是 User 表的 id 作为 AccessToken 的外键,名称是 userId。access-token.json 文件末尾,我们看到类似的内容
"relations": {
"user": {
"type": "belongsTo",
"model": "User",
"foreignKey": "userId"
}
}
belongsTo 代表它是 User 的从模型,userId 作为外键,其值为对应 User 元素的 id。
一旦定义了模型之间的关系,LoopBack 会为我们自动生成一系列的 REST API 接口,例如可以使用 Ouser 模型中的接口,得到 AccessToken 模型的数据。下图显示了这些生成的接口
例如我们想获取某一个用户 id 的所有 Token 信息,就可以使用上图展示的第一个接口获取
[
{
"id": "9g9SCL6LAFPy20WLf7u0Q2KIAcgXv8Nfur3BxHs7xq1501UzBNcJYNlDRmbXSmrh",
"ttl": 7257600,
"created": "2016-05-22T08:43:56.380Z",
"userId": "56e9853decfd499b641b82a1"
},
{
"id": "BtFnIenOd003UmGmFxJs6f6bcaeIBvcyD5q94zpxoQ5nv9ojUQqmRJ3rAbH9oU5n",
"ttl": 7257600,
"created": "2016-05-22T08:44:38.014Z",
"userId": "56e9853decfd499b641b82a1"
}
]
使用 cluster 模式运行服务
因为 http 是无状态的,因此可以启动多个平行的服务进程并行处理 http 请求。cluster-works 模式的另一个好处是,主进程是所有 work 进程的父进程,work 的异常退出,主进程都可以捕获到,并报警。cluster 模块是 Node 原生支持的模块。我们在 server 目录下,添加一个文件 cluster.js,文件内容为
var cluster = require('cluster');
var workers = {};
var WorkersLen = function (){
var len = 0;
for(var id in workers){
++len;
}
return len;
};
var createWorker = function (){
var worker = cluster.fork();
workers[worker.id] = worker;
worker.on('exit', function(code){
delete workers[worker.id];
});
worker.on('message', function(msg){
do {
if(msg.cmd === 'suicide'){
createWorker();
break;
}
}while(false);
});
};
function StartWorkers() {
var n = 0;
require('os').cpus().forEach(function(){
createWorker();
});
}
if(cluster.isMaster){
StartWorkers();
process.on('exit', function(){
for(var id in workers){
workers[id].kill();
}
});
}else{
require('./server.js').start();
}
以上代码包含了 work 进程与主进程的通信。Node 进程之间使用 Unix 域套接字通信,这是一种非常高效的方式。主进程监听了 message 事件,回调函数的参数是一个 json 对象。可以根据这个 json 对象的内容,区分这个事件的不同类型,然后分别处理。
接下来还需要稍微修改一下 server.js 文件。在文件末尾,曾为 server.js 添加了如下代码
process.on('uncaughtException', function (err){
console.error('uncaughtException: %s', err.message);
});
现在我们希望遇到这个未捕获异常,除了打印出异常信息之外,程序能够优雅的退出,而不是在某一个时刻崩溃掉。于是将上述代码修改为
process.on('uncaughtException', function (err){
console.error('worker uncaughtException: %s', err.message);
var worker = require('cluster').worker;
if(worker){
process.send({ cmd: 'suicide', stack: err.stack, message:err.message});
Server.close(function(){
process.exit(1);
});
}
});
当子进程收到一个未捕获异常时,就向父进程发送一个 message 事件,并附上异常信息,500毫秒之后退出。父进程收到这个类型为 suicide 的 message 事件之后,立即重启动一个 work。事实上,cluster.js 文件内还可以处理更多的逻辑。但无论如何,cluster.js 的代码都该越简单越好,它的稳定性应该与 Node 引擎一致。我们可以再结合 pm2 工具运行 cluster.js,pm2 可以保证 cluster.js 的运行,这样可以进一步提供服务健壮性。
顺便一提,2015年后半年,IBM 收购了 loopback
参考资料
LoopBack 官网 https://docs.strongloop.com/display/SL/Installing+StrongLoop