記一次preact遷移到react16.6.7的經歷

lhyt發表於2019-03-01

0. 前言

preact作為備胎,但是具有體積小,diff演算法優化過的特點,簡單活動頁用上它是不錯的選擇。但是考慮到react令人興奮的新特性,preact並沒有按時更新去完全支援它,更嚴重的是一些babel外掛、一些庫配合preact會有問題。所以,還是不得不遷移了。

如何遷移?package.json直接修改版本,刪掉preact,重灌,完事!

too young

1. 從alias改起

首先,一般是這樣子接入preact的,使得我們程式碼裡面毫無感覺我們用的是preact。在webpack的alias裡面配置:

  alias: {
    react: `preact-compat`,
    `react-dom`: `preact-compat`
  },
複製程式碼

所以,第一步先把這個去掉

2. 語法上

  1. preact的元素陣列可以不寫key,切換回來必然警告很多,需要把key補上
render() {
    return (
      [
        <div key="container">2</div>,
        <div key="more">1</div>,
      ]
    );
  }
複製程式碼
  1. 元素的style可以寫字串,轉回react是報錯,導致頁面白屏
<div style={`background: url(${this.props.h5img});`} />
複製程式碼
  1. will這些不安全的生命週期,需要手動修改
  2. state必須初始化,不能直接想有this.state.xx就有。必須保證後面用到this.state之前,對state有初始化,否則是null

3. preact相關的router遷移回react生態

首先,import的preact-router得換成react-router。然後,將對應的語法和生態遷移到react相關的。

preact-router:

          <Router history={history}>
              <Main path="/" history={history} />
              <GetGiftForm path="/get_gift_form" history={history} />
              <Join path="/join" history={history} />
              <NewUser path="/new_user" history={history} />
          </Router>
複製程式碼

react-router:

          <Router history={history}>
            <div>
              <Route exact path="/" history={history} component={Main} />
              <Route path="/get_gift_form" history={history} component={GetGiftForm} />
              <Route path="/join" history={history} component={Join} />
              <Route path="/new_user" history={history} component={NewUser} />
            </div>
          </Router>
複製程式碼

preact-router有一個route方法,就是直接將路由push或者replace的,而react-router是沒有這個方法的。實際上底層就是封裝history路由加上內部的setstate:

import { route } from `preact-router`;
route(`/a`);
複製程式碼

問題來了,如果沒有這個方法,想用指令碼跳轉路由怎麼辦?直接history上改,只能改位址列的url顯示但不能更新元件以及內部狀態。所以我們只能找和react-router配合起來用的相關的庫。

觀察一下,都用到了history屬性,傳入一個history,這個是用了一個history的庫建立的,所以我們儘可能的讓它接入兩種路由得到一樣的效果:

import createHashHistory from `history/createHashHistory`;
const history = createHashHistory();
複製程式碼

列印了history,發現它有push、replace屬性,大概也猜到應該就是像route的效果的,一驗證發現可行:

history.push(`/a`);
複製程式碼

另外,還有preact-router的路由更新監聽是這樣的:

        <Router history={history} onChange={this.handleRoute}>
             ......
        </Router>
複製程式碼

切換到react的話,沒有這個方法。於是我們繼續找history,發現有一個listen屬性,看名字是一個監聽函式,也就是可以實現路由更新監聽的:

    history.listen((location) => {
      ...
    });
複製程式碼

4. 非同步路由

用preact-router的時候,有些元件是非同步的:

        <Router history={history}>
            { trackRoute() }
        </Router>
複製程式碼

trackRoute函式元件:

import React from `react`;
import AsyncRoute from `preact-async-route`;
export default () => {
  return (
    <AsyncRoute
      path="/track"
      getComponent={async () => {
        return import(`./comp` /* webpackChunkName: `async_track` */);
      }}
    />
  );
};
複製程式碼

效果就是,動態import,程式碼分割。react也有一個類似的,react-async-router,但是用法和我們的之前的preact-async-route差得遠而且不能優雅接入。既然是16.6.7了,我們可以試一下新特性:lazy+suspence

import React, { lazy, Suspense } from `react`;
const Comp = lazy(() => import(`./comp` /* webpackChunkName: `async_track` */));
function TrackRoute() {
  return (
    <Suspense fallback={<div />}>
      <Comp />
    </Suspense>
  );
}
export default TrackRoute;
複製程式碼

5. 內部實現原理不一樣的相容

有一個頁面是這樣的:

// Main.jsx
render() {
    return (
      <div>
        <Page1 />
        <Page2 />
        <Page3 />
         ...
      </div>
    );
  }
複製程式碼

除了page1是原來就在的,其他每一個Pagex元件,返回Page元件,在Page內部,當頁碼是當前頁返回對應的元素,否則返回空:

// Pagex
render() {
    return (
        <Page />
    );
  }

// Page
render() {
    return currentPage === page ? <somedom> : null
  }
複製程式碼

這裡,我們可以猜一下,Main是最大的元件,內部狀態頁碼在切換,所有的Pagex元件跟著更新,做出對應的變化。Pagex的更新,走的是didupdate。

實際上,preact的是第一個內部是Page實現的Pagex元件會unmount然後重新didmount。這裡是Page2先解除安裝再掛載,交換位置page1直接到page3的話也是page3先解除安裝再掛載。一些動畫操作就放在了didmount,之前都是這樣做的,但大家沒有發現是什麼問題,因為看見是這樣,開發起來沒毛病,又沒有bug,就不太在意。切換回react,發現動畫不生效,才發現因為內部渲染機制不一樣導致的。所以我們把函式的呼叫放在didupdate裡面,並且加上執行過一次的標記判斷。

6. 減少無必要的函式執行

getSnapshotBeforeUpdate

做一個像qq聊天起跑那樣的東西,頭和腳固定,中間無限長。這裡要求視覺給3個圖,頭、腳、中間1px高的圖。如果內容不滿屏,不顯示腳不能滾動,如果大於1屏顯示腳。

image

這裡當然少不了原生dom操作了,需要判斷高度,有沒有大於1屏,要不要overflow:hidden:

  componentDidUpdate() {
    if (this.state.hasFooter) {
      return;
    }
     const last = document.querySelector(`.act-card:last-of-type`);
    if (last) {
      const { top } = last.getBoundingClientRect();
      if (SCREEN_HEIGHT - top < MAX_BOTTOM_DISTANCE) {
        setTimeout(() => {
          this.setState({ hasFooter: true }); // eslint:不能在didupdate裡面setstate
        });
        document.body.style.overflow = `auto`;
      } else {
        document.body.style.overflow = `hidden`;
      }
    }
  }
複製程式碼

這裡需要執行兩次下面一大塊邏輯,第二次是無必要的,我們可以利用getSnapshotBeforeUpdate生命週期配合didupdate使用:

  getSnapshotBeforeUpdate(_, prevstate) {
    if (!prevstate.hasFooter && prevstate.actCards.length) {
      return true;
    }
    return null;
  }

  componentDidUpdate(_, __, snapshot) {
    if (!snapshot) {
      return;
    }
    ......
  }
複製程式碼

memo

可以說函式式元件的purecomponent,而且第二個引數能傳入第二個類似shouldComponentUpdate的函式進行比較。既然能自定義化,那麼對比於purecomponent的自帶物件淺比較就是更加的靈活了,比如:

import React, { memo } from `react`;
export default memo((props) => {
  const isNoAct = !props.actCards.length || props.loading;
  return (
    <section className="body-container">
      <div>
        {
          !isNoAct ? props.actCards.map((actCard, index) => (
            <div
              key={index}
            >
              ......          
            </div>
          ))
            : (
              <div className="no">
              暫無
              </div>
            )
        }
      </div>
      {props.hasFooter && <div className="body-footer" src={footer} alt="err" />}
    </section>
  );
}, (prevprops, nextprops) => {
  // 少渲染一次,一開始actcards什麼都沒有,我們不比較actcards陣列
  if (prevprops.hasFooter !== nextprops.hasFooter
    || prevprops.loading !== nextprops.loading) {
    return false;
  }
  return true;
});
複製程式碼

這裡我們就少了一次從actcards陣列的undefined[]的過程的比較,而這時候一直是loading狀態,沒有更新的意義。如果這裡return之前又是有像前面那個聊天氣泡那種效果需要dom操作的,那就傷效能了。這裡我列舉出來的只是把程式碼刪減過的簡單結果,實際上開發的時候邏輯是遠遠比這demo複雜的

相關文章