随着 Serverless 技术的出现,高可用、高并发和运维能力下沉到了 Serverless 平台底层,对开发者透明,开发者只需要关心自己的业务代码开发。原本门槛很高的后端业务开发变得触手可及,前端开发者更容易掌握后端业务开发。越来越多的开发者转型成为全栈开发者。当以一个全栈开发者视角重新审视前后端应用开发体验时,会发现传统开发体验有很大的提升空间。

Malagu 框架在设计之初就已经意识到了这一点:

  • 采用了对 Serverless、前后端一体化更为友好的语言 Typescript
  • 提供了前后端通用的 IoC 容器等基础设施
  • 抽象了前后端统一的渐进式解决方案:组件化方案
  • 前端像调用本地方法一样调用后端接口的 RPC 通信方式

前后端一体化开发优势

如果您不是全栈开发者,您仍然可以基于 Malagu 框架按照传统的前后端分离开发方式开发,与以前相比没有任何区别。如果您是一个全栈开发者,前后端一体化开发可能更加适合您。

Malagu 提供统一的开发语言、IoC 容器、工程化规范、命令行工具、渐进式方案,让您心智负担更小、项目更易维护、后端掌握的技能无缝应用到前端上。甚至前后端代码都可以在同一个项目中开发。

以前,我们只能针对单一的前端或者后端抽象通用代码;现在,我们可以基于前后端一起抽象通用代码,抽象的范围更大,进一步提升代码的复用能力。同时,也不存在前后端开发人员联调和沟通成本,而且更容易做全链路优化。

是否适合大型项目

对于小项目,采用前后端一体化开发,效率肯定是更高的,一个项目丢给一为全栈开发者开发,前后端联调、沟通成本都是省了。但是,对于大型项目,不可能让一个全栈开发者来开发,我们需要借助团队的力量。此时,您可能会觉得前后端一体化开发不适合大型项目。

但是,我们可以换一个角度来看,一个大型项目面对的问题往往是复杂的,我们应该从实现层面跳出来,从架构层面去寻找解决方案。我们可以把一个大型项目从业务视角拆解成许多个小项目,这些小项目再交由全栈开发者开发。

把一个复杂的问题分解成一个个简单的小问题,是一个更为可行的方案。让采用风险更低的渐进式地更新迭代应用的方案成为可能。

团队组织方式

在过去,前后端分工,按照技术类型纵向地组织团队;现在,前后端一体化开发,将一个大型应用从业务视角拆解成许多个微应用,按照业务功能横向地组织团队。

前后端一体化开发体验

以前端调用后端接口这样一个十分常见的场景为例,使用前后端一体化开发的流程是:

  1. 定义接口
  2. 后端实现接口
  3. 前端调用接口(像调用本地方法一样调用后端接口)

代码目录结构

  1. ├── src
  2. ├── browser 前端代码目录
  3. ├── module.ts 前端模块入口文件
  4. └── user.view.tsx 前端页面调用后端接口
  5. ├── common 公共代码目录
  6. ├── index.ts
  7. └── user-protocol.ts 接口定义
  8. └── node 后端代码目录
  9. ├── entity 数据库实体类定义文件夹
  10. ├── index.ts
  11. └── user.ts 用户实体类定义
  12. ├── module.ts 后端模块入口文件
  13. └── user-service.ts 后端实现接口

定义接口

  1. // src/common/user-protocol.ts
  2. export const UserService = Symbol('UserService');
  3. export interface UserService {
  4. list(): Promise<User[]>;
  5. get(id: number): Promise<User | undefined>;
  6. remove(id: number): Promise<void>
  7. modify(user: User): Promise<void>;
  8. create(user: User): Promise<User>;
  9. }
  10. export interface User {
  11. id: number;
  12. name: string;
  13. age: number;
  14. }

后端实现接口

  1. // src/node/user-service.ts
  2. import { Transactional, OrmContext } from '@malagu/typeorm/lib/node';
  3. import { Rpc } from '@malagu/rpc'
  4. import { User } from './entity';
  5. import { UserService } from '../common';
  6. @Rpc(UserService) // 相当于告诉框架,可以让前端通过 JSON RPC 的方式调用
  7. export class UserServiceImpl implements UserService {
  8. @Transactional({ readOnly: true }) // 开启只读事务
  9. list(): Promise<User[]> {
  10. const repo = OrmContext.getRepository(User);
  11. return repo.find();
  12. }
  13. @Transactional({ readOnly: true })
  14. get(id: number): Promise<User | undefined> {
  15. const repo = OrmContext.getRepository(User);
  16. return repo.findOne(id);
  17. }
  18. @Transactional() // 开启读写事务
  19. async remove(id: number): Promise<void> {
  20. const repo = OrmContext.getRepository(User);
  21. await repo.delete(id);
  22. }
  23. @Transactional()
  24. async modify(user: User): Promise<void> {
  25. const repo = OrmContext.getRepository(User);
  26. await repo.update(user.id, user);
  27. }
  28. @Transactional()
  29. create(user: User): Promise<User> {
  30. const repo = OrmContext.getRepository(User);
  31. return repo.save(user);
  32. }
  33. }

前端调用接口

  1. // src/browser/user.view.tsx
  2. import * as React from 'react';
  3. import { View } from '@malagu/react';
  4. import { RpcUtil } from '@malagu/rpc';
  5. import { DataTable } from 'grommet';
  6. import { UserService, User } from '../common';
  7. function Users() {
  8. const [ data, setData ] = React.useState<User[]>([]);
  9. React.useEffect(() => {
  10. // 通过 RpcUtil 获取接口 UserService 的远程调用代理对象
  11. const userService = RpcUtil.get<UserService>(UserService);
  12. // 像调用本地方法一样调用后端接口
  13. userService.list().then(users => setData(users))
  14. }, [])
  15. return (
  16. <DataTable
  17. margin={{ vertical: 'medium' }}
  18. columns={[
  19. {
  20. property: 'name',
  21. header: 'Name',
  22. primary: true
  23. },
  24. {
  25. property: 'age',
  26. header: 'Age'
  27. }
  28. ]}
  29. data={data}
  30. >
  31. </DataTable>
  32. );
  33. }
  34. @View({ component: Users})
  35. export default class {}