項目總結 20171226

clipboard.png

1. 藉助require.context預加載圖片

詳情文檔地址css

項目中爲了確保頁面顯示時,圖片已經所有加載完畢,所以須要提早加載圖片,加載圖片的過程使用進度條顯示。html

在webpack構建的項目中,可使用require.context來獲取到靜態資源的地址。語法以下:node

require.context(directory, useSubdirectories = false, regExp = /^\.\//)

第一個參數表示要搜索的文件夾目錄,該目錄支持相對路徑與在配置文件中定義的路徑別名。
第二個參數表示是否搜索其子目錄。
第三個參數是一個用來匹配文件的正則表達式。react

require.context('modules/App', true, /\.(png|jpg|jpeg|gif)$/);
// 建立一個包含App目錄下全部圖片的上下文模塊

可使用該上下文模塊自帶的keys方法獲得路徑組成的數組。webpack

const images = require.context('modules/App/', true, /\.(png|jpeg|jpg|gif)$/);
console.log(images.keys());

效果大概以下圖所示。git

clipboard.png

獲得圖片路徑以後,就能夠藉助Promise.all來完成圖片預加載,確保圖片加載完成以後再渲染頁面。github

Promise.all(images.keys().map(path => {
  const image = new Image();
  image.src = path;
  image.onload = image.onerror = () => {
    resolve();
  }
}))

可是在開發中遇到一個問題,本地頁面引用的圖片是編譯事後的圖片地址,並非相對路徑,所以若是直接這樣的話會所以地址不一致而報錯。web

clipboard.png

解決辦法是在設置image對象src屬性時,修改以下:正則表達式

image.src = images(path);
// images 是由require.context 建立的上下文模塊

打印出images(path)以後的圖片路徑以下:api

clipboard.png

上面的修復方式可使用以下的知識點來理解。

const ctx = require.context('modules/App', true, /*\.js/);
const table = ctx('./table.js');

// 上面的代碼等價於
const table = require('modules/App/table.js'); // 使用require引入模塊

參考文檔

當還須要從服務端提早加載其餘資源時,可使用數組的concat方法一塊兒放入Promise.all中。

Promise.all(images.keys().map(
  // ...
).concat(http.get('/api/v1/summary')))

2. 細節優化

整個頁面的顯示,一共有15頁構成,因爲每一頁的邏輯與效果都有很多差別,所以將每一頁定義爲了一個組件,最初在引入這些模塊時很糟糕的這樣作:

import Page00 from './Page00';
import Page01 from './Page01';
import Page02 from './Page02';
import Page03 from './Page03';
import Page04 from './Page04';
import Page05 from './Page05';
import Page06 from './Page06';
import Page07 from './Page07';
import Page08 from './Page08';
import Page09 from './Page09';
import Page10 from './Page10';
import Page11 from './Page11';
import Page12 from './Page12';
import Page13 from './Page13';
import Page14 from './Page14';

// render裏也很複雜

// ...
render() {
  return (
      <Fragment>
        <Page00 />
        <Page01 />
        <Page02 />
        ...
         <Page14 />
      </Fragment>
  )
}

當組件更多時,這樣的引入方式天然是不合理的,可使用循環的方式來引入代碼,優化以下:

const allPages = [];

for(let i = 0; i < 15; i++) {
  const id = `0${i}`.slice(-2);
  allPages.push(require(`./Page${id}`).default)
}

這樣就將全部的Page組件放在了allPages數組中。

render裏也可使用map來渲染。

render() {
  return (
    <div className="pages">
      {allPages.map(({ id, Component: Page }) => <Page key={id} {...other} />)}
    </div>
  )
}

3. 使用高階組件處理公共邏輯

每個Page組件中,都有共同的元素或邏輯,包括logo,分享當前屏幕截圖按鈕,統計邏輯,判斷對應頁面是否顯示等。能夠將這些共用邏輯使用高階組件來處理以簡化代碼。

所以定義了withBox組件來處理它們。

import React from 'react';
import logo from './images/logo.png';
import { sendEvent } from 'utils/track';
import share from './share';

export default function(Wrapped, checkProp) {
  return class NewPage extends React.Component {
    shareScreen = () => {
      const id = this.refs.box.getAttribute('data-page-id');
      this.refs.box.classList.add('will-screenshot');
      setTimeout(() => share.shareScreenshot(), 100);
      setTimeout(() => this.refs.box.classList.remove('will-screenshot'), 1500);

      sendEvent('share-click', 'page' + id);
      sendEvent('click', 'share-btn');
    };

    render() {
      const { id, className, ...props } = this.props;
      const cls = className ? `page${id} ${className}` : `page${id}`;

      if (!checkProp || (props.info[checkProp] !== null && props.info[checkProp] !== 'undefined')) {
        return (
          <section className={cls} data-page-id={id} ref="box">
            <Wrapped {...props} />
            <button className="share-btn aninode fadeIn" onClick={this.shareScreen} />
               <img className="logo aninode fadeIn" src={logo} alt="tigerbrokers" />
          </section>
        );
      }
      return null;
    }
  };
}

4. 經過添加/刪除元素的方式統一控制動畫

首先定義一個class以下,將會參與動畫的元素(或其父級)都添加該class以隱藏。

.aninode {
  visibility: hidden;
}

並在同元素(或父級)添加了animated時,元素顯示。

.animated {
  &.aninode, .aninode {
    visibility: visible;
  }
}

並在運動元素的class中添加了animated時,運動生效,所以定義運動css時,應該這樣作:

.animated {
  &.flyTopIn, .flyTopIn {
    animation-name: flyTopIn;
    animation-duration: 1s;
  }
  /* more */
}

所以,運動元素在運動開始以前,應該保持這樣

<div class="test aninode flyTopIn"></div>

須要運動時,在該元素的class中添加animated便可。

<div class="test aninode flyTopIn animated"></div>

// or

<div class="animated">
  <div class="test aninode flyTopIn"></div>
</div>

使用sass的循環語法定義delay樣式

@for $i from 0 through $delay_count {
  .animated .delay#{$i * 100} {
    animation-delay: $i * 100;
    animation-fill-mode: backwards;
  }
}

具體實現可參考

5. 小數精度問題致使的bug

js的計算中,常常會遇到小數精度的問題,最初沒有注意,致使數據顯示出了不少問題。例如以下計算結果

1.099 * 100
109.89999999999999

解決方法以下:

(1.099 * 100).toFixed(2)

clipboard.png

6. Promise與setTimeout的寫法問題

利用setTimeout判斷某個對象是否注入成功。

// 錯誤寫法
export const checkSDK = () => {
    var timer = null;
    const start = Date.now();

    return new Promise((resolve, reject) => {
        if (typeof window.TigerBridge === 'object') {
            resolve();
            return;
        }
        if (Date.now() - start <= 5 * 1000) {
            clearTimeout(timer);
            timer = setTimeout(checkSDK, 100);
            return;
        }

        reject();
    })
}


// 正確寫法

export const checkBridge = () => {
    var timer = null;
    const start = Date.now();

    function check(resolve, reject) {
        if (typeof window.TigerBridge === 'object') {
            resolve();
            return true;
        } 
        if (Date.now() - start <= 5 * 1000) {
            clearTimeout(timer);
            timer = setTimeout(check.bind(null, resolve, reject), 100);
            return;
        }
        reject();
        return false;
    }

    return new Promise((resolve, reject) => check(resolve, reject))
}

本地模擬注入過程

if (process.env.NODE_ENV != 'production') {
    setTimeout(() => {
        window.TigerBridge = {
            getAccessToken: () => {
                return pkg.token;
            },
            isAccountPermissionLimited: () => false
        };
    }, 1600);
}

7. 圖片串行加載優化

一次性加載全部圖片會致使瀏覽器http線程阻塞嚴重。所以須要稍做優化,讓圖片一張一張加載。

// 優化前
images.keys().map(path => new Promise(resolve => {
  const image = new Image();
  image.src = images(path);
  image.onload = image.onerror = resolve;
}))

// 優化後
images.keys().reduce((cachePromise, path) => cachePromise.then(() => {
  return new Promise(resolve => {
    const image = new Image();
    const complete = () => {
      clearTimeout(timer);
      resolve();
    }
    const timer = setTimeout(complete, 1000);  // 單張圖片最多加載1s
    image.src = images(path);
    image.onload = image.onerror = complete;
  })
}), Promise.resolve());
相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息