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
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...