背景
Malagu 是基于 TypeScript 的 Serverless First、可扩展和组件化的应用框架。
主要特点:
- 「Serverless First」,默认提供适配 Serverless 最佳实践,也支持运行在服务器上
- 「支持前后端一体化」,基于 JSON RPC,前端像调用本地方法一样调用后端方法
- 前后端「支持 rpc 和 mvc」两种通信形式,mvc 可以满足传统纯后端 rest 风格接口开发需要
- 「支持 typeorm」,事务 AOP 和事务传播行为,也可以集成其他的开源 ORM 框架
- 提供「类 spring security 的认证和授权」,现在已经集成了「开箱即用的 authing 平台」,实现了 oidc 认证和授权
- 「组件化」,框架本身也是基于组件化实现,将复杂大型项目拆解成一个个 Malagu 组件,提高了代码的复用能力、降低了代码维护难度
- 「依赖注入和 AOP」,抽象了一套前后端统一的依赖注入,依赖注入的体验与 spring 差不多
- 「命令行工具插件化」,默认提供初始化、运行、构建、部署能力,通过插件可以扩展命令行的能力
- 「零配置」,提供了一套默认行为,你也可以通过 Malagu 配置文件覆盖默认行为。
- 「支持适配任意前端框架」,目前集成了 React 前端框架,另外,UI 框架集成了 antd、md、grommet 等
简介
本文将使用 Malagu 框架介绍基于 Serverless 的前后端一体化微应用开发体验,当然基于传统服务器的前后端一体化微应用开发也是可行的,相较于 Serverless 来说,部署运维成本更高,资源利用率更低。前后单一体化,不仅仅是前后端使用同一种开发语言、同一套 IoC 方案、同一个项目开发等等,这里面其实包含了更多的丰富内涵和创新空间。接下来,我将向大家展示 Malagu 在前后端一体化的开发体验是怎么样的。
一、前期准备
安装 yarn 和 malagu cli 工具,其中 malagu cli 是辅助你开发、调试和发布基于 Malagu 框架开发的项目的命令行工具。因为 malagu 提供的模板,默认使用 yarn 管理依赖,所有也需要安装 yarn 工具。
npm install -g yarn
npm install -g @malagu/cli
二、初始化前后端一体化项目
我们可以使用 malagu init 命令快速基于模板创建初始化项目,命令行提供了一些列不同场景的内置模板,在这里,我们选择 fc-admin-app 这个前后端一体化的模板(支持模糊搜索模板)。
malagu init
初始化后的项目结构如下:
.
├── .github
│ └── workflows
│ └── deploy.yml 当你将代码提交到 Github 上,下次提交代码的时候,自动触发对于环境的 CI/CD
├── .gitignore
├── .malagu 与 Malagu 相关的配置文件、构建编译结果等等文件
│ ├── backend 后端相关的文件
│ │ ├── dist 后端构建编译输出的产物
│ │ │ ├── index.js
│ │ │ └── index.js.map
│ │ └── malagu.yml 该文件修改无效,框架把所有 Malagu 组件的配置文件进行拓扑排序并合并的通用配置和后端配置的最终配置文件
│ ├── bootstrap 当选择部署到阿里云函数计算平台,默认采用 Custom 运行时,bootstrap 为启动文件
│ └── frontend 前端相关的文件
│ ├── dist 前端构建编译输出的产物
│ │ ├── 0.c6ed55dba53a380668f5.js
│ │ ├── 1.5221182caa277536b162.js
│ │ ├── 10.859a880f04b6d20e3ee2.js
│ │ ├── 11.e4f1cf442ae521a66b25.js
│ │ ├── 2.fa57b5d2f3e9b8cef6fd.js
│ │ ├── 4.9a057e8d19275402be2a.js
│ │ ├── 5.f46d5a366fac8d44e16c.js
│ │ ├── 6.400e58dfb6324d61b6f0.js
│ │ ├── 7.68ede76a9cedea2f3ddf.js
│ │ ├── 8.80d19d1ade1c304f7311.js
│ │ ├── 9.f78098320e6470c6e3e3.js
│ │ ├── favicon.ico
│ │ ├── index.html
│ │ ├── main.104fd814ecc08b3a89d8.js
│ │ ├── service-worker.js
│ │ └── workbox-25caeab4.js
│ └── malagu.yml 该文件修改无效,框架把所有 Malagu 组件的配置文件进行拓扑排序并合并的通用配置和前端配置的最终配置文件
├── .vscode
│ └── launch.json 启动 vscode 单步调试的配置文件
├── README.md
├── favicon.ico 网站图标,网站入口文件 index.html 默认提供,你也可以在项目根下面自定义 index.html 文件
├── malagu-database.yml 可选,数据库相关配置
├── malagu-pre.yml 可选,预发环境配置
├── malagu-release.yml 可选,线上环境配置
├── malagu-test.yml 可选,测试环境配置
├── malagu.yml 可选,通用配置
├── package.json
├── src
│ ├── browser
│ │ ├── application-lifecycle.ts 前端应用生命周期钩子,此示例中主要用于初始化用户登录状态
│ │ ├── module.ts 前端模块入口文件
│ │ └── user.view.tsx 前端页面实现
│ ├── common
│ │ ├── index.ts
│ │ └── user-protocol.ts 接口定义
│ └── node
│ ├── entity 数据库实体类定义文件夹
│ │ ├── index.ts
│ │ └── user.ts 用户实体类定义
│ ├── module.ts 后端模块入口文件
│ └── user-service.ts
├── tsconfig.json
└── yarn.lock
说明:上面相关的 Malagu 配置默认都可以省略,模板默认提供三套环境:测试、预发、线上,供你选择使用,如果你不需要这么多环境删掉就好了。源码的目录组织不一定要按照上面的来,上面只是默认的组织方式而已。
三、本地运行项目
本地运行命令为 malagu serve,默认端口为 3000,可以通过 -p,--port 自定义端口,本地运行会自动带上 local 模式,也就是说默认会加载组件的 malagu-local.yml 文件,如果你有些配置只会在本地运行用到,你可以配置到 malagu-local.yml 文件里。
四、本地调试项目
大部分情况下,我们通过 console.log 就能解决遇到的很多问题。当然,我们也可以选在使用 vscode 自带的单步调试能力,使用 Malagu 自带的应用模板已经内置了 vscode 单步调试配置,你可以直接使用。前端调试也是类似,只不过你需要使用浏览器的调试工具而已,与传统前端项目并没有什么区别。
五、构建与部署项目
Malagu 框架本身是一个跨平台的应用框架,理论上你可以把它部署到任意云平台或者服务器上,目前 Malagu 已经适配了阿里云函数计算、腾讯云函数、国外的 Vercel 平台(以前叫 Zeit 平台)以及服务器。
下面以部署到阿里云函数计算为例。想要部署到阿里云函数计算上,首先你需要添加一个平台适配器组件:@malagu/fc-adapter(如果是腾讯云函数则是 @malagu/scf-adapter)。
如果你是首次使用阿里云或者函数计算,请先注册一个阿里云账号,并开通函数计算服务。
首次执行 malagu deploy 命令会提示输入账号 ID (必须是主账号) 和 AK(可以是主账号的 AK,也可以是子账号的) 等信息,Malagu 会使用你提供的信息进行部署。如果你使用子账号 AK,请确保子账号拥有函数计算的读写权限,以及 RAM 的 PassRole 权限。如果你以前使用过阿里云官方工具 Funcraft,Malagu 会复用 Funcraft 的账号和 AK 配置。账号信息也可以配置在配置文件中,如 malagu.yml 或者 .env。
Malagu 部署策略是一个应用对应着一个函数,对于服务和函数,框架提供了一套默认配置,你也可以通过配置文件自定义配置,后面会详细介绍。
构建与部署默认会带上 remote 默认,与 local 模式恰恰相反(一个是本地,一个是线上),也就是说默认会加载组件的 malagu-remotel.yml 文件,如果你有些配置只会在线上运行用到,你可以配置到 malagu-remote.yml 文件里。
六、组件配置
组件属性通过 yaml 文件来配置,默认在组件项目的根目录下加载 malagu.yml
属性文件,当 malagu.yml
配置了属性 mode: test
(支持指定多个 mode) ,则尝试加载根目录的 malagu-test.yml
属性文件,规则是: malagu-[mode].yml
。
配置方式如下:
- 公共属性文件:
malagu.yml
- 模式属性文件:
malagu-[mode].yml
支持表达式如下:
引用其他属性值
port: 3000
host: localhost
url: 'https://${host}:${port}'
引用环境变量值
password: ${env.PASSWORD}
默认值设置
password: '${env.PASSWORD?:123456}'
在代码中使用:
@Component()
export class A {
@Value('foo') // 引用组件属性 foo
protected readonly foo: string;
}
更多详细文档:组件属性。
七、部署配置
正如前面所说,Malagu 框架本身是一个跨平台的应用框架,往往我们会提供一个平台适配组件来适配对应的平台。开发平台适配组件遵循的是零配置原则,我们会提供一套默认的配置策略。当然,你也可以自定义策略来覆盖全部或者部分策略。不同平台的部署策略是不一样的,以函数计算平台为例,部署的时候,默认的服务名为 malagu,默认的函数名称为你 package.json 里面定义得项目名称,默认的运行时采用 Custom 等等。
自定义服务名:
deployConfig:
service:
name: xxxxx
自定义内存大小:
deployConfig:
function:
memorySize: 512
绑定自定义域名:
deployConfig:
customDomain:
name: demo.malagu.cellbang.com # 需要在你的域名解析配置对于的 CNAME 才能正常访问到你的函数,利润 CNAME 记录值:13048890.cn-hangzhou.fc.aliyuncs.com
更多详细文档:@malagu/fc-adapter 文档。
八、数据库操作
在 Malagu 框架中操作数据库很方便,我们已经为你集成了 TypeOrm 框架,能够很方便地操作各种关系型数据库,以及 MongoDB、Redis 等等,你只需要添加组件 @malagu/typeorm,并配置好数据库链接即可。
添加数据库组件
yarn add @malagu/typeorm # npm i @malagu/typeorm
配置数据库链接
支持多数据源配置,存在多个数据源的时候,需要为链接指定名称。数据库链接配置推荐使用单独的配置文件,比如 malagu-database.yml,并且不要提交到 git 仓库。当你需要使用 CI/CD 的时候,项目模板已经提供了一个默认的 Github Actions 配置,你只需要在你的 Github 上配置好数据库相关和云厂商 AK 的秘钥即可使用。
# malagu-database.yml
backend:
malagu:
typeorm:
ormConfig:
- type: "${ env.DE_TYPE ?: 'mysql'}"
host: "${ env.DB_HOST ?: 'localhost'}"
port: "${ env.DB_PORT ?: 3306}"
synchronize: true
username: "${ env.DB_USERNAME ?: 'root'}"
password: "${ env.DB_PASSWORD ?: 'root'}"
database: "${ env.DB_DATABASE ?: 'test'}"
操作数据库
import { Transactional, OrmContext } from '@malagu/typeorm/lib/node';
import { Rpc } from '@malagu/rpc'
import { User } from './entity';
import { UserService } from '../common';
@Rpc(UserService)
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);
}
}
更多详细文档:@malagu/typeorm 文档。
九、前后端一体化开发
前后端一体化开发主要分为三个步骤:定义接口 -> 后端实现接口 -> 前端调用接口(像调用本地方法一样调用后端接口)。
定义接口
// src/common/user-protocol.ts
export 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.ts
import { 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.tsx
import * 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 (
<DataTable
margin={{ vertical: 'medium' }}
columns={[
{
property: 'name',
header: 'Name',
primary: true
},
{
property: 'age',
header: 'Age'
}
]}
data={data}
>
</DataTable>
);
}
@View({ component: Users})
export default class {}
写在最后
Malagu 是一个 Serverless First 的应用开发框架,它可以用来开发前端应用、后端应用和前后端一体化应用。Malagu 提供了一种组件机制,组件机制具有可扩展、模块化等特性,而 Malagu 框架本身也是由一系列组件组成,你们可以很轻松地开发属于自己业务的组件,让组件可以在多个业务项目之间进行复用。