demo地址:github.com/xwb007/Koa-…
本文为前端小白自学总结,有错误的地方恳请各位大佬指出!
一、为什么需要服务端渲染?
1、SEO不友好
2、首次请求时间较长,体验不好,等待白屏
二、客户端渲染和服务端渲染
1、客户端
2、服务端
三、开始构建项目(webpack-4.x)
webpack 本地环境和生产环境配置我就不多阐述了,直接开始我的思路
首先要用koa来启动服务,webpack来打包我们的react代码
sever/app.js-koa一些中间件
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文件
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-dom 的 StaticRouter很好的针对服务端,ctx.url为请求的路由,data为请求的参数,需要在你渲染前传递过去。
.clientRoute.js
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 };
复制代码