|
2 | 2 |
|
3 | 3 | <div align="center"> |
4 | 4 |
|
5 | | -**轻量级、类型安全的 TypeScript 对象映射库** |
| 5 | +**TypeScript 对象映射库** |
6 | 6 |
|
7 | 7 | [](https://www.typescriptlang.org/) |
8 | 8 | [](LICENSE) |
|
12 | 12 |
|
13 | 13 | </div> |
14 | 14 |
|
15 | | -## 📖 简介 |
| 15 | +## 简介 |
16 | 16 |
|
17 | | -**Orika-JS** 是一个专为 TypeScript 设计的对象映射库,灵感来自 Java 的 Orika 框架。它帮助你在分层架构中优雅地处理不同对象模型之间的转换(PO/DO/DTO/VO)。 |
| 17 | +Orika-JS 是一个类型安全的对象映射库,用于简化不同数据模型之间的转换(Entity/DTO/VO)。灵感来自 Java 的 Orika 框架。 |
18 | 18 |
|
19 | | -### 为什么需要对象映射? |
| 19 | +## 特性 |
20 | 20 |
|
21 | | -现代软件架构中,分层设计是最佳实践。不同层级使用不同的对象模型: |
| 21 | +- **类型安全** - 完整的 TypeScript 类型推导和编译时检查 |
| 22 | +- **自动映射** - 同名字段自动映射,零配置即可使用 |
| 23 | +- **异步支持** - 支持异步转换器和并发控制 |
| 24 | +- **框架适配** - 提供 Vue 3 / React 适配器 |
| 25 | +- **轻量级** - 零运行时依赖,支持 Tree-shaking |
22 | 26 |
|
23 | | -``` |
24 | | -┌─────────────────┬────────────────────┬───────────────────┐ |
25 | | -│ 表现层 (API) │ 业务层 (Service) │ 持久层 (DB) │ |
26 | | -├─────────────────┼────────────────────┼───────────────────┤ |
27 | | -│ DTO/VO │ DO/BO │ PO/Entity │ |
28 | | -└─────────────────┴────────────────────┴───────────────────┘ |
29 | | - ↓ ↓ ↓ |
30 | | - 需要转换 需要转换 需要转换 |
31 | | -``` |
32 | | - |
33 | | -传统的手写转换代码存在诸多问题: |
34 | | -- ❌ 大量重复的样板代码 |
35 | | -- ❌ 字段遗漏导致的运行时错误 |
36 | | -- ❌ 模型变更后需要同步修改多处 |
37 | | -- ❌ 缺乏类型安全保障 |
38 | | - |
39 | | -**Orika-JS 采用声明式配置,一次定义,全局复用:** |
40 | | -- ✅ 完整的 TypeScript 类型推导 |
41 | | -- ✅ 约定优于配置(同名字段自动映射) |
42 | | -- ✅ 支持字段重命名、嵌套对象、自定义转换 |
43 | | -- ✅ 框架集成(Vue 3 / React) |
44 | | - |
45 | | -## ✨ 核心特性 |
46 | | - |
47 | | -| 特性 | 说明 | |
48 | | -|------|------| |
49 | | -| 🔒 **类型安全** | 完整的 TypeScript 泛型支持,编译时类型检查 | |
50 | | -| 🎯 **约定优于配置** | 同名字段自动映射,零配置即可使用 | |
51 | | -| ⚡️ **高性能** | 映射缓存、惰性求值、批量处理优化 | |
52 | | -| 🔄 **异步支持** | 原生支持异步转换器和并发控制 | |
53 | | -| 🎨 **灵活配置** | 字段重命名、条件映射、自定义转换器 | |
54 | | -| 🚀 **框架集成** | Vue 3 响应式 / React Hooks | |
55 | | -| 📦 **零依赖** | 核心库无运行时依赖,支持 Tree-shaking | |
56 | | - |
57 | | -## 📦 安装 |
| 27 | +## 安装 |
58 | 28 |
|
59 | 29 | ```bash |
60 | | -# 核心库(必需) |
| 30 | +# 核心库 |
61 | 31 | npm install @orika-js/core |
62 | 32 |
|
63 | | -# Vue 3 项目 |
| 33 | +# Vue 3 |
64 | 34 | npm install @orika-js/vue3 |
65 | 35 |
|
66 | | -# React 项目 |
| 36 | +# React |
67 | 37 | npm install @orika-js/react |
68 | 38 | ``` |
69 | 39 |
|
70 | | -## 🚀 快速开始 |
71 | | - |
72 | | -### 基础用法 |
73 | | - |
74 | | -**3 步完成对象映射:** |
| 40 | +## 快速开始 |
75 | 41 |
|
76 | 42 | ```typescript |
77 | 43 | import { createMapperBuilder, MapperFactory } from '@orika-js/core'; |
78 | 44 |
|
79 | | -// 1️⃣ 定义模型 |
| 45 | +// 定义模型 |
80 | 46 | class UserEntity { |
81 | 47 | id: number; |
82 | 48 | username: string; |
83 | 49 | password: string; |
84 | 50 | email: string; |
85 | | - createdAt: Date; |
86 | 51 | } |
87 | 52 |
|
88 | 53 | class UserDTO { |
89 | 54 | id: number; |
90 | | - displayName: string; // 字段重命名 |
91 | | - email: string; // 同名字段自动映射 |
| 55 | + displayName: string; |
| 56 | + email: string; |
92 | 57 | } |
93 | 58 |
|
94 | | -// 2️⃣ 配置映射(只需配置一次) |
| 59 | +// 配置映射 |
95 | 60 | createMapperBuilder<UserEntity, UserDTO>() |
96 | 61 | .from(UserEntity).to(UserDTO) |
97 | | - .mapField('username', 'displayName') // 字段重命名 |
98 | | - .exclude('password', 'createdAt') // 排除敏感字段 |
| 62 | + .mapField('username', 'displayName') |
| 63 | + .exclude('password') |
99 | 64 | .register(); |
100 | 65 |
|
101 | | -// 3️⃣ 执行映射 |
| 66 | +// 执行映射 |
102 | 67 | const factory = MapperFactory.getInstance(); |
103 | | -const entity = { |
104 | | - id: 1, |
105 | | - username: 'Alice', |
106 | | - password: 'secret', |
107 | | - email: 'alice@example.com', |
108 | | - createdAt: new Date() |
109 | | -}; |
110 | | - |
111 | | -const dto = factory.map(entity, UserEntity, UserDTO); |
112 | | -// 结果: { id: 1, displayName: 'Alice', email: 'alice@example.com' } |
113 | | -``` |
| 68 | +const dto = factory.map(userEntity, UserEntity, UserDTO); |
114 | 69 |
|
115 | | -### 高级特性 |
| 70 | +// 批量映射 |
| 71 | +const dtos = factory.mapArray(users, UserEntity, UserDTO); |
116 | 72 |
|
117 | | -```typescript |
118 | | -// 自定义转换逻辑 |
| 73 | +// 自定义转换 |
119 | 74 | createMapperBuilder<User, UserDTO>() |
120 | 75 | .from(User).to(UserDTO) |
121 | | - .forMember('age', (src) => 2024 - src.birthYear) |
122 | | - .forMember('fullName', (src) => `${src.firstName} ${src.lastName}`) |
123 | | - .register(); |
124 | | - |
125 | | -// 异步转换(如需要查询数据库) |
126 | | -createMapperBuilder<Post, PostDTO>() |
127 | | - .from(Post).to(PostDTO) |
128 | | - .forMemberAsync('author', async (src) => { |
129 | | - return await fetchUser(src.authorId); |
130 | | - }) |
| 76 | + .forMember('age', (src) => new Date().getFullYear() - src.birthYear) |
| 77 | + .forMemberAsync('author', async (src) => await fetchUser(src.authorId)) |
131 | 78 | .register(); |
132 | | - |
133 | | -// 批量映射 |
134 | | -const dtos = factory.mapArray(users, User, UserDTO); |
135 | | - |
136 | | -// 双向映射 |
137 | | -const { toB, toA } = factory.bidirectional(UserEntity, UserDTO); |
138 | | -const dto = toB(entity); |
139 | | -const entity2 = toA(dto); |
140 | 79 | ``` |
141 | 80 |
|
142 | | -## 🎨 框架集成 |
| 81 | +## 框架集成 |
143 | 82 |
|
144 | 83 | ### Vue 3 |
145 | 84 |
|
146 | | -`@orika-js/vue3` 提供完整的 Vue 3 响应式系统集成: |
147 | | - |
148 | 85 | ```typescript |
149 | | -import { useMapper, mapToReactive, mapToComputed } from '@orika-js/vue3'; |
| 86 | +import { useMapper, mapToReactive, mapToComputed, createPiniaMapperPlugin } from '@orika-js/vue3'; |
150 | 87 |
|
151 | | -// Composition API |
152 | 88 | const { map, mapArray } = useMapper(UserEntity, UserDTO); |
153 | | -const userDTO = map(userEntity); |
154 | | - |
155 | | -// 响应式映射 |
156 | 89 | const reactiveDTO = mapToReactive(user, User, UserDTO); |
| 90 | +const computedDTO = mapToComputed(userRef, User, UserDTO); |
157 | 91 |
|
158 | | -// 计算属性(自动追踪依赖) |
159 | | -const userRef = ref(user); |
160 | | -const userDTO = mapToComputed(userRef, User, UserDTO); |
161 | | -``` |
162 | | - |
163 | | -**Pinia Store 集成:** |
164 | | - |
165 | | -```typescript |
166 | | -import { createPiniaMapperPlugin } from '@orika-js/vue3'; |
167 | | - |
168 | | -const pinia = createPinia(); |
| 92 | +// Pinia |
169 | 93 | pinia.use(createPiniaMapperPlugin()); |
170 | | - |
171 | | -// 在 Store 中使用 |
172 | | -export const useUserStore = defineStore('user', () => { |
173 | | - const users = ref([]); |
174 | | - |
175 | | - async function fetchUsers() { |
176 | | - const data = await api.getUsers(); |
177 | | - users.value = this.$mapper.mapArray(data, UserEntity, UserDTO); |
178 | | - } |
179 | | - |
180 | | - return { users, fetchUsers }; |
181 | | -}); |
| 94 | +const users = this.$mapper.mapArray(data, UserEntity, UserDTO); |
182 | 95 | ``` |
183 | 96 |
|
184 | | -📚 [查看 Vue 3 完整文档](./packages/vue3) |
| 97 | +[完整文档](./packages/vue3) · [示例](./examples/vue3-app) |
185 | 98 |
|
186 | 99 | ### React |
187 | 100 |
|
188 | | -`@orika-js/react` 提供全面的 Hooks、组件和 HOC: |
189 | | - |
190 | 101 | ```typescript |
191 | | -import { useMapper, useMemoizedMapper, MapperProvider } from '@orika-js/react'; |
192 | | - |
193 | | -function App() { |
194 | | - return ( |
195 | | - <MapperProvider> |
196 | | - <UserProfile /> |
197 | | - </MapperProvider> |
198 | | - ); |
199 | | -} |
200 | | - |
201 | | -function UserProfile() { |
202 | | - const [user, setUser] = useState(userEntity); |
203 | | - |
204 | | - // 基础映射 Hook |
205 | | - const { map } = useMapper(UserEntity, UserDTO); |
206 | | - const dto = map(user); |
207 | | - |
208 | | - // 记忆化映射(自动缓存) |
209 | | - const memoizedDTO = useMemoizedMapper(user, UserEntity, UserDTO); |
210 | | - |
211 | | - return <div>{dto.displayName}</div>; |
212 | | -} |
213 | | -``` |
| 102 | +import { useMapper, useMemoizedMapper, MapperProvider, Mapper, withMapper } from '@orika-js/react'; |
214 | 103 |
|
215 | | -**声明式组件:** |
| 104 | +const { map } = useMapper(UserEntity, UserDTO); |
| 105 | +const dto = useMemoizedMapper(user, UserEntity, UserDTO); |
216 | 106 |
|
217 | | -```tsx |
| 107 | +// 组件 |
218 | 108 | <Mapper source={user} sourceClass={UserEntity} destClass={UserDTO}> |
219 | | - {(dto, isMapping, error) => ( |
220 | | - error ? <ErrorDisplay /> : |
221 | | - isMapping ? <Loading /> : |
222 | | - <UserProfile data={dto} /> |
223 | | - )} |
| 109 | + {(dto) => <div>{dto.displayName}</div>} |
224 | 110 | </Mapper> |
225 | | -``` |
226 | | - |
227 | | -**HOC 模式:** |
228 | | - |
229 | | -```typescript |
230 | | -const UserProfileWithMapper = withMapper({ |
231 | | - sourceClass: UserEntity, |
232 | | - destClass: UserDTO, |
233 | | - sourceProp: 'user', |
234 | | - destProp: 'userDTO' |
235 | | -})(UserProfile); |
236 | | -``` |
237 | | - |
238 | | -📚 [查看 React 完整文档](./packages/react) |
239 | | - |
240 | | -## 📚 包说明 |
241 | | - |
242 | | -| 包 | 版本 | 说明 | |
243 | | -|---|------|------| |
244 | | -| [@orika-js/core](./packages/core) |  | 核心映射引擎,零依赖 | |
245 | | -| [@orika-js/vue3](./packages/vue3) |  | Vue 3 适配器,支持响应式和 Pinia | |
246 | | -| [@orika-js/react](./packages/react) |  | React 适配器,提供 Hooks 和组件 | |
247 | | - |
248 | | -## 🎯 实际应用场景 |
249 | | - |
250 | | -### 场景 1: API 数据转换 |
251 | | - |
252 | | -```typescript |
253 | | -// API 响应 → DTO → 前端展示 |
254 | | -async function fetchUsers() { |
255 | | - const response = await fetch('/api/users'); |
256 | | - const rawData = await response.json(); |
257 | | - |
258 | | - // 自动排除敏感字段、格式化日期 |
259 | | - return factory.mapArray(rawData, UserEntity, UserDTO); |
260 | | -} |
261 | | -``` |
262 | 111 |
|
263 | | -### 场景 2: 表单提交 |
264 | | - |
265 | | -```typescript |
266 | | -// 表单数据 → 请求对象 → API |
267 | | -function submitForm(formData: UserFormData) { |
268 | | - const request = factory.map(formData, UserFormData, CreateUserRequest); |
269 | | - return api.createUser(request); |
270 | | -} |
| 112 | +// HOC |
| 113 | +const Enhanced = withMapper({ sourceClass: UserEntity, destClass: UserDTO })(Component); |
271 | 114 | ``` |
272 | 115 |
|
273 | | -### 场景 3: 分层架构 |
| 116 | +[完整文档](./packages/react) · [示例](./examples/react-demo) |
274 | 117 |
|
275 | | -``` |
276 | | -Controller (DTO) → Service (DO) → Repository (PO) → Database |
277 | | - ↓ ↓ ↓ |
278 | | - 用户请求 业务逻辑 数据持久化 |
279 | | -``` |
| 118 | +## 包说明 |
280 | 119 |
|
281 | | -每一层都使用适合的对象模型,通过 Orika-JS 自动转换。 |
| 120 | +| 包 | 说明 | |
| 121 | +|---|------| |
| 122 | +| [@orika-js/core](./packages/core) | 核心映射引擎,零依赖 | |
| 123 | +| [@orika-js/vue3](./packages/vue3) | Vue 3 适配器 | |
| 124 | +| [@orika-js/react](./packages/react) | React 适配器 | |
282 | 125 |
|
283 | | -## 🛠 开发 |
| 126 | +## 开发 |
284 | 127 |
|
285 | 128 | ```bash |
286 | | -# 安装依赖 |
287 | | -pnpm install |
288 | | - |
289 | | -# 构建所有包 |
290 | | -pnpm build |
291 | | - |
292 | | -# 开发模式(监听文件变化) |
293 | | -pnpm dev |
294 | | - |
295 | | -# 运行示例 |
296 | | -cd examples/vue3-app && pnpm dev |
297 | | -cd examples/react-demo && pnpm dev |
| 129 | +pnpm install # 安装 |
| 130 | +pnpm build # 构建 |
| 131 | +pnpm dev # 开发 |
298 | 132 | ``` |
299 | 133 |
|
300 | | -## 📖 示例 |
301 | | - |
302 | | -查看 [examples](./examples) 目录获取完整示例: |
303 | | - |
304 | | -- **基础示例** |
305 | | - - `01-basic.ts` - 基础映射 |
306 | | - - `02-async.ts` - 异步映射 |
307 | | - - `03-collections.ts` - 集合映射 |
308 | | - - `04-validation.ts` - 数据验证 |
309 | | - - `05-advanced.ts` - 高级特性 |
310 | | - |
311 | | -- **框架集成** |
312 | | - - `vue3-app/` - Vue 3 完整应用示例 |
313 | | - - `react-demo/` - React 应用示例 |
314 | | - |
315 | | -## 🤝 贡献 |
316 | | - |
317 | | -欢迎提交 Issue 和 Pull Request! |
318 | | - |
319 | | -## 📄 许可证 |
320 | | - |
321 | | -[MIT](./LICENSE) © [Steven Lee](https://github.com/stevenleep) |
| 134 | +查看 [examples](./examples) 目录获取更多示例。 |
322 | 135 |
|
323 | | -## 🔗 链接 |
| 136 | +## 许可证 |
324 | 137 |
|
325 | | -- [GitHub 仓库](https://github.com/stevenleep/orika-js) |
326 | | -- [问题反馈](https://github.com/stevenleep/orika-js/issues) |
327 | | -- [变更日志](./CHANGELOG.md) |
| 138 | +[MIT](./LICENSE) · [GitHub](https://github.com/stevenleep/orika-js) · [Issues](https://github.com/stevenleep/orika-js/issues) |
0 commit comments