Java同构渲染,从零开始构建react应用(五)同构之服务端渲染

前言

上文讲到使用react进行客户端渲染页面,这次讲解在服务端利用前端react的代码来渲染页面并输出到客户端,即构建同构应用。

PS:同构,我是这样理解的,同一份代码可以同时运行在客户端和服务端。

利用ts实现纯脚本组件的同构

当我们的组件不包含样式,图片等服务端无法直接解析处理的时候,我们可以直接利用ts的tsc命令将组件编译成相应的js,服务端则可以直接运行该js得到渲染的结果,当然这种情况实际并不存在,这里只是作为例子来讲解。

服务端bundle.tsx

我们在server目录下新建bundle.tsx将其作为前端react组件的一个打包入口文件。我们通过将它打包,并在服务端执行得到我们需要的渲染结果。

// ./src/server/bundle.tsx

import * as React from 'react';

/* tslint:disable-next-line no-submodule-imports */

import { renderToString } from 'react-dom/server';

import App from '../client/component/app';

export default {

render() {

return renderToString();

},

};

可以看到,我们直接将客户端的App组件引入,并输出一个拥有render方法的对象,在服务端入口文件中我们只需要引入该bundle对象,并调用其render方法就可以得到渲染出的html字符串了。

PS:tslint:disable类似于eslint的对应语法,用来使得相应的规则不生效

// ./src/server/index.tsx

...

router.get('/*', (ctx: Koa.Context, next) => { // 配置一个简单的get通配路由

const html = bundle.render(); // 获得渲染出的html字符串

ctx.type = 'html';

ctx.body = `

...

${html}

...

`;

next();

});

...

PS:...代表代码省略

客户端/服务端渲染对比

在chrome中打开localhost:3344后可以看的页面上的hello world,我们右键页面选择View Page Source,可以看到两种方法渲染的不同:

客户端渲染:

bVWzZo?w=836&h=382

服务端渲染:

bVWzZJ?w=1154&h=396

显而易见,服务端渲染会直接输出组件渲染的内容,浏览器在接收到这些内容后就会直接绘制呈现给我们,而客户端渲染会在react框架初始化完毕之后再进行,所以对比两种情况,客户端渲染时白屏时间会更长一些,且刷新页面时会有闪烁的感觉。

利用webpack实现非纯脚本组件的同构

在我们实际开发环境中,必然存在组件里引用样式文件,引用图片的情况,这种情况下ts并不具备webpack相应的将这些资源转换为js可处理的功能,所以我们需要使用webpack来处理服务端的bundle.tsx文件,使得服务端可以运行打包后的js文件。

服务端bundle.tsx的webpack配置文件

在客户端,像react这样的库,webpack会把它打包到输出的js文件里,而在服务端我们并不需要这么做,所以配置文件和客户端有很大不同。

// ./src/webpack/server.ts

import * as path from 'path';

import * as webpack from 'webpack';

import * as nodeExternals from 'webpack-node-externals';

import { cloneDeep } from 'lodash'; // lodash提供的深度复制方法cloneDeep

// 客户端+服务端全环境公共配置baseConfig,项目根目录路径baseDir,获取tsRule的方法getTsRule

import baseConfig, { baseDir, getTsRule } from './base';

const serverBaseConfig: webpack.Configuration = cloneDeep(baseConfig); // 服务端全环境公共配置

serverBaseConfig.entry = { // 入口属性配置

'server-bundle': [

'./src/server/bundle.tsx',

],

};

serverBaseConfig.externals = [nodeExternals()],

serverBaseConfig.node = {

__dirname: true,

__filename: true,

};

serverBaseConfig.target = 'node';

serverBaseConfig.output.libraryTarget = 'commonjs2';

const serverDevConfig: webpack.Configuration = cloneDeep(serverBaseConfig); // 服务端开发环境配置

serverDevConfig.cache = false; // 禁用缓存

serverDevConfig.output.filename = '[name].js'; // 使用源文件名作为打包后文件名

(serverDevConfig.module as webpack.NewModule).rules.push(

getTsRule('./src/webpack/tsconfig.server.json'),

);

serverDevConfig.plugins.push(

new webpack.NoEmitOnErrorsPlugin(), // 编译出错时跳过输出阶段,以保证输出的资源不包含错误。

);

const serverProdConfig: webpack.Configuration = cloneDeep(serverBaseConfig); // 服务端生产环境配置

// TODO 服务端生产环境配置暂不处理和使用

export default {

development: serverDevConfig,

production: serverProdConfig,

};

疑问一:webpack-node-externals是干啥用的?

答:该库的作用是让webpack忽略node_modules里的库,避免将他们打包到输出文件中去。

疑问二:target为何要设置为node?

答:这是为了让webpack打包时忽略node内建的库,比如fs。

疑问三:配置的node属性设置__dirname和__filename为true是什么意思?

答:这是为了让webpack使用真实的相对当前上下文的路径,可以避免打包出的文件里路径错误。简单点说就是在源文件里使用__dirname,在打包后这个__dirname会被替换为源文件的相对路径值,而不是打包输出的文件的相对路径值。

疑问四:libraryTarget设置为commonjs2是什么意思?

答:将入口起点的返回值将分配给 module.exports 对象,参见官方文档详解:output-librarytarget

服务端TypeScript配置文件

相较客户端配置,服务端需要多include一个入口文件即bundle.tsx

// ./src/webpack/tsconfig.server.json

{

"compilerOptions": {

"target": "es5",

"jsx": "react"

},

"include": [

"../../src/client/**/*",

"../../src/server/bundle.tsx"

]

}

服务端webpack执行时机

目前我们准备好了服务端的webpack配置文件,现在要选择一个时机将其执行,那就在客户端webpack打包完毕之后吧,这样在一起有序的执行也好管理哈。

// ./src/webpack/webpack-dev-server.ts

...

import webpackServerConfig from './server';

export default (app: Koa, serverCompilerDone) => {

const clientDevConfig = webpackClientConfig.development;

const serverDevConfig = webpackServerConfig.development;

const clientCompiler = webpack(clientDevConfig);

clientCompiler.plugin('done', () => {

const serverCompiler = webpack(serverDevConfig);

serverCompiler.plugin('done', serverCompilerDone);

serverCompiler.run((err, stats) => {

if (err) {

console.error(stats);

}

});

});

...

};

我们通过complier.plugin方法,来实现打包完成后的回调操作,我们改造了webpack-dev-server.ts输出的函数,接收第二个参数作为服务端webpack打包完成后的回调函数。

引用服务端打包输出的bundle文件

// ./src/server/index.ts

...

let bundle;

const bundleFile = path.join(__dirname, '../../bundle/server-bundle.js');

...

if (isDev) {

webpackDevServer(app, () => {

delete require.cache[require.resolve(bundleFile)];

bundle = require(bundleFile).default;

}); // 仅在开发环境使用

}

...

我们定义bundle变量用于接收server-bundle.js的输出结果,也就是我们上面提到的拥有一个render方法的对象。由于node的require缓存机制,所以我们每次打包完server-bundle.js后都需要先删除缓存,再给bundle赋值。

疑问五:require.cache的键值为何要使用require.resolve包裹文件名?

答:require源码在进行缓存时以绝对路径(使用其内部resolve方法获得)为key,所以这里需要包裹一下以获得真实的key。

疑问六:bundle为何是require(bundleFile)的default值?

答:因为bundle.tsx输出的就是default,export default xxx相当于exports.default = xxx。

小结

虽然在上述第二种方法里,我们没有实际引入样式、图片等文件,但是这个操作我想应该不难,加一个对应的loader(file-loader, css-loader等)即可实现。在写这篇文章之前,上述第二种方法里关于bundle的动态更新方法我一直是参考使用vue里的create-bundle-runner(利用vm实现自己的require),写文章的时候发现其实我目前的应用场景并没有那么复杂,效率性能也没有那么高要求,所以就使用了原生的require方法来实现。

参见:vuejs:create-bundle-runner

Thanks

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值