使用 egg 重构工程

背景介绍

签证工程原来使用的也是 node 服务端渲染的模式,只不过引用的前端资源在另一个工程里,node 工程和前端工程维护两份代码,node 工程需要进行首屏渲染,给window.__INITIAL_STATE__ 属性赋值准备好的初始 state,加载前端资源时,react 会拿 INITIAL_STATE 的数据生成虚拟 DOM,通过 diff 算法判断 DOM 结构没有变化的话即使用服务端的首屏渲染的页面,并且将页面的生命周期函数、DOM 节点的事件加入到服务端渲染的静态页面上,也就是激活标记。

使用 egg 重构签证工程原理和上面的一致,只不过将原来的两个工程用更合理的方式写在一起(visa_node 工程)。主要参考线上运行的旅行星推官的代码。最后实现了:

  1. 前后端使用同一份代码,并且通过环境判断处理了node 环境可能引用的浏览器环境的 window、document 变量的文件(这些文件主要是页面 require 进的一些立即执行的方法中包含了这些变量);
  2. 接口请求前后端统一使用 axios 库;
  3. 申请了 beta 机器和线上机器进行部署。

下面会介绍一下服务端渲染、egg 框架、签证 aggregate 页面同构核心方法。

ssr 介绍

什么是服务端渲染(SSR)?

react、vue 这些构建客户端应用程序的框架,默认情况下可以通过 js 生成 DOM 并操作 DOM。也可以将同一个组件在服务器端渲染为静态的 HTML 字符串(比如
ReactDOMServer.renderToString ),直接发送到浏览器,最后将这些静态标记“激活”为客户端可交互的应用程序。这种服务端渲染的应用程序也被称为“同构”,因为应用程序的大部分代码都可以在服务器和客户端上运行。

为什么使用服务器端渲染 (SSR) ?

更好的 SEO、更快的首屏渲染、便捷开发(前端不需要配置 nginx、代理,只需和后端定义好接口)

egg 框架介绍及签证重构

egg 是什么?

官网有详细介绍。

egg 框架是阿里开源的一个服务于企业级的基础框架,基于 koa 进行二次开发,奉行「约定优于配置」,即在 koa 框架的基础上,基于一定的约定,根据功能差异将代码放到不同的目录下管理,从而降低整体团队的沟通成本和开发成本。

目录结构

visa_node 工程的主要的目录结构:

以签证工程的 aggregate 页面为例,讲一下 如何使一份代码同时在服务端和前端运行。

router.js

/**
 * @param {Egg.Application} app - egg application
 */
module.exports = app => {
    const {router, controller, middleware} = app;

    router.get('/', controller.home.index);
    router.get('/visanode/aggregate', controller.aggregate.index);
};

当执行 GET /,controller 文件夹下的 home 文件里的 index 方法就会执行,url 匹配到 /visanode/aggregate 同理。支持目录级联访问:${directoryName}.${fileName}.${functionName}.

controller/aggregate.js

const Controller = require('egg').Controller;
// App 根节点,Store 为最初始定义的那个,一般 state 为空对象
const {App, Store} = require('../../src/page/aggregate/index.js');
const {queryInit, fetchFilter} = require('../../src/page/aggregate/actions.js');

class aggregateController extends Controller {
    async index() {
        const {ctx} = this;
        const {query} = ctx.request.query;

        // 业务逻辑,Store dispatch action,准备页面首次渲染需要的数据
        Store.dispatch(queryInit(query));
        await Store.dispatch(fetchFilter(query));

        // renderReactSSR 是在 helper 对象上扩展的一个属性,用于渲染页面
        await ctx.helper.renderReactSSR(
            'aggregate.nj',
            App,
            Store,
            `${query}签证产品推荐`
        );
    }
}

module.exports = aggregateController;

app/extend/helper.js

/**
     * React 服务端渲染
     * @param {String} viewPath 视图路径
     * @param {Object} component 组件
     * @param {Object} store 数据源
     * @param {String} title 标题
     * @param {Object} other 其他
     * @return {Object} 视图信息
     */
    renderReactSSR(viewPath, component, store, title = '去哪儿网', other = {}) {
        const reactDOM = ReactDOMServer.renderToString(
            React.createElement(
                Provider,
                {store},
                React.createElement(component)
            )
        );
        return this.ctx.render(viewPath, {
            title,
            reactDOM,
            initialState: JSON.stringify(store.getState()),
            skString: global.skString,
            ...other
        });
    }

this.ctx.render(viewPath, option) : 框架在 ctx 对象上提供了 render 方法,返回值为 Promise ,render 方法会直接赋值给 ctx.body。 所以我们在 controller 里渲染页面的时候要这样写:

  await ctx.helper.renderReactSSR(...);

view模板渲染

app/view/aggregate.nj

{% extends "./layout.html" %}

{% block header %}
    <link rel="stylesheet" href="{{ ctx.loadManifest('aggregate.css') }}" />
{% endblock %}

{% block body %}
    <div class="yo-root" id="app">{{ reactDOM | safe }}</div>
    <script> window.__INITIAL_STATE__ = {{ initialState | safe }}; </script>
    <script type="text/javascript" src="{{ ctx.loadManifest('vendor.js') }}"></script>
    <script type="text/javascript" src="{{ ctx.loadManifest('aggregate.js') }}"></script>
{% endblock %}

ctx.loadManifest 加载的是前端使用 webpack 打包后的资源。| safe 意思是将输入到页面的内容通过一个 safe 的过滤器转译一下。window.__INITIAL_STATE__ 存放的是 initialState,前端渲染 DOM 的时候会使用这个 state。

src/page/aggregate/index.js

import Store from './store';
import hydrateToPage from 'util/hydrateToPage';
import React from 'react';

import Header from './components/header.js';
import List from './components/list.js';

import 'style/page/aggregate.scss';
require('./ui/immersive'); // 适配

const App = () => {
    return (
        <div className="g-wrap">
            <Header />
            <List />
        </div>
    );
};

export {App, Store}; // 导出的这两个对象在 controller 里被引入,服务端渲染
hydrateToPage(App, Store); // 前端渲染方法

src/util/hydrateToPage.js

import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import isNodeEnv from './isNodeEnv';

const thirdpartApp = isNodeEnv ? null : require('./thirdparty/thirdpartApp').default;

export default (Component, store) => {
    if (isNodeEnv) {
        return null; // 如果是 node 环境,运行这个环境返回 null
    }
    // 有时候需要在所有页面加额外的东西,可以在这里加
    if (thirdpartApp.isccb) {
        document.body.className += ' ccb-bg';
    }

    ReactDOM.render(
        <Provider store={store}>
            <Component />
        </Provider>,
        document.getElementById('app')
    );
};

webpack 里添加页面入口文件:

entry: {
   aggregate: './src/page/aggregate/index.js',
}

最后会在 prd 目录下的 manifest.json文件中生成下面的资源映射,view 模板渲染中引的便是这里的前端资源:

"aggregate.css": "http://q.dev.qunarzz.com:7013/prd/aggregate@dev.css",
"aggregate.js": "http://q.dev.qunarzz.com:7013/prd/aggregate@dev.js",
"vendor.js": "http://q.dev.qunarzz.com:7013/prd/vendor.bundle.js"

egg 内置对象

本地开发

package.json

"scripts": {
    "dev": "export VISA_PORT=7012 && egg-bin dev --port 7012",
    "dev-js": "webpack-dev-server --config webpack.config.dev.js --port 7013",
  }

本地开发只需运行这两个命令:

  1. npm run dev-js: 运行前端
  2. npm run dev: 运行后端

最后

Egg.js 为企业级框架和应用而生,我们希望由 Egg.js 孕育出更多上层框架,帮助开发团队和开发人员降低开发和维护成本 ”

相关链接:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,589评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,615评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,933评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,976评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,999评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,775评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,474评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,359评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,854评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,007评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,146评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,826评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,484评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,029评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,153评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,420评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,107评论 2 356

推荐阅读更多精彩内容