React Hook реализует веб-сайт онлайн-обоев с нуля

React.js
React Hook реализует веб-сайт онлайн-обоев с нуля

возобновить

🎉Теперь поддерживается загрузка изображений с несколькими разрешениями (в настоящее время Safari не поддерживает)

🎉 Постоянно обновляется, совместимо с ПК, планшетом, телефоном и устройствами с различным разрешением!

предисловие

Я узнал некоторое время назадReactзнания, попробуйте использоватьClass Componentа такжеHookСуществует два способа проведения проектной практики, среди которыхClass ComponentИспользование в основном вращается вокруг жизненного цикла,HookЭто относительно новая функциональная реализация, которая ослабляет существование жизненного цикла, что может бытьReactПуть в будущее.

Попробуйте использовать официальнуюcreate react appи обеспечивают муравьиumiпостроить проект,create react appДайте только самое основноеreactУпаковка проекта и текущая конфигурация (маршрутизация и другие связанные конфигурации должны быть реализованы самостоятельно), иumiПредоставление подробной конфигурации из коробки (предварительная обработка CSS состоит из выбора, AUI Framework ввести третьей стороне, автоматизированная конструкция маршрутизации пакета) может быть гибко выбран в соответствии с требованием.

использовалant design react

(P.S. Так обои менять удобнее 😂)

В связи с реализованным проектом「在线壁纸网站」, в сравнении сreact uiбиблиотека, окончательно выбраннаяsemantic ui react.

Преимущества заключаются в следующем:

  • Поддержка пользовательского стиля меток рендеринга компонентов
  • Поддерживает богатые стили и конфигурации компонентов
  • Поддержка инверсии цвета компонентов для упрощения реализации暗黑模式
  • Компоненты названы на основе семантики, которую легко найти и использовать.

Tip:

Эта статья в основном знакомитreact hookПостроение базового проекта, бэкенд основан наNodeРеализовать простую переадресацию и обработку интерфейса, которая пока не рассматривается в этой статье.reduxВведение и внутренняя реализация будут постепенно обновляться позже.

обои от360壁纸库, я хотел бы выразить свою благодарность здесь.Он используется только для обучения и общения, а не для коммерческого использования.

Адрес связанного документа:

react | create react app | umi | ant design react | semantic ui react

Показать результаты

qrcode_aotianwinter.github.io.png

Начинать

Далее будет представлен процесс построения проекта через два аспекта, а именно инициализацию проекта и формальную разработку проекта (компонента)😁.

Во время презентации мы сначала разработайте дизайнерские идеи и проблемы, а затем описываем детали реализации, в конечном итоге присоединят соответствующий исходный код для достижения 💪.

Пожалуйста, исправьте все ошибки в тексте, и предложения приветствуются 😘.

Завершить инициализацию проекта

для лучшего понимания и изученияReact项目Процесс строительства и навыки, здесь мы решили использовать официально предоставленныеcreate react app

Эта инициализация проекта разделена на следующие этапы:

Разделение и создание каталогов --> Ввести соответствующий пакет зависимостей --> Инициализировать глобальный стиль CSS --> Завершить модуль обработки маршрутизации

отдел каталогов

  • src
    • api 「api定义及拦截器处理」
    • assets 「图片等静态资源」
    • basicUI 「基础UI组件」
    • components 「自定义封装组件」
    • conifg 「项目配置文件(主题、样式、导航等配置)」
    • layouts 「布局组件」
    • routes 「路由配置」
    • store 「redux相关(此次内容不涉及)」
    • views 「页面组件」

импорт зависимостей

Разберите зависимости, используемые в этом проекте

PS: частичные зависимости добавляются во время процесса разработки проекта. Только известные необходимые зависимости могут быть введены, когда проект изначально построен.

реагировать на основные зависимости

  • react 「react核心依赖」
  • react-dom 「react-dom关联核心依赖」
  • react-scripts 「react开发、运行等相关配置依赖」
  • react-router-config 「提供路由静态配置」
  • react-router-dom 「react-dom的增强,提供基础路由容器组件及路由操作能力」

Зависимости сторонних компонентов

  • react-lazyload 「懒加载组件」
  • styled-components 「样式组件」
  • semantic-ui-react 「语义化react ui库」
  • react-infinite-scroller 「无限滚动组件」
  • react-transition-group 「切换动画组件」

разное

  • axios 「请求处理」

импорт стиля

для разных браузеров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Поверхностное сравнение сделано, чтобы предотвратить ненужный рендеринг, который не будет повторяться позже.

  • BlankLayout (используется для сопоставления с исходным маршрутом и прямого отображения содержимого, соответствующего маршруту)

/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)
  • Passiclayout (для создания общего расположения страниц)

Он будет представлен позже здесь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Сборка (не мешает заставка, где масса компонентов альтернативная глобальная маска Загрузка) для маршрутизации первого загрузочного исполнения.

представлять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середина.

Подробнее о настройке панели навигации см.:Навигация конфигурации

Подробная реализация

getActiveItemByPathName的方法用来根据路由信息比对菜单项信息,获取当前路由对应激活的菜单项。 пройти черезselectActiveItemactiveItemНачальное значение , здесь реализуется операция инициализации активированного пункта меню.

Затем импортируйте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

существуетPhoneNavuseStateactiveIndexа также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)

Профиль навигации

Подробнее о настройке панели навигации см.:конфигурация навигации

Реализовать загрузку главной страницы (ядро)

Концепт дизайна

В целях облегчения повторного использования, расширения и обновления компонентов, а также совместимости с устройствами разного разрешения считается, что он разделен на следующие функциональные компоненты модуля.

  • Меню навигации по категориям обоев (Категория и выбор обоев)
  • Список изображений (управлять макетом картинок, предоставить ленивую нагрузку)
  • Компонент изображения (обеспечивает поддержку изображения-заполнителя, загрузку анимации перехода)
  • Компонент предварительного просмотра большого изображения (предоставляет функцию предварительного просмотра большого изображения в полноэкранном режиме)
  • Компонент загрузки (обеспечивает выбор разрешения и загрузку изображения)

Чтобы еще больше прояснить и разделить функции каждого компонента, функциональная логика каждого компонента сортируется с помощью карты связей, как показано на следующем рисунке:

组件逻辑思维导图

Подробная реализация

Обои вид навигационного меню

Этот компонент относительно прост, обходит переданный родительский компонент.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утверждениеisLoadedonLoadПрослушивание и модифицировать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>ярлык для загрузки.

Наконечник:Невозможно из-за механизма безопасности 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)

Страница WallPaper

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Определение различных компонентов и начальное состояние состояния, соответственно, запрос, загружено ли изображение, отображается ли предварительный просмотр, отображается ли загрузка, загружены ли все изображения, информация о выбранном изображении, изображение список, список видов (подробности см. в комментариях к коду), пройти черезcreateRefsticky

Затем используйтеuseEffectПолные связанные побочные эффекты, здесь используйте дваuseEffectДобейтесь разделения интересов.

ПервыйuseEffect, второй параметр[]МоделированиеcomponentDidMountЭффекты жизненного цикла, здесьgetTypes()Получите тип обоев.

второйuseEffect, второй параметр[queryInfo],Прямо сейчасqueryInfoПосле замены позвонитеupdateImgList()Метод обновляет список изображений.

дляgetTypes()а такжеupdateImgList()реализация, черезaxiosОтправьте запрос и сохраните нормальный результат в соответствующем состоянии компонента. существуетupdateImgList()isFinishedдляtrueArray.concat()imgListfasle.

Обратный вызов для клика по категории обоев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Корреляций не так много, поэтому я не буду их здесь повторять.

Подробнее о настройке нижнего колонтитула см.:.

Эпилог

ReactHookЕго использование также увеличивает ваше собственное понимание и мышление в процессе проектирования и разделения компонентов.

Если в статье есть упущения или ошибки, просьба указать на них.

Проект все еще дорабатывается и обновляется, и мы приветствуем предложения и вдохновение.

Проект был открытgithubДобро пожаловать на загрузку и использование, в будущем будет улучшено больше функций 🎉Исходный код и описание проекта

Наконечник:Не забудьте, если вам нравитсяstarО 😘, если есть вопросы 🧐 добро пожаловатьissuesАктивный обмен.

Другие статьи:

Node реализует автоматическое развертывание переднего плана с нуля

Инструмент автоматического развертывания сборки с открытым исходным кодом ⚡ auto-deploy-app