上篇里,我们只是把 LoopBack 3 当成一个快速搭 CRUD 接口的工具,一个 CoffeeShop 模型, 一套 MySQL 后端,再配上 API Explorer,能方便地查店铺列表、看 REST 响应。

如果你只想写个内部小服务,这已经够用。但真实世界的应用很少这么简单,通常还会有用户系统、评论、权限、以及前端页面。 这也是官方在 Getting started part II 想做的事:用一套 Coffee Shop Reviews 小应用,把 LoopBack 的几块常用能力拼在一起: 多数据源、模型关系、ACL、remote hook,再加一个基于 AngularJS SDK 的前端。

所以这篇下篇,我们在上篇的基础上,一步步把这个小站搭起来:先接上第二个 MongoDB 数据源, 再补上 Review / Reviewer 模型和它们之间的关系,接着用 ACL 和 remote hook 把“谁能做什么”说清楚, 最后再让前端跑起来,把所有东西串成一个可以点点点的完整应用。

Coffee Shop Reviews 应用需求分析

官方示例的目标是做一个“咖啡店版 Yelp”。应用的大致结构是:

  • 数据源

    • MySQL:继续存上篇中的 CoffeeShop 数据。
    • MongoDB:专门存 Reviewer(用户)和 Review(评论)。
  • 模型(Models)

    • CoffeeShop:咖啡店(上篇已经有)。
    • Review:某个咖啡店的一条评论。
    • Reviewer:评论人,继承 LoopBack 内置的 User 模型,用它来完成注册、登录、注销这些操作。
  • 模型关系(Relations)

    • 一个 CoffeeShop 有很多 Review。
    • 一个 CoffeeShop 有很多 Reviewer。
    • 一个 Review 属于一个 CoffeeShop。
    • 一个 Review 属于一个 Reviewer。
    • 一个 Reviewer 有很多 Review。
  • 权限规则(ACL)

    • 所有人都能看评论。
    • 想写 / 改 / 删评论,必须先登录。
    • 任何人都可以注册账号并登录/登出。
    • 用户只能改自己的评论,不能顺手把评论对应的咖啡店改掉。
  • 前端

    • 用 AngularJS SDK 自动生成访问 REST API 的客户端服务,再配上一个简单的单页应用 UI。

这一篇,就是一步一步把这套东西复刻一遍。

前期准备

先把官方的 “loopback-getting-started-intermediate” 仓库拉下来跑一跑,心里有个大致画面:

git clone https://github.com/strongloop/loopback-getting-started-intermediate.git
cd loopback-getting-started-intermediate

npm install
node .

# 控制台里会看到:
# Web server listening at: http://0.0.0.0:3000/
# Browse your REST API at http://0.0.0.0:3000/explorer
# > models created sucessfully

在浏览器打开:

  • http://0.0.0.0:3000/:前端页面。
  • http://0.0.0.0:3000/explorer:API Explorer。

你会看到:

  • 首页是一个 Coffee Shop 列表 + 登录入口。
  • 登录后可以新增评论、查看自己的评论、查看所有评论。

你可以选择:

  • 自己从上篇的项目继续搭cd <你的上篇项目目录>
  • 或者偷个懒:直接 clone 上篇起点仓库 loopback-getting-started 当作基础,然后按下面步骤改。

后面的命令我默认你已经在自己的项目目录里,例如:

cd loopback-getting-started   # 或你的上篇项目

添加二个数据源

上篇里,我们只有一个 MySQL 数据源,用来存咖啡店。 现在要再加一个 MongoDB 数据源,用于 Reviewer / Review。

用 CLI 新建数据源配置

在项目根目录执行(LB3 CLI):

lb datasource

根据提示大致填成这样:

  • 数据源名称:mongoDs
  • 选择连接器:MongoDB
  • Host:demo.strongloop.com(官方示例用的演示服务器,你也可以换成本地 Mongo)
  • Port:27017
  • Database:getting_started_intermediate(随你,只要后面配置一致)
  • 用户名:demo
  • 密码:L00pBack

执行完后,server/datasources.json 会多出一个类似这样的片段:

"mongoDs": {
  "name": "mongoDs",
  "connector": "mongodb",
  "host": "demo.strongloop.com",
  "port": 27017,
  "database": "getting_started_intermediate",
  "username": "demo",
  "password": "L00pBack"
}

注意:

真正写业务时,别把密码硬编码在 repo 里,用环境变量或配置文件区分开发 / 测试 / 生产会舒服很多。

如果你有自己的 MongoDB(比如本机),就把 host/database/用户名密码改成自己的。

添加 Review 和 Reviewer 模型

现在 CoffeeShop 还孤零零一张表,我们来加上 Review 和 Reviewer 两个模型,并把假数据灌进去。

评论(Review)

执行:

lb model

按提示填:

  • Model name:Review
  • Data source:选刚才的 mongoDs (mongodb)
  • Base class:PersistedModel
  • Expose via REST:Yes
  • Common/server only:Common

属性字段可以设成:

字段名 类型 是否必填
date date
rating number
comments string

生成完之后,会有:

  • common/models/review.json:模型定义(字段、关系、ACL)。
  • common/models/review.js:以后放业务逻辑(remote hook 就写这里)。

Reviewer

再来一个:

lb model

选择项大致是:

  • Model name:Reviewer
  • Data source:mongoDs
  • Base class:User(这一步很关键)
  • Expose via REST:Yes
  • Common/server only:Common

不需要再添加属性——登录、密码、email 等字段都从 User 继承了。

很多应用里的“用户子类”(比如 Reviewer、Admin、Customer), 其实都可以直接继承 LoopBack 内置 User,少写很多轮子的代码。

更新 boot 脚本

官方给了一个 server/boot/create-sample-models.js,里面用 automigrateasync.parallel 做了几件事:

  • 对 MySQL 的 CoffeeShop 做 automigrate,并插入几家示例咖啡店。
  • 对 MongoDB 的 Reviewer 做 automigrate,并创建几个示例用户(比如 foo@bar.com 等)。
  • 对 MongoDB 的 Review 做 automigrate,并自动生成一些评论,帮你填满数据列表。

你可以照着官方脚本,把自己项目里的 create-sample-models.js 替换掉,再安装它用到的依赖:

npm install --save async

重启项目:

node .

# 控制台里应该能看到:
# > models created sucessfully

这时 MySQL 和 MongoDB 里都已经有数据了。

把模型串起来

有了三张表,下一步就是搞清楚它们之间怎么互相引用。

在 Coffee Shop Reviews 应用里,关系是这样的:

  • CoffeeShop hasMany Review
  • CoffeeShop hasMany Reviewer
  • Review belongsTo CoffeeShop
  • Review belongsTo Reviewer(外键叫 publisherId
  • Reviewer hasMany Review(也用 publisherId 做外键)

用 lb relation 把关系写进模型

在项目根目录反复执行:

lb relation

每一次调用都描述一条关系,比如(简化版描述):

  1. CoffeeShop 有很多 Review

    • From model:CoffeeShop
    • Relation type:hasMany
    • To model:Review
    • Property name:reviews
  2. CoffeeShop 有很多 Reviewer

    • From model:CoffeeShop
    • Relation type:hasMany
    • To model:Reviewer
    • Property name:reviewers
  3. Review 属于一个 CoffeeShop

    • From model:Review
    • Relation type:belongsTo
    • To model:CoffeeShop
    • Property name:coffeeShop
  4. Review 属于一个 Reviewer

    • From model:Review
    • Relation type:belongsTo
    • To model:Reviewer
    • Property name:reviewer
    • Foreign key:publisherId
  5. Reviewer 有很多 Review

    • From model:Reviewer
    • Relation type:hasMany
    • To model:Review
    • Property name:reviews
    • Foreign key:publisherId

生成完成后,review.jsonreviewer.json 里会多出 relations 段,清楚地把这些关系写进去。

注意:

  • 一个模型既 belongsTo 又被 hasMany,通常就是“一对多”的两端。
  • 重用 publisherId 做双向关系(Reviewer ↔ Review)能让数据结构更干净,以后查询“某个用户的所有评论”会非常顺。

加上权限

有了用户、有了数据,接下来就要把“谁能干什么”说清楚。

这一节的目标,是用 ACL 实现前面提到的几条规则:

  • 所有人可以读评论(匿名访问 GET /Reviews 没问题)。
  • 想创建 / 修改 / 删除评论,必须登录。
  • 登录用户可以 CRUD 自己的评论,但不能改别人写的,也不能改 CoffeeShop 本身。

用 lb acl 定义规则

还是在项目根目录,多次运行:

lb acl

每次执行定义一条 ACL 记录,几条主要规则可以这么配:

  1. 先全盘禁用

    • Model:All existing models
    • Scope:All methods and properties
    • Access type:All
    • Role:All users ($everyone)
    • Permission:Explicitly deny
  2. 任何人都能读评论

    • Model:Review
    • Scope:All methods and properties
    • Access type:Read
    • Role:All users ($everyone)
    • Permission:Allow
  3. 登录用户可以读 CoffeeShop 列表

    • Model:CoffeeShop
    • Scope:All methods and properties
    • Access type:Read
    • Role:Any authenticated user ($authenticated)
    • Permission:Allow
  4. 登录用户可以调用写评论(Review.create)

    • Model:Review
    • Scope:Single method
    • Method name:create
    • Role:Any authenticated user
    • Permission:Allow
  5. 评论作者可以修改 / 删除自己的评论

    • Model:Review
    • Scope:All methods and properties(写操作)
    • Access type:Write
    • Role:The user owning the object ($owner)
    • Permission:Allow

最终在 common/models/review.json 里,你会看到一个 acls 数组,大致长这样:

"acls": [
  { "accessType": "*",   "principalType": "ROLE", "principalId": "$everyone",     "permission": "DENY"  },
  { "accessType": "READ","principalType": "ROLE", "principalId": "$everyone",     "permission": "ALLOW" },
  { "accessType": "EXECUTE","principalType": "ROLE","principalId": "$authenticated","permission": "ALLOW","property": "create" },
  { "accessType": "WRITE","principalType": "ROLE","principalId": "$owner",        "permission": "ALLOW" }
]

建议:

  • 先 deny all,再按功能慢慢放行
  • 这样不会因为漏配某条规则,让接口在你没注意的情况下暴露出去。

添加业务逻辑

现在的 REST API 还缺点业务逻辑:

  • 用户创建评论时,其实不应该手写 datepublisherId
  • 这两个字段完全可以由服务端自动填。

这就是 remote hook 的用武之地。官方示例让我们在 Review 上挂一个 beforeRemote('create'): 每次调用 REST 的 create 方法时,自动填充日期和发布人。

在 review.js 里加钩子

编辑 common/models/review.js,写一个类似下面的实现:

module.exports = function (Review) {
  Review.beforeRemote("create", function (ctx, unused, next) {
    const data = ctx.args.data || {};

    // 自动把评论时间设为当前时间
    data.date = Date.now();

    // 从 accessToken 里拿当前用户 ID,当成 publisherId
    if (ctx.req && ctx.req.accessToken) {
      data.publisherId = ctx.req.accessToken.userId;
    }

    ctx.args.data = data;

    next();
  });
};

这段逻辑做了两件事:

  • 在真正写入数据库前,把评论时间补上。
  • 把当前登录用户的 userId 写进 publisherId 字段,和前面关系里配置的外键对上。

对比一下 operation hook:

  • remote hook:只在“通过 REST 调用某个方法”时触发。
  • operation hook:无论是 REST、脚本还是其它方式,只要模型在做 create/save/update 这些操作,就会触发。 在这里我们只关心 HTTP API 的调用,所以选 remote hook 正好合适。

用 AngularJS SDK 做一个简单前端

后端已经比较像样了,最后一步是给它配一个浏览器端 UI。

LoopBack 提供了 AngularJS SDK,可以帮你自动生成和模型一一对应的 Angular $resource 服务,省掉一大堆手写 HTTP 调用的重复劳动。

用 lb-ng 生成客户端服务

AngularJS SDK 自带一个 lb-ng 命令,用来扫描你的 server/server.js,然后生成一个 lb-services.js 文件(里面是各个模型的 Angular 服务)。

如果还没装过:

npm install -g loopback-sdk-angular-cli

在项目根目录创建目录:

mkdir -p client/js/services

然后执行类似下面的命令:

lb-ng server/server.js client/js/services/lb-services.js

生成的 lb-services.js 就是你在 Angular 里访问 LoopBack REST API 的“桥”。

拷贝官方示例的前端代码

官方教程为了省事,直接让你从 intermediate 示例里把现成的前端拷过来:

git clone https://github.com/strongloop/loopback-getting-started-intermediate.git
cp -r loopback-getting-started-intermediate/client ./client

拷完之后,你的 client 目录大概长这样:

  • client/index.html:单页应用入口。
  • client/css/style.css:样式。
  • client/js/app.js:AngularJS 应用入口、路由配置。
  • client/js/controllers/*.js:比如 auth.js,处理登录/登出、切换“全部评论 / 我的评论”等逻辑。

index.html 会把 lb-services.js 引入进来,然后在控制器里通过 Review, CoffeeShop, Reviewer 这些 Angular 服务来访问后端。

这一步使用现成代码,把它跑起来,多看看请求 / 响应长什么样,对以后自己写前端很有帮助。

总结

如果你只打算用 LoopBack 写内部 API,上面的很多步骤其实已经够你做一堆项目了。如果你还想深入,可以继续:

  • Tutorials & Examples 里的一堆示例仓库(loopback-example-access-controlloopback-example-app-logic 等)。
  • 关于 “Controlling data access”、“Creating model relations”、“Remote methods / Remote hooks” 这些专题文档,里面有更完整的细节。

参考资料