Let the middle tier take on the data acquisition responsibilities
Earlier we said that when the browser communicates with the server, Node serves as the middle layer for rendering the page, and the data is fetched from the real data server.
Let’s examine whether our previous code implements the concept of a middle tier. SRC /public/index.js is the code that our client will run. It can be found that the interface requesting the service is a Java interface, which violates the concept of the middle layer. The interface requesting the service should also be a middle layer interface, which is convenient for us to check errors.
Here we only need to make our Node-server into a proxy server, that is, a proxy function, here we rely on express-HTTP-proxy.
npm install express-http-proxy --save
Copy the code
SRC /server/index.js Modifies the obtaining mode of store
import express from 'express';
import proxy from 'express-http-proxy';
import { matchRoute } from 'react-router-config';
import { render } from './utils';
import { getStore } from '.. /store'; / / use the store
import routes from '.. /Routes';
const app = express();
app.use(express.static('public'));
app.use('/api', proxy('xx.xx.xx.xx', {
proxyReqPathResolver: (req) = > { // Which path to forward to
return req.url;
}
}))
app.get(The '*'.function(req, res) {
const store = getStore();
const matchedRoutes = matchRoute(routes, req,path);
const promises = [];
matchedRoutes.forEach(item= > {
if(item.route.loadData) { promises.push(item.route.loadData(store)); }});Promise.all(promises).then(() = >{ res.send(render(store, routes, req)); })})var server = app.listen(3000);
Copy the code
SRC/components/Home/store/actions, js, delete the requested domain name.
import axios from 'axios';
import { CHANGE_LIST } from './constants';
const changeList = (list) = > {
type: CHANGE_LIST,
list
}
export const getHomeList = (server) = > {
let url = ' ';
if (server) { // The server environment uses the real address
url = 'xx.xx.xx.xx/api/getlist'
} else { // The browser environment uses relative addresses to do forwarding
url = '/api/getlist'
}
return (dispatch) = > {
return axios.get(url).then(res= > {
constlist = res.data.data; dispatch(changeList(list)); }}})Copy the code
src/components/Home/index.js
import React, { Component } from 'react';
import Header from '.. /Header';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions';
class Home extends Component {
getList() {
const { list } = this.props;
return this.props.list.map(item= > <div key={item.id}>{item.title}</div>)}render() {
return <div>
<Header>
<div>Home</div>
{this.getList()}
<button onClick={()= >{ alert('click1'); } > button</button>
</div>} componentDidMount() { if (! this.props.list.length) { this.props.getHomeList(); }}} home. loadData = (store) => {// Execute action to expand store. return store.dispatch(getHomeList(false)); } const mapStatetoProps = state => ({ list: state.home.newsList }); const mapDispatchToProps = dispatch => ({ getHomeList() { dispatch(getHomeList(true)); } }) export default connect(mapStatetoProps, mapDispatchToProps)(Home);Copy the code
withExtraArgument
In the above code we pass a Boolean value to determine the request path is still a bit troublesome, we use withExtraArgument to tidy up.
src/components/Home/index.js
import React, { Component } from 'react';
import Header from '.. /Header';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions';
class Home extends Component {
getList() {
const { list } = this.props;
return this.props.list.map(item= > <div key={item.id}>{item.title}</div>)}render() {
return <div>
<Header>
<div>Home</div>
{this.getList()}
<button onClick={()= >{ alert('click1'); } > button</button>
</div>} componentDidMount() { if (! this.props.list.length) { this.props.getHomeList(); }}} home. loadData = (store) => {// Execute action to expand store. return store.dispatch(getHomeList()); } const mapStatetoProps = state => ({ list: state.home.newsList }); const mapDispatchToProps = dispatch => ({ getHomeList() { dispatch(getHomeList()); } }) export default connect(mapStatetoProps, mapDispatchToProps)(Home);Copy the code
src/components/Home/store/actions.js
import { CHANGE_LIST } from './constants';
const changeList = (list) = > {
type: CHANGE_LIST,
list
}
export const getHomeList = (server) = > {
return (dispatch, getState, axiosInstance) = > {
return axiosInstance.get(url).then(res= > {
constlist = res.data.data; dispatch(changeList(list)); }}})Copy the code
src/store/index.js
import { createStore, applyMiddleware, combineReducers } from 'redux';
import thunk from 'redux-thunk';
import { reducer as homeReducer} from '.. /components/Home/store';
import clientAxios from '.. /client/request';
import serverAxios from '.. /server/request';
const reducer = combineReducers({
home: homeReducer
});
export const getStore = () = > {
return createStore(reducer, applyMiddleware(thunk.withExtraArgument(serverAxios)));
}
export const getClientStore = () = > {
const defaultState = window.context.state;
// defaultState as the default value
return createStore(reducer, defaultState, applyMiddleware(thunk.withExtraArgument(clientAxios)));
}
Copy the code
src/client/request.js
import axios from 'axios';
const instance = axios.create({
baseURL: '/'
})
Copy the code
src/server/request.js
import axios from 'axios';
const instance = axios.create({
baseURL: 'xx.xx.xx.xx'
})
Copy the code
renderRoutes
src/App.js
import React from 'react';
import Header from './component/Header';
import { renderRoutes } from 'react-router-config';
const App = (props) = > {
return (<div>
<Header />
{renderRoutes(props.route.routes)}
</div>)}export default App;
Copy the code
We want the App component to be displayed no matter how the user accesses it.
src/Routes.js
import React from 'react';
import App from './App';
import Home from './components/Home';
import Login from './components/Login';
export default [{
path: '/'.component: App,
routes: [{path: '/'.component: Home,
exact: true.key: 'home'.loadData: Home.loadData
},
{
path: '/login'.component: Login,
key: 'login'.exact: true}}]]Copy the code
Here we build a secondary route that matches App components when the user accesses the directory, and App and login components when the user accesses the /login path.
src/server/utils.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Route } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import { Provider } from 'react-redux';
export const render = (store, routes, req) = > {
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
<div>
{renderRoutes(routes)}
</div>
</StaticRouter>
</Provider>
));
return `
<html>
<body>
<div id="root">${content}</div>
<script>
window.context = {
state: The ${JSON.stringfiy(store.getState())}
}
</script>
<script src="/index.js"></script>
</body>
</html>
`;
}
Copy the code
src/components/Home/index.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions';
class Home extends Component {
getList() {
const { list } = this.props;
return this.props.list.map(item= > <div key={item.id}>{item.title}</div>)}render() {
return <div>
<div>Home</div>
{this.getList()}
<button onClick={()= >{ alert('click1'); } > button</button>
</div>
}
componentDidMount() {
if (!this.props.list.length) {
this.props.getHomeList();
}
}
}
Home.loadData = (store) = > {
// Execute action to expand store.
return store.dispatch(getHomeList());
}
const mapStatetoProps = state= > ({
list: state.home.newsList
});
const mapDispatchToProps = dispatch= > ({
getHomeList(){ dispatch(getHomeList()); }})export default connect(mapStatetoProps, mapDispatchToProps)(Home);
Copy the code
src/components/Login/index.js
import React from 'react';
const Login = () = > {
return <div>Login</div>
}
export default Login;
Copy the code
src/client/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import routes from '.. /Routes';
import { getClientStore } from '.. /store'; / / use the store
import { Provider } from 'react-redux';
const store = getClientStore();
const App = () = > {
return (
<Provider store={store}>
<BrowserRouter>
<div>
{renderRoutes(routes)}
</div>
</BrowserRouter>
</Provider>
)
}
ReactDOM.hydrate(<App />.document.getElementById('root'));
Copy the code
Request Failure Handling
If the request in our action fails, it fires a catch instead of a THEN, which will cause the site to get stuck and not respond. Because the promise collection in server/index.js will fail, it will never return success.
Promise.all(promises).then(() = > {
res.send(render(store, routes, req));
})
Copy the code
So we can put a catch in there.
Promise.all(promises).then(() = > {
res.send(render(store, routes, req));
}).catch(() = > {
res.end('sorry');
})
Copy the code
The problem is that we don’t know what went wrong, or when we have multiple components rendering, we expect the ones that interface properly to return properly.
We can wrap a new Promise around loadData and call resolve whether loadData succeeds or fails to ensure that all requests are completed. Promise.all can be executed normally.
src/server/index.js
import express from 'express';
import proxy from 'express-http-proxy';
import { matchRoute } from 'react-router-config';
import { render } from './utils';
import { getStore } from '.. /store'; / / use the store
import routes from '.. /Routes';
const app = express();
app.use(express.static('public'));
app.use('/api', proxy('xx.xx.xx.xx', {
proxyReqPathResolver: (req) = > { // Which path to forward to
return req.url;
}
}))
app.get(The '*'.function(req, res) {
const store = getStore();
const matchedRoutes = matchRoute(routes, req,path);
const promises = [];
matchedRoutes.forEach(item= > {
if (item.route.loadData) {
const promise = new Promise((resolve, reject) = >{ item.route.loadData(store).then(resolve).catch(resolve); }) promises.push(promise); }});Promise.all(promises).then(() = >{ res.send(render(store, routes, req)); })})var server = app.listen(3000);
Copy the code
How do YOU support CSS styling
First we need webPack to compile the CSS file.
The webpack.server.js server uses isomorphic-style-loader to replace the style-loader of the client.
const Path = require('path');
const NodeExternals = require('webpack-node-externals'); // The server running webPack needs to run NodeExternals, which is used to keep node modules such as Express from being packaged into JS.
const merge = require('webpack-merge');
const config = require('./webpack.base.js');
const serverConfig = {
target: 'node'.mode: 'development'.entry: './src/server/index.js'.output: {
filename: 'bundle.js'.path: Path.resolve(__dirname, 'build')},externals: [NodeExternals()],
module: {
rules: [{test: /\.css? $/,
use: ['isomorphic-style-loader', {
loader: 'css-loader'.options: {
importLoaders: 1.modules: true.localIdentName: '[name]_[local]_[hase:base64:5]'}}]}}module.exports = merge(config, serverConfig);
Copy the code
The webpack.client.js client is loaded using style-loader.
const Path = require('path');
const merge = require('webpack-merge');
const config = require('./webpack.base.js');
const clientConfig = {
mode: 'development'.entry: './src/client/index.js'.output: {
filename: 'index.js'.path: Path.resolve(__dirname, 'public')},module: {
rules: [{test: /\.css? $/,
use: ['style-loader', {
loader: 'css-loader'.options: {
importLoaders: 1.modules: true.localIdentName: '[name]_[local]_[hase:base64:5]'}}]}};module.exports = merge(config, clientConfig);
Copy the code
src/components/Home/style.css
body {
background: green;
}
.test {
background: red;
}
Copy the code
src/components/Home/index.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions';
import styles from './style.css';
class Home extends Component {
componentWillMount() { // Handle styles
if (this.props.staticContext) { // Server running exists, client running does not exist. So don't do it on the client. Store the style in context.
this.props.staticContext.css = styles._getCss(); }}getList() {
const { list } = this.props;
return this.props.list.map(item= > <div key={item.id}>{item.title}</div>)}render() {
return <div className={styles.test}>
<div>Home</div>
{this.getList()}
<button onClick={()= >{ alert('click1'); } > button</button>
</div>
}
componentDidMount() {
if (!this.props.list.length) {
this.props.getHomeList();
}
}
}
Home.loadData = (store) = > {
// Execute action to expand store.
return store.dispatch(getHomeList());
}
const mapStatetoProps = state= > ({
list: state.home.newsList
});
const mapDispatchToProps = dispatch= > ({
getHomeList(){ dispatch(getHomeList()); }})export default connect(mapStatetoProps, mapDispatchToProps)(Home);
Copy the code
SRC /server/index.js handles the style in the render method.
import express from 'express';
import proxy from 'express-http-proxy';
import { matchRoute } from 'react-router-config';
import { render } from './utils';
import { getStore } from '.. /store'; / / use the store
import routes from '.. /Routes';
const app = express();
app.use(express.static('public'));
app.use('/api', proxy('xx.xx.xx.xx', {
proxyReqPathResolver: (req) = > { // Which path to forward to
return req.url;
}
}))
app.get(The '*'.function(req, res) {
const store = getStore();
const matchedRoutes = matchRoute(routes, req,path);
const promises = [];
matchedRoutes.forEach(item= > {
if (item.route.loadData) {
const promise = new Promise((resolve, reject) = >{ item.route.loadData(store).then(resolve).catch(resolve); }) promises.push(promise); }});Promise.all(promises).then(() = > {
consthtml = render(store, routes, req, context) res.send(html); })})var server = app.listen(3000);
Copy the code
src/server/utils.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Route } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import { Provider } from 'react-redux';
export const render = (store, routes, req, context) = > {
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
<div>
{renderRoutes(routes)}
</div>
</StaticRouter>
</Provider>
));
const cssStr = context.css ? context.css : ' ';
return `
<html>
<head>
<style>${cssStr}</style>
</head>
<body>
<div id="root">${content}</div>
<script>
window.context = {
state: The ${JSON.stringfiy(store.getState())}
}
</script>
<script src="/index.js"></script>
</body>
</html>
`;
}
Copy the code
How the styles of multiple components are integrated. We can use an array to store CSS styles.
src/server/utils.js
import React from 'react';
import { renderToString } from 'react-dom/server';
import { StaticRouter, Route } from 'react-router-dom';
import { renderRoutes } from 'react-router-config';
import { Provider } from 'react-redux';
export const render = (store, routes, req, context) = > {
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={{}}>
<div>
{renderRoutes(routes)}
</div>
</StaticRouter>
</Provider>
));
const cssStr = context.css.length ? context.css.join('\n') : ' ';
return `
<html>
<head>
<style>${cssStr}</style>
</head>
<body>
<div id="root">${content}</div>
<script>
window.context = {
state: The ${JSON.stringfiy(store.getState())}
}
</script>
<script src="/index.js"></script>
</body>
</html>
`;
}
Copy the code
src/components/Home/index.js
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions';
import styles from './style.css';
class Home extends Component {
componentWillMount() { // Handle styles
if (this.props.staticContext) { // Server running exists, client running does not exist. So don't do it on the client. Store the style in context.
this.props.staticContext.css.push(styles._getCss()); }}getList() {
const { list } = this.props;
return this.props.list.map(item= > <div key={item.id}>{item.title}</div>)}render() {
return <div className={styles.test}>
<div>Home</div>
{this.getList()}
<button onClick={()= >{ alert('click1'); } > button</button>
</div>
}
componentDidMount() {
if (!this.props.list.length) {
this.props.getHomeList();
}
}
}
Home.loadData = (store) = > {
// Execute action to expand store.
return store.dispatch(getHomeList());
}
const mapStatetoProps = state= > ({
list: state.home.newsList
});
const mapDispatchToProps = dispatch= > ({
getHomeList(){ dispatch(getHomeList()); }})export default connect(mapStatetoProps, mapDispatchToProps)(Home);
Copy the code
SRC /server/index.js handles the style in the render method.
import express from 'express';
import proxy from 'express-http-proxy';
import { matchRoute } from 'react-router-config';
import { render } from './utils';
import { getStore } from '.. /store'; / / use the store
import routes from '.. /Routes';
const app = express();
app.use(express.static('public'));
app.use('/api', proxy('xx.xx.xx.xx', {
proxyReqPathResolver: (req) = > { // Which path to forward to
return req.url;
}
}))
app.get(The '*'.function(req, res) {
const store = getStore();
const matchedRoutes = matchRoute(routes, req,path);
const promises = [];
matchedRoutes.forEach(item= > {
if (item.route.loadData) {
const promise = new Promise((resolve, reject) = >{ item.route.loadData(store).then(resolve).catch(resolve); }) promises.push(promise); }});Promise.all(promises).then(() = > {
const context = { css: []};consthtml = render(store, routes, req, context) res.send(html); })})var server = app.listen(3000);
Copy the code
There’s actually a problem with the code above. On the Home component we mount a loadData method, but the Home file we export is not the Home component, but the connect wrapped component, so we export another component. Fortunately, connect analyzes the properties of the original component and mounts them to the current output, so we can still call the loadData method when we use the Home component later. This is not a good idea, however, and it is best to declare it directly to avoid confusing code usage.
Mount loadData to ExportHome.
src/components/Home/index.js
.// Home.loadData = (store) => {
// // Run action to expand the store.
// return store.dispatch(getHomeList());
// }
const mapStatetoProps = state= > ({
list: state.home.newsList
});
const mapDispatchToProps = dispatch= > ({
getHomeList(){ dispatch(getHomeList()); }})const ExportHome = connect(mapStatetoProps, mapDispatchToProps)(Home);
ExportHome.loadData = (store) = > {
// Execute action to expand store.
return store.dispatch(getHomeList());
}
export default ExportHome
Copy the code
Simplify code with higher-order components
The style we wrote above is too cumbersome, we first need to use the componentWillMount life cycle, and then inject its style into the Context. So each component needs a piece of code like this. This is not a reasonable design. We can sort it out. Use higher-order components.
SRC/withstyle.js creates the higher-order component function. This function returns a component. In fact, this function is a function that generates higher-order components, and the returned components are called higher-order components, and their job is to render the pre-push style.
Our function accepts the style file styles because the component doesn’t know where styles are. You also receive the DecoratedComponent that was originally to be rendered, render it in the higher-order component, and pass in the parameters.
import React, { Component } from 'react';
export default (DecoratedComponent, styles) => {
return class NewComponent extends Component {
componentWillMount() {
if (this.props.staticContext) {
this.props.staticContext.css.push(styles._getCss()); }}render() {
return <DecoratedComponent {. this.props} / >}}}Copy the code
Now that the higher-order components are written, let’s transform the Home component.
SRC/components/Home/index, js, here can delete their own componentWillMount, introducing the withStyle function, and then at the bottom of the export use wrapped in withStyle Home components, when we pass styles style is ok. withStyle(Home, styles);
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { getHomeList } from './store/actions';
import styles from './style.css';
import withStyle from '.. /.. /withStyle';
class Home extends Component {
getList() {
const { list } = this.props;
return this.props.list.map(item= > <div key={item.id}>{item.title}</div>)}render() {
return <div className={styles.test}>
<div>Home</div>
{this.getList()}
<button onClick={()= >{ alert('click1'); } > button</button>
</div>
}
componentDidMount() {
if (!this.props.list.length) {
this.props.getHomeList(); }}}const mapStatetoProps = state= > ({
list: state.home.newsList
});
const mapDispatchToProps = dispatch= > ({
getHomeList(){ dispatch(getHomeList()); }})const ExportHome = connect(mapStatetoProps, mapDispatchToProps)(withStyle(Home, styles));
ExportHome.loadData = (store) = > {
// Execute action to expand store.
return store.dispatch(getHomeList());
}
export default ExportHome
Copy the code
SEO optimization
SEO optimization is also called search engine optimization.
Title and description are rarely useful for SEARCH engine optimization, they are just descriptions of the site. Baidu’s search classifies websites by matching them with all their text content. So most of the time, the websites we search are the same as the content we need, but the titile website we search does not contain the search keywords.
A website is composed of text, multimedia, link three parts.
In today’s Internet, content needs to be original, original works will get more traffic, SEO will analyze the originality of content. So we can add original attributes to the text.
Link to the content of the site and the content of the current site to be relevant, the stronger the relevance of SEO weight is higher.
Multimedia also needs to be original.
React-Helmet
React-helmet lets you customize your page’s title and meta
import React, { Component, Fragment } from 'react';
import { Helmet } from 'react-helmet';
class Home extends Component {
render() {
return <Fragment>
<Helmet>
<title>This is the title of the Helmet definition</title>
<meta name="description" content="This is the description of what Helmet is." />
</Helmet>
<div>Home</div>
{this.getList()}
<button onClick={()= >{ alert('click1'); } > button</button>
</Fragment>}}Copy the code
The above code is just client-side rendering. The server short rendering is a little different, but it’s also very simple. Let’s modify utils.js
src/server/utils.js
.import { Helmet } from 'react-helmet';
export const render = (store, routes, req, context) = >{...const helmet = Helmet.renderStatic();
return `
<html>
<head>
${helmet.title.toString()}
${helmet.meta.toString()}
<style>${cssStr}</style>
</head>
<body>
<div id="root">${content}</div>
<script>
window.context = {
state: The ${JSON.stringfiy(store.getState())}
}
</script>
<script src="/index.js"></script>
</body>
</html>
`;
}
Copy the code
pre-rendered
Url: localhost: 8000 / render? url=http://localhost:3000/
server.js
const prerender = require('prerender');
const server = prerender({
port: 8000
});
server.start();
Copy the code
run
node ./server.js
Copy the code
The page element exists in the url content that is accessed. We can put a layer of Nginx on the outer layer of the website and forward the request to the pre-render server if it is a spider, or to the real server if it is a user.