摘要:很久沒(méi)上掘金發(fā)現(xiàn)草稿箱里存了好幾篇沒(méi)發(fā)的文章最近梳理下發(fā)出來(lái)往期面試官系列如何實(shí)現(xiàn)深克隆面試官系列的實(shí)現(xiàn)面試官系列前端路由的實(shí)現(xiàn)面試官系列基于數(shù)據(jù)劫持的雙向綁定優(yōu)勢(shì)所在面試官系列你為什么使用前端框架前言設(shè)計(jì)前端組件是最能考驗(yàn)開(kāi)發(fā)者基本功的測(cè)
往期很久沒(méi)上掘金,發(fā)現(xiàn)草稿箱里存了好幾篇沒(méi)發(fā)的文章,最近梳理下發(fā)出來(lái)
面試官系列(1): 如何實(shí)現(xiàn)深克隆
面試官系列(2): Event Bus的實(shí)現(xiàn)
面試官系列(3): 前端路由的實(shí)現(xiàn)
面試官系列(4): 基于Proxy 數(shù)據(jù)劫持的雙向綁定優(yōu)勢(shì)所在
面試官系列(5): 你為什么使用前端框架
設(shè)計(jì)前端組件是最能考驗(yàn)開(kāi)發(fā)者基本功的測(cè)試之一,因?yàn)檎{(diào)用Material design、Antd、iView 等現(xiàn)成組件庫(kù)的 API 每個(gè)人都可以做到,但是很多人并不知道很多常用組件的設(shè)計(jì)原理。
能否設(shè)計(jì)出通用前端組件也是區(qū)分前端工程師和前端api調(diào)用師的標(biāo)準(zhǔn)之一,那么應(yīng)該如何設(shè)計(jì)出一個(gè)通用組件呢");
下文中提到的組件庫(kù)通常是指單個(gè)組件,而非集合的概念,集合概念的組件庫(kù)是 Antd iView這種,我們所說(shuō)的組件庫(kù)是指集合中的單個(gè)組件,集合性質(zhì)的組件庫(kù)需要考慮的要更多.
前端組件庫(kù)的設(shè)計(jì)原則
組件庫(kù)的技術(shù)選型
如何快速啟動(dòng)一個(gè)組件庫(kù)項(xiàng)目
如何設(shè)計(jì)一個(gè)輪播圖組件
我們?cè)趯W(xué)習(xí)設(shè)計(jì)模式的時(shí)候會(huì)遇到很多種設(shè)計(jì)原則,其中一個(gè)設(shè)計(jì)原則就是單一職責(zé)原則,在組件庫(kù)的開(kāi)發(fā)中同樣適用,我們?cè)瓌t上一個(gè)組件只專注一件事情,單一職責(zé)的組件的好處很明顯,由于職責(zé)單一就可以最大可能性地復(fù)用組件,但是這也帶來(lái)一個(gè)問(wèn)題,過(guò)度單一職責(zé)的組件也可能會(huì)導(dǎo)致過(guò)度抽象,造成組件庫(kù)的碎片化。
舉個(gè)例子,一個(gè)自動(dòng)完成組件(AutoComplete),他其實(shí)是由 Input 組件和 Select 組件組合而成的,因此我們完全可以復(fù)用之前的相關(guān)組件,就比如 Antd 的AutoComplete組件中就復(fù)用了Select組件,同時(shí)Calendar、 Form 等等一系列組件都復(fù)用了 Select 組件,那么Select 的細(xì)粒度就是合適的,因?yàn)?Select 保持的這種細(xì)粒度很容易被復(fù)用.
那么還有一個(gè)例子,一個(gè)徽章數(shù)組件(Badge),它的右上角會(huì)有紅點(diǎn)提示,可能是數(shù)字也可能是 icon,他的職責(zé)當(dāng)然也很單一,這個(gè)紅點(diǎn)提示也理所當(dāng)然也可以被多帶帶抽象為一個(gè)獨(dú)立組件,但是我們通常不會(huì)將他作為獨(dú)立組件,因?yàn)樵谄渌麍?chǎng)景中這個(gè)組件是無(wú)法被復(fù)用的,因?yàn)闆](méi)有類似的場(chǎng)景再需要小紅點(diǎn)這個(gè)小組件了,所以作為獨(dú)立組件就屬于細(xì)粒度過(guò)小,因此我們往往將它作為 Badge 的內(nèi)部組件,比如在 Antd 中它以ScrollNumber的名稱作為Badge的內(nèi)部組件存在。
所以,所謂的單一職責(zé)組件要建立在可復(fù)用的基礎(chǔ)上,對(duì)于不可復(fù)用的單一職責(zé)組件我們僅僅作為獨(dú)立組件的內(nèi)部組件即可。
我們要設(shè)計(jì)的本身就是通用組件庫(kù),不同于我們常見(jiàn)的業(yè)務(wù)組件,通用組件是與業(yè)務(wù)解耦但是又服務(wù)于業(yè)務(wù)開(kāi)發(fā)的,那么問(wèn)題來(lái)了,如何保證組件的通用性,通用性高一定是好事嗎");
比如我們?cè)O(shè)計(jì)一個(gè)選擇器(Select)組件,通常我們會(huì)設(shè)計(jì)成這樣
這是一個(gè)我們最常見(jiàn)也最常用的選擇器,但是問(wèn)題是其通用性大打折扣
當(dāng)我們有一個(gè)需求是長(zhǎng)這樣的時(shí)候,我們之前的選擇器組件就不符合要求了,因?yàn)檫@個(gè) Select 組件的最下部需要有一個(gè)可拓展的條目的按鈕
這個(gè)時(shí)候我們難道要重新修改之前的選擇器組件,甚至再造一個(gè)符合要求的選擇器組件嗎");
Antd 的 Select 組件預(yù)留了dropdownRender來(lái)進(jìn)行自定義渲染,其依賴的 rc-select組件中的代碼如下
Antd 依賴了大量以rc-開(kāi)頭的底層組件,這些組件被react-component團(tuán)隊(duì)(同時(shí)也就是Antd 團(tuán)隊(duì))維護(hù),其主要實(shí)現(xiàn)組件的底層邏輯,Antd 則是在此基礎(chǔ)上添加Ant Design設(shè)計(jì)語(yǔ)言而實(shí)現(xiàn)的
當(dāng)然類似的設(shè)計(jì)還有很多,通用性設(shè)計(jì)其實(shí)是一定意義上放棄對(duì) DOM 的掌控,而將 DOM 結(jié)構(gòu)的決定權(quán)轉(zhuǎn)移給開(kāi)發(fā)者,dropdownRender其實(shí)就是放棄對(duì) Select 下拉菜單中條目的掌控,Antd 的 Select 組件其實(shí)還有一個(gè)沒(méi)有在文檔中體現(xiàn)的方法getInputElement應(yīng)該是對(duì) Input 組件的自定義方法,Antd整個(gè) Select 的組件設(shè)計(jì)非常復(fù)雜,基本將所有的 DOM 結(jié)構(gòu)控制權(quán)全部暴露給了開(kāi)發(fā)者,其本身只負(fù)責(zé)底層邏輯和最基本的 DOM 結(jié)構(gòu).
這是 Antd 所依賴的 re-select 最終 jsx 的結(jié)構(gòu),其 DOM 結(jié)構(gòu)很簡(jiǎn)單,但是暴露了大量自定義渲染的接口給開(kāi)發(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>
);
那么這么多需要自定義的地方,這個(gè) Select 組件豈不是很難用");
組件的形態(tài)(DOM結(jié)構(gòu))永遠(yuǎn)是千變?nèi)f化的,但是其行為(邏輯)是固定的,因此通用組件的秘訣之一就是將 DOM 結(jié)構(gòu)的控制權(quán)交給開(kāi)發(fā)者,組件只負(fù)責(zé)行為和最基本的 DOM 結(jié)構(gòu)
由于CSS 本身的眾多缺陷,如書(shū)寫(xiě)繁瑣(不支持嵌套)、樣式易沖突(沒(méi)有作用域概念)、缺少變量(不便于一鍵換主題)等不一而足。為了解決這些問(wèn)題,社區(qū)里的解決方案也是出了一茬又一茬,從最早的 CSS prepocessor(SASS、LESS、Stylus)到后來(lái)的后起之秀 PostCSS,再到 CSS Modules、Styled-Components 等。
Antd 選擇了 less 作為 css 的預(yù)處理方案,Bootstrap 選擇了 Scss,這兩種方案孰優(yōu)孰劣已經(jīng)爭(zhēng)論了很多年了:
SCSS和LESS相比有什么優(yōu)勢(shì)?
但是不管是哪種方案都有一個(gè)很煩人的點(diǎn),就是需要額外引入 css,比如 Antd 需要這樣顯示引入:
import Button from "antd/lib/button";
import "antd/lib/button/style";
為了解決這種尷尬的情況,Antd 用 Babel 插件將這種情況 Hack 掉了
而material-ui并不存在這種情況,他不需要顯示引入 css,這個(gè)最流行的 React 前端組件庫(kù)里面只有 js 和 ts 兩種代碼,并不存在 css 相關(guān)的代碼,為什么呢");
他們用 jss 作為css-in-js 的解決方案,jsx 的引入已經(jīng)將 js 和 html 耦合,css-in-js將 css 也耦合進(jìn)去,此時(shí)組件便不需要顯示引入 css,而是直接引用 js 即可.
這不是退化到史前前端那種寫(xiě)內(nèi)聯(lián)樣式的時(shí)代了嗎");
并不是,史前前端的內(nèi)聯(lián)樣式是整個(gè)項(xiàng)目耦合的狀態(tài),當(dāng)然要被拋棄到歷史的垃圾堆中,后來(lái)的樣式和邏輯分離,實(shí)際上是以頁(yè)面為維度將 js css html 解耦的過(guò)程,如今的時(shí)代是組件化的時(shí)代了,jsx 已經(jīng)將 js 和 html 框定到一個(gè)組件中,css 依然處于分離狀態(tài),這就導(dǎo)致了每次引用組件卻還需要顯示引入 css,css-in-js 正式徹底組件化的解決方案.
當(dāng)然,我個(gè)人目前在用 styled-components,其優(yōu)點(diǎn)引用如下:
首先,styled-components 所有語(yǔ)法都是標(biāo)準(zhǔn) css 語(yǔ)法,同時(shí)支持 scss 嵌套等常用語(yǔ)法,覆蓋了所有 css 場(chǎng)景。
在樣式復(fù)寫(xiě)場(chǎng)景下,styled-components 支持在任何地方注入全局 css,就像寫(xiě)普通 css 一樣
styled-components 支持自定義 className,兩種方式,一種是用 babel 插件, 另一種方式是使用 styled.div.withConfig({ componentId: "prefix-button-container" }) 相當(dāng)于添加 className="prefix-button-container"
className 語(yǔ)義化更輕松,這也是 class 起名的初衷
更適合組件庫(kù)使用,直接引用 import "module" 即可,否則你有三條路可以走:像 antd 一樣,多帶帶引用 css,你需要給 node_modules 添加 css-loader;組件內(nèi)部直接 import css 文件,如果任何業(yè)務(wù)項(xiàng)目沒(méi)有 css-loader 就會(huì)報(bào)錯(cuò);組件使用 scss 引用,所有業(yè)務(wù)項(xiàng)目都要配置一份 scss-loader 給 node_modules;這三種對(duì)組件庫(kù)來(lái)說(shuō),都沒(méi)有直接引用來(lái)的友好
當(dāng)你寫(xiě)一套組件庫(kù),需要多帶帶發(fā)包,又有統(tǒng)一樣式的配置文件需求,如果這個(gè)配置文件是 js 的,所有組件直接引用,對(duì)外完全不用關(guān)注。否則,如果是 scss 配置文件,擺在面前還是三條路:每個(gè)組件多帶帶引用 scss 文件,需要每個(gè)業(yè)務(wù)項(xiàng)目給 node_modules 添加 scss-loader(如果業(yè)務(wù)用了 less,還要裝一份 scss 是不);或者業(yè)務(wù)方只要使用了你的組件庫(kù),就要在入口文件引用你的 scss 文件,比如你的組件叫 button,scss 可能叫 common-css,別人聽(tīng)都沒(méi)聽(tīng)過(guò),還要查文檔;或者業(yè)務(wù)方在 webpack 配置中多帶帶引用你的 common-css,這也不科學(xué),如果用了3個(gè)組件庫(kù),天天改 webpack 配置也很不方便。
當(dāng) css 設(shè)置了一半樣式,另一半真的需要 js 動(dòng)態(tài)傳入,你不得不 css + css-in-js 混合使用,項(xiàng)目久了,維護(hù)的時(shí)候發(fā)現(xiàn)某些 css-in-js 不變了,可以固化在 css 里,css 里固定的值又因?yàn)樾氯デ笞兊每勺兞?,你又得拿出?lái)放在 css-in-js 里,實(shí)踐過(guò)就知道有多么煩心。
選 Typescript ,因?yàn)榫抻泊蠓ê?..
可以看看知乎問(wèn)題下我的回答你為什么不用 Typescript
或者看此文TypeScript體系調(diào)研報(bào)告
組件的具體實(shí)現(xiàn)部分當(dāng)然是組件庫(kù)的核心,但是在現(xiàn)代前端庫(kù)中其他部分也必不可少,我們需要一堆工具來(lái)輔助我們開(kāi)發(fā),例如編譯工具、代碼檢測(cè)工具、打包工具等等。
市面上打包工具數(shù)不勝數(shù),最火爆的當(dāng)然是需要配置工程師專門配置的webpack,但是在類庫(kù)開(kāi)發(fā)領(lǐng)域它有一個(gè)強(qiáng)大的對(duì)手就是 rollup。
現(xiàn)代市面上主流的庫(kù)基本都選擇了 rollup 作為打包工具,包括Angular React 和 Vue, 作為基礎(chǔ)類庫(kù)的打包工具 rollup 的優(yōu)勢(shì)如下:
Tree Shaking: 自動(dòng)移除未使用的代碼, 輸出更小的文件
Scope Hoisting: 所有模塊構(gòu)建在一個(gè)函數(shù)內(nèi), 執(zhí)行效率更高
Config 文件支持通過(guò) ESM 模塊格式書(shū)寫(xiě) 可以一次輸出多種格式:
模塊規(guī)范: IIFE, AMD, CJS, UMD, ESM Development 與 production 版本: .js, .min.js
雖然上面部分功能已經(jīng)被 webpack 實(shí)現(xiàn)了,但是 rollup 明顯引入得更早,而Scope Hoisting更是殺手锏,由于 webpack 不得不在打包代碼中構(gòu)建模塊系統(tǒng)來(lái)適應(yīng) app 開(kāi)發(fā)(模塊系統(tǒng)對(duì)于單一類庫(kù)用處很小),Scope Hoisting將模塊構(gòu)建在一個(gè)函數(shù)內(nèi)的做法更適合類庫(kù)的打包.
由于 JavaScript 各種詭異的特性和大型前端項(xiàng)目的出現(xiàn),代碼檢測(cè)工具已經(jīng)是前端開(kāi)發(fā)者的標(biāo)配了,Douglas Crockford最早于2002創(chuàng)造出了 JSLint,但是其無(wú)法拓展,具有極強(qiáng)的Douglas Crockford個(gè)人色彩,Anton Kovalyov由于無(wú)法忍受 JSLint 無(wú)法拓展的行為在2011年發(fā)布了可拓展的JSHint,一時(shí)之間JSHint成為了前端代碼檢測(cè)的流行解決方案.
隨后的2013年,Nicholas C. Zakas鑒于JSHint拓展的靈活度不夠的問(wèn)題開(kāi)發(fā)了全新的基于 AST 的 Lint 工具 ESLint,并隨著 ES6的流行統(tǒng)治了前端界,ESLint 基于Esprima進(jìn)行 JavaScript 解析的特性極易拓展,JSHint 在很長(zhǎng)一段時(shí)間無(wú)法支持 ES6語(yǔ)法導(dǎo)致被 ESLint 超越.
但是在 Typescript 領(lǐng)域 ESLint 卻處于弱勢(shì)地位,TSLint 的出現(xiàn)要比 ESLint 正式支持 Typescript 早很多,目前 TSLint 似乎是 TS 的事實(shí)上的代碼檢測(cè)工具.
注: 文章成文較早,我也沒(méi)想到前陣子 TS 官方欽點(diǎn)了 ESLint,TSLint 失寵了,面向未來(lái)的官方標(biāo)配的代碼檢測(cè)工具肯定是 ESLint 了,但是 TSLint 目前依然被大量使用,現(xiàn)在仍然可以放心使用
代碼檢測(cè)工具是一方面,代碼檢測(cè)風(fēng)格也需要我們做選擇,市面上最流行的代碼檢測(cè)風(fēng)格應(yīng)該是 Airbnb 出品的eslint-config-airbnb,其最大的特點(diǎn)就是極其嚴(yán)格,沒(méi)有給開(kāi)發(fā)者任何選擇的余地,當(dāng)然在大型前端項(xiàng)目的開(kāi)發(fā)中這種嚴(yán)格的代碼風(fēng)格是有利于協(xié)作的,但是作為一個(gè)類庫(kù)的代碼檢測(cè)工具而言并不適合,所以我們選擇了eslint-config-standard這種相對(duì)更為寬松的代碼檢測(cè)風(fēng)格.
以下兩種 commit 哪個(gè)更嚴(yán)謹(jǐn)且易于維護(hù)");
最開(kāi)始使用 commit 的時(shí)候我也經(jīng)常犯下圖的錯(cuò)誤,直到看到很多明星類庫(kù)的 commit 才意識(shí)到自己的錯(cuò)誤,寫(xiě)好 commit message 不僅有助于他人 review, 還可以有效的輸出 CHANGELOG, 對(duì)項(xiàng)目的管理實(shí)際至關(guān)重要.
目前流行的方案是 Angular 團(tuán)隊(duì)的規(guī)范,其關(guān)于 head 的大致規(guī)范如下:
type: commit 的類型
feat: 新特性
fix: 修改問(wèn)題
refactor: 代碼重構(gòu)
docs: 文檔修改
style: 代碼格式修改, 注意不是 css 修改
test: 測(cè)試用例修改
chore: 其他修改, 比如構(gòu)建流程, 依賴管理.
scope: commit 影響的范圍, 比如: route, component, utils, build...
subject: commit 的概述, 建議符合 50/72 formatting
body: commit 具體修改內(nèi)容, 可以分為多行, 建議符合 50/72 formatting
footer: 一些備注, 通常是 BREAKING CHANGE 或修復(fù)的 bug 的鏈接.
當(dāng)然規(guī)范人們不一定會(huì)遵守,我最初知道此類規(guī)范的時(shí)候也并沒(méi)有嚴(yán)格遵循,因?yàn)槿丝倳?huì)偷懶,直到用commitizen將此規(guī)范集成到工具流中,每個(gè) commit 就不得不遵循規(guī)范了.
我具體參考了這篇文章: 優(yōu)雅的提交你的 Git Commit Message
業(yè)務(wù)開(kāi)發(fā)中由于前端需求變動(dòng)頻繁的特性,導(dǎo)致前端對(duì)測(cè)試的要求并沒(méi)有后端那么高,后端業(yè)務(wù)邏輯一旦定型變動(dòng)很少,比較適合測(cè)試.
但是基礎(chǔ)類庫(kù)作為被反復(fù)依賴的模塊和較為穩(wěn)定的需求是必須做測(cè)試的,前端測(cè)試庫(kù)也可謂是種類繁多了,經(jīng)過(guò)比對(duì)之后我還是選擇了目前最流行也是被三大框架同時(shí)選擇了的 Jest 作為測(cè)試工具,其優(yōu)點(diǎn)很明顯:
開(kāi)箱即用,內(nèi)置斷言、測(cè)試覆蓋率工具,如果你用 MoCha 那可得自己手動(dòng)配置 n 多了
快照功能,Jest 可以利用其特有的快照測(cè)試功能,通過(guò)比對(duì) UI 代碼生成的快照文件
速度優(yōu)勢(shì),Jest 的測(cè)試用例是并行執(zhí)行的,而且只執(zhí)行發(fā)生改變的文件所對(duì)應(yīng)的測(cè)試,提升了測(cè)試速度
當(dāng)然以上是主要工具的選擇,還有一些比如:
代碼美化工具 prettier,解放人肉美化,同時(shí)利于不同人協(xié)作的風(fēng)格一致
持續(xù)集成工具 travis-ci,解放人肉測(cè)試 lint,利于保證每次 push 的可靠程度
那么以上這么多配置難道要我們每次都自己寫(xiě)嗎");
我們?cè)诮?APP 項(xiàng)目時(shí)通常會(huì)用到框架官方提供的腳手架,比如 React 的 create-react-app,Angular 的 Angular-Cli 等等,那么能不能有一個(gè)專門用于組件開(kāi)發(fā)的快速啟動(dòng)的腳手架呢");
有的,我最近開(kāi)發(fā)了一款快速啟動(dòng)組件庫(kù)開(kāi)發(fā)的命令行工具--create-component
利用
create-component init
來(lái)快速啟動(dòng)項(xiàng)目,我們提供了豐富的可選配置,只要你做好技術(shù)選型后,根據(jù)提示去選擇配置即可,create-component 會(huì)自動(dòng)根據(jù)配置生成腳手架,其靈感就來(lái)源于 vue-cli和 Angular-cli.
說(shuō)了很多理論,那么實(shí)戰(zhàn)如何呢");
輪播圖(Carousel),在 Antd 中被稱為走馬燈,可能是前端開(kāi)發(fā)者最常見(jiàn)的組件之一了,不管是在 PC 端還是在移動(dòng)端我們總能見(jiàn)到他的身影.
那么我們通常是如何使用輪播圖的呢");
<Carousel>
<div><h3>1h3>div>
<div><h3>2h3>div>
<div><h3>3h3>div>
<div><h3>4h3>div>
Carousel>
問(wèn)題是我們?cè)?b>Carousel中放入了四組div為什么一次只顯示一組呢");
圖中被紅框圈住的為可視區(qū)域,可視區(qū)域的位置是固定的,我們只需要移動(dòng)后面div的位置就可以做到1 2 3 4四個(gè)子組件輪播的效果,那么子組件2目前在可視區(qū)域是可以被看到的,1 3 4應(yīng)該被隱藏,這就需要我們?cè)O(shè)置overflow 屬性為 hidden來(lái)隱藏非可視區(qū)域的子組件.
復(fù)制查看動(dòng)圖: images2015.cnblogs.com/blog/979044…
因此就比較明顯了,我們?cè)O(shè)計(jì)一個(gè)可視窗口組件Frame,然后將四個(gè) div共同放入幻燈片組合組件SlideList中,并用SlideItem分別將 div包裹起來(lái),實(shí)際代碼應(yīng)該是這樣的:
<Frame>
<SlideList>
<SlideItem>
<div><h3>1h3>div>
SlideItem>
<SlideItem>
<div><h3>2h3>div>
SlideItem>
<SlideItem>
<div><h3>3h3>div>
SlideItem>
<SlideItem>
<div><h3>4h3>div>
SlideItem>
SlideList>
Frame>
我們不斷利用translateX來(lái)改變SlideList的位置來(lái)達(dá)到輪播效果,如下圖所示,每次輪播的觸發(fā)都是通過(guò)改變transform: translateX()來(lái)操作的
搞清楚基本原理那么實(shí)現(xiàn)起來(lái)相對(duì)容易了,我們以移動(dòng)端的實(shí)現(xiàn)為例,來(lái)實(shí)現(xiàn)一個(gè)基礎(chǔ)的移動(dòng)端輪播圖.
首先我們要確定可視窗口的寬度,因?yàn)槲覀冃枰@個(gè)寬度來(lái)計(jì)算出SlideList的長(zhǎng)度(SlideList的長(zhǎng)度通常是可視窗口的倍數(shù),比如要放三張圖片,那么SlideList應(yīng)該為可視窗口的至少3倍),不然我們無(wú)法通過(guò)translateX來(lái)移動(dòng)它.
我們通過(guò)getBoundingClientRect來(lái)獲取可視區(qū)域真實(shí)的長(zhǎng)度,SlideList的長(zhǎng)度那么為:
slideListWidth = (len + 2) * width(len 為傳入子組件的數(shù)量,width 為可視區(qū)域?qū)挾?
至于為什么要+2后面會(huì)提到.
/**
* 設(shè)置輪播區(qū)域尺寸
* @param x
*/
private setSize(x");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 ");0,
})
}
獲取到了總長(zhǎng)度之后如何實(shí)現(xiàn)輪播呢");onTouchStart onTouchMove onTouchEnd.
onTouchStart顧名思義是在手指觸摸到屏幕時(shí)觸發(fā)的事件,在這個(gè)事件里我們只需要記錄下手指觸摸屏幕的橫軸坐標(biāo) x 即可,因?yàn)槲覀儠?huì)通過(guò)其橫向滑動(dòng)的距離大小來(lái)判斷是否觸發(fā)輪播
/**
* 處理觸摸起始時(shí)的事件
*
* @private
* @param {React.TouchEvent} e
* @memberof Carousel
*/
private onTouchStart(e: React.TouchEvent) {
clearInterval(this.autoPlayTimer)
// 獲取起始的橫軸坐標(biāo)
const { x } = getPosition(e)
this.setSize(x)
this.setState({
startPositionX: x,
})
}
onTouchMove顧名思義是處于滑動(dòng)狀態(tài)下的事件,此事件在onTouchStart觸發(fā)后,onTouchEnd觸發(fā)前,在這個(gè)事件中我們主要做兩件事,一件事是判斷滑動(dòng)方向,因?yàn)橛脩艨赡芟蜃蠡蛘呦蛴一瑒?dòng),另一件事是讓輪播圖跟隨手指移動(dòng),這是必要的用戶反饋.
/**
* 當(dāng)觸摸滑動(dòng)時(shí)處理事件
*
* @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
// 判斷滑動(dòng)方向
const direction = deltaX > 0 ");"right" : "left"
this.setState({
direction,
moveDeltaX: deltaX,
// 改變translateX來(lái)達(dá)到輪播組件跟隨手指移動(dòng)的效果
translateX: -(slideItemWidth * currentIndex) + deltaX,
})
}
onTouchEnd顧名思義是滑動(dòng)完畢時(shí)觸發(fā)的事件,在此事件中我們主要做一個(gè)件事情,就是判斷是否觸發(fā)輪播,我們會(huì)設(shè)置一個(gè)閾值threshold,當(dāng)滑動(dòng)距離超過(guò)這個(gè)閾值時(shí)才會(huì)觸發(fā)輪播,畢竟沒(méi)有閾值的話用戶稍微觸碰輪播圖就造成輪播,誤操作會(huì)造成很差的用戶體驗(yàn).
/**
* 滑動(dòng)結(jié)束處理的事件
*
* @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ā)那么進(jìn)行輪播操作
this.handleSwipe(direction!)
} else {
// 輪播不觸發(fā),那么輪播圖回到原位
this.handleMisoperation()
}
}
我們常見(jiàn)的輪播圖肯定不是生硬的切換,一般在輪播中會(huì)有一個(gè)漸變或者緩動(dòng)的動(dòng)畫(huà),這就需要我們加入動(dòng)畫(huà)效果.
我們制作動(dòng)畫(huà)通常有兩個(gè)選擇,一個(gè)是用 css3自帶的動(dòng)畫(huà)效果,另一個(gè)是用瀏覽器提供的requestAnimationFrame API
孰優(yōu)孰劣");requestAnimationFrame 靈活性更高,能實(shí)現(xiàn) css3實(shí)現(xiàn)不了的動(dòng)畫(huà),比如眾多緩動(dòng)動(dòng)畫(huà) css3都束手無(wú)策,因此我們毫無(wú)疑問(wèn)地選擇了requestAnimationFrame.
雙方對(duì)比請(qǐng)看張?chǎng)涡翊笊竦腃SS3動(dòng)畫(huà)那么強(qiáng),requestAnimationFrame還有毛線用?
想用requestAnimationFrame實(shí)現(xiàn)緩動(dòng)效果就需要特定的緩動(dòng)函數(shù),下面就是典型的緩動(dòng)函數(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;
}
}
緩動(dòng)函數(shù)接收四個(gè)參數(shù),分別是:
t: 時(shí)間
b:初始位置
_c:結(jié)束的位置
d:速度
通過(guò)這個(gè)函數(shù)我們能算出每一幀輪播圖所在的位置, 如下:
在獲取每一幀對(duì)應(yīng)的位置后,我們需要用requestAnimationFrame不斷遞歸調(diào)用依次移動(dòng)位置,我們不斷調(diào)用animation函數(shù)是其觸發(fā)函數(shù)體內(nèi)的this.setState({ translateX: tweenQueue[0], })來(lái)達(dá)到移動(dòng)輪播圖位置的目的,此時(shí)將這數(shù)組內(nèi)的30個(gè)位置依次快速執(zhí)行就是一個(gè)緩動(dòng)動(dòng)畫(huà)效果.
/**
* 遞歸調(diào)用,根據(jù)軌跡運(yùn)動(dòng)
*
* @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)了一個(gè)問(wèn)題,當(dāng)我們移動(dòng)輪播圖到最后的時(shí)候,動(dòng)畫(huà)出現(xiàn)了問(wèn)題,當(dāng)我們向左滑動(dòng)最后一個(gè)輪播圖div4時(shí),這種情況下應(yīng)該是圖片向左滑動(dòng),然后第一張輪播圖div1進(jìn)入可視區(qū)域,但是反常的是圖片快速向右滑動(dòng)div1出現(xiàn)在可是區(qū)域...
因?yàn)槲覀兇藭r(shí)將位置4設(shè)置為了位置1,這樣才能達(dá)到不斷循環(huán)的目的,但是也造成了這個(gè)副作用,圖片行為與用戶行為產(chǎn)生了相悖的情況(用戶向左劃動(dòng),圖片向右走).
目前業(yè)界的普遍做法是將圖片首尾相連,例如圖片1前面連接一個(gè)圖片4,圖片4后跟著一個(gè)圖片1,這就是為什么之前計(jì)算長(zhǎng)度時(shí)要+2
slideListWidth = (len + 2) * width(len 為傳入子組件的數(shù)量,width 為可視區(qū)域?qū)挾?
當(dāng)我們移動(dòng)圖片4時(shí)就不會(huì)出現(xiàn)上述向左滑圖片卻向右滑的情況,因?yàn)檎鎸?shí)情況是:
圖片4 -- 滑動(dòng)為 -> 偽圖片1 也就是位置 5 變成了位置 6
當(dāng)動(dòng)畫(huà)結(jié)束之后,我們迅速把偽圖片1的位置設(shè)置為真圖片1,這其實(shí)是個(gè)障眼法,也就是說(shuō)動(dòng)畫(huà)執(zhí)行過(guò)程中實(shí)際上是圖片4到偽圖片1的過(guò)程,當(dāng)結(jié)束后我們偷偷把偽圖片1換成真圖片1,因?yàn)閮蓚€(gè)圖一模一樣,所以這個(gè)轉(zhuǎn)換的過(guò)程用戶根本看不出來(lái)...
如此一來(lái)我們就可以實(shí)現(xiàn)無(wú)縫切換的輪播圖了
我們實(shí)現(xiàn)了輪播圖的基本功能,但是其通用性依然存在缺陷:
提示點(diǎn)的自定義: 我的實(shí)現(xiàn)是一個(gè)小點(diǎn),而 antd 是用的條,這個(gè)地方完全可以將 dom 結(jié)構(gòu)的決定權(quán)交給開(kāi)發(fā)者.
方向的自定義: 本輪播圖只有水平方向的實(shí)現(xiàn),其實(shí)也可以有縱向輪播
多張輪播:除了單張輪播也可以多張輪播
以上都是可以對(duì)輪播圖進(jìn)行拓展的方向,相關(guān)的還有性能優(yōu)化方面
我們的具體代碼中有一個(gè)相關(guān)實(shí)現(xiàn),我們的輪播圖其實(shí)是有自動(dòng)輪播功能的,但是很多時(shí)候頁(yè)面并不在用戶的可視頁(yè)面中,我們可以根據(jù)是否頁(yè)面被隱藏來(lái)取消定時(shí)器終止自動(dòng)播放.
github項(xiàng)目地址
以上 demo 僅供參考,實(shí)際項(xiàng)目開(kāi)發(fā)中最好還是使用成熟的開(kāi)源組件,要有造輪子的能力和不造輪子的覺(jué)悟
react-slick
復(fù)雜組件設(shè)計(jì)
精讀《請(qǐng)停止 css-in-js 的行為》
使用 rollup
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/7883.html
摘要:獲取的對(duì)象范圍方法獲取的是最終應(yīng)用在元素上的所有屬性對(duì)象即使沒(méi)有代碼,也會(huì)把默認(rèn)的祖宗八代都顯示出來(lái)而只能獲取元素屬性中的樣式。因此對(duì)于一個(gè)光禿禿的元素,方法返回對(duì)象中屬性值如果有就是據(jù)我測(cè)試不同環(huán)境結(jié)果可能有差異而就是。 花了很長(zhǎng)時(shí)間整理的前端面試資源,喜歡請(qǐng)大家不要吝嗇star~ 別只收藏,點(diǎn)個(gè)贊,點(diǎn)個(gè)star再走哈~ 持續(xù)更新中……,可以關(guān)注下github 項(xiàng)目地址 https:...
摘要:?jiǎn)栴}回答者黃軼,目前就職于公司擔(dān)任前端架構(gòu)師,曾就職于滴滴和百度,畢業(yè)于北京科技大學(xué)。最后附上鏈接問(wèn)題我目前是一名后端工程師,工作快五年了。 showImg(https://segmentfault.com/img/bVbuaiP?w=1240&h=620); 問(wèn)題回答者:黃軼,目前就職于 Zoom 公司擔(dān)任前端架構(gòu)師,曾就職于滴滴和百度,畢業(yè)于北京科技大學(xué)。 1. 前端開(kāi)發(fā) 問(wèn)題 大...
摘要:經(jīng)歷月份開(kāi)放的簡(jiǎn)歷,收到了蠻多詢問(wèn)和面試,算是招人旺季,需要跳槽的小伙伴抓住機(jī)會(huì)。現(xiàn)在是面試了家公司左右,有些高頻問(wèn)題會(huì)標(biāo)記次數(shù)總次數(shù),可供大家參考。最后祝大家面試順利,拿到心儀的,寫(xiě)錯(cuò)的地方請(qǐng)不吝賜教,謝謝。 經(jīng)歷 7月份開(kāi)放的簡(jiǎn)歷,收到了蠻多詢問(wèn)和面試,算是招人旺季,需要跳槽的小伙伴抓住機(jī)會(huì)。一開(kāi)始廣泛看面試題,沒(méi)抓住重點(diǎn)復(fù)習(xí),有很多平時(shí)也沒(méi)怎么用到,導(dǎo)致一開(kāi)始面試的時(shí)候,問(wèn)的問(wèn)題...
摘要:經(jīng)歷月份開(kāi)放的簡(jiǎn)歷,收到了蠻多詢問(wèn)和面試,算是招人旺季,需要跳槽的小伙伴抓住機(jī)會(huì)?,F(xiàn)在是面試了家公司左右,有些高頻問(wèn)題會(huì)標(biāo)記次數(shù)總次數(shù),可供大家參考。最后祝大家面試順利,拿到心儀的,寫(xiě)錯(cuò)的地方請(qǐng)不吝賜教,謝謝。 經(jīng)歷 7月份開(kāi)放的簡(jiǎn)歷,收到了蠻多詢問(wèn)和面試,算是招人旺季,需要跳槽的小伙伴抓住機(jī)會(huì)。一開(kāi)始廣泛看面試題,沒(méi)抓住重點(diǎn)復(fù)習(xí),有很多平時(shí)也沒(méi)怎么用到,導(dǎo)致一開(kāi)始面試的時(shí)候,問(wèn)的問(wèn)題...
摘要:經(jīng)歷月份開(kāi)放的簡(jiǎn)歷,收到了蠻多詢問(wèn)和面試,算是招人旺季,需要跳槽的小伙伴抓住機(jī)會(huì)?,F(xiàn)在是面試了家公司左右,有些高頻問(wèn)題會(huì)標(biāo)記次數(shù)總次數(shù),可供大家參考。最后祝大家面試順利,拿到心儀的,寫(xiě)錯(cuò)的地方請(qǐng)不吝賜教,謝謝。 經(jīng)歷 7月份開(kāi)放的簡(jiǎn)歷,收到了蠻多詢問(wèn)和面試,算是招人旺季,需要跳槽的小伙伴抓住機(jī)會(huì)。一開(kāi)始廣泛看面試題,沒(méi)抓住重點(diǎn)復(fù)習(xí),有很多平時(shí)也沒(méi)怎么用到,導(dǎo)致一開(kāi)始面試的時(shí)候,問(wèn)的問(wèn)題...
閱讀 1714·2021-11-22 15:33
閱讀 2084·2021-10-08 10:04
閱讀 3542·2021-08-27 13:12
閱讀 3418·2019-08-30 13:06
閱讀 1466·2019-08-29 16:43
閱讀 1390·2019-08-29 16:40
閱讀 785·2019-08-29 16:15
閱讀 2745·2019-08-29 14:13