Рендеринг на стороне сервера React16+Redux+Router4+Koa+Webpack (загрузка по запросу, горячее обновление)

React.js koa Webpack Redux

Схема структуры проекта

Основные конструктивные идеи этого проекта:

  1. Среда разработки использует webpack-dev-server в качестве внутреннего сервера для обеспечения горячего обновления без обновления страницы, включая горячее обновление изменений компонентов и редюсеров.
  2. Рабочая среда использует koa в качестве внутреннего сервера, совместно использует код createApp с внешним интерфейсом и получает метод createApp, читая файл после упаковки, а затем разделяет код по мере необходимости с помощью react-loadable, запрашивает исходные данные. перед рендерингом и вставляет его на домашнюю страницу.

Адрес на гитхабе: GitHub.com/my2010/Re AC…

структура кода

Внешний интерфейс использует react+redux+router4, а redux-thunk используется для обработки асинхронных действий. Внешняя и внутренняя части совместно используют configureStore и createApp, а также конфигурацию маршрутизации внешнего интерфейса, необходимые для внутренней части, поэтому они представлены в одном файле.

export default {
  configureStore,
  createApp,
  routesConfig
}
где configureStore.js:
import {createStore, applyMiddleware,compose} from "redux";
import thunkMiddleware from "redux-thunk";
import createHistory from 'history/createMemoryHistory';
import {  routerReducer, routerMiddleware } from 'react-router-redux'
import rootReducer from '../store/reducers/index.js';

const routerReducers=routerMiddleware(createHistory());//路由
const composeEnhancers = process.env.NODE_ENV=='development'?window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose;

const middleware=[thunkMiddleware,routerReducers];

let configureStore=(initialState)=>createStore(rootReducer,initialState,composeEnhancers(applyMiddleware(...middleware)));

export default configureStore;

Среди них я поставил роутер в редуктор

const routerReducers=routerMiddleware(createHistory());//路由
const middleware=[thunkMiddleware,routerReducers];

Таким образом, информация о маршрутизаторе может быть напрямую прочитана в редюсере без необходимости передавать ее от компонента слой за слоем.

createApp.js
import React from 'react';
import {Provider} from 'react-redux';
import Routers from './router/index';
import Loadable from 'react-loadable';

const createApp=({store,history,modules})=>{
  if(process.env.NODE_ENV==='production'){
    return (
      <Loadable.Capture report={moduleName => modules.push(moduleName)}>
        <Provider store={store}>
          <Routers history={history} />
        </Provider>
      </Loadable.Capture>
    )
  }else{
    return (
      <Provider store={store}>
        <Routers history={history} />
      </Provider>
    )
  }
}

export default createApp;

История, используемая внешним интерфейсом:

import createHistory from 'history/createBrowserHistory';
let history=createHistory();

История, используемая серверной частью:

import createHistory from 'history/createMemoryHistory';
let history=createHistory();

Горячая загрузка версии для разработчиков

if(process.env.NODE_ENV==='development'){
  if(module.hot){
    module.hot.accept('./store/reducers/index.js',()=>{
      let newReducer=require('./store/reducers/index.js');
      store.replaceReducer(newReducer)
      /*import('./store/reducers/index.js').then(({default:module})=>{
        store.replaceReducer(module)
      })*/
    })
    module.hot.accept('./app/index.js',()=>{
      let {createApp}=require('./app/index.js');
      let newReducer=require('./store/reducers/index.js');
      store.replaceReducer(newReducer)
      let application=createApp({store,history});
      hydrate(application,document.getElementById('root'));
      /*import('./app/index.js').then(({default:module})=>{
        let {createApp}=module;
        import('./store/reducers/index.js').then(({default:module})=>{
          store.replaceReducer(module)
          let application=createApp({store,history});
          render(application,document.getElementById('root'));
        })
      })*/
    })
  }
}

К ним относятся горячее обновление компонентов и горячее обновление редуктора, а при добавлении измененных файлов можно использовать требование или импорт.

Генерация внешнего узла dom

const renderApp=()=>{
  let application=createApp({store,history});
  hydrate(application,document.getElementById('root'));
}

window.main = () => {
  Loadable.preloadReady().then(() => {
    renderApp()
  });
};

Среди них Loadable.preloadReady() — это метод записи «реагирующей загрузки» загрузки по запросу, который также используется во время рендеринга сервера.

router4 динамически загружается по запросу

Этот проект использует react-loadable для реализации загрузки по требованию.

const Loading=(props)=>
  <div>Loading...</div>

const LoadableHome = Loadable({
  loader: () =>import(/* webpackChunkName: 'Home' */'../../containers/Home'),
  loading: Loading,
});
const LoadableUser = Loadable({
  loader: () =>import(/* webpackChunkName: 'User' */'../../containers/User'),
  loading: Loading,
});

const routesConfig=[{
  path: '/',
  exact: true,
  component: LoadableHome,
  thunk: homeThunk,
}, {
  path: '/user',
  component: LoadableUser,
  thunk: ()=>{},
}];

Его можно использовать не только для маршрутизации, но и для динамического импорта() компонента в компонент для динамической загрузки компонентов по требованию.thunk: homeThunkЭто обработка действий при переходе маршрута, потому что первая возможность заключается в том, что перед входом на домашнюю страницу серверу необходимо запросить начальные данные домашней страницы, а затем отобразить их во внешнем интерфейсе, а вторая возможность заключается в том, что сервер заходит на страницу пользователя.При переходе страницы на домашнюю ей также необходимо запросить начальные данные.В это время они запрашиваются при использовании внешнего компонента ComponentDidMount.Поэтому для того,чтобы сделать этот метод общедоступным, он запрашивается при использовании маршрута перехода, будь то с внешней ссылки или с сервера.

export const homeThunk=store=>store.dispatch(getHomeInfo())
//模拟动态请求数据
export const getHomeInfo=()=>async(dispatch,getState)=>{
  let {name,age}=getState().homeInfo;
  if(name || age)return
  await new Promise(resolve=>{
    let homeInfo={name:'wd2010',age:'25'}
    console.log('-----------请求getHomeInfo')
    setTimeout(()=>resolve(homeInfo),1000)
  }).then(homeInfo=>{
    dispatch({type:GET_HOME_INFO,data:homeInfo})
  })
}

И серверная часть черезreact-router-configизmatchRoutesчтобы соответствовать текущему URL-адресу и маршруту routeConfig

let branch=matchRoutes(routesConfig,ctx.req.url)
let promises = branch.map(({route,match})=>{
    return route.thunk?(route.thunk(store)):Promise.resolve(null)
  });
await Promise.all(promises)

коа визуализировать renderToString

Строка rootString, необходимая для внешней HTML-страницы, отображается с помощью метода renderToString через объекты createApp, configureStore и routeConfig, предоставляемые интерфейсной частью. В сочетании с загрузкой по требованию есть:

let store=configureStore();
let history=createHistory({initialEntries:[ctx.req.url]});
let rootString= renderToString(createApp({store,history,modules}));

Используйте react-loadable, когда файл записи сервера koa прослушивает порт:

Loadable.preloadAll().then(() => {
  app.listen(port)
})

Таким образом, рендеринг бэкенда koa может динамически загружаться по запросу.

В динамически сгенерированном html нет User.js:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>yyy</title>
  <link href="/css/style.7dae77f648cd2652a570.css" rel="stylesheet"></head>
  <body>
    <div id="root"></div>
    <script type="text/javascript" src="/manifest.7dae77f6.js"></script>
    <script type="text/javascript" src="/vendors.7dae77f6.js"></script>
    <script type="text/javascript" src="/client.7dae77f6.js"></script>
  </body>
  <script>window.main()</script>
</html>

При каждом обновлении localhost уже содержит весь контент в верхней части страницы, решая проблемы с белым экраном в верхней части страницы и SEO-поиском.

Эпилог

После выполнения этого упражнения я подумал, после компиляции кода будет короткий белый экран при запросе данных, требуемых первым экраном перед рендерингом сервера.На данный момент проблема белого экрана не решена , так можно ли скомпилировать код?А как насчет запроса всех данных, которые нужны домашней странице? Я также думаю, что процесс компиляции в это время занимает много времени, и запрашиваются данные, которые можно пропустить во фронтенд-роутинге. Все проблемы с белым экраном в верхней части экрана кажутся решенными, но на самом деле есть решения получше.

Так как я впервые делаю рендеринг сервера React, многие места сделаны со ссылкой на практики великих богов, и есть еще много вещей, которые я не понимаю. Пожалуйста, дайте мне больше указателей. Полный код вGitHub.com/my2010/Re AC…