跳到主要内容
CNode

node 框架 LoopBack 教程

分享
Cclassfellow发布于 10 年前最后回复 10 年前
5220690
目录 · 8 个章节

本文系原创,转载请注明出处

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)—控制器(C)的设计规范。上图中的 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 为模型默认生成的接口包括

读系列

  1. exists - 模型的数据源中对应id项是否存在
  2. findById - 根据id返回数据源对应的项
  3. find - 返回所有满足匹配查询条件的项
  4. count - 返回满足匹配查询条件的项目个数

写系列

  1. create - 创建新项
  2. upsert - 更新项
  3. 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/namehttp://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 的钩子分为两种

  1. 接口调用执行前和执行后,分别对应 beforeRemote 和 afterRemote;
  2. 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

前一篇—模块机制

查看回复

回复 (5)

C
chengang4505#510 年前

mark

D
dpc761218914#410 年前

👍

来自酷炫的 CNodeMD

I
idreamshen#310 年前

nice, mark

S
sunfeng90#210 年前

Great, I use it now!

S
singcl#110 年前

先Mark From Noder

参与回复

登录后即可参与回复。登录