使用 egg 重構(gòu)工程

背景介紹

簽證工程原來(lái)使用的也是 node 服務(wù)端渲染的模式,只不過(guò)引用的前端資源在另一個(gè)工程里,node 工程和前端工程維護(hù)兩份代碼,node 工程需要進(jìn)行首屏渲染,給window.__INITIAL_STATE__ 屬性賦值準(zhǔn)備好的初始 state宁否,加載前端資源時(shí),react 會(huì)拿 INITIAL_STATE 的數(shù)據(jù)生成虛擬 DOM缀遍,通過(guò) diff 算法判斷 DOM 結(jié)構(gòu)沒(méi)有變化的話即使用服務(wù)端的首屏渲染的頁(yè)面慕匠,并且將頁(yè)面的生命周期函數(shù)、DOM 節(jié)點(diǎn)的事件加入到服務(wù)端渲染的靜態(tài)頁(yè)面上域醇,也就是激活標(biāo)記台谊。

使用 egg 重構(gòu)簽證工程原理和上面的一致,只不過(guò)將原來(lái)的兩個(gè)工程用更合理的方式寫(xiě)在一起(visa_node 工程)譬挚。主要參考線上運(yùn)行的旅行星推官的代碼锅铅。最后實(shí)現(xiàn)了:

  1. 前后端使用同一份代碼,并且通過(guò)環(huán)境判斷處理了node 環(huán)境可能引用的瀏覽器環(huán)境的 window减宣、document 變量的文件(這些文件主要是頁(yè)面 require 進(jìn)的一些立即執(zhí)行的方法中包含了這些變量)盐须;
  2. 接口請(qǐng)求前后端統(tǒng)一使用 axios 庫(kù);
  3. 申請(qǐng)了 beta 機(jī)器和線上機(jī)器進(jìn)行部署漆腌。

下面會(huì)介紹一下服務(wù)端渲染贼邓、egg 框架阶冈、簽證 aggregate 頁(yè)面同構(gòu)核心方法。

ssr 介紹

什么是服務(wù)端渲染(SSR)塑径?

react女坑、vue 這些構(gòu)建客戶端應(yīng)用程序的框架,默認(rèn)情況下可以通過(guò) js 生成 DOM 并操作 DOM统舀。也可以將同一個(gè)組件在服務(wù)器端渲染為靜態(tài)的 HTML 字符串(比如
ReactDOMServer.renderToString )匆骗,直接發(fā)送到瀏覽器,最后將這些靜態(tài)標(biāo)記“激活”為客戶端可交互的應(yīng)用程序誉简。這種服務(wù)端渲染的應(yīng)用程序也被稱為“同構(gòu)”绰筛,因?yàn)閼?yīng)用程序的大部分代碼都可以在服務(wù)器和客戶端上運(yùn)行。

為什么使用服務(wù)器端渲染 (SSR) 描融?

更好的 SEO、更快的首屏渲染衡蚂、便捷開(kāi)發(fā)(前端不需要配置 nginx窿克、代理,只需和后端定義好接口)

egg 框架介紹及簽證重構(gòu)

egg 是什么毛甲?

官網(wǎng)有詳細(xì)介紹年叮。

egg 框架是阿里開(kāi)源的一個(gè)服務(wù)于企業(yè)級(jí)的基礎(chǔ)框架,基于 koa 進(jìn)行二次開(kāi)發(fā)玻募,奉行「約定優(yōu)于配置」只损,即在 koa 框架的基礎(chǔ)上,基于一定的約定七咧,根據(jù)功能差異將代碼放到不同的目錄下管理跃惫,從而降低整體團(tuán)隊(duì)的溝通成本和開(kāi)發(fā)成本。

目錄結(jié)構(gòu)

visa_node 工程的主要的目錄結(jié)構(gòu):

以簽證工程的 aggregate 頁(yè)面為例艾栋,講一下 如何使一份代碼同時(shí)在服務(wù)端和前端運(yùn)行爆存。

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);
};

當(dāng)執(zhí)行 GET /,controller 文件夾下的 home 文件里的 index 方法就會(huì)執(zhí)行蝗砾,url 匹配到 /visanode/aggregate 同理先较。支持目錄級(jí)聯(lián)訪問(wèn):${directoryName}.${fileName}.${functionName}.

controller/aggregate.js

const Controller = require('egg').Controller;
// App 根節(jié)點(diǎn),Store 為最初始定義的那個(gè)悼粮,一般 state 為空對(duì)象
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;

        // 業(yè)務(wù)邏輯闲勺,Store dispatch action,準(zhǔn)備頁(yè)面首次渲染需要的數(shù)據(jù)
        Store.dispatch(queryInit(query));
        await Store.dispatch(fetchFilter(query));

        // renderReactSSR 是在 helper 對(duì)象上擴(kuò)展的一個(gè)屬性扣猫,用于渲染頁(yè)面
        await ctx.helper.renderReactSSR(
            'aggregate.nj',
            App,
            Store,
            `${query}簽證產(chǎn)品推薦`
        );
    }
}

module.exports = aggregateController;

app/extend/helper.js

/**
     * React 服務(wù)端渲染
     * @param {String} viewPath 視圖路徑
     * @param {Object} component 組件
     * @param {Object} store 數(shù)據(jù)源
     * @param {String} title 標(biāo)題
     * @param {Object} other 其他
     * @return {Object} 視圖信息
     */
    renderReactSSR(viewPath, component, store, title = '去哪兒網(wǎng)', 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 對(duì)象上提供了 render 方法菜循,返回值為 Promise ,render 方法會(huì)直接賦值給 ctx.body申尤。 所以我們?cè)?controller 里渲染頁(yè)面的時(shí)候要這樣寫(xiě):

  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 意思是將輸入到頁(yè)面的內(nèi)容通過(guò)一個(gè) safe 的過(guò)濾器轉(zhuǎn)譯一下子眶。window.__INITIAL_STATE__ 存放的是 initialState,前端渲染 DOM 的時(shí)候會(huì)使用這個(gè) 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}; // 導(dǎo)出的這兩個(gè)對(duì)象在 controller 里被引入臭杰,服務(wù)端渲染
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 環(huán)境,運(yùn)行這個(gè)環(huán)境返回 null
    }
    // 有時(shí)候需要在所有頁(yè)面加額外的東西谚中,可以在這里加
    if (thirdpartApp.isccb) {
        document.body.className += ' ccb-bg';
    }

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

webpack 里添加頁(yè)面入口文件:

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

最后會(huì)在 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 內(nèi)置對(duì)象

本地開(kāi)發(fā)

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",
  }

本地開(kāi)發(fā)只需運(yùn)行這兩個(gè)命令:

  1. npm run dev-js: 運(yùn)行前端
  2. npm run dev: 運(yùn)行后端

最后

Egg.js 為企業(yè)級(jí)框架和應(yīng)用而生,我們希望由 Egg.js 孕育出更多上層框架宪塔,幫助開(kāi)發(fā)團(tuán)隊(duì)和開(kāi)發(fā)人員降低開(kāi)發(fā)和維護(hù)成本 ”

相關(guān)鏈接:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市某筐,隨后出現(xiàn)的幾起案子比搭,更是在濱河造成了極大的恐慌,老刑警劉巖南誊,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件身诺,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡抄囚,警方通過(guò)查閱死者的電腦和手機(jī)霉赡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)幔托,“玉大人穴亏,你說(shuō)我怎么就攤上這事≈靥簦” “怎么了嗓化?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)谬哀。 經(jīng)常有香客問(wèn)我蟆湖,道長(zhǎng),這世上最難降的妖魔是什么玻粪? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任隅津,我火速辦了婚禮,結(jié)果婚禮上劲室,老公的妹妹穿的比我還像新娘伦仍。我一直安慰自己,他們只是感情好很洋,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布充蓝。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪谓苟。 梳的紋絲不亂的頭發(fā)上官脓,一...
    開(kāi)封第一講書(shū)人閱讀 49,829評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音涝焙,去河邊找鬼卑笨。 笑死,一個(gè)胖子當(dāng)著我的面吹牛仑撞,可吹牛的內(nèi)容都是我干的赤兴。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼隧哮,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼桶良!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起沮翔,我...
    開(kāi)封第一講書(shū)人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤陨帆,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后采蚀,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體疲牵,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年搏存,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片矢洲。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡璧眠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出读虏,到底是詐尸還是另有隱情责静,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布盖桥,位于F島的核電站灾螃,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏揩徊。R本人自食惡果不足惜腰鬼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望塑荒。 院中可真熱鬧熄赡,春花似錦、人聲如沸齿税。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至拧篮,卻和暖如春词渤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背串绩。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工缺虐, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赏参。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓志笼,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親把篓。 傳聞我的和親對(duì)象是個(gè)殘疾皇子纫溃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

推薦閱讀更多精彩內(nèi)容