The principle is bundleless + React-refresh + Websocket communication to achieve hot update
preface
Bundleless + react-refresh hot update sequence diagram: Bundleless + react-refresh hot update
A few points to note in the sequence diagram above:
-
It is necessary to inject some Websocket client code of client.js into the client HTML to respond to the hot update of the file system, similar to webpack in the configuration entry entry to inject the required hot update code
-
In the React hot update provider react-refresh, you need to inject the registry code before the entire component tree and register the component tree.
Then you need to create a new JS entry point which must run before any code in your app, including
react-dom
(!) This is important; if it runs afterreact-dom
, nothing will work. That entry point should do something like this:
An introduction to and detailed usage of react-refresh can be found at github.com/facebook/re… A detailed description is available for reference in this article.
-
In addition to accepting changes to the component tree, you need to actively accept the changed message and pass it through the Webpack
module.hot.accept(); Copy the code
In Bundleless, it is similar to loading the module dynamically by **import and executing the accept method (present in import.meta metadata) ** to receive the new module.
Once you hook this up, you have one last problem. Your bundler doesn’t know that you’re handling the updates, so it probably reloads the page anyway. You need to tell it not to. This is again bundler-specific, but the approach I suggest is to check whether all of the exports are React components, and in that case, “accept” the update. In webpack it could look like something:
/ /... ALL MODULE CODE...
const myExports = module.exports;
// Note: I think with ES6 exports you might also have to look at .__proto__, at least in webpack
if (isReactRefreshBoundary(myExports)) {
module.hot.accept(); // Depends on your bundler
enqueueUpdate();
}
Copy the code
At the above github.com/facebook/re… Detailed introduction.
The following details some of the bugs and issues I encountered in applying Bundless + React-Refresh.
The react-Refresh registered component must come first
There are several reasons why I didn’t register before
It’s not in the front
This code:
import RefreshRuntime from "/@react-refresh" / / important
RefreshRuntime.injectIntoGlobalHook(window)
window.$RefreshReg$ = () = > {}
window.$RefreshSig$ = () = > (type) = > type
Copy the code
Global runtime dependency injection (DI) to ensure that react-refresh provides the ability to update registered components effectively. This must be implemented in the header, preferably in the head tag, before the entire App is loaded.
The most important of these is RefreshRuntime, which is the actual path that this variable takes in
react-refresh/cjs/react-refresh-runtime.development.js
Copy the code
Be sure to import the correct file path, errors will cause it to fail to run.
If production environment variables are not specified globally
window.process = { env: { NODE_ENV: "development" }}
Copy the code
It will also lead to failure to register. I just introduced an error at that time, and it was always in the PROD environment, so it was invalid.
Every react file is overwritten by react-refresh/ Babel
Since each component is registered with react-refresh for subsequent hot updates, each file needs to be translated via Babel’s plugin: react-refresh/ Babel:
const result = require('@babel/core').transformSync(code, {
plugins: [require('react-refresh/babel')].ast: false.sourceMaps: true.sourceFileName: path
});
Copy the code
The translated code will have logic like this:
// ...
function Test() {
return /* @__PURE__ */React.createElement("div".null."test ff33 ajd19s3479");
}
_c = Test;
var _c;
$RefreshReg$(_c, "Test");
// ...
Copy the code
This is equivalent to registering the component and setting it up for future update dependencies.
In addition, the file also needs to inject the following code:
const header = '// This is the logic to register the current react file with react-refresh. Import RefreshRuntime from ".. /node_modules/react-refresh/cjs/react-refresh-runtime.development.js"; import { createContext } from '/public/HotModuleReplacementClient.js'; import.meta.hot = createContext('${path}');
let prevRefreshReg;
let prevRefreshSig;
if (import.meta.hot) {
prevRefreshReg = window.$RefreshReg$;
prevRefreshSig = window.$RefreshSig$;
window.$RefreshReg$ = (type, id) => {
RefreshRuntime.register(type, The ${JSON.stringify(path)}+ " " + id) }; window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform; } `.replace(/[\n]+/gm.' ');
const footer = ` if (import.meta.hot) { window.$RefreshReg$ = prevRefreshReg; window.$RefreshSig$ = prevRefreshSig; import.meta.hot.accept(); / / this is module after receiving the accept RefreshRuntime. PerformReactRefresh (); // This is the logic that performs updates to the component tree} '
Copy the code
At this point, react-Refresh can be synergistic
What does Accept do (vite only)?
You’ll often see articles saying that hot updates like Snowpack and Vite are implemented through import.meta.hot, which is a bit of a muddle that misses the point.
In fact, accept only serves as a bridge to connect the module to receive. After accept, it only stores a copy of the module data and is responsible for the update of the module after the next introduction. Vite explains github.com/vitejs/vite:
Note that Vite’s HMR does not actually swap the originally imported module: if an accepting module re-exports imports from a dep, then it is responsible for updating those re-exports (and these exports must be using
let
). In addition, importers up the chain from the accepting module will not be notified of the change.This simplified HMR implementation is sufficient for most dev use cases, while allowing us to skip the expensive work of generating proxy modules.
Websocket and react-refresh are the only hot updates that can be easily implemented. React-refresh registers components, ws notifies the front end of active import, and dynamically loads JS execution. React-refresh is responsible for notifying the component tree for component rendering, as shown below:
const client = new WebSocket('ws://localhost:8000');
const moduleMap = new Map(a); client.addEventListener('open'.function (event) {
client.send('start');
});
client.addEventListener('message'.function (payload) {
console.log('received', payload);
updateModule(payload.data)
})
async function updateModule (id) {
// Dynamic import is introduced
await import(`${id}? r=The ${new Date().getTime()}`);
}
export const createContext = function (id) {
return {
// An external call to accept triggers
accept() {
// todo
// moduleMap stores copies}}}Copy the code
Here’s an important BUG to note if your code has logic like this:
document.appendChild(node)
Copy the code
This leads to a bizarre BUG: document.appendChild(node) is executed again because js is introduced dynamically by import, which causes the node to be inserted twice.
CSS hot Update?
There is also a point to focus on, how to achieve the CSS hot update, in fact, are modular, CSS should also be closer to modular.
Bundleless’s solution is to treat CSS as a normal file, read the content, execute it by converting it into JS code, and insert it into CSS stylesheet:
const fs = require('fs');
module.exports = function (url, context) {
const cssContent = fs.readFileSync(`${context}${url}`).toString();
const testStyle = ` var addSheets = function (content) { var style = new CSSStyleSheet(); style.replaceSync(content); document.adoptedStyleSheets = [...document.adoptedStyleSheets, style]; } addSheets(cssCode); `
const content = `var cssCode = \`${cssContent.toString()}\ `;${testStyle}`;
return content;
}
Copy the code
CSSStyleSheet is a special entity. In Vite, it stores CSS content in the CSS table of CSSStyleSheet. It uses a Map file and hash values to Map the relationship between each module and the CSS file. Feedback to component modules by updating CSS content in Map data. Refer to MDN for more details.
subsequent
This is the summary of the first version of Bundleless hot update. There are some flaws. I hope that the details and flaws of the full hot update can be added together in the future.