ES6的出品为JS成为企业级语言扫清障碍,与之配套的,我们需要一个真正的企业级框架。快递像一个精巧的微内核,不足以支撑起一个大项目。以下是LoopBack的一些入门知识,它是一个真正的企业级框架,随着使用的深入,读者将会发现它更多的用法和优秀的特性。本篇将对它的主要用法做一个详细的介绍。
LoopBack是建立在Express基础上的企业级Node.js框架,这个框架支持
它可以像Express那样被使用。除此之外,LoopBack作为一个面向企业级的Web框架,提供了更丰富的功能,这在我们添加模型,权限控制,连接数据源等操作时,极大的提升我们的效率。例如可以通过修改配置增加模型,并指定模型的数据源。它默认提供了一些基础模型,例如用户这个模型包含了注册登录等逻辑。我们可以非常方便的继承这些内建模型,实现个性化的定制。它还提供了Hook编程的机制。它同时提供了可视化的调试页面,自动生成对应的前端SDK。这些功能在开发大型Web服务的时候,将帮助我们更容易查看和管理项目。本篇将会详细的介绍LoopBack的使用。
StrongLoop是生成LoopBack框架的工具程序,我们首先安装它。运行
npm install -g strongloop
安装完成之后,可以运行slc -v查看是否安装成功(需要事先建立slc的软链接)。
紧接着,我们运行slc loopback,这是一个交互式的命令,首先提示用户输入项目名称,这里就输入环回。接下来根据引导,按步骤填写相应信息即可。输入项目名称之后,接下来的步骤我们可以直接敲回车即可。最后strongloop会帮助我们创建loopback目录,并且在目录下创建默认的项目文件。我们进入loopback文件夹,运行slc loopback:model,创建一个模型。我们可以随意输一个模块名,例如酷。接下来要求选择数据源,这里先选择默认值db(memory),敲回车即可。下一步要求选择模型的基类,也选用默认值PersistedModel,代表此模型与持久化数据源连接。接下来,会出现
通过REST API公开炫酷?是
当我们选择'Y',LoopBack会为我们的模型生成REST API的代码。之后直接点击回车完成步骤即可。我们查看一下loopback目录都包含哪些文件
LoopBack符合模型(M) - 视图(V) - 控制器©的设计规范。上图中的服务器文件夹,包含了程序的启动代码,配置信息。路由部分的逻辑也在服务器目录下。快速支持将路由分组,因此服务器目录可以对应MVC的C.client目录包含给用户展示的前端代码,也包含由后台处理的用于生成页面的模板。这个目录对应MVC的V.common目录下有一个模特文件夹,这里的代码处理具体的业务逻辑和数据,对应M.
我们进入服务器文件夹,运行节点server.js,可以看到如下信息
Web服务器监听:http://0.0.0.0 : 3000 浏览您的REST API,网址为http://0.0.0.0:3000/explorer
在本地打开浏览器访问http://0.0.0.0:3000/explorer,可以看到如下界面
这是LoopBack集成的一个非常棒的功能,它列出了所有对外的模型和每一个模型的接口.LoopBack默认生成的接口都是REST API风格。点击某一个接口,界面会展开,展开的界面提供了测试功能。我们可以将构造好的参数填入输入框,然后查看接口的返回结果。
LoopBack为模型默认生成的接口包括
读系列
写系列
默认这些REST API可以被访问。如果需要屏蔽某一个,可以在模型的JS文件内部,例如cool.js,内部增加调用
module.exports = function(Cool) {
Cool.disableRemoteMethod('findById', true);
// 省略...
这样就能屏蔽掉了findById这个接口。
当LoopBack服务启动的时候,它会按照文件名的字符串顺序,加载位于/ server / root里面的所有后缀名为.js的文件。这提供了一个初始化整个系统的机会。例如我们可以利用这个机制挂载模块,或者将初始化数据库的代码放到这个目录。
在浏览器中打开explorer调试接口虽然方便,但在实际项目中,别人随意可以查看这个界面存在着一定的风险。这时候就可以利用LoopBack加载服务器/ root里面JS文件的机制,为explorer的访问增加权限控制。接下来在服务器/ 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文件的内容,将默认的配置去除,或者直接删除这个文件。在服务器目录下重新启动服务,然后在本地用浏览器打开网址http://0.0.0.0 :3000 / explorer,出现提示,要输入用户名和密码。
mountLoopBackExplorer函数的参数服务器是LoopBack传进来的,这个对象代表LoopBack程序本身。它在server.js文件开头创建
var app = module.exports = loopback();
app.models包含了所有的模型,假如我们希望访问酷这个模型,可以通过如下形式
app.models.cool
得到此模型对象,之后便可以调用这个对象的函数。
LoopBack添加路由的方式与Express一致.LoopBack实现了MVC模型,在这个框架下,它提供了另外一种添加模块并导出API的方式。我们先来看Express添加路由的方法。默认生成的服务器/服务器的.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控制路由的逻辑中,应该将路由分类,以后方便管理。在服务器目录中新建一个文件夹,命名为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定义了模型之间的关系,属性定义了模型对应的持久化字段.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时会带上他的令牌信息,$ owner代表这个用户只能访问自己的信息,而对其他用户的数据没有访问权限。如果换成$ authenticated,那么只要用户的令牌信息合法,就可以调用这个接口。下面我们继续修改acls这个键,使得测试接口重新可访问,我们添加一个针对test接口的访问控制项
"acls": [
{
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "DENY"
}
,{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$everyone",
"permission": "ALLOW",
"property": "test"
}
]
principalId设置为对所有的人进行访问控制,权限字段设置为允许。重启服务后,这个接口变得可访问.accessType的取值有三个,分别是READ,WRITE和EXECUTE。一般来讲,我们自定义的接口accessType使用EXECUTE修饰,principalId使用$ everyone或者$ authenticated修饰。对于每一个模型,LoopBack框架会自动生成一系列固定模式的REST API,用于存取模型数据。这部分接口的accessType常会用到READ和WRITE。接下来,我们基于LoopBack的一个内建模型用户,建立一个用户体系,允许使用者创建新用户,生成用户Token,然后再进一步讨论LoopBack的权限控制,之后本章还将讨论LoopBack中模型之间的关系。
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继承自用户,对应的JS文件在LoopBack模块目录的公共/模型文件夹中。用户代表了对用户操作的模型,包含了注册,登录等逻辑。接着编写ouser.js文件内容,如下
module.exports = function(Ouser) {
};
因为模型Ouser继承自用户,因此源文件中可以调用用户定义的方法,用户对外的接口也被Ouser模型继承。在model-config.json文件中,我们屏蔽掉用户,使这个模型的接口不对外。
"User": {
"dataSource": "db",
"public":false
}
接下来,我们重启服务,浏览器打开explorer,可以看到,刚才新添加的模型Ouser已经存在,并且包含了一系列REST API。这些API是LoopBack自动添加的,根据英文注释,不难理解每一个接口的含义。我们可以直接在explorer的界面中,创建一个新用户,先找到创建用户的接口
在输入框中填写如下内容
{
"nickname":"Json"
,"email":"[email protected]"
,"password":"12345"
}
昵称是模型Ouser的一个属性。另外两个是基类用户自带的属性。然后点击尝试一下,返回如下内容
{
"nickname": "Json",
"email": "[email protected]",
"id": 1
}
这个表新创建了一个用户。接下来调用Ouser的/ ousers / login方法,试着尝试使用邮箱和密码登录。在凭证输入框输入如下内容
{"email":"[email protected]"
,"password":"12345"
}
点击发送按钮,将返回如下数据
{
"id": "F7IliK3irck8ILWkAdEucYGoXw67j50GTYKIsurYx1EuZb61QcohEsAxcqLw0RMS",
"ttl": 1209600,
"created": "2016-07-24T06:20:28.436Z",
"userId": 1
}
这个表我们已经登录成功,并返回一个此用户的令牌信息。目前服务使用的是基于memory存储方案,服务重启,数据丢失。复制这个令牌信息,将他拷贝到如下图所示的输入框,然后点击设置访问令牌按钮
接着,我们点开get / ousers / {id}这个接口,在id对应的输入框输入1,点击试试按钮,将返回这个id为1的用户对应的信息。
以上过程演示了注册,登录和根据有效Token访问用户信息的步骤。而真正用于实际的步骤比这个要复杂一些。现在回过头再来看看,模型是怎么添加的,在model-config.json,我们添加了如下内容
"Ouser":{
"dataSource": "db",
"public": true
}
dataSource字段的内容是db,表示Ouser使用名称为db的数据源。这个数据源在同级目录的datasources.json中定义,我们看一下这个文件的内容
{
"db": {
"name": "db",
"connector": "memory"
}
}
连接器字段的值为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字段中的名称,pass和dbname以实际的为准.database字段代表数据库名称,用户名代表mongodb的用户名,密码是数据库连接密码。使用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在启动时会到服务器/根目录下依次加载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用于权限控制。上述代码创建了角色表,并添加了一个角色admin。在RoleMapping中,将权限角色admin与Ouser表中新创建的用户关联起来。此用户登录成功之后,就可以访问用户限定的接口。
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "admin",
"permission": "ALLOW",
"property": "test"
}
用户登录成功之后,服务端向浏览器返回其有效Token,程序中可以将这个Token保存到域名所在的cookie中,这样以后的http访问请求就会自带这个令牌信息,LoopBack根据这个令牌信息,从AccessToken中反查出用户id,如果Token有效,此用户就拥有了$ 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的钩子分为两种
删除之前保存之前保存,删除之后删除之前
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的一个基础模型电子邮件并增加相应的邮件配置,在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 - 中间件最早在这个阶段执行会话 - 准备会话对象auth - 权限认证解析 - 解析请求体路由 - 路由请求文件 - 对静态文件的请求final - 错误处理
每一个阶段又可分成三个子阶段,例如身份验证阶段,可分为
“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 }}
}
在程序中可以定义很多模型,这些模型可能存在一些关系。例如一个用户可能在多处登录,因此可以存在多个有效的令牌信息。也就是说用户模型的一个用户对应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代表了这两个模型之间的关联键。也就是用户表的id作为AccessToken的外键,名称是userId.access -token.json文件末尾,我们看到类似的内容
"relations": {
"user": {
"type": "belongsTo",
"model": "User",
"foreignKey": "userId"
}
}
belongsTo代表它是用户的从模型,userId作为外键,其值为对应用户元素的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"
}
]
因为http是无状态的,因此可以启动多个平行的服务进程并行处理http请求.cluster-works模式的另一个好处是,主进程是所有work进程的父进程,work的异常退出,主进程都可以捕获到,并报警.cluster模块是节点原生支持的模块。我们在服务器目录下,添加一个文件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域套接字通信,这是一种非常高效的方式。主进程监听了消息事件,回调函数的参数是一个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);
});
}
});
当子进程收到一个未捕获异常时,就向父进程发送一个消息事件,并附上异常信息,500毫秒之后退出。父进程收到这个类型为自杀的消息事件之后,立即重启动一个工作。事实上,cluster.js文件内还可以处理更多的逻辑。但无论如何,cluster.js的代码都该越简单越好,它的稳定性应该与节点引擎一致。我们可以再结合pm2工具运行集群.js,pm2可以保证cluster.js的运行,这样可以进一步提供服务健壮性。
原文:https://cnodejs.org/topic/57e5b2859c495dce044f397c