explain-unicloud
Version:
888 lines (755 loc) • 25.2 kB
Markdown
# EXPLAIN-UNICLOUD
快速、极简的uniCloud云函数开发框架。已支持云函数URL化,已支持RESTful。
框架交流QQ群:[970799055](https://jq.qq.com/?_wv=1027&k=KFkDL5gp)。
## 示例项目目录结构
```
┌── cloudfunctions 云函数目录
| ├── common 云函数公共模块
| | └── explain-unicloud 核心框架
| | └── explain-validator 数据校验器模块
| | └── explain-unicloud-mvc MVC模块
| ├── app 普通应用目录
| | ├── index.js 云函数入口
| | ├── startup.js 启动配置、路由配置、中间件、过滤器配置等
| | ├── services 云函数集
| | ├── filters 过滤器
| | ├── schemas JSON Schema目录
| | └── views 视图页
| └── mvc MVC应用目录
| ├── index.js 云函数入口
| ├── startup.js 启动配置、路由配置、中间件、过滤器配置等
| ├── models 模型
| ├── views 视图
| └── controllers 控制器
├── js_sdk
| └── explain-creq 云函数和http请求模块
└── common
├── creq.interceptor.callfunction 云函数请求拦截器配置
└── creq.interceptor.request http请求拦截器配置
```
## 使用方式
### 匹配模式
|参数|类型|必填|说明|
|-- |-- |-- |-- |
|service/serviceKey|String|是|需要指向的service的,若参数名与业务参数冲突可通过`app.init`配置`serviceKey`改变参数名称|
|action/actionKey|String|是|需要指向的action的,若参数名与业务参数冲突可通过`app.init`配置`actionKey`改变参数名称|
|data/dataKey|String|是|请求包含的参数,若参数名与业务参数冲突可通过`app.init`配置`dataKey`改变参数名称|
#### 云函数入口
```javascript
// app -> index.js
const explain = require("explain-unicloud");
exports.main = async (event, context) => explain.run({
event,
context,
(app) => {
app.init({baseDir: __dirname});
}
});
```
#### 云函数
```javascript
// app -> services -> values.js
// 继承抽象父类explain.service,无需写构造函数即可调用this.event,this.context,this.explain
const explain = require("explain-unicloud");
module.exports = class home extends explain.service {
index(data) {
return 'Hello, World!';
}
}
```
#### 前端
```javascript
uniCloud.callFunction({
name: 'app',
data: {
service: 'values',
action: 'getValuesAsync',
data: {}
}
}).then(res => {
console.log(res)
})
```
推荐使用`explain-creq`,封装了请求拦截器,响应拦截器和错误拦截器,可自由定制请求数据处理和响应结果处理
```javascript
import creq from '@/uni_modules/explain-creq/js_sdk/explain-creq.js'
creq.callFunction('values', 'getValuesAsync', {
name: 'app', // 指定云函数名称
data: {} // 请求参数
}).then(res => {
console.log(res)
})
```
### 路由模式
#### 云函数入口
```javascript
// app -> index.js
const explain = require("explain-unicloud");
exports.main = async (event, context) => explain.run({
event,
context,
(app) => {
app.init({baseDir: __dirname});
// 批量添加路由
app.route.add([{
route: "api/values", // 可省略,省略后路由模板为service,也就是"values"
service: "values", // services目录下的service文件名
routes: [
// GET api/values 获取列表数据
{
// route: "", // 路由模板与service一致则可省略
// httpMethod: "GET", // action名称以对应RESTful动词开头则可省略
action: "getValuesAsync" // service中被调用的方法名称
},
// GET api/values/5 获取id为5的数据,{id}会自动解析为参数注入event.data中
{
route: "{id}", // 将动态参数作为路由
// httpMethod: "GET", // action名称以对应RESTful开头则可省略
action: "getValueAsync"
},
// POST api/values 新增一条数据
{
// route: "", // 路由模板与service一致则可省略
// httpMethod: "POST", // action名称以对应RESTful开头则可省略,除此之外还支持以create、add、insert开头
action: "postValueAsync"
},
// PUT api/values/5 更新id为5的数据,{id}会自动解析为参数注入event.data中
{
route: "{id}",
// httpMethod: "PUT", // action名称以对应RESTful开头则可省略,除此之外还支持以update开头
action: "putValueAsync"
},
// DELETE api/values/5 删除id为5的数据,{id}会自动解析为参数注入event.data中
{
route: "{id}",
// httpMethod: "DELETE", // action名称以对应RESTful开头则可省略,除此之外还支持以remove开头
action: "deleteValueAsync"
},
// GET api/values/exists/5 获取id为5的数据是否存在,{id}会自动解析为参数注入event.data中
{
route: "exists/{id}",
httpMethod: "GET",
action: "checkValueIsExistsAsync"
},
// GET api/values/new/10 获取type为new,column为10的数据,{type}、{column}会自动解析为参数注入event.data中
{
route: "{type}/{column}",
httpMethod: "GET",
action: "getValuesAsync"
},
// GET api/values/2020/02/28 获取year为2020,month为02,day为28的数据,{year}、{month}、{day}会自动解析为参数注入event.data中
{
route: "{year}/{month}/{day}",
httpMethod: "GET",
action: "getValueByYearAndMonthAndDayAsync"
},
// GET api/values/2020-02-28 获取year为2020,month为02,day为28的数据,{year}、{month}、{day}会自动解析为参数注入event.data中
{
route: "{year}-{month}-{day}",
httpMethod: ["GET"],
action: "getValueByYearAndMonthAndDayAsync"
},
// GET api/values/date-2020-02-28/time:19:30:00 获取year为2020,month为02,day为28,hour为19,minute为30,second为00的数据,{year}、{month}、{day}、{hour}、{minute}、{second}会自动解析为参数注入event.data中
{
route: "date-{year}-{month}-{day}/time:{hour}:{minute}:{second}",
httpMethod: ["GET"],
action: "getValueByYearAndMonthAndDayAsync"
}
]
}, {
route: "api/test",
service: "test",
routes: [{
route: "checktoken",
httpMethod: ["GET", "POST"],
action: "checkToken"
}]
}]);
// 添加单个路由,此处将服务端渲染html作为示例
app.route.add({
// 云函数URL化后访问:https://云函数域名/请求路径/index.html
route: "index.html",
service: "home",
routes: [{
action: "index",
httpMethod: ["GET"]
}]
});
// 设置根路由,设置后云函数URL化不用输入路由可到达指定方法
app.route.add([{
route: "/",
service: "home",
routes: [{
action: "index",
httpMethod: ["GET"]
}]
}, {
route: "/",
service: "values",
routes: [{
action: "postValueAsync",
httpMethod: ["POST"]
}, {
action: "putValueAsync",
httpMethod: ["PUT"]
}, {
action: "deleteValueAsync",
httpMethod: ["DELETE"]
}]
}]);
}
});
```
#### 云函数
```javascript
// app -> services -> values.js
// 继承抽象父类explain.service,无需写构造函数即可调用this.event,this.context,this.explain
const explain = require("explain-unicloud");
module.exports = class values extends explain.service {
async getValuesAsync() {
return {
data: ["explain-unicloud", "explain-admin"],
message: "获取成功"
}
}
async getValueAsync({
id
}) {
return {
data: {
id,
name: "Sansnn"
},
message: "获取成功"
}
}
async postValueAsync({
name,
like
}) {
return {
data: {
id: 9,
name,
like
},
message: "添加成功"
}
}
async putValueAsync({
id,
name,
like
}) {
return {
data: {
id,
name,
like
},
message: "更新成功"
}
}
async deleteValueAsync({
id
}) {
return {
data: {
id
},
message: "删除成功"
}
}
async checkValueIsExistsAsync({
id
}) {
return {
data: {
id,
checked: true
},
message: "检查成功"
}
}
async getValueByYearAndMonthAndDayAsync({
year,
month,
day
}) {
return {
data: {
date: `${year}-${month}-${day}`
},
message: "获取成功"
}
}
}
更多示例请前往目录uniCloud -> cloudfunctions -> app -> services中查看
```
#### 前端
```javascript
uni.request({
url: 'http://tcb-e386czuna1dv2wib7e6bd-d064f3.service.tcloudbase.com/http/app/api/values',
method: 'post',
data: {
name: 'Sansnn',
like: ['explain-unicloud', 'explain-admin']
},
success: res => {
console.log(res)
}
})
```
推荐使用`explain-creq`,封装了请求拦截器,响应拦截器和错误拦截器,可自由定制请求数据处理和响应结果处理
```javascript
import creq from '@/uni_modules/explain-creq/js_sdk/explain-creq.js'
creq.request('http://tcb-e386czuna1dv2wib7e6bd-d064f3.service.tcloudbase.com/http/app/api/values', 'post', {
data: {
name: 'Sansnn',
like: ['explain-unicloud', 'explain-admin']
}
}).then(res => {
console.log(res)
})
更多示例请前往目录pages -> route -> route.vue中查看
```
## 云函数URL化(仅支持路由模式)
#### 示例
> 腾讯云 GET http://${spaceId}.service.tcloudbase.com/${path}/api/values?a=1&b=2
> 阿里云 GET https://${spaceId}.bspapp.com/${path}/api/values?a=1&b=2
> 完整参考 GET http://tcb-e386czuna1dv2wib7e6bd-d064f3.service.tcloudbase.com/http/app/api/values
## 使用中间件
执行顺序为使用中间件时的顺序,先使用的先执行,后使用的后执行。
### app.use()
### 示例
```javascript
// app -> startup.js
module.exports = (app) => {
app.init({
baseDir: __dirname
});
// 使用中间件示例
app.use(async ({
event,
context,
explain,
next
}) => {
// 异常处理中间件
try {
console.log("m1-0")
await next();
console.log("m1-1")
} catch (e) {
// 将响应信息改为异常信息
explain.response.body = {
message: e.message + (explain.request.service === "test" && explain.request
.action === "exception" ? ",经过了异常处理中间件" : "")
}
}
});
app.use(async ({
next
}) => {
console.log("m2-0")
await next();
console.log("m2-1")
});
app.use(async ({
next
}) => {
console.log("m3-0")
await next();
console.log("m3-1")
});
}
```
## 使用过滤器
执行顺序为注册过滤器时的顺序,先注册的先执行,后注册的后执行。
### onActionExecuting
方法执行时触发。
### 示例:实现用户身份验证
#### 云函数入口
```javascript
// app -> startup.js
module.exports = (app) => {
app.init({
baseDir: __dirname
});
// 使用过滤器示例
app.filter.add([{
filter: require("./filters/tokenFilter"), // 添加身份验证过滤器
// 忽略一下service和action
ignore: [{
service: "values"
}, {
service: "home"
}, {
service: "test",
actions: ["exception"]
}]
}]);
}
```
#### 过滤器代码
```javascript
// app -> filters -> tokenFilter.js
// 需要继承抽象父类explain.filter
const explain = require("explain-unicloud");
module.exports = class tokenFilter extends explain.filter {
async onActionExecuting() {
let {
explain,
context
} = this;
if (!explain.request.data.token) {
// 使用explain.response.body直接返回至客户端
explain.response.body = {
code: 401,
message: "缺少token"
}
return;
}
// let user = checkToken(event.request.data.token); // 自行封装的token验证方法
let user = explain.request.data.token == "Sansnn" ? {
id: 1,
name: "Sansnn"
} : null;
if (user) {
context.user = user;
} else {
explain.response.body = {
code: 401,
message: "token无效"
}
}
}
}
```
#### 云函数
```javascript
// app -> services -> test.js
const explain = require("explain-unicloud");
module.exports = class test extends explain.service {
async checkToken() {
return {
checked: true,
data: this.context.user // 得到根据token解析出来的用户对象
}
}
}
```
### onActionExecuted
方法执行后触发。
### 示例:实现请求日志记录
#### 云函数入口
```javascript
// app -> startup.js
module.exports = (app) => {
app.init({
baseDir: __dirname
});
// 使用过滤器示例
app.filter.add([{
filter: require("./filters/requestFilter"), // 添加请求记录过滤器
ignore: [{
service: "home",
actions: ["index"]
}]
}]);
}
```
#### 过滤器代码
```javascript
// app -> filters -> requestFilter.js
// 需要继承抽象父类explain.filter
const explain = require("explain-unicloud");
module.exports = class requestFilter extends explain.filter {
async onActionExecuting() {
let {
explain
} = this;
console.log("------------");
console.log("请求开始");
if (explain.request.service) {
console.log(`service: ${explain.request.service}`);
}
if (explain.request.action) {
console.log(`action: ${explain.request.action}`);
}
if (explain.request.route) {
console.log(`route: ${explain.request.route}`);
}
if (explain.request.routeTemplate) {
console.log(`routeTemplate: ${explain.request.routeTemplate}`);
}
if (explain.request.httpMethod) {
console.log(`httpMethod: ${explain.request.httpMethod}`);
}
if (explain.request.data) {
console.log(`data: ${JSON.stringify(explain.request.data)}`);
}
console.log("------------");
console.log(this.explain)
}
async onActionExecuted() {
console.log(this.explain)
let {
explain
} = this;
console.log("------------");
console.log("请求结束");
console.log(`response: ${JSON.stringify(explain.response.body)}`);
console.log("------------");
// 可对explain.response.body重新赋值来改变响应结果
if (explain.mode === "route") {
explain.response.body = {
response: explain.response.body,
service: explain.request.service,
action: explain.request.action,
data: explain.request.data,
route: explain.request.route,
routeTemplate: explain.request.routeTemplate,
httpMethod: explain.request.httpMethod
}
} else {
explain.response.body = {
response: explain.response.body,
service: explain.request.service,
action: explain.request.action,
data: explain.request.data
}
}
}
}
```
### onException
发生异常触发。
### 示例:实现全局异常处理
#### 云函数入口
```javascript
// app -> startup.js
module.exports = (app) => {
app.init({
baseDir: __dirname
});
// 使用过滤器示例
app.filter.add([{
filter: require("./filters/exceptionFilter") // 添加异常处理过滤器
}]);
}
```
#### 过滤器代码
```javascript
// app -> filters -> exceptionFilter.js
// 需要继承抽象父类explain.filter
const explain = require("explain-unicloud");
module.exports = class exceptionFilter extends explain.filter {
async onException() {
let {
explain
} = this;
// 输出日志
console.error("------------");
console.error("发生错误");
if (explain.request.service) {
console.error(`service: ${explain.request.service}`);
}
if (explain.request.action) {
console.error(`action: ${explain.request.action}`);
}
if (explain.request.route) {
console.error(`route: ${explain.request.route}`);
}
if (explain.request.httpMethod) {
console.error(`httpMethod: ${explain.request.httpMethod}`);
}
if (explain.request.routeTemplate) {
console.error(`routeTemplate: ${explain.request.routeTemplate}`);
}
if (explain.request.data) {
console.error(`data: ${JSON.stringify(explain.request.data)}`);
}
console.error(`异常信息: ${explain.exception.message}`);
console.error("原始异常: ", explain.exception);
console.error("------------");
// 发送异常信息到电子邮件
//
throw new Error(explain.exception.message + ",经过了异常处理过滤器");
}
}
```
## 配置说明
### app.init(options)
#### 初始化,baseDir是必须配置的
|参数|类型|必填|说明|
|-- |-- |-- |-- |
|baseDir|String|是|项目根目录|
|serviceDir|String|否|service目录,/开头,/结尾,默认`/services/`|
|serviceKey|String|否|匹配模式下service参数别名,作用是与其他参数冲突时可以修改为别的名称,默认`service`|
|actionKey|String|否|匹配模式下action参数别名,作用是与其他参数冲突时可以修改为别的名称,默认`action`|
|dataKey|String|否|匹配模式下data参数别名,作用是与其他参数冲突时可以修改为别的名称,默认`data`|
|enableMatchMode|Boolen|否|启用匹配模式,默认为`true`,`false`为禁用,禁用后仅支持路由模式访问业务函数|
|matchIgnore|Array|否|为保证安全性,匹配模式可忽略所指定的service和actions,忽略后仅配置路由模式后可访问,格式为`[{service: "serviceName", actions: ["actionName1", "actionName2"]},{...}]`,不写`actions`表示忽略该service下所有action|
#### 示例
```javascript
// 初始化
app.init({
baseDir: __dirname, // 项目根目录
serviceDir: "/services/", // service目录
serviceKey: "service", // 匹配模式下service参数别名,默认"service",作用是与其他参数冲突时可以修改为别的名称
actionKey: "action", // 匹配模式下action参数别名,默认"action",作用是与其他参数冲突时可以修改为别的名称
dataKey: "data", // 匹配模式下data参数别名,默认"data",作用是与其他参数冲突时可以修改为别的名称
enableMatchMode: true, // 启用匹配模式,false为禁用,禁用后仅支持路由模式访问业务函数
matchIgnore: [ // 匹配模式忽略指定的service和actions,忽略后仅配置路由模式后可访问
{
service: "home",
actions: ["index"]
}
]
});
```
## 基类说明
### service
服务基类,继承后构造函数会自动注入`event`,`context`,`explain`几个对象,可通过`this.event`,`this.context`,`this.explain`进行调用。
#### explain对象
框架扩展,主要内容如下:
|参数|类型|说明|
|-- |-- |-- |
|mode|String|获取本次请求为`match`匹配模式还是`route`路由模式|
|config|Object|通过`app.init`初始化时的配置信息,详见`app.init`|
|request|Object|请求对象,详情见下方`explain.request`|
|response|Object|响应对象,详情见下方`explain.response`|
|exception|Object|异常对象|
|routes|Array|已配置的路由集合|
|middlewares|Object|已配置的中间件集合|
|filters|Array|已配置的过滤器集合|
|useService|Function|在服务中使用其他service,若被使用的service是继承自explain.service,则会自动注入`event`,`context`,`explain`,详情见下|
##### explain.request
|参数|类型|说明|
|-- |-- |-- |
|service|String|匹配到的`service`文件名称|
|action|String|匹配到的`action`方法名称|
|data|Object|客户端传入的参数|
|isHttp|Boolen|是否是HTTP请求、是否是云函数URL化|
|route|String|客户端传入的路由,只有当`mode`为`route`时该属性才有值|
|routeTemplate|String|成功匹配的路由模板,只有当`mode`为`route`时该属性才有值|
|httpMethod|String|HTTP Method,只有当`mode`为`route`时该属性才有值|
##### explain.response
|参数|类型|说明|
|-- |-- |-- |
|mpserverlessComposedResponse|Boolen|使用阿里云返回集成响应是需要此字段为true|
|statusCode|Number|HTTP StatusCode|
|headers|Object|HTTP Reponse Headers|
|body|Object|集成响应内容主体|
##### explain.useService(obj)
|参数|类型|必填|说明|
|-- |-- |-- |-- |
|obj|Function|是|引入的未被实例化的service对象|
### filter
过滤器基类,继承后构造函数会自动注入`event`,`context`,`explain`几个对象,可通过`this.event`,`this.context`,`this.explain`进行调用,每个对象的作用同`service`基类。
## 内置模块
### dateTime 日期时间操作
#### now(formatString, timezone)
获取当前时间
|参数|类型|必填|说明|
|-- |-- |-- |-- |
|formatString|String|否|日期格式化,`yyyy`表示年份,`MM`表示月份,`dd`表示日期,`HH`表示时,`mm`表示分,`ss`表示秒,`fff`表示毫秒。不填则返回为时间戳|
|timezone|Number|否|formatString存在时有效,格式化为指定时区的时间,默认8|
#### format(timestamp, formatString, timezone)
日期格式化
|参数|类型|必填|说明|
|-- |-- |-- |-- |
|timestamp|Number/String|是|要格式化的时间戳|
|formatString|String|是|日期格式化,`yyyy`表示年份,`MM`表示月份,`dd`表示日期,`HH`表示时,`mm`表示分,`ss`表示秒,`fff`表示毫秒|
|timezone|Number|否|时区,默认8|
#### addYears(timestamp, value)
添加年份
|参数|类型|必填|说明|
|-- |-- |-- |-- |
|timestamp|Number/String|是|时间戳|
|value|Number|是|要添加的年份数值|
#### addMonths(timestamp, value)
添加月份
|参数|类型|必填|说明|
|-- |-- |-- |-- |
|timestamp|Number/String|是|时间戳|
|value|Number|是|要添加的月份数值|
#### addDays(timestamp, value)
添加日期
|参数|类型|必填|说明|
|-- |-- |-- |-- |
|timestamp|Number/String|是|时间戳|
|value|Number|是|要添加的日期数值|
#### addHours(timestamp, value)
添加时
|参数|类型|必填|说明|
|-- |-- |-- |-- |
|timestamp|Number/String|是|时间戳|
|value|Number|是|要添加的时数值|
#### addMinutes(timestamp, value)
添加分
|参数|类型|必填|说明|
|-- |-- |-- |-- |
|timestamp|Number/String|是|时间戳|
|value|Number|是|要添加的分数值|
#### addSeconds(timestamp, value)
添加秒
|参数|类型|必填|说明|
|-- |-- |-- |-- |
|timestamp|Number/String|是|时间戳|
|value|Number|是|要添加的秒数值|
#### addMilliseconds(timestamp, value)
添加毫秒
|参数|类型|必填|说明|
|-- |-- |-- |-- |
|timestamp|Number/String|是|时间戳|
|value|Number|是|要添加的毫秒数值|
#### 使用示例
```javascript
const { dateTime } = require("explain-unicloud");
dateTime.now("yyyy-MM-dd HH:mm:ss"); // 得到格式为2020-11-11 00:00:00的当前时间
dateTime.format(1605024000000, "yyyy年MM月dd日HH时mm分ss秒"); // 格式化时间为2020年11月11日00时00分00秒
dateTime.addYears(1605024000000, 2); // 原始日期为2020年11月11日,年份增加2年,结果为2022年11月11日
dateTime.addMonths(1605024000000, 13); // 原始日期为2020年11月,增加13个月,结果为2021年12月
dateTime.addHours(1605024000000, -1); // 原始日期为2020年11月11日00时,增加-1时,结果为2020年11月10日23时
```
### object 对象操作
#### sort(obj)
属性名称按照ASCII码从小到大排序(字典序)
|参数|类型|必填|说明|
|-- |-- |-- |-- |
|obj|Object|是|需要排序的对象|
#### 使用示例
```javascript
const { object } = require("explain-unicloud");
var obj = {
b: 1,
c: 2,
a: 3
}
obj = object.sort(obj); // 结果为{a:3,b:1,c:2}
```
<!-- ## 扩展模块
|名称|说明|链接|
|-- |-- |-- |
|explain-qrcode|二维码模块|[https://ext.dcloud.net.cn/plugin?id=3359](https://ext.dcloud.net.cn/plugin?id=3359)|
|explain-cache|缓存模块|[https://ext.dcloud.net.cn/plugin?id=3425](https://ext.dcloud.net.cn/plugin?id=3425)|
|explain-weixin|微信模块|[https://ext.dcloud.net.cn/plugin?id=3678](https://ext.dcloud.net.cn/plugin?id=3678)|
## 前端模板
|名称|说明|链接|
|-- |-- |-- |
|explain-mall|商城模板|[https://ext.dcloud.net.cn/plugin?id=3637](https://ext.dcloud.net.cn/plugin?id=3637)|
|explain-form|低代码表单生成组件|[https://ext.dcloud.net.cn/plugin?id=4880](https://ext.dcloud.net.cn/plugin?id=4880)|
## 项目模板
|名称|说明|链接|
|-- |-- |-- |
|explain-admin|云端一体后台模板|[https://ext.dcloud.net.cn/plugin?id=5084](https://ext.dcloud.net.cn/plugin?id=5084)|
-->
### 更多模块正在开发中,敬请期待...
## 解决方案
|名称|说明|链接|
|-- |-- |-- |
|unicloud云函数TS开发最优解决方案|感谢 `老沈-1018715564@qq.com` 提供|[https://www.yinzhuoei.com/index.php/archives/470](https://www.yinzhuoei.com/index.php/archives/470)|
## 第三方插件(感谢插件作者):
[json格式数据展示](https://ext.dcloud.net.cn/plugin?id=2687) @作者:罗魔什