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. 語法上
- preact的元素陣列可以不寫key,切換回來必然警告很多,需要把key補上
render() {
return (
[
<div key="container">2</div>,
<div key="more">1</div>,
]
);
}
複製程式碼
- 元素的style可以寫字串,轉回react是報錯,導致頁面白屏
<div style={`background: url(${this.props.h5img});`} />
複製程式碼
- will這些不安全的生命週期,需要手動修改
- 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屏顯示腳。
這裡當然少不了原生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複雜的