获取中...

-

Just a minute...

仓库链接:本篇代码

运行方法参考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节点,导致水合失败,降级到纯客户端渲染。。。我被这个东西搞吐了,查了很久为什么水合失败,幸好最后发现了

相关文章
评论
分享
  • 动手实现react ssr

    关于ssr相关的介绍,可以移步之前的一篇博文,本文不再赘述 日常工作中,我们可能都接触过ssr,不过我们可能通常是借助于框架(例如Next.js)的支持来完成ssr。今天我想尝试,摆脱高度封装的前端框架,只依赖react相关的库,来实...

    动手实现react ssr
  • 从invalid hook call报错说到npm install相关

    遇到了invalid hook call报错上周,业务下游的同事反馈了一个生产问题,说我们传的值有问题,领导让我帮忙排查一下。我最近一直没做那个项目相关的需求,打开那个项目切到release分支,拉代码,npm i然后启动,结果就白屏...

    从invalid hook call报错说到npm install相关
  • React中副作用的执行时机

    为什么一定要useEffect?之前看到过,说不要把副作用直接写在函数式组件的函数体内,需要用useEffect把副作用包裹起来。我当时的理解就是,函数组件重新渲染的话,整个函数都会被执行一遍。如果我就想让一个副作用,每次组件渲染都重...

    React中副作用的执行时机
  • 聊聊react与函数式编程

    react不是魔法 今天我想来聊一聊函数式编程,和它在react中的体现。 何为函数式编程我们可以把函数式编程理解成一种范式,一个规范。纯函数是函数式编程的关键概念,函数式编程希望我们尽可能多地使用纯函数。正是依赖于纯函数的特点,...

    聊聊react与函数式编程
  • react的一些坑

    记录一些刚写react踩过的坑吧,想到多少就写多少 从一个工作中的需求开始有个需求是,点击按钮出来一个弹窗。本来弹窗有个关闭按钮,但是UED在视觉稿上面加了行字,“点击关闭按钮或弹窗外的其他地方,关闭弹窗”。我想了想,合理,避免有...

    react的一些坑
  • 使用react的一些心得与感想

    刚忙完公司的新人训练营,9个人的团队2周完成一个小项目。我负责用remax开发一个微信小程序。remax就是蚂蚁的一个开源框架,让开发者能用react的语法去开发微信小程序。开发体验整体来讲非常不错,感觉也加深了我对react的理解...

    使用react的一些心得与感想
  • 基于tapable来模拟webpack插件机制

    前两天看到ByteFE公众号发了篇文章,从源码去讲webpack。我之前也从很多方面去学习webpack,只是源码一直没太看得进去,这一次我想仔细地读一读。 简要介绍tapable说实话,之前看webpack源码一直没看的太懂,...

    基于tapable来模拟webpack插件机制
  • 深入js——作用域,作用域链,执行栈(一)

    最近在补js基础。当初自学js的时候,总是感觉学的不扎实。只能说不去上手,再怎么死读书,也理解不了。积累了经验,然后自己去思考,这个时候再去看理论,就容易理解了。 感觉工作一年以来,确实学到了很多,但是过于专注于应用方面的知识,反倒...

    深入js——作用域,作用域链,执行栈(一)
  • webpack运行时代码简要分析

    开个坑,主要也是怕自己后面忘了 简单写写,有空了补上 __webpack_modules是一个数组,每个元素都是一个函数,函数接受三个参数:第一个参数module好像没用到,第二个是一个对象(会把本模块要导出的属性,赋值给这个对象),...

    webpack运行时代码简要分析
  • webpack动态加载原理简述

    为什么需要动态加载上一篇讲我写webpack针对markdown的loader的时候,提到了希望能动态加载以优化性能。篇幅原因没有展开说,打算单独写一篇文章讲一下。 当我们访问网站的时候,浏览器会向服务器请求页面资源。首先请求的当然是...

    webpack动态加载原理简述
Please check the parameter of comment in config.yml of hexo-theme-Annie!