记一次preact迁移到react16.6.7的经历

0. 前言

preact做为备胎,可是具备体积小,diff算法优化过的特色,简单活动页用上它是不错的选择。可是考虑到react使人兴奋的新特性,preact并无按时更新去彻底支持它,更严重的是一些babel插件、一些库配合preact会有问题。因此,仍是不得不迁移了。javascript

如何迁移?package.json直接修改版本,删掉preact,重装,完事!java

too youngreact

1. 从alias改起

首先,通常是这样子接入preact的,使得咱们代码里面毫无感受咱们用的是preact。在webpack的alias里面配置:webpack

alias: {
    react: 'preact-compat',
    'react-dom': 'preact-compat'
  },
复制代码

因此,第一步先把这个去掉web

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:npm

<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:json

<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复杂的