Koa-React-SSR 服務端渲染,技術分享(koa2.0 + React16.x+webpack4.x)

demo地址:github.com/xwb007/Koa-…css

本文爲前端小白自學總結,有錯誤的地方懇請各位大佬指出!html

1、爲何須要服務端渲染?

一、SEO不友好前端

二、首次請求時間較長,體驗很差,等待白屏node

2、客戶端渲染和服務端渲染

一、客戶端

avatar

二、服務端

avatar

3、開始構建項目(webpack-4.x)

webpack 本地環境和生產環境配置我就很少闡述了,直接開始個人思路mysql

首先要用koa來啓動服務,webpack來打包咱們的react代碼react

sever/app.js-koa一些中間件webpack

import Koa from 'koa';
import json from 'koa-json';
import bodyParser from 'koa-bodyparser';
import logger from 'koa-logger';
import session from 'koa-session';
import compress from 'koa-compress';
import convert from 'koa-convert';
import cors from 'koa2-cors';

const app = new Koa();
app.use(convert(session(app)));
app.use(compress());
app.use(bodyParser());
app.use(cors());
app.use(json());
app.use(logger());

export default app;
複製代碼

而後就是server/server.dev.js-啓動本地開發server文件ios

const app = require('./app.js'),
    convert = require('koa-convert'),
    webpack = require('webpack'),
    fs = require('fs'),
    path = require('path'),
    devMiddleware = require('koa-webpack-dev-middleware'),
    hotMiddleware = require('koa-webpack-hot-middleware'),
    views = require('koa-views'),
    router = require('./routes'),
    clientRoute = require('./middlewares/clientRoute'),
    config = require('../build/webpack.dev.config'),
    port = process.env.port || 3000,
    compiler = webpack(config);

compiler.plugin('emit', (compilation, callback) => {
    const assets = compilation.assets;
    let file, data;

    Object.keys(assets).forEach(key => {
        if (key.match(/\.html$/)) {
            file = path.resolve(__dirname, key);
            data = assets[key].source();
            fs.writeFileSync(file, data);
        }
    });
    callback();
});

app.use(views(path.resolve(__dirname, '../views'), { map: { html: 'ejs' } }));
app.use(router.routes());
app.use(router.allowedMethods());
app.use(clientRoute);

app.use(
    convert(
        devMiddleware(compiler, {
            noInfo: true,
            publicPath: config.output.publicPath
        })
    )
);

app.use(convert(hotMiddleware(compiler)));
複製代碼
//ejs模版渲染
app.use(views(path.resolve(__dirname, '../views'), { map: { html: 'ejs' } }));
複製代碼

在項目中咱們會使用到rudux 、router路由徹底有react前端來控制,在用戶請求路由時傳到前端路由來匹配進行渲染相對應的路由模塊,react-router-domStaticRouter很好的針對服務端,ctx.url爲請求的路由,data爲請求的參數,須要在你渲染前傳遞過去。git

.clientRoute.jses6

import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { StaticRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import store from '../../client/redux/store';

import { RoutesIndex, routes } from '../../client/router/index';

import getData from '../../client/common/getData';

async function clientRoute(ctx, next) {
    for (let item of routes) {
        if (item.path == ctx.url) {
            const data = await getData(ctx.url);
            await ctx.render('index', {
                root: renderToStaticMarkup(
                    <Provider store={store}> <StaticRouter location={ctx.url} context={data}> <RoutesIndex /> </StaticRouter> </Provider>
                )
            });
            break;
        }
    }
    await next();
}

export default clientRoute;

複製代碼

咱們再來看看getData

client/common/getData.js

import request from './request';

async function getData(path) {
    switch (path) {
        case '/':
            let data = {};
            await request.config({ url: '/api/user/getUserInfo' }).then(res => {
                data = res;
            });
            return data;
        default:
            break;
    }
}

export default getData;
複製代碼

就是一個根據路由來匹配你當前路由須要的參數,而後用axios進行異步請求 request是我封裝的axios因此 es6 的async await超級好用呢,emmmmmm...這樣就能在咱們渲染返回html以前請求所須要的異步數據在服務端渲染,再來看看前端的配置,首先和react的cli基本同樣

client/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router } from 'react-router-dom';
import { RoutesIndex } from './router';

import { Provider } from 'react-redux';
import store from './redux/store';

ReactDOM.render(
    <Provider store={store}> <Router> <RoutesIndex /> </Router> </Provider>,
    document.getElementById('root')
);
複製代碼

這兒要注意的問題就是Router必須放在這個地方,不能放在router/index.js裏面,由於在node中引入BrowserRouter是要報錯的

clinet/router/index.js

/** * 返回一個基本的RoutesIndex */
import React from 'react';
import { Route } from 'react-router-dom';

import Home from '../page/Home';
import About from '../page/About';

const routes = [{ path: '/', component: Home }, { path: '/about', component: About }];

class RoutesIndex extends React.Component {
    render() {
        return (
            <div className="app-container"> {routes.map((item, index) => ( <Route key={index} path={item.path} exact component={item.component} /> ))} </div> ); } } export default { RoutesIndex, routes }; 複製代碼

我把routes放在一個數組中,新增的路由都在routes裏面配,這樣作的主要目的就是爲了在服務端渲染的時候方便匹配路由,再不是請求的路由時直接await next();

for (let item of routes) {
        if (item.path == ctx.url) {
            ...js
        }
    }
    await next();
複製代碼

redux我就不闡述了,我這裏沒有加入中間件redux-thunk,就是一個很簡單的redux狀態管理器

還有一點就是在咱們頁面中怎麼處理服務端傳遞過來的參數和前端本身請求的參數?

<StaticRouter location={ctx.url} context={data}>
    <RoutesIndex /> </StaticRouter>
複製代碼

StaticRouter 傳遞的data在前端頁面會傳入props中的staticContext,在瀏覽器是看不見的,須要的服務端控制檯打印查看。

./Home/index.jsx

import React, { Component } from 'react';
import getData from '../../common/getData';
import styles from './index.scss';

class Home extends Component {
    constructor(props) {
        super(props);
        this.state = {
            user: props.staticContext
        };
    }

    async componentDidMount() {
        this.setState({ user: await getData(this.props.match.path) });
    }

    render() {
        const { user } = this.state;
        return (
            <div className={styles.box}> <h1>hello koa-react-template</h1> <p>{user && user.userId}</p> <p>{user && user.name}</p> <p>{user && user.gender}</p> <p>{user && user.age}</p> </div>
        );
    }
}

export default Home;
複製代碼

說到這裏差很少就要結束了,就是還有一點問題,我就有個大膽的想法了,我這樣作就是在後臺請求的數據和前端請求的數據一致,這個時候就不該該在setState從新render頁面了,因此能夠把服務端請求的數據和html之前返回客戶端,而後在前端請求了新的數據後進行比較,若是不一致就render,一直就不render,這樣會不會更好呢??

注意問題

我在項目中使用了mysql,若是你沒有安裝mysql,沒事,你能夠直在server\controllers\user.js 進行註釋

// import query from './config';

// const findUserInfo = () => {
// const _sql = 'select * from user';
// return query(_sql, []);
// };

const getUserInfo = async ctx => {
    let data = {};

    // await findUserInfo().then(result => {
    // data = result[0];
    // });

    data = {
        userId: 1001,
        name: 'xwb007',
        gender: '男',
        age: 24
    };

    ctx.body = data;
};

export default { getUserInfo };
複製代碼

做爲一個前端小白,第一次分享技術,若是有大佬以爲有問題,能夠指出來,感激涕零。你要是以爲還闊以,對你有幫助,歡迎stars。源碼git

讓咱們一塊兒愉快的掉頭髮吧!!!!

相關文章
相關標籤/搜索