Compare commits

...
13 Commits
Author SHA1 Message Date
ascoders 9867c1ee9d 166 2020-09-14 09:52:10 +08:00
ascoders e8a0539984 Merge branch 'v2' of https://github.com/dt-fe/weekly into v2 2020-09-07 11:28:18 +08:00
ascoders 685e8ba32a 165 2020-09-07 11:27:07 +08:00
黄子毅 6c9493df7b Merge pull request #268 from spiritree/patch-1
Update 163.精读《Spring 概念》.md
2020-09-02 09:41:32 +08:00
深樹 1994438792 Update 163.精读《Spring 概念》.md
`lass` -> `class`
2020-09-01 16:04:21 +08:00
ascoders 60be3e354b fix 2020-08-31 10:25:00 +08:00
ascoders 9d0bb5c996 163 2020-08-31 10:22:21 +08:00
ascoders 304dc5fc36 163 2020-08-24 09:35:45 +08:00
ascoders 819b6d7452 162 2020-08-17 10:24:38 +08:00
ascoders b066c8a5d7 161 2020-08-10 10:11:03 +08:00
ascoders 80fb60c6fb 160 2020-07-27 09:37:27 +08:00
ascoders 6a2031899d 159 2020-07-20 09:55:34 +08:00
ascoders aedcc2fd56 158 2020-07-13 10:26:30 +08:00
11 changed files with 3859 additions and 15 deletions
@@ -222,7 +222,7 @@ shouldComponentUpdate(nextProps) {
虽然今天总结了 4 种比较 Object 对象的方式,但在实际项目中,应该尽可能使用引用对比,其次是浅对比和手动对比,最坏的情况是使用深对比。
> 讨论地址是:[精读《如何比较 Object 对象》· Issue #258 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/258)
> 讨论地址是:[精读《如何比较 Object 对象》· Issue #258 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/258)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
+481
View File
@@ -0,0 +1,481 @@
## 1 引言
随着 [Typescript 4 Beta](https://devblogs.microsoft.com/typescript/announcing-typescript-4-0-beta/) 的发布,又带来了许多新功能,其中 Variadic Tuple Types 解决了大量重载模版代码的顽疾,使得这次更新非常有意义。
## 2 简介
### 可变元组类型
考虑 `concat` 场景,接收两个数组或者元组类型,组成一个新数组:
```typescript
function concat(arr1, arr2) {
return [...arr1, ...arr2];
}
```
如果要定义 `concat` 的类型,以往我们会通过枚举的方式,先枚举第一个参数数组中的每一项:
```typescript
function concat<>(arr1: [], arr2: []): [A];
function concat<A>(arr1: [A], arr2: []): [A];
function concat<A, B>(arr1: [A, B], arr2: []): [A, B];
function concat<A, B, C>(arr1: [A, B, C], arr2: []): [A, B, C];
function concat<A, B, C, D>(arr1: [A, B, C, D], arr2: []): [A, B, C, D];
function concat<A, B, C, D, E>(arr1: [A, B, C, D, E], arr2: []): [A, B, C, D, E];
function concat<A, B, C, D, E, F>(arr1: [A, B, C, D, E, F], arr2: []): [A, B, C, D, E, F];)
```
再枚举第二个参数中每一项,如果要完成所有枚举,仅考虑数组长度为 6 的情况,就要定义 36 次重载,代码几乎不可维护:
```typescript
function concat<A2>(arr1: [], arr2: [A2]): [A2];
function concat<A1, A2>(arr1: [A1], arr2: [A2]): [A1, A2];
function concat<A1, B1, A2>(arr1: [A1, B1], arr2: [A2]): [A1, B1, A2];
function concat<A1, B1, C1, A2>(
arr1: [A1, B1, C1],
arr2: [A2]
): [A1, B1, C1, A2];
function concat<A1, B1, C1, D1, A2>(
arr1: [A1, B1, C1, D1],
arr2: [A2]
): [A1, B1, C1, D1, A2];
function concat<A1, B1, C1, D1, E1, A2>(
arr1: [A1, B1, C1, D1, E1],
arr2: [A2]
): [A1, B1, C1, D1, E1, A2];
function concat<A1, B1, C1, D1, E1, F1, A2>(
arr1: [A1, B1, C1, D1, E1, F1],
arr2: [A2]
): [A1, B1, C1, D1, E1, F1, A2];
```
如果我们采用批量定义的方式,问题也不会得到解决,因为参数类型的顺序得不到保证:
```typescript
function concat<T, U>(arr1: T[], arr2, U[]): Array<T | U>;
```
在 Typescript 4,可以在定义中对数组进行解构,通过几行代码优雅的解决可能要重载几百次的场景:
```typescript
type Arr = readonly any[];
function concat<T extends Arr, U extends Arr>(arr1: T, arr2: U): [...T, ...U] {
return [...arr1, ...arr2];
}
```
上面例子中,`Arr` 类型告诉 TS `T``U` 是数组类型,再通过 `[...T, ...U]` 按照逻辑顺序依次拼接类型。
再比如 `tail`,返回除第一项外剩下元素:
```typescript
function tail(arg) {
const [_, ...result] = arg;
return result;
}
```
同样告诉 TS `T` 是数组类型,且 `arr: readonly [any, ...T]` 申明了 `T` 类型表示除第一项其余项的类型,TS 可自动将 `T` 类型关联到对象 `rest`
```typescript
function tail<T extends any[]>(arr: readonly [any, ...T]) {
const [_ignored, ...rest] = arr;
return rest;
}
const myTuple = [1, 2, 3, 4] as const;
const myArray = ["hello", "world"];
// type [2, 3, 4]
const r1 = tail(myTuple);
// type [2, 3, ...string[]]
const r2 = tail([...myTuple, ...myArray] as const);
```
另外之前版本的 TS 只能将类型解构放在最后一个位置:
```typescript
type Strings = [string, string];
type Numbers = [number, number];
// [string, string, number, number]
type StrStrNumNum = [...Strings, ...Numbers];
```
如果你尝试将 `[...Strings, ...Numbers]` 这种写法,将会得到一个错误提示:
```text
A rest element must be last in a tuple type.
```
但在 Typescript 4 版本支持了这种语法:
```typescript
type Strings = [string, string];
type Numbers = number[];
// [string, string, ...Array<number | boolean>]
type Unbounded = [...Strings, ...Numbers, boolean];
```
对于再复杂一些的场景,例如高阶函数 `partialCall`,支持一定程度的柯里化:
```typescript
function partialCall(f, ...headArgs) {
return (...tailArgs) => f(...headArgs, ...tailArgs);
}
```
我们可以通过上面的特性对其进行类型定义,将函数 `f` 第一个参数类型定义为有顺序的 `[...T, ...U]`
```typescript
type Arr = readonly unknown[];
function partialCall<T extends Arr, U extends Arr, R>(
f: (...args: [...T, ...U]) => R,
...headArgs: T
) {
return (...b: U) => f(...headArgs, ...b);
}
```
测试效果如下:
```typescript
const foo = (x: string, y: number, z: boolean) => {};
// This doesn't work because we're feeding in the wrong type for 'x'.
const f1 = partialCall(foo, 100);
// ~~~
// error! Argument of type 'number' is not assignable to parameter of type 'string'.
// This doesn't work because we're passing in too many arguments.
const f2 = partialCall(foo, "hello", 100, true, "oops");
// ~~~~~~
// error! Expected 4 arguments, but got 5.
// This works! It has the type '(y: number, z: boolean) => void'
const f3 = partialCall(foo, "hello");
// What can we do with f3 now?
f3(123, true); // works!
f3();
// error! Expected 2 arguments, but got 0.
f3(123, "hello");
// ~~~~~~~
// error! Argument of type '"hello"' is not assignable to parameter of type 'boolean'
```
值得注意的是,`const f3 = partialCall(foo, "hello");` 这段代码由于还没有执行到 `foo`,因此只匹配了第一个 `x:string` 类型,虽然后面 `y: number, z: boolean` 也是必选,但因为 `foo` 函数还未执行,此时只是参数收集阶段,因此不会报错,等到 `f3(123, true)` 执行时就会校验必选参数了,因此 `f3()` 时才会提示参数数量不正确。
### 元组标记
下面两个函数定义在功能上是一样的:
```typescript
function foo(...args: [string, number]): void {
// ...
}
function foo(arg0: string, arg1: number): void {
// ...
}
```
但还是有微妙的区别,下面的函数对每个参数都有名称标记,但上面通过解构定义的类型则没有,针对这种情况,Typescript 4 支持了元组标记:
```typescript
type Range = [start: number, end: number];
```
同时也支持与解构一起使用:
```typescript
type Foo = [first: number, second?: string, ...rest: any[]];
```
### Class 从构造函数推断成员变量类型
构造函数在类实例化时负责一些初始化工作,比如为成员变量赋值,在 Typescript 4,在构造函数里对成员变量的赋值可以直接为成员变量推导类型:
```typescript
class Square {
// Previously: implicit any!
// Now: inferred to `number`!
area;
sideLength;
constructor(sideLength: number) {
this.sideLength = sideLength;
this.area = sideLength ** 2;
}
}
```
如果对成员变量赋值包含在条件语句中,还能识别出存在 `undefined` 的风险:
```typescript
class Square {
sideLength;
constructor(sideLength: number) {
if (Math.random()) {
this.sideLength = sideLength;
}
}
get area() {
return this.sideLength ** 2;
// ~~~~~~~~~~~~~~~
// error! Object is possibly 'undefined'.
}
}
```
如果在其他函数中初始化,则 TS 不能自动识别,需要用 `!:` 显式申明类型:
```typescript
class Square {
// definite assignment assertion
// v
sideLength!: number;
// ^^^^^^^^
// type annotation
constructor(sideLength: number) {
this.initialize(sideLength);
}
initialize(sideLength: number) {
this.sideLength = sideLength;
}
get area() {
return this.sideLength ** 2;
}
}
```
### 短路赋值语法
针对以下三种短路语法提供了快捷赋值语法:
```typescript
a &&= b; // a = a && b
a ||= b; // a = a || b
a ??= b; // a = a ?? b
```
### catch error unknown 类型
Typescript 4.0 之后,我们可以将 catch error 定义为 `unknown` 类型,以保证后面的代码以健壮的类型判断方式书写:
```typescript
try {
// ...
} catch (e) {
// error!
// Property 'toUpperCase' does not exist on type 'unknown'.
console.log(e.toUpperCase());
if (typeof e === "string") {
// works!
// We've narrowed 'e' down to the type 'string'.
console.log(e.toUpperCase());
}
}
```
PS:在之前的版本,`catch (e: unknown)` 会报错,提示无法为 `error` 定义 `unknown` 类型。
### 自定义 JSX 工厂
TS 4 支持了 `jsxFragmentFactory` 参数定义 Fragment 工厂函数:
```json
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"jsx": "react",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
}
}
```
还可以通过注释方式覆盖单文件的配置:
```typescript
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = (
<>
<div>Hello</div>
</>
);
```
以上代码编译后解析结果如下:
```typescript
// Note: these pragma comments need to be written
// with a JSDoc-style multiline syntax to take effect.
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "preact";
let stuff = h(Fragment, null, h("div", null, "Hello"));
```
### 其他升级
其他的升级快速介绍:
**构建速度提升**,提升了 `--incremental` + `--noEmitOnError` 场景的构建速度。
**支持 `--incremental` + `--noEmit` 参数同时生效。**
**支持 `@deprecated` 注释,** 使用此注释时,代码中会使用 ~~删除线~~ 警告调用者。
**局部 TS Server 快速启动功能,** 打开大型项目时,TS Server 要准备很久,Typescript 4 在 VSCode 编译器下做了优化,可以提前对当前打开的单文件进行部分语法响应。
**优化自动导入,** 现在 `package.json` `dependencies` 字段定义的依赖将优先作为自动导入的依据,而不再是遍历 `node_modules` 导入一些非预期的包。
除此之外,还有几个 Break Change
`lib.d.ts` 类型升级,主要是移除了 `document.origin` 定义。
覆盖父 Class 属性的 getter 或 setter 现在都会提示错误。
通过 `delete` 删除的属性必须是可选的,如果试图用 `delete` 删除一个必选的 key,则会提示错误。
## 3 精读
Typescript 4 最大亮点就是可变元组类型了,但可变元组类型也不能解决所有问题。
拿笔者的场景来说,函数 `useDesigner` 作为自定义 React Hook 与 `useSelector` 结合支持 connect redux 数据流的值,其调用方式是这样的:
```typescript
const nameSelector = (state: any) => ({
name: state.name as string,
});
const ageSelector = (state: any) => ({
age: state.age as number,
});
const App = () => {
const { name, age } = useDesigner(nameSelector, ageSelector);
};
```
`name``age` 是 Selector 注册的,内部实现方式必然是 `useSelector` + reduce,但类型定义就麻烦了,通过重载可以这么做:
```typescript
import * as React from 'react';
import { useSelector } from 'react-redux';
type Function = (...args: any) => any;
export function useDesigner();
export function useDesigner<T1 extends Function>(
t1: T1
): ReturnType<T1> ;
export function useDesigner<T1 extends Function, T2 extends Function>(
t1: T1,
t2: T2
): ReturnType<T1> & ReturnType<T2> ;
export function useDesigner<
T1 extends Function,
T2 extends Function,
T3 extends Function
>(
t1: T1,
t2: T2,
t3: T3,
t4: T4,
): ReturnType<T1> &
ReturnType<T2> &
ReturnType<T3> &
ReturnType<T4> &
;
export function useDesigner<
T1 extends Function,
T2 extends Function,
T3 extends Function,
T4 extends Function
>(
t1: T1,
t2: T2,
t3: T3,
t4: T4
): ReturnType<T1> &
ReturnType<T2> &
ReturnType<T3> &
ReturnType<T4> &
;
export function useDesigner(...selectors: any[]) {
return useSelector((state) =>
selectors.reduce((selected, selector) => {
return {
...selected,
...selector(state),
};
}, {})
) as any;
}
```
可以看到,笔者需要将 `useDesigner` 传入的参数通过函数重载方式一一传入,上面的例子只支持到了三个参数,如果传入了第四个参数则函数定义会失效,因此业界做法一般是定义十几个重载,这样会导致函数定义非常冗长。
但参考 TS4 的例子,我们可以避免类型重载,而通过枚举的方式支持:
```typescript
type Func = (state?: any) => any;
type Arr = readonly Func[];
const useDesigner = <T extends Arr>(
...selectors: T
): ReturnType<T[0]> &
ReturnType<T[1]> &
ReturnType<T[2]> &
ReturnType<T[3]> => {
return useSelector((state) =>
selectors.reduce((selected, selector) => {
return {
...selected,
...selector(state),
};
}, {})
) as any;
};
```
可以看到,最大的变化是不需要写四遍重载了,但由于场景和 `concat` 不同,这个例子返回值不是简单的 `[...T, ...U]`,而是 `reduce` 的结果,所以目前还只能通过枚举的方式支持。
当然可能存在不用枚举就可以支持无限长度的入参类型解析的方案,因笔者水平有限,暂未想到更好的解法,如果你有更好的解法,欢迎告知笔者。
## 4 总结
Typescript 4 带来了更强类型语法,更智能的类型推导,更快的构建速度以及更合理的开发者工具优化,唯一的几个 Break Change 不会对项目带来实质影响,期待正式版的发布。
> 讨论地址是:[精读《Typescript 4》· Issue #259 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/259)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
@@ -0,0 +1,115 @@
## 1 引言
在说低代码搭建之前,首先要理解什么是搭建(本文搭建指通过 Web 交互搭建一个自定义的新页面)。
**我认为搭建的本质是提效** ,而提效又分为对研发人员的提效,以及对客户的提效:
- 对研发人员的提效:相对于 Pro Code 模式,搭建的抽象程度更高,通过牺牲部分定制性换来更高效的开发方式。
- 对客户的提效:如果用户有任何搭建 Web 应用的诉求,本质上从阿里云购买服务器自建是最普适的方案,但由于专业性要求高,用户群会很窄,因此需要针对不同用户的诉求开发定制方案,本质上是通过降低通用性换取更低的上手成本,或者针对某个领域降低上手成本,比如 BI 搭建。
提效虽然被说烂了,但软件工程发展中,几乎大部分工作都能归结到在提效。比如 Vscode、Typescript 提升编码效率;React、Vue 框架提升程序研发效率;工作台、可持续集成提升协同开发效率,等等,连微软都称自己的使命是赋能全球每一人、每个组织成就不凡,很大程度上就是在说提升整个社会的生产效能。
低代码开发平台(Low-Code Development Platform)则更进一步,允许通过零代码或少量代码就可以快速创建应用。
从实践结果来看,完全零代码想要覆盖所有领域是不可能的,而 100% 全代码是可以覆盖所有领域,但研发成本太高,所以介于两者之间的低代码模式是值得尝试的,因为许多定制场景往往不需要太多高深的代码就能搞定,很多复杂逻辑可能几个简单的赋值语句、或者条件语句就可以搞定,但如果不允许写代码,其使用成本甚至比写少量代码还要高。
所以搭建本质解决的是提效问题,考虑提效就要看性价比,是使用者学习几行简单代码后,利用低代码平台效率更高,还是使用者坚持不写代码,使用繁琐的搭建交互成本更高?有人说代码学不会,但简单代码本质和搭建无异,都是对电脑指令的输入。
还有一些场景将背后复杂度转移到了其他链路,比如数据搭建场景,虽然搭建器没有低代码能力,但却能实现复杂业务逻辑,原因是这个复杂度被 SQL 层吃掉了,既然复杂度无法消除,那么哪一层实现的效率更高,就由哪一层去做才是合理的。
## 2 精读
低代码不仅仅包括 “能写代码”,主要具备如下四个特性:物料接入、编排能力、渲染能力、出码能力。
### 物料接入
通用搭建引擎要能够接入通用物料,即组件自身不关心搭建环境,就可以被搭建平台所使用。
这需要搭建平台本身不对组件代码实现有入侵,可以对组件暴露的 props 做完全控制,要做到自动识别组件有哪些 props 变量,并根据类型自动推荐编辑表单类型。
除了简单的文本、数字、下拉框等编辑器 Setter 之外,还有如下几种复杂编辑器:
- 回调函数编辑器。
- Node 节点编辑器。
- 文本国际化编辑器。
- 表达式编辑器。
回调函数编辑器与表达式编辑器都是低代码能力的体现,本质上就是利用代码描述某个变量值或者回调。
Node 节点编辑器专门处理节点类型 props 参数,比如 `props.header``propder.footer`,在代码模式描述为组件,在可视化模式需转化为画布下钻模式进行编辑。
### 编排能力
编排能力包含页面编排与逻辑编排,是低代码搭建的核心能力。
#### 页面编排
页面编排包含很多交互行为,比如拖拽组件、布局,其中布局大有可为,比如云凤蝶的编辑模式,通过自由拖拽布局,降低了使用者对 DOM 流式布局的理解成本,但通过自适应四周边距模拟出了流式布局自动撑开容器,容器间碰撞挤压的效果。
组件与组件形成的组合可以形成一个新的物料,一般称为模版,比如一个页面整体也可以称为模版,这个模版组件的 id 就是页面根节点的容器组件。但模版也有不能满足的场景,比如期望组件形成的组合拥有一套全新配置,此时就延伸出低代码业务组件的概念,可以认为将模版当作一个整体编辑,可以为模版设置任意的编辑表单,这个编辑表单的值可以透传到里面每个组件中读取。
#### 逻辑编排
逻辑编排是低代码能力的核心,在低代码引擎中,所有组件参数都可以用低代码描述,比如一个 `props.color` 可以通过颜色选择器选一个固定值,也可以转换为表达式模式写一段代码。
这段代码除了拥有普通 JS 能力外,还拥有基本状态管理的能力,即可以访问当前作用域下的状态 `this.state`,而状态作用域又被容器所分割,容器分为持有状态的容器与不持有状态的,一个持有状态容器内的子组件状态是互通的。
除了基本状态管理能力外,还拥有访问上下文能力,即调用引擎一些 API 对画布进行操作,一般都用于组件回调,在回调里调用 `this.setState` 设置状态也属于操作上下文的行为。除了上下文外,还有风格化、国际化、取数等能力可以通过 `this` 访问到,其中取数能力专门抽到引擎层做,就是为了让所有组件与取数逻辑解耦,组件只要拿到数据、isFetching,而不需要真正发送取数请求。
逻辑编排的另一个维度就是可视化,将上述低代码能力通过可视化方式表达为逻辑节点与线条,在描述与维护复杂逻辑时有一定优势。
### 渲染能力
搭建特殊之处在于,搭建过程几乎只能在 PC 端完成,但发布后的应用往往有多端渲染的诉求,比如越来越多的公司使用手机查看 BI 报表,甚至报表需要嵌入到微信、支付宝小程序中;PC 搭建的表单往往也有大量手机端填报的诉求。
所以编辑和渲染端应该是分离的,但为了保证逻辑一致性,核心代码需要复用,所以搭建引擎最好采用 UI 无关的内核 + 业务层拓展 UI 实现方式来做,UI 无关的内核只负责存储、操作画布数据,排除设计器附加的一堆 Panel 后,渲染时可以复用逻辑内核往往就足够了。
组件的跨端复用也是必须的,现在跨端渲染的技术方案也有不少。
### 出码能力
LowCode 与 ProCode 互转也是一大难题,首先互转的好处不必多说,可以自由的在提效与定制间切换,一定是最理想的开发模式,但实现起来有不少阻碍。
首先是 LowCode 转 ProCode,这个比较简单,原因是 LowCode 本身用 JSON 定义,代码是 JSON 的超集,从子集转换到超集本身没有技术障碍。
从 ProCode 转换到 LowCode 就麻烦了,一种方式是限定 ProCode 的能力,甚至用一种新的语法替代原生 JS,本质上都是通过将 ProCode 的能力范围限制住,使得 LowCode 可以接住。另一种方式是不对称转换,即从 ProCode 转换为 LowCode 后会存在功能缺失,或者即便功能不缺失,但 LowCode 无法对应的功能无法在搭建平台编辑。
### 运行时能力
只拥有上述低代码能力的搭建平台还是太通用了,虽然功能很强大,但在具体的业务场景不一定有多大的提效,具体的业务场景要有具体的解决方案,搭建本质是提效的,如果原子化、低代码的内容太多,就本末倒置,只是用另一种方式写代码罢了,并没有真正做到利用搭建提升开发效率。
通用的业务定制方式有如下三种:
- 定制业务组件:比如将某个复杂业务系统 80% 场景都要用到的组件固化为一个业务定制组件,省去了大部分配置时间,让使用者感受到提效。
- 定制业务模版和低代码业务组件:更进一步,将业务模版固化下来,本质上类似代码模版,或者利用低代码业务组件,在不开发新组件的前提下,制作一个针对某个业务场景的混合组件。
- 定制业务配置项:有些业务场景专业度很高,一方面是用户群不一样,一方面是搭建效率考虑,都应该提供一种基于业务角度出发的配置项,既符合业务思考逻辑,又节省配置步骤。
以上通用方式都是通过引擎已有的开放能力可以做到的,但对数据场景来说,有一些依赖引擎运行时能力场景,需要将引擎运行时能力抽象出来,配合低代码实现。
比如让当前页面所有配置相同数据集的组件自动建立筛选联动关联,虽然筛选联动关联可以通过低代码方式配置,但当画布组件数量变化时,或者有组件动态调用 API 新增组件时,静态的配置很难满足动态关联场景,此时我们可以拓展出一些全局运行时能力,让组件实现这些运行时能力时可以拿到画布信息,在引擎实际调用时再动态运行,而不是编辑生成一份静态 JSON 与渲染完全割裂。
运行时能力在不同平台针对不同垂直场景时会存在差异,如果希望打通底层引擎,可以提供拓展插槽,提供动态注册引擎运行时能力的机制。
## 3 总结
一个低代码搭建平台通吃一切场景是不可能的,只要有人愿意为垂直业务场景做 “量身定制”,用户就会立刻觉得搭建效率得到了提升,我们应当站在用户的角度,以用户利益最大化的方式做平台。
但搭建平台维护成本很高,每个业务场景都单独维护一套肯定不是长久之计,我们需要设计一套有弹性的低代码核心引擎,各个业务都可以基于他为自己的用户群 “量身定制” 一套专属设计器,共享搭建引擎通用的能力与协议,并自由拓展定制能力。
所以不仅渲染态是多态的,设计器也应该是多态的,其中可以被固化为标准的部分需要沉淀下来,比如物料接入规范、编排能力、出码能力、运行时能力,让各个搭建平台做到合而不同。
国内外都有非常多做的相当不错的搭建系统,但要不就太通用,具体场景提效不明显,要不就太垂直,换一个业务场景做不了。现在阿里中后台低代码搭建组织就在制定规范,将引擎通用能力固化为标准协议,让不同搭建平台可以对齐规范与功能,未来还会不断收敛核心引擎实现,基于它可以打造出千千万万个垂直领域的搭建平台,贴着业务做搭建提效,同时引擎内核与规范还能保持互通。
笔者所在阿里数据中台体验技术团队就是中后台低代码搭建组织的一员,将数据搭建领域做到极致。在技术上,我们在打通中后台搭建与数据搭建的技术方案,在产品上,我们正在逐渐统一阿里集团数据搭建平台,对外也携 QuickBI 成为国内唯一一家进入 Gartner 象限的 BI 产品,未来可期。
阿里数据中台体验技术团队正在火热招人中,如果感兴趣可以联系 ziyi.hzy@alibaba-inc.com 。
> 讨论地址是:[精读《对低代码搭建的理解》· Issue #260 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/260)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
+308
View File
@@ -0,0 +1,308 @@
## 1 引言
函数缓存是重要概念,本质上就是用空间(缓存存储)换时间(跳过计算过程)。
对于无副作用的纯函数,在合适的场景使用函数缓存是非常必要的,让我们跟着 https://whatthefork.is/memoization 这篇文章深入理解一下函数缓存吧!
## 2 概述
假设又一个获取天气的函数 `getChanceOfRain`,每次调用都要花 100ms 计算:
```jsx
import { getChanceOfRain } from "magic-weather-calculator";
function showWeatherReport() {
let result = getChanceOfRain(); // Let the magic happen
console.log("The chance of rain tomorrow is:", result);
}
showWeatherReport(); // (!) Triggers the calculation
showWeatherReport(); // (!) Triggers the calculation
showWeatherReport(); // (!) Triggers the calculation
```
很显然这样太浪费计算资源了,当已经计算过一次天气后,就没有必要再算一次了,我们期望的是后续调用可以直接拿上一次结果的缓存,这样可以节省大量计算。因此我们可以做一个 `memoizedGetChanceOfRain` 函数缓存计算结果:
```jsx
import { getChanceOfRain } from "magic-weather-calculator";
let isCalculated = false;
let lastResult;
// We added this function!
function memoizedGetChanceOfRain() {
if (isCalculated) {
// No need to calculate it again.
return lastResult;
}
// Gotta calculate it for the first time.
let result = getChanceOfRain();
// Remember it for the next time.
lastResult = result;
isCalculated = true;
return result;
}
function showWeatherReport() {
// Use the memoized function instead of the original function.
let result = memoizedGetChanceOfRain();
console.log("The chance of rain tomorrow is:", result);
}
```
在每次调用时判断优先用缓存,如果没有缓存则调用原始函数并记录缓存。这样当我们多次调用时,除了第一次之外都会立即从缓存中返回结果:
```jsx
showWeatherReport(); // (!) Triggers the calculation
showWeatherReport(); // Uses the calculated result
showWeatherReport(); // Uses the calculated result
showWeatherReport(); // Uses the calculated result
```
然而对于有参数的场景就不适用了,因为缓存并没有考虑参数:
```jsx
function showWeatherReport(city) {
let result = getChanceOfRain(city); // Pass the city
console.log("The chance of rain tomorrow is:", result);
}
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("London"); // Uses the calculated answer
```
由于参数可能性很多,所以有三种解决方案:
### 1. 仅缓存最后一次结果
仅缓存最后一次结果是最节省存储空间的,而且不会有计算错误,但带来的问题就是当参数变化时缓存会立即失效:
```jsx
import { getChanceOfRain } from "magic-weather-calculator";
let lastCity;
let lastResult;
function memoizedGetChanceOfRain(city) {
if (city === lastCity) {
// Notice this check!
// Same parameters, so we can reuse the last result.
return lastResult;
}
// Either we're called for the first time,
// or we're called with different parameters.
// We have to perform the calculation.
let result = getChanceOfRain(city);
// Remember both the parameters and the result.
lastCity = city;
lastResult = result;
return result;
}
function showWeatherReport(city) {
// Pass the parameters to the memoized function.
let result = memoizedGetChanceOfRain(city);
console.log("The chance of rain tomorrow is:", result);
}
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("Tokyo"); // Uses the calculated result
showWeatherReport("Tokyo"); // Uses the calculated result
showWeatherReport("London"); // (!) Triggers the calculation
showWeatherReport("London"); // Uses the calculated result
```
在极端情况下等同于没有缓存:
```jsx
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("London"); // (!) Triggers the calculation
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("London"); // (!) Triggers the calculation
showWeatherReport("Tokyo"); // (!) Triggers the calculation
```
### 2. 缓存所有结果
第二种方案是缓存所有结果,使用 Map 存储缓存即可:
```jsx
// Remember the last result *for every city*.
let resultsPerCity = new Map();
function memoizedGetChanceOfRain(city) {
if (resultsPerCity.has(city)) {
// We already have a result for this city.
return resultsPerCity.get(city);
}
// We're called for the first time for this city.
let result = getChanceOfRain(city);
// Remember the result for this city.
resultsPerCity.set(city, result);
return result;
}
function showWeatherReport(city) {
// Pass the parameters to the memoized function.
let result = memoizedGetChanceOfRain(city);
console.log("The chance of rain tomorrow is:", result);
}
showWeatherReport("Tokyo"); // (!) Triggers the calculation
showWeatherReport("London"); // (!) Triggers the calculation
showWeatherReport("Tokyo"); // Uses the calculated result
showWeatherReport("London"); // Uses the calculated result
showWeatherReport("Tokyo"); // Uses the calculated result
showWeatherReport("Paris"); // (!) Triggers the calculation
```
这么做带来的弊端就是内存溢出,当可能参数过多时会导致内存无限制的上涨,最坏的情况就是触发浏览器限制或者页面崩溃。
### 3. 其他缓存策略
介于只缓存最后一项与缓存所有项之间还有这其他选择,比如 LRUleast recently used)只保留最小化最近使用的缓存,或者为了方便浏览器回收,使用 WeakMap 替代 Map。
最后提到了函数缓存的一个坑,必须是纯函数。比如下面的 CASE:
```jsx
// Inside the magical npm package
function getChanceOfRain() {
// Show the input box!
let city = prompt("Where do you live?");
// ... calculation ...
}
// Our code
function showWeatherReport() {
let result = getChanceOfRain();
console.log("The chance of rain tomorrow is:", result);
}
```
`getChanceOfRain` 每次会由用户输入一些数据返回结果,导致缓存错误,原因是 “函数入参一部分由用户输入” 就是副作用,我们不能对有副作用的函数进行缓存。
这有时候也是拆分函数的意义,将一个有副作用函数的无副作用部分分解出来,这样就能局部做函数缓存了:
```jsx
// If this function only calculates things,
// we would call it "pure".
// It is safe to memoize this function.
function getChanceOfRain(city) {
// ... calculation ...
}
// This function is "impure" because
// it shows a prompt to the user.
function showWeatherReport() {
// The prompt is now here
let city = prompt("Where do you live?");
let result = getChanceOfRain(city);
console.log("The chance of rain tomorrow is:", result);
}
```
最后,我们可以将缓存函数抽象为高阶函数:
```jsx
function memoize(fn) {
let isCalculated = false;
let lastResult;
return function memoizedFn() {
// Return the generated function!
if (isCalculated) {
return lastResult;
}
let result = fn();
lastResult = result;
isCalculated = true;
return result;
};
}
```
这样生成新的缓存函数就方便啦:
```jsx
let memoizedGetChanceOfRain = memoize(getChanceOfRain);
let memoizedGetNextEarthquake = memoize(getNextEarthquake);
let memoizedGetCosmicRaysProbability = memoize(getCosmicRaysProbability);
```
`isCalculated``lastResult` 都存储在 `memoize` 函数生成的闭包内,外部无法访问。
## 3 精读
### 通用高阶函数实现函数缓存
原文的例子还是比较简单,没有考虑函数多个参数如何处理,下面我们分析一下 Lodash `memoize` 函数源码:
```jsx
function memoize(func, resolver) {
if (
typeof func != "function" ||
(resolver != null && typeof resolver != "function")
) {
throw new TypeError(FUNC_ERROR_TEXT);
}
var memoized = function () {
var args = arguments,
key = resolver ? resolver.apply(this, args) : args[0],
cache = memoized.cache;
if (cache.has(key)) {
return cache.get(key);
}
var result = func.apply(this, args);
memoized.cache = cache.set(key, result) || cache;
return result;
};
memoized.cache = new (memoize.Cache || MapCache)();
return memoized;
}
```
原文有提到缓存策略多种多样,而 Lodash 将缓存策略简化为 key 交给用户自己管理,看这段代码:
```jsx
key = resolver ? resolver.apply(this, args) : args[0];
```
也就是缓存的 key 默认是执行函数时第一个参数,也可以通过 `resolver` 拿到参数处理成新的缓存 key。
在执行函数时也传入了参数 `func.apply(this, args)`
最后 `cache` 也不再使用默认的 Map,而是允许用户自定义 `lodash.memoize.Cache` 自行设置,比如设置为 WeakMap:
```jsx
_.memoize.Cache = WeakMap;
```
### 什么时候不适合用缓存
以下两种情况不适合用缓存:
1. 不经常执行的函数。
2. 本身执行速度较快的函数。
对于不经常执行的函数,本身就不需要利用缓存提升执行效率,而缓存反而会长期占用内存。对于本身执行速度较快的函数,其实大部分简单计算速度都很快,使用缓存后对速度没有明显的提升,同时如果计算结果比较大,反而会占用存储资源。
对于引用的变化尤其重要,比如如下例子:
```jsx
function addName(obj, name){
return {
...obj,
name:
}
}
```
`obj` 添加一个 key,本身执行速度是非常快的,但添加缓存后会带来两个坏处:
1. 如果 `obj` 非常大,会在闭包存储完整 `obj` 结构,内存占用加倍。
2. 如果 `obj` 通过 mutable 方式修改了,则普通缓存函数还会返回原先结果(因为对象引用没有变),造成错误。
如果要强行进行对象深对比,虽然会避免出现边界问题,但性能反而会大幅下降。
## 4 总结
函数缓存非常有用,但并不是所有场景都适用,因此千万不要极端的将所有函数都添加缓存,仅限于计算耗时、可能重复利用多次,且是纯函数的。
> 讨论地址是:[精读《函数缓存》· Issue #261 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/261)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
@@ -0,0 +1,109 @@
## 1 引言
[「可视化搭建系统」——从设计到架构,探索前端的领域和意义](https://juejin.im/post/6854573220532748302) 这篇文章主要分析了现阶段可视化搭建的几种表现形式和实现原理,并重点介绍了基于富文本的可视化搭建思路,让人耳目一新。
基于富文本的可视化搭建看似很新颖,但其实早就被广泛使用了,任何一个富文本编辑器几乎都有插入表格功能,这就是一个典型插入自定义组件的场景。
使用过 [语雀](https://www.yuque.com/) 的同学应该知道,这个产品的富文本编辑器可以插入各种各样自定义区块,是 “最像搭建” 的富文本编辑器。
那么积木式搭建和富文本搭建存在哪些差异,除了富文本更倾向于记录静态内容外,还有哪些差异,两者是否可以结合?本文将围绕这两点进行讨论。
## 2 精读
还是先顺着原文谈谈对可视化搭建的理解:
可视化搭建是通过可视化方式代替开发。**前端代码开发主要围绕的是 html + js + css**,那么无论是 markdown 语法,还是创建另一套模版语言亦或 JSON 构成的 DSL,**都是用一种 dsl + 组件 + css 的方式代替 html + js + css**,可视化搭建则更进一步,用 ui 代替了 dsl + 组件,**即精简为 ui 操作 + css**。
可以看到,这种转换的推演过程存在一定瑕疵,因为每次转换都有部分损耗:
**用 dsl + 组件 代替 html + js。**
如果 dsl 拓展得足够好,理论上可以达到 html 的水平,尤其在垂直业务场景是不需要那么多特殊 html 标签的。
但用组件代替 js 就有点奇怪了,首先并不是所有 js 逻辑都沉淀在组件里,一定有组件间的联动逻辑是无法通过一个组件 js 完成的,另一方面如果将 js 逻辑寄托在组件代码里,本质上是没有提效的,用源码开发项目与开发搭建平台的组件都是 pro code,更极端一点来说,无论是组件间联动还是整个应用都可以用一个组件来写,那搭建平台就无事可做了,这个组件也成了整个应用,game over。
为了弥补这块缺憾,低代码能力的呼声越来越高,而低代码能力的核心在于设计是否合理,比如暴露哪些 API 可以覆盖大部分需求?写多少代码合适,如何以最小 API 透出最大弥补组件间缺失的 js 能力?目前来看,以状态数据驱动的低代码是相对优雅的。
**用 ui 操作 代替 dsl + 组件。**
UI 操作并不是标准的,相比直接操作模版或者 JSON DSL,UI 化后就仁者见仁智者见智了,但 UI 化带来的效率提升是巨大的,因为所见即所得是生产力的源泉,从直观的 UI 布局来看,就比维护代码更轻松。但 UI 化也存在两个问题,一个是可能有人觉得不如 markdown 效率高,另一个是功能有丢失。
对于第一点 UI 操作效率不如 markdown 高,可能很多程序员都崇尚用 markdown 维护文档而不是富文本,原因是觉得程序员维护代码的效率反而比所见即所得高,但那可能是错觉,原因是还没有遇到好用的富文本编辑器,体验过语雀富文本编辑器后,相信大部分程序员都不会再想回头写 markdown。当然语雀富文本战胜 markdown 的原因有很多,我觉得主要两点是吸收并兼容了 markdown 操作习惯,与支持了更多仅 UI 能做到的拓展能力,对 markdown 形成降维打击。
第二点功能丢失很好理解,markdown 有一套标准语法和解析器可以验证,但 UI 操作并没有标准化,也没有独立验证系统,如果无法回退到源码模式,UI 没有实现的功能就做不到。
回到富文本搭建上,其实富文本搭建和普通网页构建并没有本质区别。html 是超文本标记语言,富文本是跨平台文档格式,从逻辑上这两个格式是可以互转的,只要富文本规则作出足够多的拓展,就可以大致覆盖 html 的能力。
但富文本搭建有着显著的特征,就是光标。
### 积木式搭建和富文本搭建的区别
富文本以文本为中心,因此编辑文字的光标会常驻,编辑的核心逻辑是排版文字,并考虑如何在文字周围添加一些自定义区块。
有了光标后,圈选也非常重要,因为大家编辑文字时有一种很自然的想法是,任何文字圈选后复制,可以粘贴到任何地方,那么所有插入到富文本中的自定义组件也要支持被圈选,被复制。
实际上富文本内插入自定义区块也可以转换为积木式搭建方案解决,比如下面的场景:
```text
文本 A
图表 B
文本 C
```
我们在文本 A 与 文本 C 之间插入图表 B,也可以理解为拖拽了三个组件:文本组件 A + 图表组件 B + 文本组件 C,然后分别编辑这三个组件,微调样式后可以达到与富文本一样的编辑效果,甚至加上自由布局后,在布局能力上会超越富文本。
虽然功能层面上富文本略有输给积木式搭建,但富文本在编辑体验上是胜出的,对于文字较多的场景,我们还是会选择富文本方式编辑而不是积木式搭建拖拽 N 个文本组件。
所以微软 OneNote 也吸取了这个经验,毕竟笔记本主要还是记录文字,因此还是采用富文本的编辑模式,但创造性的加入了一个个独立区块,点击任何区域都会创造一个区块,整个文档可以由一个区块构成,也可以是多个区块组合而成,这样对于连贯性的文字场景可以采用一个富文本区块,对于自定义区块较多,比如大部分是图片和表格的,还可以回到积木式搭建的体验。由于 OneNote 采用绝对定位模拟流式布局的思路,当区块重叠时还可以自动挤压底部区块,因此多区块模式下编辑体验还是相对顺畅的。
可以看出来这是一种结合的尝试,从前端角度来看,富文本本质上是对一个 div 进行 contenteditable 申明,那么一个应用可以整体是 contenteditable 的,也可以局部几个区块是,这种代码层面的自由度体现在搭建上就是积木式搭建可以与富文本搭建自由结合。
### 积木式搭建与富文本搭建如何结合
对于积木式搭建来说,富文本只是其中一个组件,在不考虑有富文本组件时是完全没有富文本能力的。比如一个搭建平台只提供了几个图表和基础控件,你是不可能在其基础上使用富文本能力的,甚至连写静态文本都做不到。
所以富文本只是搭建中一个组件,就像 contenteditable 也只能依附于一个标签,整个网页还是由标签组成的。但对于一个提供了富文本组件的积木式搭建系统来说,文字与控件混排又是一个痛点,毕竟要以一个个区块组件的方式去拖拽文本节点,成本比富文本模式大得多。
所以理想情况是富文本与整个搭建系统使用同一套 DSL 描述结构,富文本只是在布局上有所简化,简化为简单的平铺模式即可,但因为 DSL 描述打通,富文本也可以描述使用搭建提供的任意组件嵌套在内,所以只要用户愿意,可以将富文本组件拉到最大,整个页面都基于富文本模式去搭建,这就变成了富文本搭建,也可以将富文本缩小,将普通控件以积木方式拖拽到画布中,走积木式搭建路线。
用代码方式描述积木式搭建:
```html
<bar-chart />
<div>
<p>header</p>
<line-chart />
<p>footer</p>
</div>
```
上述模式需要拖拽 `bar-chart``div``p``line-chart``p` 共 5 个组件。富文本模式则类似下面的结构:
```html
<bar-chart />
<div contenteditable>
<p>header</p>
<line-chart />
<p>footer</p>
</div>
```
只要拖拽 `bar-chart``div` 两个组件即可,`div` 内部的文字通过光标输入,`line-chart` 通过富文本某个按钮或者键盘快捷键添加。
可以看到虽然操作方式不同,但本质上描述协议并没有本质区别,我们理论上可以将任何容器标签切换为富文本模式。
## 3 总结
富文本是一种重要的交互模式,可以基于富文本模式做搭建,也可以在搭建系统中嵌入富文本组件,甚至还可以追求搭建与富文本的结合。
富文本组件既可以是搭建系统中一个组件,又可以在内部承载搭建系统的所有组件,做到这一步才算是真正发挥出富文本的潜力。
> 讨论地址是:[精读《可视化搭建思考 - 富文本搭建》· Issue #262 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/262)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
@@ -0,0 +1,185 @@
## 1 引言
本周跟着 [Tasks, microtasks, queues and schedules](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) 这篇文章一起深入理解这些概念间的区别。
先说结论:
- Tasks 按顺序执行,浏览器可能在 Tasks 之间执行渲染。
- Microtasks 也按顺序执行,时机是:
- 如果没有执行中的 js 堆栈,则在每个回调之后。
- 在每个 task 之后。
## 2 概述
### Event Loop
在说这些概念前,先要介绍 Event Loop。
首先浏览器是多线程的,每个 JS 脚本都在单线程中执行,每个线程都有自己的 Event Loop,同源的所有浏览器窗口共享一个 Event Loop 以便通信。
Event Loop 会持续循环的执行所有排队中的任务,浏览器会为这些任务划分优先级,按照优先级来执行,这就会导致 Tasks 与 Microtasks 执行顺序与调用顺序的不同。
### promise 与 setTimeout
看下面代码的输出顺序:
```js
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
Promise.resolve()
.then(function () {
console.log("promise1");
})
.then(function () {
console.log("promise2");
});
console.log("script end");
```
正确答案是 `script start`, `script end`, `promise1`, `promise2`, `setTimeout`,在线程中,同步脚本执行优先级最高,然后 promise 任务会存放到 MicrotaskssetTimeout 任务会存放到 TasksMicrotasks 会优先于 Tasks 执行。
Microtasks 中文可以翻译为微任务,只要有 Microtasks 插入,就会不断执行 Microtasks 队列直到结束,在结束前都不会执行到 Tasks。
### 点击冒泡 + 任务
下面给出了更复杂的例子,提前说明后面的例子 Chrome、Firefox、Safari、Edge 浏览器的结果完全不一样,但只有 Chrome 的运行结果是对的!为什么 Chrome 是对的呢,请看下面的分析:
```html
<div class="outer">
<div class="inner"></div>
</div>
```
```js
// Let's get hold of those elements
var outer = document.querySelector(".outer");
var inner = document.querySelector(".inner");
// Let's listen for attribute changes on the
// outer element
new MutationObserver(function () {
console.log("mutate");
}).observe(outer, {
attributes: true,
});
// Here's a click listener…
function onClick() {
console.log("click");
setTimeout(function () {
console.log("timeout");
}, 0);
Promise.resolve().then(function () {
console.log("promise");
});
outer.setAttribute("data-random", Math.random());
}
// …which we'll attach to both elements
inner.addEventListener("click", onClick);
outer.addEventListener("click", onClick);
```
点击 `inner` 区块后,正确输出顺序应该是:
```text
click
promise
mutate
click
promise
mutate
timeout
timeout
```
逻辑如下:
1. 点击触发 `onClick` 函数入栈。
2. 立即执行 `console.log('click')` 打印 `click`
3. `console.log('timeout')` 入栈 Tasks。
4. `console.log('promise')` 入栈 microtasks。
5. `outer.setAttribute('data-random')` 的触发导致监听者 `MutationObserver` 入栈 microtasks。
6. `onClick` 函数执行完毕,此时线程调用栈为空,开始执行 microtasks 队列。
7. 打印 `promise`,打印 `mutate`,此时 microtasks 已空。
8. 执行冒泡机制,outer div 也触发 `onClick` 函数,同理,打印 `promise`,打印 `mutate`
9. 都执行完后,执行 Tasks,打印 `timeout`,打印 `timeout`
### 模拟点击冒泡 + 任务
如果将触发 `onClick` 行为由点击改为:
```js
inner.click();
```
结果会不同吗?答案是会(单元测试与用户行为不符合,单测也有无解的时候)。然而四大浏览器的执行结果也是完全不一样,但从逻辑上讲仍然 Chrome 是对的,让我们看下 Chrome 的结果:
```text
click
click
promise
mutate
promise
timeout
timeout
```
逻辑如下:
1. `inner.click()` 触发 `onClick` 函数入栈。
2. 立即执行 `console.log('click')` 打印 `click`
3. `console.log('timeout')` 入栈 Tasks。
4. `console.log('promise')` 入栈 microtasks。
5. `outer.setAttribute('data-random')` 的触发导致监听者 `MutationObserver` 入栈 microtasks。
6. 由于冒泡改为 js 调用栈执行,所以此时 js 调用栈未结束,不会执行 microtasks,反而是继续执行冒泡,outer 的 `onClick` 函数入栈。
7. 立即执行 `console.log('click')` 打印 `click`
8. `console.log('timeout')` 入栈 Tasks。
9. `console.log('promise')` 入栈 microtasks。
10. `MutationObserver` 由于还没调用,因此这次 `outer.setAttribute('data-random')` 的改动实际上没有作用。
11. js 调用栈执行完毕,开始执行 microtasks,按照入栈顺序,打印 `promise``mutate``promise`
12. microtasks 执行完毕,开始执行 Tasks,打印 `timeout``timeout`
## 3 精读
基于任务调度这么复杂,且浏览器实现方式很不同,下面两件事是我很不推荐的:
1. 业务逻辑 “巧妙” 依赖了 microtasks 与 Tasks 执行逻辑的微妙差异。
2. 死记硬背调用顺序。
且不说依赖了调用顺序的业务逻辑本身就很难维护,不同浏览器之间对任务调用顺序还是不同的,这可能源于对 W3C 标准规范理解的偏差,也可能是 BUG,这会导致依赖于此的逻辑非常脆弱。
虽然上面两个例子非常复杂,但我们也不必把这个例子当作经典背诵,只要记住文章开头提到的执行逻辑就可以推导:
- Tasks 按顺序执行,浏览器可能在 Tasks 之间执行渲染。
- Microtasks 也按顺序执行,时机是:
- 如果没有执行中的 js 堆栈,则在每个回调之后。
- 在每个 task 之后。
记住 `Promise``Microtasks``setTimeout``Tasks`JS 一次 Event Loop 完毕后,即调用栈没有内容时才会执行 `Microtasks` -> `Tasks`,在执行 `Microtasks` 过程中插入的 `Microtasks` 会按顺序继续执行,而执行 `Tasks` 中插入的 `Microtasks` 得等到调用栈执行完后才继续执行。
上面说的内容都是指一次 Event Loop 时立即执行的优先级,不要和执行延迟时间弄混淆了。
把 JS 线程的 Event Loop 当作一个函数,函数内同步逻辑执行优先级是最高的,如果遇到 `Microtasks``Tasks` 就会立即记录下来,当一次 Event Loop 执行完后立即调用 `Microtasks`,等 `Microtasks` 队列执行完毕后可能进行一些渲染行为,等这些浏览器操作完成后,再考虑执行 `Tasks` 队列。
## 4 总结
最后,还是要强调一句,不要依赖 `Microtasks``Tasks` 的执行顺序,尤其在申明式编程环境中,我们可以把 `Microtasks``Tasks` 都当作是异步内容,在渲染时做好状态判断即可,不用关心先后顺序。
> 讨论地址是:[精读《Tasks, microtasks, queues and schedules》· Issue #264 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/264)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
+306
View File
@@ -0,0 +1,306 @@
[spring](https://spring.io/) 是 Java 非常重要的框架,且蕴含了一系列设计模式,非常值得研究,本期就通过 [Spring学习](https://www.cnblogs.com/wmyskxz/p/8820371.html) 这篇文章了解一下 spring。
## spring 为何长寿
spring 作为一个后端框架,拥有 17 年历史,这在前端看来是不可思议的。前端几乎没有一个框架可以流行超过 5 年,就最近来看,react、angular、vue 三大框架可能会活的久一点,他们都是前端相对成熟阶段的产物,我们或多或少可以看出一些设计模式。然而这些前端框架与 spring 比起来还是差距很大,我们来看看 spring 到底强大在哪。
### 设计模式
设计模式是一种思想,不依附于任何编程语言与开发框架。比如你学会了工厂设计模式,可以在后端用,也可以转到前端用,可以在 Go 语言用,也可以在 Typescript 用,可以在 React 框架用,也可以在 Vue 里用,所以设计模式是一种具有迁移能力的知识,学会后可以受益整个职业生涯,而语言、框架则不具备迁移性,前端许多同学都把精力花在学习框架特性上,遇到前端技术迭代时期就尴尬了,这就是为什么大公司面试要问框架原理,就是看看你能否抓住一些不变的东西,所以洋洋洒洒的说上下文相关的细节也不是面试官想要的,真正想听到的是你抽象后对框架原理共性的总结。
spring 框架就用到了许多设计模式,包括:
工厂模式:用工厂生产对象实例来代替原始的 new。所谓工厂就是屏蔽实例话的细节,调用处无需关心实例化对象需要的环境参数,提升可维护性。spring 的 BeanFactory 创建 bean 对象就是工厂模式的体现。
代理模式:允许通过代理对象访问目标对象。Spring 实现 AOP 就是通过动态代理模式。
单例模式:单实例。spring 的 bean 默认都是单例。
包装器模式:将几个不同方法通用部分抽象出来,调用时通过包装器内部引导到不同的实现。比如 spring 连接多种数据库就使用了包装器模式简化。
观察者模式:这个前端同学很熟悉,就是事件机制,spring 中可以通过 ApplicationEvent 实践观察者模式。
适配器模式:通过适配器将接口转换为另一个格式的接口。spring AOP 的增强和通知就使用了适配器模式。
模板方法模式:父类先定义一些函数,这些函数之间存在调用关联,将某些设定为抽象函数等待子类继承时去重写。spring 的 `jdbcTemplate``hibernateTemplate` 等数据库操作类使用了模版方法模式。
### 全家桶
spring 作为一个全面的 java 框架,提供了系列全家桶满足各种场景需求:spring mvc、spring security、spring data、spring boot、spring cloud。
- spring boot:简化了 spring 应用配置,约定大于配置的思维。
- spring data:是一个数据操作与访问工具集,比如支持 jdbc、redis 等数据源操作。
- spring cloud:是一个微服务解决方案,基于 spring boot,集成了服务发现、配置管理、消息总线、负载均衡、断路器、数据监控等各种服务治理能力。
- spring security:支持一些安全模型比如单点登录、令牌中继、令牌交换等。
- spring mvcMVC 思想的 web 框架。
## IOC
IOCInverse of Control)控制反转。IOC 是 Spring 最核心部分,因为所有对象调用都离不开 IOC 模式。
假设我们有三个类:Country、Province、City,最大的类别是国家,其次是省、城市,国家类需要调用省类,省类需要调用城市类:
```java
public class Country {
private Province province;
public Country(){
this.province = new Province()
}
}
public class Province {
private City city;
public Province(){
this.city = new City()
}
}
public class City {
public City(){
// ...
}
}
```
假设来了一个需求,City 实例化时需增加人口(people)参数,我们就要改动所有类代码:
```java
public class Country {
private Province province;
public Country(int people){
this.province = new Province(people)
}
}
public class Province {
private City city;
public Province(int people){
this.city = new City(people)
}
}
public class City {
public City(int people){
// ...
}
}
```
那么在真实业务场景中,一个底层类可能被数以千计的类使用,这么改显然难以维护。IOC 就是为了解决这个问题,它使得我们可以只改动 City 的代码,而不用改动其他类的代码:
```java
public class Country {
private Province province;
public Country(Province province){
this.province = province
}
}
public class Province {
private City city;
public Province(City city){
this.city = city
}
}
public class City {
public City(int people){
// ...
}
}
```
可以看到,增加 `people` 属性只需要改动 city 类。然而这样做也是有成本的,就是类实例化步骤会稍微繁琐一些:
```java
City city = new City(1000);
Province province = new Province(city);
Country country = new Country(province);
```
这就是控制反转,由 Country 依赖 Province 变成了类依赖框架(上面的实例化代码)注入。
然而手动维护这种初始化依赖是繁琐的,spring 提供了 bean 容器自动做这件事,我们只需要利用装饰器 Autowired 就可以自动注入依赖:
```java
@Component
public class Country {
@Autowired
private Province province;
}
@Component
public class Province {
@Autowired
public City city;
}
@Component
public class City {
}
```
实际上这种自动分析并实例化的手段,不仅比手写方便,还能解决循环依赖的问题。在实际场景中,两个类相互调用是很常见的,假设现在有 A、B 类相互依赖:
```java
@Component
public class A {
@Autowired
private B b;
}
@Component
public class B {
@Autowired
public A a;
}
```
那么假设我们想获取 A 实例,会经历这样一个过程:
```text
获取 A 实例 -> 实例化不完整 A -> 检测到注入 B -> 实例化不完整 B -> 检测到注入 A -> 注入不完整 A -> 得到完整 B -> 得到完整 A -> 返回 A 实例
```
其实 spring 仅支持单例模式下非构造器的循环依赖,这是因为其内部有一套机制,让 bean 在初始化阶段先提前持有对方引用地址,这样就可以同时实例化两个对象了。
除了方便之外,IOC 配合 spring 容器概念还可以使获取实例时不用关心一个类实例化需要哪些参数,只需要直接申明获取即可,这样在类的数量特别多,尤其是大量代码不是你写的情况下,不需要阅读类源码也可以轻松获取实例,实在是大大提升了可维护性。
说到这就提到了 Bean 容器,在 spring 概念中,Bean 容器是对 class 的加强,如果说 Class 定义了类的基本含义,那 Bean 就是对类进行使用拓展,告诉我们应该如何实例化与使用这个类。
举个例子,比如利用注解描述的这段 Bean 类:
```java
@Configuration
public class CityConfig {
@Scope("prototype")
@Lazy
@Bean(initMethod = "init", destroyMethod = "destroy")
public City city() {
return new City()
}
}
```
可以看到,额外描述了是否延迟加载,是否单例,初始化与析构函数分别是什么等等。
下面给出一个从 Bean 获取实例的例子,采用比较古老的 xml 配置方式:
```java
public interface City {
Int getPeople();
}
```
```java
public class CityImpl implements City {
public Int getPeople() {
return 1000;
}
}
```
接下来用 xml 描述这个 bean:
```xml
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" default-autowire="byName">
<bean id="city" class="xxx.CityImpl"/>
</beans>
```
`bean` 支持的属性还有很多,由于本文并不做入门教学,就不一一列举了,总之 `id` 是一个可选的唯一标志,接下来我们可以通过 `id` 访问到 city 的实例。
```java
public class App {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("classpath:application.xml");
// 从 context 中读取 Bean,而不 new City()
City city = context.getBean(City.class);
System.out.println(city.getPeople());
}
}
```
可以看到,程序任何地方使用 city 实例,只需要调用 `getBean` 函数,就像一个工厂把实例化过程给承包了,我们不需要关心 City 构造函数要传递什么参数,不需要关心它依赖哪些其他的类,只要这一句话就可以拿到实例,是不是在维护项目时省心了很多。
## AOP
AOPAspect Oriented Program)面向切面编程。
AOP 是为了解决主要业务逻辑与次要业务逻辑之间耦合问题的。主要业务逻辑比如登陆、数据获取、查询等,次要业务逻辑比如性能监控、异常处理等等,次要业务逻辑往往有:不重要、和业务关联度低、贯穿多处业务逻辑的特性,如果没有好的设计模式,只能在业务代码里将主要逻辑与次要逻辑混合起来,但 AOP 可以做到主要、次要业务逻辑隔离。
使用 AOP 就是在定义在哪些地方(类、方法)切入,在什么地方切入(方法前、后、前后)以及做什么。
比如说,我们想在某个方法前后分别执行两个函数计算执行时间,下面是主要业务逻辑:
```java
@Component("work")
public class Work {
public void do() {
System.out.println("执行业务逻辑");
}
}
```
再定义切面方法:
```java
@Component
@Aspect
class Broker {
@Before("execution(* xxx.Work.do())")
public void before(){
// 记录开始时间
}
@After("execution(* xxx.Work.do())")
public void after(){
// 计算时间
}
}
```
再通过 xml 定义扫描下这两个 Bean,就可以在运行 `work.do()` 之前执行 `before()`,之后执行 `after()`
还可以完全覆盖原函数,利用 `joinPoint.proceed()` 可以执行原函数:
```java
@Component
@Aspect
class Broker {
@Around("execution(* xxx.Work.do())")
public void around(ProceedingJoinPoint joinPoint) {
// 记录开始时间
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
// 计算时间
}
}
```
关于表达式 `"execution(* xxx.Work.do())"` 是用正则的方式匹配,`*` 表示任意返回类型的方法,后面就不用解释了。
可以看到,我们可以在不修改原方法的基础上,在其执行前后增加自定义业务逻辑,或者监控其报错,非常适合做次要业务逻辑,且由于不与主要业务逻辑代码耦合,保证了代码的简洁,且次要业务逻辑不容易遗漏。
## 总结
IOC 特别适合描述业务模型,后端天然需要这一套,然而随着前端越做越重,如果某个业务场景下需要将部分业务逻辑放到前端,也是非常推荐使用 IOC 设计模式来做,这是后端沉淀了近 20 年的经验,没有必要再另辟蹊径。
AOP 对前端有帮助但没有那么大,因为前端业务逻辑较为分散,如果要进行切面编程,往往用 `window` 事件监听来做会更彻底,可能这都是前端没有流行 AOP 的原因。当然前端约定大于配置的趋势下,比如打点或监控都集成到框架内部,往往也做到了业务代码无感,剩下的业务代码也就没有 AOP 的需求。
最后,spring 的低侵入式设计,使得业务代码不用关心框架,让业务代码能够快速在不同框架间切换,这不仅方便了业务开发者,更使得 spring 走向成功,这是前端还需要追赶的。
> 讨论地址是:[精读《Spring 概念》· Issue #265 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/265)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
@@ -0,0 +1,783 @@
bi-designer 是阿里数据中台团队自研的前端搭建引擎,基于它开发了阿里内部最大的数据分析平台,以及阿里云上的 QuickBI。
> bi-designer 目前没有开源,因此文中使用的私有 npm 源 `@alife/bi-designer` 是无法在公网访问的。
本文介绍 bi-designer 设计器的使用 API。
bi-designer 设计有如下几个特点:
- **心智统一:编辑模式与渲染模式统一**。
- **通用搭建:支持接入任意通用 npm 组件**。
- **低入侵:围绕数据分析能力做了增强,但对组件代码无入侵**。
## 渲染画布
做搭建,第一步是将画布渲染出来,需要用到 `Designer``Canvas` 组件:
```jsx
import { Designer, Canvas } from '@alife/bi-designer'
export () => (
<Designer>
<Canvas />
</Designer>
)
```
- `Designer`:数据容器,用于管理渲染引擎数据流。
- 参数 `defaultPageSchema`:页面 DSL 默认值。
- 参数 `defaultMode`:控制编辑渲染状态,`edit` or `render`
- `Canvas`:渲染画布的所有组件,会根据 DSL 结构将组件一一渲染出来。
## 编辑模式
编辑模式 = 渲染画布(编辑模式)+ 拓展一些自定义面板。
```jsx
import { Designer, Canvas } from '@alife/bi-designer'
export () => (
<Designer defaultMode="edit">
<div>Header</div>
<Canvas />
<div>Footer</div>
</Designer>
)
```
编辑模式的拓展采用了 JSX 模式,没有增加任何新的语法,只要放置任意数量的组件,并将画布 `Canvas` 摆放在想要的位置即可。
`defaultMode` 描述了当前引擎所处状态,有 `edit``render` 两个可选值,可以通过 `{ mode } = useDesigner(modeSelector)` 获取。bi-designer 没有对 `mode` 做任何特殊处理,我们可以在 panel、组件中判断不同的 `mode` 走不同的逻辑,以此区分编辑与渲染态。
## 页面 DSL 结构
`pageSchema` 描述了页面 DSL 信息,其结构是一个 `Map<组件 id, 组件实例信息>`
这里统一一下名词:
- 组件实例信息:`componentInstance`
- 组件元信息:`componentMeta`
那么 `pageSchema` 的结构大致如下:
```json
{
"componentInstances": {
"1": {
"id": "1",
"componentName": "root",
},
"2": {
"id": "2",
"parentId": "1",
"componentName": "button",
}
}
}
```
根据 `id` `parentId` 关系描述了组件父子关系,对于同一个父节点在流式布局下的顺序,还会增加 `index` 标记顺序。
## 注册组件
DSL 描述信息中最重要的是 `componentName`,为了告诉渲染引擎这个组件是什么,我们需要将组件元信息(`componentMetas`)传递给 `Designer`
```jsx
import { Designer, Canvas, Interfaces } from '@alife/bi-designer'
export () => (
<Designer componentMetas={componentMetas}>
<Canvas />
</Designer>
)
const componentMetas: Interfaces.ComponentMetas = {
button: {
componentName: 'button',
element: Button
}
}
```
关于 `componentMeta` 会在下一篇精读详细介绍,这里只说明两个最重要的属性:
- `componentName`:组件名,唯一。
- `element`:组件 UI 对象,对应一个 React 组件实例。
注意这里就留下了不少拓展空间,`componentMetas` 可以存储在服务端,`element` 可以远程异步加载,也可以在项目代码中固化,但传递给渲染引擎的 API 是固定的。
## 布局
bi-designer 支持流式布局、磁贴布局、自由布局三种模式,通过 `Designer.layout` 属性定义:
```jsx
import { Designer, Canvas, Interfaces } from '@alife/bi-designer'
import { LayoutMover } from '@alife/bi-designer-stream-layout'
export () => (
<Designer layout={LayoutMover}>
<Canvas />
</Designer>
)
```
我们提供了三种不同的布局包,切换对应的包即可切换布局,你甚至可以再包裹一层,通过代码控制在运行时切换布局。
`layout` 会包裹在每个组件外层,无论是流式、磁贴还是自由布局,都可以通过附着在每个组件外层来实现。
## 操作/获取画布内容
只要在数据容器 `Designer` 下,就可以通过 `useDesigner()` 获取画布信息或者修改画布内容。
举个例子,比如实现组件配置面板,需要获取到 **当前选中组件**,以及实现操作 **更新 DSL 中某个组件信息**
```jsx
import { Designer, Canvas, useDesigner, selectedComponentsSelector } from '@alife/bi-designer';
const EditPanel = () => {
const { updateComponentById, selectedComponents } =
useDesigner(selectedComponentsSelector());
// 在合适的时候调用 updateComponentById 更新 selectedComponents
// 渲染组件配置表单..
}
export () => (
<Designer>
<Canvas />
<EditPanel />
</Designer>
)
```
我们在 `Canvas` 下面渲染了一个自定义组件 `EditPanel` 作为组件配置面板,这个配置面板中,最重要的是这块代码:
```jsx
import { useDesigner, selectedComponentsSelector } from '@alife/bi-designer';
const { updateComponentById, selectedComponents } =
useDesigner(selectedComponentsSelector());
```
- `useDesigner` 是 React Hook,导出的函数都是静态的,不会因为画布信息变更而导致组件重渲染。
- 如果需要监听一些会变化的元素,比如当前选中组件,就需要用 Selector 完成,当这些信息变更时,使用了这些 Selector 的组件也会重渲染,具体 Selector 有很多,比如:
- `selectedComponentsSelector`: 当前选中的组件。
- `pageSchemaSelector`: 当前画布 DSL。
- `modeSelector`: 当前渲染模式。等等。
- 对画布组件操作有几个重要的静态方法,包括:
- `updateComponentById`: 更新某个 id 组件信息。
- `addComponent`: 添加组件。
- `deleteComponent`: 删除组件。
- `moveComponent`: 移动组件。等等。
- 除此之外,`useDesigner` 还提供了很多有用的方法,在用到时再介绍。
## 主题风格
通过 `pageSchema.theme` 设置主题风格:
```jsx
import { Designer } from '@alife/bi-designer'
const App = () => (
<Designer
defaultPageSchema={{
theme: { primaryColor: '#333' }
}}
/>
)
```
我们也可以在运行时使用 `setTheme` 动态修改主题风格,做到动态切换主题:
```jsx
const { setTheme, theme } = useDesigner();
return <Button onClick={() => {
setTheme({
...theme,
primaryColor: '#ffffff'
})
}} />
```
这些主题颜色,组件可以通过 css 变量拿到:
```css
.ok-button {
color: var(--primaryColor);
}
```
## 获取组件数据
数据分析引擎中,组件是由数据驱动展示的,这些数据可能来自 OLAP 数据集,或者普通 URL 接口,但无论如何数据都是一个组件重要组成部分,因此对组件的取数与数据操作是 bi-designer 的一个重点。
可以利用 `fetchStateSelector` 获取任意组件的数据信息,包括取数状态、数据、是否有查询错误等:
```jsx
import { useDesigner, fetchStateSelector } from '@alife/bi-designer';
const App = () => {
const { fetchState } = useDesigner(fetchStateSelector(componentInstance.id));
console.log(
fetchState.isFetching, // 是否在取数中
fetchState.isFilterReady, // 筛选条件是否准备好了
fetchState.data, // 取数结果
fetchState.error, // 取数错误,如果取数阶段报错的话
)
}
```
bi-designer 将所有组件的取数状态统一管理,因此可以跨组件获取数据信息,实现一些复杂需求:比如某些组件配置面板要获取组件取数结果填充配置表单。
## 组件加载器
组件加载器 `ComponentLoader` 可以加载任意组件, `Canvas` 就是基于此实现的。
### 加载画布中已有组件
通过申明 id 加载一个画布中已有组件,与其共享同一套数据:
```jsx
import { ComponentLoader } from '@alife/bi-designer'
const App = () => {
return <ComponentLoader id="some-id-already-exist" />
}
```
### 加载一个额外的新组件
如果这个组件不需要响应事件,只是做简单的渲染,那就不需要记录到数据流中,此时仅申明 `componentName` 即可:
```jsx
import { ComponentLoader } from '@alife/bi-designer'
const App = () => {
return <ComponentLoader componentName="button" />
}
```
但这种方式加载的组件存在如下问题:
- 其组件 `id` 不会存储到 `pageSchema` ,后端可能无法做一些校验。
- 无法响应事件,因为事件响应前提是组件信息存在于 `pageSchema` 中。
### 加载一个有事件功能的额外新组件
通过申明 `id``componentName` 加载一个全新组件,为了在其销毁时做有效清理,请将其 id 记录到 `useKeepComponentLoaders` 中。
```jsx
import { ComponentLoader, useDesigner } from '@alife/bi-designer'
const App = () => {
const { useKeepComponentLoaders } = useDesigner();
useKeepComponentLoaders(["1"])
return <ComponentLoader id="1" componentName="button" />
}
```
通过此方式加载的组件会在其渲染时记录到 `pageSchema` 中。
> 注意,此时 id 与仅写一个 id 时含义不同,这个 id 在当前父组件作用域下唯一就可以。
## 全屏功能
所有组件实例都可以存在副本,共享一套状态数据,可以通过 `ComponentLoader` 随时渲染一个组件副本:
```jsx
import { ComponentLoader } from '@alife/bi-designer'
// ... 任意可拿到 componentInstance 处
return (
<ComponentLoader id={componentInstance.id} />
)
```
那么全屏就是将组件渲染到一个新容器内,非常 easy。
## 局部配置覆盖
可以通过 `DesignerProvider` 实现干涉其子元素 `useDesigner` 获取信息的能力:
```jsx
import { DesignerProvider, ComponentLoader } from '@alife/bi-designer';
// 某个组件内,或者某个 UI 内以 render 模式加载组件
// ...
return (
<DesignerProvider mode="render">
<ComponentLoader id={id} />
</DesignerProvider>
)
```
举个例子,比如在编辑模式下要全屏预览组件,可以通过 `ComponentLoader + id` 把某个画布组件实例渲染到弹出的 Modal 中,但问题是当前属于编辑模式,组件还可以被拖拽甚至响应编辑效果,我们只想让局部变成渲染状态,怎么做呢?
答案就是通过 `DesignerProvider` 包裹这个 Modal,这个 Modal 内部无论是组件还是其他 Panel 代码通过 `const { mode } = useDesigner(modeSelector)` 拿到的值都会被强制覆盖为 `render`
## 配置国际化
国际化信息在 `pageSchema.i18n` 定义:
```jsx
import { Designer } from '@alife/bi-designer'
const App = () => (
<Designer
defaultPageSchema={{
i18n: {
"zh-CN": {
你好: "你好",
中国: "中国"
},
"en-US": {
你好: "Hello",
中国: "China"
}
}
}}
defaultLocaleKey="zh-CN"
/>
)
```
- `defaultLocaleKey`: 默认国际化语言,可以通过 `{ setLocaleKey } = useDesigner()` 动态改变。
这样在 DSL 中通过描述 `JSExpression` 表达式的 `this.i18n` 访问:
```json
{
"componentInstances": {
"1": {
"id": "1",
"componentName": "button",
"props": {
"text": {
"type": "JSExpression",
"value": "this.i18n['你好']"
}
}
}
}
}
```
## 容器拓展组件 props
`componentMeta.container` 可以定义组件外层容器,但有的时候我们想在容器做一点事情,比如获取宽高,以 props 的方式传递给子组件。
因为子组件以 `children` 的方式书写不易拓展,因此提供了 `PropsProvider` 来拓展子组件拿到的 props
```jsx
import { Interfaces, PropsProvider } from '@alife/bi-designer'
const ComponentContainer = ({ children }) => {
return (
// 注入 width 和 height
<PropsProvider width={100} height={100}>
{children}
</PropsProvider>
)
}
const Element = ({ width, height }) => {
// width=100
// height=100
}
const componentMeta: Interfaces.ComponentMeta = {
element: Element,
container: ComponentContainer
};
```
上面的例子中,因为 `container` 注入了 `width`,因此组件可以通过 `props.width` 拿到容器注入的值。
## 撤销重做
撤销重做按钮在基于每个搭建系统都有,在 bi-designer 的使用方式是这样:
```jsx
import { useDesigner } from '@alife/bi-designer'
export default () => {
const { undo, redo } = useDesigner()
// 撤销调用 undo()
// 重做调用 redo()
}
```
是不是觉得很简单?是的,因为所有值得撤销重做的操作在引擎内部使用了 `HistoryManager` 管理,因此引擎知道每一个可以被撤销或者重做的操作,直接调用函数即可。
## 组件复制
执行 `copyComponent` 命令即可复制组件,比如:
```jsx
const App() {
const { copyComponent } = useDesigner()
// 复制组件 copyComponent(componentInstance)
}
```
`copyComponent` 的参数分别为:
```jsx
function copyComponent(
componentInstance?: ComponentInstance,
parentId?: string,
index?: number
)
```
- 如不指定 `parentId` ,默认复制到自己父元素下。
- 如不指定 `index` ,默认复制到当前元素下方。
## 组件模版
如果觉得某些组件配置可能被复用,可以在画布组件右上角增加一个 “添加到组件模版” 按钮,bi-designer 也提供了生成、添加组件模版的方法。
### 创建组件模版
利用 `createCombine` 函数从画布中已有组件创建出组件模版,也可以将其生成结果持久化,作为一个固定的组件模版:
```jsx
const ComponentContainer: Interfaces.InnerComponentElement = ({ componentInstance }) => {
const { createCombine } = useDesigner();
const setToCombine = React.useCallback(() => {
// 创建组件模版
const combine = createCombine(componentInstance.id)
}, [createCombine]);
}
```
`createCombine` 的参数就是画布中组件的 `id`
### 添加组件模版到画布
利用 `addCombine` 函数将组件模版添加到画布,第一个参数就是上面生成的 `combine` 对象:
```jsx
const App = () => {
const { addCombine } = useDesigner();
const addComponent = React.useCallback(() => {
// 创建组件模版
const combine = addCombine(combine, parentId)
}, [addCombine]);
}
```
## 渲染完成标识
当画布中所有组件都完成渲染了,可能要做一些监控上报,或者告诉截图软件可以截图了,bi-designer 提供了这种回调时机 `onRendered`:
```jsx
import { Designer } from '@alife/bi-designer'
const App = () => (
<Designer
onRendered={errors => {
errors.map(each => {
// 错误组件 id
console.log(each.id)
// 错误信息
console.log(each.error)
})
// 渲染完毕
}}
/>
)
```
- `errors`: 如果有组件代码报错,引擎会吞掉这个错误保证其他组件正常渲染,并把错误组件的 id 和错误信息返回到这里。
## 自定义数据流
如果 `useDesigner` 提供的数据流无法满足业务需要,可以通过进行自定义拓展。
### 1. 拓展字段
举个例子,我们需要新增一个 `edges` 字段描述当前画布中有哪些 “边节点”:
```jsx
import { Designer } from '@alife/bi-designer';
const App = ({ defaultPageSchema }) => (
<Designer defaultPageSchema={{
...defaultPageSchema,
edges: []
}} />
)
```
可以看到,只要任意拓展 `pageSchema` 即可。
### 2. 通过 useDesigner 拿到拓展字段
首先定义一个 `edgesSelector`
```jsx
import { DesignerState } from '@alife/bi-designer';
export const edgesSelector = () => (state: DesignerState) => {
return {
// 从 pageSchema.edges 读取 edges
edges: state.pageSchema?.edges as Edge[],
};
};
```
在需要读取的地方结合 `useDesigner`
```jsx
import { useDesigner } from '@alife/bi-designer';
import { edgesSelector } from './selector'
const Panel = () => {
// 自带类型
const { edges } = useDesigner(edgesSelector())
}
```
### 3. 通过 useDesigner 修改拓展字段
通过 `setPageSchema` 更新拓展字段:
```jsx
import { useDesigner } from '@alife/bi-designer';
const Panel = () => {
const { setPageSchema } = useDesigner()
const handleChangeEdges = React.useCallback(newEdges => {
setPageSchema(pageSchema => ({
...pageSchema,
newEdges
}))
}, [setPageSchema])
}
```
总结一下,这个拓展字段由业务定义,透过 `useDesigner` 读与改,使业务数据管理方式更聚合。
## 存储临时非结构化数据
对于非结构化数据比如组件 `ref` 是不能存储到数据流的,既不能使用 `setPageSchema`,也不能调用 `updateComponentId` 存储到 `componentInstance` 中。
此时可以利用 `temporary` 进行临时数据存取,要注意非结构化数据是无法监听变化的,引用永远保持不变:
```jsx
import { useDesigner } from '@alife/bi-designer';
const App = () => (
const { temporary } = useDesigner()
// 写
temporary.set('component1', ref)
// 读
console.log(temporary.get('component1'))
)
```
temporary 本质是个 Map,所以拥有 Map 类型所有语法。
## 拦截画布操作
如果你限制某个低配版本只能在画布使用最多 50 个组件,我们需要阻止画布超过 50 个组件的添加,这个场景可以通过 `DesignerProps` 生命周期可以对画布操作进行拦截。
`shouldAddComponents()` 返回 `false` 可以阻止画布添加组件:
```jsx
import { Designer } from '@alife/bi-designer'
const App = () => (
<Designer
shouldAddComponents={({addedComponentInstancesArray, pageSchema}) => {
// 阻止添加
return false
}}
/>
)
```
- `addedComponentInstancesArray` :添加的组件, `ComponentInstance[]` 类型。
`shouldMoveComponents()` 返回 `false` 可以阻止画布移动组件:
```jsx
import { Designer } from '@alife/bi-designer'
const App = () => (
<Designer
shouldmoveComponents={({movedComponentInstancesArray, targetComponentInstance, pageSchema}) => {
// 阻止移动
return false
}}
/>
)
```
- `movedComponentInstancesArray` :移动的组件,`ComponentInstance[]` 类型。
- `taragetComponentInstance` :要移动到的父组件实例信息, `ComponentInstance` 类型。
`shouldDeleteComponents()` 返回 `false` 可以阻止画布删除组件:
```jsx
import { Designer } from '@alife/bi-designer'
const App = () => (
<Designer
shouldAddComponents={({deletedComponentInstancesArray, pageSchema}) => {
// 阻止删除
return false
}}
/>
)
```
- `deletedComponentInstancesArray` :删除的组件, `ComponentInstance[]` 类型。
## 仅刷新可视区域组件
默认组件都会以按需加载的方式渲染,即对于不在可视区域的组件,不会触发任何重渲染,以此提升交互操作的效率,以及首屏速度。
对于筛选条件等可能影响到其他组件的组件,可以通过 `ComponentMeta.keepActive` 强制保持激活状态:
```jsx
import { Interfaces } from '@alife/bi-designer'
const componentMeta: Interfaces.ComponentMeta = {
keepActive: true
}
```
- `keepActive`:组件始终保持激活状态,即不出现在可视区域也会被渲染与响应刷新,默认关闭。
对于特殊场景比如截图,可能要求所有组件强制为 `active` 状态,可以通过 `forceActive` 函数实现:
```jsx
import { Interfaces, useDesigner } from '@alife/bi-designer'
const Test: Interfaces.ComponentElement = () => {
const { forceActive, cancelForceActive } = useDesigner()
// forceActive() 强制所有组件 active
// cancelForceActive() 取消强制 active,组件根据实际情况 active
};
```
可以通过 `getSnapshot().actives` 获取任意组件当前瞬时 `active` 状态:
```jsx
import { useDesigner } from '@alife/bi-designer'
const Test = () => {
const { getSnapshot, id } = useDesigner()
// 当前组件激活状态
const active = getSnapshot().actives[id]
};
```
## 上下文数据对象
组件 DSL 描述中,表达式类型(`JSExpression`)可以通过 `this.` 访问到上下文数据对象。上下文数据对象符合如下规则:
- 任何组件都通过配置 `ComponentMeta.stateful` 持有上下文。
- 画布根节点 `root` 一定是 `stateful` 的。
- `JSFunction``JSExpression` 都可通过 `this.state` 访问上下文, `this.setState` 修改上下文。
举例子:
```jsx
// 初始化 pageSchema
const defaultPageSchema: Interfaces.PageSchema = {
componentInstances: {
test1: {
id: 'test1',
componentName: 'test',
parentId: 'jtw4x8ns',
index: 0,
props: {
variable: {
type: 'JSExpression',
value: 'this.state.variable + "%"',
},
onClick: {
type: 'JSFunction',
value: 'function onClick() { this.setState({ variable: 5 }) }',
},
},
}
}
};
```
这个例子中,组件调用 `this.props.onClick` 会修改上下文 `a=5` ,触发后,其 `this.props.variable` 拿到的值会变为 5% 。
任何组件或容器只要设置了 `stateful` 就可以持有状态:
```jsx
import { Interfaces } from '@alife/bi-designer'
const statefulComponentMeta: Interfaces.ComponentMeta = {
stateful: true
}
```
被有状态的容器包裹的组件 `this.state``this.setState` 都局限在当前状态容器内,也就是当前状态容器内组件的 state 是互通的,且一个有状态容器与外部环境是隔离的,可以独立运行。
## 工具类拓展
工具类拓展可以通过上下文访问,如下是拓展方式:
```jsx
import { Interfaces } from '@alife/bi-designer'
// DSL 中增加 utils 描述
const defaultPageSchema: Interfaces.PageSchema = {
utils: [
{
name: 'format',
type: 'function',
content: `function format(str){ return str + '%' }`,
},
]
};
```
- `name` :工具函数名。
- `type` :类型,包括 `npm``umd``function`
- `content` :内容。
用法:
```jsx
JSFunction JSExpression 都可以通过 this.utils 访问工具类拓展函数比如
// DSL 中增加 Expression 描述
const defaultPageSchema: Interfaces.PageSchema = {
componentInstances: {
test: {
id: 'tg43g42f',
componentName: 'expressionComponent',
index: 0,
props: {
variable: {
type: 'JSExpression',
value: 'this.utils.format("100")',
}
},
},
},
};
```
上面的例子中,组件拿到的 `props.variable` 值为 100% 。
## 总结
如果你认真看完了全文,就会发现,bi-designer 是一个集成了数据流的开发框架,而不仅是一个渲染引擎,但却可以和你现有的业务代码友好相处,没有入侵性。
像渲染完成标识、按需渲染、组件加载器、局部配置覆盖等功能是强依赖渲染引擎存在的,因此较难在剥离渲染引擎的条件下转换为代码,因为做 BI 分析工具毕竟不是做研发提效用,业务上没有出码的必要,因此我们会做许多依赖渲染引擎的能力增强。
更多数据分析特性的功能将在下一个话题 API 之组件说明。
> 讨论地址是:[精读《数据搭建引擎 bi-designer API-设计器》· Issue #267 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/267)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
File diff suppressed because it is too large Load Diff
+250
View File
@@ -0,0 +1,250 @@
筛选条件是 BI 搭建的核心概念,我们大部分所说的探索式分析、图表联动也都属于筛选条件的范畴,**其本质就是一个组件对另一个组件的数据查询起到筛选作用**。
## 筛选组件是如何作用的
我们最常见的筛选条件就是表单场景的查询控件,如下图所示:
<img width=300 src="https://img.alicdn.com/tfs/TB107njjRFR4u4jSZFPXXanzFXa-724-302.png">
若干 “具有输出能力” 的组件作为筛选组件,点击查询按钮时触发其作用组件重新取数。
注意这里 “具有输出能力” 的组件不仅是输入框等具有输入性质的组件,其实所有具备交互能力的组件都可以,甚至可以由普通组件承担筛选触发的能力:
<img width=300 src="https://img.alicdn.com/tfs/TB1V.sVgSslXu8jSZFuXXXg7FXa-858-196.png">
一个表格的表头点击也可以触发筛选行为,或者柱状图的一个柱子被点击都可以,只要进行到这层抽象,**组件间联动本质也属于筛选行为**。
同样重要的,筛选作用的组件也可以是具备输入能力的组件:
<img width=450 src="https://img.alicdn.com/tfs/TB1qqrxUpT7gK0jSZFpXXaTkpXa-1280-198.png">
当目标组件是具备筛选能力组件时,这就是筛选联动场景了,所以 **筛选联动也属于普通筛选行为**。至于目标组件触发取数后,是否立即修改其筛选值,进而触发后续的筛选联动,就完全由业务特性决定了。
一个组件也可以自己联动自己筛选,比如折线图点击下钻的场景,就是自己触发了筛选,作用到自己的例子。
## 什么是筛选组件
**任何组件都可以是筛选组件**
可能最容易理解的是输入框、下拉框、日期选择器等具备输入特征的组件,这些组件只能说天然适合作为筛选组件,但不代表系统设计要为这些组件特殊处理。
扩大想一想,其实普通的按钮、表格、折线图等等 **具有展示属性的组件也具有输入特性的一面**,比如按钮被点击时触发查询、单元格被点击时想查询当前城市的数据趋势、折线图某条线被点击时希望自身从年下钻到月等等。
所以 **不存在筛选组件这概念,而是任何组件都具有筛选的能力**,因此筛选是一种任何组件都具有的能力,而不局限在某几个组件上,一旦这么设计,可以做到以下几点:
1. 实现输入类组件到展示类组件的筛选,符合基本筛选诉求。
2. 实现展示类组件到展示类组件的筛选,属于图表联动图表的高级功能。
3. 实现输入类组件到输入类组件的筛选,属于筛选联动功能。
4. 实现组件自身到自身的筛选,实现下钻功能。
下面介绍 bi-designer 的筛选条件设计。
## 筛选条件设计
基于上述分析,bi-designer 在组件元信息中没有增加所谓的筛选组件类型,而是将其设定为一种筛选能力,任何组件都能触发。
### 如何触发筛选
组件调用 `onFilterChange` 即可完成筛选动作:
```jsx
import { useDesigner } from "@alife/bi-designer";
const InputFilter = () => {
const { onFilterChange } = useDesigner();
return (
<input onChange={(event) => () => onFilterChange(event.target.value)} />
);
};
```
但这种开发方式违背了 **低侵入** 的设计理念,我们可以采用组件与引擎解构的方式,让输入框变更的时候直接调用 `props.onChange` ,这个组件保持了最大的独立性:
```jsx
const InputFilter = ({ onChange }) => {
return <input onChange={(event) => () => onChange(event.target.value)} />;
};
```
那渲染引擎怎么将 `onFilterChange` 映射到 `props.onChange` 呢?如下配置 DSL 即可:
```json
{
"props": {
"onChange": {
"type": "JSExpression",
"value": "this.onFilterChange"
}
}
}
```
### 筛选影响哪些组件
一般筛选组件会选择作用于的目标组件,类似下图:
<img width=300 src="https://img.alicdn.com/tfs/TB1RJHPUxD1gK0jSZFsXXbldVXa-768-486.png">
这些信息会存储在筛选组件的组件配置中,即 `componentInstance.props`,筛选目标组件在 `componentMeta.eventConfigs` 组件元信息的事件中配置:
```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
eventConfigs: ({ componentInstance }) =>
componentInstance.props.targets?.map((target) => ({
// 筛选取数
type: "filterFetch",
// 触发组件
source: componentInstance.id,
// 作用组件
target: target.id,
})),
};
```
如上所示,假设作用于组件存储在 `props.targets` 字段中,我们将其 `map` 一下都设置为 `filterFetch` 类型,表示筛选作用,`source` 触发源是自己,`target` 目标组件是存储的 `target.id`
这样当 `source` 组件调用了 `onFilterChange``target` 组件就会触发取数,并在取数参数中拿到作用于其的筛选组件信息与筛选值。
### 组件如何感知筛选条件
组件取数是结合了筛选条件一起的,只要如上设置了 `filterFetch`,渲染引擎会自动在计算取数参数的回调函数 `getFetchParam` 中添加 `filters` 代表筛选组件信息,组件可以结合自身 `componentInstance``filters` 推导出最终取数参数:
<img width=300 src="https://img.alicdn.com/tfs/TB1tDDTUuH2gK0jSZJnXXaT1FXa-870-434.png">
最终,组件元信息只要写一个 `getFetchParam` 回调函数即可,**可以自动拿到作用于它的筛选组件,而不用关心是哪些配置导致了关联,只要响应式的去处理筛选作用即可**。
```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
// 组装取数参数
getFetchParam: ({ componentInstance, filters }) => {
// 结合 componentInstance 与 filters.map... 返回取数参数
},
};
```
## 筛选组件间联动带来的频繁取数问题
对于筛选联动的复杂场景,会遇到频繁取数的问题。
假设国家、省、市三级联动筛选条件同时 `filterFetch` 作用于一个表格,这个表格取数的筛选条件需要同时包含国家、省、市三个参数,但我们又设置了 国家、省、市 这三个筛选组件之间的 `filterFetch` 作为筛选联动,那么国家切换后、省改变、联动市改变,这个过程筛选值会变化三次,但我们只想表格组件取数函数仅执行最后的一次,怎么办呢?
<img width=350 src="https://img.alicdn.com/tfs/TB1CFD9UBr0gK0jSZFnXXbRRXXa-984-656.png">
如上图所示,其实每个筛选条件在渲染引擎数据流中还存储了一个 `ready` 状态,表示筛选条件是否就绪,**一个组件关联的筛选条件只要有一个 `ready` 不为 `true`,组件就不会触发取数**。
因此我们需要在筛选变化的过程中,总是保证一个筛选组件的 `ready``false`,等筛选间联动完毕了,所有筛选器的 `ready``true`,组件才会取数,我们可以使用 `filterReady` 筛选依赖配置:
```jsx
import { Interfaces, createComponentInstancesArray } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
eventConfigs: ({ componentInstance }) =>
componentInstance.props.targets?.map((target) => ({
// 筛选就绪依赖
type: "filterReady",
// 触发组件
source: componentInstance.id,
// 作用组件
target: target.id,
})),
};
```
这样配置后,当 `source` 组件触发 `onFilterChange` 后,`target` 组件的筛选 `ready` 会立即设置为 `false`,只有 `target` 组件取完数后主动触发 `onFilterChange` 才会将自己的 `ready` 重新置为 `true`。**That'a all,其他流程没有任何感知**。
## 若干筛选组件聚合成一个查询控件
除了联动外,也会存在防止频繁查询的诉求,希望将多个筛选条件绑定成一个大筛选组件,在点击 “查询” 按钮时再取数:
<img width=400 src="https://img.alicdn.com/tfs/TB1nmHVUuH2gK0jSZJnXXaT1FXa-972-174.png">
可以利用 **筛选作用域** 轻松实现此功能,只需要两步:
### 筛选组件设置独立筛选作用域
```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
// 通过 componentInstance 判断,如果是全局筛选器内部,则设置 filterScope
filterScope: ({ componentInstance }) => ["my-custom-scope-name"],
};
```
这样,这批筛选组件就与其作用的组件属于不同的 **筛选作用域** 了,所以筛选不会对其立即生效,功能实现了一半。
### 确认按钮点击时调用 `submitFilterScope`
```jsx
import { useDesigner } from '@alife/bi-designer'
const componentMeta: Interfaces.ComponentMeta = {
const { submitFilterScope } = useDesigner()
// 点击确认按钮时,调用 submitFilterScope('my-custom-scope-name')
};
```
你可以在点击查询按钮后调用 `submitFilterScope` 并传入对应作用域名称,这样作用域内筛选组件就会立即对其 `target` 组件生效了。
至于确认按钮、UI 上的聚合,这些你可以写一个自定义组件去做,利用 `ComponentLoader` 把筛选组件聚合到一起加载,总之功能与 UI 是解耦的。
如果你对原理感兴趣,可以再多看一下这张图:
<img width=400 src="https://img.alicdn.com/tfs/TB1eZPJhAcx_u4jSZFlXXXnUFXa-1082-645.png">
### 突破筛选作用域
然而实际场景中,可能存在更复杂的组合,见下面的例子:
<img width=350 src="https://img.alicdn.com/tfs/TB1cfGNiIVl614jSZKPXXaGjpXa-966-600.png">
筛选器 1 同时对 筛选器 2、表格 产生筛选作用 `filterFetch`,但对 表格 的作用希望通过查询按钮拦截住,而对 筛选器 2 的作用希望能立即生效,对于这个例子有两种方式解决:
最简单的方式就是将 筛选器 1、筛选器 2 设置为相同作用域 `group1`,这样就通过作用域分割自然实现了效果,**而且这本质上是两个筛选器 UI 不在一起,但筛选作用域相同的例子**:
<img width=350 src="https://img.alicdn.com/tfs/TB1_kn1UpT7gK0jSZFpXXaTkpXa-1056-660.png">
但是再变化一下,如果筛选器 2 也对表格产生筛选作用,那我们将 筛选器 1、筛选器 2 放入同一个 `group1` 等于对表格的查询都会受到 “查询” 按钮的控制,但 **我们又希望筛选器 2 可以立即作用于表格**
<img width=350 src="https://img.alicdn.com/tfs/TB1ftP3Urr1gK0jSZFDXXb9yVXa-968-602.png">
如图所示,我们只能将 筛选器 1 的筛选作用域设置为 `group1`,这样 筛选器 2 与 表格 属于同一个筛选作用域,他们之间筛选会立即生效,我们只要解决 筛选器 1 不能立即作用于 筛选器 2 的问题即可,可以通过 `ignoreFilterScope` 方式突破筛选作用域:
```jsx
import { Interfaces } from "@alife/bi-designer";
const componentMeta: Interfaces.ComponentMeta = {
eventConfigs: ({ componentInstance }) =>
componentInstance.props.targets?.map((target) => ({
// 筛选取数
type: "filterFetch",
// 触发组件
source: componentInstance.id,
// 作用组件
target: target.id,
// 突破筛选作用域
ignoreFilterFetch: true,
})),
};
```
我们只要在 `source: 筛选器1` `target: 筛选器2``filterFetch` 配置中,将 `ignoreFilterFetch` 设置为 `true`,这个 `filterFetch` 就会忽略筛选作用域,实现立即 筛选器 1 立即作用到 筛选器 2 的效果。
## 总结
你还有哪些特殊的筛选诉求?可以用这套筛选设计解决吗?
> 讨论地址是:[精读《BI 搭建 - 筛选条件》· Issue #270 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/270)
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
> 关注 **前端精读微信公众号**
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)
-14
View File
@@ -11,17 +11,3 @@
## 关注前端精读微信公众号
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
## Special Sponsors
<table>
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://e.coding.net/?utm_source=weekly" target="_blank">
<img width="300" src="https://img.alicdn.com/tfs/TB107D.QbrpK1RjSZTEXXcWAVXa-1000-332.png">
</a>
</td>
</tr>
</tbody>
</table>