一、koa+React服務端渲染:Hello World

目標

以koa為后端服務器,實現react的服務端渲染。最終的目的是想要實現一個admin的后臺單頁面應用和一個移動端得單頁面。這里先從admin開始。當用戶訪問 /admin 這個地址的時候,在服務端渲染好頁面,然后返回。

項目目錄

|-- app
??|-- controller
????|-- admin.js (在這里面調用ctx.render('admin')實現頁面渲染)
??|-- middleware
????|-- react_view.js (在這里給koa的context添加render方法,已確保在controller里面可以調用ctx.render)
??|- view
????|-- admin.js(這個是編譯后的,可以直接用于服務端渲染的文件)
??|-- web
????|-- component(存放react組件)
????|-- page
??????|-- browser
????????|-- admin.js
??????|-- server
????????|-- admin.js
|-- build (存放build后的文件)

項目設置

  1. 創建項目目錄
makedir react-isomorphic
  1. 進入目錄
cd react-isomorphic
  1. 初始化
npm init

這一步會問你一些問題,全部按Enter就好

  1. 安裝react和koa相關的包
npm install koa koa-router koa-static react react-dom --save
  1. 安裝webpack和編譯所需要的包
npm install webpack webpack-cli babel-core babel-preset-env babel-preset-react  babel-loader clean-webpack-plugin --save-dev

babel-core 是babel的核心包
babel-preset-env 用于將es2015+編譯成es5
babel-preset-react 用于編譯react的jsx語法
babel-loader 用webpack和babel編譯js
clean-webpack-plugin 用于編譯前,清空編譯目錄

  1. 配置babel
    在項目根目錄下創建文件.babelrc,并填入內容:
{
  "presets": ["env", "react"]
}
  1. 配置webpack
    在項目根目錄下創建 webpack.config.js
const path = require('path');
const CleanWebpackPlugin = require('clean-webpack-plugin');

// 客戶端 react 應用的入口文件
const adminBrowserFilePath = path.resolve(__dirname, './app/web/page/browser/admin');
// 服務端 react 應用的入口文件
const adminServerFilePath = path.resolve(__dirname, './app/web/page/server/admin');
const browserBuildPath = path.resolve(__dirname, './build');
const serverBuildPath = path.resolve(__dirname, './app/view');

module.exports = [
  {
    name: 'browser',
    entry: {
      admin: adminBrowserFilePath
    },
    output: {
      path: browserBuildPath,
      filename: 'static/js/[name].js',
      chunkFilename: 'static/js/[name].chunk.js',
      publicPath: '/'
    },
    target: 'web',
    resolve: {
      extensions: ['.js', '.jsx']
    },
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /(node_modules\/)/,
          use: {
            loader: 'babel-loader'
          }
        }
      ]
    },
    plugins: [
      new CleanWebpackPlugin(['build'])
    ]
  },
  {
    name: 'server',
    entry: {
      admin: adminServerFilePath
    },
    output: {
      path: serverBuildPath,
      filename: '[name].js',
      publicPath: '/',
      libraryTarget: 'commonjs'
    },
    target: 'node',
    resolve: {
      extensions: ['.js', '.jsx']
    },
    module: {
      rules: [
        {
          test: /\.(js|jsx)$/,
          exclude: /(node_modules\/)/,
          use: {
            loader: 'babel-loader'
          }
        }
      ]
    },
    plugins: [
      new CleanWebpackPlugin(['app/view'])
    ]
  }
];

編寫應用

./app/web/component/app/Admin.js

import React from 'react';

const App = ({ msg }) => {
  return (
    <div>Hello { msg }</div>
  )
};

export default App;

./app/web/component/app/layout/AdminLayout.js

// 這個是頁面的layout文件
import React from 'react';

const Layout = ({state, children}) => {
  return (
    <html>
      <head>
        <title>Admin</title>
      </head>
      <body>
       { children }
       <script dangerouslySetInnerHTML={{__html: `window.__STATE__ = ${JSON.stringify(state)}`}}/>
       <script src="/static/js/admin.js"></script>
      </body>
    </html>
  );
};

export default Layout;

./app/web/page/browser/admin.js

import React from 'react';
import ReactDOM from 'react-dom';

import AdminApp from '../../component/app/Admin';

ReactDOM.hydrate((<AdminApp {...window.__STATE__}/>), document.getElementById('root'));

./app/web/page/server/admin.js
這個是服務段渲染的入口文件,我門將通過后臺直接給react傳入初始屬性(即context,一個普通的對象)。與客戶段渲染不同的是,客戶端通常是執行完js后,通過ajax向服務器請求初始狀態相關的數據。比如:一個用于展示個人信息的頁面,服務端渲染的話,出來的結果直接是一個帶有個人信息的html文本,而客戶端則需要發送一次請求到后端獲取,然后再渲染。

import React from 'react';

import AdminLayout from '../../component/layout/AdminLayout';
import AdminApp from '../../component/app/Admin';

const server = context => {
  return (
    <AdminLayout>
      <AdminApp {...context}/>
    </AdminLayout>
  )
};
export default server;

目前為止一個最簡單的React頁面就完成了,但是為了和koa整合起來,還需要實現一個為koa對象實現一個render方法。這里我把實現代碼放到middleware目錄下。
./app/middleware/react_view.js

const assert = require('assert');
const path = require('path');
const fs = require('fs');
const ReactDOMServer = require('react-dom/server');

const defaults = {
  view: path.resolve(process.cwd(), 'view'),
  extname: 'js'
};

module.exports = (options, app) => {
  options = options || {};
  options = Object.assign(options, defaults);
  assert(typeof options.view === 'string', 'options.view required, and must be a string');
  assert(fs.existsSync(options.view), `Directory ${options.view} not exists`);
  options.extname = options.extname.trim().replace(/^\.?/, '.');
  app.context.render = function (filename, _context) {
    if (!path.extname(filename)) {
      filename += options.extname;
    }
    let filepath = path.isAbsolute(filename) ? filename : path.resolve(options.view, filename);
    const context = Object.assign({}, this.state, _context);

    try {
      // 獲取server/admin.js編譯后的文件
      let view = require(filepath);
      view = view.default || view;
      // view是一個函數,調用后返回一個react組件,然后把react組件渲染成html字符串
      this.body = ReactDOMServer.renderToString(view(context));
      this.type = 'html';
    } catch (err) {
      err.code = 'REACT';
      throw err;
    }
  }
};

然后需要做的是,實現一個controller用于返回頁面給前端
./app/controller/admin.js

exports.admin = ctx => {
  ctx.render('admin', { msg: 'World' });
};

controller寫好了以后現在需要配置路由
./app/router.js

const admin = require('./controller/admin');

module.exports = app => {
  const { router } = app;
  router.get('/admin', admin.admin);
};

然后實例化一個koa對象
./app/app.js

const Koa = require('koa');
const serve = require('koa-static');
const Router = require('koa-router');
const path = require('path');
const router = new Router();
const routes = require('./router');
const reactView = require('./middleware/react_view');

const app = new Koa();

// 給koa對象增加一個router屬性
Object.defineProperties(app, {
  router: {
    get() {
      return router;
    }
  }
});

// 給koa的上下文ctx對象增加render方法
reactView({
  view: path.resolve(__dirname, './view')
}, app);

routes(app);

app.use(serve(path.resolve(__dirname, '../build')));

app.use(router.routes());

app.on('error', function(err, ctx){
  log.error('server error', err, ctx);
});


module.exports = app;

以上所有的代碼已經完成,現在就是設置啟動端口
./index.js

require('./app/app')
  .listen(process.env.PORT || 3000, () => {
    console.log('Server is running on 3000');
  });

現在我們添加兩個命令到package.json,用于編譯react和啟動應用。

{
...
  "scripts": {
    "build": "webpack",
    "start": "node index.js"
  }
...
}

到現在應用就可以運行了,在當前項目根目錄下執行命令

npm run build && npm run start

打開瀏覽器,輸入http://localhost:3000/admin,如果沒有錯誤的話,你應該能看到


項目地址:https://github.com/leitc/isomorphic-react/tree/0.1

總結

目前只實現了基本的hello world頁面,還缺少路由跳轉,樣式的引入,熱更新,和部署流程,后面后持續加入。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容