一個豆瓣電影 MovieDob 網頁版

前言

前面已經寫了一個 一個豆瓣電影小程序 的微信小程序;如今這個是 React+Typescript 的網頁版,基於 這裏 的修改版,antd 換爲 antd-mobilecss

源碼在線預覽react

一、關於 Class 組件、函數組件+Hooks

1.1 何時用 函數組件+Hooks ?

小功能組件,好比倒計時、數量加減框、評分、搜索框等等 不涉及 異步請求、各類監聽(scroll等)的組件webpack

  • React Hooks 的使用仍是愉快的,簡單功能的開發很省代碼;
  • useMemo 能夠看成 computed 使用,useEffect 能夠實現 watch 的效果,也能夠有 mount/unmount 的效果,還有其餘方便的東西
  • 基本上能夠視爲 Class 組件的 render 部分;

1.2 繼續用 Class 組件

組件銷燬前,請求還在繼續~

  • 要麼 撤銷請求(axios CancelToken)
    這個比較麻煩,要對每一個請求作處理。。。
  • 要麼 在組件 unmount 前 重寫 this.setState 方法
public componentWillUnmount() {
  // 組件銷燬後,不操做數據
  this.setState = () => {};
}
複製代碼

監聽滾動

  • 函數+Hooks 寫法:ios

    • **useEffect 第二個參數爲傳空數組[]_onScrolluseState 只會起做用一次!!!很詭異(Capture Value ?)。。。 一開始我是這麼寫的,
    • useEffect 第二個參數不傳: 這樣就能夠,可是這樣又會致使 每次 state 變化 執行一次,官方的 Demo 寫法好像就是這樣的。。。不知道這是否是正確的姿式!?!

    能夠在 這裏 看看git

  • Class 組件寫法:github

// src/views/home/index.tsx
  constructor(props: IProps) {
    super(props);
    this._onScroll = this._onScroll.bind(this);
  }
  
  public componentDidMount() {
    window.addEventListener('scroll', this._onScroll);
  }

  public componentWillUnmount() {
    window.removeEventListener('scroll', this._onScroll);
  }
複製代碼

二、列表 keep-alive

因爲 React 沒有像 Vue 提供的 <keep-alive></keep-alive> 組件,要實現這個就本身動手來,這個在 這裏 已經大概說了一下web

2.1 路由的寫法

這裏的 AuthRoute 是基於官方 Route 的封裝;主要就是使用 Route 的 render 方法 渲染列表頁,而後詳情頁是做爲 children 掛在列表頁下面的axios

// src/routes/home.tsx
import AuthRoute from '@/routes/auth-route';
import * as React from 'react';
import Loadable from '@loadable/component';

const Home = Loadable(() => import('@/views/home'));
const SearchList = Loadable(() => import('@/views/search-list'));

// home
export default [
  <AuthRoute 
    key="search"
    path="/search"
    render={() => (
      <SearchList>
        <AuthRoute 
          exact={true} 
          path="/search/movie-detail/:id" 
          component={Loadable(() => import('@/views/movie-detail'))} 
        />
      </SearchList>
    )}
  />,
  <AuthRoute 
    key="home" 
    path="/" 
    render={() => (
      <Home>
        <AuthRoute 
          exact={true} 
          path="/movie-detail/:id" 
          component={Loadable(() => import('@/views/movie-detail'))} 
        />
      </Home>
    )}
  />
]
複製代碼

2.2 列表組件的處理

2.2.1 詳情頁組件

  • 在詳情頁路由時,隱藏列表頁的內容
  • this.props.children 就是上面 <Home> 裏面的東西
// src/views/home/index.tsx

  public isDetailPage() {
    return this.props.location.pathname.includes("/movie-detail/");
  }

  public render() {
    const { 
      movieLineStatus, 
      isLoading, 
      movieLine, 
      movieComing, 
      movieTop250, 
      isTop250FullLoaded
    } = this.state;

    return (
      <div className={`${styles.home}`}>
        {!this.isDetailPage() &&
          <HeaderSearch onConfirm={(val) => this.onConfirm(val)} />
        }
        <div 
          className={`${styles['home-content']} center-content`}
          style={{ 
            display: this.isDetailPage() 
            ? 'none' 
            : 'block' 
          }}>
          <section className={styles['movie-block']}>
            <div className={styles['block-title']}>
              <span className={`${styles['title-item']} ${movieLineStatus === 0 && styles['title-active']}`}
                onClick={() => this.movieStatusChange(0)}
              >院線熱映</span>
              <span className={`${styles['title-item']} ${movieLineStatus === 1 && styles['title-active']}`}
                onClick={() => this.movieStatusChange(1)}
              >即將上映</span>
            </div>
    
            {movieLineStatus === 0 ? (
              <MovieItem movieList={movieLine} toDetail={(id: string) => this.toDetail(id)} />
            ) : (
              <MovieItem movieList={movieComing} toDetail={(id: string) => this.toDetail(id)} />
            )}
          </section>
    
          <MovieTop250 isLoading={isLoading} movieTop250={movieTop250} toDetail={(id: string) => this.toDetail(id)} />
    
          {isLoading && <Loading />}

          <TopBtn />

          {isTop250FullLoaded && <div className={styles.nomore}>沒有更多數據了~</div>}
        </div>
        
        {/* detial */}
        { this.props.children }
      </div>
    )
  }
複製代碼

2.2.2 滾動位置恢復

  • 在列表頁路由下,監聽滾動事件,保存滾動條位置 scrollTop
  • 進入詳情頁路由時,移除滾動事件監聽
  • 回到列表頁面時,恢復滾動條位置
// src/views/home/index.tsx
  constructor(props: IProps) {
    super(props);
    this._onScroll = this._onScroll.bind(this);
  }

  public componentDidMount() {
    this._getMovieLine();
    this._getMovieTop250();
    getMovieTop250All();

    this.props.history.listen(route => {
      this.onRouteChange(route);
    })

    window.addEventListener('scroll', this._onScroll);
  }

  public componentWillUnmount() {
    // 組件銷燬後,不操做數據
    this.setState = () => {};
    window.removeEventListener('scroll', this._onScroll);
  }

  // 監聽路由變化
  public onRouteChange(route: any) {
    // 首頁
    if (route.pathname === '/') {
      const { scrTop } = this.state;
      window.addEventListener('scroll', this._onScroll);
      // 恢復滾動條位置
      this.setScrollTop(scrTop);
    }
    // 詳情頁
    if (route.pathname.includes("/movie-detail/")) {
      // 重置滾動條位置
      this.setScrollTop(0);
      window.removeEventListener('scroll', this._onScroll);
    }
  }

  // 設置滾動條位置
  public setScrollTop(top: number) {
    document.body.scrollTop = top;
    document.documentElement.scrollTop = top;
  }

  public _onScroll() {
    const winHeight = window.innerHeight;
    const srcollHeight = document.documentElement.scrollHeight;
    const scrollTop = document.body.scrollTop || document.documentElement.scrollTop;
    const toBottom = srcollHeight - winHeight - scrollTop;

    if (toBottom <= 200) {
      this._getMovieTop250({ start: this.state.currentPage*10 });
    }
    if (this.props.location.pathname === '/') {
      this.setState({ scrTop: scrollTop });
    } else {
      window.removeEventListener('scroll', this._onScroll);
    }
  }
複製代碼

三、代碼預加載 prefetch

webpack v4.6.0+ 的功能,文檔小程序

在首頁路由,瀏覽器空閒時下載代碼,從首頁進入詳情頁時直接從緩存中讀取,沒有白屏微信小程序

使用如:

const Detail = Loadable(() => import(/* webpackPrefetch: true */ '@/views/movie-detail'));
複製代碼

路由:

// src/routes/home.tsx
import AuthRoute from '@/routes/auth-route';
import * as React from 'react';
import Loadable from '@loadable/component';

const Home = Loadable(() => import('@/views/home'));
const SearchList = Loadable(() => import('@/views/search-list'));
const Detail = Loadable(() => import(/* webpackPrefetch: true */ '@/views/movie-detail'));

// home
export default [
  <AuthRoute 
    key="search"
    path="/search"
    render={() => (
      <SearchList>
        <AuthRoute 
          exact={true} 
          path="/search/movie-detail/:id" 
          component={Detail} 
        />
      </SearchList>
    )}
  />,
  <AuthRoute 
    key="home" 
    path="/" 
    render={() => (
      <Home>
        <AuthRoute 
          exact={true} 
          path="/movie-detail/:id" 
          component={Detail} 
        />
      </Home>
    )}
  />
]
複製代碼

四、定位 position: sticky;

根據父元素的內容位置定位,會被限制在 padding 內,能夠用 margin 負邊距或者 transform 等改變位置;

4.1 回到頂部按鈕

父元素有 padding: 10px 20px;,子元素設置 position: sticky; bottom: 0; left: 100%; ,可是會被限制在 padding 的範圍內,原來是使用 bottom: 0; rihgt: 0; 的,可是 right: 0; 不起做用。。。因此用 margin-right: -10px 修改一下位置

CSS.supports('position', 'sticky') 能夠判斷瀏覽器是否支持 position: sticky;

<TopBtn /> 樣式:

// src/components/scrollToTop/scrollToTop.scss
.top-btn {
  width: 50px;
  height: 50px;
  display: flex;
  justify-content: center;
  align-items: center;
  bottom: 20px;
  border-radius: 100px;
  border: 1px solid #eee;
  background: #fff;
  box-shadow: 0 2px 10px -1px rgba(0, 0, 0, 0.1);
  z-index: 9;
  &:active {
    background: #eee;
  }
}
.top-btn-fixed {
  position: fixed;
  right: 20px;
  @extend .top-btn;
}

.top-btn-sticky {
  position: sticky;
  left: 100%;
  margin-right: -10px;
  @extend .top-btn;
}
複製代碼

<TopBtn /> 組件:

// src/components/scrollToTop/index.tsx
import * as React from 'react';
import styles from './scrollToTop.scss';

const { useState, useEffect } = React;

/** * scrollToTop */
function scrollToTop() {
  const [showBtn, setShowBtn] = useState(false);

  useEffect(() => {
    const height = window.innerHeight;

    // 滾動距離大於一屏高度則顯示,不然隱藏
    setShowBtn(() => (
      document.body.scrollTop >= height
      || document.documentElement.scrollTop >= height
    ));
  }, [document.body.scrollTop, document.documentElement.scrollTop]);

  function toTop() {
    if (window.scroll) {
      window.scroll({ top: 0, left: 0, behavior: 'smooth' });
      
    } else {
      document.body.scrollTop = 0;
      document.documentElement.scrollTop = 0;
    }
  }

  return (
    <div className={ CSS.supports('position', 'sticky') ? styles['top-btn-sticky'] : styles['top-btn-fixed'] } style={{visibility: showBtn ? 'visible' : 'hidden'}} onClick={toTop} > <i className="iconfont icon-arrow-upward-outline" /> </div> ); } export default scrollToTop; 複製代碼

最後

其餘的沒什麼,項目自己也不復雜;框架用的是以前搭的 React+Typescript+antd-mobile,axios/css-modules/sass 等等這些都是標配啦;東西很少,原來的是 antd 這個是移動端因此換成 antd-mobile

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息