面試官: 寫過『通用前端組件』嗎?

来源:https://www.cnblogs.com/duxinyi/archive/2019/10/15/11676908.html
-Advertisement-
Play Games

前言 設計前端組件是最能考驗開發者基本功的測試之一,因為調用Material design、Antd、iView 等現成組件庫的 API 每個人都可以做到,但是很多人並不知道很多常用組件的設計原理。 能否設計出通用前端組件也是區分前端工程師和前端api調用師的標準之一,那麼應該如何設計出一個通用組件 ...


前言

設計前端組件是最能考驗開發者基本功的測試之一,因為調用Material design、Antd、iView 等現成組件庫的 API 每個人都可以做到,但是很多人並不知道很多常用組件的設計原理。

能否設計出通用前端組件也是區分前端工程師和前端api調用師的標準之一,那麼應該如何設計出一個通用組件呢?

下文中提到的組件庫通常是指單個組件,而非集合的概念,集合概念的組件庫是 Antd iView這種,我們所說的組件庫是指集合中的單個組件,集合性質的組件庫需要考慮的要更多.


文章目錄

  1. 前端組件庫的設計原則
  2. 組件庫的技術選型
  3. 如何快速啟動一個組件庫項目
  4. 如何設計一個輪播圖組件

1.前端組件庫的設計原則

1.1 細粒度的考量

我們在學習設計模式的時候會遇到很多種設計原則,其中一個設計原則就是單一職責原則,在組件庫的開發中同樣適用,我們原則上一個組件只專註一件事情,單一職責的組件的好處很明顯,由於職責單一就可以最大可能性地復用組件,但是這也帶來一個問題,過度單一職責的組件也可能會導致過度抽象,造成組件庫的碎片化。

舉個例子,一個自動完成組件(AutoComplete),他其實是由 Input 組件和 Select 組件組合而成的,因此我們完全可以復用之前的相關組件,就比如 Antd 的AutoComplete組件中就復用了Select組件,同時Calendar、 Form 等等一系列組件都復用了 Select 組件,那麼Select 的細粒度就是合適的,因為 Select 保持的這種細粒度很容易被覆用.

那麼還有一個例子,一個徽章數組件(Badge),它的右上角會有紅點提示,可能是數字也可能是 icon,他的職責當然也很單一,這個紅點提示也理所當然也可以被單獨抽象為一個獨立組件,但是我們通常不會將他作為獨立組件,因為在其他場景中這個組件是無法被覆用的,因為沒有類似的場景再需要小紅點這個小組件了,所以作為獨立組件就屬於細粒度過小,因此我們往往將它作為 Badge 的內部組件,比如在 Antd 中它以ScrollNumber的名稱作為Badge的內部組件存在。

所以,所謂的單一職責組件要建立在可復用的基礎上,對於不可復用的單一職責組件我們僅僅作為獨立組件的內部組件即可。

1.2 通用性考量

我們要設計的本身就是通用組件庫,不同於我們常見的業務組件,通用組件是與業務解耦但是又服務於業務開發的,那麼問題來了,如何保證組件的通用性,通用性高一定是好事嗎?

比如我們設計一個選擇器(Select)組件,通常我們會設計成這樣


這是一個我們最常見也最常用的選擇器,但是問題是其通用性大打折扣

當我們有一個需求是長這樣的時候,我們之前的選擇器組件就不符合要求了,因為這個 Select 組件的最下部需要有一個可拓展的條目的按鈕

這個時候我們難道要重新修改之前的選擇器組件,甚至再造一個符合要求的選擇器組件嗎?一旦有這種情況發生,那麼只能說明之前的選擇器組件通用性不夠,需要我們重新設計.

Antd 的 Select 組件預留了dropdownRender來進行自定義渲染,其依賴的 rc-select組件中的代碼如下

Antd 依賴了大量以rc-開頭的底層組件,這些組件被react-component團隊(同時也就是Antd 團隊)維護,其主要實現組件的底層邏輯,Antd 則是在此基礎上添加Ant Design設計語言而實現的

當然類似的設計還有很多,通用性設計其實是一定意義上放棄對 DOM 的掌控,而將 DOM 結構的決定權轉移給開發者,dropdownRender其實就是放棄對 Select 下拉菜單中條目的掌控,Antd 的 Select 組件其實還有一個沒有在文檔中體現的方法getInputElement應該是對 Input 組件的自定義方法,Antd整個 Select 的組件設計非常複雜,基本將所有的 DOM 結構控制權全部暴露給了開發者,其本身只負責底層邏輯和最基本的 DOM 結構.

這是 Antd 所依賴的 re-select 最終 jsx 的結構,其 DOM 結構很簡單,但是暴露了大量自定義渲染的介面給開發者.

return (
      <SelectTrigger
        onPopupFocus={this.onPopupFocus}
        onMouseEnter={this.props.onMouseEnter}
        onMouseLeave={this.props.onMouseLeave}
        dropdownAlign={props.dropdownAlign}
        dropdownClassName={props.dropdownClassName}
        dropdownMatchSelectWidth={props.dropdownMatchSelectWidth}
        defaultActiveFirstOption={props.defaultActiveFirstOption}
        dropdownMenuStyle={props.dropdownMenuStyle}
        transitionName={props.transitionName}
        animation={props.animation}
        prefixCls={props.prefixCls}
        dropdownStyle={props.dropdownStyle}
        combobox={props.combobox}
        showSearch={props.showSearch}
        options={options}
        multiple={multiple}
        disabled={disabled}
        visible={realOpen}
        inputValue={state.inputValue}
        value={state.value}
        backfillValue={state.backfillValue}
        firstActiveValue={props.firstActiveValue}
        onDropdownVisibleChange={this.onDropdownVisibleChange}
        getPopupContainer={props.getPopupContainer}
        onMenuSelect={this.onMenuSelect}
        onMenuDeselect={this.onMenuDeselect}
        onPopupScroll={props.onPopupScroll}
        showAction={props.showAction}
        ref={this.saveSelectTriggerRef}
        menuItemSelectedIcon={props.menuItemSelectedIcon}
        dropdownRender={props.dropdownRender}
        ariaId={this.ariaId}
      >
        <div
          id={props.id}
          style={props.style}
          ref={this.saveRootRef}
          onBlur={this.onOuterBlur}
          onFocus={this.onOuterFocus}
          className={classnames(rootCls)}
          onMouseDown={this.markMouseDown}
          onMouseUp={this.markMouseLeave}
          onMouseOut={this.markMouseLeave}
        >
          <div
            ref={this.saveSelectionRef}
            key="selection"
            className={`${prefixCls}-selection
            ${prefixCls}-selection--${multiple ? 'multiple' : 'single'}`}
            role="combobox"
            aria-autocomplete="list"
            aria-haspopup="true"
            aria-controls={this.ariaId}
            aria-expanded={realOpen}
            {...extraSelectionProps}
          >
            {ctrlNode}
            {this.renderClear()}
            {this.renderArrow(!!multiple)}
          </div>
        </div>
      </SelectTrigger>
    );

那麼這麼多需要自定義的地方,這個 Select 組件豈不是很難用?因為好像所有地方都需要開發者自定義,通用性設計在將 DOM 結構決定權交給開發者的同時也保留了預設結構,在開發者沒有顯示自定義的時候預設使用預設渲染結構,其實 Select 的基本使用很方便,如下:

    <Select defaultValue="lucy" style={{ width: 120 }} disabled>
      <Option value="lucy">Lucy</Option>
    </Select>

組件的形態(DOM結構)永遠是千變萬化的,但是其行為(邏輯)是固定的,因此通用組件的秘訣之一就是將 DOM 結構的控制權交給開發者,組件只負責行為和最基本的 DOM 結構


2 技術選型

2.1 css 解決方案

由於CSS 本身的眾多缺陷,如書寫繁瑣(不支持嵌套)、樣式易衝突(沒有作用域概念)、缺少變數(不便於一鍵換主題)等不一而足。為瞭解決這些問題,社區里的解決方案也是出了一茬又一茬,從最早的 CSS prepocessor(SASS、LESS、Stylus)到後來的後起之秀 PostCSS,再到 CSS Modules、Styled-Components 等。

Antd 選擇了 less 作為 css 的預處理方案,Bootstrap 選擇了 Scss,這兩種方案孰優孰劣已經爭論了很多年了:

SCSS和LESS相比有什麼優勢?

但是不管是哪種方案都有一個很煩人的點,就是需要額外引入 css,比如 Antd 需要這樣顯示引入:

import Button from 'antd/lib/button';
import 'antd/lib/button/style'; 

為瞭解決這種尷尬的情況,Antd 用 Babel 插件將這種情況 Hack 掉了

material-ui並不存在這種情況,他不需要顯示引入 css,這個最流行的 React 前端組件庫裡面只有 js 和 ts 兩種代碼,並不存在 css 相關的代碼,為什麼呢?

他們用 jss 作為css-in-js 的解決方案,jsx 的引入已經將 js 和 html 耦合,css-in-js將 css 也耦合進去,此時組件便不需要顯示引入 css,而是直接引用 js 即可.

這不是退化到史前前端那種寫內聯樣式的時代了嗎?

並不是,史前前端的內聯樣式是整個項目耦合的狀態,當然要被拋棄到歷史的垃圾堆中,後來的樣式和邏輯分離,實際上是以頁面為維度將 js css html 解耦的過程,如今的時代是組件化的時代了,jsx 已經將 js 和 html 框定到一個組件中,css 依然處於分離狀態,這就導致了每次引用組件卻還需要顯示引入 css,css-in-js 正式徹底組件化的解決方案.

當然,我個人目前在用 styled-components,其優點引用如下:

  1. 首先,styled-components 所有語法都是標準 css 語法,同時支持 scss 嵌套等常用語法,覆蓋了所有 css 場景。

  2. 在樣式覆寫場景下,styled-components 支持在任何地方註入全局 css,就像寫普通 css 一樣

  3. styled-components 支持自定義 className,兩種方式,一種是用 babel 插件, 另一種方式是使用 styled.div.withConfig({ componentId: "prefix-button-container" }) 相當於添加 className="prefix-button-container"

  4. className 語義化更輕鬆,這也是 class 起名的初衷

  5. 更適合組件庫使用,直接引用 import "module" 即可,否則你有三條路可以走:像 antd 一樣,單獨引用 css,你需要給 node_modules 添加 css-loader;組件內部直接 import css 文件,如果任何業務項目沒有 css-loader 就會報錯;組件使用 scss 引用,所有業務項目都要配置一份 scss-loader 給 node_modules;這三種對組件庫來說,都沒有直接引用來的友好

  6. 當你寫一套組件庫,需要單獨發包,又有統一樣式的配置文件需求,如果這個配置文件是 js 的,所有組件直接引用,對外完全不用關註。否則,如果是 scss 配置文件,擺在面前還是三條路:每個組件單獨引用 scss 文件,需要每個業務項目給 node_modules 添加 scss-loader(如果業務用了 less,還要裝一份 scss 是不);或者業務方只要使用了你的組件庫,就要在入口文件引用你的 scss 文件,比如你的組件叫 button,scss 可能叫 common-css,別人聽都沒聽過,還要查文檔;或者業務方在 webpack 配置中單獨引用你的 common-css,這也不科學,如果用了3個組件庫,天天改 webpack 配置也很不方便。

  7. 當 css 設置了一半樣式,另一半真的需要 js 動態傳入,你不得不 css + css-in-js 混合使用,項目久了,維護的時候發現某些 css-in-js 不變了,可以固化在 css 里,css 里固定的值又因為新去求變得可變了,你又得拿出來放在 css-in-js 里,實踐過就知道有多麼煩心。

2.2 js 解決方案

選 Typescript ,因為巨硬大法好...

可以看看知乎問題下我的回答你為什麼不用 Typescript

或者看此文TypeScript體系調研報告


3. 如何快速啟動一個組件庫項目

組件的具體實現部分當然是組件庫的核心,但是在現代前端庫中其他部分也必不可少,我們需要一堆工具來輔助我們開發,例如編譯工具、代碼檢測工具、打包工具等等。

3.1 打包工具(rollup vs webpack)

市面上打包工具數不勝數,最火爆的當然是需要配置工程師專門配置的webpack,但是在類庫開發領域它有一個強大的對手就是 rollup。

現代市面上主流的庫基本都選擇了 rollup 作為打包工具,包括Angular React 和 Vue, 作為基礎類庫的打包工具 rollup 的優勢如下:

  • Tree Shaking: 自動移除未使用的代碼, 輸出更小的文件
  • Scope Hoisting: 所有模塊構建在一個函數內, 執行效率更高
  • Config 文件支持通過 ESM 模塊格式書寫
    可以一次輸出多種格式:
  • 模塊規範: IIFE, AMD, CJS, UMD, ESM
    Development 與 production 版本: .js, .min.js

雖然上面部分功能已經被 webpack 實現了,但是 rollup 明顯引入得更早,而Scope Hoisting更是殺手鐧,由於 webpack 不得不在打包代碼中構建模塊系統來適應 app 開發(模塊系統對於單一類庫用處很小),Scope Hoisting將模塊構建在一個函數內的做法更適合類庫的打包.

3.2 代碼檢測

由於 JavaScript 各種詭異的特性和大型前端項目的出現,代碼檢測工具已經是前端開發者的標配了,Douglas Crockford最早於2002創造出了 JSLint,但是其無法拓展,具有極強的Douglas Crockford個人色彩,Anton Kovalyov由於無法忍受 JSLint 無法拓展的行為在2011年發佈了可拓展的JSHint,一時之間JSHint成為了前端代碼檢測的流行解決方案.

隨後的2013年,Nicholas C. Zakas鑒於JSHint拓展的靈活度不夠的問題開發了全新的基於 AST 的 Lint 工具 ESLint,並隨著 ES6的流行統治了前端界,ESLint 基於Esprima進行 JavaScript 解析的特性極易拓展,JSHint 在很長一段時間無法支持 ES6語法導致被 ESLint 超越.

但是在 Typescript 領域 ESLint 卻處於弱勢地位,TSLint 的出現要比 ESLint 正式支持 Typescript 早很多,目前 TSLint 似乎是 TS 的事實上的代碼檢測工具.

註: 文章成文較早,我也沒想到前陣子 TS 官方欽點了 ESLint,TSLint 失寵了,面向未來的官方標配的代碼檢測工具肯定是 ESLint 了,但是 TSLint 目前依然被大量使用,現在仍然可以放心使用

代碼檢測工具是一方面,代碼檢測風格也需要我們做選擇,市面上最流行的代碼檢測風格應該是 Airbnb 出品的eslint-config-airbnb,其最大的特點就是極其嚴格,沒有給開發者任何選擇的餘地,當然在大型前端項目的開發中這種嚴格的代碼風格是有利於協作的,但是作為一個類庫的代碼檢測工具而言並不適合,所以我們選擇了eslint-config-standard這種相對更為寬鬆的代碼檢測風格.

3.3 commit 規範

以下兩種 commit 哪個更嚴謹且易於維護?

最開始使用 commit 的時候我也經常犯下圖的錯誤,直到看到很多明星類庫的 commit 才意識到自己的錯誤,寫好 commit message 不僅有助於他人 review, 還可以有效的輸出 CHANGELOG, 對項目的管理實際至關重要.

目前流行的方案是 Angular 團隊的規範,其關於 head 的大致規範如下:

  • type: commit 的類型
  • feat: 新特性
  • fix: 修改問題
  • refactor: 代碼重構
  • docs: 文檔修改
  • style: 代碼格式修改, 註意不是 css 修改
  • test: 測試用例修改
  • chore: 其他修改, 比如構建流程, 依賴管理.
  • scope: commit 影響的範圍, 比如: route, component, utils, build...
  • subject: commit 的概述, 建議符合 50/72 formatting
  • body: commit 具體修改內容, 可以分為多行, 建議符合 50/72 formatting
  • footer: 一些備註, 通常是 BREAKING CHANGE 或修複的 bug 的鏈接.

當然規範人們不一定會遵守,我最初知道此類規範的時候也並沒有嚴格遵循,因為人總會偷懶,直到用commitizen將此規範集成到工具流中,每個 commit 就不得不遵循規範了.

我具體參考了這篇文章: 優雅的提交你的 Git Commit Message

3.4 測試工具

業務開發中由於前端需求變動頻繁的特性,導致前端對測試的要求並沒有後端那麼高,後端業務邏輯一旦定型變動很少,比較適合測試.

但是基礎類庫作為被反覆依賴的模塊和較為穩定的需求是必須做測試的,前端測試庫也可謂是種類繁多了,經過比對之後我還是選擇了目前最流行也是被三大框架同時選擇了的 Jest 作為測試工具,其優點很明顯:

  1. 開箱即用,內置斷言、測試覆蓋率工具,如果你用 MoCha 那可得自己手動配置 n 多了
  2. 快照功能,Jest 可以利用其特有的快照測試功能,通過比對 UI 代碼生成的快照文件
  3. 速度優勢,Jest 的測試用例是並行執行的,而且只執行發生改變的文件所對應的測試,提升了測試速度

3.5 其它

當然以上是主要工具的選擇,還有一些比如:

  • 代碼美化工具 prettier,解放人肉美化,同時利於不同人協作的風格一致
  • 持續集成工具 travis-ci,解放人肉測試 lint,利於保證每次 push 的可靠程度

3.6 快速啟動腳手架

那麼以上這麼多配置難道要我們每次都自己寫嗎?組件的具體實現才是組件庫的核心,我們為什麼要花這麼多時間在配置上面?

我們在建立 APP 項目時通常會用到框架官方提供的腳手架,比如 React 的 create-react-app,Angular 的 Angular-Cli 等等,那麼能不能有一個專門用於組件開發的快速啟動的腳手架呢?

有的,我最近開發了一款快速啟動組件庫開發的命令行工具--create-component

利用

create-component init <name>

來快速啟動項目,我們提供了豐富的可選配置,只要你做好技術選型後,根據提示去選擇配置即可,create-component 會自動根據配置生成腳手架,其靈感就來源於 vue-cli和 Angular-cli.


4. 如何設計一個輪播圖組件

說了很多理論,那麼實戰如何呢?設計一個通用組件試試吧!

4.1 輪播圖基本原理

輪播圖(Carousel),在 Antd 中被稱為走馬燈,可能是前端開發者最常見的組件之一了,不管是在 PC 端還是在移動端我們總能見到他的身影.

那麼我們通常是如何使用輪播圖的呢?Antd 的代碼如下


  <Carousel>
    <div><h3>1</h3></div>
    <div><h3>2</h3></div>
    <div><h3>3</h3></div>
    <div><h3>4</h3></div>
  </Carousel>

問題是我們在Carousel中放入了四組div為什麼一次只顯示一組呢?

圖中被紅框圈住的為可視區域,可視區域的位置是固定的,我們只需要移動後面div的位置就可以做到1 2 3 4四個子組件輪播的效果,那麼子組件2目前在可視區域是可以被看到的,1 3 4應該被隱藏,這就需要我們設置overflow 屬性為 hidden來隱藏非可視區域的子組件.

複製查看動圖: https://images2015.cnblogs.com/blog/979044/201707/979044-20170710105934040-1007626405.gif

因此就比較明顯了,我們設計一個可視視窗組件Frame,然後將四個 div共同放入幻燈片組合組件SlideList中,並用SlideItem分別將 div包裹起來,實際代碼應該是這樣的:

 <Frame>
    <SlideList>
        <SlideItem>
            <div><h3>1</h3></div>  
        </SlideItem>
        <SlideItem>
            <div><h3>2</h3></div>  
        </SlideItem>
        <SlideItem>
            <div><h3>3</h3></div>  
        </SlideItem>
        <SlideItem>
            <div><h3>4</h3></div>  
        </SlideItem>
    </SlideList>
  </Frame>

我們不斷利用translateX來改變SlideList的位置來達到輪播效果,如下圖所示,每次輪播的觸發都是通過改變transform: translateX()來操作的

4.2 輪播圖基礎實現

搞清楚基本原理那麼實現起來相對容易了,我們以移動端的實現為例,來實現一個基礎的移動端輪播圖.

首先我們要確定可視視窗的寬度,因為我們需要這個寬度來計算出SlideList的長度(SlideList的長度通常是可視視窗的倍數,比如要放三張圖片,那麼SlideList應該為可視視窗的至少3倍),不然我們無法通過translateX來移動它.

我們通過getBoundingClientRect來獲取可視區域真實的長度,SlideList的長度那麼為:

slideListWidth = (len + 2) * width(len 為傳入子組件的數量,width 為可視區域寬度)

至於為什麼要+2後面會提到.

  /**
   * 設置輪播區域尺寸
   * @param x
   */
  private setSize(x?: number) {
    const { width } = this.frameRef.current!.getBoundingClientRect()
    const len = React.Children.count(this.props.children)
    const total = len + 2

    this.setState({
      slideItemWidth: width,
      slideListWidth: total * width,
      total,
      translateX: -width * this.state.currentIndex,
      startPositionX: x !== undefined ? x : 0,
    })
  }

獲取到了總長度之後如何實現輪播呢?我們需要根據用戶反饋來觸發輪播,在移動端通常是通過手指滑動來觸發輪播,這就需要三個事件onTouchStart onTouchMove onTouchEnd.

onTouchStart顧名思義是在手指觸摸到屏幕時觸發的事件,在這個事件里我們只需要記錄下手指觸摸屏幕的橫軸坐標 x 即可,因為我們會通過其橫向滑動的距離大小來判斷是否觸發輪播

  /**
   * 處理觸摸起始時的事件
   *
   * @private
   * @param {React.TouchEvent} e
   * @memberof Carousel
   */
  private onTouchStart(e: React.TouchEvent) {
    clearInterval(this.autoPlayTimer)
    // 獲取起始的橫軸坐標
    const { x } = getPosition(e)
    this.setSize(x)
    this.setState({
      startPositionX: x,
    })
  }

onTouchMove顧名思義是處於滑動狀態下的事件,此事件在onTouchStart觸發後,onTouchEnd觸發前,在這個事件中我們主要做兩件事,一件事是判斷滑動方向,因為用戶可能向左或者向右滑動,另一件事是讓輪播圖跟隨手指移動,這是必要的用戶反饋.

 /**
   * 當觸摸滑動時處理事件
   *
   * @private
   * @param {React.TouchEvent} e
   * @memberof Carousel
   */
  private onTouchMove(e: React.TouchEvent) {
    const { slideItemWidth, currentIndex, startPositionX } = this.state
    const { x } = getPosition(e)

    const deltaX = x - startPositionX
    // 判斷滑動方向
    const direction = deltaX > 0 ? 'right' : 'left'

    this.setState({
      direction,
      moveDeltaX: deltaX,
      // 改變translateX來達到輪播組件跟隨手指移動的效果
      translateX: -(slideItemWidth * currentIndex) + deltaX,
    })
  }

onTouchEnd顧名思義是滑動完畢時觸發的事件,在此事件中我們主要做一個件事情,就是判斷是否觸發輪播,我們會設置一個閾值threshold,當滑動距離超過這個閾值時才會觸發輪播,畢竟沒有閾值的話用戶稍微觸碰輪播圖就造成輪播,誤操作會造成很差的用戶體驗.

  /**
   * 滑動結束處理的事件
   *
   * @private
   * @memberof Carousel
   */
  private onTouchEnd() {
    this.autoPlay()
    const { moveDeltaX, slideItemWidth, direction } = this.state
    const threshold = slideItemWidth * THRESHOLD_PERCENTAGE
    // 判斷是否輪播
    const moveToNext = Math.abs(moveDeltaX) > threshold

    if (moveToNext) {
        // 如果輪播觸發那麼進行輪播操作
      this.handleSwipe(direction!)
    } else {
        // 輪播不觸發,那麼輪播圖回到原位
      this.handleMisoperation()
    }
  }

4.3 輪播圖的動畫效果

我們常見的輪播圖肯定不是生硬的切換,一般在輪播中會有一個漸變或者緩動的動畫,這就需要我們加入動畫效果.

我們製作動畫通常有兩個選擇,一個是用 css3自帶的動畫效果,另一個是用瀏覽器提供的requestAnimationFrame API

孰優孰劣?css3簡單易用上手快,相容性好,requestAnimationFrame 靈活性更高,能實現 css3實現不了的動畫,比如眾多緩動動畫 css3都束手無策,因此我們毫無疑問地選擇了requestAnimationFrame.

雙方對比請看張鑫旭大神的CSS3動畫那麼強,requestAnimationFrame還有毛線用?

想用requestAnimationFrame實現緩動效果就需要特定的緩動函數,下麵就是典型的緩動函數

type tweenFunction = (t: number, b: number, _c: number, d: number) => number
const easeInOutQuad: tweenFunction = (t, b, _c, d) => {
    const c = _c - b;
    if ((t /= d / 2) < 1) {
      return c / 2 * t * t + b;
    } else {
      return -c / 2 * ((--t) * (t - 2) - 1) + b;
    }
}

緩動函數接收四個參數,分別是:

  • t: 時間
  • b:初始位置
  • _c:結束的位置
  • d:速度

通過這個函數我們能算出每一幀輪播圖所在的位置, 如下:

在獲取每一幀對應的位置後,我們需要用requestAnimationFrame不斷遞歸調用依次移動位置,我們不斷調用animation函數是其觸發函數體內的this.setState({ translateX: tweenQueue[0], })來達到移動輪播圖位置的目的,此時將這數組內的30個位置依次快速執行就是一個緩動動畫效果.

  /**
   * 遞歸調用,根據軌跡運動
   *
   * @private
   * @param {number[]} tweenQueue
   * @param {number} newIndex
   * @memberof Carousel
   */
  private animation(tweenQueue: number[], newIndex: number) {
    if (tweenQueue.length < 1) {
      this.handleOperationEnd(newIndex)
      return
    }
    this.setState({
      translateX: tweenQueue[0],
    })
    tweenQueue.shift()
    this.rafId = requestAnimationFrame(() => this.animation(tweenQueue, newIndex))
  }

但是我們發現了一個問題,當我們移動輪播圖到最後的時候,動畫出現了問題,當我們向左滑動最後一個輪播圖div4時,這種情況下應該是圖片向左滑動,然後第一張輪播圖div1進入可視區域,但是反常的是圖片快速向右滑動div1出現在可是區域...

因為我們此時將位置4設置為了位置1,這樣才能達到不斷迴圈的目的,但是也造成了這個副作用,圖片行為與用戶行為產生了相悖的情況(用戶向左划動,圖片向右走).

目前業界的普遍做法是將圖片首尾相連,例如圖片1前面連接一個圖片4,圖片4後跟著一個圖片1,這就是為什麼之前計算長度時要+2

slideListWidth = (len + 2) * width(len 為傳入子組件的數量,width 為可視區域寬度)

當我們移動圖片4時就不會出現上述向左滑圖片卻向右滑的情況,因為真實情況是:

圖片4 -- 滑動為 -> 偽圖片1 也就是位置 5 變成了位置 6

當動畫結束之後,我們迅速把偽圖片1的位置設置為真圖片1,這其實是個障眼法,也就是說動畫執行過程中實際上是圖片4偽圖片1的過程,當結束後我們偷偷把偽圖片1換成真圖片1,因為兩個圖一模一樣,所以這個轉換的過程用戶根本看不出來...

如此一來我們就可以實現無縫切換的輪播圖了

4.4 改進方向

我們實現了輪播圖的基本功能,但是其通用性依然存在缺陷:

  1. 提示點的自定義: 我的實現是一個小點,而 antd 是用的條,這個地方完全可以將 dom 結構的決定權交給開發者.

  1. 方向的自定義: 本輪播圖只有水平方向的實現,其實也可以有縱向輪播

  1. 多張輪播:除了單張輪播也可以多張輪播

以上都是可以對輪播圖進行拓展的方向,相關的還有性能優化方面

我們的具體代碼中有一個相關實現,我們的輪播圖其實是有自動輪播功能的,但是很多時候頁面並不在用戶的可視頁面中,我們可以根據是否頁面被隱藏來取消定時器終止自動播放.

github項目地址

以上 demo 僅供參考,實際項目開發中最好還是使用成熟的開源組件,要有造輪子的能力和不造輪子的覺悟

參考鏈接

公眾號

想要實時關註筆者最新的文章和最新的文檔更新請關註公眾號程式員面試官,後續的文章會優先在公眾號更新.

簡歷模板: 關註公眾號回覆「模板」獲取

《前端面試手冊》: 配套於本指南的突擊手冊,關註公眾號回覆「fed」獲取

2019-08-12-03-18-41

本文由博客一文多發平臺 OpenWrite 發佈!


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

-Advertisement-
Play Games
更多相關文章
  • 示例代碼托管在: "http://www.github.com/dashnowords/blogs" 博客園地址: "《大史住在大前端》原創博文目錄" 華為雲社區地址: "【你要的前端打怪升級指南】" [TOC] 一. CSS動畫 和 JS動畫 Web動畫的本質是元素 狀態改變造成的樣式變更 ,CS ...
  • 不知道你們有沒有發現,null和undefined竟然有這樣的關係~ ...
  • Vue.js是一套構建用戶界面的漸進式框架。與其他重量級框架不同的是,Vue 採用自底向上增量開發的設計。Vue 的核心庫只關註視圖層,並且非常容易學習,非常容易與其它庫或已有項目整合。另一方面,Vue 完全有能力驅動採用單文件組件和Vue生態系統支持的庫開發的複雜單頁應用。 那麼在windows系 ...
  • 一、回調函數 優點:簡單,方便,易用 缺點:易造成回調函數地獄,回調函數中嵌套多個回調函數,因為多個非同步操作造成強耦合,代碼亂做一團,無法管理。 二、事件監聽 優點:與回調函數相比,事件監聽實現了代碼的解耦,方便代碼管理 缺點:使用不方便,每次都要手動地綁定和觸發事件 三、Promise 優點:將回 ...
  • 背景 現在手上在做的 React 項目因為年代久遠,用的 "Redux" ,寫代碼的體驗不太好,所以想升級一下引入 dva。以往使用 dva 都是使用 "dva cli" 直接生成 dva 項目,或者在使用 "ant design pro" 的時候使用 umi 直接生成 react + antd + ...
  • 理解javascript事件執行機制 眾所周知,js是一個單線程的語言,這意味著同一時間只能做一件事,但是我們又說js是非同步的。首先,單線程並不是沒有優點。作為瀏覽器腳本語言,JavaScript 的主要用途是與用戶互動,以及操作 DOM。這決定了它只能是單線程,否則會帶來很複雜的同步問題。比如,假 ...
  • 簡單的描述了淺拷貝和深拷貝的區別後,分別進行實現且所有方法都已進行試驗。 ...
  • CSS樣式 CSS概述 CSS Cascading Style Shees層疊樣式表 HTML定義網頁的內容,CSS定義內容的樣式。 內容和樣式相互分離,便於修改樣式。 CSS語法 註意:1.最後一條聲明可以沒有分號,但是為了以後修改方便,一般也加上分號。 2.為了使用樣式更加容易閱讀,可以將每條代 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...