Taro Next H5 跨框架組件庫實踐

来源:https://www.cnblogs.com/o2team/archive/2020/04/13/12693390.html
-Advertisement-
Play Games

作者:凹凸曼 JJ "Taro" 是一款多端開發框架。開發者只需編寫一份代碼,即可生成各小程式端、H5 以及 React Native 的應用。 "Taro Next" 近期已發佈 beta 版本,全面完善對小程式以及 H5 的支持,歡迎體驗! 背景 Taro Next 將支持使用多框架開發 過去的 ...


作者:凹凸曼 - JJ
Taro 是一款多端開發框架。開發者只需編寫一份代碼,即可生成各小程式端、H5 以及 React Native 的應用。

Taro Next 近期已發佈 beta 版本,全面完善對小程式以及 H5 的支持,歡迎體驗!

背景

Taro Next 將支持使用多框架開發

過去的 Taro 1 與 Taro 2 只能使用 React 語法進行開發,但下一代的 Taro 框架對整體架構進行了升級,支持使用 React、Vue、Nerv 等框架開發多端應用。

為了支持使用多框架進行開發,Taro 需要對自身的各端適配能力進行改造。本文將重點介紹對 Taro H5 端組件庫的改造工作。

Taro H5

Taro 遵循以微信小程式為主,其他小程式為輔的組件與 API 規範。

但瀏覽器並沒有小程式規範的組件與 API 可供使用,例如我們不能在瀏覽器上使用小程式的 view 組件和 getSystemInfo API。因此我們需要在 H5 端實現一套基於小程式規範的組件庫和 API 庫。

Taro H5 架構圖

在 Taro 1 和 Taro 2 中,Taro H5 的組件庫使用了 React 語法進行開發。但如果開發者在 Taro Next 中使用 Vue 開發 H5 應用,則不能和現有的 H5 組件庫相容。

所以本文需要面對的核心問題就是:我們需要在 H5 端實現 React、Vue 等框架都可以使用的組件庫

方案選擇

我們最先想到的是使用 Vue 再開發一套組件庫,這樣最為穩妥,工作量也沒有特別大。

但考慮到以下兩點,我們遂放棄了此思路:

  1. 組件庫的可維護性和拓展性不足。每當有問題需要修複或新功能需要添加,我們需要分別對 React 和 Vue 版本的組件庫進行改造。
  2. Taro Next 的目標是支持使用任意框架開發多端應用。倘若將來支持使用 Angular 等框架進行開發,那麼我們需要再開發對應支持 Angular 等框架的組件庫。

那麼是否存在著一種方案,使得只用一份代碼構建的組件庫能相容所有的 web 開發框架呢?

答案就是 Web Components

但在組件庫改造為 Web Components 的過程並不是一帆風順的,我們也遇到了不少的問題,故藉此文向大家娓娓道來。

Web Components 簡介

Web Components 由一系列的技術規範所組成,它讓開發者可以開發出瀏覽器原生支持的組件。

技術規範

Web Components 的主要技術規範為:

  • Custom Elements
  • Shadow DOM
  • HTML Template

Custom Elements 讓開發者可以自定義帶有特定行為的 HTML 標簽。

Shadow DOM 對標簽內的結構和樣式進行一層包裝。

<template> 標簽為 Web Components 提供復用性,還可以配合 <slot> 標簽提供靈活性。

示例

定義模板:

<template id="template">
  <h1>Hello World!</h1>
</template>

構造 Custom Element:

class App extends HTMLElement {
  constructor () {
    super(...arguments)

    // 開啟 Shadow DOM
    const shadowRoot = this.attachShadow({ mode: 'open' })

    // 復用 <template> 定義好的結構
    const template = document.querySelector('#template')
    const node = template.content.cloneNode(true)
    shadowRoot.appendChild(node)
  }
}
window.customElements.define('my-app', App)

使用:

<my-app></my-app>

Stencil

使用原生語法去編寫 Web Components 相當繁瑣,因此我們需要一個框架幫助我們提高開發效率和開發體驗。

業界已經有很多成熟的 Web Components 框架,一番比較後我們最終選擇了 Stencil,原因有二:

  1. Stencil 由 Ionic 團隊打造,被用於構建 Ionic 的組件庫,證明經受過業界考驗。
  2. Stencil 支持 JSX,能減少現有組件庫的遷移成本。

Stencil 是一個可以生成 Web Components 的編譯器。它糅合了業界前端框架的一些優秀概念,如支持 Typescript、JSX、虛擬 DOM 等。

示例:

創建 Stencil Component:

import { Component, Prop, State, h } from '@stencil/core'

@Component({
  tag: 'my-component'
})
export class MyComponent {
  @Prop() first = ''
  @State() last = 'JS'

  componentDidLoad () {
    console.log('load')
  }

  render () {
    return (
      <div>
        Hello, my name is {this.first} {this.last}
      </div>
    )
  }
}

使用組件:

<my-component first='Taro' />

在 React 與 Vue 中使用 Stencil

到目前為止一切都那麼美好:使用 Stencil 編寫出 Web Components,即可以在 React 和 Vue 中直接使用它們。

但實際使用上卻會出現一些問題,Custom Elements Everywhere 通過一系列的測試用例,羅列出業界前端框架對 Web Components 的相容問題及相關 issues。下麵將簡單介紹 Taro H5 組件庫分別對 React 和 Vue 的相容工作。

相容 React

1. Props

1.1 問題

React 使用 setAttribute 的形式給 Web Components 傳遞參數。當參數為原始類型時是可以運行的,但是如果參數為對象或數組時,由於 HTML 元素的 attribute 值只能為字元串或 null,最終給 WebComponents 設置的 attribute 會是 attr="[object Object]"

attribute 與 property 區別

1.2 解決方案

採用 DOM Property 的方法傳參。

我們可以把 Web Components 包裝一層高階組件,把高階組件上的 props 設置為 Web Components 的 property:

const reactifyWebComponent = WC => {
  return class extends React.Component {
    ref = React.createRef()

    update () {
      Object.entries(this.props).forEach(([prop, val]) => {
        if (prop === 'children' || prop === 'dangerouslySetInnerHTML') {
          return
        }
        if (prop === 'style' && val && typeof val === 'object') {
          for (const key in val) {
            this.ref.current.style[key] = val[key]
          }
          return
        }
        this.ref.current[prop] = val
      })
    }

    componentDidUpdate () {
      this.update()
    }

    componentDidMount () {
      this.update()
    }

    render () {
      const { children, dangerouslySetInnerHTML } = this.props
      return React.createElement(WC, {
        ref: this.ref,
        dangerouslySetInnerHTML
      }, children)
    }
  }
}

const MyComponent = reactifyWebComponent('my-component')

註意:

  • children、dangerouslySetInnerHTML 屬性需要透傳。
  • React 中 style 屬性值可以接受對象形式,這裡需要額外處理。

2. Events

2.1 問題

因為 React 有一套合成事件系統,所以它不能監聽到 Web Components 發出的自定義事件。

以下 Web Component 的 onLongPress 回調不會被觸發:

<my-view onLongPress={onLongPress}>view</my-view>
2.2 解決方案

通過 ref 取得 Web Component 元素,手動 addEventListener 綁定事件。

改造上述的高階組件:

const reactifyWebComponent = WC => {
  return class Index extends React.Component {
    ref = React.createRef()
    eventHandlers = []

    update () {
      this.clearEventHandlers()

      Object.entries(this.props).forEach(([prop, val]) => {
        if (typeof val === 'function' && prop.match(/^on[A-Z]/)) {
          const event = prop.substr(2).toLowerCase()
          this.eventHandlers.push([event, val])
          return this.ref.current.addEventListener(event, val)
        }

        ...
      })
    }

    clearEventHandlers () {
      this.eventHandlers.forEach(([event, handler]) => {
        this.ref.current.removeEventListener(event, handler)
      })
      this.eventHandlers = []
    }

    componentWillUnmount () {
      this.clearEventHandlers()
    }

    ...
  }
}

3. Ref

3.1 問題

我們為瞭解決 Props 和 Events 的問題,引入了高階組件。那麼當開發者向高階組件傳入 ref 時,獲取到的其實是高階組件,但我們希望開發者能獲取到對應的 Web Component。

domRef 會獲取到 MyComponent,而不是 <my-component></my-component>

<MyComponent ref={domRef} />
3.2 解決方案

使用 forwardRef 傳遞 ref。

改造上述的高階組件為 forwardRef 形式:

const reactifyWebComponent = WC => {
  class Index extends React.Component {
    ...

    render () {
      const { children, forwardRef } = this.props
      return React.createElement(WC, {
        ref: forwardRef
      }, children)
    }
  }
  return React.forwardRef((props, ref) => (
    React.createElement(Index, { ...props, forwardRef: ref })
  ))
}

4. Host's className

4.1 問題

在 Stencil 里我們可以使用 Host 組件為 host element 添加類名。

import { Component, Host, h } from '@stencil/core';

@Component({
  tag: 'todo-list'
})
export class TodoList {
  render () {
    return (
      <Host class='todo-list'>
        <div>todo</div>
      </Host>
    )
  }
}

然後在使用 <todo-list> 元素時會展示我們內置的類名 “todo-list” 和 Stencil 自動加入的類名 “hydrated”:

但如果我們在使用時設置了動態類名,如: <todo-list class={this.state.cls}>。那麼在動態類名更新時,則會把內置的類名 “todo-list” 和 “hydrated” 抹除掉。

關於類名 “hydrated”:

Stencil 會為所有 Web Components 加上 visibility: hidden; 的樣式。然後在各 Web Component 初始化完成後加入類名 “hydrated”,將 visibility 改為 inherit。如果 “hydrated” 被抹除掉,Web Components 將不可見。

因此我們需要保證在類名更新時不會覆蓋 Web Components 的內置類名。

4.2 解決方案

高階組件在使用 ref 為 Web Component 設置 className 屬性時,對內置 class 進行合併。

改造上述的高階組件:

const reactifyWebComponent = WC => {
  class Index extends React.Component {
    update (prevProps) {
      Object.entries(this.props).forEach(([prop, val]) => {
        if (prop.toLowerCase() === 'classname') {
          this.ref.current.className = prevProps
            // getClassName 在保留內置類名的情況下,返回最新的類名
            ? getClassName(this.ref.current, prevProps, this.props)
            : val
          return
        }

        ...
      })
    }

    componentDidUpdate (prevProps) {
      this.update(prevProps)
    }

    componentDidMount () {
      this.update()
    }

    ...
  }
  return React.forwardRef((props, ref) => (
    React.createElement(Index, { ...props, forwardRef: ref })
  ))
}

相容 Vue

不同於 React,雖然 Vue 在傳遞參數給 Web Components 時也是採用 setAttribute 的方式,但 v-bind 指令提供了 .prop 修飾符,它可以將參數作為 DOM property 來綁定。另外 Vue 也能監聽 Web Components 發出的自定義事件。

因此 Vue 在 Props 和 Events 兩個問題上都不需要額外處理,但在與 Stencil 的配合上還是有一些相容問題,接下來將列出主要的三點。

1. Host's className

1.1 問題

同上文相容 React 第四部分,在 Vue 中更新 host element 的 class,也會覆蓋內置 class。

1.2 解決方案

同樣的思路,需要在 Web Components 上包裝一層 Vue 的自定義組件。

function createComponent (name, classNames = []) {
  return {
    name,
    computed: {
      listeners () {
        return { ...this.$listeners }
      }
    },
    render (createElement) {
      return createElement(name, {
        class: ['hydrated', ...classNames],
        on: this.listeners
      }, this.$slots.default)
    }
  }
}

Vue.component('todo-list', createComponent('todo-list', ['todo-list']))

註意:

  • 我們在自定義組件中重覆聲明瞭 Web Component 該有的內置類名。後續開發者為自定義組件設置類名時,Vue 將會自動對類名進行合併
  • 需要把自定義組件上綁定的事件通過 $listeners 透傳給 Web Component。

2. Ref

2.1 問題

為瞭解決問題 1,我們給 Vue 中的 Web Components 都包裝了一層自定義組件。同樣地,開發者在使用 ref 時取到的是自定義組件,而不是 Web Component。

2.2 解決方案

Vue 並沒有 forwardRef 的概念,只可簡單粗暴地修改 this.$parent.$refs

為自定義組件增加一個 mixin:

export const refs = {
  mounted () {
    if (Object.keys(this.$parent.$refs).length) {
      const refs = this.$parent.$refs

      for (const key in refs) {
        if (refs[key] === this) {
          refs[key] = this.$el
          break
        }
      }
    }
  },
  beforeDestroy () {
    if (Object.keys(this.$parent.$refs).length) {
      const refs = this.$parent.$refs

      for (const key in refs) {
        if (refs[key] === this.$el) {
          refs[key] = null
          break
        }
      }
    }
  }
}

註意:

  • 上述代碼沒有處理迴圈 ref,迴圈 ref 還需要另外判斷和處理。

3. v-model

3.1 問題

我們在自定義組件中使用了渲染函數進行渲染,因此對錶單組件需要額外處理 v-model

3.2 解決方案

使用自定義組件上的 model 選項,定製組件使用 v-model 時的 prop 和 event。

改造上述的自定義組件:

export default function createFormsComponent (name, event, modelValue = 'value', classNames = []) {
  return {
    name,
    computed: {
      listeners () {
        return { ...this.$listeners }
      }
    },
    model: {
      prop: modelValue,
      event: 'model'
    },
    methods: {
      input (e) {
        this.$emit('input', e)
        this.$emit('model', e.target.value)
      },
      change (e) {
        this.$emit('change', e)
        this.$emit('model', e.target.value)
      }
    },
    render (createElement) {
      return createElement(name, {
        class: ['hydrated', ...classNames],
        on: {
          ...this.listeners,
          [event]: this[event]
        }
      }, this.$slots.default)
    }
  }
}

const Input = createFormsComponent('taro-input', 'input')
const Switch = createFormsComponent('taro-switch', 'change', 'checked')
Vue.component('taro-input', Input)
Vue.component('taro-switch', Switch)

總結

當我們希望創建一些不拘泥於框架的組件時,Web Components 會是一個不錯的選擇。比如跨團隊協作,雙方的技術棧不同,但又需要公用部分組件時。

本次對 React 語法組件庫進行 Web Components 化改造,工作量不下於重新搭建一個 Vue 組件庫。但日後當 Taro 支持使用其他框架編寫多端應用時,只需要針對對應框架與 Web Components 和 Stencil 的相容問題編寫一個膠水層即可,總體來看還是值得的。

關於膠水層,業界相容 React 的方案頗多,只是相容 Web Components 可以使用 reactify-wc,配合 Stencil 則可以使用官方提供的插件 Stencil DS Plugin。倘若 Vue 需要相容 Stencil,或需要提高相容時的靈活性,還是建議手工編寫一個膠水層。

本文簡單介紹了 Taro Next、Web Components、Stencil 以及基於 Stencil 的組件庫改造歷程,希望能為讀者們帶來一些幫助與啟迪。


歡迎關註凹凸實驗室博客:aotu.io

或者關註凹凸實驗室公眾號(AOTULabs),不定時推送文章:

歡迎關註凹凸實驗室公眾號


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 有 3 種定義函數的方式 函數聲明 函數表達式 Function 構造函數(很少使用) 函數聲明 關鍵字後需要指定函數名 函數表達式 關鍵字後不用指定函數名;函數末尾需要添加一個分號,就像聲明其他變數時一樣 Function 構造函數 構造函數可以接收任意數量的參數,但最後一個參數始終都被看成是函數 ...
  • 剛看了下《JavaScript入門與實戰》視頻前2章,簡單講解了下學習js需要註意的事項與準備;雖然學習本身是一件無聊且枯燥的事情,但是誰叫咱是靠鍵盤吃飯的,沒辦法,"如果可以靠臉吃飯,誰願意靠才華",當然這個扯遠了;前2節感興趣的是這個打字拒絕‘二指彈’,學會用我們的鍵盤,建議電腦安裝金山打字練習... ...
  • 微信小程式 wx.request 發起 HTTPS 網路請求。 示例代碼 不進行二次封裝確實不太好用 分享下我這邊 的封裝 api.js js const app = getApp() const request = (url, options) = { return new Promise((re ...
  • 版權聲明:本文轉載至CSDN博主「詩人與黑客」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。原文鏈接:https://blog.csdn.net/qq_41899174/article/details/82797089感謝博主,感謝分享,對於for迴圈咋們不能 ...
  • layui產生背景 layui相對於vue來說確實稍有遜色,但是官網提供的入門文檔以及完善的框架結構,使的很多人開始用layui來開發前端頁面,那麼什麼人會去使用layui呢? 針對後端開發人員,在對前端知之甚少的情況下需要自行開發前端頁面 前後端數據動態交互要求較低的前端開發工程師 測試開發工程師 ...
  • 學習分享變數聲明、變數類型,如何對它們進行賦值、改變、計算等一系列操作,掌握不同運算符的使用方法,靈活運用算術運算符、比較運算符、邏輯運算符對變數和數值進行操作。 2-1 什麼是變數 2-2 給變數取個名字(變數命名) 2-3 確定你的存在(變數聲明) 2-4 多樣化的我(變數賦值) 2-5 表達出 ...
  • 本文譯自 "How to use async functions with Array.filter in Javascript Tamás Sallai " 。 0. 如何僅保留滿足非同步條件的元素 在 "第一篇文章中" ,我們介紹了 async / await 如何幫助處理非同步事件,但在非同步處理集 ...
  • video設置靜音,在ios8,io9發現都無法靜音,得出結論如下: 1、如果預設給video標簽加muted屬性,調試獲取到的為true,但是依然有聲音,即:即使設置為true,也是有聲音;2、通過js改變muted是無法改變ios8下的值的,改變了之後列印依然為改變之前的;3、網上看到其他的一個 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...