React Server Side Rendering - 如何从服务器呈现:productId params传入?

时间:2016-02-11 12:01:08

标签: javascript express reactjs webpack react-router

我有一个在客户端和服务器上呈现的React应用程序(node和express)。如果有人在网址http://webaddress.com/products/1中输入内容,我会尝试让渲染工作正常。如果我输入并输入(或刷新页面),应用程序崩溃,因为它不知道如何获取URL中的1来解析并获得正确的产品。

如果我点击导航链接到products / 1的链接,应用程序就可以正常运行并显示正确的产品。

如何让React-Router从访问者在http://webaddress.com/products/1中输入的网址中获取:productsId(1 in / products / 1)参数?

这是我的server.js:

import express from 'express';
import http from 'http';

var PageNotFound = require('./js/components/PageNotFound.react');

import React from 'react';
import { renderToString } from 'react-dom/server';
import { match, RouterContext } from 'react-router';

import { routes } from './routes';

const app = express();

app.use(express.static('public'));

app.set('view engine', 'ejs');

/* We call match(), giving it the routes object defined above and the req.url, which contains the the url of the request. */

app.get('*', (req, res) => {
  // routes is our object of React routes defined above
  match({ routes, location: req.url }, (err, redirectLocation, props) => {
    if (err) {
      // something went badly wrong, so 500 with a message
      res.status(500).send(err.message);
    } else if (redirectLocation) {
      // we matched a ReactRouter redirect, so redirect from the server
      res.redirect(302, redirectLocation.pathname + redirectLocation.search);
    } else if (props) {
      // console.log("props on server.js: ", props);
      // if we got props, that means we found a valid component to render
      // for the given route. renderToString() from ReactDOM takes that RoutingContext
      // component and renders it with the properties required.
      const markup = renderToString(<RouterContext {...props} />);

      // render `index.ejs`, but pass in the markup we want it to display
      res.render('index', { markup })

    } else {
      // no route match, so 404. In a real app you might render a custom
      // 404 view here
      console.log("not found page");
      res.sendStatus(404);
      // respond with html page
       if (req.accepts('html')) {
         res.render('404', { url: req.url });
         return;
       }

       // respond with json
       if (req.accepts('json')) {
         res.send({ error: 'Not found' });
         return;
       }

       // default to plain-text. send()
       res.type('txt').send('Not found');
    }
  });
});

const server = http.createServer(app);

app.set('port', (process.env.PORT || 3000))

app.get('/', (req, res) => {
  var result = 'App is Running'
  res.send(result);
}).listen(app.get('port'), () => {
  console.log('App is running, server is listening on port', app.get('port'));
});

这里是routes.js文件:

import TopLevelContainerApp from './js/components/TopLevelContainerApp.react'
import Home from './js/components/Home.react';
import Product from './js/components/Product.react';

const routes = {
  path: '',
  component: SolidBroadheadApp,
  childRoutes: [
    {
      path: '/',
      component: Home
    },
    {
      path: '/products/:productId',
      component: Product
    }
}
export { routes };

这是客户端渲染js:

import React from 'react';
import ReactDOM from 'react-dom';
import { Router, browserHistory } from 'react-router';

import { routes } from './../routes';

ReactDOM.render(
  <Router routes={routes} history={browserHistory} />, document.getElementById('website-app')
);

这是ProductStore.js:

var AppDispatcher = require('../dispatcher/AppDispatcher');
var EventEmitter = require('events').EventEmitter;
var assign = require('object-assign');

var articles = null;
var links = null;
var product = null;

function setArticles(receivedArticles) {
  articles = receivedArticles;
  return articles;
}

function setLinks(receivedLinks) {
  links = receivedLinks;
  return links;
}

function setProduct(productId) {
  console.log("products store productId: ", productId);
  function filterById(obj) {
    return obj.id === productId;
  }

  var filteredArticlesArr = articles.filter(filterById);
  product = filteredArticlesArr[0];
  return product;
};

function emitChange() {
  ProductsStore.emit('change');
}

var ProductsStore = assign({}, EventEmitter.prototype, {

  addChangeListener: function(callback) {
    this.on('change', callback);
  },

  removeChangeListener: function(callback) {
    this.removeListener('change', callback);
  },

  getArticles: function() {
    return articles;
  },

  getLinks: function() {
    return links;
  },

  getProduct: function() {
    return product;
  }

});

function handleAction(action) {

  switch (action.type) {

    case 'received_products_articles':
    setArticles(action.articles);
    emitChange();
    break;

    case 'get_links':
    setLinks(action.articles);
    emitChange();
    break;

    case 'get_product':
    setProduct(action.productId);
    emitChange();
    break;
  }

}

ProductsStore.dispatchToken = AppDispatcher.register(handleAction);

module.exports = ProductsStore;

以下是呈现特定产品的Product组件:

var React = require('react');
var ProductArticle = require('./products-solid-components/ProductArticle.react');

var ProductsStore = require('./../stores/ProductsStore');

    // gets all the products 
    var ProductsArticlesWebUtilsAPI = require('./../../utils/ProductsArticlesWebUtilsAPI');
    ProductsArticlesWebUtilsAPI.initializeArticles();

var Product = React.createClass({

  getInitialState: function() {
    return {
      articles: ProductsStore.getArticles(),
      product: ProductsStore.getProduct()
    };
  },


  componentDidMount: function() {
    ProductsStore.addChangeListener(this.onProductChange);
  },

  componentWillUnmount: function() {
    ProductsStore.removeChangeListener(this.onProductChange);
  },

  onProductChange: function() {
    this.setState({
      articles: ProductsStore.getArticles(),
      product: ProductsStore.getProduct()
    });
  },

  render: function() {

    if (this.state.articles)
    if (this.state.product) {

      return (
        <div className="product">
          <section className="container-fluid">
            <ProductArticle name={this.state.product.name} largeImage={this.state.product.largeImage} description={this.state.product.description} />
          </section>
      );

    } else {
      return (
        <div>Product is on the way</div>
      );
    }


  }
});

module.exports = Product;

这是获取信息的文件:

var ProductsActionCreators = require('../js/actions/ProductsActionCreators');

var productArticles = [
  {
    "id": 1,
    "name": "product 1",
    "largeImage": "1.png",
    "largeVideo": "1.mp4",
    "description": "1 description",
  },
  {
    "id": 2,
    "name": "product 2",
    "largeImage": "2.png",
    "largeVideo": "2.mp4",
    "description": "2 description",
  },
  {
    "id": 3,
    "name": "product 3",
    "largeImage": "3.png",
    "largeVideo": "3.mp4",
    "description": "3 description",
  },
];

var products = [];

function separateProductIdsAndNamesOut(productArticles) {
  console.log("productArticles: " + productArticles);
    products = productArticles.map(function(product) {
    return { id: product.id, name: product.name };
  });
  return products;
}

function initializeArticles() {
  return ProductsActionCreators.receiveArticles(productArticles);
}

// to build links in a nav component without payload of video and large img etc
function initializeProductsForNav() {
  return ProductsActionCreators.receiveArticlesIdsAndNames(separateProductIdsAndNamesOut(productArticles));
}

module.exports = {
  initializeArticles: initializeArticles,
  initializeProductsForNav: initializeProductsForNav
};

更新: 当我手动输入网址或在页面上点击刷新一次时,来自server.js的console.log:

props on server.js:  { routes: 
   [ { path: '', component: [Object], childRoutes: [Object] },
     { path: '/products/:productId', component: [Object] } ],
  params: { productId: '4' },
  location: 
   { pathname: '/products/4',
     search: '',
     hash: '',
     state: null,
     action: 'POP',
     key: '8b8lil',
     query: {},
     '$searchBase': { search: '', searchBase: '' } },
  components: 
   [ { [Function] displayName: 'SolidBroadheadApp' },
     { [Function] displayName: 'Product' } ],
  history: 
   { listenBefore: [Function],
     listen: [Function],
     transitionTo: [Function],
     push: [Function],
     replace: [Function],
     go: [Function],
     goBack: [Function],
     goForward: [Function],
     createKey: [Function],
     createPath: [Function],
     createHref: [Function],
     createLocation: [Function],
     setState: [Function],
     registerTransitionHook: [Function],
     unregisterTransitionHook: [Function],
     pushState: [Function],
     replaceState: [Function],
     isActive: [Function],
     match: [Function],
     listenBeforeLeavingRoute: [Function] },
  router: 
   { listenBefore: [Function: listenBefore],
     listen: [Function: listen],
     transitionTo: [Function: transitionTo],
     push: [Function: push],
     replace: [Function: replace],
     go: [Function: go],
     goBack: [Function: goBack],
     goForward: [Function: goForward],
     createKey: [Function: createKey],
     createPath: [Function: createPath],
     createHref: [Function: createHref],
     createLocation: [Function: createLocation],
     setState: [Function],
     registerTransitionHook: [Function],
     unregisterTransitionHook: [Function],
     pushState: [Function],
     replaceState: [Function],
     __v2_compatible__: true,
     setRouteLeaveHook: [Function: listenBeforeLeavingRoute],
     isActive: [Function: isActive] } }
this.state.searchResult:  null
this.state.productLinks in component:  null

更新2:我删除了404和splat路由,服务器console.log显示:

App is running, server is listening on port 3000
props.params on server.js:  { productId: '4' } // looks good; proper productId is passed.
this.state.productLinks in component:  null
this.props in product component:  { productId: '4' } // looks good; should render properly
props.params on server.js:  { productId: 'build.js' } // why is this assigned 'build.js' causing the problem???
this.state.productLinks in component:  null
this.props in product component:  { productId: 'build.js' } // why is this assigned 'build.js'? This is probably the cause of the problem.

该过程似乎运行了两次,第一次分配了正确的ID,第二次,它分配了#build; js&#39;那是由webpack构建的? WTF。

显示道具从productId重写为&#39; build.js&#39;的要点: https://gist.github.com/gcardella/1367198efffddbc9b78e

Webpack配置:

var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: path.join(process.cwd(), 'client/client-render.js'),
  output: {
    path: './public/',
    filename: 'build.js'
  },
  module: {
    loaders: [
      {
        test: /.js$/,
        loader: 'babel'
      }
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify('production'),
        APP_ENV: JSON.stringify('browser')
      }
    })
  ]
}

更新3:我通过删除状态检查来修复双周期,但是在渲染服务器端时失败,因为状态尚未设置。所以我添加一个检查以查看产品组件中是否存在state.product:

var React = require('react');
var ProductArticle = require('./products-solid-components/ProductArticle.react');
var ProductPresentation = require('./products-solid-components/ProductPresentation.react');
var ProductLargeImage = require('./products-solid-components/ProductLargeImage.react');

var LargeLeftImageArticle = require('./reusable-tool-components/LargeLeftImageArticle.react');
var LargeRightImageArticle = require('./reusable-tool-components/LargeRightImageArticle.react');

var ProductsStore = require('./../stores/ProductsStore');

// if (process.env.APP_ENV === 'browser') {
    var ProductsArticlesWebUtilsAPI = require('./../../utils/ProductsArticlesWebUtilsAPI');
    ProductsArticlesWebUtilsAPI.initializeArticles();
// }

var Product = React.createClass({

  getInitialState: function() {
    return {
      articles: ProductsStore.getArticles(),
      product: ProductsStore.getProduct()
    };
  },


  componentDidMount: function() {
    ProductsStore.addChangeListener(this.onProductChange);
  },

  componentWillUnmount: function() {
    ProductsStore.removeChangeListener(this.onProductChange);
  },

  onProductChange: function() {
    this.setState({
      articles: ProductsStore.getArticles(),
      product: ProductsStore.getProduct()
    });
  },

  render: function() {
    console.log("this.props in product component: ", this.props);

    // if (this.state.articles)

      console.log("after check for this.state.product on component this.props.params.productId: ", this.props.params.productId);
      if (this.state.product) {
      return (
        <div className="product">
          <section className="container-fluid">
            <ProductArticle name={this.state.product.name} largeImage={this.state.product.largeImage} description={this.state.product.description} />
          </section>
        </div>
      );
    } else {
      console.log("no state");
      return (
       // if there is no state, how do I render something on the server side?
      );
    }
  }
});

module.exports = Product;

2 个答案:

答案 0 :(得分:1)

事实证明它根本不是路由问题。不同领域存在许多问题:

我必须将bundle.js重命名为其他名称,例如client-bundle.js

然后我不得不修复我的ProductsStore中的问题来过滤产品数组以匹配productId:

function setProduct(productId) {
  var filteredProductsArr = products.filter(function(product) {
    return product.id == productId;
  });

  product = filteredProductsArr[0];
  return product;
};

然后我必须在我的产品组件的componentDidMount()中添加一个动作创建器: ProductsActionCreators.getProduct(this.props.params.productId);

这一切都像魅力一样:)

答案 1 :(得分:0)

您应该可以使用

检索此内容
this.props.params.productId

一个很好的例子:https://github.com/rackt/react-router/blob/master/examples/query-params/app.js