乐趣区

关于javascript:可视化搭建-定义联动协议

尽管底层框架提供了通用的组件值与联动配置,能够建设对组件任意 props 的映射,但这只是一个能力,还不是协定。

业务层是能够确定一个协定的,还要让这个协定具备拓展性。

咱们先从使用者角度设计 API,再看看如何依据已有的组件值与联动能力去实现。

设计联动协定

首先,不同的业务方会定义不同的联动协定,因而该联动协定须要通过拓展的形式注入:

import {createDesigner} from 'designer'
import {onReadComponentMeta} from 'linkage-protocol'

return <Designer onReadComponentMeta={onReadComponentMeta} />

首先可视化搭建框架反对 onReadComponentMeta 属性,用于拓展所有已注册的组件元信息,而联动协定的拓展就是基于组件值与组件联动能力的,因而这种是最正当的拓展形式。

之后咱们就注册了一个固定的联动协定,它形如下:

{
  "componentName": "input",
  "linkage": [{
    "target": "input1",
    "do": {"value": "{{ $self.value +'hello'}}"
    }
  }]
}

只有在组件实例上定义 linkage 属性,就能够失效联动。比方下面的例子:

  • target: 联动指标。
  • do: 联动成果,比方该例子为,组件 ID 为 input1 的组件,组件值同步为以后组件实例的组件值 + 'hello'
  • $self: 形容本人实例,比方能够从 $self.value 拿到本人的组件值,从 $self.props 拿到本人的 props。

更近一步,target 还能够反对数组,就示意同时对多个组件失效雷同规定。

咱们还能够反对更简单的语法,比方让该组件能够同步其余组件值:

{
  "componentName": "input",
  "linkage": [{"deps": ["input1", "input2"]
    "props": {"text": "{{ $deps[0].value + deps[1].value }}"
    }
  }]
}

下面的例子示意,该组件实例的 props.text 同步为 input1 + input2 的组件值:

  • deps: 形容依赖列表,每个依赖实例都能够在表达式里用 $deps[] 拜访到,比方 $deps[0].props 能够拜访组件 ID 为 input1 组件的 props。
  • props: 同步组件的 props。

如果定义了 target 则作用于指标组件,未定义 target 则作用于本身。但无论如何,表达式的 $self 都指向本人实例。

总结一下,该联动协定容许组件实例实现以下成果:

  1. 设定组件值、组件 props 的联动成果。
  2. 能够将本人的组件值同步给组件实例,也能够将其余组件值同步给本人。

基本上,能够满足任意组件联动到任意组件的诉求。而且甚至反对组件间传递,比方 A 组件的组件值同步组件 B,B 组件的组件值同步组件 C,那么 A 组件 setValue() 后,组件 B 和 组件 C 的组件值会同时更新。

实现联动协定

以上联动协定只是一种实现,咱们能够基于组件值与组件联动设定任意协定,因而实现联动协定的思维具备通用性,但为了不便,咱们以下面说的这个协定为例子,阐明如何用可视化搭建框架的根底性能实现协定。

首先解读组件实例的 linkage 属性,将联动定义转化为组件联动关系,因为联动协定实质上就是产生了组件联动。接下来代码片段比拟长,因而会尽量应用代码正文来解释:

const extendMeta = {
  // 定义 valueRelates 关系,就是咱们上一节提到的定义组件联动关系的 key
  valueRelates: ({componentId, selector}) => {
    // 利用 selector 读取组件实例 linkage 属性
    // 因为 selector 的个性,会实时更新,因而联动协定变动后,联动状态也会实时更新
    const linkage = selector(({componentInstance}) => componentInstance.linkage)

    // 返回联动数组,构造: [{sourceComponentId, targetComponentId, payload}]
    return linkage.map(relation => {const result = [];

        // 定义此类联动类型,就叫做 simpleRelation
        const payload = {
          type: 'simpleRelation',
          do: JSON.parse(JSON.stringify(relation.do)
              // 将 $deps[index] 替换为 $deps[componentId]
              .replace(/\$deps\[([0-9]+)\]/g,
                (match: string, index: string) =>
                  `$deps['${relation.deps[Number(index)]}']`,
              )
              // 将 $self 替换为 $deps[componentId]
              .replace(/\$self/g, () => `$deps['${componentId}']`),
          ),
        };
        // 通过下面的代码,表达式里无论是 $self. 还是 $deps[0]. 都转化为了
        // $deps[componentId] 这个具体组件 ID,这样前面解决流程会简略而对立

        // 读取 deps,并定义 dep 组件作为 source,target 作为指标组件
        // 这是最要害的一步,将 dep -> target 关系绑定上
        relation.target.forEach((targetComponentId) => {if (relation.deps) {relation.deps.forEach((depIdPath: string) => {
              result.push({
                sourceComponentId: depIdPath,
                targetComponentId,
              });
            });
          }

          // 定义本人到 target 指标组件的联动关系
          result.push({
            sourceComponentId: componentId,
            targetComponentId,
            payload,
          });
        });

        return result;
      }).flat()}
}

上述代码利用 valueRelates,将联动协定的关联关系提取进去,转化为值联动关系。

接着,咱们要实现 props 同步性能,实现这个性能天然是利用 runtimeProps 以及 selector.relates,将关联到以后组件的组件值,依照联动协定的表达式执行,并更新到对应 key 上,上面是大抵实现思路:

const extendMeta = {runtimeProps: ({ componentId, selector, getProps, getMergedProps}) => {
    // 拿到作用于本人的值关联信息: relates
    const relates = selector(({relates}) => relates);

    // 记录最终因为值联动而影响的 props
    let relationProps: any = {};

    // 记录关联到本人的组件此时组件值
    const $deps = relates?.reduce((result, next) => ({
        ...result,
        [next.componentId]: {value: next.value,},
      }),
      {},);

    // 为了让每个依赖变动都能失效,多对一每一项 do 都带过去了,须要依照 relationIndex 先去重
    relates
      .filter((relate) => relate.payload?.type === 'simpleRelation')
      .forEach((relate) => {
        const expressionArgs = {// $deps[].value 指向依赖的 value
          $deps,
          get,
          getProps: relate.componentId === componentId ? getProps : getMergedProps,
        };

        // 解决 props 联动
        if (isObject(relate.payload?.do?.props)) {Object.keys(relate.payload?.do?.props).forEach((propsKey) => {
            relationProps = set(
              propsKey,
              selector(() =>
                  // 这个函数是要害,传入组件 props 与表达式,返回新的 props 值
                  getExpressionResult(get(propsKey, relate.payload?.do?.props),
                    expressionArgs,
                  ),
                {
                  compare: equals,
                  // 依据表达式数量可能不同,所以不启用缓存
                  cache: false,
                },
              ),
              relationProps,
            );
          });
        }
      });

    return relationProps
  }
}

其中比较复杂函数就是 getExpressionResult,它要解析表达式并执行,原理就是利用代码沙盒执行字符串函数,并利用正则替换变量名以匹配上下文中的变量,大抵代码如下:

// 代码执行沙盒,传入字符串 js 函数,利用 new Function 执行
function sandBox(code: string) {
  // with 是要害,利用 with 定制代码执行的上下文
  const withStr = `with(obj) {${code}
  }`;
  const fun = new Function('obj', withStr);

  return function (obj: any) {return fun(obj);
  };
}

// 获取沙盒代码执行后果,能够传入参数笼罩沙盒内上下文
function getSandBoxReturnValue(code: string, args = {}) {
  try {return sandBox(code)(args);
  } catch (error) {
    // eslint-disable-next-line no-console
    console.warn(error);
  }
}

// 如果对象是字符串则间接返回,是 {{}} 表达式则执行后返回
function getExpressionResult(code: string, args = {}) {if (code.startsWith('{{') && code.endsWith('}}')) {// {{}} 内的表达式
    let codeContent = code.slice(2, code.length - 2);

    // 将形如 $deps['id'].props.a.b.c
    // 转换为 get('a.b.c', getProps('id'))
    codeContent = codeContent.replace(/\$deps\[['"]([a-zA-Z0-9]*)['"]\]\.props\.([a-zA-Z0-9.]*)/g,
      (str: string, componentId: string, propsKeyPath: string) => {return `get('${propsKeyPath}', getProps('${componentId}'))`;
      },
    );

    return getSandBoxReturnValue(`return ${codeContent}`, args);
  }

  return code;
}

其中 with 是沙盒执行时替换代码上下文的要害。

总结

componentMeta.valueRelatescomponentMeta.runtimeProps 能够灵便的定义组件联动关系,与更新组件 props,利用这两个申明式 API,甚至能够实现组件联动协定。总结一下,蕴含以下几个关键点:

  1. depstarget 利用 valueRelates 转化为组件值关联关系。
  2. 将联动协定定义的绝对关系(比拟容易写于容易记)转化为相对关系(利用 componentId 定位),不便框架解决。
  3. 利用 with 执行表达式上下文。
  4. 利用 runtimeProps + selector 实现注入组件 props 与响应联动值 relates 变动,从而实现按需联动。

探讨地址是:精读《定义联动协定》· Issue #471 · dt-fe/weekly

如果你想参加探讨,请 点击这里,每周都有新的主题,周末或周一公布。前端精读 – 帮你筛选靠谱的内容。

关注 前端精读微信公众号

<img width=200 src=”https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg”>

版权申明:自在转载 - 非商用 - 非衍生 - 放弃署名(创意共享 3.0 许可证)

退出移动版