2020年春节回东北的路上凌晨四点多诞生了一个想法,然后就开始吭哧坑次设计开发,基于我以前开发的 ServiceFramework 框架,一个春节假期就迭代开发出了一套名为 web-platform 框架。 该框架包含三个部分:
我们分别简要介绍下1,2 两个层面的东西。
对于1, 我们的理论基础是,Web交互的基础形态就两个: Form 和 Table。 Form 可以让人告诉机器干什么, Table 则是机器告诉人他执行的结果。 Web上其他丰富多彩的交互形态,本质上都是这两者的变形或者组合。通过组合多个 Form 形成 Form 向导,我们可以帮助用户完成一个非常复杂的交互需求。
比如,对于一个插件商店,我们有两个核心流程:查看下载和上传。
入口:
查看检索插件,并获得下载地址:
上传自定义插件:
和 ToC 用户不同的是,对于 ToB 用户,帮其完成工作是核心。所以基础的 Form/Table 打造的足够完善,其实已经完全可以满足用户诉求。
以 Form/Table 为核心交互元素的通用 Web UI 界面带来的价值在于,你无需开发繁琐的前端, 仅需要开发 API 即可。在前面的示例图中,我们无需开发一行前端代码。
对于2, 我认为 Web 开发者过于陶醉于 Rest 带来的丰富语义(比如 POST/GET/PUT/DELETE 等对资源的语义),然而这套语义除了给我们感官上的享受,并没有带来额外价值。同理,现在流行的 Json进Json出似乎带来了统一,但我也认为是程序员的自hign, 他并没有到来额外的效率价值。 整个 Rest 抽象的语义,99%的需求,应该就是一个普通的函数,这个函数的输入是一个 Map[String,String]
, 输出是一个 String
,用户根据输入完成对应的方法逻辑即可。同样的,对于复杂痛苦的数据库操作,我们可以借助 Scala 等语言的编译时能力,对case class进行操作,系统在编译的时候自动将这些操作转化为原生的SQL,实现运行时 0 开销。
下面是我开发的一个Action, 它遵循了我上面提到的规范,整个类很简单,开发者不用学习任何Rest相关的只是,只是实现一个 _run
函数而已。而且每个Action也可以互相调用。
另外值得注意的事,我们扩展了参数的定义能力,他不仅仅可以定义成参数名字,还能做前端形态的定义,比如 pageNum 是个输入框,pluginType 是个选择框,前端会根据这些信息,绘制合理的 Form。
基本理念讲完,然我们开始使用 web-platform的开发之旅吧。
在此基础上,安装一个命令行工具 sfcli(以及手动安装相关依赖):
pip install watchdog requests click uuid sfcli
在开发代码之前,我们初始化项目,增加依赖,修改数据库连接配置,以及启动应用进行基本的校验。
通过下面的命令可以创建一个模板项目:
sfcli create --name byzer-extension-market
打开项目根目录下的的pom文件,添加一个依赖:
<dependency>
<groupId>tech.mlsql.serviceframework.baseweb</groupId>
<artifactId>ar_runtime_web_console-lib_${scala.binary.version}</artifactId>
<version>1.0.3</version>
</dependency>
修改数据库连接文件(位于项目根目录下的 config/application.yml):
development:
datasources:
mysql:
host: 127.0.0.1
port: 3306
database: basic_app_runtime
username: xxxx
password: xxxxx
initialSize: 8
disable: false
removeAbandoned: true
testWhileIdle: true
removeAbandonedTimeout: 30
maxWait: 100
filters: stat,log4j
mongodb:
disable: true
redis:
disable: true
找到启动类 tech.mlsql.app_runtime.plugin.App
,新增三个三个插件,他们分别对应数据库支持,用户系统支持,以及 API master 支持。
new DBPluginDesc,
new UserPluginDesc,
new ConsolePluginDesc,
修改后的结果如下:
通过 IDE 启动该项目,默认端口为 9007, 访问地址 http://127.0.0.1:9007/web/
可以得到如下界面:
点击 API LIST
, 找打一个叫 hello_world
的 API
点击进去,随意输入:
现在整个项目已经Run起来了,我们可以着手开发我们要的功能了。
使用 web-platform 框架做开发,我们要关注【面向用户的 API 设计】 而不是面向 【 应用的API设计 】,两者的区别在于,面向用户的API设计,首先是考虑用户使用你的API希望能否走通核心流程,而面向应用的API设计一般都是为了方便程序调用的。
APIs Master 提供了 Form 作为基本的人机交互手段,提供了 Table 作为结果展示,提供了 Nav 向导组织多个 API 完成一个具体的任务。我们要充分考虑如何适配这种能力。
我认为Byzer Extension Market 核心流程有三个:
其中,注册登已经被实现过了。所以可以抽象出三个核心 API:
我们主要会议上传扩展为例子,介绍主要开发流程。
上传扩展分成两部分:
很多人第一感觉是在一个API里实现这两个功能,我个人偏向于使用两个 API,一个Form 来完成。
首先新建一个 UploadExtensionAction
类,继承 ActionRequireLogin
,
class UploadExtensionAction extends ActionRequireLogin {
override def _run(params: Map[String, String]): String = {
}
override def _help(): String = {
}
}
接着,我们定义一个描述Form表单元素的类:
object UploadExtensionAction {
object Params {
val USER_NAME = Input("userName", "")
val PLUGIN_NAME = Input("pluginName", "")
val PLUGIN_TYPE = Select("pluginType", values = List(), valueProvider = Option(() => {
List(
KV(Option("byzer_app"), Option("byzer_app")),
KV(Option("byzer_et"), Option("byzer_et")),
KV(Option("byzer_ds"), Option("byzer_ds"))
)
}))
val PLUGIN_VERSION = Input("version", "")
val FILE = Upload("file", valueProviderName = UploadExtensionFields_file.action)
}
def action = "uploadExtension"
def plugin = PluginItem(UploadExtensionAction.action,
classOf[UploadExtensionAction].getName, PluginType.action, None)
}
这里最显眼的是嵌套 object Params
, 他定义了五个参数,定义每个参数的Form元素,其中第五个元数 FILE
比较特殊,他有一个 valueProviderName
, 表示该元素用户操作后,会和Action UploadExtensionFields_file.action
进行交互。这意味着用户拖拽文件到上传框后,UploadExtensionFields_file.action
会提供服务端上传能力,上传后返回的结果会作为 file
参数和其他四个参数一起发送给 UploadExtensionAction
。
信息收集的一个简单实现如下:
override def _run(params: Map[String, String]): String = {
import UploadExtensionAction._
val userName = getUserName(params)
val pluginName = params(Params.PLUGIN_NAME.name)
val versionOpt = params.get(Params.PLUGIN_VERSION.name)
val pluginType = ByzerPluginType.from(params.get(Params.PLUGIN_TYPE.name).getOrElse("byzer_app"))
val jarPath = params(Params.FILE.name)
val saveSuccess = ExtensionService.saveUploadInfo(userName, pluginName, jarPath, versionOpt.get, pluginType, JSONTool.toJsonStr(
params - ActionRequireLogin.Params.ADMIN_TOKEN.name - UserService.Config.LOGIN_TOKEN
))
if (saveSuccess) {
JSONTool.toJsonStr(Map("msg" -> "Save extension success"))
} else {
JSONTool.toJsonStr(Map("msg" -> "Extension have the same version exists"))
}
}
接着我们来实现下 UploadExtensionFields_file
,整个代码也很简单:
class UploadExtensionFields_file extends ActionRequireLogin {
override def _run(params: Map[String, String]): String = {
val actionContext = ActionContext.context()
val items = actionContext.others(ActionContext.Config.formItems).asInstanceOf[java.util.List[FileItem]]
if (items.size() != 1) {
render(400, JSONTool.toJsonStr(List(Map("msg" -> "Only support one file to upload"))))
}
val paths = ArrayBuffer[Map[String, String]]()
try {
val repoLocation = UploadExtensionFields_file.repo
items.asScala.filterNot(f => f.isFormField).map {
item =>
val tempFilePath = PathFun(repoLocation).add("files").add(getUserName(params)).add(item.getFieldName).toPath
if (new File(tempFilePath).exists()) {
render(400, JSONTool.toJsonStr(Map("msg" -> s"${item.getFieldName} is already exits")))
}
val relativePath = "files" + PathFun.pathSeparator + getUserName(params) + PathFun.pathSeparator + item.getFieldName
val fileContent = item.getInputStream()
val targetPath = new File(tempFilePath)
FileUtils.copyInputStreamToFile(fileContent, targetPath)
fileContent.close()
paths += Map("path" -> relativePath)
}
} catch {
case e: Exception =>
throw e
}
JSONTool.toJsonStr(paths)
}
override def _help(): String = JSONTool.toJsonStr(
FormParams.toForm(TestAction.Params).toList.reverse)
}
这里引入了 ActionContext
对象,该对象可以提供 上传的文件句柄给你,然后你进行一些文件相关的操作就好,所以可以看到,非常简单,根本无需和 Rest中的比如 httpServlete打交道。
上面两个 Action开发好以后,就可以注册在描述类中进行配置:
现在,重启应用,就可以在 API List 页面中查看到我们开发的 API 了:
用户可以点击 View
后进入界面进行测试功能(是不是比辅助的API调试工具如 PostMan更好用?)。
API 开发完成后,我们希望用户能够更加方便的使用我们的 API,而不是需要自己去找到这个 API,此时向导就起到作用了。
向导创建是在 APIs Master里手工完成的。创建完成后,就可以得到这样的入口:
然后用户比如点击进 上传扩展 向导后,就可以进行相关 API 操作了, 而不用到 API列表中去寻找。
web-platform 使用 embedded jetty, 所以可以使用主类直接启动,部署上会很方便。如果用户想要开发直接的 Web 界面,既可以独立开发,也可以使用 sfcli 创建项目的时候带上参数 --include-ui 然后系统会自动创建一个 reactjs 的前端项目,也是很方便的。
web-platform 也可以用于帮助用户迅速的构建可使用的 MVP 产品。