node 框架 LoopBack 教程
发布于 3 年前 作者 classfellow 15715 次浏览 来自 分享

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

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 目录都包含哪些文件 <br>

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, 可以看到如下界面 <br> <br>

这是 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 ,出现提示,要输入用户名和密码。 <br> <br> 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,测试我们新添加的接口如下图 <br> <br>

我们在输入框随意输入一个字符串,点击测试按钮,可以立即查看返回结果。可见 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 的界面中,创建一个新用户,先找到创建用户的接口 <br>

在输入框中填写如下内容

{
"nickname":"Json"
,"email":"1234@qq.com"
,"password":"12345"
}

nickname 是模型 Ouser 的一个属性。另外两个是基类 User 自带的属性。然后点击Try it out,返回如下内容

{
  "nickname": "Json",
  "email": "1234@qq.com",
  "id": 1
}

这代表新创建了一个用户。接下来调用 Ouser 的 /ousers/login 方法,试着尝试使用邮箱和密码登录。在 credentials 输入框输入如下内容

{"email":"1234@qq.com"
,"password":"12345"
}

点击发送按钮,将返回如下数据

{
  "id": "F7IliK3irck8ILWkAdEucYGoXw67j50GTYKIsurYx1EuZb61QcohEsAxcqLw0RMS",
  "ttl": 1209600,
  "created": "2016-07-24T06:20:28.436Z",
  "userId": 1
}

这代表我们已经登录成功,并返回一个此用户的 Token 信息。目前服务使用的是基于 memory 存储方案,服务重启,数据丢失。复制这个 Token 信息,将他拷贝到如下图所示的输入框,然后点击 Set Access Token 按钮 <br> <br> 接着,我们点开 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: 'admin@e.com', 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:'admin@domain.com', //发送邮箱
      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 模型的数据。下图显示了这些生成的接口 <br>

例如我们想获取某一个用户 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

前一篇—模块机制

回到顶部