├── .gitignore
├── Files.md
├── Geo Point.md
├── Live Query.md
├── Object.md
├── ParseServer脚手架.zip
├── Promise.md
├── Query.md
├── README.md
├── Roles.md
├── SUMMARY.md
├── Session.md
├── Users.md
├── analytics.md
├── assets
├── 1.png
├── GIF.gif
├── clp_vs_acl_diagram.png
├── dash-shot.png
├── dashboard.png
└── email.png
├── book.json
├── cloudcode.md
├── config.md
├── dashboard.md
├── data.md
├── error-code.md
├── error-handling.md
├── get-started.md
├── parseserver.md
├── performance.md
├── relation.md
├── security.md
├── tui-song-tong-77e528-pushnotification.md
├── yan-zheng-ma-deng-lu.md
└── 教程例子TodoList.zip
/.gitignore:
--------------------------------------------------------------------------------
1 | # Node rules:
2 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
3 | .grunt
4 |
5 | ## Dependency directory
6 | ## Commenting this out is preferred by some people, see
7 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git
8 | node_modules
9 |
10 | # Book build output
11 | _book
12 |
13 | # eBook build output
14 | *.epub
15 | *.mobi
16 | *.pdf
--------------------------------------------------------------------------------
/Files.md:
--------------------------------------------------------------------------------
1 | # 文件
2 |
3 | ## 创建一个Parse文件
4 |
5 | `Parse.File`使你可以在云端存储文件,以免将大型数据存储在`Parse.Object`中,造成数据冗余。最常用的使用场景就是存储图片了,但是你也可以用它来存储文档、视频、音乐和任何二进制文件(最大支持10M)。
6 |
7 | `Parse.File`的使用超级简单,你有两种方法可以创建一个文件。首先是使用base64编码字符串创建:
8 |
9 | ```js
10 | var base64 = "V29ya2luZyBhdCBQYXJzZSBpcyBncmVhdCE=";
11 | var file = new Parse.File("myfile.txt", { base64: base64 });
12 | ```
13 |
14 | 或者,你可以从字节内容的数组中创建一个文件:
15 |
16 | ```js
17 | var bytes = [ 0xBE, 0xEF, 0xCA, 0xFE ];
18 | var file = new Parse.File("myfile.txt", bytes);
19 | ```
20 |
21 | Parse将会根据文件名自动检查文件类型,你也可以使用第三个参数`Content-Type`来指定文件类型:
22 |
23 | ```js
24 | var file = new Parse.File("myfile.zzz", fileData, "image/png");
25 | ```
26 |
27 | 在H5应用中最常见的是,用html表单上传文件,这在现代化的浏览器中实现超级简单。创建一个input标签,使用户可以从本地设备选择文件上传:
28 |
29 | ```js
30 |
31 | ```
32 |
33 | 然后,在某个点击事件中,获得引用的文件:
34 |
35 | ```js
36 | var fileUploadControl = $("#profilePhotoFileUpload")[0];
37 | if (fileUploadControl.files.length > 0) {
38 | var file = fileUploadControl.files[0];
39 | var name = "photo.jpg";
40 |
41 | var parseFile = new Parse.File(name, file);
42 | }
43 | ```
44 |
45 | 注意在上面例子中,我们的文件名是photo.jpg,两处注意:
46 |
47 | * 你不需要担心文件名冲突,因为上传的每个文件都会有一个唯一的id,所以上传n个photo.jpg的文件都不在话下。
48 | * 给你上传的文件指定文件拓展名是很重要的,Parse会根据扩展名来对这个文件做相应的计算、处理。所以,如果你上传的是png图片,请确保你的文件名以.png结尾。
49 |
50 | 下一步就是存储文件到云端了。和Parse.Object一样,有多种保存文件的方法,你可以选择适合你的方法处理回调和错误:
51 |
52 | ```js
53 | parseFile.save().then(function() {
54 | // The file has been saved to Parse.
55 | }, function(error) {
56 | // The file either could not be read, or could not be saved to Parse.
57 | });
58 | ```
59 |
60 | 最后,你可以用Parse.Object关联Parse.File,就像关联其他对象一样:
61 |
62 | ```js
63 | var jobApplication = new Parse.Object("JobApplication");
64 | jobApplication.set("applicantName", "Joe Smith");
65 | jobApplication.set("applicantResumeFile", parseFile);
66 | jobApplication.save();
67 | ```
68 |
69 | ---
70 |
71 | ## 获得文件
72 |
73 | 获得文件的最佳实践还要视你的应用而定。因为有跨域请求,所以最好先确保跨域能正常工作。典型的应用场景就是在页面上显示上传的图片,下面我们用jQuery在页面上显示上传的图片文件:
74 |
75 | ```js
76 | var profilePhoto = profile.get("photoFile");
77 | $("profileImg")[0].src = profilePhoto.url();
78 | ```
79 |
80 | 如果你想在云代码中处理文件数据,你可以用我们的http请求库来获取文件:
81 |
82 | ```js
83 | Parse.Cloud.httpRequest({ url: profilePhoto.url() }).then(function(response) {
84 | // 文件数据在 response.buffer 中。
85 | });
86 | ```
87 |
88 | 你可以使用[REST API](http://docs.parseplatform.org/rest/guide/#deleting-files)删除对象引用的文件,但是你需要提供master key才有权限删除文件。
89 |
90 | 如果你的文件没有被任何对象引用,那么不能通过REST API删除文件,你应该到应用设置页面清理未使用的文件。注意,删除后就不能再被访问到了。
91 |
92 |
--------------------------------------------------------------------------------
/Geo Point.md:
--------------------------------------------------------------------------------
1 | # 坐标
2 |
3 | Parse允许你在对象中关联真实世界的经纬坐标,使用`Parse.GeoPoint`,可以查询这个对象和参考位置的距离。这让你可以超级简单的实现基于坐标的功能,比如附近的人、最近的KTV。
4 |
5 | ---
6 |
7 | ## Parse.GeoPoint
8 |
9 | 要关联一个坐标到对象,你首先需要创建一个`Parse.GeoPoint`。假设你要创建一个纬度40.00、经度-30.00的坐标:
10 |
11 | ```js
12 | var point = new Parse.GeoPoint({latitude: 40.0, longitude: -30.0});
13 | ```
14 |
15 | 这个位置将会作为一个普通的字段存储在对象中。
16 |
17 | ```js
18 | placeObject.set("location", point);
19 | ```
20 |
21 | ---
22 |
23 | ## 坐标查询
24 |
25 | 现在假设,你有一大堆具有位置坐标的对象,要找出最靠近某个点的对象,可以通过给`Parse.Query`增加一个`near`条件,就能得到结果:
26 |
27 | ```js
28 | // 用户位置
29 | var userGeoPoint = userObject.get("location");
30 | // 创建一个位置查询
31 | var query = new Parse.Query(PlaceObject);
32 | // 附近的用户,location为字段名
33 | query.near("location", userGeoPoint);
34 | // 限制返回数量
35 | query.limit(10);
36 | // 查询结果
37 | query.find({
38 | success: function(placesObjects) {
39 | }
40 | });
41 | ```
42 |
43 | 注意,查询结果`placesObjects`是根据距离从近到远排序的,除非你使用`ascending()`或者`descending()`设置了其他排序规则。
44 |
45 | 如果要根据距离过滤查询结果,可以使用`withinMiles`(英米内),`withinKilometers`(千米内)和`withinReadians`(弧度内)。
46 |
47 | 你可以查询特定区域内的对象。要查询距离范围内的对象,给`Parse.Query`增加`withinGeoBox`条件即可:
48 |
49 | ```js
50 | var southwestOfSF = new Parse.GeoPoint(37.708813, -122.526398);
51 | var northeastOfSF = new Parse.GeoPoint(37.822802, -122.373962);
52 |
53 | var query = new Parse.Query(PizzaPlaceObject);
54 | query.withinGeoBox("location", southwestOfSF, northeastOfSF);
55 | query.find({
56 | success: function(pizzaPlacesInSF) {
57 | ...
58 | }
59 | });
60 | ```
61 |
62 | ####
63 |
64 | ## 注意
65 |
66 | 有几个需要注意的地方:
67 |
68 | 1. 每一个`Parse.Object`类应当只有一个字段是`Parse.GeoPoint`对象;
69 | 2. `near`条件限制同时也会限制搜索结果在100英里内;
70 | 3. 坐标不能大于或等于最终范围,纬度应该在-90.00到90.00间,经度应在-180.00到180.00间。超过将造成error。
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/Live Query.md:
--------------------------------------------------------------------------------
1 | # 实时请求
2 |
3 | > 译者注:介绍下实时请求的使用场景,比如,某商品的扫码付款页面,在用户付款后,服务端确认付款成功后通知客户端成功付款的事件,客户端收到事件,知道用户付款成功,然后做对应的交互,完成付款。
4 |
5 | ## 标准API {#standard-api}
6 |
7 | 就像我们在[LiveQuery protocol](https://github.com/parse-community/parse-server/wiki/Parse-LiveQuery-Protocol-Specification)里面说到的,我们保留了一个WebSocket连接来与Parse实时请求服务通信。当实时请求在服务端使用时,使用的是[`ws`](https://www.npmjs.com/package/ws)包,当在浏览器端使用时,使用的是window.WebSocket。我们考虑到在大多数使用场景下,WebSocket不是必需的,所以我们提供了简单的API让你可以专注于你的业务逻辑。
8 |
9 | 提示:实时请求只在[JS SDK ~1.8](https://www.gitbook.com/book/jaweii/parse/edit#)版本以上支持。
10 |
11 | ## 创建订阅 {#create-a-subscription}
12 |
13 | ```js
14 | let query = new Parse.Query('Game');
15 | let subscription = query.subscribe();
16 | ```
17 |
18 | 你创建的`subscription`实际上是一个事件触发器,关于事件触发器的更多信息,[查看这里](https://www.gitbook.com/book/jaweii/parse/edit#)。你将会通过`subscription`拿到实时请求事件,我们会为你打开WebSocket连接到实时请求服务。
19 |
20 | ## 事件处理 {#event-handling}
21 |
22 | 我们定义了一些事件类型,你可以通过`subscription`对象拿到:
23 |
24 | ### OPEN 事件
25 |
26 | ```js
27 | subscription.on('open', () => {
28 | console.log('subscription opened');
29 | });
30 | ```
31 |
32 | 当你调用`query.subscribe()`,我们发送了一个订阅请求到实时请求服务器,当我们从实时请求服务器的得到确认消息,就会触发open事件。
33 |
34 | 当客户端关闭WebSocket连接后,我们会自动重连到实时请求服务器,如果重连成功,也会触发open事件。
35 |
36 | ### CREAT事件
37 |
38 | ```js
39 | subscription.on('create', (object) => {
40 | console.log('object created');
41 | });
42 | ```
43 |
44 | 当一个新的Parse对象被创建,并且它满足你订阅的`ParseQuery`时,将会触发create事件。`object`是指创建的对象。
45 |
46 | ### UPDATE事件
47 |
48 | ```js
49 | subscription.on('update', (object) => {
50 | console.log('object updated');
51 | });
52 | ```
53 |
54 | 当一个已经存在的,满足你订阅的`ParseQuery`的`ParseObject`被更新,将会触发update事件。object是指更新后的ParseObject对象。
55 |
56 | ### ENTER事件
57 |
58 | ```js
59 | subscription.on('enter', (object) => {
60 | console.log('object entered');
61 | });
62 | ```
63 |
64 | 当一个已存在的,不满足`ParseQuery`的`ParseObject`变得满足后,就会触发enter事件。object是指变更后的最新对象。
65 |
66 | ### LEAVE事件
67 |
68 | ```js
69 | subscription.on('leave', (object) => {
70 | console.log('object left');
71 | });
72 | ```
73 |
74 | 和enter事件相反,当一个已存在的`ParseObject`对象从满足变成不满足`ParseQuery`后,就会触发leave事件。
75 |
76 | ### DELETE 事件
77 |
78 | ```js
79 | subscription.on('delete', (object) => {
80 | console.log('object deleted');
81 | });
82 | ```
83 |
84 | 当一个已存在,满足`ParseQuery`的`ParseObject`对象被删除后,就会触发delete事件。object对象是指被删除的对象。
85 |
86 | ### CLOSE 事件
87 |
88 | ```js
89 | subscription.on('close', () => {
90 | console.log('subscription closed');
91 | });
92 | ```
93 |
94 | 当客户端断开和LiveQuery服务的WebSocket连接后,就会触发close事件。
95 |
96 | ## 取消订阅 {#unsubscribe}
97 |
98 | ```js
99 | subscription.unsubscribe();
100 | ```
101 |
102 | 如果你想停止从`ParseQuery`接收事件,你可以使用`unsubscribe`方法取消订阅。然后,你将不能从`subscription`收到任何事件。
103 |
104 | # 关闭
105 |
106 | ```js
107 | Parse.LiveQuery.close();
108 | ```
109 |
110 | 当你使用完实时请求,你可以调用close方法,WebSocket将会断开和实时请求服务器的链接,取消重连,取消订阅所有的订阅。在此之后,如果你再次调用`query.subscribe()`,将会创建一个新的连接。
111 |
112 | ## 设置服务地址 {#setup-server-url}
113 |
114 | ```js
115 | Parse.liveQueryServerURL = 'ws://XXXX'
116 | ```
117 |
118 | 大多数的情况你不需要设置这个,如果你设置了服务地址,我们将会提取出主机地址,并使用ws://hostname作为默认的实时请求服务地址。总之,如果你想定义你自己的实时请求地址,或使用不同的协议,比如wss,你可以通过这个来设置。
119 |
120 | ## WebSocket 状态 {#websocket-status}
121 |
122 | 我们暴露出了3个方法来为你监控WebSocket链接的状态。
123 |
124 | ### OPEN 事件 {#open-event-1}
125 |
126 | ```js
127 | Parse.LiveQuery.on('open', () => {
128 | console.log('socket connection established');
129 | });
130 | ```
131 |
132 | 当WebSocket完成了连接到实时请求服务器,将会触发open事件。
133 |
134 | ### CLOSE 事件 {#close-event-1}
135 |
136 | ```js
137 | Parse.LiveQuery.on('close', () => {
138 | console.log('socket connection closed');
139 | });
140 | ```
141 |
142 | 当WebSocket关闭了和实时请求服务器的链接,将会触发close事件。
143 |
144 | ### ERROR 事件 {#error-event}
145 |
146 | ```js
147 | Parse.LiveQuery.on('error', (error) => {
148 | console.log(error);
149 | });
150 | ```
151 |
152 | 当出现某个网络错误,或者实时请求服务器发生错误,将会触发error事件。
153 |
154 | ## 重连 {#reconnect}
155 |
156 | 由于实时请求功能依赖WebSocket连接,为了保持WebScoket连接是一直打开的,每当我们发现连接丢失后,就会自动尝试重连,我们会在后台按间隔指数级增加的时间重连。不过,如果WebScoket连接时通过`Parse.LiveQuery.close()`或`client.close()`关闭的,将不会重连。
157 |
158 | ## SessionToken {#sessiontoken}
159 |
160 | 当你订阅一个ParseQuery,我们会发送一个当前用户的sessionToken到实时请求服务器。有一点要注意,当你退出当前用户后,sessionToken将失效,你应该取消订阅后重新订阅;否则,你可能会面临安全问题,因为你会收到不应发送给你的事件。
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
--------------------------------------------------------------------------------
/Object.md:
--------------------------------------------------------------------------------
1 | # 对象
2 |
3 | ## Parse.Object
4 |
5 | Parse的对象存储是建立在`Parse.Object`上,每一个`Parse.Object`包含了JSON格式的键值对,它的数据是无模式的,这意味着你无需提前为每一个`Parse.Object` 指定存在的键,你只需要设置任何你想要的键值对就行了,后端会自动创建并存储它。
6 |
7 | 举例说明,假设你正在记录游戏玩家的分数,一个`Parse.Object` 可以包含:
8 |
9 | ```
10 | score: 1337, playerName: "Sean Plott", cheatMode: false
11 | ```
12 |
13 | 键名必须是字母和数字组成的string类型的,键值可以是string、boolean、array和任何可以被JSON编码的类型。
14 |
15 | 每一个`Parse.Object` 都是一个使用表名构造的表记录对象。你可以通过不同的实例来区分不同的数据。为了让你的代码更加诗意,我们推荐你使用驼峰命名,就像gameScore或者GameScore。
16 |
17 | ---
18 |
19 | 要创建一个新的子类,使用`Parse.Object.extend` 方法即可。如果你熟悉`Backbone.Model`,那么你已经知道如何去使用`Parse.Object`了,它们创建和修改的设计是一样的。
20 |
21 | ```js
22 | // 创建一个新的Parse.Object子类
23 | var GameScore = Parse.Object.extend("GameScore");
24 |
25 | // 创建一个新的子类实例
26 | var gameScore = new GameScore();
27 |
28 | // 你也可以使用backbone的语法直接创建.
29 | var Achievement = Parse.Object.extend({
30 | className: "Achievement"
31 | });
32 | ```
33 |
34 | 你可以添加额外的方法和属性到你的`Parse.Object`子类:
35 |
36 | ```js
37 | // 一个复杂的Parse.Object子类
38 | var Monster = Parse.Object.extend("Monster", {
39 | // 实例方法
40 | hasSuperHumanStrength: function () {
41 | return this.get("strength") > 18;
42 | },
43 | // 实例属性
44 | initialize: function (attrs, options) {
45 | this.sound = "Rawr"
46 | }
47 | }, {
48 | // 类方法
49 | spawn: function(strength) {
50 | var monster = new Monster();
51 | monster.set("strength", strength);
52 | return monster;
53 | }
54 | });
55 |
56 | var monster = Monster.spawn(200);
57 | alert(monster.get('strength')); // Displays 200.
58 | alert(monster.sound); // Displays Rawr.
59 | ```
60 |
61 | 如果想为任意一个对象创建一个实例,你也可以使用`Parse.Object`直接构造。`new Parse.Object(className)`将会创建一个指定类名的实例。
62 |
63 | 如果你的项目已经使用了ES6,恭喜!从1.6.0版本开始,JavaScript SDK 已经和ES6类兼容,你可以使用`extends`关键词继承`Parse.Object`:
64 |
65 | ```js
66 | class Monster extends Parse.Object {
67 | constructor() {
68 | // Pass the ClassName to the Parse.Object constructor
69 | super('Monster');
70 | // All other initialization
71 | this.sound = 'Rawr';
72 | }
73 |
74 | hasSuperHumanStrength() {
75 | return this.get('strength') > 18;
76 | }
77 |
78 | static spawn(strength) {
79 | var monster = new Monster();
80 | monster.set('strength', strength);
81 | return monster;
82 | }
83 | }
84 | ```
85 |
86 | 但是,当你使用extends时,SDK将不会自动识别你的子类。如果你想要查询返回的对象使用你的Parse.Object子类,你将需要注册这个子类,就像我们在其他平台做的一样:
87 |
88 | ```js
89 | // 指定Monster子类以后
90 | Parse.Object.registerSubclass('Monster', Monster);
91 | ```
92 |
93 | ---
94 |
95 | ## 保存对象
96 |
97 | 假设你想保存之前描述的对象`GameScore`到Parse服务器,这个接口和`BackBone.Model`类似,即`save`方法:
98 |
99 | ```js
100 | var GameScore = Parse.Object.extend("GameScore");
101 | var gameScore = new GameScore();
102 |
103 | gameScore.set("score", 1337);
104 | gameScore.set("playerName", "Sean Plott");
105 | gameScore.set("cheatMode", false);
106 |
107 | gameScore.save(null, {
108 | success: function(gameScore) {
109 | // 保存成功
110 | alert('New object created with objectId: ' + gameScore.id);
111 | },
112 | error: function(gameScore, error) {
113 | // 包含了错误码和错误信息
114 | alert('Failed to create new object, with error code: ' + error.message);
115 | }
116 | });
117 | ```
118 |
119 | 在上面代码运行后,你可能会想确认是否真的保存成功了,你可以在Parse的数据浏览器中查看,你将会看到类似下面的数据:
120 |
121 | ```js
122 | objectId: "xWMyZ4YEGZ", score: 1337, playerName: "Sean Plott", cheatMode: false,
123 | createdAt:"2011-06-10T18:33:42Z", updatedAt:"2011-06-10T18:33:42Z"
124 | ```
125 |
126 | ---
127 |
128 | ## 取得对象
129 |
130 | 比保存数据到云端更有趣的是,从云端再次取回数据。如果你已经有了`objectId`,你可以通过`Parse.Query`来获得完整的`Parse.Object`。
131 |
132 | ```js
133 | var GameScore = Parse.Object.extend("GameScore");
134 | var query = new Parse.Query(GameScore);
135 | query.get("xWMyZ4YEGZ", {
136 | success: function(gameScore) {
137 | // The object was retrieved successfully.
138 | },
139 | error: function(object, error) {
140 | // The object was not retrieved successfully.
141 | // error is a Parse.Error with an error code and message.
142 | }
143 | });
144 | ```
145 |
146 | 通过`get`方法,即可从`Parse.Object`中获得值:
147 |
148 | ```js
149 | var score = gameScore.get("score");
150 | var playerName = gameScore.get("playerName");
151 | var cheatMode = gameScore.get("cheatMode");
152 | ```
153 |
154 | 有三个值的键名是预留的属性,无法通过`get`方法获取,也不能通过`set`方法修改:
155 |
156 | ```js
157 | var objectId = gameScore.id;
158 | var updatedAt = gameScore.updatedAt;
159 | var createdAt = gameScore.createdAt;
160 | ```
161 |
162 | 如果你需要更新一个已有的对象,你可以调用`fetch`方法,获取云端的最新数据:
163 |
164 | ```js
165 | myObject.fetch({
166 | success: function(myObject) {
167 | // The object was refreshed successfully.
168 | },
169 | error: function(myObject, error) {
170 | // The object was not refreshed successfully.
171 | // error is a Parse.Error with an error code and message.
172 | }
173 | });
174 | ```
175 |
176 | ---
177 |
178 | ## 更新对象
179 |
180 | 更新对象超级简单,只需要设置一些新数据,然后调用`save`方法就可以了:
181 |
182 | ```js
183 | // Create the object.
184 | var GameScore = Parse.Object.extend("GameScore");
185 | var gameScore = new GameScore();
186 |
187 | gameScore.set("score", 1337);
188 | gameScore.set("playerName", "Sean Plott");
189 | gameScore.set("cheatMode", false);
190 | gameScore.set("skills", ["pwnage", "flying"]);
191 |
192 | gameScore.save(null, {
193 | success: function(gameScore) {
194 | // Now let's update it with some new data. In this case, only cheatMode and score
195 | // will get sent to the cloud. playerName hasn't changed.
196 | gameScore.set("cheatMode", true);
197 | gameScore.set("score", 1338);
198 | gameScore.save();
199 | }
200 | });
201 | ```
202 |
203 | Parse会自动判断哪些数据是修改过的,所有只有”脏”字段会被发送到云端。
204 |
205 | ### 计数器
206 |
207 | 上述的例子中包含了常用的使用情况,但score字段是一个计数器,我们需要不断的更新玩家最后的分数,上述的方法虽然也可用,但是过于麻烦,而且如果你有多个客户端同时更新,可能回导致一些问题。
208 |
209 | 为了存储计数器类型的数据,Parse提供了原子级增减的方法,`increment`和`decrement` 。所以,计数器的更新我们可以这样写:
210 |
211 | ```js
212 | gameScore.increment("score");
213 | gameScore.save();
214 | ```
215 |
216 | 你可以给`increment`和`decrement`传入第二个参数,作为要增减的数值,如果没有传入,默认是1。
217 |
218 | ### 数组
219 |
220 | 为了存储数组型数据,我们提供了三个方法,可以原子级操作给定键值的数组:
221 |
222 | * `add`增加一个元素到对象的数组字段的尾部。
223 | * `addUnique`如果指定数组字段中不包含这个元素,才插入这个元素到数组中。不保证插入的位置。
224 | * `remove`从给定对象的数组字段中移除所有元素。
225 |
226 | 举例说明,比如我们要添加一些元素到“skills”字段中,可以这样:
227 |
228 | ```js
229 | gameScore.addUnique("skills", "flying");
230 | gameScore.addUnique("skills", "kungfu");
231 | gameScore.save();
232 | ```
233 |
234 | 注意,目前不支持在同一次`save`中,原子级`add`或`remove`数组字段中的元素,你必须在每次不同的数组操作后调用`save`。
235 |
236 | ---
237 |
238 | ## 删除对象
239 |
240 | 从云端删除一个对象:
241 |
242 | ```js
243 | myObject.destroy({
244 | success: function(myObject) {
245 | // The object was deleted from the Parse Cloud.
246 | },
247 | error: function(myObject, error) {
248 | // The delete failed.
249 | // error is a Parse.Error with an error code and message.
250 | }
251 | });
252 | ```
253 |
254 | 你可以用`unset`方法从对象中删除一个字段:
255 |
256 | ```js
257 | // 删除playerName字段
258 | myObject.unset("playerName");
259 |
260 | // Saves the field deletion to the Parse Cloud.
261 | // If the object's field is an array, call save() after every unset() operation.
262 | myObject.save();
263 | ```
264 |
265 | 请注意,不推荐使用object.set\(null\)的方式从对象中删除字段,这可能会造成意外的问题。
266 |
267 | ---
268 |
269 | ## 关系型数据
270 |
271 | 一个对象可能和其他对象存在关联,比如,一个博客应用中,一篇文章\(Post\)的对象,可能关联着许多评论\(Comment\)对象。Parse支持所有的关系类型,包括一对一,一对多,多对多。
272 |
273 | ### 一对一和一对多
274 |
275 | 一对一和一对多关系,是通过将`Parse.Object`的值设为其他对象来模型化的,比如,每个`Comment`对象,都对应了一个`Post`对象。
276 |
277 | 要创建一篇新文章和一条评论,你可以这样写:
278 |
279 | ```js
280 | // Declare the types.
281 | var Post = Parse.Object.extend("Post");
282 | var Comment = Parse.Object.extend("Comment");
283 |
284 | // 创建Post
285 | var myPost = new Post();
286 | myPost.set("title", "I'm Hungry");
287 | myPost.set("content", "Where should we go for lunch?");
288 |
289 | // 创建Comment
290 | var myComment = new Comment();
291 | myComment.set("content", "Let's do Sushirrito.");
292 |
293 | // 设置关系
294 | myComment.set("parent", myPost);
295 |
296 | // 保存
297 | myComment.save();
298 | ```
299 |
300 | 在内部,Parse框架只会把引用对象存储在一处,以保证一致性。你也可以通过objectId来指向关联对象:
301 |
302 | ```js
303 | var post = new Post();
304 | post.id = "1zEcyElZ80";
305 |
306 | myComment.set("parent", post);
307 | ```
308 |
309 | 默认情况下,在拉取一个对象时,是不会将关联的对象一起拉取的,关联的对象需要再次拉取:
310 |
311 | ```js
312 | var post = fetchedComment.get("parent");
313 | post.fetch({
314 | success: function(post) {
315 | var title = post.get("title");
316 | }
317 | });
318 | ```
319 |
320 | ### 多对多关系
321 |
322 | 多对多关系是通过`Parse.Relation`模型化的。这和存储一个对象到数组字段中类似,不同之处在于,你不需要一次拉取这个关系中的所有对象。另外这允许你拓展多于数组字段的对象。
323 |
324 | 假如,一个`User`可能有许多她喜欢的的`Posts`,那么你可以把`User`喜欢的`Posts`作为`relation`,要添加一篇`Post`到她喜欢的列表中,你可以这么做:
325 |
326 | ```js
327 | var user = Parse.User.current();
328 | var relation = user.relation("likes");
329 | relation.add(post);
330 | user.save();
331 | ```
332 |
333 | 你可以这样从Parse.Relation中删除一篇文章:
334 |
335 | ```js
336 | relation.remove(post);
337 | user.save();
338 | ```
339 |
340 | 你可以多次使用`add`和`remove`再调用`save`:
341 |
342 | ```js
343 | relation.remove(post1);
344 | relation.remove(post2);
345 | user.save();
346 | ```
347 |
348 | 你也可以通过传入一个`Parse.Object`数组来`add`或者`remove`:
349 |
350 | ```js
351 | relation.add([post1, post2, post3]);
352 | user.save();
353 | ```
354 |
355 | 默认情况下,关联的对象列表是没有拉取到本地,你可以使用query返回的Parse.Query得到用户喜欢的文字列表:
356 |
357 | ```js
358 | relation.query().find({
359 | success: function(list) {
360 | // list contains the posts that the current user likes.
361 | }
362 | });
363 | ```
364 |
365 | 如果你只想要`Posts`的部分子集,你可以添加额外的条件到`query`:
366 |
367 | ```js
368 | var query = relation.query();
369 | query.equalTo("title", "I'm Hungry");
370 | query.find({
371 | success:function(list) {
372 | // list contains post liked by the current user which have the title "I'm Hungry".
373 | }
374 | });
375 | ```
376 |
377 | 更多关于`Parse.Query`的详情,请查看指南的查询\(Queries\)章节。`Parse.Relation`的查询和`Parse.Object`的查询类似,所以`Parse.Object`的任何查询,都可以用在`Parse.Relation`上。
378 |
379 | ---
380 |
381 | ## 数据类型
382 |
383 | 目前为止,我们使用的键值的类型有String、Number、和Parse.Object。Parse也支持Date和null。你还可以存储JSON Object 和JSON Array到你的Parse.Object。下面列出的,是对象的字段支持的所有类型:
384 |
385 | * String =>`String`
386 | * Number =>`Number`
387 | * Bool =>`bool`
388 | * Array =>`JSON Array`
389 | * Object =>`JSON Object`
390 | * Date =>`Date`
391 | * File =>`Parse.File`
392 | * Pointer => other`Parse.Object`
393 | * Relation =>`Parse.Relation`
394 | * Null =>`null`
395 |
396 | 一些例子:
397 |
398 | ```js
399 | var number = 42;
400 | var bool = false;
401 | var string = "the number is " + number;
402 | var date = new Date();
403 | var array = [string, number];
404 | var object = { number: number, string: string };
405 | var pointer = MyClassName.createWithoutData(objectId);
406 |
407 | var BigObject = Parse.Object.extend("BigObject");
408 | var bigObject = new BigObject();
409 | bigObject.set("myNumber", number);
410 | bigObject.set("myBool", bool);
411 | bigObject.set("myString", string);
412 | bigObject.set("myDate", date);
413 | bigObject.set("myArray", array);
414 | bigObject.set("myObject", object);
415 | bigObject.set("anyKey", null); // this value can only be saved to an existing key
416 | bigObject.set("myPointerKey", pointer); // shows up as Pointer in the Data Browser
417 | bigObject.save();
418 | ```
419 |
420 | 我们不推荐在对象中存储大件的二进制文件,比如图片和文档。对象应该在128字节以内。
421 |
422 | 我们推荐你使用Parse.File来存储文件、图片,和其他文件 。你可以初始化一个Parse.File对象设置你的文件。具体可以看文件\(Files\)章节。
423 |
424 | 关于Parse处理数据的更多信息,请查看文档的数据\(Data\)章节。
425 |
426 | ##
427 |
428 |
429 |
430 |
--------------------------------------------------------------------------------
/ParseServer脚手架.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaweii/Parse-JavaScript-translation/94a510d0f37dad0edaf1bedc9480e0bb8df47817/ParseServer脚手架.zip
--------------------------------------------------------------------------------
/Promise.md:
--------------------------------------------------------------------------------
1 | # Promises
2 |
3 | 除了回调,Parse JavaScript SDK的所有异步方法,都返回一个Promise。通过promise,你的代码会比在回调中嵌套代码清晰很多。
4 |
5 | ## 举例
6 |
7 | 假设我们想保存一个Parse.Object,这是一个异步的操作,回调式的写法是这样的:
8 |
9 | ```js
10 | object.save({ key: value }, {
11 | success: function(object) {
12 | // the object was saved.
13 | },
14 | error: function(object, error) {
15 | // saving the object failed.
16 | }
17 | });
18 | ```
19 |
20 | 而在Promise的链式写法中,是这样的:
21 |
22 | ```js
23 | object.save({ key: value }).then(function(obj){
24 | //保存成功
25 | }).catch(err=>{
26 | //保存失败
27 | })
28 |
29 |
30 | //译者补充两个写法
31 | object.set('key',value)
32 | object.save().then(obj=>{/* ... */}).catch(function(err){/*...*/})
33 |
34 | //或
35 | object.set('key',value).save(obj=>{/*...*/},err=>{/*...*/})
36 | ```
37 |
38 | ---
39 |
40 | 如果你尚不了解Promise用法,可以参考此教程[Promise对象 ](http://es6.ruanyifeng.com/#docs/promise)。
41 |
42 |
--------------------------------------------------------------------------------
/Query.md:
--------------------------------------------------------------------------------
1 | # 查询
2 |
3 | 我们已经知道了`Parse.Query`是如何使用`get`从Parse云端查询一个对象的,下面还有许多其他方法,比如你可以一次查询多个对象,可以在一个查询上限制条件等。
4 |
5 | ## 基本查询
6 |
7 | 很多时候,`get`可能满足不了你的欲望。
8 |
9 | 所以,`Parse.Query`提供了其他的方法,可以让你查询一批对象,而不是一个对象。
10 |
11 | 最常用的方法是始创建一个`Parse.Query`,为其设置查询条件,然后使用`find`方法查询匹配的对象列表。比如,要查询一个玩家的`playerName`的得分,我们使用`equalTo`方法来设定“等于”查询条件:
12 |
13 | ```js
14 | var GameScore = Parse.Object.extend("GameScore");
15 | var query = new Parse.Query(GameScore);
16 | query.equalTo("playerName", "Dan Stemkoski");
17 | query.find({
18 | success: function(results) {
19 | alert("Successfully retrieved " + results.length + " scores.");
20 | // Do something with the returned Parse.Object values
21 | for (var i = 0; i < results.length; i++) {
22 | var object = results[i];
23 | alert(object.id + ' - ' + object.get('playerName'));
24 | }
25 | },
26 | error: function(error) {
27 | alert("Error: " + error.code + " " + error.message);
28 | }
29 | });
30 | ```
31 |
32 | ---
33 |
34 | ## 查询条件
35 |
36 | 下面有几个方法可以约束`Parse.Query`的查询结果,比如你可以用`notEqualTo`方法,传入键值对来筛选结果:
37 |
38 | ```js
39 | query.notEqualTo("playerName", "Michael Yabuti");
40 | ```
41 |
42 | 你可以给定多个约束条件,只有条件匹配的对象会被作为查询结果返回。在口语中,这和“且”的条件限制相似:
43 |
44 | ```js
45 | query.notEqualTo("playerName", "Michael Yabuti");
46 | query.greaterThan("playerAge", 18);
47 | ```
48 |
49 | 你可以限定查询结果的返回数量,默认情况下,返回数量是100:
50 |
51 | ```js
52 | query.limit(10); // limit to at most 10 results
53 | ```
54 |
55 | 如果你只想查询一个结果,可以用更方便的`first`方法替代`find`方法:
56 |
57 | ```js
58 | var GameScore = Parse.Object.extend("GameScore");
59 | var query = new Parse.Query(GameScore);
60 | query.equalTo("playerEmail", "dstemkoski@example.com");
61 | query.first({
62 | success: function(object) {
63 | // Successfully retrieved the object.
64 | },
65 | error: function(error) {
66 | alert("Error: " + error.code + " " + error.message);
67 | }
68 | });
69 | ```
70 |
71 | 你可以通过设置skip来跳过x个查询结果,这在分页功能中很有:
72 |
73 | ```js
74 | query.skip(10); // skip the first 10 results
75 | ```
76 |
77 | 对于可排序的字段类型,如string、number、date,你可以指定字段类型的排序,控制返回结果的排序:
78 |
79 | ```js
80 | // Sorts the results in ascending order by the score field
81 | query.ascending("score");
82 |
83 | // Sorts the results in descending order by the score field
84 | query.descending("score");
85 | ```
86 |
87 | 对于可排序类型,你还可以在查询中使用比较:
88 |
89 | ```js
90 | //wins < 50
91 | query.lessThan("wins", 50);
92 |
93 | // wins <= 50
94 | query.lessThanOrEqualTo("wins", 50);
95 |
96 | // wins > 50
97 | query.greaterThan("wins", 50);
98 |
99 | // wins >= 50
100 | query.greaterThanOrEqualTo("wins", 50);
101 | ```
102 |
103 | 如果你想查询与数组中任意值匹配的对象,可以使用`containedIn`方法,并提供一个可接受值的数组。这样可以一个请求完成多个请求的功能。比如,你想查询数组中玩家的分数结果:
104 |
105 | ```js
106 | // Finds scores from any of Jonathan, Dario, or Shawn
107 | query.containedIn("playerName",
108 | ["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"]);
109 | ```
110 |
111 | 如果你想查询和数组中任意值都不匹配的对象,你可以使用`notContainedIn`,并给定一个排除值的数组。比如,你想查询不在数组中的玩家分数:
112 |
113 | ```js
114 | // Finds scores from anyone who is neither Jonathan, Dario, nor Shawn
115 | query.notContainedIn("playerName",
116 | ["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"]);
117 | ```
118 |
119 | 如果你想查询指定字段已被设置的对象,你可以使用`exists`;反之,如果想查询未被设置的对象,你可以使用`doesNotExist`:
120 |
121 | ```js
122 | // Finds objects that have the score set
123 | query.exists("score");
124 |
125 | // Finds objects that don't have the score set
126 | query.doesNotExist("score");
127 | ```
128 |
129 | 你可以使用`meachesKeyInQuery`方法查询对象,对象中的某个键值是和另一个请求返回的一组结果中的键值匹配的。比如,如果你有一个包含了运动队的类,并且你保存了用户的老家信息在user类中,你现在要查询获胜队伍的成员家乡信息,你可以这样请求:
130 |
131 | ```js
132 | var Team = Parse.Object.extend("Team");
133 | var teamQuery = new Parse.Query(Team);
134 | teamQuery.greaterThan("winPct", 0.5);
135 | var userQuery = new Parse.Query(Parse.User);
136 | userQuery.matchesKeyInQuery("hometown", "city", teamQuery);
137 | userQuery.find({
138 | success: function(results) {
139 | // results has the list of users with a hometown team with a winning record
140 | }
141 | });
142 | ```
143 |
144 | 反之,如果你要查询,对象中的某个键值是和另一个请求返回的一组结果中的键值是不匹配的,用`doesNotMatchKeyInQuery`。比如,查询战败队成员的家乡信息:
145 |
146 | ```js
147 | var losingUserQuery = new Parse.Query(Parse.User);
148 | losingUserQuery.doesNotMatchKeyInQuery("hometown", "city", teamQuery);
149 | losingUserQuery.find({
150 | success: function(results) {
151 | // results has the list of users with a hometown team with a losing record
152 | }
153 | });
154 | ```
155 |
156 | 你可以使用`select`限定返回的字段。如果你想查询只包含了`score`和`playerName`字段的数据记录\(还有指定的内置字段objectId、createdAt和updatedAt\):
157 |
158 | ```
159 | var GameScore = Parse.Object.extend("GameScore");
160 | var query = new Parse.Query(GameScore);
161 | query.select("score", "playerName");
162 | query.find().then(function(results) {
163 | // each of results will only have the selected fields available.
164 | });
165 | ```
166 |
167 | 剩下的字段你可以使用`fetch`方法,在需要的时候拉取:
168 |
169 | ```
170 | query.first().then(function(result) {
171 | // only the selected fields of the object will now be available here.
172 | return result.fetch();
173 | }).then(function(result) {
174 | // all fields of the object will now be available here.
175 | });
176 | ```
177 |
178 | ### 比较查询 {#hash858517260}
179 |
180 | | 逻辑操作 | Query 方法 |
181 | | :--- | :--- |
182 | | 等于 | `equalTo` |
183 | | 不等于 | `notEqualTo` |
184 | | 大于 | `greaterThan` |
185 | | 大于等于 | `greaterThanOrEqualTo` |
186 | | 小于 | `lessThan` |
187 | | 小于等于 | `lessThanOrEqualTo` |
188 |
189 | ---
190 |
191 | ## 数组查询
192 |
193 | 对于数组类型的字段,你可以查询到`arrayKey`包含了2的对象:
194 |
195 | ```js
196 | // Find objects where the array in arrayKey contains 2.
197 | query.equalTo("arrayKey", 2);
198 | ```
199 |
200 | 你也可以这样查询到键值包含了2、3、4的的对象:
201 |
202 | ```js
203 | // Find objects where the array in arrayKey contains all of the elements 2, 3, and 4.
204 | query.containsAll("arrayKey", [2, 3, 4]);
205 | ```
206 |
207 | ---
208 |
209 | ## 字符串查询
210 |
211 | > 如果你想实现一个通用的搜索功能,我们推荐你看看这篇博文:[Implementing Scalable Search on a NoSQL Backend](http://blog.parse.com/learn/engineering/implementing-scalable-search-on-a-nosql-backend/)
212 |
213 | 我们可以使用`startWith`限制特定开头的字符串值,类似与MySQL中的LIKE操作。这是索引过的,所以在大数据集中也有效:
214 |
215 | ```js
216 | // Finds barbecue sauces that start with "Big Daddy's".
217 | var query = new Parse.Query(BarbecueSauce);
218 | query.startsWith("name", "Big Daddy's");
219 | ```
220 |
221 | 上面的例子将会查询到`name`以“Big Daddy's”开头的对象,"Big Daddy's" 和 "Big Daddy's BBQ"都会被匹配到,但是"big daddys's"或者"BBQ Sauce:Big Daddy's"不会。
222 |
223 | 使用正则查询是非常耗费性能的,尤其是超过10万条数据时。Parse限制了给定时间里特定应用可以执行多少次这样的查询。
224 |
225 | ---
226 |
227 | ## 关系查询
228 |
229 | 有几种方法可以查询关系型的数据。如果你想查询某字段为某个对象的话,可以直接像查询其他类型一样使用`equalTo`。比如每个`Comment`都有一个`post`字段指向`Post`,你可以这样查询特定`Post`的评论:
230 |
231 | ```js
232 | // Assume Parse.Object myPost was previously created.
233 | var query = new Parse.Query(Comment);
234 | query.equalTo("post", myPost);
235 | query.find({
236 | success: function(comments) {
237 | // comments now contains the comments for myPost
238 | }
239 | });
240 | ```
241 |
242 | 如果你想查询某字段包含一个和另一个查询匹配的对象,你可以使用`matchesQuery`。为了查询包含图片的文章的评论,你可以这样:
243 |
244 | ```js
245 | var Post = Parse.Object.extend("Post");
246 | var Comment = Parse.Object.extend("Comment");
247 | var innerQuery = new Parse.Query(Post);
248 | innerQuery.exists("image");
249 | var query = new Parse.Query(Comment);
250 | query.matchesQuery("post", innerQuery);
251 | query.find({
252 | success: function(comments) {
253 | // comments now contains the comments for posts with images.
254 | }
255 | });
256 | ```
257 |
258 | 反之,你想查询某字段不包含和另一个查询匹配的对象,你可以使用`doesNotMatchesQuery`:
259 |
260 | ```js
261 | var Post = Parse.Object.extend("Post");
262 | var Comment = Parse.Object.extend("Comment");
263 | var innerQuery = new Parse.Query(Post);
264 | innerQuery.exists("image");
265 | var query = new Parse.Query(Comment);
266 | query.doesNotMatchQuery("post", innerQuery);
267 | query.find({
268 | success: function(comments) {
269 | // comments now contains the comments for posts without images.
270 | }
271 | });
272 | ```
273 |
274 | 你也可以通过`objectId`来查询关联对象:
275 |
276 | ```js
277 | var post = new Post();
278 | post.id = "1zEcyElZ80";
279 | query.equalTo("post", post);
280 | ```
281 |
282 | 在某些情况下,你想要在一次查询中返回多个类型的关联对象,那么你可以使用`include`方法。比如说你想查询最新的10条评论,并同时查询到关联的文章:
283 |
284 | ```js
285 | var query = new Parse.Query(Comment);
286 |
287 | // Retrieve the most recent ones
288 | query.descending("createdAt");
289 |
290 | // Only retrieve the last ten
291 | query.limit(10);
292 |
293 | // 评论对象同时包含文章对象
294 | query.include("post");
295 |
296 | query.find({
297 | success: function(comments) {
298 | // Comments now contains the last ten comments, and the "post" field
299 | // has been populated. For example:
300 | for (var i = 0; i < comments.length; i++) {
301 | // This does not require a network access.
302 | var post = comments[i].get("post");
303 | }
304 | }
305 | });
306 | ```
307 |
308 | 你也可以使用`post.author`的形式嵌套查询,如果你想查询包含了文章和文章作者的评论,你可以这样:
309 |
310 | ```js
311 | query.include(["post.author"]);
312 | ```
313 |
314 | 你可以多次调用`include`,发起一个包含了多个字段的请求,这个功能在`Parse.Query`的其他查询方法中也有效,比如`first`和`get`。
315 |
316 | ---
317 |
318 | ## 统计查询
319 |
320 | 如果你只是想知道有多少个匹配的对象,但不需要拿到对象的详细信息,你可以使用`count`方法替代`find`方法。比如,你想知道特定玩家玩了多少个游戏:
321 |
322 | ```js
323 | var GameScore = Parse.Object.extend("GameScore");
324 | var query = new Parse.Query(GameScore);
325 | query.equalTo("playerName", "Sean Plott");
326 | query.count({
327 | success: function(count) {
328 | // The count request succeeded. Show the count
329 | alert("Sean has played " + count + " games");
330 | },
331 | error: function(error) {
332 | // The request failed
333 | }
334 | });
335 | ```
336 |
337 | ---
338 |
339 | ## 组合查询
340 |
341 | 更复杂的查询情况下,你可能需要用到组合查询。一个组合查询是多个子查询的组合\(如"and或"or"\)。
342 |
343 | 需要注意的是,我们不支持在组合查询的子查询中使用GeoPint或者非过滤类型的约束条件。
344 |
345 | ### 或查询
346 |
347 | 如果你想查询多个请求中符合其中一个即可的数据,你可以使用Parse.Query.or方法构建一个由传入的子查询组成的或查询。比如你想查询胜利次数在指定范围的玩家,你可以这样:
348 |
349 | ```js
350 | var lotsOfWins = new Parse.Query("Player");
351 | lotsOfWins.greaterThan("wins", 150);
352 |
353 | var fewWins = new Parse.Query("Player");
354 | fewWins.lessThan("wins", 5);
355 |
356 | var mainQuery = Parse.Query.or(lotsOfWins, fewWins);
357 | mainQuery.find()
358 | .then(function(results) {
359 | // results contains a list of players that either have won a lot of games or won only a few games.
360 | })
361 | .catch(function(error) {
362 | // There was an error.
363 | });
364 | ```
365 |
366 | ### 与查询
367 |
368 | 如果你想查询和所有条件都符合的数据,通常只需要一次请求。你可以添加额外的条件,它实际上就是与查询:
369 |
370 | ```js
371 | var query = new Parse.Query("User");
372 | query.greaterThan("age", 18);
373 | query.greaterThan("friends", 0);
374 | query.find()
375 | .then(function(results) {
376 | // results contains a list of users both older than 18 and having friends.
377 | })
378 | .catch(function(error) {
379 | // There was an error.
380 | });
381 | ```
382 |
383 | 但如果这个世界真的有这么简单那就好了。有时你可能需要用到与组合查询,Parse.Query.and方法可以构建一个由传入的子程序组成的与查询。比如你想查询用户年龄为16或者18,并且ta的好友少于两人的用户,你可以这样:
384 |
385 | ```js
386 | var age16Query = new Parse.Query("User");
387 | age16Query.equalTo("age", 16);
388 |
389 | var age18Query = new Parse.Query("User");
390 | age18Query.equalTo("age", 18);
391 |
392 | var friends0Query = new Parse.Query("User");
393 | friends0Query.equalTo("friends", 0);
394 |
395 | var friends2Query = new Parse.Query("User");
396 | friends2Query.greaterThan("friends", 2);
397 |
398 | var mainQuery = Parse.Query.and(
399 | Parse.Query.or(age16Query, age18Query),
400 | Parse.Query.or(friends0Query, friends2Query)
401 | );
402 | mainQuery.find()
403 | .then(function(results) {
404 | // results contains a list of users in the age of 16 or 18 who have either no friends or at least 2 friends
405 | // results: (age 16 or 18) and (0 or >2 friends)
406 | })
407 | .catch(function(error) {
408 | // There was an error.
409 | });
410 | ```
411 |
412 | ##
413 |
414 |
415 |
416 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Parse Server JavaScript中文指南
2 |
3 | 如果你是前端。
4 |
5 | 如果你想顺带着轻描淡写地搞定后端。
6 |
7 | 如果你想摇身一变成为全栈开发。
8 |
9 | 如果你喜欢没事鼓捣鼓捣业余项目。
10 |
11 | Parse后端服务,Facebook开源项目。
12 |
13 | ---
14 |
15 | 中文指南地址:[https://parse-zh.buzhundong.com/](https://parse-zh.buzhundong.com/)
16 |
17 | 英文指南地址:[http://docs.parseplatform.org/js/guide](http://docs.parseplatform.org/js/guide)
18 |
19 | API文档地址:[http://parseplatform.org/Parse-SDK-JS/api/v1.11.0/index.html](http://parseplatform.org/Parse-SDK-JS/api/v1.11.0/index.html)
20 |
21 | Parse服务商: [https://www.back4app.com/](https://www.back4app.com/)
22 |
23 | 翻译:朱嘉伟\([http://buzhundong.com](http://buzhundong.com)\)
24 |
25 | ===11/13更新==
26 |
27 | 看到有几网友想加我微信,或想加QQ群,所以我建了个群QQ群以便大家交流:131762705,欢迎加入。
28 |
--------------------------------------------------------------------------------
/Roles.md:
--------------------------------------------------------------------------------
1 | # 角色
2 |
3 | 随着你的应用的规模增长,你可能发现你需要更高颗粒度的权限管理来控制用户访问,而不是用户链接ACL的方法管理。要解决这个需求,Parse提供了基于用户角色的权限管理。角色提供了一种合理的方法,通过访问权限对用户分组。角色对象包含了用户和其他角色。任何角色被允许访问的数据,那么角色所包含的用户和角色也可以访问该数据。
4 |
5 | 比如,在具有角色扮演的应用中,可能有一些用户身份是主人,主人有权限领取皮鞭、项圈等;可能还有一些用户身份是宠物,无权限领取那些物件。如果一个主人有很多宠物,一个个给宠物设置权限是很麻烦的。通过角色,就不必手动的为每个用户授予访问每个资源的权限。
6 |
7 | 我们提供了一个专门的类`Parse.Role`,它在你的客户端代码中代表角色对象,`Parse.Role`是`Parse.Object`的子类,并且具有`Parse.Object`的所有功能,比如灵活的模型、自动持久化、键值接口。`Parse.Object`的所有方法,`Parse.Role`都具备,不同的是,`Parse.Role`有角色管理功能。
8 |
9 | ---
10 |
11 | ## Parse.Role属性
12 |
13 | `Parse.Role`的这些属性`Parse.Object`没有:
14 |
15 | * name:角色名,必需,并且只能在创建的时候设置。这个值是由字母、数字、空格或下划线组成,name将被用来识别角色,而不需要objectId。
16 | * users:继承此角色被授予权限的用户。
17 | * roles:继承此角色被授予权限的角色。
18 |
19 | ---
20 |
21 | ## 角色安全
22 |
23 | `Parse.Role`使用的是和Parse上其他对象一样的安全模型(ACL),不同的是它需要被显示的设置。通常,只有权限足够大的用户,比如管理员,才可以创建和修改角色,所以你应该定义合理的ACL。记住,如果你授予一个用户对角色的写权限,那么这个用户可以添加其他用户到这个角色中,甚至删除所有角色。
24 |
25 | 要创建一个新的`Parse.Role`,你要这样写:
26 |
27 | ```js
28 | // 为此角色禁用写权限
29 | var roleACL = new Parse.ACL();
30 | roleACL.setPublicReadAccess(true);
31 | var role = new Parse.Role("Administrator", roleACL);
32 | role.save();
33 | ```
34 |
35 | 你可以通过`Parse.Role`上的users和roles关联对象,添加用户和角色来继承你的新角色权限:
36 |
37 | ```js
38 | var role = new Parse.Role(roleName, roleACL);
39 | role.getUsers().add(usersToAddToRole);
40 | role.getRoles().add(rolesToAddToRole);
41 | role.save();
42 | ```
43 |
44 | 当你指派ACL到你的角色时应尤其小心,确保只有有权修改的用户可以修改数据。
45 |
46 | ---
47 |
48 | ## 基于角色的对象安全
49 |
50 | 现在你为你的应用创建了一组角色,你可以在ACL中使用他们定义对象权限。每一个`Parse.Object`可以指定一个`Parse.ACL`,它提供了访问控制列表,可以标示哪些用户和角色可以读写对象。
51 |
52 | 给予一个对象某角色的读写权限非常简单:
53 |
54 | ```js
55 | var moderators = /* 查询到的 Parse.Role */;
56 | var wallPost = new Parse.Object("WallPost");
57 | var postACL = new Parse.ACL();
58 | postACL.setRoleWriteAccess(moderators, true);
59 | wallPost.setACL(postACL);
60 | wallPost.save();
61 | ```
62 |
63 | 你也可以不使用查询获取角色,直接给ACL指定角色名:
64 |
65 | ```js
66 | var wallPost = new Parse.Object("WallPost");
67 | var postACL = new Parse.ACL();
68 | postACL.setRoleWriteAccess("Moderators", true);
69 | wallPost.setACL(postACL);
70 | wallPost.save();
71 | ```
72 |
73 | ---
74 |
75 | ## 角色等级
76 |
77 | 如上所述,一个角色可以包含另一个角色,两个角色间建立了父子关系,这种关系的结果就是,当父对象的权限被承认后,所有的子对象也会被承认。
78 |
79 | 这些类型的关系通常在具有用户管理的应用中存在,比如论坛。用户的小部分子集是管理员,具备最高级别的权限,可以创建新板块,设置全局消息等。还有部分用户是版主,他们负责确保用户创建的内容是合适的。任何具备管理员权限的用户也会具备版主的权限。要完成这样的关系,你要使你的管理员角色成为版主角色的子角色:
80 |
81 | ```js
82 | var administrators = /* 你的管理员角色 */;
83 | var moderators = /* 你的版主角色 */;
84 | moderators.getRoles().add(administrators);
85 | moderators.save();
86 | ```
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/SUMMARY.md:
--------------------------------------------------------------------------------
1 | # Summary
2 |
3 | ## Overview
4 |
5 | * [介绍](README.md)
6 | * [开始](get-started.md)
7 | * [对象\(Objects\)](Object.md)
8 | * [查询\(Queries\)](Query.md)
9 | * [实时请求\(LiveQueries\)](Live Query.md)
10 | * [用户\(Users\)](Users.md)
11 | * [会话\(Sessions\)](Session.md)
12 | * [角色\(Roles\)](Roles.md)
13 | * [文件\(Files\)](Files.md)
14 | * [链式写法\(Promises\)](Promise.md)
15 | * [坐标\(GeoPint\)](Geo Point.md)
16 | * [配置\(Config\)](config.md)
17 | * [推送通知\(PushNotification\)](tui-song-tong-77e528-pushnotification.md)
18 | * [分析\(Analytics\)](analytics.md)
19 | * [数据\(Data\)](data.md)
20 | * [对象关系\(Relation\)](relation.md)
21 | * [错误处理\(Error Handling\)](error-handling.md)
22 | * [安全\(Security\)](security.md)
23 | * [性能\(Performance\)](performance.md)
24 | * [错误码\(Error Code\)](error-code.md)
25 | * [番外:ParseServer配置指南](parseserver.md)
26 | * [番外:仪表盘配置指南](dashboard.md)
27 | * [番外:云代码使用指南](cloudcode.md)
28 |
29 |
--------------------------------------------------------------------------------
/Session.md:
--------------------------------------------------------------------------------
1 | # 会话
2 |
3 | 会话表示已登录用户的的实例,会话会在用户注册或登录时自动创建,也会在用户注销时自动删除。每个用户都有一个独特的`Session`,如果用户从一个已登录设备发起一个登录请求,那么用户原本的`Session`就会被自动覆盖。`Session`对象被存储在Parse的`Session`类中,你可以在Parse.com的数据浏览器上看到他们,我们提供了相应的API集合为你在应用中管理`Session`。
4 |
5 | `Session`是Parse `Object`的子类,所以你可以像处理其他Parse对象一样查询、更新、删除`Session`。当你注册或登录用户时,Parse云端会自动创建session,你不需要手动的创建`Session`对象。删除`Session`会造成当前设备上使用此session的用户被注销。
6 |
7 | 不同于其他的Parse对象,`Session`类在云代码中没有触发器,所以你不需要为`Session`类注册`beforeSave`或`afterSave`。
8 |
9 | ---
10 |
11 | ## 会话属性
12 |
13 | `Session`对象具有下面这些字段:
14 |
15 | * `sessionToken`(只读):用于验证Parse API请求的字符串标识,在请求的相应结果中,只有当前`Session`对象包含`sessionToken`。
16 | * `user`(只读):指向所属的用户对象
17 | * `createdWith`(只读):关于session是如何创建的信息(e.g `{ "action": "login", "authProvider": "password"}`)。
18 |
19 | * `action`可能的值有:`login`,`signup`,`create`,`upgrade`。当保存的`Session`对象是开发者手动创建的,值就会是`create`;当用户从旧的`sessionToken`更新到新的`sessionToken`,值就会是`upgrade`。
20 | * `authProvider` 可能的值有,`password`,`anonymous`,`facebook`,`twitter`。
21 |
22 | * `restricted`(只读):表示session是否受限制。
23 |
24 | * 受限制session对Parse上的`User`、`Session`、`Role`没有写权限,受限制的会话也不能读取未受限制的会话。
25 | * 通过用户注册或登录,由Parse云端自动创建的session,全都是未受限制的;由开发者通过客户端手段创建保存的new `Session`对象,都是受限制的。
26 |
27 | * `expiresAt`(只读):`Session`对象将被自动删除的日期(UTC),你可以在你应用的Parse面板设置页面进行设置(一年不活动过期或永不过期)。
28 |
29 | * `installationId`(只能设置一次):表示会话从哪里登录。对于Parse SDK,这个字段会在用户注册或登录时自动设置。除了`installationId`,所有的字段只能由Parse云端自动设置。但请记住,所有已登录设备都可以读取相同用户的其他session,除非你禁用表级权限。
30 |
31 | ---
32 |
33 | ## 处理无效的session token
34 |
35 | 使用可撤回session,如果你的`Session`对象被Parse云端删除了,那么你对应的当前用户session也会失效,这在某些情况下可能会用上,比如你从用户`Session`管理界面上,将用户强制注销,或者你手动的从云端删除了session。
36 |
37 | session还可以通过设置自动过期来自动删除。 如果一个设备上的session token无法和Parse云端匹配上,那么所有来自此设备的请求都将返回错误信息:“Error 209: invalid session token”。
38 |
39 | 要处理这个错误,我们推荐写一个全局的功能函数,并在你的Parse请求发生错误时调用,然后你就可以在全局函数中处理无效“invalid session token”。同时你应该提示用户重新登录,这样用户就能获得新的session token。像这样:
40 |
41 | ```js
42 | function handleParseError(err) {
43 | switch (err.code) {
44 | case Parse.Error.INVALID_SESSION_TOKEN:
45 | Parse.User.logOut();
46 | ... // 提示用户重新登录
47 | ... // If Express.js, redirect the user to the log in route
48 | break;
49 |
50 | ... // Other Parse API errors that you want to explicitly handle
51 | }
52 | }
53 |
54 | // 为所有api请求的报错调用全局错误处理方法
55 | query.find().then(function() {
56 | ...
57 | }, function(err) {
58 | handleParseError(err);
59 | });
60 | ```
61 |
62 | ---
63 |
64 | ## 会话安全
65 |
66 | `Session`对象只有user字段指定的用户可以访问,所有的`Session`对象都有一个ACL,只允许指定用户读写,你不能修改这个ACL。这意味着会话的查询只会返回与当前登录用户相匹配的会话。
67 |
68 | 当你使用login方法登录时,Parse云端将会自动创建一个未受限制的`Session`对象,使用signup、脸书、推特登录也是一样。
69 |
70 | 通过客户端SDK手动创建的`Session`对象始终是受限制的,你不能使用api手动创建一个未受限制的会话。
71 |
72 | 受限制的会话禁止创建、修改、更新`User`、`Session`、`Role`中的任何数据,受限制的会话也不能读取未受限制的会话。受限制的会话在“Parse for IoT”设备(例如 Arduino、Embedded C)中很有用,这在不可信的物理环境用的也比手机应用环境用的多。无论如何,记住受限制的会话仍然可以读取`User`、`Session`、`Role`类中的数据,并且可以像正常session一样读写其他class中的任何数据。这对IoT设备的安全和加密存储仍然非常重要。
73 |
74 | 如果你想阻止受限制会话修改`User`、`Session`、`Role`以外的class,你可以为在云代码中为其class写一个`beforeSave`处理器:
75 |
76 | ```js
77 | Parse.Cloud.beforeSave("MyClass", function(request, response) {
78 | Parse.Session.current().then(function(session) {
79 | if (session.get('restricted')) {
80 | response.error('禁止写操作');
81 | }
82 | response.success();
83 | });
84 | });
85 | ```
86 |
87 | 如果你想在Parse上为`Session`表设置表级权限(CLP),以限制session的读写,但不要在用户注册、登录、注销时限制Parse云自动增删session。我们推荐你禁用`Session`所有不需要的CLP。下面有一些`Session` CLP的用例:
88 |
89 | * **Find、Delete** - 用以构建一个供用户查看其他登录的在线设备,并且注销其他设备的用户.的功能。如果你不需要个功能,你应该禁用这个权限。
90 | * **Create **- 用于“Parse for IoT”应用,如果是的app是手机应用或web应用,如果你的IoT设备不需要用户会话,你应该禁用这个权限。
91 | * **Get、Update、Add Field** - 除非你需要这些操作,否则你应该禁用这些权限。
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/Users.md:
--------------------------------------------------------------------------------
1 | # 用户
2 |
3 | 在许多应用的核心功能中,都有用户账户的概念,它让用户以一种安全的方式访问自己的信息。我们提供了专门的用户类,通过调用`Parse.User`可以实现很多用户管理的功能。
4 |
5 | `Parse.User`是`Parse.Object`的子类,它们具有一样的特征,比如灵活的模式,自动持久化,和键值接口。`Parse.Object`上的所有方法,`Parse.User`也有,不同的是,`Parse.User`还具备一些额外的用户操作方法。
6 |
7 | ---
8 |
9 | ## 用户属性
10 |
11 | Parse.User具备一些`Parse.Object`没有的属性:
12 |
13 | * username:用户名,必须
14 | * password:密码,必须
15 | * email:邮箱,可选
16 |
17 | 我们将通过使用`Parse.User`的各个使用例子来一一详细了解。
18 |
19 | ---
20 |
21 | ## 注册
22 |
23 | 你的应用可能首先要做的就是要求用户注册,下面的代码是典型的注册示例:
24 |
25 | ```js
26 | var user = new Parse.User();
27 | user.set("username", "my name");
28 | user.set("password", "my pass");
29 | user.set("email", "email@example.com");
30 |
31 | // 还可以设置电话号码
32 | user.set("phone", "415-392-0202");
33 |
34 | user.signUp(null, {
35 | success: function(user) {
36 | // 注册成功.
37 | },
38 | error: function(user, error) {
39 | // Show the error message somewhere and let the user try again.
40 | alert("Error: " + error.code + " " + error.message);
41 | }
42 | });
43 | ```
44 |
45 | 此调用是异步在你的Parse应用上创建一个新用户。在此之前,它会检查你的账号和邮箱是否被使用。另外,在云端密码也用bcrypt进行了加密,我们不会将密码明文存储,也不会将密码明文传输给客户端。
46 |
47 | 需要注意的是,我们使用的是`signUp`方法,而不是`save`方法。新的`Parse.User`应该始终是通过`signUp`方法创建的,而后对用户信息的更新才可以用`save`方法完成。
48 |
49 | 如果注册失败,你应该查看返回的错误对象,最有可能的情况是用户名或邮箱已经被别的用户使用,你应该告知用户,要求用户使用不同的用户名或邮箱。
50 |
51 | 你也可以决定使用邮箱地址作为用户名,只需要要求用户输入他们的邮箱,不过邮箱是保存在`username`属性上,`Parse.User`会正常工作。我们将会在密码重置章节回顾这是如何处理的。
52 |
53 | ---
54 |
55 | ## 登录
56 |
57 | 当然,在我们允许用户注册以后,你将来还需要他们登录,以访问他们的账户。为了实现此功能,你可以使用用户类的`logIn`方法:
58 |
59 | ```js
60 | Parse.User.logIn("myname", "mypass", {
61 | success: function(user) {
62 | // Do stuff after successful login.
63 | },
64 | error: function(user, error) {
65 | // The login failed. Check error to see why.
66 | }
67 | });
68 | ```
69 |
70 | ---
71 |
72 | ## 邮箱验证
73 |
74 | 启用应用设置中的邮箱验证,可以让应用为已验证用户保留部分体验。邮箱验证添加一个`emailVerified`字段在`Parse.User`对象上,当`Parse.User`的`email`被设置或修改了,`emailVerified`就会被设置成`false`,Parse会发送一封链接邮件给用户,验证通过后`emailVerified`就会被设置为`true`。
75 |
76 | `emailVerified`有三种状态:
77 |
78 | 1. `true` - 用户通过Parse发送的验证邮件验证成功。用户账号首次创建的时候,值永远不会为`true`。
79 | 2. `false`- 用户还没有验证ta的邮箱。
80 | 3. `missing`- 当邮箱验证未启用,或者`Parse.User`没有`email`值。
81 |
82 | ---
83 |
84 | ## 当前用户
85 |
86 | 如果用户每次打开你的应用都要登录,那是很麻烦的 。你可以通过使用缓存的当前`Parse.User`对象,避免这个麻烦。
87 |
88 | 请注意,这个功能在Node.js环境中,默认是禁止的,这是为了阻止在服务端配置上有状态的用法。
89 |
90 | 无论何时,当你使用`signup`或`login`方法后,用户登录信息都被缓存到了`localStorage`中,你可以把它当做用户会话使用,以此判断用户是否登录。
91 |
92 | ```js
93 | var currentUser = Parse.User.current();
94 | if (currentUser) {
95 | // do stuff with the user
96 | } else {
97 | // show the signup or login page
98 | }
99 | ```
100 |
101 | 你可以通过注销用户来清除当前用户:
102 |
103 | ```js
104 | Parse.User.logOut().then(() => {
105 | var currentUser = Parse.User.current(); // this will now be null
106 | });
107 | ```
108 |
109 | ---
110 |
111 | ## 设置当前用户
112 |
113 | 如果你已经创建了自己的身份验证程序,或者用户通过其他方式登录到了服务端,你可以传入session token到`become`方法,这个方法会在设置当前用户前确认session token是否可用。
114 |
115 | ```js
116 | Parse.User.become("session-token-here").then(function (user) {
117 | // The current user is now set to user.
118 | }, function (error) {
119 | // The token could not be validated.
120 | });
121 | ```
122 |
123 | ---
124 |
125 | ## 用户安全
126 |
127 | `Parse.User`类默认情况下是安全的,它的数据默认只有用户自己可以修改,其他用户只有读的权限,除非`Parse.User`权限设置了允许其他用户修改。
128 |
129 | 特别注意,除非你通过了`logIn`或者`signUp`的验证,否则你无法使用`save`或者`delete`方法。这确保了只有用户自己可以修改自己的数据。
130 |
131 | 下面的代码说明了此安全策略:
132 |
133 | ```js
134 | var user = Parse.User.logIn("my_username", "my_password", {
135 | success: function(user) {
136 | user.set("username", "my_new_username"); //修改用户名
137 | user.save(null, {
138 | success: function(user) {
139 | // 修改成功
140 |
141 | // Get the user from a non-authenticated method
142 | var query = new Parse.Query(Parse.User);
143 | query.get(user.objectId, {
144 | success: function(userAgain) {
145 | userAgain.set("username", "another_username");
146 | userAgain.save(null, {
147 | error: function(userAgain, error) {
148 | // This will error, since the Parse.User is not authenticated
149 | }
150 | });
151 | }
152 | });
153 | }
154 | });
155 | }
156 | });
157 | ```
158 |
159 | 从`Parse.User.current()`获取的`Parse.User`永远是通过了验证的。
160 |
161 | 如果你想检查一个`Parse.User`是否通过登录验证,你可以使用`authenticated`方法。你不需要去检查那些通过`authenticated`方法获取的`Parse.User`。
162 |
163 | ---
164 |
165 | ## 对象安全
166 |
167 | 有些应用在`Parse.User`上的安全策略也可以应用在其他对象上,对于任意一个对象,你可以指定哪个用户可以读取和修改。为了支持这种安全类型,每一个对象都有一个`access control list`,通过`Parse.ACL`类实现。
168 |
169 | 要指定一个对象只有某个用户可以读写,最简单的方法始使用`Parse.ACL`。这是通过为`Parse.User`初始化一个`Parse.ACL`完成的:`new Parse.ACL(user)`生成一个`Parse.ACL`,它限制了`user`的访问。当对象被保存后,它的ACL会被更新,就像其他的属性一样。
170 |
171 | 那么,要创建一个只有当前用户可以访问的私人笔记,可以这样:
172 |
173 | ```js
174 | var Note = Parse.Object.extend("Note");
175 | var privateNote = new Note();
176 | privateNote.set("content", "This note is private!");
177 | privateNote.setACL(new Parse.ACL(Parse.User.current()));
178 | privateNote.save();
179 | ```
180 |
181 | 这个note将只有当前用户可以访问,并且,当前用户登录的任何设备都可以访问。这个功能在你想启用跨设备访问用户数据时非常有用,权限也可以给多个用户授权,你可以使用`setReadAccess`和`setWriteAccess`为`Parse.ACL`添加独有的权限。比如,你有一条消息要发送给几个用户,他们中的每一个用户都有权限读取和修改:
182 |
183 | ```js
184 | var Message = Parse.Object.extend("Message");
185 | var groupMessage = new Message();
186 | var groupACL = new Parse.ACL();
187 |
188 | // 用户数组
189 | for (var i = 0; i < userList.length; i++) {
190 | groupACL.setReadAccess(userList[i], true);
191 | groupACL.setWriteAccess(userList[i], true);
192 | }
193 |
194 | groupMessage.setACL(groupACL);
195 | groupMessage.save();
196 | ```
197 |
198 | 你也可以使用`setPublicReadAccess`和`setPublicWriteAccess`一次授权所有用户,这在留言板中发布评论这种模式中很有用。要创建一篇文章只有它的作者可以编辑,但是所有人可以访问,可以这样:
199 |
200 | ```js
201 | var publicPost = new Post();
202 | var postACL = new Parse.ACL(Parse.User.current());
203 | postACL.setPublicReadAccess(true);
204 | publicPost.setACL(postACL);
205 | publicPost.save();
206 | ```
207 |
208 | 禁止的操作,比如删除一个对象,但是你没有修改的权限,会导致一个`Parse.Error.OBJECT_NOT_FOUND`错误码。出于安全的目的,这样可以阻止客户端判断哪些对象是受保护的,哪些对象是完全不存在的。
209 |
210 | ---
211 |
212 | ## 重置密码
213 |
214 | 现实中,只要你已将用户密码传入系统,用户就会忘记密码。鉴于这种情况,我们的库提供了一种方法让他们重设密码。
215 |
216 | 要触发重置密码的流程,先询问用户的邮箱地址,然后调用:
217 |
218 | ```js
219 | Parse.User.requestPasswordReset("email@example.com", {
220 | success: function() {
221 | // 重置密码请求已发出
222 | },
223 | error: function(error) {
224 | // Show the error message somewhere
225 | alert("Error: " + error.code + " " + error.message);
226 | }
227 | });
228 | ```
229 |
230 | 这会尝试用给定的邮箱和用户的`email`或`username`字段进行匹配,并且会发送一封密码重置的邮件。你可以选择让用户用邮箱作为他们的用户名,也可以把邮箱单独的存在`email`字段。
231 |
232 | 密码重置流程如下:
233 |
234 | 1. 用户通过输入他们的邮箱来请求重置密码
235 | 2. Parse发送一封 邮件到他们的邮箱,其中包含一个密码重置的链接
236 | 3. 用户点击链接,将被引导到指定的Parse页面,在这个页面上重置密码
237 | 4. 用户输入新的密码后,他们的密码就被重置为新密码
238 |
239 | 注意在此流程中的消息传递所引用的应用名,是你创建Parse应用时的app名。
240 |
241 | ---
242 |
243 | ## 查询
244 |
245 | 要查询用户,你可以为用户创建一个新Parse.Query:
246 |
247 | ```js
248 | var query = new Parse.Query(Parse.User);
249 | query.equalTo("gender", "female"); // find all the women
250 | query.find({
251 | success: function(women) {
252 | // Do stuff
253 | }
254 | });
255 | ```
256 |
257 | ---
258 |
259 | ## 联合查询
260 |
261 | 假如你在开发一个博客应用,想要提交一篇新博文,以及查询作者的所有文章,你可以这样:
262 |
263 | ```js
264 | var user = Parse.User.current();
265 |
266 | // 提交一篇新博文
267 | var Post = Parse.Object.extend("Post");
268 | var post = new Post();
269 | post.set("title", "My New Post");
270 | post.set("body", "This is some great content.");
271 | post.set("user", user);
272 | post.save(null, {
273 | success: function(post) {
274 | // 查询作者的所有文章
275 | var query = new Parse.Query(Post);
276 | query.equalTo("user", user);
277 | query.find({
278 | success: function(usersPosts) {
279 | // userPosts contains all of the posts by the current user.
280 | }
281 | });
282 | }
283 | });
284 | ```
285 |
286 | ## 手机验证码登录
287 |
288 | 参考译者的另外写的教程[Parse服务的手机验证码用户注册/登录教程](http://buzhundong.com/post/Parse%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%89%8B%E6%9C%BA%E9%AA%8C%E8%AF%81%E7%A0%81%E7%94%A8%E6%88%B7%E6%B3%A8%E5%86%8C-%E7%99%BB%E5%BD%95%E6%95%99%E7%A8%8B.html)
289 |
290 | ## 脸书登录
291 |
292 | ## 领英登录
293 |
294 |
295 |
296 |
--------------------------------------------------------------------------------
/analytics.md:
--------------------------------------------------------------------------------
1 | # 分析
2 |
3 | > 译者注:此功能在Parse未关闭服务前,在Parse.com上可用;现在仪表盘上已经移除此功能,并建议用户使用第三方分析服务,所以本章可跳过。
4 |
5 | Parse提供了一些钩子来帮助你分析你应用的一些数据。你知道,了解你的app在做什么,和谁做的,什么时候做的,做的频率,做了多久,嗯,是很重要的。
6 |
7 | 虽然本节会介绍不同的方法,来最大化利用Parse的分析服务,但开发者也可以使用Parse仪表盘上已经可以使用的指标来分析。
8 |
9 | 在Parse仪表盘,不需要执行任何客户端逻辑,你就可以实时看到你应用的API请求统计图表(有设备类型、Parse类名、REST请求),并且可以筛选你感兴趣的数据。
10 |
11 | ---
12 |
13 | ## 常用分析
14 |
15 | `Parse.Analytics`允许你跟踪自定义事件,只需要使用一些string类型的键值即可。这些自定义的指标将可以在你应用的仪表盘上看到。
16 |
17 | 假设你的应用提供了公寓列表搜索的功能,并且你想跟踪这个功能的使用情况。下面我们来跟踪三个维度的搜索数据:
18 |
19 | ```js
20 | var dimensions = {
21 | // 定义价格范围
22 | priceRange: '1000-1500',
23 | // 用户是否使用了筛选功能
24 | source: 'craigslist',
25 | // 这次搜索发生在工作日还是周末
26 | dayType: 'weekday'
27 | };
28 | // 发送搜索事件及数据到Parse后端
29 | Parse.Analytics.track('search', dimensions);
30 | ```
31 |
32 | `Parse.Analytics`甚至可以用于异常跟踪,只需要使用下面的代码,就可以为你的应用记录错误频率、错误信息。
33 |
34 | ```
35 | var codeString = '' + error.code;
36 | Parse.Analytics.track('error', { code: codeString });
37 | ```
38 |
39 | 注意目前Parse每次调用`Parse.Analytics.track()`存储指标,最多只有前8个维度数据会被保存。
40 |
41 |
--------------------------------------------------------------------------------
/assets/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaweii/Parse-JavaScript-translation/94a510d0f37dad0edaf1bedc9480e0bb8df47817/assets/1.png
--------------------------------------------------------------------------------
/assets/GIF.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaweii/Parse-JavaScript-translation/94a510d0f37dad0edaf1bedc9480e0bb8df47817/assets/GIF.gif
--------------------------------------------------------------------------------
/assets/clp_vs_acl_diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaweii/Parse-JavaScript-translation/94a510d0f37dad0edaf1bedc9480e0bb8df47817/assets/clp_vs_acl_diagram.png
--------------------------------------------------------------------------------
/assets/dash-shot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaweii/Parse-JavaScript-translation/94a510d0f37dad0edaf1bedc9480e0bb8df47817/assets/dash-shot.png
--------------------------------------------------------------------------------
/assets/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaweii/Parse-JavaScript-translation/94a510d0f37dad0edaf1bedc9480e0bb8df47817/assets/dashboard.png
--------------------------------------------------------------------------------
/assets/email.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaweii/Parse-JavaScript-translation/94a510d0f37dad0edaf1bedc9480e0bb8df47817/assets/email.png
--------------------------------------------------------------------------------
/book.json:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": [ "theme-api","neo-navigator","edit-link","page-footer-ex" ,"theme-vuejs-2"],
3 | "pluginsConfig": {
4 | "theme-api": {
5 | "languages": [
6 | {
7 | "lang": "js",
8 | "name": "JavaScript",
9 | "default": true
10 | },
11 | {
12 | "lang": "go",
13 | "name": "Go"
14 | }
15 | ]
16 | },
17 | "edit-link": {
18 | "base": "https://github.com/jaweii/Parse-JavaScript-translation/blob/master",
19 | "label": "编辑此页"
20 | },
21 | "page-footer-ex": {
22 | "copyright": "",
23 | "markdown": false,
24 | "update_label": "updated",
25 | "update_format": "YYYY-MM-DD HH:mm:ss"
26 | },
27 | "disqus": {
28 | "shortName": "XXXXXXX"
29 | }
30 | },
31 | "links" : {
32 | "sidebar" : {
33 | "New Issue" : "https://github.com/jaweii/Parse-JavaScript-translation/issues/new"
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/cloudcode.md:
--------------------------------------------------------------------------------
1 | # 云代码使用指南
2 |
3 | 云代码,就是运行在云端,而非客户端的函数代码。
4 |
5 | 云代码是构建在ParseServer中的,默认云代码的入口文件是`./cloud/main.js`。
6 |
7 | ## 云函数
8 |
9 | 就像你在代码中调用别人的函数一样,调用云函数就是调用云端的函数。
10 |
11 | 假设我们在云代码文件中定义一个`averageStart`函数,功能是通过传入的电影名返回评论数量:
12 |
13 | ```js
14 | Parse.Cloud.define("averageStars", function(request, response) {
15 | const query = new Parse.Query("Review");
16 | query.equalTo("movie", request.params.movie);
17 | .find()
18 | .then((results) => {
19 | let sum = 0;
20 | for (let i = 0; i < results.length; ++i) {
21 | sum += results[i].get("stars");
22 | }
23 | response.success(sum / results.length);
24 | })
25 | .catch(() => {
26 | response.error("movie lookup failed");
27 | });
28 | });
29 | ```
30 |
31 | 然后,在客户端可以这样调用`averageStarts`方法:
32 |
33 | ```js
34 | Parse.Cloud.run('averageStars', { movie: 'The Matrix' }).then(function(ratings) {
35 | // ratings = 4.5
36 | });
37 | ```
38 |
39 | ## Cloud Jobs
40 |
41 | 云作业,当你需要定义一个执行时间很长的云函数,并且不用等待它响应,你就可以使用云作业。我们来定义一个云作业:
42 |
43 | ```js
44 | Parse.Cloud.job("myJob", function(request, status) {
45 | // 请求参数
46 | const params = request.params;
47 | //请求头
48 | const headers = request.headers;
49 |
50 | // get the parse-server logger
51 | const log = request.log;
52 |
53 | // 更新作业状态
54 | status.message("I just started");
55 | doSomethingVeryLong().then(function(result) {
56 | //作业完成
57 | status.success("I just finished");
58 | })
59 | .catch(function(error) {
60 | // 作业失败
61 | status.error("There was an error");
62 | });
63 | });
64 | ```
65 |
66 | 云作业需要提供master key才能调用,你可以通过REST API调用:
67 |
68 | ```
69 | curl -X POST -H 'X-Parse-Application-Id: appId' -H 'X-Parse-Master-Key: masterKey' https://my-parse-server.com/parse/jobs/myJob
70 | ```
71 |
72 | ## 触发器
73 |
74 | 注册对应class表的钩子后,对应的事件将触发对应的钩子。
75 |
76 | 在下面触发器中,你需要注意,如果你要注册ParseServer预定义的class表,比如User表,你不应该给第一个参数传入字符\(“Review”\),而是传入对象,比如`Parse.User`。
77 |
78 | ### beforeSave
79 |
80 | 当云端收到保存某对象的请求后,在保存某对象前,会触发此方法。
81 |
82 | 若在方法中调用response.error,对象将不会被保存,客户端返回错误;若在方法中调用response.success,对象将会被成功保存。
83 |
84 | ```js
85 | //注册Review表的beforeSave触发器,有对象被保存前,触发此方法。
86 | Parse.Cloud.beforeSave("Review", function(request, response) {
87 | if (request.object.get("stars") < 1) {
88 | response.error("少于1星,不能获取");
89 | } else if (request.object.get("stars") > 5) {
90 | response.error("多余5星,不能获取");
91 | } else {
92 | response.success();
93 | }
94 | });
95 | ```
96 |
97 | 我们还可以在函数中修改对象,比如,将评论内容限制在140字以内:
98 |
99 | ```js
100 | Parse.Cloud.beforeSave("Review", function(request, response) {
101 | var comment = request.object.get("comment");
102 | if (comment.length > 140) {
103 | // Truncate and add a ...
104 | request.object.set("comment", comment.substring(0, 137) + "...");
105 | }
106 | response.success();
107 | });
108 | ```
109 |
110 | ### afterSave
111 |
112 | 在某对象保存后,会触发此方法。
113 |
114 | 假设你要在一条文章评论保存后,更新文章评论数量的字段:
115 |
116 | ```js
117 | Parse.Cloud.afterSave("Comment", function(request) {
118 | const query = new Parse.Query("Post");
119 | query.get(request.object.get("post").id)
120 | .then(function(post) {
121 | post.increment("comments");
122 | return post.save();
123 | })
124 | .catch(function(error) {
125 | console.error("Got an error " + error.code + " : " + error.message);
126 | });
127 | });
128 | ```
129 |
130 | 不管上面方法中有没有报错,客户端都会返回成功。
131 |
132 | ### beforeDelete
133 |
134 | 在对象被删除前,会触发此方法。
135 |
136 | 假设你有一个相册应用,当用户发起了删除某个相册的操作,需要先检查这个相册里有没有照片,如果有就不允许删除,可以这样:
137 |
138 | ```js
139 | Parse.Cloud.beforeDelete("Album", function(request, response) {
140 | const query = new Parse.Query("Photo");
141 | query.equalTo("album", request.object);
142 | query.count()
143 | .then(function(count) {
144 | if (count > 0) {
145 | response.error("Can't delete album if it still has photos.");
146 | } else {
147 | response.success();
148 | }
149 | })
150 | .catch(function(error) {
151 | response.error("Error " + error.code + " : " + error.message + " when getting photo count.");
152 | });
153 | });
154 | ```
155 |
156 | 若在方法中调用response.error,对象将不会被保存,客户端返回错误;若在方法中调用response.success,对象将会被成功保存。
157 |
158 | ### afterDelete
159 |
160 | 在对象被删除后,会触发此方法。
161 |
162 | 假设你在一篇博文被删除后,同时删除这篇博文的所有评论,可以这样:
163 |
164 | ```js
165 | Parse.Cloud.afterDelete("Post", function(request) {
166 | const query = new Parse.Query("Comment");
167 | query.equalTo("post", request.object);
168 | query.find()
169 | .then(Parse.Object.destroyAll)
170 | .catch(function(error) {
171 | console.error("Error finding related comments " + error.code + ": " + error.message);
172 | });
173 | });
174 | ```
175 |
176 | ### beforeFind
177 |
178 | 在find查询进入后,会触发此方法。
179 |
180 | 如果你想为查询加上额外的限制、条件、你可以注册这个触发器。
181 |
182 | #### 例子
183 |
184 | ```js
185 | // 属性
186 | Parse.Cloud.beforeFind('MyObject', function(req) {
187 | let query = req.query; // Parse.Query
188 | let user = req.user; // user
189 | let triggerName = req.triggerName; // beforeFind
190 | let isMaster = req.master; // 如果使用查询masterKey
191 | let isCount = req.count; // 如果是count查询
192 | let logger = req.log; //logger
193 | let installationId = req.installationId; // installationId
194 | });
195 |
196 | // 限制返回字段
197 | Parse.Cloud.beforeFind('MyObject', function(req) {
198 | let query = req.query; // Parse.Query
199 | query.select(['key1', 'key2']);
200 | });
201 |
202 | // 异步支持
203 | Parse.Cloud.beforeFind('MyObject', function(req) {
204 | let query = req.query;
205 | return aPromise().then((results) => {
206 | // do something with the results
207 | query.containedIn('key', results);
208 | });
209 | });
210 |
211 | // 返回一个不同的查询
212 | Parse.Cloud.beforeFind('MyObject', function(req) {
213 | let query = req.query;
214 | let otherQuery = new Parse.Query('MyObject');
215 | otherQuery.equalTo('key', 'value');
216 | return Parse.Query.or(query, otherQuery);
217 | });
218 |
219 | // 拒绝查询
220 | Parse.Cloud.beforeFind('MyObject', function(req) {
221 | // throw an error
222 | throw new Parse.Error(101, 'error');
223 |
224 | // rejecting promise
225 | return Promise.reject('error');
226 | });
227 | ```
228 |
229 |
230 |
231 |
--------------------------------------------------------------------------------
/config.md:
--------------------------------------------------------------------------------
1 | # 配置
2 |
3 | `Parse.Config`是存储在Parse上的配置对象,是远程应用配置的最佳实践。它可以帮你实现全局设置、每日消息之类的功能。
4 |
5 | 要开始使用`Parse.Config`,你需要在Parse配置面板添加一些键值对到你的app,然后你就可以在客户端拉取到`Parse.Config`对象:
6 |
7 | ```js
8 | Parse.Config.get().then(function(config) {
9 | var winningNumber = config.get("winningNumber");
10 | var message = "Yay! The number is " + winningNumber + "!";
11 | console.log(message);
12 | }, function(error) {
13 | // Something went wrong (e.g. request timed out)
14 | });
15 | ```
16 |
17 | ---
18 |
19 | ## 获取配置
20 |
21 | `ParseConfig`即便在糟糕的网络下,也会尽可能的保持可用,默认配置会被缓存,以确保最后一次成功拉取的配置始终可用。在下面的例子中,我们从服务器拉取配置的最新版本,如果`get`失败,我们通过`current`回退到最近一次成功拉取的配置:
22 |
23 | ```js
24 | Parse.Config.get().then(function(config) {
25 | console.log("Yay! Config was fetched from the server.");
26 |
27 | var welcomeMessage = config.get("welcomeMessage");
28 | console.log("Welcome Message = " + welcomeMessage);
29 | }, function(error) {
30 | console.log("拉取失败,使用缓存配置");
31 |
32 | var config = Parse.Config.current();
33 | var welcomeMessage = config.get("welcomeMessage");
34 | if (welcomeMessage === undefined) {
35 | welcomeMessage = "Welcome!";
36 | }
37 | console.log("Welcome Message = " + welcomeMessage);
38 | });
39 | ```
40 |
41 | ---
42 |
43 | ## 当前配置
44 |
45 | 每一个`Parse.Config`实例都是不可改变的,当你从云端获取到一个新的`Parse.Config`实例,这并不会修改已经存在的任何`Parse.Config`,但是会通过`Parse.Config.current()`新创建一个替换掉之前的,因此,你可以安全地传递任何`current()`对象,并且认定它不会自动改变。
46 |
47 | 如果每次要使用配置的时候都拉取一次,这可能过于麻烦,你可以通过`current()`的缓存来避免此麻烦,然后隔一段时间拉取一次就可以了。
48 |
49 | ```js
50 | // 应用每过12小时重新更新配置
51 | var refreshConfig = function() {
52 | var lastFetchedDate;
53 | var configRefreshInterval = 12 * 60 * 60 * 1000;
54 | return function() {
55 | var currentDate = new Date();
56 | if (lastFetchedDate === undefined ||
57 | currentDate.getTime() - lastFetchedDate.getTime() > configRefreshInterval) {
58 | Parse.Config.get();
59 | lastFetchedDate = currentDate;
60 | }
61 | };
62 | }();
63 | ```
64 |
65 | ---
66 |
67 | ## 参数
68 |
69 | `ParseConfig`支持`Parse.Object`支持的绝大多数数据类型:
70 |
71 | string
72 |
73 | * number
74 | * Date
75 | * Parse.File
76 | * Parse.GeoPoint
77 | * JS Array
78 | * JS Object
79 |
80 | 目前配置支持最多100个参数,并且支持的所有参数总大小为128Kb。
81 |
82 |
--------------------------------------------------------------------------------
/dashboard.md:
--------------------------------------------------------------------------------
1 | # 仪表盘配置指南
2 |
3 | Parse仪表盘是一个单独的应用,用来管理Parse Server 应用。[Parse仪表盘GitHub地址](https://github.com/parse-community/parse-dashboard)。
4 |
5 | ## 安装
6 |
7 | ```js
8 | npm install -g parse-dashboard
9 | ```
10 |
11 | 你可以通过在命令行中提供app ID、master key、URL、appName来启动仪表盘:
12 |
13 | ```
14 | parse-dashboard --appId yourAppId --masterKey yourMasterKey --serverURL "https://example.com/parse" --appName optionalName
15 | ```
16 |
17 | 你还可以通过--host、--port、--mountPath来指定主机、端口和挂载路径,你可以使用任何值作为appName,如果没有指定,默认将会使用appID。
18 |
19 | 启动完成后,你可以通过[http://localhost:4040](http://localhost:4040/)访问 仪表盘。
20 |
21 | 
22 |
23 | ## 配置
24 |
25 | ### 文件
26 |
27 | 你也可以在命令行中使用配置文件来启动仪表盘,只需要在你的仪表盘项目目录创建一个parse-dashboard-config.json的文件,并符合下面的格式:
28 |
29 | ```js
30 | {
31 | "apps": [
32 | {
33 | "serverURL": "http://localhost:1337/parse",
34 | "appId": "myAppId",
35 | "masterKey": "myMasterKey",
36 | "appName": "MyApp"
37 | }
38 | ]
39 | }
40 | ```
41 |
42 | 然后你就可以使用`parse-dashboard --config parse-dashboard-config.json`命令来启动仪表盘了。
43 |
44 | ### 环境变量
45 |
46 | 这只在parse-dashboard命令行中可用。
47 |
48 | 有两种方法可以让你配置仪表盘的环境变量。
49 |
50 | #### 多应用
51 |
52 | 在`PARSE_DASHBOARD_CONFIG` 中提供完整的JSON配置,它会像配置文件一样被解析。
53 |
54 | #### 单应用
55 |
56 | 你也可以单独的定义每一个配置项:
57 |
58 | ```
59 | HOST: "0.0.0.0"
60 | PORT: "4040"
61 | MOUNT_PATH: "/"
62 | PARSE_DASHBOARD_TRUST_PROXY: undefined // Or "1" to trust connection info from a proxy's X-Forwarded-* headers
63 | PARSE_DASHBOARD_SERVER_URL: "http://localhost:1337/parse"
64 | PARSE_DASHBOARD_MASTER_KEY: "myMasterKey"
65 | PARSE_DASHBOARD_APP_ID: "myAppId"
66 | PARSE_DASHBOARD_APP_NAME: "MyApp"
67 | PARSE_DASHBOARD_USER_ID: "user1"
68 | PARSE_DASHBOARD_USER_PASSWORD: "pass"
69 | PARSE_DASHBOARD_SSL_KEY: "sslKey"
70 | PARSE_DASHBOARD_SSL_CERT: "sslCert"
71 | PARSE_DASHBOARD_CONFIG: undefined // Only for reference, it must not exist
72 | ```
73 |
74 | ## 管理多个应用
75 |
76 | 在一个仪表盘中管理多个应用也是可以的,只需要在配置项`apps`数组中增加额外的应用配置即可。
77 |
78 | ```
79 | {
80 | "apps": [
81 | {
82 | "serverURL": "https://api.parse.com/1", // Hosted on Parse.com
83 | "appId": "myAppId",
84 | "masterKey": "myMasterKey",
85 | "javascriptKey": "myJavascriptKey",
86 | "restKey": "myRestKey",
87 | "appName": "My Parse.Com App",
88 | "production": true
89 | },
90 | {
91 | "serverURL": "http://localhost:1337/parse", // Self-hosted Parse Server
92 | "appId": "myAppId",
93 | "masterKey": "myMasterKey",
94 | "appName": "My Parse Server App"
95 | }
96 | ]
97 | }
98 | ```
99 |
100 | ## 配置应用图标
101 |
102 | Parse仪表盘可以为每个应用可选的配置图标,以便你在仪表盘列表中快速识别它们。
103 |
104 | 你需要在配置项中定义`iconsFolder`,并且为每个app配置定义`iconName`,`iconsFolder`的路径相对配置文件的,如果你的Parse仪表盘是安装在全局的,你需要为`iconsFolder`指定完整路径。为了直观理解,下面例子中`icons`的所在位置是和配置文件一样的:
105 |
106 | ```
107 | {
108 | "apps": [
109 | {
110 | "serverURL": "http://localhost:1337/parse",
111 | "appId": "myAppId",
112 | "masterKey": "myMasterKey",
113 | "appName": "My Parse Server App",
114 | "iconName": "MyAppIcon.png",
115 | }
116 | ],
117 | "iconsFolder": "icons"
118 | }
119 | ```
120 |
121 | ## 作为Express中间件运行
122 |
123 | 你可以将Parse仪表盘作为Express的中间件运行:
124 |
125 | ```js
126 | var express = require('express');
127 | var ParseDashboard = require('parse-dashboard');
128 |
129 | var dashboard = new ParseDashboard({
130 | "apps": [
131 | {
132 | "serverURL": "http://localhost:1337/parse",
133 | "appId": "myAppId",
134 | "masterKey": "myMasterKey",
135 | "appName": "MyApp"
136 | }
137 | ]
138 | });
139 |
140 | var app = express();
141 |
142 | // make the Parse Dashboard available at /dashboard
143 | app.use('/dashboard', dashboard);
144 |
145 | var httpServer = require('http').createServer(app);
146 | httpServer.listen(4040);
147 | ```
148 |
149 | 如果你想将ParseServer和Parse仪表盘运行在同一个服务上,可以将他们作为express中间件运行:
150 |
151 | ```js
152 | var express = require('express');
153 | var ParseServer = require('parse-server').ParseServer;
154 | var ParseDashboard = require('parse-dashboard');
155 |
156 | var api = new ParseServer({
157 | // Parse Server settings
158 | });
159 |
160 | var options = { allowInsecureHTTP: false };
161 |
162 | var dashboard = new ParseDashboard({
163 | // Parse Dashboard settings
164 | }, options);
165 |
166 | var app = express();
167 |
168 | // make the Parse Server available at /parse
169 | app.use('/parse', api);
170 |
171 | // make the Parse Dashboard available at /dashboard
172 | app.use('/dashboard', dashboard);
173 |
174 | var httpServer = require('http').createServer(app);
175 | httpServer.listen(4040);
176 | ```
177 |
178 | ## 部署仪表盘
179 |
180 | ### 安全
181 |
182 | 为了安全地部署仪表盘,防止你应用的master key泄露,你需要使用HTTPS或基本的身份验证。
183 |
184 | 部署后的仪表盘会检查你使用的连接是否安全,如果你在负载均衡或前置代理后部署仪表盘,那么仪表盘将不会检查连接是否安全,这种情况下,你可以使用--strstProxy选项来启动仪表盘,以依赖客户端安全连接的头部X-Forwarded-\* 。这对于Heroku这样的托管服务很有用,这样你可以信任提供的代理头来判断你使用的是HTTP或HTTPS。你也可以在仪表盘作为express中间件使用的时候设置这个选项。
185 |
186 | ```js
187 | var trustProxy = true;
188 | var dashboard = new ParseDashboard({
189 | "apps": [
190 | {
191 | "serverURL": "http://localhost:1337/parse",
192 | "appId": "myAppId",
193 | "masterKey": "myMasterKey",
194 | "appName": "MyApp"
195 | }
196 | ],
197 | "trustProxy": 1
198 | });
199 | ```
200 |
201 | ### 身份验证
202 |
203 | 你可以通过添加用户名和密码到配置项中,来为仪表盘添加基本的身份验证:
204 |
205 | ```js
206 | {
207 | "apps": [{"...": "..."}],
208 | "users": [
209 | {
210 | "user":"user1",
211 | "pass":"pass"
212 | },
213 | {
214 | "user":"user2",
215 | "pass":"pass"
216 | }
217 | ],
218 | "useEncryptedPasswords": true | false
219 | }
220 | ```
221 |
222 | 你可以将密码明文或bcrypt加密存储,如果是使用bcrypt加密存储,你需要配置`useEncryptedPassword`参数为`true`。你可以使用任何bcrypt在线加密工具为你的密码加密,比如:https://www.bcrypt-generator.com。
223 |
224 | ### 基于用户的访问权限
225 |
226 | 如果你为仪表盘配置了多应用管理,你可以基于用户来限制应用的管理。像这样:
227 |
228 | ```js
229 | {
230 | "apps": [{"...": "..."}],
231 | "users": [
232 | {
233 | "user":"user1",
234 | "pass":"pass1",
235 | "apps": [{"appId": "myAppId1"}, {"appId": "myAppId2"}]
236 | },
237 | {
238 | "user":"user2",
239 | "pass":"pass2",
240 | "apps": [{"appId": "myAppId1"}]
241 | } ]
242 | }
243 | ```
244 |
245 | 这样配置后,user1可以管理myAppId1和myAppId2,user2只能管理myAppId1。
246 |
247 | ## 使用只读masterKey
248 |
249 | 你可以通过配置readOnlyMasterKey来限制在仪表盘对对象的写操作,readOnlyMasterKey和masterKey很相似,只不过没有写权限。
250 |
251 | ### 所用用户使用只读masterKey
252 |
253 | 在ParseServer中配置如下:
254 |
255 | ```
256 | {
257 | "masterKey": "YOUR_MASTER_KEY_HERE",
258 | "readOnlyMasterKey": "YOUR_READ_ONLY_MASTER_KEY",
259 | }
260 | ```
261 |
262 | 在仪表盘中配置如下:
263 |
264 | ```js
265 | var trustProxy = true;
266 | var dashboard = new ParseDashboard({
267 | "apps": [
268 | {
269 | "serverURL": "http://localhost:1337/parse",
270 | "appId": "myAppId",
271 | "masterKey": "YOUR_READ_ONLY_MASTER_KEY",
272 | "appName": "MyApp"
273 | }
274 | ],
275 | "trustProxy": 1
276 | });
277 | ```
278 |
279 | ### 部分用户使用只读masterKey
280 |
281 | 使用`readOnly`将用户标记为只读用户:
282 |
283 | ```js
284 | {
285 | "apps": [
286 | {
287 | "appId": "myAppId1",
288 | "masterKey": "myMasterKey1",
289 | "readOnlyMasterKey": "myReadOnlyMasterKey1",
290 | "serverURL": "myURL1",
291 | "port": 4040,
292 | "production": true
293 | },
294 | {
295 | "appId": "myAppId2",
296 | "masterKey": "myMasterKey2",
297 | "readOnlyMasterKey": "myReadOnlyMasterKey2",
298 | "serverURL": "myURL2",
299 | "port": 4041,
300 | "production": true
301 | }
302 | ],
303 | "users": [
304 | {
305 | "user":"user1",
306 | "pass":"pass1",
307 | "readOnly": true,
308 | "apps": [{"appId": "myAppId1"}, {"appId": "myAppId2"}]
309 | },
310 | {
311 | "user":"user2",
312 | "pass":"pass2",
313 | "apps": [{"appId": "myAppId1"}]
314 | }
315 | ]
316 | }
317 | ```
318 |
319 | user1将只有读操作的权限。
320 |
321 | ### 指定应用只读
322 |
323 | 在每个用户的apps配置项中,你可以基于每个应用的给定只读权限:
324 |
325 | ```js
326 | {
327 | "apps": [
328 | {
329 | "appId": "myAppId1",
330 | "masterKey": "myMasterKey1",
331 | "readOnlyMasterKey": "myReadOnlyMasterKey1",
332 | "serverURL": "myURL",
333 | "port": 4040,
334 | "production": true
335 | },
336 | {"...": "..."}
337 | ],
338 | "users": [
339 | {
340 | "user":"user",
341 | "pass":"pass",
342 | "apps": [{"appId": "myAppId", "readOnly": true}, {"appId": "myAppId2"}]
343 | }
344 | ]
345 | }
346 | ```
347 |
348 | user将会myAppId只有读操作的权限。
349 |
350 |
351 |
352 |
353 |
354 |
355 |
356 |
--------------------------------------------------------------------------------
/data.md:
--------------------------------------------------------------------------------
1 | # 数据
2 |
3 | Parse SDK被设计成让开发者无需考虑数据是如何被存储的,只需要添加数据到对象,数据就会被正确的存储。
4 |
5 | 然而某些情况下,了解数据是如何被存储到云端,是很有用的。
6 |
7 | ---
8 |
9 | ## 数据存储
10 |
11 | 本质上,Parse存储的是JSON格式,所以任何可以转换成JSON的数据都可以被存储到Parse。这在对象章节有做介绍。
12 |
13 | 键名中`$`或`.`以及`__type`是被保护的关键词,用以处理其他类型,所以不要使用它们作为键名。键名只能由数字、字母和下划线组成,并且必须以字母开头,键值可以是任何能被JSON编码的类型。
14 |
15 | ---
16 |
17 | ## 数据类型锁定
18 |
19 | 当一个class表创建完成,它还没有被定义各字段类型,这意味着你创建首个class对象的字段类型,可以是任何你希望的类型。
20 |
21 | 不过,一旦字段类型被设置了至少一次,这个字段的类型将被锁定为设置的字段类型。举个例子,加入你有一个`User`对象,你为其设置了`String`类型的`name`字段,那么以后`name`的数据类型只能是`String`,如果你尝试保存其他类型到这个字段,服务器会返回错误。
22 |
23 | 有一个特殊情况,就是任何类型的字段都可以被设置为`null`。
24 |
25 | ---
26 |
27 | ## 数据浏览器
28 |
29 | 数据浏览器是用来增删查改你的应用数据的后台界面,在这里,你可以看到保存的每个对象的原始JSON值。
30 |
31 | 当你使用这个界面时,你需要注意一下几点:
32 |
33 | * `objectId`,`createdAt`,`updatedAt`字段不能被编辑,它们是自动设置的;
34 | * 值为“\(empty\)”的字段表示其对象字段还未被设置,这和值为`null`不同;
35 | * 你可以选中之后点击删除键删除字段的内容;
36 |
37 | 数据浏览器也是测试云代码功能\(比如beforeSave\)的到地方,当你从数据浏览器修改或删除一个对象,就会触发对应的云代码,就像他们被你的客户端代码修改或删除了一样。
38 |
39 | ---
40 |
41 | > 译者注:本章的数据导入和数据导出两小节,其功能在Parse未关闭服务前,在Parse.com上可用;现在仪表盘上已经移除此功能,开发者可以使用MongoDB可视化工具导入导出数据。下面内容可跳过。
42 |
43 | ## 导入数据
44 |
45 | 你可以通过CSV或JSON文件导入数据到你的Parse应用。要通过一个SCV或JSON文件创建一个class表,可以进入数据浏览器,点击左栏的“import”按钮。
46 |
47 | JSON文件的格式是我们的REST格式的对象数组,或者一个数组格式的results对象,它必须遵循混JSON规范。一个JSON文件的标准格式类似这样:
48 |
49 | ```js
50 | { "results": [
51 | {
52 | "score": 1337,
53 | "playerName": "Sean Plott",
54 | "cheatMode": false,
55 | "createdAt": "2012-07-11T20:56:12.347Z",
56 | "updatedAt": "2012-07-11T20:56:12.347Z",
57 | "objectId": "fchpZwSuGG"
58 | }]
59 | }
60 | ```
61 |
62 | 两种格式的对象包含的键值应符合一下要求:
63 |
64 | * 键名只能包含数字、字母和下划线,并且已字母开头;
65 | * 键值不能包含强行换行"\n";
66 |
67 | 通常一个对象被保存到Parse云后,会被支配一个唯一的标识符,即`objectId`,还有`createdAt`和`updatedAt`字段也会被创建。当你从JSON文件导入数据时,这几个字段可以手动设置。请记住以下几点:
68 |
69 | * `objectId`字段的值应是10个唯一存在的由字母和数字组成的字符串;
70 | * 当你设置`createdAt`或`updatedAt`字段时,值应为IOS 8601标准的时间戳;
71 |
72 | 除了公开的字段,Parse用户表中的对象还可以可以设置bryptPassword字段,它的值是bcrypt哈希后加盐得到的字符串,详情可以查看[StackOverflow answer](http://stackoverflow.com/a/5882472/1351961)。大多数给予OpenSSL的bcrypt实现都内置了生成这个字符串的方法。
73 |
74 | 包含User对象的文件格式类似这样:
75 |
76 | ```js
77 | { "results":
78 | [{
79 | "username": "cooldude",
80 | "createdAt": "1983-09-13T22:42:30.548Z",
81 | "updatedAt": "2015-09-04T10:12:42.137Z",
82 | "objectId": "ttttSEpfXm",
83 | "sessionToken": "dfwfq3dh0zwe5y2sqv514p4ib",
84 | "bcryptPassword": "$2a$10$ICV5UeEf3lICfnE9W9pN9.O9Ved/ozNo7G83Qbdk5rmyvY8l16MIK"
85 | }]
86 | }
87 | ```
88 |
89 | ---
90 |
91 | ## 导出数据
92 |
93 | 你可以在你的应用设置页导出数据。数据导出的网络优先级低于产品请求的优先级,所以可以保证应用请求的查询能被顺利完成,所以你的数据导出速度可能会比较低。
94 |
95 | ### _导出格式_
96 |
97 | 每一个数据表会导出一个我们的REST API所使用JSON格式的压缩文件。由于数据本质上是以JSON格式存储,所以我们可以确保导出的数据和Parse保存的数据相同。其他不能表示Parse数据类型的格式,比如CSV,如果你想使用CSV格式,可以使用网上的JSON-to-CSV转换工具。
98 |
99 | ### _离线分析_
100 |
101 | 对于离线分析,我们推荐你用交替的方法访问你的数据,以免一次提取整个数据表。比如,你可以尝试只导出自上次导出以后发生了变化的数据。这里有一些实现方法:
102 |
103 | * 在Node应用中使用SDK,`Parse.Query.eac()`允许你提取每个匹配的对象。你可以给查询条件设置为查询自上次应用运行后发生了改变的数据,你的node应用可以将这些数据写入磁盘进行离线分析。
104 | * 在脚本中使用REST API,你可以针对你的class表使用skip、limit来查询,然后将数据写入磁盘进行离线分析。你可以再加上只有新被修改的对象可以被提取的条件。
105 | * 如果上面两种方法不能满足你的需求,你可以尝试使用数据浏览器的选择性数据导出。通过点击漏洞图标为需要的数据创建一个过滤器,比如过滤出只有新被修改的数据,当过滤器被应用后,点击数据浏览器右上方的导出数据图标即可。这种导出方法只会导出符合你条件的数据。
106 |
107 |
108 |
109 |
--------------------------------------------------------------------------------
/error-code.md:
--------------------------------------------------------------------------------
1 | # 错误码
2 |
3 | 下面是Parse API可能返回的所有错误码列表,你还可以参考 [RFC2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html) 的HTTP错误码,以便获取更多错误信息
4 |
5 | ## API 问题 {#api-issues}
6 |
7 | | Name | Code | Description |
8 | | :--- | :--- | :--- |
9 | | `UserInvalidLoginParams` | 101 | Invalid login parameters. Check error message for more details. |
10 | | `ObjectNotFound` | 101 | The specified object or session doesn’t exist or could not be found. Can also indicate that you do not have the necessary permissions to read or write this object. Check error message for more details. |
11 | | `InvalidQuery` | 102 | There is a problem with the parameters used to construct this query. This could be an invalid field name or an invalid field type for a specific constraint. Check error message for more details. |
12 | | `InvalidClassName` | 103 | Missing or invalid classname. Classnames are case-sensitive. They must start with a letter, and a-zA-Z0-9\_ are the only valid characters. |
13 | | `MissingObjectId` | 104 | An unspecified object id. |
14 | | `InvalidFieldName` | 105 | An invalid field name. Keys are case-sensitive. They must start with a letter, and a-zA-Z0-9\_ are the only valid characters. Some field names may be reserved. Check error message for more details. |
15 | | `InvalidPointer` | 106 | A malformed pointer was used. You would typically only see this if you have modified a client SDK. |
16 | | `InvalidJSON` | 107 | Badly formed JSON was received upstream. This either indicates you have done something unusual with modifying how things encode to JSON, or the network is failing badly. Can also indicate an invalid utf-8 string or use of multiple form encoded values. Check error message for more details. |
17 | | `CommandUnavailable` | 108 | The feature you tried to access is only available internally for testing purposes. |
18 | | `NotInitialized` | 109 | You must call Parse.initialize before using the Parse library. Check the Quick Start guide for your platform. |
19 | | `ObjectTooLarge` | 116 | The object is too large. Parse Objects have a max size of 128 kilobytes. |
20 | | `ExceededConfigParamsError` | 116 | You have reached the limit of 100 config parameters. |
21 | | `InvalidLimitError` | 117 | An invalid value was set for the limit. Check error message for more details. |
22 | | `InvalidSkipError` | 118 | An invalid value was set for skip. Check error message for more details. |
23 | | `OperationForbidden` | 119 | The operation isn’t allowed for clients due to class-level permissions. Check error message for more details. |
24 | | `CacheMiss` | 120 | The result was not found in the cache. |
25 | | `InvalidNestedKey` | 121 | An invalid key was used in a nested JSONObject. Check error message for more details. |
26 | | `InvalidACL` | 123 | An invalid ACL was provided. |
27 | | `InvalidEmailAddress` | 125 | The email address was invalid. |
28 | | `DuplicateValue` | 137 | Unique field was given a value that is already taken. |
29 | | `InvalidRoleName` | 139 | Role’s name is invalid. |
30 | | `ReservedValue` | 139 | Field value is reserved. |
31 | | `ExceededCollectionQuota` | 140 | You have reached the quota on the number of classes in your app. Please delete some classes if you need to add a new class. |
32 | | `ScriptFailed` | 141 | Cloud Code script failed. Usually points to a JavaScript error. Check error message for more details. |
33 | | `FunctionNotFound` | 141 | Cloud function not found. Check that the specified Cloud function is present in your Cloud Code script and has been deployed. |
34 | | `JobNotFound` | 141 | Background job not found. Check that the specified job is present in your Cloud Code script and has been deployed. |
35 | | `SuccessErrorNotCalled` | 141 | success/error was not called. A cloud function will return once response.success\(\) or response.error\(\) is called. A background job will similarly finish execution once status.success\(\) or status.error\(\) is called. If a function or job never reaches either of the success/error methods, this error will be returned. This may happen when a function does not handle an error response correctly, preventing code execution from reaching the success\(\) method call. |
36 | | `MultupleSuccessErrorCalls` | 141 | Can’t call success/error multiple times. A cloud function will return once response.success\(\) or response.error\(\) is called. A background job will similarly finish execution once status.success\(\) or status.error\(\) is called. If a function or job calls success\(\) and/or error\(\) more than once in a single execution path, this error will be returned. |
37 | | `ValidationFailed` | 142 | Cloud Code validation failed. |
38 | | `WebhookError` | 143 | Webhook error. |
39 | | `InvalidImageData` | 150 | Invalid image data. |
40 | | `UnsavedFileError` | 151 | An unsaved file. |
41 | | `InvalidPushTimeError` | 152 | An invalid push time was specified. |
42 | | `HostingError` | 158 | Hosting error. |
43 | | `InvalidEventName` | 160 | The provided analytics event name is invalid. |
44 | | `ClassNotEmpty` | 255 | Class is not empty and cannot be dropped. |
45 | | `AppNameInvalid` | 256 | App name is invalid. |
46 | | `MissingAPIKeyError` | 902 | The request is missing an API key. |
47 | | `InvalidAPIKeyError` | 903 | The request is using an invalid API key. |
48 |
49 | ## Push 相关问题 {#push-related-errors}
50 |
51 | | Name | Code | Description |
52 | | :--- | :--- | :--- |
53 | | `IncorrectType` | 111 | A field was set to an inconsistent type. Check error message for more details. |
54 | | `InvalidChannelName` | 112 | Invalid channel name. A channel name is either an empty string \(the broadcast channel\) or contains only a-zA-Z0-9\_ characters and starts with a letter. |
55 | | `InvalidSubscriptionType` | 113 | Bad subscription type. Check error message for more details. |
56 | | `InvalidDeviceToken` | 114 | The provided device token is invalid. |
57 | | `PushMisconfigured` | 115 | Push is misconfigured in your app. Check error message for more details. |
58 | | `PushWhereAndChannels` | 115 | Can’t set channels for a query-targeted push. You can fix this by moving the channels into your push query constraints. |
59 | | `PushWhereAndType` | 115 | Can’t set device type for a query-targeted push. You can fix this by incorporating the device type constraints into your push query. |
60 | | `PushMissingData` | 115 | Push is missing a ‘data’ field. |
61 | | `PushMissingChannels` | 115 | Non-query push is missing a ‘channels’ field. Fix by passing a ‘channels’ or ‘query’ field. |
62 | | `ClientPushDisabled` | 115 | Client-initiated push is not enabled. Check your Parse app’s push notification settings. |
63 | | `RestPushDisabled` | 115 | REST-initiated push is not enabled. Check your Parse app’s push notification settings. |
64 | | `ClientPushWithURI` | 115 | Client-initiated push cannot use the “uri” option. |
65 | | `PushQueryOrPayloadTooLarge` | 115 | Your push query or data payload is too large. Check error message for more details. |
66 | | `InvalidExpirationError` | 138 | Invalid expiration value. |
67 | | `MissingPushIdError` | 156 | A push id is missing. Deprecated. |
68 | | `MissingDeviceTypeError` | 157 | The device type field is missing. Deprecated. |
69 |
70 | ## File 相关问题 {#file-related-errors}
71 |
72 | | Name | Code | Description |
73 | | :--- | :--- | :--- |
74 | | `InvalidFileName` | 122 | An invalid filename was used for Parse File. A valid file name contains only a-zA-Z0-9\_. characters and is between 1 and 128 characters. |
75 | | `MissingContentType` | 126 | Missing content type. |
76 | | `MissingContentLength` | 127 | Missing content length. |
77 | | `InvalidContentLength` | 128 | Invalid content length. |
78 | | `FileTooLarge` | 129 | File size exceeds maximum allowed. |
79 | | `FileSaveError` | 130 | Error saving a file. |
80 | | `FileDeleteError` | 131 | File could not be deleted. |
81 |
82 | ## Installation 相关问题 {#installation-related-errors}
83 |
84 | | Name | Code | Description |
85 | | :--- | :--- | :--- |
86 | | `InvalidInstallationIdError` | 132 | Invalid installation id. |
87 | | `InvalidDeviceTypeError` | 133 | Invalid device type. |
88 | | `InvalidChannelsArrayError` | 134 | Invalid channels array value. |
89 | | `MissingRequiredFieldError` | 135 | Required field is missing. |
90 | | `ChangedImmutableFieldError` | 136 | An immutable field was changed. |
91 |
92 | ## Purchase 相关问题 {#purchase-related-errors}
93 |
94 | | Name | Code | Description |
95 | | :--- | :--- | :--- |
96 | | `ReceiptMissing` | 143 | Product purchase receipt is missing. |
97 | | `InvalidPurchaseReceipt` | 144 | Product purchase receipt is invalid. |
98 | | `PaymentDisabled` | 145 | Payment is disabled on this device. |
99 | | `InvalidProductIdentifier` | 146 | The product identifier is invalid. |
100 | | `ProductNotFoundInAppStore` | 147 | The product is not found in the App Store. |
101 | | `InvalidServerResponse` | 148 | The Apple server response is not valid. |
102 | | `ProductDownloadFilesystemError` | 149 | The product fails to download due to file system error. |
103 |
104 | ## User 相关问题 {#user-related-errors}
105 |
106 | | Name | Code | Description |
107 | | :--- | :--- | :--- |
108 | | `UsernameMissing` | 200 | The username is missing or empty. |
109 | | `PasswordMissing` | 201 | The password is missing or empty. |
110 | | `UsernameTaken` | 202 | The username has already been taken. |
111 | | `UserEmailTaken` | 203 | Email has already been used. |
112 | | `UserEmailMissing` | 204 | The email is missing, and must be specified. |
113 | | `UserWithEmailNotFound` | 205 | A user with the specified email was not found. |
114 | | `SessionMissing` | 206 | A user object without a valid session could not be altered. |
115 | | `MustCreateUserThroughSignup` | 207 | A user can only be created through signup. |
116 | | `AccountAlreadyLinked` | 208 | An account being linked is already linked to another user. |
117 | | `InvalidSessionToken` | 209 | The device’s session token is no longer valid. The application should ask the user to log in again. |
118 |
119 | ## 第三方登录 {#linked-services-errors}
120 |
121 | | Name | Code | Description |
122 | | :--- | :--- | :--- |
123 | | `LinkedIdMissing` | 250 | A user cannot be linked to an account because that account’s id could not be found. |
124 | | `InvalidLinkedSession` | 251 | A user with a linked \(e.g. Facebook or Twitter\) account has an invalid session. Check error message for more details. |
125 | | `InvalidGeneralAuthData` | 251 | Invalid auth data value used. |
126 | | `BadAnonymousID` | 251 | Anonymous id is not a valid lowercase UUID. |
127 | | `FacebookBadToken` | 251 | The supplied Facebook session token is expired or invalid. |
128 | | `FacebookBadID` | 251 | A user with a linked Facebook account has an invalid session. |
129 | | `FacebookWrongAppID` | 251 | Unacceptable Facebook application id. |
130 | | `TwitterVerificationFailed` | 251 | Twitter credential verification failed. |
131 | | `TwitterWrongID` | 251 | Submitted Twitter id does not match the id associated with the submitted access token. |
132 | | `TwitterWrongScreenName` | 251 | Submitted Twitter handle does not match the handle associated with the submitted access token. |
133 | | `TwitterConnectFailure` | 251 | Twitter credentials could not be verified due to problems accessing the Twitter API. |
134 | | `UnsupportedService` | 252 | A service being linked \(e.g. Facebook or Twitter\) is unsupported. Check error message for more details. |
135 | | `UsernameSigninDisabled` | 252 | Authentication by username and password is not supported for this application. Check your Parse app’s authentication settings. |
136 | | `AnonymousSigninDisabled` | 252 | Anonymous users are not supported for this application. Check your Parse app’s authentication settings. |
137 | | `FacebookSigninDisabled` | 252 | Authentication by Facebook is not supported for this application. Check your Parse app’s authentication settings. |
138 | | `TwitterSigninDisabled` | 252 | Authentication by Twitter is not supported for this application. Check your Parse app’s authentication settings. |
139 | | `InvalidAuthDataError` | 253 | An invalid authData value was passed. Check error message for more details. |
140 | | `LinkingNotSupportedError` | 999 | Linking to an external account not supported yet with signup\_or\_login. Use update instead. |
141 |
142 | ## 客户端问题 {#client-only-errors}
143 |
144 | | Name | Code | Description |
145 | | :--- | :--- | :--- |
146 | | `ConnectionFailed` | 100 | The connection to the Parse servers failed. |
147 | | `AggregateError` | 600 | There were multiple errors. Aggregate errors have an “errors” property, which is an array of error objects with more detail about each error that occurred. |
148 | | `FileReadError` | 601 | Unable to read input for a Parse File on the client. |
149 | | `XDomainRequest` | 602 | A real error code is unavailable because we had to use an XDomainRequest object to allow CORS requests in Internet Explorer, which strips the body from HTTP responses that have a non-2XX status code. |
150 |
151 | ## 操作问题 {#operational-issues}
152 |
153 | | Name | Code | Description |
154 | | :--- | :--- | :--- |
155 | | `RequestTimeout` | 124 | The request was slow and timed out. Typically this indicates that the request is too expensive to run. You may see this when a Cloud function did not finish before timing out, or when a`Parse.Cloud.httpRequest`connection times out. |
156 | | `InefficientQueryError` | 154 | An inefficient query was rejected by the server. Refer to the Performance Guide and slow query log. |
157 | | `RequestLimitExceeded` | 155 | This application has exceeded its request limit \(legacy Parse.com apps only\). |
158 | | `TemporaryRejectionError` | 159 | An application’s requests are temporary rejected by the server \(legacy Parse.com apps only\). |
159 | | `DatabaseNotMigratedError` | 428 | You should migrate your database as soon as possible \(legacy Parse.com apps only\). |
160 |
161 | ## 其他问题 {#other-issues}
162 |
163 | | Name | Code | Description |
164 | | :--- | :--- | :--- |
165 | | `OtherCause` | -1 | An unknown error or an error unrelated to Parse occurred. |
166 | | `InternalServerError` | 1 | Internal server error. No information available. |
167 | | `ServiceUnavailable` | 2 | The service is currently unavailable. |
168 | | `ClientDisconnected` | 4 | Connection failure. |
169 |
170 |
171 |
172 |
--------------------------------------------------------------------------------
/error-handling.md:
--------------------------------------------------------------------------------
1 | # 错误处理
2 |
3 | Parse的大多数JavaScript函数会在回调中返回一个成功或失败的对象,类似于Backbone中的"options"对象。
4 |
5 | `success`和`error`这两个回调中,每当操作完成且没有错误,就会调用`seccess`在`save`或`get`中,`success`通常会是一个Parse.Object;在`find`中,会是一个Parse.Object数组。当与云端交互结束后出现了任何错误,就会调用`error`,`error`可能是连接云端的问题,也可能是执行请求操作的问题。
6 |
7 | 我们来看一个例子,在下面代码中,我们尝试去拉取一个云端不存在的对象,云端将会返回一个`error`,我们要根据`error`信息做相应的处理:
8 |
9 | ```js
10 | var query = new Parse.Query(Note);
11 | query.get("aBcDeFgH", {
12 | success: function(results) {
13 | // 不会被调用
14 | alert("Everything went fine!");
15 | },
16 | error: function(model, error) {
17 | // error是一个包含了错误信息的Parse.Error实例
18 | if (error.code === Parse.Error.OBJECT_NOT_FOUND) {
19 | alert("Uh oh, we couldn't find the object!");
20 | }
21 | }
22 | });
23 | ```
24 |
25 | 下面代码中的查询也会失败,因为设备没有连接到云端。我们需要增加一点代码来处理这个错误:
26 |
27 | ```js
28 | var query = new Parse.Query(Note);
29 | query.get("thisObjectIdDoesntExist", {
30 | success: function(results) {
31 | // 不会被调用
32 | alert("Everything went fine!");
33 | },
34 | error: function(model, error) {
35 | // error是一个包含了错误信息的Parse.Error实例
36 | if (error.code === Parse.Error.OBJECT_NOT_FOUND) {
37 | alert("Uh oh, we couldn't find the object!");
38 | } else if (error.code === Parse.Error.CONNECTION_FAILED) {
39 | alert("Uh oh, we couldn't even connect to the Parse Cloud!");
40 | }
41 | }
42 | });
43 | ```
44 |
45 | 对于像`save`和`signUp`这样会影响特定Parse.Object的方法,`error`方法的第一个参数将会是Parse.Object本身,第二个参数将会是Parse.Error对象。这是为了和类Backbone框架兼容。
46 |
47 | 要查看Parse.Eroor错误码列表,请查看错误码章节。
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/get-started.md:
--------------------------------------------------------------------------------
1 | # 开始
2 |
3 | Parse服务的后端搭建很简单,在本书的[GitHub](https://github.com/jaweii/Parse-JavaScript-translation)仓库中,有译者上传的配置好的ParseServer项目,下载下来安装对应的依赖后运行即可。
4 |
5 | 然后,在前端使用Parse的SDK即可与后端完成交互。
6 |
7 | ## 安装Parse服务
8 |
9 | 你可以照着[ParseServer文档](http://docs.parseplatform.org/parse-server/guide/) 自己搭建Parse服务,也可以直接下载[配置好的Parse服务项目](https://github.com/jaweii/Parse-JavaScript-translation),大概长这样:
10 |
11 | 
12 |
13 | 这个项目是基于官方的例子,集成了仪表盘和文件管理功能。
14 |
15 | 下载完这个项目后,安装依赖。你可以将目录移到合适的位置,因为它将作为我们的后端服务。
16 |
17 | 然后你还需要 [安装MongoDB](https://www.mongodb.com/download-center#community),安装完成后使用`mongod -dbpath "你想要保存数据库的路径"` 命令运行数据库服务,然后运行`mongo`命令测试能不能连接上:
18 |
19 | ```
20 | $ mongo
21 | MongoDB shell version: 3.2.9
22 | connecting to: test
23 | > ^C
24 | bye
25 | ```
26 |
27 | 如果连接成功,在项目目录运行 `npm run start`即可开启Parse后端服务。
28 |
29 | ```
30 | $ npm run start
31 |
32 | > parse-server-example@1.4.0 start C:\Users\Administrator\Desktop\Parse\parse-server-example-master
33 | > cross-env APP_ID='myAppId' CLIENT_KEY='123456' MASTER_KEY='123456' PORT=2018 SERVER_URL=http://localhost:2018/parse node index.js
34 |
35 | DATABASE_URI not specified, falling back to localhost.
36 | parse-server-example running on port 2018.
37 | ```
38 |
39 | 你可以通过访问 [http://localhost:2018/test](http://localhost:2018/test) 来测试是否运行成功。
40 |
41 | 如果运行成功,你可以访问[http://localhost:2018/dashboard](http://localhost:2018/dashboard) 进入仪表盘,进行可视化管理。
42 |
43 | 其中, [http://localhost:2018/parse](http://localhost:2018/parse) 就是我们的后端服务地址,在初始化SDK时我们将会用到。你也可以通过修改index.js的代码来修改服务地址和其他服务信息,也可以在package.json中修改环境变量来修改服务信息。
44 |
45 | ## 集成Parse SDK
46 |
47 | 集成Parse SDK到你的JavaScript项目中,最简单的方法是通过[npm](https://npmjs.org/parse)安装。
48 |
49 | 但如果你想使用Parse的预编译文件,你可以从[npm cdn](https://npmcdn.com/)获得。
50 |
51 | 同时,你可以从[https://npmcdn.com/parse/dist/parse.js](https://npmcdn.com/parse/dist/parse.js) 获得开发版本,从[https://npmcdn.com/parse/dist/parse.min.js ](https://npmcdn.com/parse/dist/parse.min.js)获得压缩后的生产版本。
52 |
53 | JavaScript的生态系统非常宽泛,它有着为数众多的框架和运行环境。为了适配各种情况,Parse的npm模块包含了为Node.js和React Native量身定制的SDK版本。最适合的才是最好的,所以,使用合适的npm包,可以确保诸如本地存储、用户会话、HTTP请求之类的项目使用合适的依赖。
54 |
55 | 在基于浏览器的应用中使用npm包:
56 |
57 | ```js
58 | var Parse = require('parse');
59 | ```
60 |
61 | 在后端应用或Node.js命令行工具中 :
62 |
63 | ```js
64 | // node.js
65 | var Parse = require('parse/node');
66 | ```
67 |
68 | 在React Native应用中 :
69 |
70 | ```js
71 | // React Native
72 | var Parse = require('parse/react-native');
73 | ```
74 |
75 | 接下来,用JavaScript初始化你的Parse-Sever,记得替换为你的应用信息:
76 |
77 | ```js
78 | Parse.initialize("你的appId","javascript key","master key");//参数2、3选填。
79 | Parse.serverURL = '你的后端服务地址'
80 | ```
81 |
82 | SDK 基于Backbone.js框架,它提供了灵活的API,允许你配置你喜欢的JS库。
83 |
84 | 目前SDK支持Firefox 23+,Chrome 17+,Safari 5+和IE 10。 IE 9仅支持使用HTTPS托管的应用程序。
85 |
86 | ## 例子
87 |
88 | 在此例子中,假设你已经搭建好ParseServer。
89 |
90 | 现在,我们使用Vue来做一个Todo List。
91 |
92 | 1、使用脚手架命令`vue init webpack todolist`创建一个vue项目。
93 |
94 | 2、安装Parse、UI依赖 ,集成到main.js。
95 |
96 | ```js
97 | // main.js
98 | import Vue from 'vue'
99 | import App from './App'
100 | import router from './router'
101 | import muse from 'muse-uI'
102 | import 'muse-ui/dist/muse-ui.css'
103 |
104 | Vue.use(muse)
105 |
106 | Vue.config.productionTip = false
107 |
108 | /* eslint-disable no-new */
109 | new Vue({
110 | el: '#app',
111 | router,
112 | components: { App },
113 | template: ''
114 | })
115 | ```
116 |
117 | 3、编写页面。
118 |
119 | ```js
120 | //app.vue
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
暂无任务
134 |
135 |
136 |
137 |
182 |
183 |
196 | ```
197 |
198 | 以上,是手写的所有代码。按惯例,上GIF:
199 |
200 | 
201 |
202 | 图中的数据都是同步保存到后端数据库的,我们从仪表盘的数据浏览器可以看到:
203 |
204 | 
205 |
206 | 你看,一个任务清单应用分分钟就完成了。
207 |
208 | 你可能对例子中SDK的用法不是很了解,如果你用过国内的leancloud、bmob等bass平台,你会发现他们超级相似;没有用过也没关系,本章之后的指南基本上都是讲SDK的使用。
209 |
210 | 另外,例子会上传到仓库中,有需要可以下载。
211 |
212 | 接下来,开始你的Parse之旅吧,有问题可以点击左上角“New Issue”发起讨论。
213 |
214 |
--------------------------------------------------------------------------------
/parseserver.md:
--------------------------------------------------------------------------------
1 | # ParseServer配置指南
2 |
3 | > 番外部分不属于原版指南的一部分,由译者译制添加,以提供完整的体验。
4 | >
5 | > ParseServer GitHub地址:[https://github.com/parse-community/Parse-Server](https://github.com/parse-community/Parse-Server)
6 |
7 | “开始”“一章有讲到,在本书GitHub有提供一个ParseServer脚手架项目,就是基于ParseServer + Express编写的,在本章会介绍更多关于ParseServer的配置,以满足开发者更多的需求。
8 |
9 | 比如,在“用户”一章有讲到,通过邮箱注册用户后,系统会自动发送一封邮件到指定邮箱,然后用户通过邮箱中的链接激活账号,但这是在使用Parse服务商的服务时才会自动发送邮件,自己搭建服务的话,这封邮件谁来发送呢?
10 |
11 | 这就要开发者自己来配置了。
12 |
13 | ### Parse Server + Express
14 |
15 | 你可以创建一个Parse实例后,挂载到Express站点上,就像刚才本书提供的ParseServer脚手架里面做的一样:
16 |
17 | ```js
18 | var express = require('express');
19 | var ParseServer = require('parse-server').ParseServer;
20 | var app = express();
21 |
22 | var api = new ParseServer({
23 | databaseURI: 'mongodb://localhost:27017/dev', // 你的Mongo数据库地址
24 | cloud: '/home/myApp/cloud/main.js', // 云代码文件的路径,绝对路径
25 | appId: 'myAppId', // appId
26 | masterKey: 'myMasterKey', // masterKey
27 | fileKey: 'optionalFileKey', // fileKey
28 | serverURL: 'http://localhost:1337/parse' // 设置Parse服务地址
29 | });
30 |
31 | // 在/parse URL 前缀上提供Parse服务
32 | app.use('/parse', api);
33 |
34 | app.listen(1337, function() {
35 | console.log('parse-server-example running on port 1337.');
36 | });
37 | ```
38 |
39 | 要查看所有可用的ParseServer 参数选项,可以运行`parse-server --help`查看。
40 |
41 | ## 配置
42 |
43 | ParseServer可以通过下面的选项来配置,你可以在创建ParseServer实例的时候传入它们,也可以配置在JSON文件中用命令行加载`parse-server path/to/configuration.json`。
44 |
45 | ### 基本选项
46 |
47 | * `appId`**\(必需\) **- 应用id。
48 | * `masterKey`**\(必需\) **- 用来无视所有ACL/CLP权限,你应该妥善保管它。
49 | * `databaseURI`**\(必需\) **- 你的数据库地址, i.e.`mongodb://user:pass@host.com/dbname`。如果你的密码有特殊字符,确保它 [URL encode 编码。](https://www.gitbook.com/book/jaweii/parse/edit#)
50 | * `port`- 端口,默认1337。
51 | * `serverURL`- ParseServer的URL,当云代码发送请求到ParseServer时会发送这个地址。
52 | * `cloud`- 云代码文件的路径地址,是绝对路径。
53 | * `push`- 配置APNS和GCM推送.参考[Push Notifications quick start](http://docs.parseplatform.org/parse-server/guide/#push-notifications_push-notifications-quick-start)。
54 |
55 | ### 客户端秘钥选项
56 |
57 | 下面密钥不是必需选项,如果设置了对应的选项,在对应的SDK初始化中需要传入你设置的秘钥。
58 |
59 | * `clientKey`
60 | * `javascriptKey`
61 | * `restAPIKey`
62 | * `dotNetKey`
63 |
64 | ### 高级选项
65 |
66 | * `allowClientClassCreation`- 设置为false后禁止客户端创建Class表,默认为true。
67 | * `enableAnonymousUsers`- 设置为false后禁用用户匿名功能,默认为true。
68 | * `auth`- 用来配置支持[3rd party authentication](http://docs.parseplatform.org/parse-server/guide/#oauth-and-3rd-party-authentication)。
69 | * `facebookAppIds`- Facebook应用Id数组。
70 | * `mountPath`- 指定服务挂载的路由. 默认为`/parse`
71 | * `filesAdapter`- 默认的文件管理可以通过适配器修改,参考[`FilesAdapter.js`](https://www.gitbook.com/book/jaweii/parse/edit#)。
72 | * `maxUploadSize`- 最大上传文件限制,默认20M。
73 | * `loggerAdapter`- 参考[`LoggerAdapter.js`](https://www.gitbook.com/book/jaweii/parse/edit#)。
74 | * `logLevel`- 设置你想要的日志记录级别,默认为`info`,查看[Winston logging levels](https://github.com/winstonjs/winston#logging-levels) 支持哪些值。
75 | * `sessionLength`- 设置session有效期,单位秒,默认31536000 seconds \(1 年\)。
76 | * `maxLimit`- 限制查询的最大返回数量,默认不限制。
77 | * `revokeSessionOnPasswordReset`- 当用户密码被重置,作废之前的session。
78 | * `accountLockout`- 当用户尝试修改其他用户的信息时,锁定用户账号。
79 | * `passwordPolicy`- 密码策略。
80 | * `customPages`- 与邮箱验证链接、密码重置链接、面向用户的页面地址哈希,可用值有:`parseFrameURL`,`invalidLink`,`choosePassword`,`passwordResetSucces`,`verifyEmailSuccess`.
81 | * `middleware`- \(CLI only\), 一个模块名,功能是express的中间件。 这个选项用来注入一个监听的中间件.
82 | * `masterKeyIps`- 一个IP数组,masterKey的使用将被限定在这个数组范围内,默认为空,不限制。如果使用了这个选项,确保使用云代码时,你的IP包含在内。
83 | * `readOnlyMasterKey`- 类似于masterKey,但是只有读权限,没有写权限。
84 |
85 | ## 邮箱验证和密码重置
86 |
87 | 使用email适配器,可以验证用户的邮箱地址和允许密码重置。下面以maigun为例,Mailgun适配器提供了发送邮件的功能,要使用这个,你需要注册Mailgun,并添加下面代码到你的项目:
88 |
89 | ```js
90 | var server = ParseServer({
91 | ...otherOptions,
92 | // 启用邮箱验证
93 | verifyUserEmails: true,
94 |
95 | // 如果 `verifyUserEmails` 为 `true` 并且
96 | // 如果 `emailVerifyTokenValidityDuration` 为 `undefined` 那么
97 | // email verify token 永不过期
98 | // 否则
99 | // email verify token 在`emailVerifyTokenValidityDuration`之后过期
100 | //
101 | // `emailVerifyTokenValidityDuration` 默认为 `undefined`
102 | //
103 | // 2小时候过期 (= 2 * 60 * 60 == 7200 seconds)
104 | emailVerifyTokenValidityDuration: 2 * 60 * 60, //单位秒 (2 hours = 7200 seconds)
105 |
106 | // 设置为false,允许用户未验证也能登录
107 | // 设置为true,则必须验证才能登录
108 | preventLoginWithUnverifiedEmail: false, // 默认 false
109 |
110 | // 你的应用的URL
111 | // 将会出现在验证邮箱和密码重置的链接中
112 | // 就像设置serverURL一样
113 | publicServerURL: 'https://example.com/parse',
114 | // 你的应用名,将作为邮件主题
115 | appName: 'Parse App',
116 | // email适配器
117 | emailAdapter: {
118 | module: '@parse/simple-mailgun-adapter',
119 | options: {
120 | // 设置发件地址
121 | fromAddress: 'parse@example.com',
122 | // 你的mailgun域名
123 | domain: 'example.com',
124 | // 你的mailgun apiKey
125 | apiKey: 'key-mykey',
126 | }
127 | },
128 |
129 | // 账户停用策略 (可选) - 默认为undefined
130 | /*
131 | 如果设置了帐户锁定策略,并且登录尝试次数超过了“阈值”次数,
132 | 则“login”api调用将返回错误代码“Parse.Error.OBJECT_NOT_FOUND”,
133 | 并显示错误消息“您的帐户由于多次登录失败而被锁定尝试。 请在<时间>分钟后再试一次。
134 | 在没有登录尝试的“持续时间”分钟之后,应用程序将允许用户再次尝试登录。
135 | */
136 | accountLockout: {
137 | duration: 5, // 账户锁定等待时长,等位分钟,设置为1-100000.
138 | threshold: 3, // 设置登录错误次数阈值,1-1000
139 | },
140 | // 可选的密码策略
141 | passwordPolicy: {
142 | //两个可选设置来执行强密码。 可以指定一个或两个。
143 | //如果两者都被指定,则两个检查都必须通过才能接受密码
144 | // 1.表示要执行的模式的RegExp对象或正则表达式字符串
145 | validatorPattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.{8,})/, // 最少8为,大小写和数字组成
146 | // 2. 在回调中使用验证方法验证
147 | validatorCallback: (password) => { return validatePassword(password) },
148 | doNotAllowUsername: true, // 是否允许账号密码相同
149 | maxPasswordAge: 90, // 设置密码过期天数
150 | maxPasswordHistory: 5, // 禁用前n个密码再次使用
151 | //设置密码重置链接超时时间
152 | resetTokenValidityDuration: 24*60*60, //24小时
153 | }
154 | });
155 | ```
156 |
157 | 你还可以安装来自社区的其他邮件适配器:
158 |
159 | * [parse-server-postmark-adapter](https://www.npmjs.com/package/parse-server-postmark-adapter)
160 | * [parse-server-sendgrid-adapter](https://www.npmjs.com/package/parse-server-sendgrid-adapter)
161 | * [parse-server-mandrill-adapter](https://www.npmjs.com/package/parse-server-mandrill-adapter)
162 | * [parse-server-simple-ses-adapter](https://www.npmjs.com/package/parse-server-simple-ses-adapter)
163 | * [parse-server-mailgun-adapter-template](https://www.npmjs.com/package/parse-server-mailgun-adapter-template)
164 | * [parse-server-sendinblue-adapter](https://www.npmjs.com/package/parse-server-sendinblue-adapter)
165 | * [parse-server-mailjet-adapter](https://www.npmjs.com/package/parse-server-mailjet-adapter)
166 | * [simple-parse-smtp-adapter](https://www.npmjs.com/package/simple-parse-smtp-adapter)
167 | * [parse-server-generic-email-adapter](https://www.npmjs.com/package/parse-server-generic-email-adapter)
168 |
169 | 在本书提供的ParseServer脚手架中已经配置好了邮件服务,使用的是译者专门编写的适配器parse-zh-email-adapter,你只需要换上你的SMTP账号就行了。
170 |
171 | 但是还有些地方要你自己修改,比如邮箱验证成功的页面:
172 |
173 | 
174 |
175 | 英文界面,对用户显然不够友好,如果要修改成理想的界面,编辑目录`./node_modules/parse-server/public_html/`下的模板即可。
176 |
177 | ## 日志
178 |
179 | ParseServer日志默认会:
180 |
181 | * 在控制台打印。
182 | * 将每天的日志保存到新文件中。
183 |
184 | 日志在Parse仪表盘中也可以查看。
185 |
186 | ### 记录每个请求
187 |
188 | 在运ParseServer服务时,将环境变量`VERBOSE`设置为'1'即可。比如:
189 |
190 | `VERBOSE='1' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
191 |
192 | ### 设置日志目录
193 |
194 | 将环境变量`PARSE_SERVER_LOGS_FOLDER`设置为新的路径即可:
195 |
196 | `PARSE_SERVER_LOGS_FOLDER='' parse-server --appId APPLICATION_ID --masterKey MASTER_KEY`
197 |
198 | ### 设置日志级别
199 |
200 | 通过环境变量`logLevel`设置:
201 |
202 | `parse-server --appId APPLICATION_ID --masterKey MASTER_KEY --logLevel LOG_LEVEL`
203 |
204 |
--------------------------------------------------------------------------------
/performance.md:
--------------------------------------------------------------------------------
1 | # 性能
2 |
3 | 随着你的应用规模增长,你将需要确保应用在负载和使用规模增长的情况下运行良好,本章将介绍如何将你的应用性能最优化。当你使用Parse服务快速构建你的原型时,你不需要考虑性能,但当你开始设计你的应用时,你很有必要考虑性能。我们强烈建议你在发布你的应用前,确保其遵循了我们的建议。
4 |
5 | 通过以下几点,可以提高你的应用性能:
6 |
7 | * 编写高效的查询
8 | * 编写受约束的查询
9 | * 使用客户端缓存
10 | * 使用云代码
11 | * 避免count查询
12 | * 使用高效的搜索技术
13 |
14 | 请将以上牢记心中。
15 |
16 | 下面我们来详细了解每一点。
17 |
18 | ## 编写高效的查询
19 |
20 | Parse对象被存储在数据库中,Parse查询将根据你编写的查询条件获取对象,为了避免每个查询都查看给定的class表的所有数据,可以为数据库使用索引,索引是指满足给定标准的项目排序列表,索引能有帮助,是因为索引允许数据库高效的查询,并在不需要查看所有数据的情况下返回匹配结果。索引的大小很小,所以可以在内存中使用,所以查找更快。
21 |
22 | ## 索引
23 |
24 | 在使用Parse服务时,你需要管理你的数据库并保持索引,如果你没有索引,每个查询将必须遍历表中每个数据才能返回匹配结果,反之,如果你有恰当的索引,遍历的数据将很少。
25 |
26 | 查询的约束的有效性排序如下:
27 |
28 | * 等于
29 | * 包含
30 | * 小于、小于等于、大于、大于等于
31 | * 匹配字符串开头
32 | * 不等于
33 | * 不包含
34 | * 其他
35 |
36 | 来看看下面的查询,获取`GameScore`对象:
37 |
38 | ```js
39 | var GameScore = Parse.Object.extend("GameScore");
40 | var query = new Parse.Query(GameScore);
41 | query.equalTo("score", 50);
42 | query.containedIn("playerName",
43 | ["Jonathan Walsh", "Dario Wunsch", "Shawn Simon"]);
44 | ```
45 |
46 | 在`score`字段上创建索引,会比在`playerName`上创建索引创造更小的搜索空间。
47 |
48 | 当检查数据类型时,布尔值的熵非常低,并且没有很好的索引,使用下面的查询约束:
49 |
50 | ```js
51 | query.equalTo("cheatMode", false);
52 | ```
53 |
54 | `cheatMode`可能的值有`true`和`false`,如果在`cheatMode`上建立索引,用处并不大,因为要返回查询结果,可能要遍历50%的对象。
55 |
56 | 数据类型根据键值空间的预期熵排序:
57 |
58 | * GeoPoints
59 | * Array
60 | * Pointer
61 | * Date
62 | * String
63 | * Number
64 | * Other
65 |
66 | 即使最好的索引策略也可能被不够好的查询打败。
67 |
68 | ## 高效的查询设计
69 |
70 | 编写高效的查询,意味着充分利用索引的好处。下面的查询约束,会使索引无效:
71 |
72 | * 不等于
73 | * 不包含
74 |
75 | 另外某些场景下,下面的查询,如果他们不能充分利用索引的好处,可能会造成查询相应变慢。
76 |
77 | * 正则表达式
78 | * 按某字段排序
79 |
80 | ### _不等于_
81 |
82 | 假设,你正在记录一个游戏中的`GameScore`表的高分,你想获取除了某个玩家以外的所有玩家分数,你可以编写这样的查询:
83 |
84 | ```js
85 | var GameScore = Parse.Object.extend("GameScore");
86 | var query = new Parse.Query(GameScore);
87 | query.notEqualTo("playerName", "Michael Yabuti");
88 | query.find().then(function(results) {
89 | // Retrieved scores successfully
90 | });
91 | ```
92 |
93 | 这个查询不能充分利用索引的好处,数据库必须对比`GameScore`表中的每一个对象,才能返回结果,随着class表的条目增长,查询时间会变长。你应该和其他字段匹配,而不是查询是否缺少值,这样才可以让数据库使用索引,查询会更快。
94 |
95 | 比如说,用户表有一个`state`字段,它可能的值有SignedUp、Verified和invited,下面查找所有最少使用过一次应用的用户的查询,是较慢的方式:
96 |
97 | ```js
98 | var query = new Parse.Query(Parse.User);
99 | query.notEqualTo("state", "Invited");
100 | ```
101 |
102 | 如果是给查询设置"包含条件"的约束,查询就会较快:
103 |
104 | ```js
105 | query.containedIn("state", ["SignedUp", "Verified"]);
106 | ```
107 |
108 | 有时候你应该重写你的查询。我们回到上一个`GameScore`的例子,假设我们要查询比指定玩家最高分高的玩家,我们可以做些不同的,先拿到指定玩家的最高分,然后执行下面查询:
109 |
110 | ```js
111 | var GameScore = Parse.Object.extend("GameScore");
112 | var query = new Parse.Query(GameScore);
113 | // 先拿到 Michael Yabuti 的highScore
114 | query.greaterThan("score", highScore);
115 | query.find().then(function(results) {
116 | // Retrieved scores successfully
117 | });
118 | ```
119 |
120 | 查询的重写取决于你的使用场景,这有时可能意味着要重新设计数据模型。
121 |
122 | ### _不包含_
123 |
124 | 类似于"不等于","不包含"查询约束不能使用索引,你应该尝试使用互补的“包含”查询约束。回到之前的用户例子,如果`state`字段还有一个“Blocked”值表示被封的用户,我们现在要查询活动的用户,下面这样是低效的:
125 |
126 | ```js
127 | var query = new Parse.Query(Parse.User);
128 | query.notContainedIn("state", ["Invited", "Blocked"]);
129 | ```
130 |
131 | 使用对应的“包含”查询约束是高效的:
132 |
133 | ```js
134 | query.containedIn("state", ["SignedUp", "Verified"]);
135 | ```
136 |
137 | 这意味着根据你的架构设置,你要相应的重写查询,甚至重写架构。
138 |
139 | ### _正则表达式_
140 |
141 | 处于性能考虑,应该尽量避免使用正则表达式,MongoDB对于字符串部分匹配的执行不是很高效。使用正则查询对性能的消耗是很高的,尤其是在表数据超过10万条后。你需要考虑在特定时间内限制多少这样的查询,你的应用才能正常运行。
142 |
143 | 正则表达式不支持索引,比如下面的查询,通过给定的`playerName`字段查找数据,字符串查询时不区分大小写的,并且不能被索引:
144 |
145 | ```js
146 | query.matches("playerName", "Michael", “i”);
147 | ```
148 |
149 | 下面的查询区分大小写,会查询每个相应字段包含了给定字符串的对象,并且不能被索引:
150 |
151 | ```js
152 | query.contains("playerName", "Michael");
153 | ```
154 |
155 | 上面两个查询都是低效的,事实上,`matches`和`contains`查询约束并没有包含在我们的查询指南中,我们也不推荐使用他们。根据你的使用场景,你应该切换到使用下面的查询约束,它可以使用索引:
156 |
157 | ```js
158 | query.startsWith("playerName", "Michael");
159 | ```
160 |
161 | 上面查询会根据给定的字符串查找数据,因为使用了所有,所有查询会更快。
162 |
163 | 作为最佳实践,当你使用正则表达式约束时,你要确保查询中的其他约束能将结果控制在数百个以内,以保证查询效率。如果你必须使用`matches`或`contains`约束,那么尽可能使用大小写敏感和锚查询,比如:
164 |
165 | ```js
166 | query.matches("playerName", "^Michael");
167 | ```
168 |
169 | 大多数场景下都会使用到包含正则表达式的搜索,稍候会详细介绍更高效的查询方法。
170 |
171 | ## 编写受约束的查询
172 |
173 | 编写受约束的查询可以保证只有客户端需要的数据被返回,这在数据使用受限和网络连接不可信的环境下非常重要。在查询章节介绍了你可以添加到查询的约束类型,用以限制返回的数据。增加约束时,你应专注设计高效的查询。
174 |
175 | 你可以使用skip和limit来获取需要的数据,查询条数限制默认为100条:
176 |
177 | ```js
178 | query.limit(10); // 限制条数10
179 | ```
180 |
181 | 如果你在GeoPoints上遇到问题,确保你指定了合理的半径:
182 |
183 | ```js
184 | var query = new Parse.Query(PlaceObject);
185 | query.withinMiles("location", userGeoPoint, 10.0);
186 | query.find().then(function(placesObjects) {
187 | // Get a list of objects within 10 miles of a user's location
188 | });
189 | ```
190 |
191 | 你可以使用select进一步的限制返回哪些字段:
192 |
193 | ```js
194 | var GameScore = Parse.Object.extend("GameScore");
195 | var query = new Parse.Query(GameScore);
196 | query.select("score", "playerName");
197 | query.find().then(function(results) {
198 | // each of results will only have the selected fields available.
199 | });
200 | ```
201 |
202 | ## 客户端缓存
203 |
204 | 对于IOS和Android应用,你可以开启查询缓存,详情查看[IOS](http://docs.parseplatform.org/ios/guide/#caching-queries)和[Android](http://docs.parseplatform.org/android/guide/#caching-queries)指南。查询缓存可以提高你的应用性能,尤其是在你想显示客户端从Parse最后一次拉取到的数据的时候。
205 |
206 | ## 使用云代码
207 |
208 | 云代码可以让你在Parse服务上运行JavaScript代码,而不是在客户端运行。
209 |
210 | 你可以使用云代码减少客户端逻辑,使应用性能感知上更好。你还可以在云代码中创建对象操作的触发器,这在验证数据和净化数据时很有用。你也可以使用云代码修改相关的对象,或启动其他处理,比如发送推送通知。
211 |
212 | 我们已经看过了通过编写受约束的查询限制返回数据,你也可以使用[云函数](http://docs.parseplatform.org/cloudcode/guide/#cloud-functions)来为你的应用限制返回的数据数量。在下面的例子中,我们定义了一个云函数来获取一部电影的平均分:
213 |
214 | ```js
215 | Parse.Cloud.define("averageStars", function(request, response) {
216 | var Review = Parse.Object.extend("Review");
217 | var query = new Parse.Query(Review);
218 | query.equalTo("movie", request.params.movie);
219 | query.find().then(function(results) {
220 | var sum = 0;
221 | for (var i = 0; i < results.length; ++i) {
222 | sum += results[i].get("stars");
223 | }
224 | response.success(sum / results.length);
225 | }, function(error) {
226 | response.error("movie lookup failed");
227 | });
228 | });
229 | ```
230 |
231 | 你也可以在客户端执行一个`Review`表的查询,只返回`stars`字段,并在客户端计算结果,随着一部电影的reviews(评论次数)增加,你可以看到返回到客户端的数量也在增加。
232 |
233 | 在你考虑优化你的查询时,你会发现你必须修改这个查询,有时候甚至是在你把应用提交到应用市场以后。不通过修改客户端来修改查询代码是可能的,如果你使用了云函数,你就是要重新设计你的架构,都可以在云代码中完成。
234 |
235 | 上面的查询平均分的云函数例子,在客户端可以像这样调用它:
236 |
237 | ```js
238 | Parse.Cloud.run("averageStars", { "movie": "The Matrix" }).then(function(ratings) {
239 | // ratings is 4.5
240 | });
241 | ```
242 |
243 | 如果以后你需要修改数据架构,你的客户端可以保持不变,只要你返回一个表示得分结果的数字就行了。
244 |
245 | ## 避免Count操作
246 |
247 | 当使用对象统计功能频繁时,可以考虑在数据库中增加一个统计变量,它会随着对象的增加而增加。然后,统计数据就可以通过简单的检索存储的变量来完成。
248 |
249 | 假设你要在你的应用中显示电影信息,并且你的数据模型是由`Movie`表和`Review`表组成,`Review`表包含了一个指向对象`Movie`对象的指针。你可能想在顶级导航统计视图显示每个电影的评论次数,查询类似这样:
250 |
251 | ```js
252 | var Review = Parse.Object.extend("Review");
253 | var query = new Parse.Query("Review");
254 | query.equalTo(“movie”, movie);
255 | query.count().then(function(count) {
256 | // Request succeeded
257 | });
258 | ```
259 |
260 | 如果你为每个UI元素执行这个查询请求,在大数据集中这会很低效。避免使用count操作的办法就是增加一个`reviews`字段到Movie表,用以表示评论次数,每当`Review`表新增一个对象,就给对应的`Movie`对象`reviews`增加计数。可以通过`afterSave`触发器完成:
261 |
262 | ```js
263 | Parse.Cloud.afterSave("Review", function(request) {
264 | // 获取movie对象id
265 | var movieId = request.object.get("movie").id;
266 | // 查询movie对象
267 | var Movie = Parse.Object.extend("Movie");
268 | var query = new Parse.Query(Movie);
269 | query.get(movieId).then(function(movie) {
270 | // 给reviews增加计数
271 | movie.increment("reviews");
272 | movie.save();
273 | }, function(error) {
274 | throw "Got an error " + error.code + " : " + error.message;
275 | });
276 | });
277 | ```
278 |
279 | 这样,就可以避免使用count,也能获取到统计信息了:
280 |
281 | ```js
282 | var Movie = Parse.Object.extend("Movie");
283 | var query = new Parse.Query(Movie);
284 | query.find().then(function(results) {
285 | // 结果中包含reviews统计字段
286 | }, function(error) {
287 | // Request failed
288 | });
289 | ```
290 |
291 | 你也可以使用单独的Parse对象来监测每个评论的数量,每当一个评论增加或减少,就通过`afterSave`或`afterDelete`触发器来做相应的计数增减。你可以根据你的使用场景来选择。
292 |
293 | ## 使用高效的查询
294 |
295 | 正如前文提高的,MongoDB对字符串部分匹配的查询支持不够好,但是,这是产品中实现可拓展的搜索功能的重要功能。
296 |
297 | 简单搜索算法只是扫描class表中的所有数据,并对每个条目进行查询。编写高效查询的关键,在于使用索引执行每个查询时,将必须被检查的数据量降到最低。你将需要用一种方便建立索引的构建你的数据模型,比如,随着数据集的增长,字符串查询将不能匹配不能被索引、会造成超时的字符串。
298 |
299 | 我们通过一个例子来了解如何构建一个高效的查询,你可以将这个例子中学到的概念应用到你的使用场景中。假设你的应用有用户发布文章,并且你想为应用提供标签搜索或关键词搜索的能力,你需要预处理你的文章,并在一个数组字段中保存标签或关键词。你可以在你的应用中在文章保存后处理,也可以使用`beforeSave`触发器在云函数中处理:
300 |
301 | ```js
302 | var _ = require("underscore");
303 | Parse.Cloud.beforeSave("Post", function(request, response) {
304 | var post = request.object;
305 | var toLowerCase = function(w) { return w.toLowerCase(); };
306 | var words = post.get("text").split(/\b/);
307 | words = _.map(words, toLowerCase);
308 | var stopWords = ["the", "in", "and"]
309 | words = _.filter(words, function(w) {
310 | return w.match(/^\w+$/) && ! _.contains(stopWords, w);
311 | });
312 | var hashtags = post.get("text").match(/#.+?\b/g);
313 | hashtags = _.map(hashtags, toLowerCase);
314 | post.set("words", words);
315 | post.set("hashtags", hashtags);
316 | response.success();
317 | });
318 | ```
319 |
320 | 上面代码会保存关键词和标签到数组字段,并会被MongoDB用多键索引。其中有几个要点需要说明。首先,所有的字符串都被转换成了小写,以便我们使用小写字母查询,并且还能大小写敏感;其次,其中过滤掉了注入"the","in","and"之类会出现在很多文章中的单词,在执行时会减少很多无用的扫描。
321 |
322 | 当你建立了关键词字段后,你就可以使用`containsAll`约束高效的查询了:
323 |
324 | ```js
325 | var Post = Parse.Object.extend("Post");
326 | var query = new Parse.Query(Post);
327 | query.containsAll("hashtags", [“#parse”, “#ftw”]);
328 | query.find().then(function(results) {
329 | // Request succeeded
330 | }, function(error) {
331 | // Request failed
332 | });
333 | ```
334 |
335 | ## 限额和其他考虑
336 |
337 | 为了让API以高性能的方式为你提供数据,在空间上会有一些限额。我们未来可能会调整这些\(不可能了\),请花点时间看看下面列表:
338 |
339 | ##### 对象
340 |
341 | * Parse对象被限制在128k以内。
342 | * 我们建议不要创建超过64个字段,以便我们为你的查询构建高校的索引。
343 | * 我们建议你的字段名吗不要超过1024个字符,否则这个字段将不会被建立索引。
344 |
345 | ##### 查询
346 |
347 | * 查询默认返回100个对象,使用limit方法可以修改。
348 | * skips和limits只可以被外部查询使用。
349 | * 相互冲突的约束将会导致只有一个约束被应用,比如说有两个`equalTo`约束应用在同一个字段和不同的值,那么只有一个会被应用。(可能你需要的是`containedIn`)。
350 | * 在复查查询or中没有地理位置查询。
351 | * 使用`$exists:false`是不明智的。
352 | * JavaScript SDK中的每一个查询方法都不能与使用地理位置约束的查询联合使用。
353 | * `containsAll`查询约束最多包含9个元素。
354 |
355 | ##### 推送通知
356 |
357 | * [Delivery of notifications is a “best effort”, not guaranteed](https://www.gitbook.com/book/jaweii/parse/edit#). 它不打算推送数据到你的应用,只是通知用户有新的数据可用。
358 |
359 | ##### 云代码
360 |
361 | * 传给云函数的params载荷被限制在50MB以内。
362 |
363 |
364 |
365 |
--------------------------------------------------------------------------------
/relation.md:
--------------------------------------------------------------------------------
1 | # 对象关系
2 |
3 | 对象关系有三种。一对一关系,即一个对象关联另一个对象的关系;一对多关系,即多个对象关联一个对象的关系;多对多关系,即多个对象中复杂的关系。
4 |
5 | 有4种方法在Parse中建立对象关系:
6 |
7 | ## 一对多关系
8 |
9 | 如果你想使用指针或数组来实现一对多的关系,有几个因素需要考虑。首先要考虑,这种关系中涉及多少个对象,如果子对象数量多余100个,推荐使用一对多的关系,如果少于100个,使用数组可能更方便,尤其是你想根据父对象一次获取所有子对象的时候。
10 |
11 | ### _使用指针_
12 |
13 | 假设我们有一个游戏应用,要跟踪玩家的分数和成就,在Parse中,我们可以把这些数据保存在Game对象中。如果这个游戏非常成功,所有玩家可能会存储几千个Game对象到系统中,对于这样的情况,子对象的数量可能任意大,指针就是最合适的选择。
14 |
15 | 要实现这样的游戏应用,我们要确保每个Game对象都关联一个用户对象,我们可以这样实现:
16 |
17 | ```js
18 | var game = new Parse.Object("Game");
19 | game.set("createdBy", Parse.User.current());
20 | ```
21 |
22 | 我们可以通过匹配用户对象来查询用户的所有Game对象:
23 |
24 | ```js
25 | var query = new Parse.Query("Game");
26 | query.equalTo("createdBy", Parse.User.current());
27 | ```
28 |
29 | 如果要获取Game对象的创建者,可以通过指针字段获取:
30 |
31 | ```js
32 | // Game对象
33 | var game = ...
34 |
35 | // Game对象创建者
36 | var user = game.get("createdBy");
37 | ```
38 |
39 | 大多数情况下,实现一对多的关系,使用指针都是最好的选择。
40 |
41 | ### _使用数组_
42 |
43 | 当我们已知子对象的数量很少时,使用数组是个好办法。`includeKey`参数提供一些便捷性,可以让你在获得父对象的同时获得所有的子对象。不过,如果子对象数量很大的话,会造成响应时间增长。
44 |
45 | 假设在我们的游戏中,要跟踪玩家在游戏中积累的武器,并且要限制武器的数量。我们已知武器的数量不大,并且需把用户的武器按顺序显示在屏幕中。在这个例子中,使用数组就是很好的方法。
46 |
47 | 现在让我们来为用户对象创建一个`weaponsList`\(武器列表\)字段,然后存储一些`Weapon`\(武器\)到`weaponsList`中:
48 |
49 | ```js
50 | // 假设我们有4个武器对象
51 | var scimitar = ...
52 | var plasmaRifle = ...
53 | var grenade = ...
54 | var bunnyRabbit = ...
55 |
56 | //插入武器到对象
57 | var weapons = [scimitar, plasmaRifle, grenade, bunnyRabbit];
58 |
59 | // 为用户设置weaponsList
60 | var user = Parse.User.current();
61 | user.set("weaponsList", weapons);
62 | ```
63 |
64 | 如果以后想再获得`Weapon`对象,只需要一行代码即可:
65 |
66 | ```js
67 | var weapons = Parse.User.current().get("weaponsList")
68 | ```
69 |
70 | 有时我们fetch一个对象,想要同时拉取父对象的子对象,只需要为`Parse.Query`添加`includeKey`即可:
71 |
72 | ```js
73 | // 用户对象的查询对象
74 | var userQuery = new Parse.Query(Parse.User);
75 |
76 | // configure any constraints on your query...
77 | // for example, you may want users who are also playing with or against you
78 |
79 | // 告诉查询对象想要同时拉取到哪个子对象
80 | userQuery.include("weaponsList");
81 |
82 | // 执行查询
83 | userQuery.find({
84 | success: function(results){
85 | // results contains all of the User objects, and their associated Weapon objects, too
86 | }
87 | });
88 | ```
89 |
90 | 你也可以通过子对象获取父对象,比如,我们要查询哪个用户具有给定的子对象,我们可以为查询对象增加一个这样的条件:
91 |
92 | ```js
93 | // 增加武器列表是否包含某武器的条件
94 | userQuery.equalTo("weaponsList", scimitar);
95 |
96 | // 或者传入武器数组查找包含多个武器的用户
97 | userQuery.containedIn("weaponsList", arrayOfWeapons);
98 | ```
99 |
100 | ---
101 |
102 | ## 多对多关系
103 |
104 | 现在我们来处理多对多关系。假设我们有一个阅读应用,我们需要构建`Book`对象和`Author`对象,一个作者可以写多本书,并且一本书可以有多个作者,这就是一个多对多关系的场景,你必须从数组、对象关系、指针三个方案中做出选择。
105 |
106 | 决策的关键点在于你是否有父子对象以外的信息要保存。
107 |
108 | 如果没有,使用关系对象或者使用数组是最简单的方案。通常,使用数组会产生更高的并发和较少的请求。
109 |
110 | 如果有,那么使用指针指向关联对象更合适。
111 |
112 | ### _使用Parse.Realtion_
113 |
114 | 使用Parse.Relation,我们可以在`Book`对象和一些`Author`对象中建立关联。你可以在数据浏览器中,在`Book`对象上创建一个relation类型的`authors`字段。然后,我们可以为Book对象关联一些author:
115 |
116 | ```js
117 | // 假设有这么几个author对象
118 | var authorOne = ...
119 | var authorTwo = ...
120 | var authorThree = ...
121 |
122 | // 构建book对象
123 | var book = new Parse.Object("Book");
124 |
125 | // 为book对象关联作者
126 | var relation = book.relation("authors");
127 | relation.add(authorOne);
128 | relation.add(authorTwo);
129 | relation.add(authorThree);
130 |
131 | // 保存
132 | book.save();
133 | ```
134 |
135 | 要获取一本书的作者列表,可以创建一个查询:
136 |
137 | ```js
138 | // 假设有一个book对象
139 | var book = ...
140 |
141 | // 通过relation方法获取relation对象
142 | var relation = book.relation("authors");
143 |
144 | // 通过relation对象构建查询对象
145 | var query = relation.query();
146 |
147 | // 执行查询即可
148 | ```
149 |
150 | 假如你想获取一个作者参与编辑过的书籍列表, 只需要为查询对象增加条件即可:
151 |
152 | ```js
153 | // 假设有一个作者对象
154 | var author = ...
155 |
156 | // 构建查询对象
157 | var query = new Parse.Query("Book");
158 |
159 | // 增加约束条件
160 | query.equalTo("authors", author);
161 | ```
162 |
163 | ### _使用指针_
164 |
165 | 假设我们要在用户间构建一个following\(我关注的\)/follower\(关注我的\)关系,一个用户可以关注多个用户,就像社交平台一样。在我们的应用中,我们不只需要知道谁关注了谁,还需要知道是什么时候关注的。要有序的记录这些数据,你必须创建一个独立的class表,这个表我们将命名为`Follow`,它有`form`字段和`to`字段,都是指向一个用户的指针。除了这些,你还可以添加一个`Date`类型的字段`date`用来记录时间。
166 |
167 | 现在,如果你想保存两个用户的关注信息,只需要在`Follow`表中设置`from`、`to`和`date`的值即可:
168 |
169 | ```js
170 | var otherUser = ...
171 |
172 | // 构建Follow对象
173 | var follow = new Parse.Object("Follow");
174 | follow.set("from", Parse.User.current());
175 | follow.set("to", otherUser);
176 | follow.set("date", Date());
177 | follow.save();
178 | ```
179 |
180 | 如果你想查找你关注的所有用户,可以创建一个`Follow`的查询对象:
181 |
182 | ```js
183 | var query = new Parse.Query("Follow");
184 | query.equalTo("from", Parse.User.current());
185 | query.find({
186 | success: function(users){
187 | ...
188 | }
189 | });
190 | ```
191 |
192 | 要查找关注我的所有用户也同样简单,通过`to`字段查找即可:
193 |
194 | ```js
195 | // 构建查询对象
196 | var query = new Parse.Query("Follow");
197 | query.equalTo("to", Parse.User.current());
198 | query.find({
199 | success: function(users){
200 | ...
201 | }
202 | });
203 | ```
204 |
205 | ### _使用数组_
206 |
207 | 在多对多关系中使用数组和在一对多关系中使用数组很像,关系一方的对象用一个数组字段包含另一方的一些对象。
208 |
209 | 还是假设我们有一个阅读应有,有`Book`和`Author`两个对象,`Book`对象有一个authors数组字段包含了`Book`的所有`Author`,数组在这里就非常合适,因为一本书的作者不会超过100个人。不过一个作者可能会写100本以上的书。
210 |
211 | 下面是我们保存`Book`和`Author`关系的示例:
212 |
213 | ```js
214 | // 假设有一个作者对象
215 | var author = ...
216 |
217 | // 假设有个Book对象
218 | var book = ...
219 |
220 | // 把author对象添加到authors数组中
221 | book.add("authors", author);
222 | ```
223 |
224 | 因为`authors`是一个数组,你应该在查询`Book`的时使用`includeKey`参数把`author`包含进来,以便服务器返回`Book`对象时同时包含作者:
225 |
226 | ```js
227 | // 构建Book查询对象
228 | var bookQuery = new Parse.Query("Book");
229 |
230 | // 告诉查询对象包含authors字段
231 | bookQuery.include("authors");
232 |
233 | // execute the query
234 | bookQuery.find({
235 | success: function(books){
236 | ...
237 | }
238 | });
239 | ```
240 |
241 | 要根据拿到的`Book`对象获取`authors`数组可以直接使用`get`方法:
242 |
243 | ```js
244 | var authorList = book.get("authors")
245 | ```
246 |
247 | 最后,如果你想根据一个用户对象,查询用户所有参与编辑过的书,直接使用`include`方法即可:
248 |
249 | ```js
250 | // 构建查询对象
251 | var bookQuery = new Parse.Query("Book");
252 |
253 | // 增加约束条件
254 | bookQuery.equalTo("authors", author);
255 |
256 | // 告诉查询对象要包含作者
257 | bookQuery.include("authors");
258 |
259 | // 执行查询
260 | bookQuery.find({
261 | success: function(books){
262 | ...
263 | }
264 | });
265 | ```
266 |
267 | ---
268 |
269 | ## 一对一关系
270 |
271 | 在某些情况下,要将一个对象分成两个对象,一对一关系就很合适。这种例子可能很少,但是也有如下两个例子:
272 |
273 | * **限制某些用户数据的可见**。在这个场景中,你要把对象一分为二,其中一部分数据对其他用户可见,关联的另一部分数据对其他用户不可见,并且被ACL保护。
274 | * **分割对象大小**。在这个场景中,你的原始对象大小超过了允许的128K,所以你决定创建第二个对象存储额外的数据。所以设计好你的数据模型很重要,可以避免对象过大要分割对象。如果你无法避免这样做,你可以考虑将大文件存储到Parse.File中。
275 |
276 | 最后,感谢你阅读了这么多,我们为这复杂程度向你致歉。虽然对象关系是比较难的部分,但是也有好的一面:
277 |
278 | 它可比人际关系简单多了。
279 |
280 |
--------------------------------------------------------------------------------
/security.md:
--------------------------------------------------------------------------------
1 | # 安全
2 |
3 | 随着应用的逐步完善,你会想使用Parse的安全功能来保护数据。本章就是介绍如何保证应用安全。
4 |
5 | 如果你的应有有潜在隐患,不只是开发者受到损失,你的用户也有可能受到损失。阅读本章,可以在你发布应用前,预防潜在隐患。
6 |
7 | ## 客户端 VS. 服务端
8 |
9 | 当你的应用首次连接云端时,它会通过App ID和client key(在JavaScript项目中也叫javascript key)来标识自己,这些都不是秘密,他们本身也不能保证应用安全。App ID和client key作为你应用的一部分,任何人都可以通过反编译或者网络监听来得到它们,尤其是在JavaScript中,只需要在浏览器中查看源文件就可以找到你的密钥。
10 |
11 | 这就是为什么Parse有很多安全功能来帮助你保护数据安全。
12 |
13 | client key在应用发布后就给了用户,所以任何可以用客户端密钥完成的事情都可以由公众,甚至是恶意的黑客来完成。
14 |
15 | 另一方面,master key定义了一种安全的机制。使用master key可以忽略应用的所有安全机制,比如表级别权限、ACL。master key就像服务器的root权限,所以你应该保管好你的master key,就像保管你的root密码一样。
16 |
17 | 你应该限制客户端的权限,把敏感的操作在云代码中用master key完成。你将在[在云代码中实现业务逻辑](#)一节学会使用云代码。
18 |
19 | 最后,建议在你服务器使用HTTPS和SSL,避免中间人攻击和运营商劫持,当然,不使用的话Parse也能正常工作。
20 |
21 | ## 表级别权限
22 |
23 | 第二个安全级别在于数据和结构级别, 执行这一级别的安全级别将会限制客户端对云端数据的访问。这在你首次使用Parse应用时就已经设置好了,以便你更高效的开发。举例:
24 |
25 | * 客户端可以在云端创建新的class表
26 | * 客户端可以给class表增加新字段
27 | * 客户端可以查询和修改云端的数据
28 |
29 | 你可以配置这些权限,应用到你应用的每个用户,或者指定的用户/角色。角色是指包含其他用户或其他角色的组,你可以将角色指派给一个对象来限制对象的使用。授权给角色的任何权限,也会同样的授权给角色的子角色和子用户,这使你可以为你的应用创建权限级别。
30 |
31 | 当你自信有了正确的class表和表关系,你应该参考下面的建议。
32 |
33 | 几乎所有class表的权限都应该做一定程度调整。对于所有对象都具有相同权限的表,表级别权限设置将会是最有效的,比如,某个场景中有个class表,里面的数据所有都可以读,但只有一个人可以写。
34 |
35 | ### _禁止创建class表_
36 |
37 | 在一开始,你就可以配置你的应用,使客户端无法在云端创建新class表,这在你的Parse服务配置里将`allowClientClassCreation`设置为`false`即可。一旦被设置后,就只能通过数据浏览器或者master key才能创建新的数据表,这可以阻止攻击者创建无数无意义的class表填充你的数据库。
38 |
39 | 你可以从ParseServerp配置指南中查阅Parse服务的配置。
40 |
41 | ### _配置表级别权限_
42 |
43 | Parse允许你为每个class表指定哪些操作是允许的,从而限制客户端访问或修改class表的方法。要修改这些设置,进入数据浏览器,选择class表,然后点击"Security"按钮。
44 |
45 | 你可以为选中的class表配置客户端执行以下操作的能力:
46 |
47 | * **Read**
48 | * **Get**:使用Get的权限,如果用户知道对象id,则可以通过id拉取对象。
49 | * **Find**:任何人使用查找权限可以查找表中的所有对象,无论他们是否知道对象id。任何表使用Find:Public权限后,将会对公众完全可读,除非你为表中对象设置了另外的ACL。
50 | * **Write**
51 | * **Update**: 任何人使用Update权限可以修改表中的任何对象字段,除非你为表中对象设置了另外的ACL。对于公开只读数据,比如游戏等级或资产,你应该为数据禁用Update权限。
52 | * **Create**: 和Update一样,任何人可以在class表中创建新对象,除非你为表中对象设置了另外的ACL。你可能也需要把这个权限禁用。
53 | * **Delete**: 使用这个权限,任何人只需要对象id,就可以删除表中的对象。除非你为表中对象设置了另外的ACL。
54 | * **Add Fields**:** **Parse表的式是根据创建的对象自建的,这在开发过程中很方便,你不需要在后端做任何修改就能增加新字段;但是,当你的应用上线后,还要通过客户端增加新字段是很少见的,所以你应该在你的应用发布后禁用此权限。
55 |
56 | 对于上面的每一个操作,你可以授权个每一个用户,也可以锁定权限给一组角色和用户。比如,一个class表要对所有用户可用,应该被设置只读,只启用get和find。一个日志表应该被设置为只写,只允许创建。要审核用户生成的内容,你应该提供update和delete权限给指定用户组和角色组。
57 |
58 | ## 对象级的权限控制
59 |
60 | 当你锁定了你的模型和表级权限后,就该思考用户如何访问数据了。对象级的权限控制可以让一个用户的数据和其他用户保持分离,因为有时不同的对象需要被不同的人访问。比如这个场景,一个用户的私人数据应该只有他自己能够访问。
61 |
62 | Parse还支持匿名用户的概念,所以你的应用可以保存和保护不需要登录的匿名用户的数据。
63 |
64 | 当一个用户登录你的应用后,Parse会为用户生成一个session,通过这个session,用户可以增加和修改他们的数据,但是不能修改其他人的数据。
65 |
66 | ### _访问控制列表_
67 |
68 | 要控制哪些人可以访问数据,最简单的办法就是使用访问控制列表,即ACL。ACL背后的思想就是为每一个对象增加一个具有某权限的用户/角色列表。一个用户需要某对象的读权限,或者这个用户在具有读权限的角色中,这个用户才可以拉取到对象的数据,同样的,一个用户具有对象的写权限,或者在具有写权限的角色中,才可以修改或删除对应的对象。
69 |
70 | 当你有了用户以后,你就可以开始使用ACL了。你要记住,用户可以通过传统用户名/密码方式注册,也可以使用Fackbook或twitter等第三方平台登录,甚至使用Parse的匿名用户功能。要为当前用户设置ACL,使其对公众不可读,你可以这样做:
71 |
72 | ```js
73 | var user = Parse.User.current();
74 | user.setACL(new Parse.ACL(user));
75 | ```
76 |
77 | 很多应用都会这样,如果你要保存敏感的用户信息,比如邮箱地址和电话号码,你需要为用户设置一个ACL,以便保护用户的私人数据不会被其他人拿到,如果一个对象没有ACL,那么所有人都可以读写这个对象的数据,用户对象除外。我们永远不会允许用户修改其他用户的数据,但是默认用户可以读取其他用户的数据。如果开发者有更新其他用户对象的需求,可以使用master key完成。
78 |
79 | 如果你想让用户的部分数据公开,另一部分数据不公开,最好的办法就是分割成两个对象,在公开的对象上增加一个指向私密对象的指针字段。
80 |
81 | ```js
82 | var privateData = Parse.Object.extend("PrivateUserData");
83 | privateData.setACL(new Parse.ACL(Parse.User.current()));
84 | privateData.set("phoneNumber", "555-5309");
85 |
86 | Parse.User.current().set("privateData", privateData);
87 | ```
88 |
89 | 当然,你也可以为对象设置不同的读写权限。比如,创建一个作者可写,公众可读的ACL:
90 |
91 | ```js
92 | var acl = new Parse.ACL();
93 | acl.setPublicReadAccess(true);
94 | acl.setWriteAccess(Parse.User.current().id, true);
95 | ```
96 |
97 | 有时基于每个用户的权限管理是不方便的,并且你想按特定权限给用户分组,就像设置管理员,使用角色功能就可以完成。角色是一种特殊的对象类型,它可以让你创建一个角色,通过ACL给角色指派权限。角色最方便的就是你无需为每个受角色约束的对象做修改,就可以为对象的用户权限做增删操作。要创建一个只有管理员可写的对象可以这样做:
98 |
99 | ```js
100 | var acl = new Parse.ACL();
101 | acl.setPublicReadAccess(true);
102 | acl.setRoleWriteAccess("admins", true);
103 | ```
104 |
105 | 当然,上面的代码是假设你已经创建了一个名为"admins"的角色,当你在开发应用时,你要创建一组特殊的角色,这将会很有用。角色还可以自由的创建和修改,比如这个场景,在两个用户建立连接后,为"friendOf\_\_"增加一个新的朋友。
106 |
107 | 这些仅仅是开始。应用可以通过ACL和表级权限执行所有种类复杂的权限控制。比如:
108 |
109 | * 对于私人数据,只有自己可以读写。
110 | * 消息面板上的文字,只有作者和管理员可以修改,公众只读。
111 | * 对于只有开发者通过master key才可以访问的日志数据,ACL可以拒绝所有权限。
112 | * 管理员创建的数据,比如每日消息,只有管理员可以修改,公众只读。
113 | * 一个用户发送给另一个用户的数据,只有他们可以读写。
114 |
115 | 下面是一个公众可读,自己可写的ACL格式:
116 |
117 | ```js
118 | {
119 | "*": { "read":true },
120 | "UserObjectId": { "read" :true, "write": true }
121 | }
122 | ```
123 |
124 | 下面是一个使用了角色的ACL格式:
125 |
126 | ```js
127 | {
128 | "role:RoleName": { "read": true },
129 | "UserObjectId": { "read": true, "write": true }
130 | }
131 | ```
132 |
133 |
134 |
135 | ### _指针类型权限_
136 |
137 | 指针类型是一种特殊的表级权限,它基于这些对象上的指针字段指向的用户,在class表中的每个对象上创建了一个虚拟ACL。比如,一个使用了`owner`字段的class表中,`owner`上设置了一个只读权限,class表中的每个对象将会对`owner`指向的用户只读。假设有一个有`sender`\(发送方\)和`receiver`\(接收方\)字段的class表,`receiver`字段上使用了只读权限,`sender`字段上使用了读写权限,那么`receiver`指向的用户对于这个表中所有的对象只读,`sender`指向的用户对于这个表中所有的用户可以读写。
138 |
139 | 需要注意的是这个ACL并没有真的在每个对象上创建,任何已经存在的ACL并不会因为你增删了指针类型权限而有任何修改,并且任何尝试和一个对象进行交互的用户都只能和这个对象进行交互,对象已经存在的ACL也会允许其操作。
140 |
141 | 有时将指针权限和ACL混合使用会造成混乱,所以我们推荐你使用指针权限就不要使用ACL。幸运的是,如果你决定使用云代码或者ACL来保护你的应用后,移除指针权限是很简单的。
142 |
143 | ##### _查询权限验证\(需要parse-server >= 2.3.0\)_
144 |
145 | 从2.3.0版本开始,parse-server引进新的表级权限\(CLP\)requiresAuthentication,CLP会阻止任何未经验证的用户执行CLP保护的操作。
146 |
147 | 比如,你想允许经过验证的用户从你的云端find和get `Announcement`,你需要设置此CLP:
148 |
149 | ```js
150 | // POST http://my-parse-server.com/schemas/Announcement
151 | // Set the X-Parse-Application-Id and X-Parse-Master-Key header
152 | // body:
153 | {
154 | classLevelPermissions:
155 | {
156 | "find": {
157 | "requiresAuthentication": true,
158 | "role:admin": true
159 | },
160 | "get": {
161 | "requiresAuthentication": true,
162 | "role:admin": true
163 | },
164 | "create": { "role:admin": true },
165 | "update": { "role:admin": true },
166 | "delete": { "role:admin": true }
167 | }
168 | }
169 | ```
170 |
171 | 效果:
172 |
173 | * 未经验证的用户将不能做任何事
174 | * 经验证的用户将可以读取这个class表的所有对象
175 | * 属于admin角色的用户将可以执行所有操作
176 |
177 | 提醒:如果你允许任何人登录你的应用,客户端仍然可以查找表中对象,以上将形同虚设。
178 |
179 | ### _CLP和ACL的相互影响_
180 |
181 | 表级权限\(CLP,Class Level Permmissions\)和访问控制列表\(ACL,Access Control List\)都是安全防护的好工具,但它们并不总是如你期望的那样交互。它们实际上代表两种不同安全层,每个请求必须通过每一层后才能返回正确的信息或做预期的修改。下图展示了这些层,一个在lass表层,一个在对象层,请求必须经过这两层验证。请注意,尽管指针权限和ACL很相似,但是指针权限是表级权限的一种,所以请求为了经过CLP验证必须先经过指针权限验证。就像你看到的,当你同时使用了CLP和ACL,判断是否授权用户发出的请求就会变得复杂。让我们通过一个例子来更好的理解CLP和ACL可以如何交互。假设我们有一个`Photo`表,并且有一个`photoObject`对象,我们的应用中有两个用户,`user1`和`user2`,现在我们在`Photo`表上设置Get CLP,禁止公众Get的请求,允许`user1`的Get请求,然后再在`photoObject`上设置ACL,只允许`user2`的Get请求。
182 |
183 | 你可能期望这会使`user1`和`user2`都能Get `PhotoObject`,但是因为CLP层和ACL层同时生效,所以`user1`和`user2`将都不能请求成功。`user1`的Get请求会通过CLP层,但是无法通过ACL层,同样的,`user2`可以通过ACL层,但是不能通过CLP层。
184 |
185 | 现在我们在来看一个指针权限的例子。假设我们有一个`Post`表,里面有一个`myPost`对象,在我们的应用中有两个用户,`poster`和`viewer`,现在我们要增加一个指针权限使`Creator`字段中的任何人都可以读写这个对象,并且对于`myPost`对象,`poster`是这个字段中的用户,我们还在这个对象上增加了ACL,使`viewer`具备只读权限。你可能期望这会允许`poster`可以读取和修改`myPost`,但是`viewer`将会被指针权限拒绝,`poster`将会被ACL拒绝,所以,两个用户都无法访问这个对象。
186 |
187 | 介于CLP、ACL和指针权限间的复杂影响,我们建议你同时使用时要小心对待。
188 |
189 | 使用CLP禁用某个类型的请求,然后其他类型的请求使用指针权限或者ACL来控制,这是经常被用到的。举个例子,你可能想禁用Photo表中的Delete权限,但另一方面又增加一个指针权限以便创建者可以编辑它,只是不允许删除它。因为指针权限和ACL之间的相互影响非常复杂,我们一般推荐你只用这两种安全机制中的一种。
190 |
191 | ### _安全边缘案例_
192 |
193 | Parse中有一些特殊的class表,他们不像其他class表一样完全遵循CLP或ACL的定义,这些例外被记录在了下表,其中"正常"表示CLP和ACL正常工作,还有一些其他信息介绍在脚注中。
194 |
195 | | 方法\表名 | `_User` | `_Installation` |
196 | | :--- | :--- | :--- |
197 | | Get | 正常 \[1, 2, 3\] | CLP无效 |
198 | | Find | 正常 \[3\] | 仅master key有效 \[6\] |
199 | | Create | 正常 \[4\] | CLP无效 |
200 | | Update | 正常 \[5\] | CLP无效 \[7\] |
201 | | Delete | 正常 \[5\] | 仅master key有效 \[7\] |
202 | | Add Field | 正常 | 正常 |
203 |
204 | 1. 登录或使用REST API `/parse/login` 行为在用户表上不遵循GET CLP,只有基于用户名/密码的登录正常工作,并且不能使用CLP禁用。
205 | 2. 拉取当前用户,或成为基于session token的用户,这两个都在REST API `/parse/users/me` 中,不遵循用户表的Get CLP 。
206 | 3. ACL 只读无法应用在已登录用户上,举例,如果所有用户都被ACL设置为禁止读操作,那么执行查询请求仍然会返回当前用户。不过,如果Find CLP被禁止,那么尝试在用户表执行find请求会返回错误。
207 |
208 | 4. Create CLP也会应用到用户注册上,所以禁止用户表上的Create CLP也会禁止用户注册,除非使用master key。
209 |
210 | 5. 用户数据的更新和删除只能由用户自己完成,公众的Delete/Update CLP仍然可用。比如说,如果你禁止用户表的公众修改权限,那么用户自己也不能编辑。不过用户对象的ACL写权限正常工作,用户仍然可以更新户删除自己的数据,并且用户不能更新或删除其他用户的数据。
211 |
212 | 6. installations表上的Get权限遵循ACL,Find权限在没有使用master key的情况下不被允许,除非你应用了`installtionId`作为约束条件。
213 |
214 | 7. installations表上的Update请求遵循ACL,但是Delete请求需要master key才可以。更多关于installtions工作的信息,请查看[http://docs.parseplatform.org/rest/guide/\#installations](http://docs.parseplatform.org/rest/guide/#installations)
215 |
216 | _云代码中的数据完整性_
217 |
218 | 对于大多数应用,指针权限、表级CLP、对象级ACL是保证你应用和用户数据安全的全部,但是有些时候,在一些边缘案例中还不足以保证安全,这种情况可以使用[云代码](http://docs.parseplatform.org/cloudcode/guide/)。
219 |
220 | 云代码允许你上传JavaScript代码到Parse服务器,并为你运行它,不同于运行在用户设备上的代码,存在被篡改的可能,云代码可以保证不会被篡改 ,所以它更可信,能负责更重要的功能。
221 |
222 | 云代码最常见的用例是阻止无效的数据被存储,这在防止客户端绕过验证逻辑时非常重要。
223 |
224 | 云代码允许你为你的class表创建一个`beforeSave`触发器,在对应class表中有对象被保存时将会运行这个触发器,并且你可以在这个触发器方法里修改对象或拒绝对象的保存操作,下面是[云函数beforeSave触发器](http://docs.parseplatform.org/cloudcode/guide/#beforesave-triggers)的创建方法,用以确保每个用户都设置了邮件地址:
225 |
226 | ```js
227 | Parse.Cloud.beforeSave(Parse.User, function(request, response) {
228 | var user = request.object;
229 | if (!user.get("email")) {
230 | response.error("Every user must have an email address.");
231 | } else {
232 | response.success();
233 | }
234 | });
235 | ```
236 |
237 | 云代码中使用验证方法可以锁定你的应用以确保数据都是可接受的,你也可以是使用`afterSave`触发器标准化你的数据\(比如格式化所有的电话号码或用户id\),你可以保留大部分从客户端直接访问Parse数据的生产力优势 ,还可以随时为你的数据执行某些不变量。
238 |
239 | 常用的验证场景包括:
240 |
241 | * 确认电话号码格式正确
242 | * 处理数据以使格式标准化
243 | * 确保邮箱地址正确
244 | * 要求每个用户设置指定范围内的年龄
245 | * 禁止用户直接修改计算字段
246 | * 禁止用户删除某个对象,除非用户满足某个条件
247 |
248 | ## 在云代码中实现业务逻辑 {#在-云代码中实现业务逻辑}
249 |
250 | 有时候有些操作可能是敏感的,需要尽可能的保证安全,这种情况下,你完全可以从客户端代码中删除这些逻辑,将所有的敏感操作都汇集到云代码功能中。
251 |
252 | 当一个云代码功能被调用,它可以使用`{useMasterKey:true}`选项来获得修改用户数据的能力,使用master key,你的云代码函数访问数据将可以无视任何ACL,这意味着它可以绕过所有的上一节讲到的安全机制。
253 |
254 | 假设你想允许用户给一个`Post`对象点赞,而又不用给用户对这个Post对象的写权限,你可以通过从客户端调用云代码函数来完成,而不是客户端直接修改`Post`对象本身。
255 |
256 | master key应该被小心使用,只对需要绕过安全机制的个别云代码函数设置`{useMasterKey:true}`:
257 |
258 | ```js
259 | Parse.Cloud.define("like", function(request, response) {
260 | var post = new Parse.Object("Post");
261 | post.id = request.params.postId;
262 | post.increment("likes");
263 | post.save(null, { useMasterKey: true }).then(function() {
264 | response.success();
265 | }, function(error) {
266 | response.error(error);
267 | });
268 | });
269 | ```
270 |
271 | 云代码最常用的一个用例就是发送一个推送通知给指定用户,通常情况下,客户端是没有权限直接发送推送通知的,因为客户端可能会修改通知内容,或发送给他们不应该发送的人。你的应用设置允许你设置是否禁用"client push",我们建议你禁用它。你应该写一个云代码函数,在发送推送前确认数据是要推送的数据。
272 |
273 | ## 总结
274 |
275 | Parse为你保护应用安全提供了多种方法,在你构建你的应用,和评估你要存储的数据时,你可以选择使用哪种方法实现。
276 |
277 | 值得再次说明的是,默认情况下用户对象对于所有其他用户对象,都是只读的。如果你想让用户的数据对其他用户不可见,你可以在用户对象上设置相应的ACL。
278 |
279 | 在很多Parse应用里,很多class表的安全防护是很简单的。对于完全公开的数据,你可以使用表级权限将整个表的权限锁定为公众可读,没人可写;对于完全私密的数据,你可以使用ACL确保只有对象所有者可以读取它。但有些时候,你可能即不想数据完全公开,又不想完全私密,比如你在朋友圈发的文字,只允许经过你设置可见的朋友可以看到,对于这样的情况,你需要结合本指南的技术,准确使用你希望的共享规则。
280 |
281 |
--------------------------------------------------------------------------------
/tui-song-tong-77e528-pushnotification.md:
--------------------------------------------------------------------------------
1 | # 推送通知
2 |
3 | 用于向IOS或Android应用推送通知。略。
4 |
5 |
--------------------------------------------------------------------------------
/yan-zheng-ma-deng-lu.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaweii/Parse-JavaScript-translation/94a510d0f37dad0edaf1bedc9480e0bb8df47817/yan-zheng-ma-deng-lu.md
--------------------------------------------------------------------------------
/教程例子TodoList.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jaweii/Parse-JavaScript-translation/94a510d0f37dad0edaf1bedc9480e0bb8df47817/教程例子TodoList.zip
--------------------------------------------------------------------------------