React+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 并不会在一开始就加载,而是在访问登录页面时才会加载进来。
上一篇: QQ影音调节声音字幕位置的方法
下一篇: 熊猫直播怎么设置自己的直播房间?