MorningSpace Lab
深入浅出LoopBack
线下手动训练营
July 21, 2018
### 关于本实践课程
* [《深入浅出LoopBack》](https://morningspace.github.io/lab-loopback/)系列讲座的配套实践课程
* 以动手实践为主,辅以理论与概念的串讲
* 以实现完整的 ☑︎ TaskMe实验项目为目标,分阶段完成不同任务
### 关于☑︎ TaskMe
* 一个管理个人任务的应用
* 支持任务的定义,修改,和删除等功能
* 麻雀虽小五脏俱全
### 通过本实践课程,您将了解到:
* LoopBack安装及框架代码生成
* 驾驭Model:定义,验证,关联
* 业务逻辑扩展:
* Remote Method
* Mixin
* Middleware
* Boot Script
* 数据访问:过滤器,数据源,连接器
* 完整项目:单元测试,测试覆盖,代码检查
* 将程序发布到API Connect
### 对本实践课程的学习建议
* 为节省时间,包括软件安装在内的准备工作建议在课程正式开始之前提前完成
* 本讲义同时适用LoopBack及API Connect,讲义中每步标题后明确标注LoopBack CLI或API Connect,表示该步骤在两种情况下执行方法有别
* 老师按讲义所列次序分步骤讲解,每一步讲解结束后学员开始手工实践,过半学员完成当前步骤后,老师继续下一步讲解
* 学员实践遇到问题时,可寻求场内老师的帮助
* 未及时完成当前步骤实践的学员,可参考[GitHub](https://github.com/morningspace/lab-loopback/tree/master/code)上当前步骤对应的样例代码,位于/code/taskme-n下,n代表第几步,如:taskme-1
### ☑︎ TaskMe: Step 0
### 准备工作
### 任务列表
包括环境准备,软件安装,及样例代码下载。
* 安装[node.js和npm](https://nodejs.org/en/download/)
* 安装[LoopBack](https://loopback.io/doc/en/lb3/Installation.html)
* LoopBack CLI,或
* IBM API Connect Developer Toolkit(可选)
* 安装[Git客户端](https://git-scm.com/downloads/guis)(可选)
* 安装[VS Code](https://code.visualstudio.com/)或其他任何一款代码编辑器(可选)
* 下载[本课程讲义及 ☑︎ TaskMe源代码](https://github.com/morningspace/lab-loopback)
注:为节省时间,建议开课前完成上述准备工作
### 安装LoopBack
有两种安装选择,后者支持将项目发布到IBM Cloud
* LoopBack CLI,或
* IBM API Connect Developer Toolkit
# install loopback-cli
$ npm install -g loopback-cli
# display the version
$ lb -v
# install api connect dev toolkit
$ npm install -g apiconnect
# display the version
$ apic -v
### ☑︎ TaskMe: Step 1
### 生成框架代码并执行
### 任务列表
* 生成应用框架代码
* 运行应用程序
* 学习使用API Explorer
### 生成应用框架代码(LoopBack CLI)
在命令行执行lb,并回答一系列问题,参考如下
![](images/ws/lb-create.png)
### 生成应用框架代码(LoopBack CLI)
看到如下结果,表示代码生成完毕
![](images/ws/lb-create-2.png)
### 生成应用框架代码(API Connect)
在命令行执行apic,并回答一系列问题,参考如下
![](images/ws/apic-create.png)
### 生成应用框架代码(API Connect)
看到如下结果,表示代码生成完毕
![](images/ws/apic-create-2.png)
### 运行应用程序
在taskme根目录下执行如下命令启动程序
# LoopBack CLI
$ node .
Web server listening at : http://localhost:3000
Browse your REST API at http://localhost:3000/explorer
# API Connect
$ apic start
Service taskme started on port 4001.
Access the application dashboard at http://...:4001/appmetrics-dash
Service taskme-gw started on port 4002.
### 学习使用API Explorer(LoopBack CLI)
在浏览器中打开http://localhost:3000/explorer ,如下
![](images/ws/lb-explorer.jpg)
### 学习使用API Explorer(API Connect)
在taskme根目录下执行:apic edit,以打开浏览器如下
![](images/ws/apic-edit.jpg)
### 学习使用API Explorer(API Connect)
点击Explore链接进入API Explorer界面
![](images/ws/apic-explorer.jpg)
注:或直接在命令行输入apic explore,但要注意API定义中的Host取值不能为$(catalog.host)
### ☑︎ TaskMe: Step 2.1
### Model的定义
### 任务列表
* 添加In Memory数据源
* 添加Task Model
* 只为Task暴露期望的REST API
* 添加Note Model
### 添加In Memory数据源(LoopBack CLI)
在命令行执行lb,并回答一系列问题,参考如下
![](images/ws/lb-ds-create.jpg)
### 添加In Memory数据源(API Connect)
在命令行执行apic,并回答一系列问题,参考如下
![](images/ws/apic-ds-create.jpg)
### 添加Task Model(LoopBack CLI)
在命令行执行lb,并回答一系列问题,参考如下
![](images/ws/lb-model-create.jpg)
### 添加Task Model(API Connect)
在命令行执行apic,并回答一系列问题,参考如下
![](images/ws/apic-model-create.jpg)
### 只为Task暴露期望的API(LoopBack CLI)
修改server/model-config.json,为Task定义如下
"Task": {
"dataSource": "db", "public": true,
"options": {
"remoting": {
"sharedMethods": {
"*": false,
"create": true,
"find": true,
"prototype.patchAttributes": true,
"deleteById": true
}
}
}
}
### 添加Note Model(LoopBack CLI)
在命令行执行lb,并回答一系列问题,参考如下
![](images/ws/lb-model2-create.jpg)
### 添加Note Model(API Connect)
在命令行执行apic,并回答一系列问题,参考如下
![](images/ws/apic-model2-create.jpg)
### ☑︎ TaskMe: Step 2.2
### Model的定制
### 任务列表
* 修改Task的定义,为Task增加state属性
* 添加SharableTask Model,令其派生自Task
* 为Task增加业务逻辑,在保存之前将title属性转成大写
### 修改Task的定义
修改common/models/task.json,为Task增加state属性
// In task.json
...
"properties": {
...
"state": {
"type": "number",
"default": 0
}
}
### 添加SharableTask Model(LoopBack CLI)
在命令行执行lb,并回答一系列问题,参考如下
![](images/ws/lb-model3-create.jpg)
### 添加SharableTask Model(API Connect)
在命令行执行apic,并回答一系列问题,参考如下
![](images/ws/apic-model3-create.jpg)
### 为Task增加业务逻辑
修改common/models/task.js,保存前将属性title转成大写
'use strict';
module.exports = function(Task) {
Task.observe('before save', function formatTitile(ctx, next) {
const model = ctx.instance ? ctx.instance : ctx.data;
if (model.title) {
model.title = model.title.toUpperCase();
}
next();
});
};
### ☑︎ TaskMe: Step 2.3
### Model的验证
### 任务列表
* 为Task Model增加验证逻辑
* title属性字符个数介于1和64之间
* state属性取值必须是0或1
### 为Task Model增加验证逻辑
'use strict';
module.exports = function(Task) {
Task.validatesLengthOf('title', { min: 1, max: 64});
Task.validatesInclusionOf('state', {
in: [0, 1], message: 'is not allowed'
});
...
};
### ☑︎ TaskMe: Step 2.4
### Model的关联
### 任务列表
* 为Task Model和Note Model建立关联
### 为Task和Note建立关联(LoopBack CLI)
修改common/models/task.json,如下
// In task.json
...
"relations": {
"notes": {
"type": "hasMany",
"model": "Note",
"foreignKey": "taskId"
}
},
...
注:也可使用lb relation命令
### 为Task和Note建立关联(API Connect)
在命令行执行apic,并回答一系列问题,参考如下
![](images/ws/apic-relation-create.jpg)
### ☑︎ TaskMe: Step 3.1
### 定义Remote Method
### 任务列表
* 为Task定义remote method:getList()
### 为Task定义getList(LoopBack CLI)
在task.json中增加getList的定义
"methods": {
"getList": {
"accepts": [{
"arg": "state", "type": "string", "required": false,
"http": { "source": "path" }
}],
"returns": [{
"arg": "tasks", "type": "[Task]", "root": true
}],
"description": "Get a list of tasks.",
"http": [{
"path": "/state/:state", "verb": "get"
}]
}
},
注:也可使用lb remote-method命令
### 为Task定义getList(API Connect)
![](images/ws/apic-method-create.jpg)
### 为Task定义getList
在common/models/task.js中增加getList的实现
module.exports = function(Task) {
...
/**
* Get a list of tasks.
* @param {string} state State of task.
* @param {Function(Error, array)} callback
*/
Task.getList = function(state, callback) {
const query = {
where: {
state: (state === 'done') ? 1 : 0
}
};
Task.find(query, callback);
};
};
### ☑︎ TaskMe: Step 3.2
### 定义Mixin
### 任务列表
* 为Task定义能自动添加日期类属性的Mixin
### 为Task定义能自动添加日期类属性的Mixin
添加server/mixins/timestamp.js,如下:
'use strict';
module.exports = function(Model) {
Model.defineProperty('created', {type: Date, defaultFn: 'now'});
Model.defineProperty('modified', {type: Date, defaultFn: 'now'});
Model.observe('before save', function event(ctx, next) {
const model = ctx.instance ? ctx.instance : ctx.data;
model.modified = new Date();
next();
});
};
### 为Task定义能自动添加日期类属性的Mixin
修改common/models/task.json,声明如下
...
"mixins": {
"Timestamp": true
}
...
### ☑︎ TaskMe: Step 3.3
### 定义Middleware
### 任务列表
* 为Task添加用于模拟认证的Middleware
### 模拟认证的Middleware(LoopBack CLI)
添加server/middlewares/auth.js,如下:
'use strict';
module.exports = function() {
return function (req, res, next) {
if (req.query && req.query.access_token) {
next();
} else {
res.sendStatus(401);
}
}
};
### 模拟认证的Middleware(LoopBack CLI)
修改server/middleware.json,如下:
...
"auth": {
"./middlewares/auth": {
"paths": [
"${restApiRoot}"
]
}
},
...
### ☑︎ TaskMe: Step 3.4
### 定义Boot Script
### 任务列表
* 在初始化阶段为Task添加Remote Hook
### 初始化阶段的Remote Hook(LoopBack CLI)
添加server/boot/init-model.js,如下:
'use strict';
module.exports = function(app) {
const Task = app.models.Task;
Task.afterRemote('getList', function(ctx, unused, next) {
ctx.res.header('X-TaskMe', 'This is a test');
next();
});
};
### ☑︎ TaskMe: Step 4.1
### 使用过滤条件
### 任务列表
* 修改Task.getList(),增加各种过滤条件
### 修改Task.getList(),增加各种过滤条件
在task.json中修改getList的定义
"methods": {
"getList": {
"accepts": [{
"arg": "state", "type": "string", "required": false,
"http": { "source": "path" }
},
{
"arg": "page", "type": "integer", "required": false,
"http": { "source": "query" }
},
{
"arg": "pageSize", "type": "integer", "required": false,
"http": { "source": "query" }
},
{
"arg": "order", "type": "string", "required": false,
"http": { "source": "query" }
},
{
"arg": "fields", "type": "string", "required": false,
"http": { "source": "query" }
}],
"returns": [ ... ],
"description": "Get a list of tasks.",
"http": [ ... ]
},
### 修改Task.getList(),增加各种过滤条件
在task.js中修改getList的实现
Task.getList = function(state, p, ps, order, fields, cb) {
const where = { state: (state === 'done') ? 1 : 0 };
const limit = ps || 100;
const skip = ((p || 1) - 1) * limit;
const query = { where, limit, skip };
if (order) {
query.order = [];
order.split(',').forEach(function(item) {
query.order.push(item);
});
}
if (fields) {
query.fields = {};
fields.split(',').forEach(function(item) {
query.fields[item] = true;
});
}
Task.find(query, cb) ;
};
### ☑︎ TaskMe: Step 4.2
### 定义数据源和连接器
### 任务列表
* 为TaskMe增加一个MongoDB的数据源
* 在Boot Script中利用auto-update为MongoDB新加Index
* 直接操作连接器,为Task增加针对title属性的全文搜索
### 添加MongoDB数据源(LoopBack CLI)
添加server/datasources.production.json,如下
{
"db": {
"name": "db",
"database": "taskme",
"connector": "mongodb"
}
}
修改package.json,增加script如下
"scripts": {
...
"start-prod": "cross-env NODE_ENV=production node .",
...
}
### 在Boot Script中添加Index(LoopBack CLI)
在server/boot/init-model.js中添加代码如下:
'use strict';
module.exports = function(app) {
...
app.datasources.db.autoupdate();
};
### 实现title属性的全文搜索(LoopBack CLI)
在common/models/task.js中添加代码如下:
Task.search = function(q, cb) {
const connector = Task.getDataSource().connector;
const collection = connector.db.collection('tasks');
const query = {
"$text": {
"$search": q,
}
};
return collection.find(query).toArray(function(err, items) {
const tasks = items.map(function(item) {
return new Task(item);
});
cb(err, tasks);
});
}
### ☑︎ TaskMe: Step 5
### 代码测试,测试覆盖,代码静态检查
### 任务列表
* 为TaskMe增加基于Jasmine的单元测试,分别测试:
* TaskMe的Model方法
* TaskMe的REST API接口
* 基于MongoDB的search逻辑
* 增加测试覆盖(istanbul)
* 增加代码静态检查(eslint)
### 改动说明
* 详见[GitHub源码](https://github.com/morningspace/lab-loopback/tree/master/code/taskme-5)
* 修改package.json
* 增加.eslintrc, .eslintignore
* 增加.istanbul.yml
* 增加测试代码在tests目录下
### ☑︎ TaskMe: Step 6
### 将程序发布到API Connect
### 在IBM Cloud上安装API Connect
* 登录[你在IBM Cloud的仪表盘](https://console.bluemix.net)
* 进入Catalog页面,搜索API Connect
* 选择创建API Connect服务到自己的空间
![](images/ws/ic-provision-apic.png)
### 进入API Connect仪表盘
* 返回仪表盘页面,并点击进入API Connect的仪表盘页面,默认会有一个名为Sandbox的catalog
![](images/ws/ic-apic-dashboard.png)
### 通过本地apic新建Application
* 用apic edit打开本地API Connect管理页面
* 点击Publish > Add and Manage Targets
* 在Publish对话框中选择Add IBM Bluemix target
* 在Select an organization and catalog对话框中选择正确的Regin,Organization,以及Catalog,如:默认的Sandbox
* 点击下一步后,选择新建一个application,名为:taskme,然后点Save保存
### 通过本地apic发布应用代码
* 再次点击Publish,并选择刚才定义并保存的Catalog+App+Org组合
![](images/ws/ic-apic-publish.jpg)
### 通过本地apic发布应用代码
* 在Publish对话框中选择Publish application,以及Stage or Publish products,并点Publish开发发布
![](images/ws/ic-apic-publish-2.jpg)
### 通过本地apic发布应用代码
* 当控制台看到如下日志时,表示应用程序发布成功
![](images/ws/ic-apic-publish-3.jpg)
### 在IBM Cloud上测试发布后的应用
* 刷新API Connect仪表盘页面可看到刚发布的taskme应用
* 点击Explore,选择Catalog > Sandbox,可打开Explore页面进行测试
![](images/ws/ic-apic-dashboard-2.png)
### 在IBM Cloud上监控发布后的应用
* 回到[你在IBM Cloud的仪表盘](https://console.bluemix.net)可见刚发布的taskme应用
![](images/ws/ic-apic-dashboard-3.png)
### 在IBM Cloud上监控发布后的应用
* 点击应用进入该应用的仪表盘,可对应用进行实时监控
![](images/ws/ic-apic-dashboard-4.png)