Frontend Development 22 min read

How We Built a Scalable Low‑Code Platform for Store Screens and TV Displays

This article details the design and evolution of a low‑code platform used for electronic menu and TV screens in retail stores, covering architecture diagrams, component decomposition, material market, plugin system, schema compatibility, data fetching optimizations, and resource caching strategies to improve scalability and stability.

Goodme Frontend Team
Goodme Frontend Team
Goodme Frontend Team
How We Built a Scalable Low‑Code Platform for Store Screens and TV Displays

Background

Following the previous article on electronic menu business, we summarize the core tasks of the electronic menu and extend the discussion to TV screen advertising. The TV business shares a similar workflow but required a separate codebase, leading to duplicated logic and maintenance challenges.

To avoid endless copy‑and‑patch cycles, we encapsulated the functionality into an SDK that ensures engineering consistency and high extensibility for rapid site building and module improvement.

Business Architecture Diagram

After optimization we introduced a layered decoupling: engine, ecosystem, and client are independent. The low‑code engine provides a unified building protocol, the ecosystem offers common plugins and components, and the client (H5) supplies offline caching and stability monitoring. This hierarchy enables a fully functional low‑code platform.

Business / Feature Split

Typical low‑code editors follow a four‑area structure, which we also adopt to improve reusability and address design gaps.

We decompose the layout into the following components:

Store – global state manager for the designer, providing core context, visual instance, event map, selected component, etc.

DesignFrame – main frame component offering layout structure; developers can choose alternative structures based on designs.

ActionBar – toolbar component with basic tools and slot positions.

DragPanel – sidebar for material library, exposing atomic, business, and market tabs.

DesignPanel – visual design area supporting callbacks, selection, rotation, scaling, and node interaction.

SettingPanel – form configuration panel handling value‑to‑node and node‑to‑value synchronization.

These layout components can be freely combined via a Layout system, isolating functionality.

Material Area

How to Achieve Material Interoperability

Different business lines previously created separate npm packages for similar material components, causing duplication and lack of reuse.

We address this by:

Creating custom components from base components and adding them to a material market for broader reuse.

Building a material market that enables cross‑business sharing of base components.

Material Market

Instead of maintaining separate npm packages per business, we expose a shared material market. Each product only needs to add required business components and verify that base components meet its needs, reducing cognitive load.

Supporting Custom Combination of Base Materials

The client display relies on two fundamental elements: images and text. While these are mandatory, we also provide custom material components for common business scenarios, packaged as large‑grain components.

Although theoretically business users could compose arbitrary UI by combining atomic image and text components, the resulting logic would be overly complex, hard to maintain, and limit future extensibility. Therefore we supply tailored custom components that can be combined with the base ones.

Each custom combination yields a new material, expanding possibilities for business users.

How to Implement the Material Market

Two sources feed the market:

Business‑specific custom materials defined via a schema stored on the client.

Shared materials extracted into a common npm package and exposed in the DragPanel.

Custom materials are created by arranging multiple base components in the design area, generating a schema that records position, size, and configuration. Rendering this schema reproduces the material without needing separate design work.

How to Avoid Schema Incompatibility

Schema changes can cause white‑screen crashes if older versions lack new fields.

Add error‑boundary components to catch missing‑field errors and display friendly messages.

Provide default values for component props to guard against undefined fields.

Maintain a changelog documenting every schema modification.

Version schemas and implement compatibility logic based on the version field.

Visual Design Area

Plugin Architecture

Common interactions such as keyboard shortcuts, mouse actions, node movement, canvas zoom, and history are extracted into plugins, improving modularity.

Example: a move‑plugin listens to arrow keys and updates node coordinates, encapsulated as a pluggable component.

By separating these concerns, new features can be added simply by implementing the corresponding plugin interface.

Page Management & Layer Structure

To simplify complex interactions, a layer‑tree visualizes component dependencies, enabling clear layout organization. The implementation traverses the page DOM, builds an ID‑based tree, and synchronizes layer visibility with canvas nodes.

Property Designer

The designer configures low‑code component properties. Since the screens are display‑only, no event binding is required; focus is on form reuse and data binding.

Form Rendering via JSON Schema

Basic property types (number, text, boolean, select, date) and composite objects are described in a JSON Schema, which drives a generic form renderer.

<code>{
  "name": "PictureGroup",
  "title": "图片组",
  "configure": [{
    "type": "title",
    "name": "图片设置(px)"
  }, {
    "type": "formItem",
    "props": {
      "name": "size",
      "noStyle": true,
      "component": "WidthAndHeightForm"
    }
  }]
}
</code>

Base property editors use Ant Design components (e.g., UploadSetter, CheckBoxSetter); composite editors use custom components like SizeSetter or FontSetter.

Composite Form Reuse & Componentization

Repeated form components across business lines are extracted into a shared npm package, standardizing style and improving maintainability.

Designers also synchronize these components with a Figma component library to avoid duplicate design effort.

Client Logic

Data Request Optimization

We replaced per‑minute polling with long‑polling + streaming. The server holds the request until data changes, delivering only MQ messages. A custom SDK abstracts this, handling topic subscription, random delay aggregation, and peak‑shaving.

<code>const initMq = () => {
  const mq = createMQ({
    accessKey: mqServer.accessKey,
    topicSet: ['content-changed', 'schedule-changed'],
    logShow: false,
    // ...
  });
  let changeContent = [];
  let timeoutId = null;
  mq.onMessage(data => {
    try {
      data?.forEach(message => {
        changeContent.push(message);
      });
      const delay = Math.floor(5000 + Math.random() * 55000);
      if (changeContent.length) {
        if (timeoutId) clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
          handleContentChange(changeContent);
          changeContent = [];
        }, delay);
      }
    } catch (e) { /* ... */ }
  });
  mq.onError(err => { /* ... */ });
  registerWithRetry(mq, logger, { shopCode, screenNum });
};
</code>

Message Communication Architecture

Clients subscribe to multiple producers, queue tasks, and process them in a WebWorker thread, enabling high extensibility for future services.

Benefits

Event‑driven architecture reduces unnecessary requests.

Server load drops dramatically (e.g., 30k+ TV screens).

Randomized delay aggregation maintains acceptable real‑time performance while smoothing peaks.

Resource Display Stability

Video Resource Caching

Network variability in stores requires robust fallback mechanisms. We compress uploads, preload resources on change, and queue failed requests for retry when connectivity returns.

Previous ServiceWorker caching could store erroneous video blobs after repeated failures. We now fetch videos via

fetch

, stream chunks, assemble a Blob, create an object URL, and store it in CacheStorage only after successful download.

<code>fetch(url.replace('http://', 'https://'))
  .then(async (res) => {
    const getReader = async (reader, chunk, contentLength, receivedLength) => {
      const { done, value } = await reader.read();
      if (done) {
        const blob = new Blob(chunk, { type: 'video/mp4' });
        return URL.createObjectURL(blob);
      }
      chunk.push(value);
      receivedLength += value.length;
      if (contentLength && receivedLength === contentLength) {
        const blob = new Blob(chunk, { type: 'video/mp4' });
        return URL.createObjectURL(blob);
      }
      return getReader(reader, chunk, contentLength, receivedLength);
    };
    if (res.headers.get('Content-Type')?.includes('video')) {
      const reader = res.body?.getReader();
      const contentLength = Number(res.headers.get('Content-Length'));
      const chunk = [];
      return getReader(reader, chunk, contentLength, 0);
    }
  })
  .catch((error) => {
    console.error('There has been a problem with your fetch operation:', error);
    return MaterialStatus.FAIL;
  });
</code>

Resource Request Queue

Failed image requests are queued when offline and retried automatically once the network is restored.

<code>let eventQueue = [];
let isOnlineRef = false;
window.ononline = function () {
  isOnlineRef = true;
  if (eventQueue.length) {
    reloadImgQueue();
  }
};
window.onoffline = function () {
  isOnlineRef = false;
};
window.addEventListener('error', function (e) {
  const { tagName, src, baseURI } = e.target;
  if (tagName?.toUpperCase() === 'IMG') {
    if (!src || src === baseURI) return;
    if (!isOnlineRef) {
      eventQueue.push(e);
      return;
    }
    setTimeout(() => {
      e.target.src = src;
    }, 5000);
  }
}, true);
const reloadImgQueue = () => {
  const queue = eventQueue;
  eventQueue = [];
  queue.forEach((e) => {
    setTimeout(() => {
      e.target.src = e.target.src;
      e.target.addEventListener('load', () => {
        queue.splice(queue.indexOf(e), 1);
      });
    }, 5000);
  });
};
</code>

Conclusion

TV and electronic menu screens in offline stores are valuable marketing assets. Optimizing operations, enhancing stability, and avoiding reinventing the wheel are our goals. This article presented the key technical solutions behind Guming’s low‑code platform, illustrating how a unified toolset can serve diverse business scenarios while maintaining extensibility and maintainability.

frontendsdkArchitecturecachingLow-codeComponent
Goodme Frontend Team
Written by

Goodme Frontend Team

Regularly sharing the team's insights and expertise in the frontend field

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.