Proposal: Server to client data sharing for Script Modules

Abstract

Script Modules were introduced in WordPress 6.5. wp_add_inline_script is often used to initialize or make data available to Scripts. Feedback on Script Modules and explorations suggest this would be a useful feature for Script Modules, but nothing exists at this time. This post will describe the problem in detail and propose a solution. The proposed solution consists of three main points:

  • A new filterFilter Filters are one of the two types of Hooks https://codex.wordpress.org/Plugin_API/Hooks. They provide a way for functions to modify data of other functions. They are the counterpart to Actions. Unlike Actions, filters are meant to work in an isolated manner, and should never have side effects such as affecting global variables and output. runs for each Script Module that is enqueued or in the dependency graph. This filter allows arbitrary data to be associated with a given Script Module and added to the rendered page.
  • Script Module data is embedded in the page as JSONJSON JSON, or JavaScript Object Notation, is a minimal, readable format for structuring data. It is used primarily to transmit data between a server and web application, as an alternative to XML. in a <script type="application/json"> tagtag A directory in Subversion. WordPress uses tags to store a single snapshot of a version (3.6, 3.6.1, etc.), the common convention of tags in version control systems. (Not to be confused with post tags.).
  • Script Modules are responsible for reading the data and performing their own initialization.
Read more: Proposal: Server to client data sharing for Script Modules

The feedback period will end 2024-05-24 (Friday, May 24). Please provide any feedback before then.

This post will use “scripts” to refer to WP Scripts and “modules” to refer to WP Script Modules.

Scripts or modules?

Most JavaScriptJavaScript JavaScript or JS is an object-oriented computer programming language commonly used to create interactive effects within web browsers. WordPress makes extensive use of JS for a better user experience. While PHP is executed on the server, JS executes within a user’s browser. https://www.javascript.com/. for WordPress is probably using scripts unless it was specifically compiled as a module. Modern JavaScript is often authored using import apiFetch from '@wordpress/api-fetch', which is module syntax. Until very recently, WordPress build tooling has always removed the module syntax and compiled a script.

Only the most recent WordPress version 6.5 introduced support for modules. WordPress CoreCore Core is the set of software required to run WordPress. The Core Development Team builds WordPress. includes exactly two modules to expose functionality at this time: @wordpress/interactivity and @wordpress/interactivity-router. JavaScript that uses the Interactivity APIAPI An API or Application Programming Interface is a software intermediary that allows programs to interact with each other and share data in limited, clearly defined ways. is probably a module.

This post will talk about a common script dependency, wp-api-fetch. The @wordpress/api-fetch module mentioned in this post is hypothetical; it does not exist at this time.

A note about modules

It’s important to know a few things about scripts versus modules in the context of this post. A few key differences:

  • Scripts and their dependencies will be executed as the page is parsed even if a dependency is never directly used.
  • Script “exports” are attached to the window object, e.g. wp-api-fetch is available as window.wp.apiFetch.
  • Modules will execute after the page has finished parsing.
  • Module dependencies can be loaded on demand.
  • Modules have true encapsulation, using import and export to share values.
More about scripts and modules…

Scripts that are either enqueued or are dependencies of enqueued scripts will be added to the page as script tags, like <script src="path/to/script.js">. The browser will fetch and execute these scripts before it continues to parse the page. There are attributes like async and defer that can change how the browser executes scripts.

Modules that are enqueued will appear on the page as script tags of type module, like <script src="path/to/module.js" type="module">. Processing of modules is deferred, they will be executed after the whole document has been parsed. The async attribute will cause a module to execute in parallel as the document is parsed. WordPress Script Modules do not use async at this time.

Modules that are in the dependency graph of enqueued modules will appear in an importmap. This is a mapping of names to URLs so that a browser knows what module to fetch when it sees an import. It’s what associates the module used in a statement like import "a-module"; with a URLURL A specific web address of a website or web page on the Internet, such as a website’s URL www.wordpress.org { "imports": { "a-module": "path/to/a-module.js" } }.

The problem

Scripts often require some initialization or other data to work correctly in WordPress. The wp-api-fetch script is a good example, it requires a significant amount of configuration. For example, Core uses the following inline script to initialize wp-api-fetch so it can send requests to the appropriate place:

$scripts->add_inline_script(
	'wp-api-fetch',
	sprintf(
		'wp.apiFetch.use( wp.apiFetch.createRootURLMiddleware( "%s" ) );',
		sanitize_url( get_rest_url() )
	),
	'after'
);

This snippet uses PHPPHP The web scripting language in which WordPress is primarily architected. WordPress requires PHP 5.6.20 or higher to create a string of JavaScript code with some data from PHP embedded. This will be included in a script tag that appears immediately after the script tag for wp-api-fetch, something like this:

<script src="path/to/wp-api-fetch.js"></script>
<script>
// The JavaScript code is printed here:
wp.apiFetch.use( wp.apiFetch.createRootURLMiddleware( "…/index.php?rest_route=/" ) );
// More code may follow from other calls to `wp_add_inline_script`…
</script>

Notice that the JavaScript depends on wp-api-fetch, it imperatively calls wp.apiFetch.use(…). That same approach with a hypothetical @wordpress/api-fetch module would look something like this:

<script type="importmap">
{ "imports": { "@wordpress/api-fetch": "…url/to/api-fetch-module.js" } }
</script>
<script type="module">
import apiFetch from '@wordpress/api-fetch';
wp.apiFetch.use( wp.apiFetch.createRootURLMiddleware( "…/index.php?rest_route=/" ) );
</script>

In this approach, the initialization module would fetch and execute the @wordpress/api-fetch module just to initialize it! That eliminates one of the important advantages of modules, their ability to load on-demand. 🤔 It looks like this imperative initialization isn’t a good fit for modules. Let’s see if there’s a better solution…

The proposal

Here are some requirements that arise from the naive implementation:

  • Modules should be fetched and initialized on demand.
  • Modules should be responsible for their own initialization when they’re executed.
  • Variables should be scoped to modules and not pollute the global namespace.
  • The data should introduce minimal overhead.

Add server data via filters

Filters provide a nice method to collect the data needed by modules. The WP_Script_Modules class introduces a new filter that runs for each module that is enqueued or present in the dependency graph. Adding or modifying data for a module looks like this:

add_filter(
	'scriptmoduledata_@wordpress/api-fetch',
	function ( $data ) {
		$data['rootURL'] =  sanitize_url( get_rest_url() )
		return $data;
	}
);

Multiple filters can be added to add or modify the data exposed to the script, and if no data is added, nothing will be serialized on the page for the client. It’s also worth mentioning that no JavaScript code is written in PHP, a nice improvement over wp_add_inline_script which requires valid JavaScript to be added.

A drawback to this approach is that all the data passed must pass through JSON. Data without a valid JSON representation is not supported by default.

Use an inert script tag to expose data on the client

The data is embedded it in the HTMLHTML HyperText Markup Language. The semantic scripting language primarily used for outputting content in web browsers. in a script tag:

<script id="scriptmoduledata_@wordpress/api-fetch" type="application/json">
{ "theData": "JSON Serializable data can be shared" }
</script>

This script tag is effectively ignored by the browser because of its type attribute. This approach is an example on MDN of how to embed data in HTML and it’s already used in WordPress to pass Interactivity API data to the client.

Because modules are always deferred, it should be safe to print these script tags at the bottom of the page. They should have limited impact on page load because the browser will not parse or execute the contents.

Modules read data from the script tag

This script tag doesn’t do anything on its own. The module is responsible for getting the data and performing its initialization when it executes:

if ( typeof document !== 'undefined' ) {
	const serializedData = document.getElementById(
		'scriptmoduledata_@wordpress/api-fetch'
	)?.textContent;
	if ( serializedData ) {
		let config = null;
		try {
			config = JSON.parse( serializedData );
		} catch {
			// there was a problem parsing the serialized data
		}
		performInitialization( config );
	}
}
function performInitialization( config ) {
	if ( config?.rootURL ) {
		registerMiddleware( createRootURLMiddleware( config.rootURL ) );
	}
	// etc.
}

This approach is used by @wordpress/interactivity to retrieve store data on the client.

Try it!

I’ve prototyped this proposal in the following PRs:

  • WordPress-develop PR 6433 applies the filters and adds the necessary filters to expose data to @wordpress/api-fetch. The proposal in this post is contained in this PR.
  • Gutenberg PR 60952 builds and registers the @wordpress/api-fetch module. This PR is helpful for testing, but is beyond the scope of this post.

Try it in the WordPress Playground here. If you run the following JavaScript in the inspector console —make sure you pick the JavaScript context, something like wp (scope:abc123)— you’ll see the @wordpress/api-fetch module log some initialization when it’s imported and then work as expected:

const { apiFetch } = await import( '@wordpress/api-fetch' );
await apiFetch( { path: '/wp/v2/block-types' } )
Screenshot of browser console showing the `@wordpress/api-fetch` module initializing on demand and being used to make a REST request.

Relevant links

Props @youknowriad, @cbravobernal, @bph, and @andronocean for review.

#javascript, #script-loader