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:

  1. 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

  2. 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 after react-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.

  1. 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.