环境准备
- git clone https://github.com/easy-team/egg-react-webpack-boilerplate.git
yarn installornpm install
编写前端代码逻辑
编写 React Router
创建 React Router 实例
添加
${root}/app/web/page/blog/router/index.jsx
import { Route } from 'react-router-dom';import Home from './home';import Detail from './detail';export default function createRouter() {return [{path: '/detail/:id',component: Detail},{path: '/',component: Home},{path: '*',component: Home}];}
创建 React Router 路由页面
- 添加
${root}/app/web/page/blog/router/home.jsx
import React, { Component } from 'react';import { connect } from 'react-redux';import { Link } from 'react-router-dom';import { hot } from 'react-hot-loader/root'import request from 'framework/request';import './home.css'class Home extends Component {static async asyncData(context, route) {return request.get('/api/blog/list', context);}render() {const { list = [] } = this.props;return <div className="easy-article-list"><ul>{list.map(function (item) {return <li key={item.id} className="easy-article-item"><h2 className="easy-article-title"><Link to={'/detail/' + item.id}>{item.title}</Link></h2><div className="easy-article-summary">{item.summary}</div><div className="easy-article-meta"><span>11Word Count:{item.wordCount} </span><span>Create Time: {item.createTime}</span></div></li>;})}</ul></div>;}}const mapStateToProps = state => {return {list: state.list};};const mapDispatchToProps = dispatch => {return {};};export default connect(mapStateToProps, mapDispatchToProps)(EASY_ENV_IS_DEV ? hot(Home) : Home);
- 添加
${root}/app/web/page/blog/router/detail.jsx
'use strict';import React, { Component } from 'react';import { connect } from 'react-redux';import { hot } from 'react-hot-loader/root'import request from 'framework/request';class Detail extends Component {static async asyncData(locals, route) {const id = route.match.params.id;return request.get(`/api/blog/${id}`, locals);}render() {const { article } = this.props;return article ? <div><h2 className="easy-article-detail-title">{article.title}</h2><div className="easy-article-info"><iframe src={article.url} frameBorder="0" width="100%" style={{minHeight: '800px'}}></iframe></div></div> : null;}}const mapStateToProps = state => {return {article: state.article};};export default connect(mapStateToProps)(EASY_ENV_IS_DEV ? hot(Detail) : Detail)
编写 React Router 聚合页面
${root}/app/web/page/blog/view/main.jsx
import React, { Component } from 'react';import { Link, Switch } from 'react-router-dom';import { hot } from 'react-hot-loader/root'import { ARTICLE_LIST, ARTICLE_DETAIL } from '../store/constant';import Layout from 'component/layout'import Header from 'component/header'import Route from '../router/route';import Home from '../router/home';import Detail from '../router/detail';import './main.css';class Main extends Component {constructor(props) {super(props);this.state = { current: props.url };}tabClick(e) {console.log('click', e.target);}render() {return <Layout {...this.props}><Header></Header><ul className="menu-tab"><li onClick={this.tabClick.bind(this)}><Link to="/">Home</Link></li></ul><Switch><Route type={ARTICLE_DETAIL} path="/detail/:id" component={Detail} /><Route type={ARTICLE_LIST} path="/" component={Home}/></Switch></Layout>;}}export default EASY_ENV_IS_DEV ? hot(Main) : Main;
编写 React Router 路由切换 Hook 高阶组件
${root}/app/web/page/blog/router/route.jsx
'use strict';import React, { Component } from 'react';import { Route } from 'react-router-dom';import { connect } from 'react-redux';import { update } from '../store/actions';class WrapperRoute extends Component {async componentWillReceiveProps(nextProps) {const { type, locals, component, computedMatch, updateState } = nextProps;if (computedMatch.url !== this.props.computedMatch.url) {const { asyncData } = component;if (asyncData) {const data = await asyncData(locals, { match: computedMatch });updateState(type, data);}}}render() {return <Route {...this.props} />}}const mapStateToProps = state => {return {locals: state}};const mapDispatchToProps = dispatch => {return {updateState: (type, data) => dispatch(update(type, data))};};export default connect(mapStateToProps, mapDispatchToProps)(WrapperRoute);
编写 React Store
创建 React Store 实例
// ${root}/app/web/page/blog/store/constant.jsexport const ARTICLE_LIST = 'ARTICLE_LIST';export const ARTICLE_DETAIL = 'ARTICLE_DETAIL';export const ADD = 'ADD';export const DEL = 'DEL';// ${root}/app/web/page/blog/store/actions.jsexport const update = (type, data) => {return {type,data};}// ${root}/app/web/page/blog/store/reducers.jsimport { ARTICLE_DETAIL, ARTICLE_LIST, ADD, DEL } from './constant';export default function update(state, action) {const newState = Object.assign({}, state);if (action.type === ADD) {const list = Array.isArray(action.data) ? action.data : [action.data];newState.list = [...newState.list, ...list];} else if (action.type === DEL) {newState.list = newState.list.filter(data => {return data.id !== action.id;});} else if (action.type === ARTICLE_LIST) {newState.list = action.data.list;newState.total = action.data.total;} else if (action.type === ARTICLE_DETAIL) {newState.article = action.data.article;}return newState;}// ${root}/app/web/page/blog/store/index.jsimport { createStore } from 'redux';import reducers from './reducers';export default function(initalState){return createStore(reducers, initalState);}
编写 React SPA 入口代码
${root}/app/web/page/blog/index.jsx webpack.config.js 的 entry 指向该文件。
import React, { Component } from 'react';import { Provider } from 'react-redux';import { BrowserRouter, StaticRouter } from 'react-router-dom';import { matchRoutes } from 'react-router-config';import createStore from './store'import createRouter from './router'import Main from './view/main'import '../../asset/css/blog.css'// 直接渲染 React Component 组件,JSX 文件结尾的 Webpack entry 自动使用 react-entry-template-loaderexport default class Entry extends Component {static async asyncData(locals) {const router = createRouter();const url = locals.url;const matchRouteList = matchRoutes(router, url);const promises = matchRouteList.map(matchRoute=> {const componentAsyncData = matchRoute.route.component.asyncData;return componentAsyncData instanceof Function ? componentAsyncData(locals, matchRoute) : null;});const list = await Promise.all(promises);return list.reduce((item, result) => {return { ...result, ...item}}, {});}render() {if (EASY_ENV_IS_BROWSER) {const store = createStore(window.__INITIAL_STATE__);const { url } = store.getState();return <Provider store={store}><BrowserRouter location={url}><Main></Main></BrowserRouter></Provider>;}const store = createStore(this.props);const { url } = store.getState();return <Provider store={store}><StaticRouter location={url} context={{}}><Main></Main></StaticRouter></Provider>;}}
编写 Node 代码
添加
${root}/app/controller/blog/index.jsNode 代码
'usestrict';const egg = require('egg');module.exports = class IndexController extends egg.Controller {async ssr(ctx) {// 1. ctx.render 方法是 egg-view-react-ssr 插件提供实现// 2. ctx.render 第一个参数 blog.js 是 webpack.config.js entry 配置的 blog 构建后 JSBundle 文件// 3. ctx.render 第二个参数 { url, title ...} 是渲染数据,会与公共 ctx.locals 合并await ctx.render('blog.js', {url: ctx.url,title: 'Egg React ',keywords: 'Egg,React,Egg React,Egg React SSR, Egg React CSR, Server Side Render, Client Side Render',description: 'Egg + React + Webpack 服务端渲染 SSR (Server Side Render) 和 前端渲染 CSR (Client Side Render) 工程骨架项目'});}async list() {this.ctx.body = this.service.article.getArtilceList();}async detail(ctx) {const id = ctx.params.id;const article = this.service.article.getArticle(Number(id));ctx.body = { article };}};
Egg 路由配置
添加
${root}/app/router.jsEgg 路由配置
module.exports = app => {const { router, controller } = app;router.get('/api/blog/list', controller.blog.index.list);router.get('/api/blog/:id', controller.blog.index.detail);router.get('/(.*?)', controller.blog.index.ssr);};
Webpack 构建配置
添加
${root}/webpack.config.js新增页面 entry 配置
module.exports = {entry: {blog: 'app/web/page/blog/index.jsx'},}
相关辅助代码
${root}/app/web/framework/request.js
'use strict';import axios from 'axios';// axios.defaults.baseURL = 'http://127.0.0.1:7001';axios.defaults.timeout = 15000;axios.defaults.xsrfHeaderName = 'x-csrf-token';axios.defaults.xsrfCookieName = 'csrfToken';export default {async post(url, json, locals = {}) {const headers = {};if (EASY_ENV_IS_NODE) {headers['x-csrf-token'] = locals.csrf;headers.Cookie = `csrfToken=${locals.csrf}`;}const res = await axios.post(`${locals.origin}${url}`, json, { headers });return res.data;},async get(url, locals = {}) {const res = await axios.get(`${locals.origin}${url}`);return res.data;}};
${root}/app/web/component/layout/index.jsx
import React, { Component } from 'react';export default class Layout extends Component {render() {if(EASY_ENV_IS_NODE) {return <html><head><title>{this.props.title}</title><meta charSet="utf-8"></meta><meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"></meta><meta name="keywords" content={this.props.keywords}></meta><meta name="description" content={this.props.description}></meta><link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"></link></head><body><div id="app">{this.props.children}</div></body></html>;}return this.props.children;}}
- Egg 中间件设置公共 locals, 该 locals 会自动与 Egg 的 render 的第二个参数合并传递给 JSX 模板
// ${root}/app/middleware/locals.jsmodule.exports = () => {return async function locale(ctx, next) {ctx.locals.locale = ctx.query.locale || 'cn';ctx.locals.origin = ctx.request.origin;await next();};};// Egg 启用 Middleware localsconst path = require('path');const fs = require('fs');module.exports = app => {const exports = {};exports.middleware = ['locals'];return exports;};
项目代码结构
代码见:https://github.com/easy-team/egg-react-webpack-boilerplate.git

