欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

React+Redux项目中的代码分割

程序员文章站 2022-05-02 16:51:31
react+redux 项目中的代码分割 准备一个基本项目 直接使用 create-react-app 初始化一个 react 项目,安装 react-router 和 redux。 接着创建两个路...

react+redux 项目中的代码分割

准备一个基本项目

直接使用 create-react-app 初始化一个 react 项目,安装 react-router 和 redux。

接着创建两个路由页面,一个主页和一个登录页面,主页中有一个按钮可以跳转到登录页面。

// index.js
import react from 'react';
import reactdom from 'react-dom';

import routerconfig from './router';

reactdom.render(, document.getelementbyid('root'));

// router.js
import react from 'react';
import { browserrouter as router, switch, route } from 'react-router-dom';

import home from './route/home';
import login from './route/login';

function routerconfig() {
  return (
      
        
          
          
        
      
  );
}

export default routerconfig;

// route/home.js
import react from 'react';
import { link } from 'react-router-dom';

export default () => {
  return (

主页

登录页面 ); } // route/login.js import react from 'react'; export default () => { return (登录页面 ); }

此时执行 yarn build 操作,主页和登录页面的代码会打包到一个文件中。

 

main_js.png

分割路由页面

通过动态导入路由分割路由页面从而实现按需加载,webpack 内置了该功能。此外,还需要使用 @babel/syntax-dynamic-import 插件提供语法支持,对应的 babel 配置如下:

{
  "presets": [
    "react-app"
  ],
  "plugins": [
    "@babel/syntax-dynamic-import"
  ]
}

调整 router.js 代码,动态导入页面组件。

const home = () => import('./route/home');
const login = () => import('./route/login');

此时执行 yarn build 可以发现已经将代码进行了分割。但是页面无法正常访问了,因为动态导入是一个异步操作,它返回的并不是一个组件而是 promise。

我们可以创建一个叫做 asynccomponent 的组件,它接受动态导入的返回值作为属性,在内部会判断组件是否加载完毕,如果加载完毕则渲染组件,否则显示 loading。

// component/asynccomponent.js

import react, { component } from 'react';

export default (loader) => {
  class asynccomponent extends component {
    state = {
      component: null,
    };

    componentwillmount() {
      if (!this.state.component) {
        loader().then(module => {
          this.setstate({ component: module.default });
        });
      }
    }

    render() {
      const comp = this.state.component;
      return comp   :

loading

; } } return asynccomponent; } // router.js const home = asynccomponent(() => import('./route/home')); const login = asynccomponent(() => import('./route/login')); function routerconfig() { return ( ); } export default routerconfig;

使用 react-loadable

我们大可不必自己实现 asynccomponent,react-loadable 就是一个不错的选择。

import react from 'react';
import { browserrouter as router, switch, route } from 'react-router-dom';
import loadable from 'react-loadable';

const loadingcomp = () => loading;

const home = loadable({
  loader: () => import('./route/home'),
  loading: loadingcomp,
});
const login = loadable({
  loader: () => import('./route/login'),
  loading: loadingcomp,
});

function routerconfig() {
  return (
      
        
          
          
        
      
  );
}

export default routerconfig;

使用 react16.6 新特性

react 新发布的 16.6 版本提供了 lazy 方法和配套的 suspense 组件,用来处理异步渲染场景。通过新特性重新实现 asynccomponent:

import react, { lazy, suspense } from 'react';

export default (loader) => {
  const othercomponent = lazy(loader);

  const component = () => {
    return (
        loading...}>
          
        
    );
  };

  return component;
}

分割 redux 模块

这一部分主要参考 twitter 的做法。

一个 redux 模块包含 reducers、actions、action creators、state selectors。我们来创建一个 user redux 模块。

// model/user.js

const reducername = 'user';

const initialstate = {
  login: false,
  profile: {
    name: 'tom',
  },
};

const createactionname = name => `app/${reducername}/${name}`;

// actions
export const login_success = createactionname('login_success');
export const update_profile = createactionname('update_profile');

// action creators
export const updateprofile = payload => ({ payload, type: update_profile });

// reducer
export default function reducer(state = initialstate, action = {}) {
  switch (action.type) {
    case login_success:
      return { ...state, login: true };
    case update_profile:
      return { ...state, profile: action.payload };
    default:
      return state;
  }
}

// selectors
export const selectloginstate = state => state.login;
export const selectprofile = state => state.profile;

然后我们把 user model 和登录页面 connect 连接起来:

import react from 'react';
import { connect } from 'react-redux';

const login = ({ name }) => {
  return (
      

{`登录页面, name: ${name}`}

); }; const mapstatetoprops = ({ user }) => ({ name: user.profile.name, }); export default connect(mapstatetoprops)(login);

到目前为止,我们将登录页面和登录页面所需要用到的状态管理相关代码(user redux moudle)整合到了一次,这对于代码分割非常有利。

但是当我们将 reducer 添加到 redux store 时,就不是那么美好了。model 相关代码在一开始就会被加载进来,即使目前还没有页面会用到它。

运行时注册 reducer

通过 replacereducer 函数,我们可以在访问相应的页面时再添加 reducer 到 redux store 中。

实现 reducerregistry 并修改 createstore 的代码:

// model/reducerregistry
export class reducerregistry {
  constructor() {
    this._emitchange = null;
    this._reducers = {};
  }

  getreducers() {
    return { ...this._reducers };
  }

  register(name, reducer) {
    this._reducers = { ...this._reducers, [name]: reducer };
    if (this._emitchange) {
      this._emitchange(this.getreducers());
    }
  }

  setchangelistener(listener) {
    this._emitchange = listener;
  }
}

const reducerregistry = new reducerregistry();
export default reducerregistry;

// model/createstore
import { combinereducers, createstore } from 'redux';
import reducerregistry from './reducerregistry';

const initialstate = {};

const combine = (reducers) => {
  const reducernames = object.keys(reducers);
  // 维持仍为加载的 reducer 的初始化状态
  object.keys(initialstate).foreach(item => {
    if (reducernames.indexof(item) === -1) {
      reducers[item] = (state = null) => state;
    }
  });
  return combinereducers(reducers);
};

const reducer = combine(reducerregistry.getreducers());
const store = createstore(reducer, initialstate);

// 当一个新的 reducer 注册时,替换 store 的 reducer
reducerregistry.setchangelistener(reducers => {
  store.replacereducer(combine(reducers));
});

export default store;

现在,我们已经可以动态加载 reducer 了。接着,我们只需要在导入页面的同时注册相应的的 reducer 即可。

导入页面和模型

我们实现一个 dynamicwrapper,接受一个页面 loader 函数和模型名称数组。默认模型都在 model 目录下,且文件名和模型相对应。

import react, { lazy, suspense } from 'react';
import reducerregistry from './model/reducerregistry';

const asynccomponent = (loader) => {
  const othercomponent = lazy(loader);

  const component = () => {
    return (
        loading...}>
          
        
    );
  };

  return component;
};

export default (comploader, modelnames) => {
  const loader = () => {
    let modelloader;
    if (modelnames) {
      modelloader = () => modelnames.map((name) => import(`./model/${name}`));
    }

    const component = comploader();
    const models = modelloader  modelloader() : [];
    return promise.all([...models, component]).then(ret => {
      if (!models || !models.length) {
        return ret[0];
      } else {
        const len = models.length;
        ret.slice(0, len).foreach((m, index) => {
          m = m.default || m;
          reducerregistry.register(modelnames[index], m);
        });
        return ret[len];
      }
    });
  };

  return asynccomponent(loader);
}

使用起来也很简单,我们调整一下 router.js 中的代码:

const home = dynamicwrapper(() => import('./route/home'));
const login = dynamicwrapper(() => import('./route/login'), ['user']);

function routerconfig() {
  return (
      
  );
}

现在 model/user.js 并不会在一开始就加载,而是在访问登录页面时才会加载进来。