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

前言

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

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

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


文章目錄

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

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

1.1 細粒度的考量

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

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

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

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

1.2 通用性考量

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

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


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

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

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

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

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

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

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

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 組件豈不是很難用?因為好像所有地方都需要開發(fā)者自定義,通用性設計在將 DOM 結構決定權交給開發(fā)者的同時也保留了默認結構,在開發(fā)者沒有顯示自定義的時候默認使用默認渲染結構,其實 Select 的基本使用很方便,如下:

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

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


2 技術選型

2.1 css 解決方案

由于CSS 本身的眾多缺陷延旧,如書寫繁瑣(不支持嵌套)、樣式易沖突(沒有作用域概念)槽地、缺少變量(不便于一鍵換主題)等不一而足迁沫。為了解決這些問題,社區(qū)里的解決方案也是出了一茬又一茬捌蚊,從最早的 CSS prepocessor(SASS集畅、LESS、Stylus)到后來的后起之秀 PostCSS缅糟,再到 CSS Modules挺智、Styled-Components 等。

Antd 選擇了 less 作為 css 的預處理方案,Bootstrap 選擇了 Scss,這兩種方案孰優(yōu)孰劣已經(jīng)爭論了很多年了:

SCSS和LESS相比有什么優(yōu)勢溺拱?

但是不管是哪種方案都有一個很煩人的點,就是需要額外引入 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 的引入已經(jīng)將 js 和 html 耦合,css-in-js將 css 也耦合進去,此時組件便不需要顯示引入 css,而是直接引用 js 即可.

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

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

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

  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 文件哺壶,如果任何業(yè)務項目沒有 css-loader 就會報錯屋吨;組件使用 scss 引用,所有業(yè)務項目都要配置一份 scss-loader 給 node_modules山宾;這三種對組件庫來說至扰,都沒有直接引用來的友好

  6. 當你寫一套組件庫,需要單獨發(fā)包资锰,又有統(tǒng)一樣式的配置文件需求敢课,如果這個配置文件是 js 的,所有組件直接引用绷杜,對外完全不用關注直秆。否則,如果是 scss 配置文件鞭盟,擺在面前還是三條路:每個組件單獨引用 scss 文件切厘,需要每個業(yè)務項目給 node_modules 添加 scss-loader(如果業(yè)務用了 less,還要裝一份 scss 是不)懊缺;或者業(yè)務方只要使用了你的組件庫疫稿,就要在入口文件引用你的 scss 文件,比如你的組件叫 button鹃两,scss 可能叫 common-css遗座,別人聽都沒聽過,還要查文檔俊扳;或者業(yè)務方在 webpack 配置中單獨引用你的 common-css途蒋,這也不科學,如果用了3個組件庫馋记,天天改 webpack 配置也很不方便号坡。

  7. 當 css 設置了一半樣式,另一半真的需要 js 動態(tài)傳入梯醒,你不得不 css + css-in-js 混合使用宽堆,項目久了,維護的時候發(fā)現(xiàn)某些 css-in-js 不變了茸习,可以固化在 css 里畜隶,css 里固定的值又因為新去求變得可變了,你又得拿出來放在 css-in-js 里,實踐過就知道有多么煩心籽慢。

2.2 js 解決方案

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

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

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


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

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

3.1 打包工具(rollup vs webpack)

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

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

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

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

3.2 代碼檢測

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

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

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

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

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

3.3 commit 規(guī)范

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


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

目前流行的方案是 Angular 團隊的規(guī)范,其關于 head 的大致規(guī)范如下:

  • 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 的鏈接.

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

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

3.4 測試工具

業(yè)務開發(fā)中由于前端需求變動頻繁的特性,導致前端對測試的要求并沒有后端那么高,后端業(yè)務邏輯一旦定型變動很少,比較適合測試.

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

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

3.5 其它

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

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

3.6 快速啟動腳手架

那么以上這么多配置難道要我們每次都自己寫嗎?組件的具體實現(xiàn)才是組件庫的核心,我們?yōu)槭裁匆ㄟ@么多時間在配置上面?

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

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

利用

create-component init <name>

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


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

說了很多理論,那么實戰(zhàn)如何呢?設計一個通用組件試試吧!

4.1 輪播圖基本原理

輪播圖(Carousel),在 Antd 中被稱為走馬燈,可能是前端開發(fā)者最常見的組件之一了,不管是在 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為什么一次只顯示一組呢?

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

復制查看動圖: 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的位置來達到輪播效果,如下圖所示,每次輪播的觸發(fā)都是通過改變transform: translateX()來操作的

4.2 輪播圖基礎實現(xiàn)

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

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

我們通過getBoundingClientRect來獲取可視區(qū)域真實的長度,SlideList的長度那么為:

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

至于為什么要+2后面會提到.

  /**
   * 設置輪播區(qū)域尺寸
   * @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,
    })
  }

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

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

  /**
   * 處理觸摸起始時的事件
   *
   * @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顧名思義是處于滑動狀態(tài)下的事件,此事件在onTouchStart觸發(fā)后,onTouchEnd觸發(fā)前,在這個事件中我們主要做兩件事,一件事是判斷滑動方向,因為用戶可能向左或者向右滑動,另一件事是讓輪播圖跟隨手指移動,這是必要的用戶反饋.

 /**
   * 當觸摸滑動時處理事件
   *
   * @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顧名思義是滑動完畢時觸發(fā)的事件,在此事件中我們主要做一個件事情,就是判斷是否觸發(fā)輪播,我們會設置一個閾值threshold,當滑動距離超過這個閾值時才會觸發(fā)輪播,畢竟沒有閾值的話用戶稍微觸碰輪播圖就造成輪播,誤操作會造成很差的用戶體驗.

  /**
   * 滑動結束處理的事件
   *
   * @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) {
        // 如果輪播觸發(fā)那么進行輪播操作
      this.handleSwipe(direction!)
    } else {
        // 輪播不觸發(fā),那么輪播圖回到原位
      this.handleMisoperation()
    }
  }

4.3 輪播圖的動畫效果

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

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

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

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

想用requestAnimationFrame實現(xiàn)緩動效果就需要特定的緩動函數(shù),下面就是典型的緩動函數(shù)

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;
    }
}

緩動函數(shù)接收四個參數(shù),分別是:

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

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


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

  /**
   * 遞歸調用,根據(jù)軌跡運動
   *
   * @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))
  }

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

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

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

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

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

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

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

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

4.4 改進方向

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

  1. 提示點的自定義: 我的實現(xiàn)是一個小點,而 antd 是用的條,這個地方完全可以將 dom 結構的決定權交給開發(fā)者.
  1. 方向的自定義: 本輪播圖只有水平方向的實現(xiàn),其實也可以有縱向輪播
  1. 多張輪播:除了單張輪播也可以多張輪播

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

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

github項目地址

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

參考鏈接

公眾號

想要實時關注筆者最新的文章和最新的文檔更新請關注公眾號程序員面試官,后續(xù)的文章會優(yōu)先在公眾號更新.

簡歷模板: 關注公眾號回復「模板」獲取

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

本文由博客一文多發(fā)平臺 OpenWrite 發(fā)布晨缴!

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末译秦,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子击碗,更是在濱河造成了極大的恐慌筑悴,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,194評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件稍途,死亡現(xiàn)場離奇詭異阁吝,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奸汇,“玉大人,你說我怎么就攤上這事甲馋。” “怎么了迄损?”我有些...
    開封第一講書人閱讀 156,780評論 0 346
  • 文/不壞的土叔 我叫張陵定躏,是天一觀的道長。 經(jīng)常有香客問我芹敌,道長痊远,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,388評論 1 283
  • 正文 為了忘掉前任氏捞,我火速辦了婚禮碧聪,結果婚禮上,老公的妹妹穿的比我還像新娘幌衣。我一直安慰自己矾削,他們只是感情好,可當我...
    茶點故事閱讀 65,430評論 5 384
  • 文/花漫 我一把揭開白布豁护。 她就那樣靜靜地躺著哼凯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪楚里。 梳的紋絲不亂的頭發(fā)上断部,一...
    開封第一講書人閱讀 49,764評論 1 290
  • 那天,我揣著相機與錄音班缎,去河邊找鬼蝴光。 笑死,一個胖子當著我的面吹牛达址,可吹牛的內容都是我干的蔑祟。 我是一名探鬼主播,決...
    沈念sama閱讀 38,907評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼沉唠,長吁一口氣:“原來是場噩夢啊……” “哼疆虚!你這毒婦竟也來了?” 一聲冷哼從身側響起满葛,我...
    開封第一講書人閱讀 37,679評論 0 266
  • 序言:老撾萬榮一對情侶失蹤径簿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后嘀韧,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體篇亭,經(jīng)...
    沈念sama閱讀 44,122評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,459評論 2 325
  • 正文 我和宋清朗相戀三年锄贷,在試婚紗的時候發(fā)現(xiàn)自己被綠了译蒂。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,605評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡谊却,死狀恐怖蹂随,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情因惭,我是刑警寧澤岳锁,帶...
    沈念sama閱讀 34,270評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站蹦魔,受9級特大地震影響激率,放射性物質發(fā)生泄漏。R本人自食惡果不足惜勿决,卻給世界環(huán)境...
    茶點故事閱讀 39,867評論 3 312
  • 文/蒙蒙 一乒躺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧低缩,春花似錦嘉冒、人聲如沸曹货。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,734評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽顶籽。三九已至,卻和暖如春银觅,著一層夾襖步出監(jiān)牢的瞬間礼饱,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,961評論 1 265
  • 我被黑心中介騙來泰國打工究驴, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留镊绪,地道東北人。 一個月前我還...
    沈念sama閱讀 46,297評論 2 360
  • 正文 我出身青樓洒忧,卻偏偏與公主長得像蝴韭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子熙侍,可洞房花燭夜當晚...
    茶點故事閱讀 43,472評論 2 348

推薦閱讀更多精彩內容

  • 前端開發(fā)面試題 面試題目: 根據(jù)你的等級和職位的變化万皿,入門級到專家級,廣度和深度都會有所增加核行。 題目類型: 理論知...
    怡寶丶閱讀 2,572評論 0 7
  • 讓我們開門見山:編寫優(yōu)秀的 CSS 代碼是件十分痛苦的事情芝雪。很多開發(fā)人員都不想做 CSS 開發(fā)减余。你讓我干什么都行,...
    前端小咖閱讀 694評論 0 1
  • 想想早年我就后怕 大森林里的狂奔 追逐和被追逐 饑餓以及一絲不掛 恐懼的眼神是放大的悲哀 惴惴不安讓一陣風 豎起汗...
    凹丁閱讀 252評論 0 0
  • 苦澀的咖啡惩系, 粉色的信箋位岔, 不知不覺中, 仿佛都戒了堡牡, 仿佛生活也抒抬, 漸漸地, 缺少了點什么晤柄? 翻開記事本擦剑, 拿起...
    半江紅閱讀 123評論 0 5
  • 這一周我加入了一個線上商學院的免費體驗課惠勒,主題是《6大職業(yè)發(fā)展妙招》,可是在一節(jié)關于天賦的課時爬坑,我的觀念被刷新了纠屋,...
    自然派媽媽閱讀 376評論 0 2