GitHub PaperStrike/Pjax , refactored from MoOx/pjax .This article introduces three parts: sequential execution of scripts, termination of Promises, and Babel Polyfills. The reading time is about 30 minutes. First published at https://sliphua.work/pjax-in-2021/ .
Pjax is not used for front-end development using modern frameworks such as React and Vue. However, Pjax is still alive and well in many static blogs generated by tools such as Hexo and Hugo, which can provide a smoother and smoother user experience.
Pjax formerly known pushState
+ Ajax, the former refers to the use of browser History API update history, which stands for Asynchronous JavaScript and XML, covering series for sending a HTTP request in JS. The MDN document has another pure-Ajax concept, and the technology and goals involved are almost the same. It is the original intention of Pjax authors and website developers to dynamically obtain and update pages through JS, and provide a smooth and fast switching process.
But in actual implementation, the core of is more than 160b3774cb5eab History API and Ajax. In addition to displaying content, when and how the browser switches pages can not be completely simulated pushState
Execute scripts sequentially
carried out
This painful section innerHTML
not executing internal scripts. There are two sources of script elements, HTML parser parsing and JS generation; there are two major stages, preparation stage and execution stage. The execution phase can only be triggered by the preparation phase or the parser, and the preparation phase will and will only be triggered at the following three moments.
- The HTML parser parses and generates the script element.
- Generated by JS and injected into the document.
- The document is generated by JS and has been injected, inserted into the child node or added
src
attribute.
When using innerHTML
, an HTML parser will be used internally to parse the string in an independent document environment where scripts are disabled. In this independent document environment, script elements go through the preparation phase but will not be executed, and the string parsing is complete After that, the generated node is transferred to the assigned element. Since the internal script elements are not generated by JS, transferring to the current document will not trigger the preparation phase, and will not be further executed.
Therefore, after using innerHTML
, outerHTML
, or DOMParser
+ replaceWith
and other methods to update the page part, special processing script elements are required to trigger the preparation phase again.
It is easy to think of using cloneNode
in JS, and there is another pit in this way. In the script preparation phase, after confirming that the HTML attributes are in compliance, the script will be marked "already started". The first step of the preparation phase is to exit when there is this mark, and the copied script element will keep this mark.
A
script
element has a flag indicating whether or not it has been "already started". Initially,script
elements must have this flag unset (script blocks, when created, are not "already started"). The cloning steps forscript
elements must set the "already started" flag on the copy if it is set on the element being cloned.To prepare a script, the user agent must act as follows:
- If the
script
element is marked as having "already started", then return. The script is not executed.... (check and determine the script's type.)
- Set the element's "already started" flag.
...
Therefore, to execute the script insertion elements, only with the current document createElement
such methods construct a new script elements , by-property replication. Construct a evalScript
function as an example:
const evalScript = (oldScript) => {
const newScript = document.createElement('script');
// Clone attributes and inner text.
oldScript.getAttributeNames().forEach((name) => {
newScript.setAttribute(name, oldScript.getAttribute(name));
});
newScript.text = oldScript.text;
oldScript.replaceWith(newScript);
};
order
The execution problem of partial update script elements has been solved in the early Pjax. The above is more to introduce basic concepts to the order problem in this section. How to make partial page refreshes execution order of the new script in line with page script execution order contained early specification , is the focus of discussion.
When JS dynamically inserts multiple executable <script>
elements continuously, the execution order will often not match the execution order when the page is first loaded.
document.body.innerHTML = `
<script>console.log(1);</script>
<script src="https://example/log/2"></script>
<script>console.log(3);</script>
<script src="https://example/log/4"></script>
<script>console.log(5);</script>
`;
// Logs 1 3 5 2 4
// or 1 3 5 4 2
[...document.body.children].forEach(evalScript);
So consult the script execution specification. According to the specification, the executable <script>
elements with compliant attribute values are divided into two types: module script elements and classic script elements according to type
attribute module
For the script generated by JS, there is a "non-blocking" tag, which is removed async
Further, in the script preparation stage, the execution timing is determined in five categories:
- Containing
defer
property, freeasync
properties, and loaded by the HTML parser classic script element; freeasync
properties, and loaded by the HTML script parser module elements:Add to this a queue, HTML parser After parsing the document, sequentially in no other scripts run implementation of the queue script.
- Containing
src
property, freedefer
norasync
properties, and parses the HTML script loaded classic elements:no other scripts running, suspends the the HTML parser 160b3774cb638b before the execution is complete.
- Containing
src
property, freedefer
norasync
properties, and generated by JS, not "non-blocking" classic elements of the script tag; containingasync
properties, and there is no "non-blocking" modular script element tagged:Add into such a queue that sequentially in no other scripts run execution.
- Contains
src
attributes, classic script elements other than the above cases; module script elements other than the above cases:In no other scripts run execution.
- Classic script elements without
src
Execute immediately, during which time the execution of any other scripts will be suspended.
By default, the scripts dynamically generated and injected by JS belong to the latter two situations, which are quite different from the first three situations that are executed in an orderly manner when the page is first loaded.
Note that you can operate the async
IDL attribute to remove the "non-blocking" mark and turn it into the third type of order. Add in evalScript
// Reset async of external scripts to force synchronous loading.
// Needed since it defaults to true on dynamically injected scripts.
if (!newScript.hasAttribute('async')) newScript.async = false;
Since inline scripts can only belong to the fifth situation, they will be executed immediately, and only the trigger timing of the script preparation phase can be adjusted. onload
event of the external script is triggered after its execution, the document can be injected after the event of the previous third type script is triggered.
- ... (execute)
- If scriptElement is from an external file, then fire an event named
load
at scriptElement.
Considering error handling, the error
event of load
event of the previous third script, that is, before the execution. Therefore, the fifth type script needs to ensure that all the previous third type scripts are executed after the end of the execution. injection. evalScript
to Promise form, the injection sequence of script elements can be easily combined with the reduce
method of the array:
// Package to promise
const evalScript = (oldScript) => new Promise((resolve) => {
const newScript = document.createElement('script');
newScript.onerror = resolve;
// ... Original
if (newScript.hasAttribute('src')) {
newScript.onload = resolve;
} else {
resolve();
}
});
/**
* Evaluate external scripts first
* to help browsers fetch them in parallel.
* Each inline script will be evaluated as soon as
* all its previous scripts are executed.
*/
const executeScripts = (iterable) => (
[...iterable].reduce((promise, script) => {
if (script.hasAttribute('src')) {
return Promise.all([promise, evalScript(script)]);
}
return promise.then(() => evalScript(script));
}, Promise.resolve())
);
executeScripts(document.body.children);
So far, the issue of the execution order of dynamically inserted JS script elements has been solved.
Abort Promise
When sending Pjax requests, using Fetch instead of XMLHttpRequest is the general trend, and there is not much content to write. AbortController and AbortSignal used to abort the fetch request are not listed as attributes of the fetch instance in a form similar to XMLHttpRequest, but are listed separately as a new API, which enhances the scalability. The purpose of its design is to become a universal interface for aborting Promise objects.
For example, in an event listener, you can also use the signal
parameter to remove the listener when the corresponding signal is terminated.
const controller = new AbortController();
const { signal } = controller;
document.body.addEventListener('click', () => {
fetch('https://example', {
signal,
}).then(onSuccess);
}, { signal });
// Remove the listener, too.
controller.abort();
To implement a custom API based on Promise that can be aborted, the specification requires developers to design abort logic in conjunction with AbortSignal
, and at least be able to:
- An accepted parameter passes an instance of AbortSignal
signal
- Use DOMException
AbortError
to express errors about the abort. - The above error is thrown immediately when the transmitted signal has been aborted.
- Listen for the abort event of the passed signal, and throw the above error immediately when it is aborted.
A simple abortable function that meets the requirements of the specification:
const somethingAbortable = ({ signal }) => {
if (signal.aborted) {
// Don't throw directly. Keep it chainable.
return Promise.reject(new DOMException('Aborted', 'AbortError'));
}
return new Promise((resolve, reject) => {
signal.addEventListener('abort', () => {
reject(new DOMException('Aborted', 'AbortError'));
});
});
}
Because the return value is always a promise, it can also be combined with the async function value of the returned Promise. Use the Promise race
static method to reject immediately when the abort event occurs, wrapping the above sequence execution function:
const executeScripts = async (iterable, { signal }) => {
if (signal.aborted) {
// Async func treats throw as reject.
throw new DOMException('Aborted', 'AbortError');
}
// Abort as soon as possible.
return Promise.race([
// promise generated by the original reduce.
originalGeneratedPromise,
new Promise((resolve, reject) => {
signal.addEventListener('abort', () => {
reject(new DOMException('Aborted', 'AbortError'));
});
}),
]);
};
However, the above function is only in compliance with the specification, and cannot directly achieve the effect of suspending the function and suspending subsequent script execution. This is mainly caused by two reasons:
- At present, to interrupt the operation of a function, it can only be done by calling
return
orthrow
internally. Promise is no exception. Simply resolve or reject in the executor does not affect the operation of subsequent parts. - The preparation phase of a script element cannot be aborted. Even if it is an external script element, after the preparation phase is triggered, it is removed before the HTTP request generated by it is completed, the HTTP request will not be interrupted, and the browser will still load it. The file tries to parse and execute.
The second point is a special case of the script execution function here. The first point is to maintain the flexibility of Promise, allowing developers to customize the abort behavior. But here we don't need special suspension behavior, just judge the suspension status of signal evalScript
For example, evalScript
in the executeScripts
function so that it can directly access the signal:
const executeScripts = async (iterable, { signal }) => {
// ... some other code.
const evalScript = (script) => {
if (signal.aborted) return;
// Original steps to execute the script.
}
// ... some other code.
};
By analogy, all Pjax steps are changed to abortable forms.
Babel Polyfills
Babel polyfill and Babel polyfills on a s away, the former is the old Babel has been officially deprecated based Regenerator-Runtime and Core-JS maintenance polyfill, which is still being tested now Babel Officially maintained polyfill selection-strategy-plugin-set.
Compared to maintaining its own polyfill, Babel focuses more on providing a more flexible polyfill selection strategy.
Current, @ babel / PRESET-env support target browser, by useBuiltIns
provide entry
and usage
two kinds of injection modes; @ babel / plugin-the Transform-Runtime not pollute the global scope, multiplexing auxiliary function library development Reduce the size of the bundle. However, these two components do not work well together, and the polyfill injection mode of the two can only choose one of them. In addition, they only support core-js
, which has great limitations.
The Babel community designed and developed Babel polyfills as a unified solution to these problems after one year of discussion It is and :
- Support designated target browser;
- Support does not pollute the global scope;
- Support @babel/plugin-transform-runtime reuse auxiliary functions;
- Support
core-js
andes-shims
, and support and encourage developers to write their own polyfill providers.
Committed to unifying Babel's selection strategy for polyfills. Babel polyfills many advantages, and its use is the general trend. official use document written very clearly, students in need can click the link to view.
Exclude
Using Babel, it is easy to introduce "unnecessary" polyfills, which greatly increases the size of the library after Pjax is packaged.
- For example, using the URL API it is easy to introduce the
web.url
module. The compressed size occupies 11 KB, which is larger than the current compressed size of the entire Pjax core. It also involvesweb.url-search-params
,es.array.iterator
andes.string.iterator
. The total size of the four compressed is about 16 KB; considering the core-js internal modules introduced by it (the part that is almost always introduced by any core-js polyfill), the total size is about 32 KB, making the size of Pjax compressed from 9 KB -> 41 KB.
This is not actually Babel's pot. The API browser compatibility core-js 160b3774cb6c58 core-js-compat clearly states that web.url
requires Safari 14 web.url
polyfill will be introduced when the target Safari version is less than 14. Then why does core-js-compat require this? URL() constructor of these early versions of Safari has such a BUG , an error will be reported when the second parameter is given and the given value is undefined
Similar problems,
- It appears in the
reduce
method in the array, and it is not caniuse database: Chromium 80-83 This method sometimes gives the wrong initial value of , so the method is compatible with 160b3774cb6 Chrome requirements have been increased to 83+; - Appears on Promise Rejection Event . When the target browser does not support the Event (Firefox <69), the entire Promise polyfill will be introduced.
There are actually many similar problems, but the current Pjax refactoring encountered basically only these three. By adding corresponding judgments to the code and excluding extreme cases, these polyfills can be completely eliminated and the Pjax bundle size can be reduced. Set "exclude" in the plug-in of the Babel configuration file:
["polyfill-corejs3", {
"method": "usage-pure",
"exclude": [
"web.url",
"es.array.reduce",
"es.promise"
]
}]
Concluding remarks
The process of reconstruction is also a process of learning.
Pjax reconstruction also involves History API packaging, the DOM Parser , Optional Chaining (?.) other new API, etc., Jest , Nock migration unit testing tool ......
The author once had an idea whether it would be better to split the three parts of this article into three articles. In the Pjax refactoring, only write a paragraph of these not painful things. But because it's too lazy, let's go.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。