cloud-obj.md 16.5 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

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

211 212 213 214 215
云对象作为云函数的一种,可以调用所有node的API和uniCloud的API。

uniCloud有众多API,另见:[uniCloud的API清单](uniCloud/cf-functions.html#unicloud-api%E5%88%97%E8%A1%A8)

与云函数入参时的`context`不同,云对象没有`context`。除上述API之外,云对象的this对象还有一批专用方法来获取当前请求的上下文信息。
雪洛's avatar
雪洛 已提交
216 217 218 219 220 221 222 223

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

**接口形式**

`this.getClientInfo()`

**示例:**
雪洛's avatar
雪洛 已提交
224 225

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

雪洛's avatar
雪洛 已提交
243
**返回值**
雪洛's avatar
雪洛 已提交
244

雪洛's avatar
雪洛 已提交
245 246 247 248 249 250 251 252 253 254
|参数名		|类型	|必备	|说明											|
|--			|--		|--		|--												|
|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
雪洛 已提交
255

雪洛's avatar
雪洛 已提交
256
**注意**
雪洛's avatar
雪洛 已提交
257

雪洛's avatar
雪洛 已提交
258
- 与云函数内获取客户端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
雪洛 已提交
259 260 261 262 263 264 265 266

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

**接口形式**

`this.getCloudInfo()`

**示例**
雪洛's avatar
雪洛 已提交
267 268

```js
雪洛's avatar
雪洛 已提交
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 296 297 298 299 300
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
雪洛 已提交
301 302
```

雪洛's avatar
雪洛 已提交
303 304
### 获取当前调用的方法名@get-method-name

305 306
本方法主要用于在`_before`等拦截器方法里,判断客户端上传的信息进行处理,比如发现客户端调用的是a方法时,执行一段特殊逻辑。详见下文的[预处理](uniCloud/cloud-obj?id=before-and-after)

雪洛's avatar
雪洛 已提交
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
**接口形式**

`this.getMethodName()`

**示例**

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

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

323 324
在云对象的普通方法里,参数可以直接获取。本方法主要用于在_`_before`等拦截器方法里,判断客户端上传的信息进行处理。详见下文的[预处理](uniCloud/cloud-obj?id=before-and-after)

雪洛's avatar
雪洛 已提交
325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
**接口形式**

`this.getParams()`

**示例**

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

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

### 预处理 _before@before

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

345
以下示例的逻辑是,当客户端调用todo云对象的add方法时,会先执行_before方法中的逻辑,判断为add方法时校验了客户端token,校验失败则直接报错返回客户端,校验通过继续执行add方法。
雪洛's avatar
雪洛 已提交
346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366

```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

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

请看以下示例:

```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
雪洛 已提交
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415

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

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

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

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

详见以下示例:

```js
雪洛's avatar
雪洛 已提交
416
// todo/index.obj.js
雪洛's avatar
雪洛 已提交
417
module.exports = {
雪洛's avatar
雪洛 已提交
418 419 420 421
	add: async function(title = '', content = '') { 
		title = title.trim()
		content = content.trim()
		if(!title || !content) {
雪洛's avatar
雪洛 已提交
422
			return {
雪洛's avatar
雪洛 已提交
423 424
				errCode: 'INVALID_TODO',
				errMsg: 'TODO标题或内容不可为空'
雪洛's avatar
雪洛 已提交
425 426
			}
		}
雪洛's avatar
雪洛 已提交
427
		// ...其他逻辑
雪洛's avatar
雪洛 已提交
428 429
		return {
			errCode: 0,
雪洛's avatar
雪洛 已提交
430
			errMsg: '创建成功'
雪洛's avatar
雪洛 已提交
431 432 433 434 435
		}
	}
}

// 客户端代码
雪洛's avatar
雪洛 已提交
436
const todo = uniCloud.importObject('todo')
雪洛's avatar
雪洛 已提交
437
try {
雪洛's avatar
雪洛 已提交
438 439
	// 不传title、content,云函数返回错误的响应
	await todo.add()
雪洛's avatar
雪洛 已提交
440
} catch (e) {
雪洛's avatar
雪洛 已提交
441 442 443
	// e.errCode === 'INVALID_TODO'
	// e.errMsg === 'TODO标题或内容不可为空'
	// e.detail === {errCode: 'INVALID_TODO',errMsg: 'TODO标题或内容不可为空'}
雪洛's avatar
雪洛 已提交
444 445 446 447
	// e.requestId === 'xxxx'
}

try {
雪洛's avatar
雪洛 已提交
448 449
	const res = await todo.add('title demo', 'content demo')
	// res = {errCode: 0,errMsg: '创建成功'}
雪洛's avatar
雪洛 已提交
450 451 452
} catch (e) {}
```

雪洛's avatar
雪洛 已提交
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 489 490

## 调用云对象

### 客户端调用@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')
```

491
## 自动显示交互界面@auto-ui
雪洛's avatar
雪洛 已提交
492

493 494
> 新增于 HBuilderX 3.4.6,本次调整属于非兼容更新。

495
背景:每次写客户端联网的代码时,开发者都免不了重复写一堆代码:先调用loading等待框,联网结束后再关闭loading,如果服务器异常则弹出提示。
496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530

从HBuilderX 3.4.6起,调用云对象的方法时,默认会自动显示交互/提示界面。

1. 在请求联网开始时显示loading等待框,
2. 结束后隐藏loading
3. 如果请求报错,显示弹窗(也可配置为显示Toast)

如果默认显示的UI不符合你的需求,你可以通过配置自定义一些交互内容,也可以直接关闭自动显示的交互界面。

- 如需关闭自动显示的UI,请在客户端导入云对象时传入参数`customUI: true`

例:

```js
uniCloud.importObject('todo', {
	customUI: true // 取消自动展示的交互提示界面
})
```

- 如需自定义默认显示的UI,配置如下:

```js
uniCloud.importObject('todo', {
	customUI: false, // 是否取消自动展示的交互界面。默认为false,配置为true时取消自动展示的交互提示界面,以下配置均不再生效
	loadingOptions: { // loading相关配置
		text: '加载中...', // 显示的loading内的提示文字。默认值为:加载中...
		mask: true // 是否使用透明遮罩,配置为true时不可点击页面其他内容。默认值为:true
	},
	errorOptions: { // 错误界面相关配置
		type: 'modal', // 错误信息展示方式,可取值:modal(弹框,默认)、toast(toast消息框)。默认值为:modal
		retry: false // 是否展示重试按钮,仅在type为modal时生效。用户点击重试按钮时将重新请求调用的方法,默认为false
	}
})
```

531 532 533
**注意**
1. 配置仅对当前`importObject`返回的云对象实例生效
2. 本功能仅对客户端调用云对象生效。在云函数或云对象中调用云对象不生效
雪洛's avatar
雪洛 已提交
534 535


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

538 539 540
`HBuilderX 3.4.8`之前,云对象无法直接本地运行。可以通过其他云函数调用本地云对象(在调用云对象的云函数右键本地运行),或者客户端调用本地云对象的方式来实现云对象的本地运行。

`HBuilderX 3.4.8`起,云对象可以本地运行。打开云对象下的js文件,按Ctrl+r或点击运行菜单运行云对象。
雪洛's avatar
雪洛 已提交
541

542
运行云对象之前需要先选择执行云对象的哪个方法,以及传递什么参数。
雪洛's avatar
雪洛 已提交
543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560

以下述云对象为例:

```js
module.exports = {
	login(username, password) {
		// ...
		return {
			errCode: 0
		}
	}
}

```

调用login方法,传递username及password参数,的运行参数配置如下:

![](https://vkceyugu.cdn.bspapp.com/VKCEYUGU-f184e7c3-1912-41b2-b81f-435d1b37c7b4/db974aec-7975-45b7-bb64-24afd8a59213.jpg)
561

562 563 564 565 566 567
## 注意事项

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

568 569 570 571 572 573 574 575 576
## 推荐最佳实践

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

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

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

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