cloud-obj.md 13.2 KB
Newer Older
雪洛's avatar
雪洛 已提交
1 2 3 4
## 云对象

> 新增于 HBuilderX 3.4.0

5
## 背景和优势
雪洛's avatar
雪洛 已提交
6

7
20年前,restful接口开发开始流行,服务器编写接口,客户端调用接口,传输json。
雪洛's avatar
雪洛 已提交
8

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
现在,替代restful的新模式来了。

云对象,服务器编写API,客户端调用API,不再开发传输json的接口。思路更清晰、代码更精简。

比如服务端编写一个云对象todo,该对象有add、get、remove、update等方法。客户端的js则可以直接import这个todo云对象,直接调用add等方法。

服务器示例代码如下:

HBuilderX中在uniCloud/cloudfunctions目录新建云函数,选择类型为云对象,起名为todo。打开云对象入口index.obj.js,添加一个add方法。

```js
// 云对象名:todo
module.exports = {
	add(title, content) {
		title = title.trim()
		content = content.trim()
		if(!title || !content) {
			return {
				errCode: 'INVALID_TODO',
				errMsg: 'TODO标题或内容不可为空'
			}
		}
		// ...其他逻辑
		return {
			errCode: 0,
			errMsg: '创建成功'
		}
	}
}
```

然后在客户端的js中,import这个todo对象,调用它的add方法

```js
const todo = uniCloud.importObject('todo') //第一步导入云对象
async function addTodo () {
	try {
		const res = await todo.add('title demo', 'content demo') //导入云对象后就可以直接调用该对象的方法了,注意使用异步await
		uni.showToast({
			title: '创建成功'
		})
	} catch (e) {
		// 符合uniCloud响应体规范 https://uniapp.dcloud.net.cn/uniCloud/cf-functions?id=resformat,自动抛出此错误 
		uni.showModal({
			title: '创建失败',
			content: e.errMsg,
			showCancel: false
		})
	}
}
```

可以看到云对象的代码非常清晰,代码行数也只有33行。

而同样的逻辑,使用传统的接口方式则需要更多代码,见下:
雪洛's avatar
雪洛 已提交
64 65 66

```js
// 传统方式调用云函数-云函数代码
雪洛's avatar
雪洛 已提交
67
// 云函数名:todo
雪洛's avatar
雪洛 已提交
68 69 70 71
// 云函数入口index.js内容如下
'use strict';
exports.main = async (event, context) => {
	const {
雪洛's avatar
雪洛 已提交
72
		method,
雪洛's avatar
雪洛 已提交
73 74
		params
	} = event
雪洛's avatar
雪洛 已提交
75
	switch(method) {
雪洛's avatar
雪洛 已提交
76 77 78 79
		case 'add': {
			let {
				title,
				content
雪洛's avatar
雪洛 已提交
80
			} = params
雪洛's avatar
雪洛 已提交
81 82 83
			title = title.trim()
			content = content.trim()
			if(!title || !content) {
雪洛's avatar
雪洛 已提交
84
				return {
雪洛's avatar
雪洛 已提交
85 86
					errCode: 'INVALID_TODO',
					errMsg: 'TODO标题或内容不可为空'
雪洛's avatar
雪洛 已提交
87 88
				}
			}
雪洛's avatar
雪洛 已提交
89
			// ...省略其他逻辑
雪洛's avatar
雪洛 已提交
90 91
			return {
				errCode: 0,
雪洛's avatar
雪洛 已提交
92
				errMsg: '创建成功'
雪洛's avatar
雪洛 已提交
93 94 95 96
			}
		}
	}
	return {
雪洛's avatar
雪洛 已提交
97 98
		errCode: 'METHOD_NOT_FOUND',
		errMsg: `Method[${method}] not found`
雪洛's avatar
雪洛 已提交
99 100 101 102
	}
};

// 传统方式调用云函数-客户端代码
雪洛's avatar
雪洛 已提交
103
async function addToDo () {
雪洛's avatar
雪洛 已提交
104 105
	try {
		const res = uniCloud.callFunction({
雪洛's avatar
雪洛 已提交
106
			name: 'todo', 
雪洛's avatar
雪洛 已提交
107
			data: {
雪洛's avatar
雪洛 已提交
108
				method: 'add',
雪洛's avatar
雪洛 已提交
109
				params: {
雪洛's avatar
雪洛 已提交
110 111
					title: 'title demo',
					content: 'content demo'
雪洛's avatar
雪洛 已提交
112 113 114 115 116 117 118 119 120
				}
			}
		})
		const {
			errCode,
			errMsg
		} = res.result
		if(errCode) {
			uni.showModal({
雪洛's avatar
雪洛 已提交
121
				title: '创建失败',
雪洛's avatar
雪洛 已提交
122 123 124 125 126 127
				content: errMsg,
				showCancel: false
			})
			return
		}
		uni.showToast({
雪洛's avatar
雪洛 已提交
128
			title: '创建成功'
雪洛's avatar
雪洛 已提交
129 130 131
		})
	} catch (e) {
		uni.showModal({
雪洛's avatar
雪洛 已提交
132
			title: '创建失败',
雪洛's avatar
雪洛 已提交
133 134 135 136 137 138 139
			content: e.message,
			showCancel: false
		})
	}
}
```

140
以上传统开发需要68行代码,对比云对象的33行代码,不但工作量大,而且逻辑也不如云对象清晰。
雪洛's avatar
雪洛 已提交
141

142
_注:以上例子仅用于方便初学者理解。实质上对于简单的数据库操作,使用clientDB在前端直接操作数据库是更简单、代码更少的方案,[另见](/uniCloud/clientdb)_
雪洛's avatar
雪洛 已提交
143

144 145 146 147 148 149
总结下云对象带来的好处:
1. 更清晰的逻辑
2. 更精简的代码
3. 更少的协作成本(以及矛盾~)
4. 客户端调用时在ide里有完善的代码提示,方法参数均可提示。(传输json可没法在ide里提示)
5. 自动支持[uniCloud响应体规范](uniCloud/cf-functions.md?id=resformat),方便错误拦截和统一处理
雪洛's avatar
雪洛 已提交
150

雪洛's avatar
雪洛 已提交
151
## 快速上手
雪洛's avatar
雪洛 已提交
152

雪洛's avatar
雪洛 已提交
153
### 创建云对象
雪洛's avatar
雪洛 已提交
154

155 156 157 158 159
云对象,其实是对云函数的封装。和创建云函数一样,在`uniCloud/cloudfunctions`目录右键新建云函数,选择云对象类型,输入云对象名称创建云对象,此处以云对象todo为例,创建的云对象包含一个`index.obj.js`

![创建云对象](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-f184e7c3-1912-41b2-b81f-435d1b37c7b4/1c2bff0e-3c93-4417-bc21-90a842c779af.jpg)

一个空的云对象内容如下
雪洛's avatar
雪洛 已提交
160 161

```js
雪洛's avatar
雪洛 已提交
162
// cloudfunctions/todo/index.obj.js
雪洛's avatar
雪洛 已提交
163
module.exports = {
雪洛's avatar
雪洛 已提交
164
	
雪洛's avatar
雪洛 已提交
165 166 167
}
```

雪洛's avatar
雪洛 已提交
168
默认云对象模板是不包含任何方法的,我们为此对象添加一个add方法作为示例。
雪洛's avatar
雪洛 已提交
169

雪洛's avatar
雪洛 已提交
170 171 172 173 174 175 176 177 178 179 180 181
```js
// cloudfunctions/todo/index.obj.js
module.exports = {
	add: function(title = '', content = '') {
		title = title.trim()
		content = content.trim()
		if(!title || !content) {
			return {
				errCode: 'INVALID_TODO',
				errMsg: 'TODO标题或内容不可为空'
			}
		}
182
		// ...其他逻辑,如操作todo数据表添加数据
雪洛's avatar
雪洛 已提交
183 184 185 186 187 188 189
		return {
			errCode: 0,
			errMsg: '创建成功'
		}
	}
}
```
雪洛's avatar
雪洛 已提交
190

雪洛's avatar
雪洛 已提交
191 192 193 194 195
至此云对象todo已经有了一个可以访问的方法了。接下来看如何使用客户端调用此云对象内的方法

### 客户端调用

客户端通过`uniCloud.importObject`方法获取云对象的实例,并可以通过此实例调用云对象内的方法。用法如下
雪洛's avatar
雪洛 已提交
196 197

```js
雪洛's avatar
雪洛 已提交
198 199
const todo = uniCloud.importObject('todo')
const res = await todo.add('title demo', 'content demo')
雪洛's avatar
雪洛 已提交
200 201
```

雪洛's avatar
雪洛 已提交
202 203 204 205 206 207
通过代码块`cco`可以快捷的输入以下代码:

```js
const todo = uniCloud.importObject('todo')
```

雪洛's avatar
雪洛 已提交
208
## 云对象的API@api
雪洛's avatar
雪洛 已提交
209

雪洛's avatar
雪洛 已提交
210 211 212 213 214 215 216 217 218
云对象的方法内可以通过this上的一些接口获取一些信息

### 获取客户端信息@get-client-info

**接口形式**

`this.getClientInfo()`

**示例:**
雪洛's avatar
雪洛 已提交
219 220

```js
雪洛's avatar
雪洛 已提交
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235
module.exports = {
	add: function() {
		const clientInfo = this.getClientInfo()
		// clientInfo = {
		// 	os,
		// 	appId,
		// 	locale,
		// 	clientIP,
		// 	userAgent,
		// 	platform,
		// 	deviceId,
		// 	uniIdToken
		// }
	}
}
雪洛's avatar
雪洛 已提交
236 237
```

雪洛's avatar
雪洛 已提交
238
**返回值**
雪洛's avatar
雪洛 已提交
239

雪洛's avatar
雪洛 已提交
240 241 242 243 244 245 246 247 248 249
|参数名		|类型	|必备	|说明											|
|--			|--		|--		|--												|
|os			|string	|是		|客户端系统										|
|appId		|string	|是		|客户端DCloud AppId								|
|locale		|string	|是		|客户端语言										|
|clientIP	|string	|是		|客户端ip										|
|userAgent	|string	|是		|客户端ua										|
|platform	|string	|是		|客户端平台,app,mp-weixin等					|
|deviceId	|string	|是		|客户端deviceId,目前同getSystemInfo内的deviceId|
|uniIdToken	|string	|是		|客户端用户token								|
雪洛's avatar
雪洛 已提交
250

雪洛's avatar
雪洛 已提交
251
**注意**
雪洛's avatar
雪洛 已提交
252

雪洛's avatar
雪洛 已提交
253
- 与云函数内获取客户端platform稍有不同,云函数未拉齐vue2、vue3版本app平台的platform值,vue2为`app-plus`,vue3为`app`。云对象无论客户端是vue2还是vue3,在app平台获取的platform均为`app`。这一点在使用uni-id时需要特别注意,详情见:[uni-id文档 preferedAppPlatform](uniCloud/uni-id.md?id=prefered-app-platform)
雪洛's avatar
雪洛 已提交
254 255 256 257 258 259 260 261

### 获取云端信息@get-cloud-info

**接口形式**

`this.getCloudInfo()`

**示例**
雪洛's avatar
雪洛 已提交
262 263

```js
雪洛's avatar
雪洛 已提交
264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
module.exports = {
	add: function(){
		const cloudInfo = this.getCloudInfo()
		// cloudInfo = {
		//     provider,
		//     spaceId
		// }
	}
}
```

**返回值**

|参数名		|类型	|必备	|说明			|
|--			|--		|--		|--				|
|provider	|string	|是		|服务空间供应商	|
|spaceId	|string	|是		|服务空间Id		|

### 获取客户端token@get-uni-id-token

**接口形式**

`this.getUniIdToken()`

**示例**

```js
module.exports = {
	add: function(){
		const token = this.getUniIdToken()
	}
}
雪洛's avatar
雪洛 已提交
296 297
```

雪洛's avatar
雪洛 已提交
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
### 获取当前调用的方法名@get-method-name

**接口形式**

`this.getMethodName()`

**示例**

```js
module.exports = {
	_before: function() { // _before的用法请看后续章节
		const methodName = this.getMethodName() // add
	}
}
```

### 获取当前参数列表@get-params

**接口形式**

`this.getParams()`

**示例**

```js
module.exports = {
	_before: function() { // _before的用法请看后续章节
		const params = this.getParams() // ['title demo', 'content demo']
	}
}
```

## 预处理与后处理@before-and-after

### 预处理 _before@before

334
云对象内可以创建一个特殊的方法_before,用来在调用常规方法之前进行预处理,一般用于拦截器、身份验证、参数校验等。
雪洛's avatar
雪洛 已提交
335

336
以下示例的逻辑是,当客户端调用todo云对象的add方法时,会先执行_before方法中的逻辑,判断为add方法时校验了客户端token,校验失败则直接报错返回客户端,校验通过继续执行add方法。
雪洛's avatar
雪洛 已提交
337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357

```js
// todo/index.obj.js
module.exports = {
	_before: function(){
		const methodName = this.getMethodName()
		if(methodName === 'add' && !this.getUniIdToken()) {
			throw new Error('token不存在')
		}
	},
	add: function(title = '', content = '') {
		return {
			errCode: 0,
			errMsg: '创建成功'
		}
	}
}
```

### 后处理 _after@after

358
与预处理`_before`对应的是后处理`_after`。云对象内可以创建一个特殊的方法_after用来再加工处理本次调用方法的返回结果或者抛出的错误
雪洛's avatar
雪洛 已提交
359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387

请看以下示例:

```js
// todo/index.obj.js
module.exports = {
	_before: function(){
		this.startTime = Date.now() // 在before内记录开始时间并在this上挂载,以供后续流程使用
	},
	add: function(title = '', content = '') {
		if(title === 'abc') {
			throw new Error('abc不是一个合法的todo标题')
		}
		return {
			errCode: 0,
			errMsg: '创建成功'
		}
	},
	_after(error, result) {
		if(error) {
			throw error // 如果方法抛出错误,也直接抛出不处理
		}
		result.timeCost = Date.now() - this.startTime
		return result
	}
}
```

## 云对象的返回值@return-value
雪洛's avatar
雪洛 已提交
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406

客户端拿到云对象的响应结果后,会自动进行结果的处理。

- 如果是正常的结果(errCode为假值[0, false, null, undefined, ...]或者结果内不含errCode)则将结果直接返回
- 如果是报错的结果(errCode为真值)将结果内的errCode和errMsg组合为错误对象抛出
- 如果是其他云函数未捕获的错误,直接将错误码和错误信息组合成错误对象抛出

前端抛出的错误对象上有以下属性

|属性名		|类型				|是否必备	|说明													|
|--			|--					|--			|--														|
|errCode	|string|number	|否			|错误码													|
|errMsg		|string				|否			|错误信息												|
|requestId	|string				|否			|当前请求的requestId									|
|detail		|Object				|否			|完成的错误响应(仅在响应符合uniCloud响应体规范时才有)	|

详见以下示例:

```js
雪洛's avatar
雪洛 已提交
407
// todo/index.obj.js
雪洛's avatar
雪洛 已提交
408
module.exports = {
雪洛's avatar
雪洛 已提交
409 410 411 412
	add: async function(title = '', content = '') { 
		title = title.trim()
		content = content.trim()
		if(!title || !content) {
雪洛's avatar
雪洛 已提交
413
			return {
雪洛's avatar
雪洛 已提交
414 415
				errCode: 'INVALID_TODO',
				errMsg: 'TODO标题或内容不可为空'
雪洛's avatar
雪洛 已提交
416 417
			}
		}
雪洛's avatar
雪洛 已提交
418
		// ...其他逻辑
雪洛's avatar
雪洛 已提交
419 420
		return {
			errCode: 0,
雪洛's avatar
雪洛 已提交
421
			errMsg: '创建成功'
雪洛's avatar
雪洛 已提交
422 423 424 425 426
		}
	}
}

// 客户端代码
雪洛's avatar
雪洛 已提交
427
const todo = uniCloud.importObject('todo')
雪洛's avatar
雪洛 已提交
428
try {
雪洛's avatar
雪洛 已提交
429 430
	// 不传title、content,云函数返回错误的响应
	await todo.add()
雪洛's avatar
雪洛 已提交
431
} catch (e) {
雪洛's avatar
雪洛 已提交
432 433 434
	// e.errCode === 'INVALID_TODO'
	// e.errMsg === 'TODO标题或内容不可为空'
	// e.detail === {errCode: 'INVALID_TODO',errMsg: 'TODO标题或内容不可为空'}
雪洛's avatar
雪洛 已提交
435 436 437 438
	// e.requestId === 'xxxx'
}

try {
雪洛's avatar
雪洛 已提交
439 440
	const res = await todo.add('title demo', 'content demo')
	// res = {errCode: 0,errMsg: '创建成功'}
雪洛's avatar
雪洛 已提交
441 442 443
} catch (e) {}
```

雪洛's avatar
雪洛 已提交
444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488

## 调用云对象

### 客户端调用@call-by-client

客户端通过`uniCloud.importObject`方法获取云对象的实例。用法如下

```js
const todo = uniCloud.importObject('todo')
const res = await todo.add('title demo', 'content demo')
```

### 云函数或云对象内调用@call-by-cloud

云函数或云对象内也可以调用同一服务空间内的云对象,用法和客户端调用云对象一致

```js
const todo = uniCloud.importObject('todo')
const res = await todo.add('title demo', 'content demo')
```

### 跨服务空间调用云对象@call-by-cloud-cross-space

云端或者客户端均有uniCloud.init方法可以获取其他服务空间的uniCloud实例,使用此实例的importObject可以调用其他服务空间的云对象,参考:[](uniCloud/concepts/space.md?id=multi-space)

客户端无论腾讯阿里均支持。云端`uniCloud.init`方法仅腾讯云支持,且仅能获取同账号下的腾讯云服务空间的uniCloud实例。

**示例代码**

```js
const mycloud = uniCloud.init({
	provider: 'tencent',
	spaceId: 'xxx'
})
const todo = mycloud.importObject('todo')
const res = await todo.add('title demo', 'content demo')
```


## 注意事项

- 云对象和云函数都在cloudfunctions目录下,但是不同于云函数,云对象的入口为`index.obj.js`,而云函数则是`index.js`**为正确区分两者uniCloud做出了限制,云函数内不可存在index.obj.js,云对象内也不可存在index.js。**
- 所有`_`开头的方法都是私有方法,客户端不可访问
- 云对象也可以引用公共模块或者npm上的包,引用方式和云函数完全一致。

雪洛's avatar
雪洛 已提交
489 490
## 本地运行@run-local

491 492 493 494 495 496 497 498 499 500 501
云对象无法直接本地运行,可以通过其他云函数调用本地云对象(在调用云对象的云函数右键本地运行),或者客户端调用本地云对象的方式来实现云对象的本地运行。

## 推荐最佳实践

uniCloud的服务器和客户端交互,有云函数、云对象、clientDB三种方式。

从云对象发布后,不再推荐使用传统云函数了。

如果是以数据库操作为主,则推荐使用clientDB,开发效率是最高的。

如果服务器端除了操作数据库外,还有复杂的、不宜公开在前端的逻辑,此时推荐使用云对象。