仓库链接:本篇代码
运行方法参考readme,请使用pnpm,因为本次使用的rspack尚未推出1.0版本,预计后续可能会有很多breakchange,所以lock文件至关重要,而我只上传了pnpm的lock,非常抱歉。
环境:node 20.12.0 pnpm 8.15.5 其余依赖以lock文件为准
上一篇,我们已经完成了服务端的部分。理论上,我们这期只要补完浏览器端水合的功能,就基本完成了。但是困难总比想象的多,而且我最初的设想也太过天真
因为我没有不依赖脚手架,从零搭建项目的经验,一开始想着不打包,直接运行,最多过一下babel转一下嘛,但是后面问题越来越多。。。
我以为要做的 我以为就是在上次的index.js里面,执行一下水合就好了
index.js 1 2 3 4 import App from "./app.jsx" ;import { hydrateRoot } from "react-dom/client" ;hydrateRoot (document .getElementById ("root" ), <App /> );
因为上次html的模板里,最后一个script标签引了这个js文件,虽然我知道不会就这么简单顺利,但是问题还是多的超出了想象。
简单列一下遇到的困难
上一期为了能在node端运行,用了cjs的规范去导入和导出。但是浏览器不支持cjs规范,就很尴尬,cjs是node环境下提供的支持。平时我们的代码能跑,都是因为webpack在对代码进行打包的时候,将代码里的require转为了自己实现的导入导出方法。现在我没用webpack打包就很尴尬,浏览器不认识require,而且这个用babel转了也没用,babel只能把import转为require,但是没有给require提供polyfill,大概是因为这个本来是webpackl的工作吧
而且还有问题就是不打包的话,浏览器端是拿不到react的代码的(不考虑node端的话,或许可以参考vite,对import进行处理,返回node_modules里的react包)。我改用cdn的方式引入react,node端又会报错。然后判断require存在的时候调require,也没有用,查了一下大概是条件判断引入require是没用的。。。
然后还遇见的问题真的是很多很多,再加上我对早些时候的什么requirejs,seajs之类的完全不了解,在我尝试了两个小时未果后,感觉有点搞不定node和浏览器的同构渲染了,所以最后还是决定上打包器
用Rspack 看到了字节Web Infra微信公众号的推文,Rspack在JSNation 2024获得”Breakthrough of the Year”奖项,与之前的Deno、Vite、Svelte、Astro、SolidJS 等杰出的项目共享同一份殊荣,我就突然觉得这个东西未来非常可期,所以打算在这个小demo里面尝试一下。
参考rspack的文档 ,我们其实不需要rsbuid这么重的脚手架,我们只需要Rspack CLI来帮我们打包就可以了。我们使用pnpm add @rspack/core @rspack/cli -D
去安装依赖,然后添加配置文件rspack.client.js和rspack.server.js,主要就是让rspack用builtin:swc-loader去处理一下我们的代码(比如处理jsx,es6等等),然后再打包到一起。模式选择用开发模式打包,这样报错我们更容易看一些。
rspack.client.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 module .exports = { entry : "./index.js" , mode : "development" , output : { filename : "index.bundle.js" , }, module : { rules : [ { test : /\.(jsx|js)$/ , exclude : [/[\\/]node_modules[\\/]/ ], loader : "builtin:swc-loader" , options : { jsc : { parser : { syntax : "typescript" , tsx : true , }, transform : { react : { runtime : "automatic" , development : false , refresh : false , }, }, }, }, }, ], }, };
rspack.server.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 module.exports = { target: "node" , entry: "./server.js" , output: { filename: "server.bundle.js" , }, mode: "development" , module: { rules: [ { test: / \.(js|jsx)$/ , exclude: [/ [\\/ ]node_modules[\\/ ]/ ], loader: "builtin:swc-loader" , options: { jsc: { parser: { syntax: "typescript" , tsx: true , }, transform: { react: { runtime: "automatic" , development: false , refresh: false , }, }, }, }, }, ], }, };
接下来我们在package.json里面,添加打包和启动服务的命令
package.json 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "scripts" : { "server" : "rm -rf dist && pnpm run build && pnpm run build:server && node ./dist/server.bundle.js" , "build" : "rspack build -c rspack.client.js" , "build:server" : "rspack build -c rspack.server.js" } , "dependencies" : { "express" : "^4.19.2" , "react" : "^18.3.1" , "react-dom" : "^18.3.1" } , "devDependencies" : { "@rspack/cli" : "^0.7.3" , "@rspack/core" : "^0.7.3" } }
接下来,还需要把server返回的html模板里的js文件,从原始代码改成打包后的产物
server.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const express = require ("express" );import App from "./app.jsx" const ReactDom = require ("react-dom/server" );const React = require ("react" );const server = express (); server.use (express.static ("." )); server.get ("/" , function (req, res, next ) { const elementString = ReactDom .renderToString (<App /> ); console .log (elementString) const html = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>my react ssr</title> </head> <body> <div id="root">${elementString} </div> <script type="module" src="./dist/index.bundle.js"></script> </body> </html>` ; res.send (html); }); server.listen (8080 );
结语 然后pnpm run server
,本地服务器顺利启动,就大功告成了。
上一次的代码里面,因为只做了服务端渲染,没有水合,所以页面是无法交互的。就比如上次我们的代码,给div添加了点击事件。但是因为上次拿到的只是一个html文件,是没有点击事件一说的,我们可以试一下,点击div,什么都没有发生。
然后在这次成功添加了水合事件之后,我们再点击div,控制台就输出了“111”,说明水合成功了,点击事件添加上了
顺便说下,上次服务端返回的html模板有问题。。。root那个div元素之后不要换行,直接拼renderToString返回的字符串就行,不然服务端返回的html的虚拟dom会多一个内容为换行符的text节点,导致水合失败,降级到纯客户端渲染。。。我被这个东西搞吐了,查了很久为什么水合失败,幸好最后发现了