1. 概述
本文将深入探讨什么是同构应用(Isomorphic Application),并介绍 Java 自带的 JavaScript 引擎 Nashorn。接着,我们会结合 React 这样的前端库,演示如何利用 Nashorn 实现一个完整的同构应用。
简单来说,目标是:用 Java 后端跑 React 前端代码,实现服务端渲染(SSR)。这在已有 Java 技术栈的团队中,是一种低成本接入现代前端生态的“骚操作”。
2. 前端发展简史
回顾一下前端的演变过程:
- 早期 Web 应用以服务端为主,比如 PHP 直接生成 HTML 返回给浏览器。
- 1995 年 Netscape 引入 JavaScript,开始将部分逻辑转移到浏览器端。
- 随着用户对交互体验要求提升,前端框架开始爆发:
- ✅ jQuery:早期的 DOM 操作王者,让 AJAX 变得简单粗暴。
- ✅ AngularJS:Google 推出,引入 MVW 模式。
- ✅ React:Facebook 提出组件化与虚拟 DOM,彻底改变前端开发方式。
- ✅ Vue:渐进式框架,易上手,生态完善。
如今,前端已不再是“切页面”,而是构建复杂的单页应用(SPA)。但 SPA 也有痛点:首屏慢、SEO 不友好。
于是,同构应用应运而生——前后端共享同一套代码,实现服务端渲染 + 客户端激活(Hydration)。
3. 什么是同构应用?
同构应用(也叫通用应用,Universal App)指的是:
同一套前端代码,既能在浏览器运行,也能在服务端运行,并生成相同的 HTML 输出。
✅ 优势:
- 首屏直出 HTML,提升加载速度
- 更利于 SEO
- 前后端逻辑统一,减少重复代码
⚠️ 关键点:服务端必须能执行 JavaScript。最常见的方案是 Node.js。
但如果你的后端是 Java 呢?总不能为了 SSR 把整个后端重构成 Node 吧?
这时候,Nashorn 就派上用场了。
4. Nashorn 是什么?
Nashorn 是 Java 8 内置的 JavaScript 引擎,无需额外依赖,开箱即用。
它的核心能力是:
✅ 将 JavaScript 编译为 JVM 字节码,在 JVM 上直接执行。
相比老一代的 Rhino 引擎,Nashorn 性能更好,兼容性更强。
使用场景:
- 在 Java 项目中嵌入 JS 脚本
- 实现 Java 与 JS 的双向调用
- 服务端渲染前端框架(如 React)
对于已有 Java 技术栈的团队,Nashorn 是一个低成本实现 SSR 的选择。
5. 构建同构应用
我们来实现一个简单的同构应用:斐波那契数列生成器。
功能:
- 页面初始显示
[0,1,1]
- 点击“Next”按钮,通过 REST 接口获取下一个数并追加显示
技术栈:
- ✅ 前端:React.js
- ✅ 后端:Spring Boot + Nashorn
6. 前端实现(React)
6.1 React 组件
var App = React.createClass({displayName: "App",
handleSubmit: function() {
var last = this.state.data[this.state.data.length-1];
var secondLast = this.state.data[this.state.data.length-2];
$.ajax({
url: '/next/'+last+'/'+secondLast,
dataType: 'text',
success: function(msg) {
var series = this.state.data;
series.push(msg);
this.setState({data: series});
}.bind(this),
error: function(xhr, status, err) {
console.error('/next', status, err.toString());
}.bind(this)
});
},
componentDidMount: function() {
this.setState({data: this.props.data});
},
getInitialState: function() {
return {data: []};
},
render: function() {
return (
React.createElement("div", {className: "app"},
React.createElement("h2", null, "Fibonacci Generator"),
React.createElement("h2", null, this.state.data.toString()),
React.createElement("input", {type: "submit", value: "Next", onClick: this.handleSubmit})
)
);
}
});
关键点解析:
getInitialState
:初始化状态为空数组componentDidMount
:组件挂载时,从 props 接收初始数据render
:生成虚拟 DOM,显示数列和按钮handleSubmit
:点击按钮时,调用/next
接口获取下一个数- 使用
React.createElement
而非 JSX(Nashorn 不支持 JSX)
6.2 使用 React 组件
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Hello React</title>
<script type="text/javascript" src="js/react.js"></script>
<script type="text/javascript" src="js/react-dom.js"></script>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript">
ReactDOM.render(
React.createElement(App, {data: [0,1,1]}),
document.getElementById("root")
);
</script>
</body>
</html>
说明:
- 引入
react.js
、react-dom.js
、jquery
- 创建
id="root"
的 div 作为挂载点 - 加载
app.js
(包含 App 组件) - 使用
ReactDOM.render
激活客户端
7. 后端实现(Spring Boot + Nashorn)
7.1 Maven 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>
说明:
spring-boot-starter-web
:Web 核心依赖tomcat-embed-jasper
:支持 JSP 编译(用于服务端渲染输出)
7.2 Web 控制器
@Controller
public class MyWebController {
@RequestMapping("/")
public String index(Map<String, Object> model) throws Exception {
ScriptEngine nashorn = new ScriptEngineManager().getEngineByName("nashorn");
nashorn.eval(new FileReader("static/js/react.js"));
nashorn.eval(new FileReader("static/js/react-dom-server.js"));
nashorn.eval(new FileReader("static/app.js"));
Object html = nashorn.eval(
"ReactDOMServer.renderToString(" +
"React.createElement(App, {data: [0,1,1]})" +
");");
model.put("content", String.valueOf(html));
return "index";
}
}
核心逻辑:
- 获取 Nashorn 引擎实例
- 加载
react.js
、react-dom-server.js
(服务端渲染专用) - 加载前端组件
app.js
- 调用
ReactDOMServer.renderToString()
生成 HTML 字符串 - 将 HTML 注入模型,传递给 JSP 视图
⚠️ 踩坑提醒:必须使用 react-dom-server.js
,否则 renderToString
不存在。
7.3 JSP 视图
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
<title>Hello React!</title>
<script type="text/javascript" src="js/react.js"></script>
<script type="text/javascript" src="js/react-dom.js"></script>
<script type="text/javascript" src="http://code.jquery.com/jquery-1.10.0.min.js"></script>
</head>
<body>
<div id="root">${content}</div>
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript">
ReactDOM.render(
React.createElement(App, {data: [0,1,1]}),
document.getElementById("root")
);
</script>
</body>
</html>
关键变化:
${content}
:插入服务端渲染的 HTML- 客户端 React 会检测到已有内容,进行“激活”而非重新渲染
7.4 REST 接口
@RestController
public class MyRestController {
@RequestMapping("/next/{last}/{secondLast}")
public int index(
@PathVariable("last") int last,
@PathVariable("secondLast") int secondLast) throws Exception {
return last + secondLast;
}
}
一个简单的 REST 接口,计算斐波那契下一项。
8. 运行应用
启动类:
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
}
启动后访问首页:
渲染流程:
- 浏览器请求
/
- Spring Controller 使用 Nashorn 执行 React 代码,生成 HTML
- JSP 将 HTML 插入
#root
- 浏览器收到完整 HTML,立即渲染
- 客户端 JS 下载完成后,React 激活页面,接管交互
✅ 核心机制:React 的 同构激活(Hydration) —— 客户端 React 会复用服务端生成的 DOM,避免重复渲染。
9. 更复杂的场景
本文例子较为简单,实际项目中还需考虑:
- ✅ 组件拆分:多个组件构成层级结构,通过 props 传递数据
- ✅ 模块化:使用
import/export
管理依赖 - ✅ 状态管理:引入 Redux 管理全局状态
- ✅ 副作用处理:使用 redux-thunk 或 Redux-Saga 处理异步
- ✅ JSX 支持:使用 Babel 将 JSX 编译为
React.createElement
⚠️ 限制:Nashorn 仅支持标准 JavaScript,不支持 JSX、ES6+ 新语法。
解决方案:
- 使用 Webpack 或 Rollup 打包前端代码
- 通过 Babel 将 JSX 和 ES6+ 编译为 ES5
- 生成一个兼容 Nashorn 的 JS 文件
最终 Nashorn 执行的是编译后的“纯 JS”,完美兼容。
10. 同构应用的优势
10.1 首屏渲染更快
- ❌ SPA:白屏 → 下载 JS → 执行 → 渲染
- ✅ 同构:直接返回 HTML,内容秒出
对用户体验提升显著,尤其在网络较差的移动端。
10.2 更利于 SEO
- ❌ SPA:搜索引擎爬虫可能无法执行 JS,抓取不到内容
- ✅ 同构:服务端返回完整 HTML,爬虫可直接解析
⚠️ 注意:现代爬虫(如 Googlebot)已支持 JS 渲染,但仍有不确定性。服务端渲染更稳妥。
11. 总结
本文通过一个简单示例,演示了如何使用 Spring Boot + Nashorn + React 实现同构应用。
核心价值:
- ✅ 利用 Java 生态实现服务端渲染
- ✅ 无需引入 Node.js,降低架构复杂度
- ✅ 提升首屏性能与 SEO
虽然 Nashorn 在复杂项目中有局限(如不支持 JSX),但结合 Webpack/Babel 构建流程后,依然能胜任中等复杂度的同构需求。
代码已托管至 GitHub:https://github.com/eugenp/tutorials/tree/master/spring-boot-modules/spring-boot-nashorn