服务器呈现的 React ExpressJS 前端泄露了用户的 Redux 存储数据

Server rendered React ExpressJS frontend leaking users' Redux store data

我有一个 ExpressJS 服务器有时会在初始呈现时呈现错误的用户数据。请参阅下面的(稍微简化的)版本。

问题是 index.ejs 文件经常在 reduxState...

中呈现错误的用户数据

我的困惑是因为我希望对 import { store } from 'routes.js' 的调用会根据每个用户的请求将商店覆盖为 {}。问题似乎是 store 正在成为网站上每个用户的组合商店。

如何确保每个用户只能看到他们在网站上的数据?

routes.js

// src/routes.js
import React from 'react';
import { createStore, applyMiddleware, compose } from "redux";
import routerConfig from "base/routes/routes";
import thunk from "redux-thunk";
import { rootReducer } from "base/reducers";

let initialState = {};

const store = createStore(
  rootReducer, initialState, compose(applyMiddleware(thunk))
);

const routes = routerConfig(store);

export {store};
export default routes;

server.js

import { store } from 'routes';

let getReduxPromise = (renderProps, request) => {
  let store = require('./routes/index.jsx').store
  let { query, params } = renderProps

  let comp = renderProps.components[renderProps.components.length - 1];

  let at = null;

  if (request && request.cookies && request.cookies.accessToken) {
    at = request.cookies.accessToken
  }

  if (comp.fetchData) {
    return comp.fetchData({ query, params, store, at }).then(response => {
      if (request) {
        if (request.cookies && request.cookies.accessToken && request.cookies.userInfo) {
          store.dispatch(
            actions.auth(request.cookies.userInfo),
            request.cookies.accessToken
          )
        } else {
          store.dispatch(actions.logout())
        }

      }
      return Promise.resolve({response, state: store.getState()})
    });
  } else {
    return Promise.resolve();
  }
}

app.get('*', (request, response) => {
  let htmlFilePath = path.resolve('build/index.html' );
  // let htmlFilePath = path.join(__dirname, '/build', 'index.html');
  let error = () => response.status(404).send('404 - Page not found');
  fs.readFile(htmlFilePath, 'utf8', (err, htmlData) => {
    if (err) {
      console.log('error 1')
      error();
    } else {
      match({routes, location: request.url}, (err, redirect, renderProps) => {
        if (err) {
          console.log('error 2')
          error();
        } else if (redirect) {
          return response.redirect(302, redirect.pathname + redirect.search)
        } else if (renderProps) {
          let parseUrl = request.url.split('/');

          if (request.url.startsWith('/')) {
            parseUrl = request.url.replace('/', '').split('/');
          }

          // User has a cookie, use this to help figure out where to send them.
          if (request.cookies.userInfo) {
            const userInfo = request.cookies.userInfo

            if (parseUrl[0] && parseUrl[0] === 'profile' && userInfo) {
              // Redirect the user to their proper profile.
              if (renderProps.params['id'].toString() !== userInfo.id.toString()) {
                parseUrl[1] = userInfo.id.toString();
                const url = '/' + parseUrl.join('/');
                return response.redirect(url);
              }
            }
          }

          getReduxPromise(renderProps, request).then((initialData) => {
            let generatedContent = initialData.response ? render(request, renderProps, initialData.response) : render(request, renderProps, {});

            const title = initialData.response.seo.title || '';
            const description = initialData.response.seo.description || '';

            var draft = [];

            const currentState =  initialData.state;

            if (currentState) {
              const reduxState = JSON.stringify(currentState, function(key, value) {
                if (typeof value === 'object' && value !== null) {
                  if (draft.indexOf(value) !== -1) {
                    // Circular reference found, discard key
                    return;
                  }
                  // Store value in our collection
                  draft.push(value);
                }
                return value;
              });
              draft = null;

              ejs.renderFile(
                path.resolve('./src/index.ejs' ),
                {
                  jsFile,
                  cssFile,
                  production,
                  generatedContent,
                  reduxState,
                  title,
                  description
                }, {},
                function(err, str) {
                  if (err) {
                    console.log('error 3')
                    console.log(err);
                  }
                  response.status(200).send(str);
                });
            } else {
              console.log('error 4')
              console.log(err)
              error();
            }

          }).catch(err => {
            console.log('error 5')
            console.log(err)
            error();
          });

        } else {
          console.log('error 6')
          console.log(err)
          error();
        }
      });
    }
  })
});

index.ejs

<!DOCTYPE html>
<html lang="en">
  <head>
    <title><%- title %></title>
    <meta name="description" content="<%- description %>"/>
    <link href="<%- cssFile %>" rel="stylesheet"/>
    <script type="text/javascript" charset="utf-8">
      window.__REDUX_STATE__ = <%- reduxState %>;
    </script>
  </head>
  <body>
    <div id="root"><%- generatedContent %></div>
    <script type="text/javascript" src="<%- jsFile %>" defer></script>
  </body>
</html>

示例 fetchData React 组件中的函数

ExamplePage.fetchData = function (options) {
  const { store, params, at } = options

  return Promise.all([
    store.dispatch(exampleAction(params.id, ACTION_TYPE, userAccessToken))
  ]).spread(() => {
    let data = {
      seo: {
        title: 'SEO Title',
        description: 'SEO Description'
      }
    }

    return Promise.resolve(data)
  })
}

定义在模块范围内的变量在整个运行环境中只有一份。这意味着每个 node.js 进程都有自己的副本,每个浏览器 tab/frame 都有自己的副本。但是,在每个选项卡或每个进程中,只有一个副本。这意味着您不能将您的商店定义为模块级常量,并且仍然为每个用​​户拥有一个新商店。你可以这样解决:

src/routes.js

import React from 'react';
import { createStore, applyMiddleware, compose } from "redux";
import routerConfig from "base/routes/routes";
import thunk from "redux-thunk";
import { rootReducer } from "base/reducers";

let initialState = {};

export function newUserEnv() {
  const store = createStore(
    rootReducer, initialState, compose(applyMiddleware(thunk))
  );

  const routes = routerConfig(store);
  return { store, routes };
}

server.js

import { newUserEnv } from 'routes';

let getReduxPromise = (renderProps, request) => {
  const { store } = newUserEnv();
  let { query, params } = renderProps
...

这会为每个请求创建一个新商店,并允许每个用户拥有自己的数据。请注意,如果您需要来自不同模块的相同商店,则需要传递它。你不能只是 import { newUserEnv } 因为它会创建一个新的。