一个模块化+低代码的页面生成器的开发记录(要点+BUG篇)

系列目录

(接上文)

多平台响应式方案

我司该项目实际上为多端平台,包含Web/小程序/APP/MS-Teams/Adobe等,所以开发时需要考虑编辑后如何在多端展示。

1. 移动端(除小程序)处理

如前所述,本项目中模块的JSON定义支持设置移动端、分平台样式样式,所以默认支持移动端响应式,具体可以见【预览】窗口的移动端效果:

preview-mobile.png

既然已支持响应式,那么不难想到,其他非浏览器端的平台,最简单的方式是使用WebView,直接嵌入H5的页面。通常的店铺主页,除了自定义的部分,还会有一些固定不变的组成部分,如头部Banner,底部Tabbar等,以美的旗舰店的京东主页为例:

app-media-homepage-marked.jpg

本项目编辑器生成的页面,其实只展示在上图中间的【自定义区域】,顶部和底部的小程序是各店铺通用的,由其他逻辑生成。如果采用WebView方案,可在中间【自定义区域】设置一个WebView窗口,直接指向web端页面即可。我司APP端也是这样实现的。

2. 小程序端处理

然而这种方案在小程序上碰壁了。因为小程序的web-view有个强制规定:

wx-mp-webview-doc.png

即小程序的WebView必须是全屏的,无法设置大小,这与上面分析的需求不符。理论上也许可以使用<cover-view>等方案局部覆盖实现,但效果显然好不到哪里去。所以WebView方案在小程序端行不通,需要另外实现一套小程序的渲染逻辑。

由于我们项目采用了JSON => DOM的方案,这种分层思想类似于React Native的逻辑,将结构抽象成的JSON后,只要各端处理好实现从节点到DOM的渲染,那边就能实现一次编辑、多端展示了。且小程序的DOM属性原本就与Web端的大致兼容(我司小程序使用Taro,所以也是基于vue),所以解决方案其实很简单:

  1. 将Web端compiler流程相关代码复制到小程序端,然后在schema规范化时,将Web端的tagName改成View:
// (其他逻辑详见上文的normalizeStrategies)
import { View, Text } from '@tarojs/components';
// 规范化nodeSchema策略集合(策略模式)
const normalizeStrategies = {
  module(nodeSchema: CommonCompProp) {
    return {
      tagName: View,
      ...objUtil.pick(nodeSchema, ['style', 'class']),
      children: nodeSchema.children || [],
    };
  },
  block(nodeSchema: CommonCompProp) {
    return {
      tagName: View,
      ...objUtil.pick(nodeSchema, ['style', 'class']),
      children: nodeSchema.children || [],
    };
  },
  ...
};

  1. 对各种组件元素(type: 'component')进行改写.基本上只需要将InputableText/ImageBox/ProductBox等组件复制一份到小程序端,然后将<template>里的元素都改成<view>/<text>等即可。另外因为小程序端的元素都是只读,所以还可以把组件里的编辑逻辑去掉,然后再微调一下就可以了。如ImageBox在小程序端的组件如下:
<template>
  <view style="width: 100%; height: 100%;">
    <view class="webpage-builder_image-box image-container"
      :style="{
        backgroundImage: imageType === 'background' ? `url(${croppedImgFullUrl})` : 'unset',
      }"
    >
      <base-image :src="croppedImgFullUrl" v-if="imageType === 'image'"></base-image>
    </view>
  </view>
</template>
<script lang="ts">
import {
  defineComponent, reactive, computed, ref, toRefs, PropType,
} from 'vue';
import { getModuleData } from '../utils';
import { stringifyUrl } from '@/utils/oss/process';
export default defineComponent({
  name: 'webpage-builder_image-box',
  components: {},
  props: {
    ... // 同web端
  },
  setup(props, { emit }) {
    const state = reactive({
      currentImagePath: getModuleData(props.data, props.valueKey),
    });
    const croppedImgFullUrl = computed(() => {
      return props.ossCropConfig ? stringifyUrl({
        url: state.currentImagePath,
        process: { image: { crop: props.ossCropConfig } },
      }) : state.currentImagePath;
    });
    return {
      ...toRefs(state),
      croppedImgFullUrl,
    };
  },
});
</script>
<style lang="scss">
.webpage-builder_image-box{
  background-size: cover;
  background-repeat: no-repeat;
  background-position: center;
  &.image-container {
    width: 100%;
    height: 100%;
  }
}
</style>

自定义新的module

为方便理解及生成模块,我另外开了个新页面,简单写了在线模块生成器,地址为:模块生成器。代码中已经使用require.context实现了对src/views/builder/modules目录下module开头的文件的自动引入,生成后将JSON复制下来,放到src/views/builder/modules文件夹下,则新增模块会自动增加到编辑器左侧的模块列表中。

CSS样式的局限

与其他低代码平台一样,低代码意味着低灵活性。出于简化的统一的需要,本项目的JSON-Schema不包含JS逻辑,如上文所述,所有模块的布局样式均使用纯CSS实现,只有组件元素内支持使用JS逻辑。这意味着目前模块只支持不特别负责的布局,目前预设模块最复杂的大概是这种类型的(modules/module11):

module-11.png

更复杂的布局,比如瀑布流等需要JS参与的布局,当前方案不支持。如果有这种需求,可以考虑将瀑布流区域设为组件元素来解决。

CSS中包含变量

模板样式中存在一种特殊情况:CSS代码中包含变量。以modules/module4为例:

module-4.png

上图中绿框为一个图片组件ImageBox, 红框为一个普通的div,其backgroundImage属性值即为绿框图片src,并进行高斯模糊。

module4-drag-image.gif

这个处理流程涉及一个之前的遗留点:如何在代码里插入动态变量并响应更新。

之前的comilpeSchema流程是将style属性视为静态json,直接赋值给h函数,这样的样式是不会动态更新。目前一个简单粗暴的处理方式是:

  1. style中的变量用特殊符号表示。参考amis的模板字符串方案, 使用${xxx}进行标识:
  backgroundImage: 'url(${imgUrl})';
  1. comilpeSchema阶段对style属性进行遍历,对${xxx}中的标识符进行变量替换
  // 解析style属性中的变量;
  const parseStyleValue = (styleObj: Obj, data: Obj) => {
    const _style: Obj = {};
    Object.entries(styleObj).forEach(([key, val]) => {
      _style[key] = parseTplExpress(data, val);
    });
    return _style;
  };
  const comilpeSchema = (...) => {
    ...
    if (attrs.style && moduleData) {
      attrs.style = parseStyleValue(attrs.style, moduleData);
    }
    ...
  }

需要注意这并不是一种好办法。因为在本项目中使用的比较少(只有两处用到),所以简单处理了一下。视情况可考虑进行如下优化:

  1. 目前的处理需遍历schema树的style对象,如果层级较多时性能可能会受影响。可以考虑参考vue2/vue3而方式,在第一次compileSchema时对依赖进行收集,来规避重复遍历。

  2. 目前的${xxx}内只支持单个变量,不支持表达式计算或函数调用。如果有这样的需要,可考虑使用eval改写。

组件元素的事件拦截

前面说到,本项目的设计是以组件元素(type:'component')作为最小编辑单位的,因此对于鼠标相关事件,如hover/click/mousedown等,此类元素应该以组件整体接收。以hover为例,只能hover到组件整体,组件内的子元素不可再选中:

component-hover.gif

一开始尝试过事件捕获、hover延时等方案,效果都不好。经思考,最后使用自定义事件和dispatchEvent实现了,代码如下:

// 捕获事件并重新抛出.按设计,组件类型应该作为一个整体接受事件, 即组件内部元素不可单独点击等.
// 所以对于type=component类型,在组件外层捕获响应事件, 并在组件外层以currentTarget重新抛出,
// 这样外面接受的的ev.target就是组件外层整体元素(currentTarget)了

export const getEventCatchAndThrowMap = (eventNames: string | string[]) => {
  const _eventNameList = Array.isArray(eventNames) ? eventNames : [eventNames];
  return Object.fromEntries(_eventNameList.map(evName => {
    return [
      evName,
      (ev: MouseEvent) => {
        if (ev.target !== ev.currentTarget) {
          ev.stopPropagation();
          const _ev = new Event(evName, { bubbles: true });
          ev.currentTarget?.dispatchEvent(_ev);
        }
      },
    ];
  }));
};

// 调用生成events-map
const events = props.status !== 'edit' ? null : getEventCatchAndThrowMap(['click', 'mouseover', 'mouseout', 'mousedown']);

// 监听:
<div class="inputable-text" v-on="events">
  ...
</div>

带contenteditable属性的容器滚动问题

本项目的文本输入组件InputableText是基于contenteditable属性来进行输入状态切换的。实际使用中发现了一个问题:当输入框容器高/宽固定,且输入文本超出输入框尺寸时,输入后切换回只读状态(contenteditable=false)后,显示的文本会停留在输入最后的输入位置,如下图:

inputable-text-scroll.gif

该bug的原因是经contenteditable属性设置的元素本质上仍然是一个block容器,表现形式与一般div类似,切换contenteditable并不会修改其滚动位置。

参考该问题讨论, 优化方案为:

  /* InputableText.vue */
  // contenteditable状态切换前,将容器滚回初始位置
  const handleChange = () => {
    if (!inputRef.value) {
      return;
    }
    inputRef.value.style.overflow = 'scroll';
    inputRef.value.scrollTo(0, 0);
    inputRef.value.style.overflow = 'hidden';
    ...
  };

grid-area属性在chrome99-102上的一个BUG

本项目的设计稿,UI大佬有点飘了,预设模板设计了大量web端和移动端页面元素顺序不一致的情况。为适配产品设计稿,本项目的模块大量使用了grid布局(web端和移动端页面元素顺序不一致的实现方案,据我了解CSS里要实现元素顺序的变动,除了grid外,好像就只有flexorder了吧?)。

然后因业务需要将页面的HTML生成为封面,我们使用了比较流行的dom-to-image库来处理。随后发现了一个浏览器的bug:

Chrome V99 ~ v101版本存在一个bug,对grid-area属性的序列化使用了“过于激进的处理方式”,导致使用dom-to-image生成的图片中grid布局错乱了。

bug相关讨论:

  • https://bugs.chromium.org/p/chromium/issues/detail?id=1305997
  • https://github.com/tsayen/dom-to-image/issues/410

好在Chromev102版本已经修复了这个问题。

chromium-bug-fix.png

如果业务需要兼容上述版本的Chrome, 相关模板可能需要改用flex之类的css属性进行重写。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容