共计 7817 个字符,预计需要花费 20 分钟才能阅读完成。
0. 准备工作
- 安装:
npm i parse tsconfig.json关键项:
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
- Node 端请使用
import Parse from 'parse/node.js';浏览器端用parse/dist/parse.min.js或parse。
1) 为 Parse.Object 建模(范型属性接口)
import Parse from 'parse'; // or 'parse/node.js'
// 1. 定义字段接口(Attributes)interface BookAttrs {
title: string;
price: number;
// 指向 Author 的 Pointer(更推荐用类,而不是裸 JSON)author?: Author | Parse.Pointer;
tags?: string[];
publishedAt?: Date;
}
// 2. 扩展 Parse.Object<T>
class Book extends Parse.Object<BookAttrs> {constructor() {super('Book'); }
// 可选:类型安全的访问器
get title() { return this.get('title'); }
set title(v: string) {this.set('title', v); }
}
// 3. 注册子类(便于 Query<Book> 获得类型)Parse.Object.registerSubclass('Book', Book);
// 4. Author 类
interface AuthorAttrs {name: string; isVIP?: boolean;}
class Author extends Parse.Object<AuthorAttrs> {constructor() {super('Author'); }
}
Parse.Object.registerSubclass('Author', Author);
说明:
Parse.Pointer在类型上可用Author | Parse.Pointer兼容;运行时传Author实例或{__type:'Pointer', className:'Author', objectId:'...'}均可。
2) 类型安全的 CRUD
// 新建并保存(自动推断 BookAttrs)const b = new Book();
b.set('title', 'Clean Code');
b.set('price', 88);
await b.save();
// 读取(get 返回 Book)const one = await new Parse.Query(Book).get('OBJECT_ID');
one.set('price', 99);
await one.save();
// 批量保存 / 删除(推断 Book | Author)const a = new Author(); a.set('name', 'Bob');
await Parse.Object.saveAll([b, a]);
await Parse.Object.destroyAll([b]);
3) Query 范型与结果类型
// 3.1 基本查询(Query<Book>)const q = new Parse.Query(Book);
q.equalTo('title', 'Clean Code');
q.greaterThan('price', 50);
q.include('author'); // 预取 author
const rows: Book[] = await q.find();
rows.forEach(book => {const author = book.get('author'); // Author | Parse.Pointer | undefined
if (author instanceof Author) {author.get('name'); // 类型安全
}
});
// 3.2 OR/AND 组合
const cheap = new Parse.Query(Book).lessThan('price', 50);
const onSale = new Parse.Query(Book).equalTo('tags', 'sale');
const orQuery = Parse.Query.or<Book>(cheap, onSale);
const result = await orQuery.find();
// 3.3 子查询(matchesQuery)const vipAuthorQ = new Parse.Query(Author).equalTo('isVIP', true);
const vipBookQ = new Parse.Query(Book).matchesQuery('author', vipAuthorQ);
const vipBooks = await vipBookQ.find();
// 3.4 只取字段(select)const light = await new Parse.Query(Book)
.select(['title', 'price'])
.limit(20)
.find();
// light 仍是 Book 实例,但未选字段访问可能返回 undefined
4) 关系(Pointer / Relation)的类型
// Pointer:直接 set Author 实例
const author = await new Author().set('name', 'Kent').save();
const book = new Book();
book.set('title', 'Patterns');
book.set('author', author); // 类型:Author | Parse.Pointer
await book.save();
// Relation:多对多
class Group extends Parse.Object<{name: string}> {constructor() {super('Group'); }
}
Parse.Object.registerSubclass('Group', Group);
const group = await new Group().set('name', 'Readers').save();
const rel: Parse.Relation<Author> = group.relation('members');
rel.add(author);
await group.save();
// 反向查询
const booksOfAuthor = await new Parse.Query(Book).equalTo('author', author).find();
5) 用户、角色与 ACL 的类型
// 注册 / 登录(Parse.User 自带类型)const u = new Parse.User();
u.set('username', 'alice');
u.set('password', 's3cret');
await u.signUp();
// ACL(对象级权限)const post = new Parse.Object<{title: string}>('Post');
post.set('title', 'Hello');
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setWriteAccess(u, true);
post.setACL(acl);
await post.save();
6) 文件(Parse.File)
const file = new Parse.File('avatar.png', { base64: base64Data});
await file.save();
const profile = new Parse.Object<{avatar?: Parse.File}>('Profile');
profile.set('avatar', file);
await profile.save();
const url: string | undefined = file.url();
7) Cloud Code(类型安全的云函数 / 触发器 /Job)
仅示意关键类型,放在
cloud/main.ts(经构建为 JS)
import Parse from 'parse/node.js';
// 7.1 类型安全的 Cloud Function
interface SumParams {a: number; b: number}
Parse.Cloud.define<SumParams, number>('sum', async (req) => {
// req: CloudFunctionRequest<SumParams>
const {a, b} = req.params;
if (typeof a !== 'number' || typeof b !== 'number') throw 'Invalid params';
// req.user / req.ip / req.installationId 可用
return a + b;
});
// 7.2 触发器(BeforeSave/AfterSave)Parse.Cloud.beforeSave<Book>(Book, async (req) => {
const book = req.object; // Book
if ((book.get('price') ?? 0) < 0) throw 'price must be >= 0';
});
Parse.Cloud.afterSave<Book>(Book, async (req) => {
const book = req.object; // Book
// 记录审计日志...
});
// 7.3 定时任务(Job)Parse.Cloud.job('rebuildStats', async (req) => {const q = new Parse.Query(Book).greaterThan('price', 50);
const rows = await q.find({useMasterKey: true}); // 选项具备类型提示
req.message(`processed: ${rows.length}`);
});
小贴士:Cloud Code 中不要使用
Parse.User.current(),而使用req.user或显式传入sessionToken。
8) 在客户端以类型安全方式调用云函数
// 泛型参数 <ReturnType>
const total = await Parse.Cloud.run<number>('sum', { a: 1, b: 2});
// 若需带 sessionToken(以某用户身份)const sessionToken = (await Parse.User.logIn('alice', 's3cret')).getSessionToken();
const totalAsUser = await Parse.Cloud.run<number>('sum', { a: 1, b: 2}, {sessionToken});
9) LiveQuery(订阅类型)
const q = new Parse.Query(Book).greaterThan('price', 0);
const sub: Parse.LiveQuerySubscription<Book> = await q.subscribe();
sub.on('create', (obj) => {
const b: Book = obj;
console.log('new:', b.get('title'));
});
sub.on('update', (obj) => {console.log('upd:', obj.get('price'));
});
sub.on('delete', (obj) => {console.log('del:', obj.id);
});
10) 聚合与 distinct(返回值建模)
// distinct(去重后的值是 unknown[] -> 指定为 string[] 更直观)const categories = await new Parse.Query(Book).distinct<string>('category');
// aggregate(Mongo 管道)——自定义结果类型
interface PriceStat {_id: string; avgPrice: number}
const pipeline = [
{$match: { price: { $gte: 50} } },
{$group: { _id: '$category', avgPrice: { $avg: '$price'} } }
];
const stats = await new Parse.Query(Book).aggregate<PriceStat>(pipeline);
11) 工具类型:帮助改进可读性
// 选取必填字段的构造器(带最小集校验)type RequiredAttrs<T, K extends keyof T> = Required<Pick<T, K>> & Partial<T>;
function createBook(attrs: RequiredAttrs<BookAttrs, 'title'|'price'>) {const b = new Book();
(Object.keys(attrs) as (keyof BookAttrs)[]).forEach(k => b.set(k, attrs[k]!));
return b;
}
// 使用
await createBook({title: 'DDD', price: 120, tags: ['design'] }).save();
12) 与表单 / 校验库配合(Zod 例)
import {z} from 'zod';
const BookSchema = z.object({title: z.string().min(1),
price: z.number().nonnegative(),
tags: z.array(z.string()).optional(),});
type BookInput = z.infer<typeof BookSchema>;
async function createBookSafe(input: BookInput) {const data = BookSchema.parse(input);
const b = new Book();
b.set('title', data.title);
b.set('price', data.price);
if (data.tags) b.set('tags', data.tags);
return b.save();}
13) Next.js / React 下的类型组织
// libs/parse.ts
import Parse from 'parse/dist/parse.min.js';
export function initParse() {if ((Parse as any)._initialized) return Parse;
Parse.initialize(import.meta.env.VITE_PARSE_APP_ID);
Parse.serverURL = import.meta.env.VITE_PARSE_SERVER_URL;
(Parse as any)._initialized = true;
return Parse;
}
// 组件中
import {useEffect, useState} from 'react';
import {initParse} from '@/libs/parse';
export function BookList() {const [list, setList] = useState<BookAttrs[]>([]);
useEffect(() => {const Parse = initParse();
(async () => {const rows = await new Parse.Query(Book).limit(20).find();
setList(rows.map(r => r.toJSON() as BookAttrs));
})();}, []);
// ...
}
14) 权限相关:类型化选项对象
// find / save / destroy 均可带选项
await new Parse.Query(Book).find({sessionToken: '...'});
await new Book().save(null, { useMasterKey: true});
await new Book().destroy({ useMasterKey: true});
15) 单元测试(Vitest/Jest)中的类型安全
import {describe, expect, it, beforeAll} from 'vitest';
import Parse from 'parse/node.js';
beforeAll(() => {Parse.initialize(process.env.APP_ID!);
Parse.serverURL = process.env.SERVER_URL!;
Parse.masterKey = process.env.MASTER_KEY!;
});
describe('Book', () => {it('should create book', async () => {const b = await new Book().set('title', 'T').set('price', 1).save();
expect(b instanceof Book).toBe(true);
expect(b.get('price')).toBe(1);
});
});
16) 小抄(Cheat Sheet · TS)
// 类与注册
declare class Book extends Parse.Object<BookAttrs> {}
Parse.Object.registerSubclass('Book', Book);
// Query<Book>
const q = new Parse.Query(Book);
const rows: Book[] = await q.find();
// 指针与关系
book.set('author', author); // Author | Parse.Pointer
const rel: Parse.Relation<Author> = group.relation('members');
// Cloud Function 类型
Parse.Cloud.define<Params, Return>('fn', (req) => {/*...*/});
// LiveQuery
const sub: Parse.LiveQuerySubscription<Book> = await q.subscribe();
总结
一般项目里:
- 先为每个 Class 建立
Attrs与class extends Parse.Object<Attrs>; registerSubclass之后,所有Query、beforeSave、LiveQuery都会得到智能提示与校验;- 利用
aggregate<T>()、distinct<T>()明确后端返回结构; - 结合表单校验库,保证类型与运行时数据双保险。
正文完

