更新css
🎉圖片現已支持 -多分辨率- 下載(Safari暫不支持)html
🎉適配PC、Pad、Phone多種分辨率設備,持續更新中!前端
前段時間學習了React
的相關知識,嘗試使用Class Component
和 Hook
兩種方式進行項目實踐,其中Class Component
的使用主要圍繞生命週期展開,Hook
是比較新的函數式實現方式,弱化了生命週期的存在,多是React
將來主推的方式。react
嘗試使用了官方提供的create react app
和螞蟻提供的umi
進行項目搭建,create react app
僅提供最爲基礎的react
項目打包和運行配置(路由等相關配置需本身實現),而umi
提供開箱即用的詳細配置(包括css預處理的選擇、第三方UI框架的引入、自動化路由構建的封裝),可根據需求狀況靈活選擇。ios
使用了ant design react
等UI庫,對於中後臺項目的搭建很是友好,體驗很棒。nginx
選擇在線壁紙網站實現,一方面能夠體驗項目搭建的完整過程,還能夠方便你們瀏覽和獲取本身喜歡的壁紙。 (PS: 這樣換壁紙比較方便😂)git
鑑於本次實現的項目爲「在線壁紙網站」
,對比相關react ui
庫,最終選擇了semantic ui react
。github
優勢以下:redux
暗黑模式
Tip:axios
本文主要介紹react hook
基礎項目的搭建,後端基於Node
實現簡單的接口轉發和處理,本文暫不涉及redux
引入和後端實現,後續逐步更新。
壁紙來自360壁紙庫
,在此表示感謝,僅用做學習交流,切勿用於商業用途。
相關文檔地址:
react | create react app | umi | ant design react | semantic ui react
下面會經過兩方面介紹項目的搭建流程,即項目的初始化工做和項目(組件)的正式開發😁。
在介紹過程當中,會首先闡述設計的構思和關注點,再介紹實現細節,最後會附上相關源碼實現💪。
文中錯誤煩請指正,不足之處歡迎提出建議😘。
爲更好的理解和學習React項目
的搭建過程和技巧,這裏選擇使用官方提供的create react app
,在此基礎上根據當前項目需求,進行項目初始化配置。
這裏項目初始化分爲如下步驟:
目錄劃分及建立-->引入相應依賴包-->初始化全局css樣式-->完成路由處理模塊
「api定義及攔截器處理」
「圖片等靜態資源」
「基礎UI組件」
「自定義封裝組件」
「項目配置文件(主題、樣式、導航等配置)」
「佈局組件」
「路由配置」
「redux相關(這次內容不涉及)」
「頁面組件」
梳理本次項目中使用到的依賴包
PS: 部分依賴是項目開發過程當中加入,初始化搭建項目時僅引入已知所需依賴便可。
react 核心依賴
「react核心依賴」
「react-dom關聯核心依賴」
「react開發、運行等相關配置依賴」
「提供路由靜態配置」
「react-dom的加強,提供基礎路由容器組件及路由操做能力」
第三方組件依賴
「懶加載組件」
「樣式組件」
「語義化react ui庫」
「無限滾動組件」
「切換動畫組件」
其餘
「請求處理」
爲實現不一樣瀏覽器中H5標籤
擁有相一樣式表現,應當統一初始化全部標籤樣式,這裏結合styled-components
的createGlobalStyle
建立全局初始化樣式。
src/style.js
import { createGlobalStyle } from 'styled-components'
// 建立全局樣式
export const GlobalStyle = createGlobalStyle` body, h1, h2, h3, h4, h5, h6, hr, p, blockquote, dl, dt, dd, ul, ol, li, pre, form, fieldset, legend, button, input, textarea, th, td { margin:0; padding:0; } body, button, input, select, textarea { font: 100% inherit; } h1, h2, h3, h4, h5, h6{ font-size:100%; } address, cite, dfn, em, var { font-style:normal; } code, kbd, pre, samp { font-family: couriernew, courier, monospace; } small{ font-size:12px; } ul, ol { list-style:none; } a { text-decoration:none; cursor: pointer; } a:hover { text-decoration:none; } sup { vertical-align:text-top; } sub{ vertical-align:text-bottom; } legend { color:#000; } fieldset, img { border:0; } button, input, select, textarea { font-size:100%; } table { border-collapse:collapse; border-spacing:0; } `
複製代碼
在App.js
中引入該全局樣式組件便可。
src/App.js
import React from 'react'
import { GlobalStyle } from './style' // init global css style
function App() {
return (
<div className="App"> <GlobalStyle/> </div>
)
}
export default App
複製代碼
進行路由配置前,先實現BlankLayout
和BasicLayout
兩個佈局組件。
因該項目較爲簡單,全部組件均使用React.memo
進行淺比較,防止非必要的渲染,後文再也不贅述。
/src/layouts/BlankLayout.js
import React from 'react'
import { renderRoutes } from 'react-router-config'
const BlankLayout = ({route}) => {
return (99
<>{renderRoutes(route.routes)}</>
)
}
export default React.memo(BlankLayout)
複製代碼
這裏後續會引入Sticky
組件和createRef
(用於Sticky
掛載目標元素)來固定Nav
(頂部導航欄,下文詳細講解),Footer
(自定義頁腳信息)組件充當頁腳信息,內容區域設置最小高度80vh
並渲染匹配的子路由對應頁面。
/src/layouts/BasicLayout.js
import React, { createRef } from 'react'
import { renderRoutes } from 'react-router-config'
import Nav from '../components/Nav'
import Footer from '../components/Footer'
import navConfig from '../config/nav'
import { Sticky } from 'semantic-ui-react'
function BasicLayout (props) {
const contextRef = createRef()
const { route } = props
return (
<div ref={contextRef}> <Sticky context={contextRef}> <Nav data={navConfig}/> </Sticky> <div style={{ minHeight: '80vh' }}> {renderRoutes(route.routes)} </div> <Footer/> </div> ) } export default React.memo(BasicLayout) 複製代碼
首先引入lazy
和Suspense
實現路由懶加載
和延遲加載回調
,並引入自定義CustomPlaceholder
組件(未防止閃屏,這裏用佔位組件替代全局遮罩Loading)實現路由首次加載效果。
引入Redirect
實現根路由重定向,引入BlankLayout
、BasicLayout
分別對應初始路由和建立頁面通用佈局。
爲後續實現選擇壁紙種類後刷新頁面可正確顯示對應種類壁紙信息,這裏採用路由傳參方式實現壁紙頁面路由。
最後引入404頁面
捕獲當前沒法正確匹配的路由。
附: React路由傳參對比
src/router/index.js
import React, { lazy, Suspense } from 'react'
import { Redirect } from 'react-router-dom'
import BlankLayout from '../layouts/BlankLayout'
import BasicLayout from '../layouts/BasicLayout'
import CustomPlaceholder from '../basicUI/Placeholder'
// 延遲加載回調
const SuspenseComponent = Component => props => {
return (
<Suspense fallback={ <CustomPlaceholder /> }> <Component {...props}></Component> </Suspense>
)
}
// 組件懶加載
const PageWallPaper = lazy(() => import('../views/WallPaper'))
const PageAbout = lazy(() => import('../views/About'))
const Page404 = lazy(() => import('../views/404'))
export default [
{
component: BlankLayout,
routes: [
{
path: "/",
component: BasicLayout,
routes: [
{
path: "/",
exact: true, // 是否精確匹配
render: () => <Redirect to={"/wallpaper/5"} />
},
{
path: "/wallpaper/:id",
exact: true,
component: SuspenseComponent(PageWallPaper)
},
// ...等其餘頁面
{
path: "/*",
exact: true,
component: SuspenseComponent(Page404)
}
]
}
]
}
]
複製代碼
爲方便後續調整頂部導航欄信息,考慮設計爲可靈活擴展的組件。
參考常見頂部導航欄設計,考慮將頂部導航欄分爲兩種狀態:
考慮到頂部導航欄配置信息較多,所以抽離Nav
配置文件及說明至/src/config/nav.js
中。
導航欄配置詳情可參考:nav配置
在頂部導航欄組件中,首先定義getActiveItemByPathName
的方法用來根據路由信息比對菜單項信息,獲取當前路由對應激活的菜單項。經過selectActiveItem
對其調用後返回activeItem
的初始值,這裏就實現了激活菜單項的初始化操做。
接着引入useState
(hook
中定義組件狀態)並定義activeItem
和phoneNavShow
兩個組件狀態,分別對應當前激活的菜單項的key
和控制是否顯示移動端菜單組件
。以後定義監聽窗口變化(使用媒體查詢函數)方法,並在useEffect
中啓用監聽函數(別忘記銷燬時移除該監聽函數),至此兩種狀態的切換邏輯基本完成。
定義了handleMenuClick
方法處理菜單子項點擊邏輯,分爲外鏈URl
和站內URL
,分別對應打開新窗口和設置激活菜單項、進行路由跳轉的邏輯。
最後是menuView
完成菜單子項的渲染,及整體佈局代碼的render
實現,主要邏輯爲經過phoneNavShow
控制渲染大屏狀態下的組件仍是移動端的PhoneNav
組件(下面即將介紹)。別忘記引入withRouter
包裹以提供路由跳轉支持。
src/components/Nav/index.js
import React, { useState, useEffect } from 'react'
import { Dropdown, Menu } from 'semantic-ui-react'
import { withRouter } from 'react-router-dom'
import PhoneNav from './PhoneNav'
function Nav (props) {
// 根據path獲取activeItem
const getActiveItemByPathName = (menus, pathname) => {
let temp = ''
menus.map((item) => {
// 存在子菜單項
if (item.subitems && item.subitems.length > 0) {
item.subitems.map((i) => {
if (i.href === pathname) {
temp = i.key
return
}
})
}
if (item.href === pathname) {
temp = item.key
return
}
})
return temp
}
const selectActiveItem = () => {
const pathname = props.location.pathname
const val = getActiveItemByPathName(props.data.leftMenu, pathname)
return val === '' ? getActiveItemByPathName(props.data.rightMenu, pathname) : val
}
const [activeItem, setActiveItem] = useState(selectActiveItem())
const [phoneNavShow, setPhoneNavShow] = useState(false)
const x = window.matchMedia('(max-width: 900px)')
// 監聽窗口變化 過窄收起側邊欄 過寬展開側邊欄
const listenScreenWidth = (x) => {
if (x.matches) { // 媒體查詢
setPhoneNavShow(false)
} else {
setPhoneNavShow(true)
}
}
useEffect(() => {
listenScreenWidth(x) // 執行時調用的監聽函數
x.addListener(listenScreenWidth) // 狀態改變時添加監聽器
return () => {
x.removeListener(listenScreenWidth) // 銷燬時移除監聽器
}
}, [x])
const handleMenuClick = (menu) => {
if (menu.externalLink) {
window.open(menu.href)
} else {
setActiveItem(menu.key)
props.history.push(menu.href)
}
}
// 根據菜單配置信息遍歷生成菜單組
const menuView = (menus) => {
return menus.map((item) => {
return item.subitems && item.subitems.length ?
(
<Dropdown key={item.key} item text={item.title} style={{ color: props.data.textColor }}>
<Dropdown.Menu>
{
item.subitems.map((i) => {
return (
<Dropdown.Item onClick={ () => handleMenuClick(i) } key={i.key}>
{i.title}
</Dropdown.Item>
)
})
}
</Dropdown.Menu>
</Dropdown>
) :
(
<Menu.Item key={item.key}
active={activeItem === item.key}
style={{ color: props.data.textColor }}
onClick={ () => handleMenuClick(item) }
>
{ item.title }
</Menu.Item>
)
})
}
return (
<Menu size='huge' style={{ padding: '0 4%', background: 'black' }}
color={props.data.activeColor} pointing secondary
>
<Menu.Item header>
<img style={{ height: '18px', width: '18px' }} src={props.data.titleIcon}/>
<span style={{ color: 'white', marginLeft: '10px' }}>
{ props.data.titleText }
</span>
</Menu.Item>
{ phoneNavShow ? (
<>
<Menu.Menu position='left'>
{ menuView(props.data.leftMenu) }
</Menu.Menu>
<Menu.Menu position='right'>
{ menuView(props.data.rightMenu) }
</Menu.Menu>
</>
) : (
<Menu.Menu position='right'>
<Menu.Item>
<PhoneNav data={props.data} handlePhoneNavClick={menu => handleMenuClick(menu)}></PhoneNav>
</Menu.Item>
</Menu.Menu>
)
}
</Menu>
)
}
export default withRouter(React.memo(Nav))
複製代碼
在PhoneNav
組件中,首先引入useState
並聲明瞭activeIndex
和visible
兩個組件狀態,分別表示當前須要激活的菜單組展開項、是否顯示全局下拉菜單
。
接着定義showPhoneNavWrapper
方法實現對展開菜單按鈕的動畫實現及控制全局下拉菜單
的顯示。定義handleMenuClick
方法實現對全局下拉菜單
子項點擊處理,這裏經過回調父組件菜單點擊方法實現,並隱藏當前全局下拉菜單
。
最後是menuView
完成菜單子項的渲染,及整體佈局代碼的render
實現(總體思路和父組件相似)。
src/components/Nav/index.js
import React, { useState } from 'react'
import { PhoneNavBt, PhoneNavWrapper } from './style'
import { Icon, Menu, Accordion, Transition } from 'semantic-ui-react'
import { withRouter } from 'react-router-dom'
function PhoneNav (props) {
const [activeIndex, setActiveItem] = useState('')
const [visible, setVisible] = useState(false)
const emList = document.getElementsByClassName('phone-nav-em')
const showPhoneNavWrapper = () => {
setVisible(!visible)
if (visible) {
emList[0].style.transform = ''
emList[1].style.transition = 'all 0.5s ease 0.2s'
emList[1].style.opacity = '1'
emList[2].style.transform = ''
} else {
emList[0].style.transform = 'translate(0px,6px) rotate(45deg)'
emList[1].style.opacity = '0'
emList[1].style.transition = ''
emList[2].style.transform = 'translate(0px,-6px) rotate(-45deg)'
}
}
const handleMenuClick = (menu) => {
props.handlePhoneNavClick(menu)
setVisible(false)
emList[0].style.transform = ''
emList[1].style.transition = 'all 0.5s ease 0.2s'
emList[1].style.opacity = '1'
emList[2].style.transform = ''
}
const menuView = (menus) => {
return menus.map((item) => {
return item.subitems && item.subitems.length ?
(
<Accordion key={item.key} styled inverted style={{ background: 'black', width: '100%'}}>
<Accordion.Title
as={Menu.Header}
active={activeIndex === item.key}
index={0}
onClick={() => setActiveItem(activeIndex === item.key ? '-1' : item.key)}
>
<Icon name='dropdown' />
{ item.title }
</Accordion.Title>
{
item.subitems.map((i) => {
return (
<Accordion.Content style={{padding: '0px'}} key={i.key} active={activeIndex === item.key}>
<Menu.Item style={{ paddingLeft: '3rem', color: props.data.textColor, background: '#1B1C1D' }}
onClick={() => handleMenuClick(i) }>
{ i.title }
</Menu.Item>
</Accordion.Content>
)
})
}
</Accordion>
)
:
(
<Menu.Item style={{ color: props.data.textColor }} onClick={() => handleMenuClick(item) } key={item.key}>
{ item.title }
</Menu.Item>
)
})
}
return (
<>
<PhoneNavBt onClick={ showPhoneNavWrapper }>
<em className='phone-nav-em'></em>
<em className='phone-nav-em'></em>
<em className='phone-nav-em'></em>
</PhoneNavBt>
<Transition visible={visible} animation='fade' duration={500}>
<PhoneNavWrapper>
<Menu style={{ width: '100%' }} inverted size='huge' vertical>
{ menuView(props.data.leftMenu) }
{ menuView(props.data.rightMenu) }
</Menu>
</PhoneNavWrapper>
</Transition>
</>
)
}
export default React.memo(PhoneNav)
複製代碼
導航欄配置詳情可參考:nav配置
爲便於組件的複用、擴展和升級,以及對不一樣分辨率設備的兼容,這裏考慮拆分爲如下功能模塊組件。
爲進一步明確、細分各組件的功能,藉助思惟導圖完成對各組件功能邏輯的梳理,以下圖:
該組件較爲簡單,遍歷父組件傳遞的props.data
,渲染對應子菜單內容便可,後續可結合Redux
實現主題切換功能。
/src/components/MenuBar/index.js
import React from 'react'
import { Menu } from 'semantic-ui-react'
function MenuBar (props) {
return (
<>
{
props.data.length ?
<Menu secondary compact size='mini' style={{ background: 'white', width: '100%', overflow: 'auto' }}>
{
props.data.map((item, index) => {
return (
<Menu.Item onClick={() => props.onMenuClick(item)} key={index}>
{item.title}
</Menu.Item>
)
})
}
</Menu> : null
}
</>
)
}
export default React.memo(MenuBar)
複製代碼
這裏根據設備寬度計算ImgView
組件包裹容器的寬和高(ImgView
會自動填充包裹容器),以確保在不一樣大小的設備下圖片顯示大小適中。
而後使用LazyLoad
懶加載組件,設置在滾動至屏幕可視區域下200px
時加載圖片,以保證未下拉時僅加載當前窗口下的圖片,最後將圖片的地址和標籤傳給ImgView
組件。
/src/basicUI/ImgListView/index.js
import React from 'react'
import LazyLoad from 'react-lazyload'
import ImgView from '../ImgView'
import { ImgListViewWrap, ImgViewWrap } from './style'
function ImgListView (props) {
const imgList = props.data
const width = (1 / (document.body.clientWidth / 360) * document.body.clientWidth).toFixed(3)
const height = (width * 0.5625).toFixed(3)
return (
<ImgListViewWrap> { imgList.length > 0 ? imgList.map((item) => { return ( <ImgViewWrap key={item.id} width={ width + 'px' } height={ height + 'px' }> <LazyLoad height={'100%'} offset={200} > <ImgView key={item.id} onPreviewClick={() => props.handlePreview(item)} onDownloadClick={() => props.handleDownload(item)} url={item.url} tag={ item.utag } /> </LazyLoad> </ImgViewWrap> ) }) : null } </ImgListViewWrap> ) } export default React.memo(ImgListView) 複製代碼
首先經過對url
的過濾獲取低分辨率圖片地址(即縮略圖),以減小圖片數據請求量。
在render
中主要包含如下部分:
關於佔位圖片,初始狀態時設置佔位圖片爲絕對定位、默認顯示,目標圖片透明度爲0。經過useState
聲明isLoaded
表示目標圖片是否加載完成,經過對onLoad
事件的監聽,修改isLoaded
的狀態,此時隱藏佔位圖片,修改目標圖片透明度爲1,至此完成加載成功後的切換(這裏使用useCallback
緩存內聯函數,防止組件更新重複建立匿名函數)。
圖片首次加載經過CSSTransition
組件,自定義fade
的動畫樣式,經過透明度的變化實現過分效果。
圖片蒙層使用絕對定位至於ImgView
下方,其中加入預覽、下載按鈕的點擊回調。
/src/basicUI/ImgView/index.js
import React, { useState, useCallback } from 'react'
import { Image, Icon } from 'semantic-ui-react'
import { CSSTransition } from 'react-transition-group'
import { ImgWrap } from './style'
import loadingImg from './loading.gif'
import './fade.css'
function ImgView (props) {
const { url, tag } = props
const [isLoaded, setIsLoaded] = useState(false)
// cache memoized version of inline callback
const handleLoaded = useCallback(() => {
setIsLoaded(true)
}, [])
const filterUrl = () => {
const array = url.split('/bdr/__85/')
// 過濾url爲低分辨率圖片,防止加載時間較長
return array.length !== 2 ? url : array[0] + '/bdm/640_360_85/' + array[1]
}
// 正式Image未加載以前沒有高度信息
return (
<ImgWrap>
<Image hidden={ isLoaded } className='img-placeholder' src={ loadingImg } rounded />
<CSSTransition
in={true}
classNames={'fade'}
appear={true}
key={1}
timeout={300}
unmountOnExit={true}
>
<Image onLoad={() => setIsLoaded(true)} style={{ opacity: isLoaded ? 1 : 0 }}
src={ filterUrl() } title={ tag } alt={ tag } rounded />
</CSSTransition>
<div className='dim__wrap'>
<span className='tag'>{ tag }</span>
<Icon onClick={ () => props.onPreviewClick() } name='eye' color='orange' />
<Icon onClick={ () => props.onDownloadClick() } name='download' color='teal' src={ filterUrl() } />
</div>
</ImgWrap>
)
}
export default React.memo(ImgView)
複製代碼
預覽組件較爲簡單,在全局遮罩下顯示圖片和標籤信息便可。
/src/basicUI/ImgPreview/index.js
function ImgPreview (props) {
const { url, utag } = props.previewImg
return (
<Dimmer active={ props.visible } onClick={props.handleClick} page> <Image style={{ maxHeight: '90vh' }} src={ url } title={ utag } alt={ utag } /> </Dimmer> ) } 複製代碼
首先封裝了圖片下載的工具類,接收圖片地址和下載後的文件名稱兩個參數。經過發送圖片地址請求,並設置返回類型爲blob
,再利用<a>
標籤進行下載便可。
Tip: 因爲Safari的安全機制,沒法進行blob
的相關讀寫操做,所以該方法在Safari中沒法使用,應在下載組件中判斷是否爲Safari瀏覽器,並提醒用戶。
/src/basicUI/DownloadModal/download.js
function download (url, fileName) {
const x = new XMLHttpRequest()
x.responseType = 'blob'
x.open('GET', url, true)
x.send()
x.onload = () => {
const downloadElement = document.createElement('a')
const href = window.URL.createObjectURL(x.response) // create download url
downloadElement.href = href
downloadElement.download = fileName // set filename (include suffix)
document.body.appendChild(downloadElement) // append <a>
downloadElement.click() // click download
document.body.removeChild(downloadElement) // remove <a>
window.URL.revokeObjectURL(href) // revoke blob
}
}
複製代碼
對於下載組件,根據下載配置文件(src/config/download_options.js
)生成下載列表選項,在點擊下載後,進行Safari判斷和提示,並根據下載配置拼接對應分辨率圖片地址進行下載。
下載分辨率配置詳情可參考:下載分辨率配置
/src/basicUI/DownloadModal/index.js
function DownloadModal (props) {
const { url, utag } = props.downloadImg
const handleDownload = (param) => {
// Safari Tip
if (/Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent)) {
alert('抱歉😅!暫不支持Safari下載!請手動保存照片!')
return
}
const array = url.split('/bdr/__85/')
array.length === 2 ? download(array[0] + param + array[1], utag + '.jpg') : download(url, utag + '.jpg')
}
return (
<Modal basic dimmer={ 'blurring' } open={ props.visible }>
<Header icon='browser' content='download options' />
<Modal.Content>
<List verticalAlign='middle'>
{ downloadOptions.length > 0
? downloadOptions.map((item, index) => {
return (
<List.Item key={ index }>
<List.Content floated='right'>
<Button onClick={ () => handleDownload(item.filterParam) }
basic color='green' icon='download' inverted size='mini' />
</List.Content>
<List.Content>
<Label>{ item.desc }</Label>
</List.Content>
</List.Item>
)
}) : null }
</List>
</Modal.Content>
<Modal.Actions>
<Button onClick={ () => props.onClose() } color='green' inverted>
OK
</Button>
</Modal.Actions>
</Modal>
)
}
export default React.memo(DownloadModal)
複製代碼
PageWallPaper
中會加載圖片相關組件,並完成對圖片加載、請求等邏輯的控制。
爲更加清晰的介紹,這裏拆解爲render
和邏輯處理
兩塊進行介紹:
在render
方法中,首先使用Sticky
組件固定MenuBar
組件至導航欄下方,將壁紙種類列表typeList
傳給該組件,並使用changeImgType
完成對點擊壁紙種類切換的處理。
而後使用InfiniteScroll
包裹ImgListView
組件,其中ImgListView
處理預覽、下載按鈕點擊事件,並接收圖片列表imgList
。無限加載組件InfiniteScroll
中根據isLoading
(是否正在加載)、isFinished
(是否所有加載完成)、imgList.length
(是否圖片列表爲空)判斷是否須要支持更多信息加載(便是否滾動會觸發loadMore
回調)。loadMore
中實現加載更多圖片。
最後根據isLoading
、isFinished
控制是否顯示正在加載、加載完成等用戶提示。 經過引入ImgPreview
、DownloadModal
實現大圖預覽和圖片下載的支持。
src/views/WallPaper/index.js => render
function PageWallPaper (props) {
<!-- 這裏僅展現render,邏輯處理部分後文介紹 -->
return (
<div ref={contextRef}>
{/* img type menu */}
<Sticky context={contextRef} offset={48} styleElement={{ zIndex: '10' }}>
<MenuBar onMenuClick={ changeImgType } data={typeList} />
</Sticky>
{/* loading img (infinity) */}
<InfiniteScroll
initialLoad
pageStart={0}
loadMore={ () => loadMoreImgs() }
hasMore={ !isLoading && !isFinished && imgList.length !== 0 }
threshold={50}
>
<ImgListView
handlePreview={ handlePreviewImg }
handleDownload = { handleDownloadImg }
data={ imgList }
/>
</InfiniteScroll>
{ isLoading ? <CustomPlaceholder /> : null }
{ isFinished ? <h1 style={{ textAlign: 'center' }}>全部圖片已加載完成!✨</h1> : null }
{/* img preview */}
<ImgPreview handleClick={ hideImgPreview } visible={ isPreview } previewImg={ currentImg } />
{/* download options */}
<DownloadModal onClose={ hideDownloadModal } visible={ isDownload } downloadImg={ currentImg } />
</div>
)
}
複製代碼
首先經過useState
定義多種組件狀態和初始狀態,分別有查詢條件、圖片是否正在加載、是否顯示預覽、是否顯示下載、是否加載完成所有圖片、當前選中圖片信息、圖片列表、種類列表(詳情請看代碼註釋),經過createRef
對節點的引用完成sticky
組件的掛載點。
接下來使用useEffect
完成相關反作用,這裏使用兩個useEffect
實現關注點的分離。
第一個useEffect
中,第二個參數爲[]
,即模擬相似componentDidMount
生命週期效果,這裏經過getTypes()
獲取壁紙類型。
第二個useEffect
中,第二個參數爲[queryInfo]
,即queryInfo
發生改變後,調用updateImgList()
方法更新圖片列表。
對於getTypes()
和updateImgList()
的實現,經過axios
發送請求並將正常的結果保存至對應組件狀態中。 在updateImgList()
中,若返回圖片列表爲空,則說明全部圖片都加載完成,此時設置isFinished
爲true
,不然經過Array.concat()
合併新舊圖片列表並保存至imgList
中,最後修改加載狀態爲fasle
。
在壁紙種類點擊的回調changeImgType()
中,判斷若不是當前頁面對應的壁紙種類,則進行頁面跳轉(需引入withRouter
支持),而後設置返回頁面頂部,並恢復組件的初始狀態,其中修改查詢對象queryInfo
的type
狀態。
對於滾動列表的加載回調loadMoreImgs
中,設置isLoading
爲true
,並修改queryInfo
的查詢參數,此時會出發第二個useEffect
的反作用,完成圖片列表的更新。
最後是通用useCallback
緩存相關內聯函數,防止組件更新重複建立匿名函數,以提高性能。
src/views/WallPaper/index.js
import React, { useState, useEffect, createRef, useCallback } from 'react'
import { withRouter } from 'react-router-dom'
import { getCategories, getPictureList } from '../../api/getData'
function PageWallPaper (props) {
const [queryInfo, setQueryInfo] = useState({type: props.match.params.id || 5, start: 0, count: 30}) // query info
const [isLoading, setIsLoading] = useState(true) // is loading img
const [isPreview, setIsPreview] = useState(false) // is preview img
const [isDownload, setIsDownload] = useState(false) // is download modal show
const [isFinished, setIsFinished] = useState(false) // is all img loading finished
const [currentImg, setCurrentImg] = useState({}) // current img info
const [imgList, setImgList] = useState([])
const [typeList, setTypeList] = useState([])
const contextRef = createRef()
useEffect(() => {
getTypes()
}, [])
useEffect(() => {
updateImgList()
}, [queryInfo])
const getTypes = async () => {
const res = await getCategories()
if (res.data) {
setTypeList(res.data.data)
}
}
// update img list
const updateImgList = async () => {
const res = await getPictureList({...queryInfo})
if (res.data) {
if (res.data.data.length === 0) {
setIsFinished(true)
} else {
setImgList(imgList.concat(res.data.data))
}
setIsLoading(false)
}
}
const changeImgType = (item) => {
if (item.key !== queryInfo.type) {
props.history.push('/wallpaper/' + item.key)
}
document.body.scrollTop = 0
document.documentElement.scrollTop = 0
// init state
setImgList([])
setIsLoading(true)
setIsFinished(false)
setQueryInfo({...queryInfo, type: item.key })
}
const loadMoreImgs = () => {
setIsLoading(true)
setQueryInfo({...queryInfo, start: queryInfo.start + queryInfo.count})
}
// cache memoized version of inline callback
// click preview
const handlePreviewImg = useCallback((img) => {
setCurrentImg(img)
setIsPreview(true)
}, [])
// click download
const handleDownloadImg = useCallback((img) => {
setCurrentImg(img)
setIsDownload(true)
}, [])
// hide ImgPreview
const hideImgPreview = useCallback(() => {
setIsPreview(false)
}, [])
// hide DownloadModal
const hideDownloadModal = useCallback(() => {
setIsDownload(false)
}, [])
}
export default withRouter(React.memo(PageWallPaper))
複製代碼
至此壁紙頁面的設計、加載邏輯開發完成,後續會繼續優化圖片加載效果、邏輯解耦等。
最後完成頁腳和異常頁的開發,頁面可根據我的喜愛進行設計,主要以樣式爲主,與hook
的關聯很少,這裏再也不贅述。
頁腳配置詳情可參考:footer配置。
- git clone
- yarn
- yarn start
複製代碼
完成nginx
配置後,結合 從零開始 Node實現前端自動化部署 體驗更佳。
以上就是對這次在線壁紙前端實現的介紹,既能夠幫助瞭解React
項目的基礎搭建流程,也鞏固了Hook
的使用,也在組件設計、拆分的過程當中增長本身的理解與思考。
文章中若有疏漏、錯誤,歡迎指出。
項目仍在完善更新中,歡迎你們提出建議和靈感。
🎉該項目已開源至 github
歡迎下載使用 後續會完善更多功能 🎉 源碼及項目說明
Tip: 喜歡的話別忘記 star
哦😘,有疑問🧐歡迎提出 issues
,積極交流。