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.