一、需求
从编程角度来说,低代码核心是为用户打造一款能够快速入门搭建、同时又能支持中等复杂度的企业级应用的编程系统(Programming System)。
编程系统分为两个方面来看:
- 一方面,我们需要引入完善的编程表达能力(如泛型、模块、函数式等等),满足复杂应用的开发。
- 另一方面,我们需要优化配套的编程环境,提升用户的编程体验(如 IDE、LanguageServer、Debugger、实时编译等等)
目标用户包含两类:
- 企业业务人员,会使用一般的办公系统如 Excel 等,经过我们的培训可以独立使用低代码平台搭建系统;
- 专业开发人员,如工商银行的专业前后端开发。
关于用户的需求复杂度和使用难度,目前我们产品中的使用曲线还是比较割裂:
我们希望通过对编程系统中的各个设施的优化,能够达到比较平滑的效果。
二、路线
在我们目前的规划中,编程系统的发展路线分为两个阶段:
第一个阶段(1-2 年)
借助 TS Language Server 能力,快速补齐和完善编程系统的表达能力。对标 OutSystems 等产品的语言能力。
举一个例子,现在用户在使用部分场景的时候,没有类型提示了。或者在使用的一些链路中,类型推断断掉了。
目前这个阶段的一些主要问题如下:
- 前端组件类型体系不完善,如属性传递、事件回调等;
- 数据查询出来,由于用户 SELECT 和 JOIN 等,会产生出来很多数据结构,如果按具名的话会爆炸;
- 用户在操作列表数据的时候,目前只能使用 ForEach。无法用 find、filter、map 等方式;
- 一个复杂的应用有划分模块的需要;
- 目前逻辑块的空间利用率过低,想用一些更合适的交互形式代替;
- 用户编程的实时反馈、互动、调试方面比较困难;
- ...
第三节“典型问题整理”有详细描述。
第二个阶段
自研 Language Server,提供智能化的推荐能力。
三、典型问题整理
1. 支持前端组件的类型体系不完善
示例 1
比如下图是一个选择框组件的属性面板:
下面是选择框组件的一个简化版的 ts 声明和使用它的页面场景:
interface ChangeEvent<T> {
item: T;
value: string;
}
interface MultipleChangeEvent<T> {
item: T;
value: Array<string>;
}
export class Select<T> extends Component {
constructor(
public options?: {
// 属性
color?: 'default' | 'primary' | 'danger',
size?: 'mini' | 'small' | 'normal' | 'large',
multiple?: false,
dataSource?: Array<T> | ((params: DataSourceParams) => Promise<Array<T>>),
value?: string | Array<string>,
// 事件
onChange?: (e: ChangeEvent<T>) => void,
},
);
constructor(
public options?: {
// 属性
color?: 'default' | 'primary' | 'danger',
size?: 'mini' | 'small' | 'normal' | 'large',
multiple?: true,
dataSource?: Array<T> | ((params: DataSourceParams) => Promise<Array<T>>),
value?: string | Array<string>,
// 事件
onChange?: (e: ChangeEvent<T>) => void,
},
);
}
class View1 {
render(): VNode {
return new components.Select<Student>({
size: 'large',
multiple: true,
dataSource: this.loadStudents,
onChange: (e) => {
console.log(e.item.age);
},
});
}
}
class View2 {
render(): VNode {
return new components.Select<Student>({
size: 'large',
multiple: true,
dataSource: this.loadStudents,
onChange: (e) => {
showMessage(e.value.length);
},
});
}
}
它已经涉及到了以下的一些语言表达能力:
- 属性都是可选类型
- 选择框的数据源可以是数组、分页数组或函数(产品概念中的逻辑)
- 有单选、多选的可能,多选时 value 为数组
- onSelect 的事件入参是一个回调函数
- 泛型 T 取决于数据源中的 T
示例 2
页面中会使用循环组件,甚至会有双循环的场景。类似如下 ts 代码表达。希望用户在编辑 Text 组件的文本时,可以自动补全一些 scope, scope2 的上下文提示。
class View {
render(): VNode {
return new components.ForComponent<Student>({
dataSource: this.loadStudents,
slotDefault: (scope) => [
new components.ForComponent<Course>({
dataSource: scope.item.courses,
slotDefault: (scope2) => [
new components.Text({
text: scope2.item.course.name,
}),
],
],
});
}
}
另外 scope 这种很生疏的字段又不想向用户暴露,我们可以用析构去除。但又会引入一些语言特性、如析构时 as。
class View {
render(): VNode {
return new components.ForComponent<Student>({
dataSource: this.loadStudents,
slotDefault: ({ item }) => [
new components.ForComponent<Course>({
dataSource: item.courses,
slotDefault: ({ item: item2 }) => [
new components.Text({
text: item2.course.name,
}),
],
],
});
}
}
2. 数据查询具名类型爆炸
比如用户了定义一个如下两个实体(对应两张数据库表):
class School {
id: Long;
name: String;
description: String;
address: String;
static get(id: Long): School;
static create(student: School): Long;
static update(student: School): void;
static delete(id: Long): void;
}
class Student {
id: Long;
name: String;
age: Integer;
gender: Gender;
number: String;
idCard: String;
phone: String;
schoolId: Long;
static get(id: Long): Student;
static create(student: Student): Long;
static update(student: Student): void;
static delete(id: Long): void;
}
首先,比如最简单的修改功能:
用户的编程意图大概如下:
let student = Student.get('123'); // 查询出一个学生
// 在表单中编辑 student
Student.update(student); // 再提交更新
那么复杂一点的情况是这样的:
示例 1
有时比如不需要查询出全部字段
用户的编程意图类似下面的方式:
interface PartialStudent {
id: Long;
name: String;
age: Long;
gender: Gender;
}
let partialStudent: PartialStudent = query('SELECT id, name, age, gender FROM Student'); // 查询出一个学生
// 在表单中编辑 partialStudent
Student.update(student); // 再提交更新。❌ 类型错误
这时用户如果直接使用我们自动生成的 update 接口会类型不匹配。需要用户额外做很多操作去处理这个事情。
另外也产生了一个 PartialStudent 这样一个临时的具名类型,用户如果使用了很多这类场景,这些具名类型就会爆炸。
示例 2
JOIN 的场景,用户想查询学生时附带查询关联的学校信息(schoolId)。
用户的编程意图类似下面的方式:
interface StudentRecord {
id: Long;
name: String;
age: Integer;
gender: Gender;
number: String;
idCard: String;
phone: String;
schooId: Long;
schoolName: String;
schoolDescription: String;
schoolAddress: String;
}
let record: StudentRecord = query('SELECT * FROM Student JOIN School ON Student.schoolId = School.id'); // 查询出一个学生及相关的学校信息
// 在表单中编辑 record
Student.update(record); // 再提交更新。❌ 类型错误
这种方式下,用户查询出来的字段比以前多,同样与 update 的类型不匹配。
后来我们参考 OutSystems,改进成以下方式:
interface StudentRecord {
student: Student;
school: School;
}
let record: StudentRecord = query('SELECT * FROM Student JOIN School ON Student.schoolId = School.id'); // 查询出一个学生及相关的学校信息
// 在表单中编辑 record
Student.update(record.student); // 再提交更新。
这种方式下,用户已经可以 update 了。但目前仍然会产生许多类似 StudentRecord 这样的具名类型,这是我们现在的一个主要问题。
3. 列表操作比较薄弱
目前只给用户提供了 ForEach。
ForEach(list, start, list.length - 1, (item, i) => {
//
});
用户需要类似下面的一些高级操作:
list.filter((student) => student.age > 20).map((student) => ({
name: student.name,
age: student.age,
number: 'HZ' + student.number,
}));
语言表达能力上可能需要 lambda 的支持。
4. 一个复杂的应用有划分模块的需要
这个上次会议上讨论过,我还是在下面列一下。
我们的模块粒度比较大,把一个大应用能拆解,支持 import/export 即可。
下面是一个伪代码:
import * as moduleB from './appB/moduleB.nasl';
export entity Student {
name: moduleB.School;
age: Integer;
}
export struct School {
name: String;
count: Integer;
}
export enum Color {
RED,
GREEN,
BLUE,
}
export logic logic1(param1: Student) {
return some;
}
后面这种定义比较繁琐:
import * as moduleB from './appB/moduleB.nasl';
namespace entities {
export entity Student {
name: moduleB.School;
age: Integer;
}
}
namespace structures {
export struct School {
name: String;
count: Integer;
}
}
namespace enums {
export enum Color {
RED,
GREEN,
BLUE,
}
}
export logic logic1(param1: entities.Student) {
return some;
}
5. 目前逻辑块的空间利用率过低
darklang 我们可以做一个参考。
但目标用户群体中包含了没有编程经验的业务人员,因此都是代码编程的方式可能对他们来说比较困难。还请老师能提供更多的一些思路。
6. 用户编程的实时反馈、互动、调试方面比较困难
作为低代码可视化为主的编程环境,想提升一些实时反馈、互动,调试方面的体验,希望老师能提供更多的思路。