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

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

内容简介:也可以使用基本项目目录安装一些依赖,想尝试 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 服务端渲染》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

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

Data Structures and Algorithm Analysis in Java

Data Structures and Algorithm Analysis in Java

Mark A. Weiss / Pearson / 2011-11-18 / GBP 129.99

Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course wa......一起来看看 《Data Structures and Algorithm Analysis in Java》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具