🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

🌈 TDesign UniApp 組件庫來了

1. 背景

跨端開發一直是前端領域的重要部分,旨在實現一套代碼在多個平台運行。國內使用 uniapp 框架人數較多,一直有外部聲音想要 uniapp 版本的 TDesign,如 TDesign Miniprogram 下的眾多 issue

轉存失敗,建議直接上傳圖片檔案

原生小程序和 uniapp 有差異,有人在 uniapp 項目裡用了原生小程序組件,需要魔改內部組件代碼。

基於以上需求,寫了 TDesign UniApp 項目。支持:

  • 🌈 暗色模式
  • 🌈 自訂主題
  • 🌍 國際化
  • 🚀 API 對齊官方
  • 🚀 類型提示
  • ...

歡迎使用,歡迎 star,歡迎反饋!

2. 預覽

掃碼查看 ↓

轉存失敗,建議直接上傳圖片檔案

(注:其他平台同樣支持,僅因平台審核等原因未能上架預覽,不影響組件庫正常使用。)

3. 快速開始

3.1. 安裝

  1. NPM 方式

    npm i tdesign-uniapp
  2. UNI_MODULES 方式

已上傳插件到 DCloud 插件市場,請打開插件詳情頁並點擊 使用 HBuilderX 導入插件

3.2. 引入並使用

  1. main.ts 中引入樣式文件

    import 'tdesign-uniapp/common/style/theme/index.css';
  2. 在文件中使用

    <template>
     <t-loading />
    </template>
    
    <script lang="ts" setup>
    import TLoading from 'tdesign-uniapp/loading/loading.vue';
    </script>

3.3. 自動導入

pages.json 配置 easycom,可實現自動導入。

  1. CLI 模式

使用 CLI 模式,即使用 node_modules 下的 tdesign-uniapp 時,配置如下。

   {
     "easycom": {
       "custom": {
         "^t-(.*)": "tdesign-uniapp/$1/$1.vue"
       }
     }
   }
  1. UNI_MODULES 模式

使用 uni_modules 下的 tdesign-uniapp 時,配置如下。

   {
     "easycom": {
       "custom": {
         "^t-(.*)": "@/uni_modules/tdesign-uniapp/components/$1/$1.vue"
       }
     }
   }

3.4. 平台相容性

平台 Vue2 Vue3 H5 Android iOS App-nvue 微信小程序 QQ小程序
支持情況 ⚠️
平台 支付寶小程序 抖音小程序 百度小程序 快手小程序 小紅書小程序 京東小程序
支持情況

4. 淺思考

有幾點是做之前要想清楚的。

4.1. 為什麼不做轉換工具

  1. 工具轉出來的可讀性差,可維護性差
  2. 轉換工具無法做到100%,總有些語法需要手動轉換。這意味著一定會有人工介入
  3. 維護轉換工具成本比維護組件庫高好幾倍,且寫出來的還不一定能完全滿足
  4. 業務真正要用的是組件庫,真正關心的也是組件庫

4.2. 與 tdesign-miniprogram 版本關係

tdesign-uniapp 有獨立的版本,並不與 tdesign-miniprogram 的版本相同。這是因為轉換後的產物很有可能有自己的 feature/bug,處理需要發版,必然導致版本分叉。

多個 tdesign-uniapp 版本會對應一個 tdesign-miniprogram 版本,會儘量提供 miniprogram 最新版本的轉換產物。

4.3. API 設計

API 一定要與官方一致,這是最不能妥協的,包括 propsevents、事件參數,參數類型、插槽、CSS變數。

這樣做的好處是,開發者沒有額外心智負擔,同時限制開發人員的胡亂發揮,以及減少開發者的決策成本。

API 儘量與小程序對齊,而不是 mobile-vue/mobile-react,因為 uniapp 語法主要是小程序的語法。

4.4. 可維護性

  • 用統一的語法
  • 不使用編譯後的、混淆後的變數

5. 轉化過程

5.1. 核心轉換邏輯

之前寫過 Press UI,整體思路差不多。就是將小程序的 wxml/wxss/js/json 轉成 uniapp 的 Vue,四個文件合成一個文件。以及將小程序的語法進行轉化,以下是核心部分:

  1. uniComponent 包裹,內部有一些公共處理
  2. properties => props
  3. setData => data 正常賦值
  4. 生命週期改造
  5. 事件改造
  6. props 文件改造,from: value: ([^{]+),to: default: $1

其他部分,如 externalClassesrelations,以及組件庫特有的受控屬性、命令調用等都需要進行額外的處理。

5.2. 事件參數

tdesign-miniprogram 中的事件參數,在 tdesign-uniapp 中都被去掉了 detail 一層。以 Picker 組件為例,在 tdesign-miniprogram 中,這樣獲取參數

onPickerChange(e) {
  console.log(e.detail.value);
}

tdesign-uniapp 中,需要去掉 .detail,即

onPickerChange(e) {
  console.log(e.value);
}

這樣做是為了簡化使用。tdesign-uniapp 中所有組件都採用了這種方式。

6. 細節

6.1. 命令調用

tdesign-uniapp 中支持命令調用的組件有

  • ActionSheet
  • Dialog
  • Message
  • Toast

TDesign UniApp 下,命令調用的核心思路是數據轉化,就是把所有 props 都聲明成 data,比如 visible => dataVisible,這樣組件自身才能既能從方法(methods)中得到值,又能從 props 中得到值。要改的地方包括

  1. data 中初始化
  2. watch 中監聽
  3. setData 收口,設置的時候都加上特殊開頭

每個組件具體實現不同。

  • Message 嵌套了一層 message-itemmessage-item 沒有 props,都是 setData 直接給的 data,所以根本不需要轉換。

    • 這是另一種解決思路了,用嵌套子組件,而不是轉換數據。子組件一嵌套,且數據全部不走 props,而是調用子組件內部方法。
    • 展示時, setMessage(組件調用、命令調用都走) => addMessage ( => showMessageItem) 或者 updateMessage
    • Message 中的 setMessage/addMessage/showMessageItem 都是指的內部的 message-item,是循環的 messageList,而不是頁面級別的 t-message
  • Dialog、ActionSheet 需要轉換

    • 調用 setData,將屬性(包含 visible: true)傳進去,同時將 instance_onConfirm 設置為 promiseresolve
  • Toast 沒有組件調用,只有命令式,無需數據轉換。

    • 調用 instance.show,內部還是 setData

6.2. 受控屬性

存在受控屬性的非表單組件有

  • 反饋類:ActionSheet、DropdownItem、Guide
  • 展示類:CheckTag、Collapse、Image-viewer
  • 導航類:Indexes、Sidebar、Steps、Tabbar、Tabs

TDesign UniApp 中受控屬性的處理,和小程序版本差不多。是將其轉成 data 開頭的內部屬性,初始化的時候,會判斷受控和非受控值。同時觸發事件的時候也要判斷當前是否存在受控屬性,非受控的時候直接改變內部值並拋出事件,受控的時候只拋出事件。以及,props 中受控屬性的默認值需是 nullundefined

不同的是,小程序受控屬性,可以使用 this.setData({ [value]: this.defaultValue }),也就是 data 中聲明了與 properties 名稱一樣的變數,Vue 中不可以,會報錯 'set' on proxy: trap returned falsish for property 'value'

總結下來,受控屬性要處理的:

  1. watch 中監聽
  2. created 中初始化
  3. methods 中新增 _trigger,作為拋出事件的收口

6.3. 三方庫

tdesign-miniprogram 執行 npm run build,在 miniprogram_dist/node_modules 目錄下 拿到 dayjstinycolor2 的產物,複製到 tdesign-uniappnpm 目錄下,用啥拿啥。

一次性工作,一般不會改。

6.4. input 受控

H5 下,uni-app 封裝了 input,且不支持受控。

Input 限制中文字符在 uni-app 實現的話,解決方案是先設置一次,然後在 nextTick 中再設置一次。

參考:ask.dcloud.net.cn/article/39736

其他方案:

  1. 可以動態創建 input 元素,不用 uni-app 包裹的,缺點是更新屬性麻煩。
  2. 動態計算 maxlength,用瀏覽器原生屬性約束,缺點是實現稍複雜、程式碼量稍多。

6.5. externalClass

uni-app 下,externalClasses 是不生效的。

參考:

所以 styleIsolation: apply-shared 不夠用,只能改成 styleIsolation: shared,這樣開發者才能在任意使用的地方覆蓋組件樣式。

可以改下 packages/site/node_modules/@dcloudio/uni-mp-compiler/dist/transforms/transformComponent.js,把 isComponentProp 方法,將 t-class 排除,就能解決,但官方不會推出。

6.6. scoped

tdesign-uniapp 必須加 scoped,否則一個自訂組件加了 styleIsolation: shared,同一頁面下其他沒加此屬性的自訂組件也會生效,只要 class 相同!

6.7. t-class

統一用 tClass,而不是 class

轉存失敗,建議直接上傳圖片檔案

6.8. distanceTop

Drawer 頂部過高,是因為子組件 popup 中使用的 --td-popup-distance-top 變數為 0,這個變數由 distanceTop 生成,distanceTop 又是由 using-custom-navbar 這個 mixin 生成。

distanceTopuni.getMenuButtonBoundingClientRect 計算生成,H5 和 App 下沒有這個API,可以直接傳入 customNavbarHeight,這個值由業務自行計算得到。

目前使用到 using-custom-navbar 這個 mixin 的組件有

  • Overlay,基礎,使用到它的也會引用
    • Popup
    • Picker
    • ActionSheet
    • Calendar
    • Dialog
    • Drawer
    • Guide
    • Toast
  • Fab
  • ImageViewer

6.9. page-scroll

APP-PLUS 下,動態監聽 onPageScroll 不生效,需要業務自己在頁面中監聽,下面給出最佳實踐之一。

// 頁面 Vue 文件下,引入組件庫提供的監聽方法
// 該方法內部會通過 event-bus,傳遞參數給對應的組件
import { handlePageScroll } from 'tdesign-uniapp/mixins/page-scroll';

export default {
  onPageScroll(e) {
    handlePageScroll(e);
  },
};

目前使用到 page-scroll 這個 mixin 的組件有

  1. Sticky
  2. Indexes
  3. Tabs(引入了 Sticky)

示例頁面有

  • Fab
  • PullDownRefresh

6.10. getCustomNavbarHeight 報錯

Cannot read properties of null (reading 'parentElement')

轉存失敗,建議直接上傳圖片檔案

這種就是 mounted 之後沒延時,沒獲取到對應元素。

6.11. site 工程中的 alias

tdesign-uniapp 在 H5 下使用 vite.config 中的 alias,不使用 workspace,可解決修改組件後必須重啟才能生效。

小程序下,這種方式需要進一步改造,只能引用同一個子工程,即不能跨 src,解決方案就是監聽組件變動,同步複製到 site 工程下。

6.12. watch

小程序的 observersvuewatch 邏輯並不完全相同,小程序下,如果 prop 接收外部傳入的實參與該 prop 的默認值不相等時,會導致 observer 被立即調用一次,Vue 而不是。

imagecalcSize 中就用到了。

6.13. auto-import

開發了 auto-import-resolver 插件,但發現微信小程序下編譯有問題,H5 下正常,推測是 uniapp 自己的問題。

轉存失敗,建議直接上傳圖片檔案

可以使用 easycom 模式。

⚠️ 注意,easycom 不支持 TIcon 這種大駝峰,只能是 t-icon,這種中劃線形式。

6.14. visible

下面幾個組件在關閉時,需要父組件中設置 visiblefalse,否則無法再次開啟。也就是 visible 只能是受控的。可以給 visible 屬性增加 v-model 語法糖。

  • drawer
  • cascader
  • calendar
  • date-time-picker
  • color-picker

7. 支付寶小程序

7.1. styleIsolation

支付寶小程序只支持在 json 文件中配置 styleIsolation,參考文檔

uni-app 會靜態分析組件中的 styleIsolation 配置,放到組件對應的 json 文件中。源碼地址:packages/uni-mp-vite/src/plugins/entry.ts

正則表達式如下:

const styleIsolationRE = [
  /defineOptions\s*[\s\S]*?styleIsolation\s*:\s*['"](isolated|apply-shared|shared)['"]/,
  /export\s+default\s+[\s\S]*?styleIsolation\s*:\s*['|"](isolated|apply-shared|shared)['|"]/,
];

所以,不能用 uniComponent 在運行時添加,只能在 Vue 中顯式聲明。

7.2. background

Stepper 中需顯式聲明 background 和 padding。

轉存失敗,建議直接上傳圖片檔案
轉存失敗,建議直接上傳圖片檔案

Search 中同樣問題。

轉存失敗,建議直接上傳圖片檔案
轉存失敗,建議直接上傳圖片檔案

7.3. disable-scroll

滾動穿透問題,uniapp 有通用方案,支付寶下無效,需要設置 disable-scroll。參考文檔

轉存失敗,建議直接上傳圖片檔案

⚠️ 注意,設置 disable-scrolltrue 後,所有子元素的滾動都不能冒泡了,即便子元素設置的 disable-scrollfalse,所以也盡可能減少 disable-scroll 屬性的覆蓋範圍。

7.4. :deep 編譯問題

避免 less 中兩個 :deep 嵌套,其中一個不會被轉化。

轉存失敗,建議直接上傳圖片檔案
轉存失敗,建議直接上傳圖片檔案

7.5. scroll-view

微信小程序 scroll-view,寬度 100%。支付寶小程序不是,需手動設置,不設置的話,撐不開。

轉存失敗,建議直接上傳圖片檔案
轉存失敗,建議直接上傳圖片檔案

8. 抖音小程序

8.1. virtualHost

遇到一個點擊事件不能傳遞的問題,排查下來以為是不能用 uniComponent 包裹,猜測其內部會靜態檢測 js 文件。後面發現是不能使用 virtualHost: true,不止 button 組件,其他組件也不一樣。

8.2. 樣式穿透

抖音小程序原生的話,可以用 externalClasses 來進行樣式覆蓋,但前面提到過 uni-app 不支持。

它也不支持標籤選擇器,加上剛說的不能用 virtualHost: true,所以它的樣式穿透是最麻煩的。

解決方案是,根據具體情況,對 class/t-class/style/custom-style 這些屬性區分平臺處理,比如

  • DropdownItem 組件中,btn 用了 class/t-class 區分,radio-group/checkbox-group 用了 custom-style
  • AvatarGroup 組件中,avatar 用了 setStylechildren 獲取),因為 avatar 是外部定義的,無法用 custom-style
  • 涉及到偽類的只能用 class,不能用 custom-style

8.3. 父子關係

抖音小程序給兩個組件綁定父子關係也是最複雜的,其他小程序及H5可以通過 provide/inject 來收集 parent,抖音小程序中找不到(下面部分截圖是放的 PressUI 組件庫的)。

轉存失敗,建議直接上傳圖片檔案

這裡想到一個辦法是遞歸調用 $parent,找最近的一個和目標組件名稱相同的 parent。比如 picker-item 中就找組件名稱為 TPicker 最近的父組件。

但是,抖音小程序子孫組件的 $parent 竟然就是頁面,頁面的所有 $children 都是拉平的。基於此,想到的辦法是從上往下遍歷這個拉平的 $children,找距離子組件最近的一個父組件。

轉存失敗,建議直接上傳圖片檔案
轉存失敗,建議直接上傳圖片檔案

但是,頁面的 $children 並不是"父子父子父子.."這樣順序排列的,而是"父父父子子子...",導致 $children 收集有問題,要麼多於實際,要麼為空。

轉存失敗,建議直接上傳圖片檔案

想到的辦法是父子組件之間傳遞一個 relationKey,這個值是唯一的,找 $parent 時就不會找錯了。

function findNearListParent(children = [], name) {
  let temp;
  for (const item of children) {
    const parentRelationKey = item.$props?.relationKey;
    const thisRelationKey = this.$props?.relationKey;
    if (item.$options.name === name && parentRelationKey === thisRelationKey) {
      temp = item;
    }
    if (item === this && temp) {
      return temp;
    }
  }

  return temp;
}

上面的 relationKey 應該永遠從業務傳入。內部組件,不管父子,都只接受 props,不自己生成,減少複雜度。這樣的話,不管用 slot<x><x-item></x> 還是用一個 <x>,都能保證 relationKey 同一個,且不論空還是不空,都相等的。

此外,還有這種游離在依賴樹之外的 vm 實例,也拿不到 provide 的值。

轉存失敗,建議直接上傳圖片檔案

這種主要發生在 Popup 組件內部的父子關係,比如 dropdown-menu 組件中的 radio-group/radiocascader 組件 tab 模式的 tabs/tab-panel

這種問題的一個解決方案是在使用它們的地方手動關聯。

8.4. 生命週期

Vue 中父子組件生命週期正常的執行順序是:父組件先創建,然後子組件創建;子組件先掛載,然後父組件掛載,即“父beforeCreate-> 父create -> 子beforeCreate-> 子created -> 子mounted -> 父mounted”。

抖音小程序並不遵循這樣的規律。

轉存失敗,建議直接上傳圖片檔案

這個問題會導致父子組件的初始化數據出問題,之前在父組件 mounted 中執行的初始邏輯,會因為還沒收集完 $children,而失敗。

轉存失敗,建議直接上傳圖片檔案

解決辦法有兩種,可用延時,也可用回調。回調更安全,延時可能跟機器性能有關。回調就是在子組件 mounted 的時候調用父組件的數據初始化方法。

9. 其他

9.1. 最簡單的

button 不是最簡單的,loading/icon 才是最簡單的,它們是 button 的子元素。

9.2. 組件歸類

轉存失敗,建議直接上傳圖片檔案

導航類

  • Navbar、Tabbar、Sidebar、Indexes 分別是上下左右四個方向的導航,固定
  • Drawer、BackTop 都是可隱藏的,點擊某處或滑動到某處時才顯示
  • Tabs 是業務中最常用的導航類組件,Steps 比 Tabs 更苛刻,有順序,這兩都以 s 結尾

反饋類

  • Overlay、Popup、Loading 基礎
  • Message、Toast、Dialog、NoticeBar 是一類,Message 上+動態,Toast 中間,更重,Dialog 中間,更重,NoticeBar 上+固定
  • DropdownMenu、ActionSheet 一個從上往下顯示,一個從下往上
  • SwipeCell,PulldownRefresh 一個向左滑,一個向下滑
  • Guide 特殊,全局,其他的都是局部

輸入類

  • Input、Textarea、Search,文字輸入
  • Radio、Checkbox、Switch,點擊選擇
  • Stepper、Slider,數字選擇(輸入)一個是點擊,一個是滑動
  • Picker,Cascader、TreeSelect,滑動選擇
  • Calendar、DatetimePicker,特殊場景
  • ColorPicker,特殊場景
  • Rate,特殊場景
  • Upload,特殊場景

9.3. 野蠻生長

只有流量大的、用戶多的APP,才可能有小程序。國內小程序生態百花齊放,沒有兩個是完全一樣的。每一種小程序框架、文檔、運營平台、開發者工具、審核等都需要不少的工作量、不少的人力。看得出來中國互聯網過去幾年發展的可以。

9.4. 圖標

轉存失敗,建議直接上傳圖片檔案

上面是幾個小程序開發者工具的圖標

  • 微信/qq、支付寶、百度(BAT)
  • 抖音、快手、小紅書(分享社區)
  • 京東

有意思的是,大家想的都差不多

  1. 體現連接
    • 抖音,平面
    • 京東,立體
    • 快手,橫向
    • 百度,中間
  2. 代碼符號
    • 支付寶
    • 小紅書
    • 微信(結合了自己的 logo)
  3. 產品 logo 變形
    • QQ
    • 微信

9.5. wxComponent

tdesign-miniprogramwxComponent 類的作用:

  1. 屬性,處理受控屬性,增加 default* 屬性的默認值,增加 style/customStyle 屬性,增加 aria* 相關屬性
  2. externalClasses,增加 class
  3. 方法,增加 _trigger,兼容受控情況下的拋出事件,非生命週期函數掛載在 methods 對象上
  4. 生命週期函數放到 lifetimes

9.6. uni-app

src/core/runtime/mp/polyfill/index.js

uni-app 中運行時對 vant-weapppolyfill 核心邏輯

9.7. data

只要不在模板中使用data 不用提前聲明,created 中動態聲明即可

created() {
  this.xxx = 'xxx';
}

9.8. Slider 組件細節

前置變數:

  • initLeft = boxLeft - halfblock
  • initRight = boxRight - halfblock
  • maxRange = boxRight - boxLeft - blockSize - 6 ( 6 是邊框)

capsule 模式下:

  1. 左邊滑塊滑動,offset = blockSize + 3currentLeft = clientLeft - initLeft - offset,就是 clientLeft - boxLeft - halfBlock - 3
  2. 右邊滑動滑動,offset = - 3currentIRight = -(clientRight - initRight - offset),就是 boxRight - clientRight - halfBlock - 3

假設 boxLeft = 0boxRight = 100, halfBlock = 10

  • 左就是 clientLeft - 13,左邊最小是 13
  • 右就是 87 - clientRight,右邊最大是 87
  • maxRange 就是 74

轉存失敗,建議直接上傳圖片檔案

圖中分別是左、右、邊框。

10. 反饋

有任何問題,建議通過 Github issues 反饋。


原文出處:https://juejin.cn/post/7571650164844068898


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝13   💬4   ❤️4
439
🥈
我愛JS
📝1   💬3   ❤️2
46
🥉
酷豪
1
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付