Parse JS · TypeScript 范型与类型安全示例

67次阅读
没有评论

共计 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.jsparse

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();

总结

一般项目里:

  1. 先为每个 Class 建立 Attrsclass extends Parse.Object<Attrs>
  2. registerSubclass 之后,所有 QuerybeforeSaveLiveQuery 都会得到智能提示与校验;
  3. 利用 aggregate<T>()distinct<T>() 明确后端返回结构;
  4. 结合表单校验库,保证类型与运行时数据双保险。

正文完
 0
一诺
版权声明:本站原创文章,由 一诺 于2025-10-18发表,共计7817字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)
验证码