TypeScript + Webpack + Koa 搭建 React 服务端渲染

栏目: Node.js · 发布时间: 4年前

内容简介:也可以使用基本项目目录安装一些依赖,想尝试 React Hooks,所以安装了 next 版本,服务端用 koa
react-dom/server
import { hydrate } from 'react-dom'

也可以使用 babel-core/register 让 React 代码能够运行在服务端,具体参考: segmentfault.com/a/119000001…

新建项目

$ mkdir customize-server-side-render 
$ cd customize-server-side-render
# 初始化一个 package.json
$ yarn init -y
复制代码

基本项目目录

|-- customize-server-side-render
    |-- config     webpack 打包配置文件和路径配置文件
        |-- paths   路径配置文件
        |-- webpack.base.js     公用的 webpack 打包配置
        |-- webpack.client.js   打包给客户端使用的脚本
        |-- webpack.server.js   打包给 node 使用的脚本
    |-- src        源码
        |-- App.tsx
        |-- index.tsx    客户端启动入口
        !-- server.tsx   服务端启动入口
    |-- server      koa 启动 http 服务代码
    |-- public      静态资源
    |-- dist        webpack 打包后的文件
    |-- package.json
    |-- tsconfig.json
    |-- tslint.json
    ...
复制代码

安装一些依赖,想尝试 React Hooks,所以安装了 next 版本,服务端用 koa

$ yarn add react@next react-dom@next koa koa-router
$ yarn add webpack webpack-cli ts-loader typescipt -D
复制代码

首先在 config 下面创建一个 paths.js,声明了有用到的 paths

const path = require('path');

function resolveResource(filename) {
  return path.resolve(__dirname, `../${filename}`);
}

module.exports = {
  clientEntry: resolveResource('src/index.tsx'),
  serverEntry: resolveResource('src/server.tsx'),
  sourceDir: resolveResource('src'),
  distDir: resolveResource('dist'),
};
复制代码
  • webpack.base.js
const paths = require('./paths');

module.exports = {
  mode: 'development',
  output: {
    path: paths.distDir,
    filename: '[name].js',
  },
  resolve: {
    extensions: ['.ts', '.tsx'],
  },
  module: {
    rules: [
      {
        test: /\.tsx?/,
        loader: 'ts-loader',
        exclude: /node_modules/,
      }
    ],
  },
};

复制代码

利用 webpack-merge 合并 webpack 配置

$ yarn add webpack-merge -D
复制代码
  • webpack.client.js
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');
const paths = require('./paths');

module.exports = merge(baseConfig, {
  target: 'web',
  entry: paths.clientEntry,
})

复制代码

运行 webpack --config config/webpack.client.js,打包出在客户端运行的脚本

打包客户端代码出现问题:

ERROR in ./node_modules/react-dom/cjs/react-dom.development.js
Module not found: Error: Can't resolve 'object-assign' in '/Users/logan/Projects/backend/customize-server-side-render/node_modules/react-dom/cjs'
 @ ./node_modules/react-dom/cjs/react-dom.development.js 19:14-38
 @ ./node_modules/react-dom/index.js
 @ ./src/index.tsx
 ...
复制代码

打包时出现了一些依赖未安装的问题,是开发版本的 react 引入的库,这里都给他安装一下

$ yarn add object-assign prop-types scheduler -D
复制代码

依然出现上面的问题

猜测可能是没有引入 babel 的原因

最终结果并不是,是由于 resolve.extensions 中我只配置了 ts 和 tsx 结尾的文件类型,但是没有 js 和 jsx 结尾的。修改 webpack.base.js

const paths = require('./paths');

module.exports = {
  output: {
    path: paths.distDir,
    filename: '[name].js',
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx'],
  },
  module: {
    rules: [
      {
        test: /\.tsx?/,
        include: paths.sourceDir,
        exclude: /node_modules/,
        loader: 'ts-loader',
      },
    ],
  },
};
复制代码

启动 Node 服务

server 下面创建一个 index.js

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

router.get('/', (ctx) => {
  ctx.body = 'Hello world Koa';
});

app.use(router.routes());

app.listen(3000);

console.log('Application is running on http://127.0.0.1:3000');
复制代码

运行 node server/index.js ,看见服务启动正常,但是修改了 server 下面的 index.js 无法自己重启 node 服务,所以准备利用 nodemon 运行

$ yarn add nodemon -D
复制代码

修改启动脚本为

$ nodemon server/index.js
复制代码

OK, node 服务能在修改后自己重启。

编译 React 在服务端

const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');
const paths = require('./paths');

module.exports = merge(baseConfig, {
  target: 'node',
  entry: {
    'server-entry': paths.serverEntry,
  },
})
复制代码

将打包后的 server-entry.js 在 server/index.js 中引入, 利用 react-dom/server 模块中的 renderToString 方法渲染成 html

const ReactDOMServer = require('react-dom/server');
const serverEntry = require('../dist/server-entry');

const str = ReactDOMServer.renderToString(serverEntry);
复制代码

但是发现 require 进来的 serverEntry 只是一个空对象。

webpack-node-externals
commonjs
const merge = require('webpack-merge');
const nodeExternals = require('webpack-node-externals');
const baseConfig = require('./webpack.base');
const paths = require('./paths');

module.exports = merge(baseConfig, {
  target: 'node',
  entry: {
    'server-entry': paths.serverEntry,
  },
  output: {
    libraryTarget: 'commonjs',
  },
  externals: [nodeExternals()],
})

复制代码

然后 require server-entry 的方式变为:

const serverEntry = require('../dist/server-entry').default;
复制代码

然后就可以看见浏览器上显示出了 的内容,但是每次运行都要 yarn dev:clientyarn dev:serveryarn dev ,而且还不能用 && 连接,因为 yarn dev:clientwebpack --watch 会卡在当前进程,所以可以用 npm-run-all 一次运行三个脚本

$ yarn add npm-run-all -D
复制代码

最终启动脚本变为:

"start": "npm-run-all --parallel \"dev\" \"dev:client\" \"dev:server\""
复制代码

利用 HTML 模板文件

public 下面新建一个 index.html

<!DOCTYPE html>
<html lang="en" dir="ltr">
  <head>
    <meta charset="utf-8">
    <title>Customize Server side render</title>
  </head>
  <body>
    <!-- 服务端会替换 <slot />,也可以使用 koa-views 等插件实现 -->
    <div class="app-container"><slot /></div>
  </body>
</html>
复制代码

修改 server/index.js 内容:

const Koa = require('koa');
const Router = require('koa-router');
// 新增
const fs = require('fs');
const path = require('path');
const ReactDOMServer = require('react-dom/server');
const serverEntry = require('../dist/server-entry').default;

const app = new Koa();
const router = new Router();

// 新增
const template = fs.readFileSync(path.resolve(__dirname, '../public/index.html'), 'utf8');

router.get('*', (ctx) => {
  // 新增
  const str = ReactDOMServer.renderToString(serverEntry);
  ctx.body = template.replace('<slot />', str);
  ctx.type = 'html';
});

app.use(router.routes());

app.listen(3000);

console.log('Application is running on http://127.0.0.1:3000');

复制代码

但是 react-dom/server 模块只是将 jsx 渲染成 html,但是他没有 document 等 html 元素,所以他并没有绑定点击事件等,所以需要将代码在浏览器端再运行一遍(浏览器激活)

将浏览器再运行一次的原理就是,将 webpack.client.js 的 output 中 path 设置为 public 目录,然后将 public 目录设置为 koa 中的静态资源目录。

  • public 设置为静态资源目录
const koaStatic = require('koa-static');

const app = new Koa();

// 这句一定要在 router.get('*') 之前,不然请求到 router.get('*') 中直接返回了,不会再找 public 中的静态资源
app.use(koaStatic(path.resolve(__dirname, '../public')));
复制代码
  • index.html 中引入即可
<script type="text/javascript" src='/app.js'></script>
复制代码

这样子,客户端运行的时候就回去加载 public/app.js ,从而达到客户端激活的目的

  • 修改 webpack.client.js
const merge = require('webpack-merge');
const baseConfig = require('./webpack.base');
const paths = require('./paths');

module.exports = merge(baseConfig, {
  target: 'web',
  entry: {
    app: paths.clientEntry,
  },
  output: {
    // 指向 public 目录
    path: paths.publicDir,
  },
});

复制代码

但是,这样子访问 http://localhost: 3000 时,他走的不是 router.get('/') , 而是 public/index.html,这个有很多种方式解决,比如修改 public/index.html -> public/template.html 等。

加载样式

安装依赖

$ yarn add style-loader css-loader scss-loader node-sass -D
复制代码

客户端打包没问题,但是 style-loader 需要 window 对象,但是 webpack.server.js 是打包给 node 用的,没有 window ,会报错

webpack:///./node_modules/style-loader/lib/addStyles.js?:23
	return window && document && document.all && !window.atob;
	^

ReferenceError: window is not defined
    at eval (webpack:///./node_modules/style-loader/lib/addStyles.js?:23:2)
    at eval (webpack:///./node_modules/style-loader/lib/addStyles.js?:12:46)
    at module.exports (webpack:///./node_modules/style-loader/lib/addStyles.js?:80:88)
    at eval (webpack:///./src/components/Container/style.scss?:16:140)
    at Object../src/components/Container/style.scss (/Users/logan/Projects/backend/customize-server-side-render/dist/server-entry.js:165:1)
    at __webpack_require__ (/Users/logan/Projects/backend/customize-server-side-render/dist/server-entry.js:20:30)
    at eval (webpack:///./src/components/Container/index.tsx?:4:69)
    at Module../src/components/Container/index.tsx (/Users/logan/Projects/backend/customize-server-side-render/dist/server-entry.js:154:1)
    at __webpack_require__ (/Users/logan/Projects/backend/customize-server-side-render/dist/server-entry.js:20:30)
    at eval (webpack:///./src/App.tsx?:6:79)
复制代码

所以将样式 loader 拆开,在 webpack.server.js 中用 isomorphic-style-loader 代替 style-loader

路由同构

服务端渲染时,不能使用 BrowswerRouter 或者 HashRouter ,而是 StaticRouter ,参考地址:

可以看到, StaticRouter 需要用到请求参数中的 path 甚至 context ,因此需要对结构做一些改变,让 node 启动的入口直接引入 <App /> ,而不是通过 require 加载 webpack 打包过的

  • src 下面新建 server 目录,新建 index.tsx ,这样服务端的内容也能够使用 typescript

  • server/index.js 内容转入 src/server/index.tsx ,安装 @types/node

  • 原本用 require 引入的方式都改为 import

  • 修改 paths 下面的 serverEntry ,修改 src/server/index.tsx 下面引用的文件路径,利用 typescript 以后,路劲引用就不用 path.resolve(__dirname, 'path/to/file') ,直接项目目录下文件夹开始就行,如果引用 project/public 下面的 public 目录,直接 public 即可。

  • 修改后的 src/server/index.tsx 为:

import * as React from 'react';
import * as fs from 'fs';
import Koa from 'koa';
import Router from 'koa-router';
import koaStatic from 'koa-static';
import * as ReactDOMServer from 'react-dom/server';
import App from '../App';

const app = new Koa();
const router = new Router();

const template = fs.readFileSync('public/template.html', 'utf8');

app.use(koaStatic('public', {
  gzip: true,
  maxage: 10,
}));

router.get('*', (ctx) => {
  const str = ReactDOMServer.renderToString(<App />);
  ctx.body = template.replace('<slot />', str);
  ctx.type = 'html';
});

app.use(router.routes());


app.listen(3000);

console.log('Application is running on http://127.0.0.1:3000');

复制代码

修改 renderToString 的过程

const str = ReactDOMServer.renderToString(
    <StaticRouter location={ctx.req.url} context={{}}>
      <App />
    </StaticRouter>
  );
复制代码

这是服务端添加了 Router ,但是这样子直接运行的话,浏览器会报错:

You should not use <Route> or withRouter() outside a <Router>
复制代码

这是因为服务端添加了 StaticRouter ,但是客户端外层却并没有添加一个 Router

修改 src/index.tsx

import * as React from 'react';
import { hydrate } from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';

hydrate(
  (
    <BrowserRouter>
      <App />
    </BrowserRouter>
  ),
  document.querySelector('.app-container') as HTMLElement,
);
复制代码

添加路由成功!

运用这些写了一个支持 React 服务端渲染的小库: github.com/wokeyi/serv… (随手点个star:grin:)


以上所述就是小编给大家介绍的《TypeScript + Webpack + Koa 搭建 React 服务端渲染》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

游戏编程算法与技巧

游戏编程算法与技巧

【美】Sanjay Madhav / 刘瀚阳 / 电子工业出版社 / 2016-10 / 89

《游戏编程算法与技巧》介绍了大量今天在游戏行业中用到的算法与技术。《游戏编程算法与技巧》是为广大熟悉面向对象编程以及基础数据结构的游戏开发者所设计的。作者采用了一种独立于平台框架的方法来展示开发,包括2D 和3D 图形学、物理、人工智能、摄像机等多个方面的技术。《游戏编程算法与技巧》中内容几乎兼容所有游戏,无论这些游戏采用何种风格、开发语言和框架。 《游戏编程算法与技巧》的每个概念都是用C#......一起来看看 《游戏编程算法与技巧》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

随机密码生成器
随机密码生成器

多种字符组合密码

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具