环境准备

编写前端代码逻辑

编写 React Router

创建 React Router 实例

添加 ${root}/app/web/page/blog/router/index.jsx

  1. import { Route } from 'react-router-dom';
  2. import Home from './home';
  3. import Detail from './detail';
  4. export default function createRouter() {
  5. return [
  6. {
  7. path: '/detail/:id',
  8. component: Detail
  9. },
  10. {
  11. path: '/',
  12. component: Home
  13. },
  14. {
  15. path: '*',
  16. component: Home
  17. }
  18. ];
  19. }

创建 React Router 路由页面

  • 添加 ${root}/app/web/page/blog/router/home.jsx
  1. import React, { Component } from 'react';
  2. import { connect } from 'react-redux';
  3. import { Link } from 'react-router-dom';
  4. import { hot } from 'react-hot-loader/root'
  5. import request from 'framework/request';
  6. import './home.css'
  7. class Home extends Component {
  8. static async asyncData(context, route) {
  9. return request.get('/api/blog/list', context);
  10. }
  11. render() {
  12. const { list = [] } = this.props;
  13. return <div className="easy-article-list">
  14. <ul>
  15. {list.map(function (item) {
  16. return <li key={item.id} className="easy-article-item">
  17. <h2 className="easy-article-title"><Link to={'/detail/' + item.id}>{item.title}</Link></h2>
  18. <div className="easy-article-summary">{item.summary}</div>
  19. <div className="easy-article-meta">
  20. <span>11Word Count:{item.wordCount} </span>
  21. <span>Create Time: {item.createTime}</span>
  22. </div>
  23. </li>;
  24. })}
  25. </ul>
  26. </div>;
  27. }
  28. }
  29. const mapStateToProps = state => {
  30. return {
  31. list: state.list
  32. };
  33. };
  34. const mapDispatchToProps = dispatch => {
  35. return {};
  36. };
  37. export default connect(mapStateToProps, mapDispatchToProps)(EASY_ENV_IS_DEV ? hot(Home) : Home);
  • 添加 ${root}/app/web/page/blog/router/detail.jsx
  1. 'use strict';
  2. import React, { Component } from 'react';
  3. import { connect } from 'react-redux';
  4. import { hot } from 'react-hot-loader/root'
  5. import request from 'framework/request';
  6. class Detail extends Component {
  7. static async asyncData(locals, route) {
  8. const id = route.match.params.id;
  9. return request.get(`/api/blog/${id}`, locals);
  10. }
  11. render() {
  12. const { article } = this.props;
  13. return article ? <div>
  14. <h2 className="easy-article-detail-title">{article.title}</h2>
  15. <div className="easy-article-info">
  16. <iframe src={article.url} frameBorder="0" width="100%" style={{minHeight: '800px'}}></iframe>
  17. </div>
  18. </div> : null;
  19. }
  20. }
  21. const mapStateToProps = state => {
  22. return {
  23. article: state.article
  24. };
  25. };
  26. export default connect(mapStateToProps)(EASY_ENV_IS_DEV ? hot(Detail) : Detail)

编写 React Router 聚合页面

${root}/app/web/page/blog/view/main.jsx

  1. import React, { Component } from 'react';
  2. import { Link, Switch } from 'react-router-dom';
  3. import { hot } from 'react-hot-loader/root'
  4. import { ARTICLE_LIST, ARTICLE_DETAIL } from '../store/constant';
  5. import Layout from 'component/layout'
  6. import Header from 'component/header'
  7. import Route from '../router/route';
  8. import Home from '../router/home';
  9. import Detail from '../router/detail';
  10. import './main.css';
  11. class Main extends Component {
  12. constructor(props) {
  13. super(props);
  14. this.state = { current: props.url };
  15. }
  16. tabClick(e) {
  17. console.log('click', e.target);
  18. }
  19. render() {
  20. return <Layout {...this.props}>
  21. <Header></Header>
  22. <ul className="menu-tab">
  23. <li onClick={this.tabClick.bind(this)}><Link to="/">Home</Link></li>
  24. </ul>
  25. <Switch>
  26. <Route type={ARTICLE_DETAIL} path="/detail/:id" component={Detail} />
  27. <Route type={ARTICLE_LIST} path="/" component={Home}/>
  28. </Switch>
  29. </Layout>;
  30. }
  31. }
  32. export default EASY_ENV_IS_DEV ? hot(Main) : Main;

编写 React Router 路由切换 Hook 高阶组件

${root}/app/web/page/blog/router/route.jsx

  1. 'use strict';
  2. import React, { Component } from 'react';
  3. import { Route } from 'react-router-dom';
  4. import { connect } from 'react-redux';
  5. import { update } from '../store/actions';
  6. class WrapperRoute extends Component {
  7. async componentWillReceiveProps(nextProps) {
  8. const { type, locals, component, computedMatch, updateState } = nextProps;
  9. if (computedMatch.url !== this.props.computedMatch.url) {
  10. const { asyncData } = component;
  11. if (asyncData) {
  12. const data = await asyncData(locals, { match: computedMatch });
  13. updateState(type, data);
  14. }
  15. }
  16. }
  17. render() {
  18. return <Route {...this.props} />
  19. }
  20. }
  21. const mapStateToProps = state => {
  22. return {
  23. locals: state
  24. }
  25. };
  26. const mapDispatchToProps = dispatch => {
  27. return {
  28. updateState: (type, data) => dispatch(update(type, data))
  29. };
  30. };
  31. export default connect(mapStateToProps, mapDispatchToProps)(WrapperRoute);

编写 React Store

创建 React Store 实例

  1. // ${root}/app/web/page/blog/store/constant.js
  2. export const ARTICLE_LIST = 'ARTICLE_LIST';
  3. export const ARTICLE_DETAIL = 'ARTICLE_DETAIL';
  4. export const ADD = 'ADD';
  5. export const DEL = 'DEL';
  6. // ${root}/app/web/page/blog/store/actions.js
  7. export const update = (type, data) => {
  8. return {
  9. type,
  10. data
  11. };
  12. }
  13. // ${root}/app/web/page/blog/store/reducers.js
  14. import { ARTICLE_DETAIL, ARTICLE_LIST, ADD, DEL } from './constant';
  15. export default function update(state, action) {
  16. const newState = Object.assign({}, state);
  17. if (action.type === ADD) {
  18. const list = Array.isArray(action.data) ? action.data : [action.data];
  19. newState.list = [...newState.list, ...list];
  20. } else if (action.type === DEL) {
  21. newState.list = newState.list.filter(data => {
  22. return data.id !== action.id;
  23. });
  24. } else if (action.type === ARTICLE_LIST) {
  25. newState.list = action.data.list;
  26. newState.total = action.data.total;
  27. } else if (action.type === ARTICLE_DETAIL) {
  28. newState.article = action.data.article;
  29. }
  30. return newState;
  31. }
  32. // ${root}/app/web/page/blog/store/index.js
  33. import { createStore } from 'redux';
  34. import reducers from './reducers';
  35. export default function(initalState){
  36. return createStore(reducers, initalState);
  37. }

编写 React SPA 入口代码

${root}/app/web/page/blog/index.jsx webpack.config.js 的 entry 指向该文件。

  1. import React, { Component } from 'react';
  2. import { Provider } from 'react-redux';
  3. import { BrowserRouter, StaticRouter } from 'react-router-dom';
  4. import { matchRoutes } from 'react-router-config';
  5. import createStore from './store'
  6. import createRouter from './router'
  7. import Main from './view/main'
  8. import '../../asset/css/blog.css'
  9. // 直接渲染 React Component 组件,JSX 文件结尾的 Webpack entry 自动使用 react-entry-template-loader
  10. export default class Entry extends Component {
  11. static async asyncData(locals) {
  12. const router = createRouter();
  13. const url = locals.url;
  14. const matchRouteList = matchRoutes(router, url);
  15. const promises = matchRouteList.map(matchRoute=> {
  16. const componentAsyncData = matchRoute.route.component.asyncData;
  17. return componentAsyncData instanceof Function ? componentAsyncData(locals, matchRoute) : null;
  18. });
  19. const list = await Promise.all(promises);
  20. return list.reduce((item, result) => {
  21. return { ...result, ...item}
  22. }, {});
  23. }
  24. render() {
  25. if (EASY_ENV_IS_BROWSER) {
  26. const store = createStore(window.__INITIAL_STATE__);
  27. const { url } = store.getState();
  28. return <Provider store={store}>
  29. <BrowserRouter location={url}>
  30. <Main></Main>
  31. </BrowserRouter>
  32. </Provider>;
  33. }
  34. const store = createStore(this.props);
  35. const { url } = store.getState();
  36. return <Provider store={store}>
  37. <StaticRouter location={url} context={{}}>
  38. <Main></Main>
  39. </StaticRouter>
  40. </Provider>;
  41. }
  42. }

编写 Node 代码

添加 ${root}/app/controller/blog/index.js Node 代码

  1. 'usestrict';
  2. const egg = require('egg');
  3. module.exports = class IndexController extends egg.Controller {
  4. async ssr(ctx) {
  5. // 1. ctx.render 方法是 egg-view-react-ssr 插件提供实现
  6. // 2. ctx.render 第一个参数 blog.js 是 webpack.config.js entry 配置的 blog 构建后 JSBundle 文件
  7. // 3. ctx.render 第二个参数 { url, title ...} 是渲染数据,会与公共 ctx.locals 合并
  8. await ctx.render('blog.js', {
  9. url: ctx.url,
  10. title: 'Egg React ',
  11. keywords: 'Egg,React,Egg React,Egg React SSR, Egg React CSR, Server Side Render, Client Side Render',
  12. description: 'Egg + React + Webpack 服务端渲染 SSR (Server Side Render) 和 前端渲染 CSR (Client Side Render) 工程骨架项目'
  13. });
  14. }
  15. async list() {
  16. this.ctx.body = this.service.article.getArtilceList();
  17. }
  18. async detail(ctx) {
  19. const id = ctx.params.id;
  20. const article = this.service.article.getArticle(Number(id));
  21. ctx.body = { article };
  22. }
  23. };

Egg 路由配置

添加 ${root}/app/router.js Egg 路由配置

  1. module.exports = app => {
  2. const { router, controller } = app;
  3. router.get('/api/blog/list', controller.blog.index.list);
  4. router.get('/api/blog/:id', controller.blog.index.detail);
  5. router.get('/(.*?)', controller.blog.index.ssr);
  6. };

Webpack 构建配置

添加 ${root}/webpack.config.js 新增页面 entry 配置

  1. module.exports = {
  2. entry: {
  3. blog: 'app/web/page/blog/index.jsx'
  4. },
  5. }

相关辅助代码

  • ${root}/app/web/framework/request.js
  1. 'use strict';
  2. import axios from 'axios';
  3. // axios.defaults.baseURL = 'http://127.0.0.1:7001';
  4. axios.defaults.timeout = 15000;
  5. axios.defaults.xsrfHeaderName = 'x-csrf-token';
  6. axios.defaults.xsrfCookieName = 'csrfToken';
  7. export default {
  8. async post(url, json, locals = {}) {
  9. const headers = {};
  10. if (EASY_ENV_IS_NODE) {
  11. headers['x-csrf-token'] = locals.csrf;
  12. headers.Cookie = `csrfToken=${locals.csrf}`;
  13. }
  14. const res = await axios.post(`${locals.origin}${url}`, json, { headers });
  15. return res.data;
  16. },
  17. async get(url, locals = {}) {
  18. const res = await axios.get(`${locals.origin}${url}`);
  19. return res.data;
  20. }
  21. };
  • ${root}/app/web/component/layout/index.jsx
  1. import React, { Component } from 'react';
  2. export default class Layout extends Component {
  3. render() {
  4. if(EASY_ENV_IS_NODE) {
  5. return <html>
  6. <head>
  7. <title>{this.props.title}</title>
  8. <meta charSet="utf-8"></meta>
  9. <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui"></meta>
  10. <meta name="keywords" content={this.props.keywords}></meta>
  11. <meta name="description" content={this.props.description}></meta>
  12. <link rel="shortcut icon" href="/favicon.ico" type="image/x-icon"></link>
  13. </head>
  14. <body><div id="app">{this.props.children}</div></body>
  15. </html>;
  16. }
  17. return this.props.children;
  18. }
  19. }
  • Egg 中间件设置公共 locals, 该 locals 会自动与 Egg 的 render 的第二个参数合并传递给 JSX 模板
  1. // ${root}/app/middleware/locals.js
  2. module.exports = () => {
  3. return async function locale(ctx, next) {
  4. ctx.locals.locale = ctx.query.locale || 'cn';
  5. ctx.locals.origin = ctx.request.origin;
  6. await next();
  7. };
  8. };
  9. // Egg 启用 Middleware locals
  10. const path = require('path');
  11. const fs = require('fs');
  12. module.exports = app => {
  13. const exports = {};
  14. exports.middleware = [
  15. 'locals'
  16. ];
  17. return exports;
  18. };

项目代码结构

代码见:https://github.com/easy-team/egg-react-webpack-boilerplate.git

egg-react-spa.png