随着 Serverless 技术的出现,高可用、高并发和运维能力下沉到了 Serverless 平台底层,对开发者透明,开发者只需要关心自己的业务代码开发。原本门槛很高的后端业务开发变得触手可及,前端开发者更容易掌握后端业务开发。越来越多的开发者转型成为全栈开发者。当以一个全栈开发者视角重新审视前后端应用开发体验时,会发现传统开发体验有很大的提升空间。
Malagu 框架在设计之初就已经意识到了这一点:
- 采用了对 Serverless、前后端一体化更为友好的语言 Typescript
- 提供了前后端通用的 IoC 容器等基础设施
- 抽象了前后端统一的渐进式解决方案:组件化方案
- 前端像调用本地方法一样调用后端接口的 RPC 通信方式
前后端一体化开发优势
如果您不是全栈开发者,您仍然可以基于 Malagu 框架按照传统的前后端分离开发方式开发,与以前相比没有任何区别。如果您是一个全栈开发者,前后端一体化开发可能更加适合您。
Malagu 提供统一的开发语言、IoC 容器、工程化规范、命令行工具、渐进式方案,让您心智负担更小、项目更易维护、后端掌握的技能无缝应用到前端上。甚至前后端代码都可以在同一个项目中开发。
以前,我们只能针对单一的前端或者后端抽象通用代码;现在,我们可以基于前后端一起抽象通用代码,抽象的范围更大,进一步提升代码的复用能力。同时,也不存在前后端开发人员联调和沟通成本,而且更容易做全链路优化。
是否适合大型项目
对于小项目,采用前后端一体化开发,效率肯定是更高的,一个项目丢给一为全栈开发者开发,前后端联调、沟通成本都是省了。但是,对于大型项目,不可能让一个全栈开发者来开发,我们需要借助团队的力量。此时,您可能会觉得前后端一体化开发不适合大型项目。
但是,我们可以换一个角度来看,一个大型项目面对的问题往往是复杂的,我们应该从实现层面跳出来,从架构层面去寻找解决方案。我们可以把一个大型项目从业务视角拆解成许多个小项目,这些小项目再交由全栈开发者开发。
把一个复杂的问题分解成一个个简单的小问题,是一个更为可行的方案。让采用风险更低的渐进式地更新迭代应用的方案成为可能。
团队组织方式
在过去,前后端分工,按照技术类型纵向地组织团队;现在,前后端一体化开发,将一个大型应用从业务视角拆解成许多个微应用,按照业务功能横向地组织团队。
前后端一体化开发体验
以前端调用后端接口这样一个十分常见的场景为例,使用前后端一体化开发的流程是:
- 定义接口
- 后端实现接口
- 前端调用接口(像调用本地方法一样调用后端接口)
代码目录结构
├── src│ ├── browser 前端代码目录│ │ ├── module.ts 前端模块入口文件│ │ └── user.view.tsx 前端页面调用后端接口│ ├── common 公共代码目录│ │ ├── index.ts│ │ └── user-protocol.ts 接口定义│ └── node 后端代码目录│ ├── entity 数据库实体类定义文件夹│ │ ├── index.ts│ │ └── user.ts 用户实体类定义│ ├── module.ts 后端模块入口文件│ └── user-service.ts 后端实现接口
定义接口
// src/common/user-protocol.tsexport const UserService = Symbol('UserService');export interface UserService {list(): Promise<User[]>;get(id: number): Promise<User | undefined>;remove(id: number): Promise<void>modify(user: User): Promise<void>;create(user: User): Promise<User>;}export interface User {id: number;name: string;age: number;}
后端实现接口
// src/node/user-service.tsimport { Transactional, OrmContext } from '@malagu/typeorm/lib/node';import { Rpc } from '@malagu/rpc'import { User } from './entity';import { UserService } from '../common';@Rpc(UserService) // 相当于告诉框架,可以让前端通过 JSON RPC 的方式调用export class UserServiceImpl implements UserService {@Transactional({ readOnly: true }) // 开启只读事务list(): Promise<User[]> {const repo = OrmContext.getRepository(User);return repo.find();}@Transactional({ readOnly: true })get(id: number): Promise<User | undefined> {const repo = OrmContext.getRepository(User);return repo.findOne(id);}@Transactional() // 开启读写事务async remove(id: number): Promise<void> {const repo = OrmContext.getRepository(User);await repo.delete(id);}@Transactional()async modify(user: User): Promise<void> {const repo = OrmContext.getRepository(User);await repo.update(user.id, user);}@Transactional()create(user: User): Promise<User> {const repo = OrmContext.getRepository(User);return repo.save(user);}}
前端调用接口
// src/browser/user.view.tsximport * as React from 'react';import { View } from '@malagu/react';import { RpcUtil } from '@malagu/rpc';import { DataTable } from 'grommet';import { UserService, User } from '../common';function Users() {const [ data, setData ] = React.useState<User[]>([]);React.useEffect(() => {// 通过 RpcUtil 获取接口 UserService 的远程调用代理对象const userService = RpcUtil.get<UserService>(UserService);// 像调用本地方法一样调用后端接口userService.list().then(users => setData(users))}, [])return (<DataTablemargin={{ vertical: 'medium' }}columns={[{property: 'name',header: 'Name',primary: true},{property: 'age',header: 'Age'}]}data={data}></DataTable>);}@View({ component: Users})export default class {}
