我正在尝试将服务器端渲染实现到我现有的React / Redux应用程序中。经过几次失败的尝试后,我想我差不多了,但我遇到了一些问题。我是React / Redux / Express / Webpack的新手,所以如果我犯了任何愚蠢的错误就道歉!
问题1 - 未定义窗口
当我使用Redux时,我需要跟踪初始状态并将其加载到我的应用程序中。在server/index.js
内部我从商店获取初始状态并使用window.__initialData__ = ${serialize(initialData)}
将其存储在窗口中。
服务器代码如下:
import express from 'express';
import React from 'react';
import { renderToString } from 'react-dom/server';
import { Provider } from 'react-redux';
import { StaticRouter, matchPath } from 'react-router-dom';
import serialize from 'serialize-javascript';
import routes from 'routes/routes';
import store from 'store';
import AppContainer from 'containers/app-container/app-container';
const app = express();
app.use(express.static('static'));
app.get('*', (req, res, next) => {
const promises = routes.reduce((accumulator, route) => {
if (matchPath(req.url, route) && route.component && route.component.initialAction) {
accumulator.push(Promise.resolve(store.dispatch(route.component.initialAction())));
}
return accumulator;
}, []);
Promise.all(promises)
.then(() => {
const context = {};
const markup = renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<AppContainer />
</StaticRouter>
</Provider>
);
const initialData = store.getState();
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>W Combinator</title>
<script src='/static/assets/app.js' defer></script>
<script src='/static/assets/vendor.js' defer></script>
<script>window.__initialData__ = ${serialize(initialData)}</script>
</head>
<body>
<div id='root'>${markup}</div>
</body>
</html>
`);
})
.catch(next);
});
app.listen(process.env.PORT || 3000, () => {
console.log('Server is listening');
});
当我在前端创建我的商店时,我尝试访问此窗口变量,如const initialState = window.__initialData__;
。我得到的问题是终端中的错误被抛出说窗口没有定义。我试图通过在Webpack中设置process.env.BROWSER
来解决这个问题,但它似乎不起作用。
Webpack代码如下:
const path = require('path');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const nodeExternals = require('webpack-node-externals');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const StyleLintPlugin = require('stylelint-webpack-plugin');
const WriteFilePlugin = require('write-file-webpack-plugin');
const basePath = path.join(__dirname, 'app');
const distPath = path.join(__dirname, 'static/assets');
const browserConfig = {
devtool: 'cheap-module-eval-source-map',
entry: {
vendor: './app/client/vendor.js',
app: './app/client/index.jsx'
},
output: {
path: distPath,
publicPath: '/assets/',
filename: '[name].js'
},
plugins: [
new CleanWebpackPlugin(['static']),
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor'
}),
new HtmlWebpackPlugin({
title: 'Default Template',
template: './app/client/index.html',
filename: 'index.html'
}),
new ExtractTextPlugin({
filename: 'style.css'
}),
new WriteFilePlugin(),
new StyleLintPlugin({
files: ['app/**/*.scss'],
syntax: 'scss'
}),
new webpack.DefinePlugin({
'process.env.BROWSER': JSON.stringify(true)
})
],
resolve: {
extensions: ['.js', '.jsx'],
modules: [
path.resolve(`${basePath}/client`),
path.resolve(`${basePath}/server`),
path.resolve('node_modules/')
]
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
include: [basePath]
},
{
test: /\.global\.scss$/,
exclude: /node_modules/,
include: basePath,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader', 'postcss-loader', {
loader: 'sass-loader',
options: {
includePaths: [basePath]
}
}]
})
},
{
test: /\.scss$/,
exclude: [/node_modules/, /\.global\.scss$/],
include: basePath,
use: ExtractTextPlugin.extract({
fallback: 'style-loader',
use: ['css-loader?modules&localIdentName=[local]-__-[hash:base64:5]', 'postcss-loader', {
loader: 'sass-loader',
options: {
includePaths: [basePath]
}
}]
})
},
{
test: /\.svg$/,
use: [
'svg-sprite-loader',
'svgo-loader'
]
},
{
test: /\.(png|jpg|gif)$/,
include: basePath,
use: [
'file-loader'
]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
include: basePath,
use: [
'file-loader'
]
}
]
}
};
const serverConfig = {
entry: './app/server/index.js',
target: 'node',
externals: [nodeExternals()],
output: {
path: basePath,
filename: 'server.js',
libraryTarget: 'commonjs2'
},
devtool: 'cheap-module-source-map',
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /(node_modules)/,
loader: 'babel-loader',
include: [basePath]
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env.BROWSER': JSON.stringify(false)
})
],
resolve: {
extensions: ['.js', '.jsx'],
modules: [
path.resolve(`${basePath}/client`),
path.resolve(`${basePath}/server`),
path.resolve('node_modules/')
]
}
};
module.exports = [browserConfig, serverConfig];
问题2 - 意外的令牌&lt;
如果我手动设置初始状态我能够让页面呈现服务器端,我遇到的问题是我收到控制台错误Unexpected token <
。我已经完成了一些阅读,我的猜测是我在我的快递服务器中缺少某种配置设置,但我看不到让它工作。
问题3 - 错误:子编译失败:
ERROR in ./node_modules/html-webpack-plugin/lib/loader.js!./app/client/index.ejs
Module build failed: Error: ENOENT: no such file or directory app/client/index.ejs
如果我手动设置初始状态并忽略上面的错误,那么我可以在页面上看到我的内容呈现而终端没有任何错误。然而,当我在我的应用程序中更改文件并保存然后我得到上面的错误出现在终端并且它不编译。该错误表明它似乎正在寻找一个似乎无法找到的index.ejs
文件。我目前没有在我的应用程序中使用任何模板引擎,我只是提供静态index.html
文件,您可以在上面的webpack配置中看到。
我的猜测再一次是我没有正确设置或者我错过了配置选项,我花了一些时间试图解决这些问题,但无法确定根本原因。
如果有人知道如何解决这些问题,那么非常感谢帮助!
Babelrc
{
"presets": ["es2015", "react"],
"plugins": ["transform-object-rest-spread"]
}
修改
问题1我已设法解决,我没有在服务器内正确创建我的商店,在const store = configureStore();
之后添加此app.get(*...
并调整我的商店设置稍微修正了这一点。作为参考,我使用this链接来帮助完成该设置。
修改
问题2是由于JS文件没有作为静态文件提供的。 app.use(express.static('./static/assets'))
解决了这个问题。