<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          組件庫設(shè)計實戰(zhàn) - 復(fù)雜組件設(shè)計

          共 10578字,需瀏覽 22分鐘

           ·

          2021-06-11 03:05

          關(guān)注并將「趣談前端」設(shè)為星標

          每早08:30按時推送技術(shù)干貨/優(yōu)秀開源/技術(shù)思維

          作者:誠身
          https://zhuanlan.zhihu.com/p/2903401

          一個成熟的組件庫通常都由數(shù)十個常用的 UI 組件構(gòu)成,這其中既有按鈕(Button),輸入框(Input)等基礎(chǔ)組件,也有表格(Table),日期選擇器(DatePicker),輪播(Carousel)等自成一體的復(fù)雜組件。

          這里我們提出一個組件復(fù)雜度的概念,一個組件復(fù)雜度的主要來源就是其自身的狀態(tài),即組件自身需要維護多少個不依賴于外部輸入的狀態(tài)。參考原先文章中提到過的木偶組件(dumb component)與智能組件(smart component),二者的區(qū)別就是是否需要在組件內(nèi)部維護不依賴于外部輸入的狀態(tài)。

          實戰(zhàn)案例 - 輪播組件
          在本篇文章中,我們將以輪播(Carousel)組件為例,一步一步還原如何實現(xiàn)一個交互流暢的輪播組件。

          最簡單的輪播組件
          拋去所有復(fù)雜的功能,輪播組件的實質(zhì),實際上就是在一個固定區(qū)域?qū)崿F(xiàn)不同元素之間的切換。在明確了這點后,我們就可以設(shè)計輪播組件的基礎(chǔ) DOM 結(jié)構(gòu)為:

          <Frame>
            <SlideList>
              <SlideItem />
              ...
              <SlideItem />
            </SlideList>
          </Frame>

          如下圖所示:

          Frame 即輪播組件的真實顯示區(qū)域,其寬高為內(nèi)部由使用者輸入的 SlideItem 決定。這里需要注意的一點是需要設(shè)置 Frameoverflow 屬性為 hidden,即隱藏超出其本身寬高的部分,每次只顯示一個 SlideItem

          SlideList 為輪播組件的軌道容器,改變其 translateX 的值即可實現(xiàn)在軌道的滑動,以顯示不同的輪播元素。

          SlideItem 是使用者輸入的輪播元素的一層抽象,內(nèi)部可以是 imgdivDOM 元素,并不影響輪播組件本身的邏輯。

          實現(xiàn)輪播元素之前的切換

          為了實現(xiàn)在不同 SlideItem 之間的切換,我們需要定義輪播組件的第一個內(nèi)部狀態(tài),即 currentIndex,即當前顯示輪播元素的 index 值。上文中我們提到了改變 SlideListtranslateX 是實現(xiàn)輪播元素切換的關(guān)鍵,所以這里我們需要將 currentIndexSlideListtranslateX 對應(yīng)起來,即:

          translateX = -(width) * currentIndex

          width 即為單個輪播元素的寬度,與 Frame 的寬度相同,所以我們可以在 componentDidMount 時拿到 Frame 的寬度并以此計算出軌道的總寬度。

          componentDidMount() {
            const width = get(this.container.getBoundingClientRect(), 'width');
          }

          render() {
            const rest = omit(this.props, Object.keys(defaultProps));
            const classes = classnames('ui-carousel'this.props.className);
            return (
              <div
                {...rest}
                className={classes}
                ref={(node) => { this.container = node; }}
              >
                {this.renderSildeList()}
                {this.renderDots()}
              </div>
            );
          }

          至此,我們只需要改變輪播組件中的 currentIndex,即可間接改變 SlideListtranslateX,以此實現(xiàn)輪播元素之間的切換。

          響應(yīng)用戶操作

          輪播作為一個常見的通用組件,在桌面和移動端都有著非常廣泛的應(yīng)用,這里我們先以移動端為例,來闡述如何響應(yīng)用戶操作。

          {map(children, (child, i) => (
            <div
              className="slideItem"
              role="presentation"
              key={i}
              style={{ width }}
              onTouchStart={this.handleTouchStart}
              onTouchMove={this.handleTouchMove}
              onTouchEnd={this.handleTouchEnd}
            >

              {child}
            </div>

          ))}

          在移動端,我們需要監(jiān)聽三個事件,分別響應(yīng)滑動開始,滑動中與滑動結(jié)束。其中滑動開始與滑動結(jié)束都是一次性事件,而滑動中則是持續(xù)性事件,以此我們可以確定在三個事件中我們分別需要確定哪些值。

          滑動開始

          • startPositionX:此次滑動的起始位置

          handleTouchStart = (e) => {
            const { x } = getPosition(e);
            this.setState({
              startPositionX: x,
            });
          }

          滑動中

          • moveDeltaX:此次滑動的實時距離

          • direction:此次滑動的實時方向

          • translateX:此次滑動中軌道的實時位置,用于渲染

          handleTouchMove = (e) => {
            const { width, currentIndex, startPositionX } = this.state;
            const { x } = getPosition(e);

            const deltaX = x - startPositionX;
            const direction = deltaX > 0 ? 'right' : 'left';
            this.setState({
              moveDeltaX: deltaX,
              direction,
              translateX: -(width * currentIndex) + deltaX,
            });
          }

          滑動結(jié)束

          • currentIndex:此次滑動結(jié)束后新的 currentIndex

          • endValue:此次滑動結(jié)束后軌道的 translateX

          handleTouchEnd = () => {
            this.handleSwipe();
          }

          handleSwipe = () => {
            const { children, speed } = this.props;
            const { width, currentIndex, direction, translateX } = this.state;
            const count = size(children);

            let newIndex;
            let endValue;
            if (direction === 'left') {
              newIndex = currentIndex !== count ? currentIndex + 1 : START_INDEX;
              endValue = -(width) * (currentIndex + 1);
            } else {
              newIndex = currentIndex !== START_INDEX ? currentIndex - 1 : count;
              endValue = -(width) * (currentIndex - 1);
            }

            const tweenQueue = this.getTweenQueue(translateX, endValue, speed);
            this.rafId = requestAnimationFrame(() => this.animation(tweenQueue, newIndex));
          }

          因為我們在滑動中會實時更新軌道的 translateX,我們的輪播組件便可以做到跟手的用戶體驗,即在單次滑動中,輪播元素會跟隨用戶的操作向左或向右滑動。

          實現(xiàn)順滑的切換動畫

          在實現(xiàn)了滑動中跟手的用戶體驗后,我們還需要在滑動結(jié)束后將顯示的輪播元素定位到新的 currentIndex。根據(jù)用戶的滑動方向,我們可以對當前的 currentIndex 進行 +1 或 -1 以得到新的 currentIndex。但在處理第一個元素向左滑動或最后一個元素向右滑動時,新的 currentIndex 需要更新為最后一個或第一個。

          這里的邏輯并不復(fù)雜,但卻帶來了一個非常難以解決的用戶體驗問題,那就是假設(shè)我們有 3 個輪播元素,每個輪播元素的寬度都為 300px,即顯示最后一個元素時,軌道的 translateX 為 -600px,在我們將最后一個元素向左滑動后,軌道的 translateX 將被重新定義為 0px,此時若我們使用原生的 CSS 動畫:

          transition: 1s ease-in-out;

          軌道將會在一秒內(nèi)從左向右滑動至第一個輪播元素,而這是反直覺的,因為用戶一個向左滑動的操作導(dǎo)致了一個向右的動畫,反之亦然。

          這個問題從上古時期就困擾著許多前端開發(fā)者,筆者也見過以下幾種解決問題的方法:

          • 將軌道寬度定義為無限長(幾百萬 px),無限次重復(fù)有限的輪播元素。這種解決方案顯然是一種 hack,并沒有從實質(zhì)上解決輪播組件的問題。

          • 只渲染三個輪播元素,即前一個,當前一個,下一個,每次滑動后同時更新三個元素。這種解決方案實現(xiàn)起來非常復(fù)雜,因為組件內(nèi)部要維護的狀態(tài)從一個 currentIndex 增加到了三個擁有各自狀態(tài)的 DOM 元素,且因為要不停的刪除和新增 DOm 節(jié)點導(dǎo)致性能不佳。

          這里讓我們再來思考一下滑動操作的本質(zhì)。除去第一和最后兩個元素,所有中間元素滑動后新的 translateX 的值都是固定的,即 -(width * currentIndex),這種情況下的動畫都可以輕松地完美實現(xiàn)。而在最后一個元素向左滑動時,因為軌道的 translateX 已經(jīng)到達了極限,面對這種情況我們?nèi)绾尾拍軐崿F(xiàn)順滑的切換動畫呢?

          這里我們選擇將最后一個及第一個元素分別拼接至軌道的頭尾,以保證在 DOM 結(jié)構(gòu)不需要改變的前提下實現(xiàn)順滑的切換動畫:

          這樣我們就統(tǒng)一了每次滑動結(jié)束后 endValue 的計算方式,即

          // left
          endValue = -(width) * (currentIndex + 1)

          // right
          endValue = -(width) * (currentIndex - 1)

          使用 requestAnimationFrame 實現(xiàn)高性能動畫

          requestAnimationFrame 是瀏覽器提供的一個專注于實現(xiàn)動畫的 API,感興趣的朋友可以再重溫一下《React Motion 緩動函數(shù)剖析》這篇專欄。

          所有的動畫本質(zhì)上都是一連串的時間軸上的值,具體到輪播場景下即:以用戶停止滑動時的值為起始值,以新 currentIndextranslateX 的值為結(jié)束值,在使用者設(shè)定的動畫時間(如0.5秒)內(nèi),依據(jù)使用者設(shè)定的緩動函數(shù),計算每一幀動畫時的 translateX 值并最終得到一個數(shù)組,以每秒 60 幀的速度更新在軌道的 style 屬性上。每更新一次,將消耗掉動畫值數(shù)組中的一個中間值,直到數(shù)組中所有的中間值被消耗完畢,動畫結(jié)束并觸發(fā)回調(diào)。

          具體代碼如下:

          const FPS = 60;
          const UPDATE_INTERVAL = 1000 / FPS;

          animation = (tweenQueue, newIndex) => {
            if (isEmpty(tweenQueue)) {
              this.handleOperationEnd(newIndex);
              return;
            }

            this.setState({
              translateX: head(tweenQueue),
            });
            tweenQueue.shift();
            this.rafId = requestAnimationFrame(() => this.animation(tweenQueue, newIndex));
          }

          getTweenQueue = (beginValue, endValue, speed) => {
            const tweenQueue = [];
            const updateTimes = speed / UPDATE_INTERVAL;
            for (let i = 0; i < updateTimes; i += 1) {
              tweenQueue.push(
                tweenFunctions.easeInOutQuad(UPDATE_INTERVAL * i, beginValue, endValue, speed),
              );
            }
            return tweenQueue;
          }

          在回調(diào)函數(shù)中,根據(jù)變動邏輯統(tǒng)一確定組件當前新的穩(wěn)定態(tài)值:

          handleOperationEnd = (newIndex) => {
            const { width } = this.state;

            this.setState({
              currentIndex: newIndex,
              translateX: -(width) * newIndex,
              startPositionX: 0,
              moveDeltaX: 0,
              dragging: false,
              direction: null,
            });
          }

          完成后的輪播組件效果如下圖:

          優(yōu)雅地處理特殊情況

          • 處理用戶誤觸:在移動端,用戶經(jīng)常會誤觸到輪播組件,即有時手不小心滑過或點擊時也會觸發(fā) onTouch 類事件。對此我們可以采取對滑動距離添加閾值的方式來避免用戶誤觸,閾值可以是輪播元素寬度的 10% 或其他合理值,在每次滑動距離超過閾值時,才會觸發(fā)輪播組件后續(xù)的滑動。

          • 桌面端適配:對于桌面端而言,輪播組件所需要響應(yīng)的事件名稱與移動端是完全不同的,但又可以相對應(yīng)地匹配起來。這里還需要注意的是,我們需要為輪播組件添加一個 dragging 的狀態(tài)來區(qū)分移動端與桌面端,從而安全地復(fù)用 handler 部分的代碼。

          // mobile
          onTouchStart={this.handleTouchStart}
          onTouchMove={this.handleTouchMove}
          onTouchEnd={this.handleTouchEnd}
          // desktop
          onMouseDown={this.handleMouseDown}
          onMouseMove={this.handleMouseMove}
          onMouseUp={this.handleMouseUp}
          onMouseLeave={this.handleMouseLeave}
          onMouseOver={this.handleMouseOver}
          onMouseOut={this.handleMouseOut}
          onFocus={this.handleMouseOver}
          onBlur={this.handleMouseOut}

          handleMouseDown = (evt) => {
            evt.preventDefault();
            this.setState({
              dragging: true,
            });
            this.handleTouchStart(evt);
          }

          handleMouseMove = (evt) => {
            if (!this.state.dragging) {
              return;
            }
            this.handleTouchMove(evt);
          }

          handleMouseUp = () => {
            if (!this.state.dragging) {
              return;
            }
            this.handleTouchEnd();
          }

          handleMouseLeave = () => {
            if (!this.state.dragging) {
              return;
            }
            this.handleTouchEnd();
          }

          handleMouseOver = () => {
            if (this.props.autoPlay) {
              clearInterval(this.autoPlayTimer);
            }
          }

          handleMouseOut = () => {
            if (this.props.autoPlay) {
              this.autoPlay();
            }
          }

          小結(jié)

          至此我們就實現(xiàn)了一個只有 tween-functions 一個第三方依賴的輪播組件,打包后大小不過 2KB,完整的源碼大家可以參考這里 carousel/index.js。

          除了節(jié)省的代碼體積,更讓我們欣喜的還是徹底弄清楚了輪播組件的實現(xiàn)模式以及如何使用 requestAnimationFrame 配合 setState 來在 react 中完成一組動畫。

          感想

          大家應(yīng)該都看過上面這幅漫畫,有趣之余也蘊含著一個樸素卻深刻的道理,那就是在解決一個復(fù)雜問題時,最重要的是思路,但僅僅有思路也仍是遠遠不夠的,還需要具體的執(zhí)行方案。這個具體的執(zhí)行方案,必須是連續(xù)的,其中不可以欠缺任何一環(huán),不可以有任何思路或執(zhí)行上的跳躍。所以解決任何復(fù)雜問題都沒有銀彈也沒有捷徑,我們必須把它弄清楚,搞明白,然后才能真正地解決它。

          ?? 看完三件事

          如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:

          • 點個【在看】,或者分享轉(zhuǎn)發(fā),讓更多的人也能看到這篇內(nèi)容

          • 關(guān)注公眾號【趣談前端】,不定期分享 前端工程化 可視化 / 低代碼 等技術(shù)文章。



          10款2021年國外頂尖的lowcode開發(fā)平臺

          2個小時, 從學(xué)到做, 我用Dooring制作了3個電商H5

          canvas圖像識取技術(shù)以及智能化設(shè)計的思考

          瀏覽 56
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天草人人草 | 欧美成人娱乐视频免费 | 一道本免费无码在线视频 | 18禁www网站 | 操逼在线 |