glass-easel

repository·master·Indexed 43 days ago

https://github.com/wechat-miniprogram/glass-easel

A JavaScript component-based UI framework designed for WeChat MiniProgram. It allows component code to run in both web and MiniProgram environments through a pluggable 'Backend' abstraction. The ecosystem includes a core module, a template compiler for WXML, a stylesheet compiler for WXSS, and a MiniProgram adapter for API compatibility.

Tokens
143.5K
Snippets
396
Records
469
Agent score
55%

What's inside glass-easel

  1. Overview of glass-easel

    master

    glass-easel is the core implementation of the new WeChat MiniProgram component framework. It is a JavaScript-based componentization framework designed for declarative UI development.

    Key characteristics:

    • Environment Agnostic: It can run in web browsers, MiniProgram environments, or other JavaScript environments by using different 'backends'.
    • Backend Concept: The 'backend' represents the runtime environment. In a web environment, the backend is the browser's DOM; in a MiniProgram environment, the backend is the MiniProgram interface. This allows component code to remain backend-independent.
    • Compatibility: It is a rewrite of the legacy MiniProgram component framework, maintaining 99% interface compatibility while adding new features and better TypeScript support.
    • Performance: It uses a single-component node tree update algorithm, providing balanced performance suitable for highly componentized development.
  2. What is a ComponentSpace and how to define one

    master

    A ComponentSpace is the core container in glass-easel for managing component and Behavior registrations. It handles storage, lookup, unified configuration of component options, global reference management, and cross-space imports/exports.

    Key rules:

    • Every component belongs to exactly one ComponentSpace.
    • When looking up a component by name, glass-easel only searches within the current ComponentSpace.
    • You can use the global default component space if you don't want to create a custom one.
    // Create a new component space
    const componentSpace = new glassEasel.ComponentSpace()
    
    // Define and register a component within this space
    export const helloWorld = componentSpace.define('hello-world')
      .registerComponent()
    
    // Using the global default space
    const defaultSpace = glassEasel.getDefaultComponentSpace()
  3. How the Event Model works in glass-easel

    master

    The event system follows a model similar to the DOM, supporting two distinct phases:

    1. Capture phase: Propagates from the outermost ancestor down to the target. This requires the capturePhase option to be enabled.
    2. Bubbling phase: Propagates from the target up to the ancestors. This requires the bubbles option to be enabled.

    Events can also be configured to bubble across the Shadow Root using the composed option.

  4. Use Data Observers to react to data changes

    master

    Data Observers allow you to trigger a response function whenever specific properties or data fields are set. This function runs before the data is applied to the template, which can improve performance by reducing unnecessary template updates.

    Key Behaviors:

    • updateData vs setData: Inside an observer, you should use updateData to update other data fields. These changes will take effect after the observer finishes executing. While setData can also be used, it behaves like updateData within an observer—it will not apply to the template until the observer completes.
    • Trigger Condition: An observer triggers whenever the watched field is set, even if the new value is identical to the old value.
    • Infinite Loop Warning: Do not update a field that the observer is currently watching inside the observer function itself, as this will cause an infinite loop.

    Implementation Methods:

    1. Chaining Method: Use the .observer(fields, callback) method directly in the component definition chain.
    2. init Method: Use the observer argument provided within the .init(({ self, lifetime, observer }) => { ... }) callback.
    // Example using Chaining
    export const addComponent = componentSpace.define()
      .data(() => ({
        a: 1,
        b: 2,
        sum: 3,
      }))
      .observer(['a', 'b'], function () {
        this.updateData({
          sum: this.data.a + this.data.b,
        })
      })
      .registerComponent()
  5. Manage components and behaviors with ComponentSpace

    master

    ComponentSpace acts as a registry for components and behaviors. It manages definitions, aliases, imports/exports, and global using configurations.

    Creating a ComponentSpace

    • getDefaultComponentSpace(): Returns the global default component space instance.
    • new ComponentSpace(defaultComponent?, baseSpace?, styleScopeManager?, allowUnusedNativeNode?): Creates a new space. You can pass an existing space as baseSpace to inherit its definitions.

    Defining Components and Behaviors

    You can define entities using several styles:

    1. Fluent/Chainable API: Use space.define(is).property(...).data(...).method(...).registerComponent().
    2. Classic Style: Use space.defineComponent(def) or space.defineBehavior(def) with a standard definition object.
    3. Trait Behavior: Use space.defineTraitBehavior(trans) to define behaviors with transformation functions.
    4. Registration: Use space.registerComponent(is, comp) to register an existing component definition.

    Import, Export, and Global Using

    • Exporting: Use exportComponent(alias, is) or exportBehavior(alias, is) to make definitions available to other spaces.
    • Importing: Use importSpace(protoDomain, space, privateUse) to bring in another space. Imported components are accessed via domain/name (e.g., space://lib/comp-name).
    • Global Using: Use setGlobalUsingComponent(key, target) to make a component available to all components within that space.
    • Lazy Loading: Use setComponentWaitingListener(listener) to handle scenarios where components are needed but not yet loaded (e.g., async loading).
    const { ComponentSpace, getDefaultComponentSpace } = glassEasel
    
    // Use default component space
    const defaultSpace = getDefaultComponentSpace()
    
    // Create new component space
    const mySpace = new ComponentSpace('my-default-comp')
    
    // Create space based on existing one (inheritance)
    const childSpace = new ComponentSpace('default', defaultSpace)
    
    // Update default options
    mySpace.updateComponentOptions({
      multipleSlots: true,
      dataDeepCopy: glassEasel.DeepCopyKind.SimpleWithRecursion,
    })
    
    const space = new ComponentSpace()
    
    // Chainable API definition
    const MyComp = space
      .define('my-comp')
      .property('title', String)
      .data(() => ({ count: 0 }))
      .method('increment', function () {
        this.setData({ count: this.data.count + 1 })
      })
      .registerComponent()
    
    // Classic style definition
    const MyComp2 = space.defineComponent({
      is: 'my-comp-2',
      properties: { title: String },
      data: { count: 0 },
      methods: {
        increment() {
          this.setData({ count: this.data.count + 1 })
        },
      },
    })
    
    // Exporting for other spaces
    space.exportComponent('my-comp-alias', 'my-comp')
    
    // Importing another space
    space.importSpace('space://lib', libSpace, false)
    
    // Global using
    space.setGlobalUsingComponent('my-icon', iconCompDef)
    
    // Placeholder listener for async loading
    space.setComponentWaitingListener((isPub, alias, owner) => {
      console.log(`Component "${alias}" is needed by`, owner.is)
    })
  6. Understand the different rendering backend modes

    master

    Glass Easel is not limited to the DOM environment; it can support various rendering backends by passing its generated node tree to them. To implement a backend, you must implement one of the three protocol modes defined by the custom backend protocol.

    Choose the mode that best fits your requirements:

    1. Composed Mode (glassEasel.composedBackend.Context): The preferred protocol. It is relatively simple and easy to use.
    2. Shadow Mode (glassEasel.backend.Context): A protocol specifically for the Shadow Tree. It typically offers the best overall performance but has a more complex protocol.
    3. DOM-like Mode (glassEasel.domlikeBackend.Context): A protocol designed for DOM environments, typically used only for adapting DOM interfaces.
  7. Reuse logic with behaviors

    master

    Behaviors are a mechanism for sharing code (properties, methods, lifecycles) between components.

    1. Define a behavior using .registerBehavior().
    2. Apply a behavior to a component using .behavior(behaviorInstance).

    Limitations:

    • Certain fields cannot be used in behaviors: template, using, generics, placeholders, and options will be ignored.
    • Diamond Inheritance: If multiple behaviors share a common ancestor behavior, glass-easel automatically deduplicates the instance, but fields with the same name in different behaviors may still overwrite each other.
    • TypeScript support for type inference is weaker for standard behaviors. For interface-based sharing, use Trait Behaviors instead.
    // Define a behavior
    export const sharedBehavior = componentSpace
      .define()
      .property('shared', String)
      .lifetime('attached', function () {
        console.log('shared attached')
      })
      .registerBehavior()
    
    // Use a behavior
    export const myComponent = componentSpace
      .define()
      .behavior(sharedBehavior)
      .template(wxml(`<div>{{ shared }}</div>`))
      .registerComponent()
  8. Use init functions for private component logic

    master

    When using the .init() function, you can define functions directly within the scope of init without using the method() helper or returning them. This is a recommended pattern for implementing private functions that should not be accessible via the component instance (this).

    export const myComponent = componentSpace.define()
      .init(function ({ lifetime }) {
        // This function is private and cannot be accessed via 'this'
        const aPlusB = (a, b) => a + b
        
        lifetime('attached', function () {
          aPlusB(1, 2) === 3 // true
        })
      })
      .registerComponent()
  9. How ComposedAncestors traversal works

    master

    ComposedAncestors traverses upwards from a specified node along the Composed Tree. Unlike ShadowAncestors, this path crosses Shadow boundaries and slot distribution boundaries, continuing all the way up to the root node.

    // Traverse upwards along the Composed Tree from div#e
    // Order: div#e → B.shadowRoot → comp-b#b → div#a → A.shadowRoot → A
    glassEasel.ElementIterator.create(B.$.e, glassEasel.ElementIteratorType.ComposedAncestors).forEach(
      (node) => {
        console.log(node)
      },
    )
  10. Batch updates using groupUpdates and updateData

    master

    To avoid forgetting to call applyDataUpdates() and to optimize performance, use groupUpdates to batch multiple data operations.

    When using groupUpdates, the changes are automatically applied to the template at the end of the callback, so you do not need to call applyDataUpdates() manually.

    • Batching path updates: Wrap replaceDataOnPath or spliceArrayDataOnPath calls inside groupUpdates.
    • Batching setData calls: Use updateData inside a groupUpdates block to combine multiple setData-style updates. This is more performant than calling setData multiple times consecutively.
    // Batching path updates (automatic apply)
    this.groupUpdates(() => {
      this.replaceDataOnPath(['obj', 'a'], 3)
      this.replaceDataOnPath(['obj', 'b'], 5)
    })
    
    // Batching multiple updateData calls for better performance
    this.groupUpdates(() => {
      this.updateData({ a: 3 })
      this.updateData({ b: 5 })
    })
  11. Restrictions on fields within Behaviors

    master

    Not all component features can be defined within a behavior. The following methods will be ignored if used inside a behavior because they cannot be effectively merged or involve template-specific node information:

    • .template()
    • .usingComponents()
    • .generics()
    • .placeholder()
    • .options()

    Properties, data, methods, and lifecycle hooks can still be declared in behaviors.

  12. Manage component data with data and staticData

    master

    Components use data to drive template rendering. When data changes, the template updates automatically.

    Use .data() with a function that returns a data object. This ensures every component instance receives an independent copy of the data.

    Using staticData (Simplified)

    Use .staticData() to pass a direct data object. The object is deep-copied for every instance.

    Note:

    • Subsequent calls to staticData will overwrite previous ones rather than merging. Use data if you need to add fields incrementally.
    • For complex objects, data(() => ({ ... })) is preferred over staticData to avoid performance overhead during deep copying.
    • data and staticData can be used together; fields in data will override those in staticData.
    // Using data (returns a function for independent copies)
    export const myComponent = componentSpace
      .define()
      .template(wxml(`<div>{{ message }}</div>`))
      .data(() => ({
        message: 'Hello world!',
      }))
      .registerComponent()
    
    // Using staticData (direct object, deep-copied)
    export const myComponent = componentSpace
      .define()
      .template(wxml(`<div>{{ message }}</div>`))
      .staticData({
        message: 'Hello world!',
      })
      .registerComponent()