Personal collection of articles: Nealyang/PersonalBlog
Chief writer public number: full stack front end selection
background
Performance optimization and reducing page load wait time are always topics in the front-end field. At present, most business cooperation modes are the front and back end separation schemes, which brings many disadvantages as well as convenience. For example, the FCP time is significantly increased (more HTTP request round-trip time consumption), which leads to what we call a long white screen time and poor user experience.
Of course, we can have many kinds of optimization methods, even the skeleton screen introduced in this paper is only the optimization of user experience, there is no improvement in performance optimization data, but its necessity is still self-evident.
This paper mainly introduces the framework screen automatic generation scheme applied in BeeMa architecture of auction source workbench. There are certain customizations, but the basic principles are the same.
Skeleton Skeleton screen
In fact, skeleton screen is to show users the general structure of the page before loading the content, and then replace the content after getting the interface data. Compared with the traditional chrysanthemum loading effect, it will give users the illusion of “part of the page has been rendered”, which can improve user experience to a certain extent. It is essentially a visual transition effect to reduce the user’s anxiety while waiting.
Project research
Skeleton screen technical scheme can be roughly three categories from the implementation:
- Manual maintenance of skeleton screen code (
HTML
,css or vue
、React
) - Use images as skeleton screens
- Automatically generate skeleton screen
For the first two schemes, there are certain maintenance costs and labor costs. Here we mainly introduce the scheme of automatically generating skeleton screen.
At present, the main use in the market is ele. me open source webpack plug-in: Page-skeleton-webpack-plugin. It generates corresponding skeleton screen pages according to different routing pages in the project and packs skeleton screen pages into corresponding static routing pages through Webpack. This approach isolates the skeleton screen code from the business code, and the skeleton screen code (image) is injected into the project through WebPack injection. The advantages are obvious but the disadvantages are also obvious: webpack configuration costs (also dependent on htML-webpack-plugin).
Technical solution
Based on the above technical research, we still decided to adopt the skeleton screen automatic generation scheme with the lowest intrusion into business code and lower configuration cost. Reference ele. me design ideas, based on BeeMa architecture and VScode plug-in to achieve a new skeleton screen generation scheme.
Design principles
Referring to the business teams currently using skeleton screens, we first need to clarify some principles that our skeleton screens need to have:
- Skeleton screen based
BeeMa
architecture - Automatically generate
- Low maintenance cost
- configurable
- High degree of reduction (strong adaptability)
- Low performance impact
- Users can modify data twice
Based on the above principles and the features of VScode plug-in of Beema architecture, our final technical design is as follows:
- Based on BeeMa Framework1 plug-in, provides skeleton screen generation configuration interface
- Selection is based on
BeeMa
Skeleton page support for SkeletonScreen height,ignoreHeight/width
, universal header and background color retention, etc - Based on the
Puppeteer
Get the pre-release page (login support) - Functionality is encapsulated in the BeeMa Framework plug-in
- The skeleton plate just spits out
HTML
Structure, style based on the user to automaticallyCSSInModel
The style of the - Skeleton screen style, precipitate to project
global.scss
To avoid repeated volume increase of inline styles
The flow chart
The technical details
Check the Puppeteer,
/** * Check the local puppeteer *@param LocalPath localPath */
export const checkLocalPuppeteer = (localPath: string) :Promise<string> = > {const extensionPuppeteerDir = 'mac-901912';
return new Promise(async (resolve, reject) => {
try {
// /puppeteer/.local-chromium
if (fse.existsSync(path.join(localPath, extensionPuppeteerDir))) {
// Mac-901912 exists locally
console.log('There is Chromium in the plugin');
resolve(localPath);
} else {
// Node_modules does not exist locally
nodeExec('tnpm config get prefix'.function (error, stdout) {
/ / / Users/nealyang/NVM/versions/node/v16.3.0
if (stdout) {
console.log('globalNpmPath:', stdout);
stdout = stdout.replace(/[\r\n]/g.' ').trim();
let localPuppeteerNpmPath = ' ';
if (fse.existsSync(path.join(stdout, 'node_modules'.'puppeteer'))) {
// If NVM is not used, the global package is in node_modules under prefix
localPuppeteerNpmPath = path.join(stdout, 'node_modules'.'puppeteer');
}
if (fse.existsSync(path.join(stdout, 'lib'.'node_modules'.'puppeteer'))) {
// With NVM, the global package is in node_modules in lib under prefix
localPuppeteerNpmPath = path.join(stdout, 'lib'.'node_modules'.'puppeteer');
}
if (localPuppeteerNpmPath) {
const globalPuppeteerPath = path.join(localPuppeteerNpmPath, '.local-chromium');
if (fse.existsSync(globalPuppeteerPath)) {
console.log('Local Puppeteer found successfully! ');
fse.copySync(globalPuppeteerPath, localPath);
resolve(localPuppeteerNpmPath);
} else {
resolve(' '); }}else {
resolve(' '); }}else {
resolve(' ');
return; }}); }}catch (error: any) {
showErrorMsg(error);
resolve(' '); }}); };Copy the code
After the webView is opened, the local Puppeteer is verified immediately
useEffect(() = >{(async() = > {const localPuppeteerPath = await callService('skeleton'.'checkLocalPuppeteerPath');
if(localPuppeteerPath){
setState("success");
setValue(localPuppeteerPath);
}else{
setState('error')}}) (); } []);Copy the code
Puppeteer is installed into the project, the webpack package does not handle Chromium binaries, you can copy Chromium into vscode extension builds.
But!! Cause build too large, download plug-in timeout!! Therefore, Puppeteer is required to be installed globally on the user’s local location.
puppeteer
/** * Get the skeleton screen HTML *@param PageUrl needs to generate the skeleton screen's pageUrl *@param Cookies Cookies required for login *@param The maximum height of the skeleton screen (the larger the height, the larger the GENERATED SKELETON HTML size) *@param IgnoreHeight Specifies the maximum height of the element to be ignored (below this height is removed from the skeleton screen) *@param IgnoreWidth Ignores the maximum width of an element (width below this is removed from the skeleton screen) *@param RootSelectId Beema renderID. Default is root *@param context vscode Extension context
* @param Progress Progress instance *@param TotalProgress totalProgress *@returns* /
export const genSkeletonHtmlContent = (
pageUrl: string.cookies: string = '[]'.skeletonHeight: number = 800.ignoreHeight: number = 10.ignoreWidth: number = 10.rootId: string = 'root'.retainNav: boolean.retainGradient: boolean.context: vscode.ExtensionContext,
progress: vscode.Progress<{ message? :string | undefined; increment? :number | undefined;
}>,
totalProgress: number = 30,
): Promise<string> = > {const reportProgress = (percent: number, message = 'Skeleton screen HTML being generated') = > {
progress.report({ increment: percent * totalProgress, message });
};
return new Promise(async (resolve, reject) => {
try {
let content = ' ';
let url = pageUrl;
if (skeletonHeight) {
url = addParameterToURL(`skeletonHeight=${skeletonHeight}`, url);
}
if (ignoreHeight) {
url = addParameterToURL(`ignoreHeight=${ignoreHeight}`, url);
}
if (ignoreWidth) {
url = addParameterToURL(`ignoreWidth=${ignoreWidth}`, url);
}
if (rootId) {
url = addParameterToURL(`rootId=${rootId}`, url);
}
if (isTrue(retainGradient)) {
url = addParameterToURL(`retainGradient=The ${'true'}`, url);
}
if (isTrue(retainNav)) {
url = addParameterToURL(`retainNav=The ${'true'}`, url);
}
const extensionPath = (context as vscode.ExtensionContext).extensionPath;
const jsPath = path.join(extensionPath, 'dist'.'skeleton.js');
const browser = await puppeteer.launch({
headless: true.executablePath: path.join(
extensionPath,
'/mac-901912/chrome-mac/Chromium.app/Contents/MacOS/Chromium',),// /Users/nealyang/Documents/code/work/beeDev/dev-works/extensions/devworks-beema/node_modules/puppeteer/.local-chromium/ma c-901912/chrome-mac/Chromium.app/Contents/MacOS/Chromium
});
const page = await browser.newPage();
reportProgress(0.2.'Launch BeeMa built-in browser');
page.on('console'.(msg: any) = > console.log('PAGE LOG:', msg.text()));
page.on('error'.(msg: any) = > console.log('PAGE ERR:'. msg.args));await page.emulate(iPhone);
if (cookies && Array.isArray(JSON.parse(cookies))) {
awaitpage.setCookie(... JSON.parse(cookies)); reportProgress(0.4.'injection cookies');
}
await page.goto(url, { waitUntil: 'networkidle2' });
reportProgress(0.5.'Open corresponding page');
await sleep(2300);
if (fse.existsSync(jsPath)) {
const jsContent = fse.readFileSync(jsPath, { encoding: 'utf-8' });
progress.report({ increment: 50.message: 'Inject built-in JavaScript scripts' });
await page.addScriptTag({ content: jsContent });
}
content = await page.content();
content = content.replace(/ <! ---->/g.' ');
// fse.writeFileSync('/Users/nealyang/Documents/code/work/beeDev/dev-works/extensions/devworks-beema/src/index.html', content, { encoding: 'utf-8' })
reportProgress(0.9.'Get page HTML schema');
await browser.close();
resolve(getBodyContent(content));
} catch (error: any) { showErrorMsg(error); }}); };Copy the code
The configuration in vscode needs to be written to p that will be injected into Chromium
The solution here is to write the configuration information to the query parameter of the URL of the page to be opened
WebView & vscode communication (configuration)
See the monorepo-based vscode plug-in and its related packages development architecture practice summary
vscode
export default (context: vscode.ExtensionContext) => () = > {
const { extensionPath } = context;
let pageHelperPanel: vscode.WebviewPanel | undefined;
const columnToShowIn = vscode.window.activeTextEditord
? vscode.window.activeTextEditor.viewColumn
: undefined;
if (pageHelperPanel) {
pageHelperPanel.reveal(columnToShowIn);
} else {
pageHelperPanel = vscode.window.createWebviewPanel(
'BeeDev'.'Skeleton plate',
columnToShowIn || vscode.ViewColumn.One,
{
enableScripts: true.retainContextWhenHidden: true,}); } pageHelperPanel.webview.html = getHtmlFroWebview(extensionPath,'skeleton'.false);
pageHelperPanel.iconPath = vscode.Uri.parse(DEV_WORKS_ICON);
pageHelperPanel.onDidDispose(
() = > {
pageHelperPanel = undefined;
},
null,
context.subscriptions,
);
connectService(pageHelperPanel, context, { services });
};
Copy the code
connectSeervice
export function connectService(webviewPanel: vscode.WebviewPanel, context: vscode.ExtensionContext, options: IConnectServiceOptions,) {
const { subscriptions } = context;
const { webview } = webviewPanel;
const { services } = options;
webview.onDidReceiveMessage(
async (message: IMessage) => {
const { service, method, eventId, args } = message;
const api = services && services[service] && services[service][method];
console.log('onDidReceiveMessage', message, { api });
if (api) {
try {
const fillApiArgLength = api.length - args.length;
const newArgs =
fillApiArgLength > 0 ? args.concat(Array(fillApiArgLength).fill(undefined)) : args;
const result = awaitapi(... newArgs, context, webviewPanel);console.log('invoke service result', result);
webview.postMessage({ eventId, result });
} catch (err) {
console.error('invoke service error', err);
webview.postMessage({ eventId, errorMessage: err.message }); }}else {
vscode.window.showErrorMessage(`invalid command ${message}`); }},undefined,
subscriptions,
);
}
Copy the code
Call the callService in the Webview
// @ts-ignore
export const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : null;
export const callService = function (service: string, method: string. args) {
return new Promise((resolve, reject) = > {
const eventId = setTimeout(() = > {});
console.log('call vscode extension service:${service} ${method} ${eventId} ${args}`);
const handler = (event) = > {
const msg = event.data;
console.log(`webview receive vscode message:}`, msg);
if (msg.eventId === eventId) {
window.removeEventListener('message', handler);
msg.errorMessage ? reject(new Error(msg.errorMessage)) : resolve(msg.result); }};// webview accepts a message from vscode
window.addEventListener('message', handler);
// WebView sends a message to vscode
vscode.postMessage({
service,
method,
eventId,
args,
});
});
};
Copy the code
const localPuppeteerPath = await callService('skeleton'.'checkLocalPuppeteerPath');
Copy the code
launchJs
Native JS is packaged by rollup
rollupConfig
export default {
input: 'src/skeleton/scripts/index.js',
output: {
file: 'dist/skeleton.js',
format: 'iife',
},
};
Copy the code
Text processing
Here we treat inline elements as text
import { addClass } from '.. /util';
import { SKELETON_TEXT_CLASS } from '.. /constants';
export default function (node) {
let { lineHeight, fontSize } = getComputedStyle(node);
if (lineHeight === 'normal') {
lineHeight = parseFloat(fontSize) * 1.5;
lineHeight = isNaN(lineHeight) ? '18px' : `${lineHeight}px`;
}
node.style.lineHeight = lineHeight;
node.style.backgroundSize = `${lineHeight} ${lineHeight}`;
addClass(node, SKELETON_TEXT_CLASS);
}
Copy the code
The style of SKELETON_TEXT_CLASS is set in global.scSS in the Beema framework.
const SKELETON_SCSS = ` // beema skeleton .beema-skeleton-text-class { background-color: transparent ! important; color: transparent ! important; background-image: linear-gradient(transparent 20%, #e2e2e280 20%, #e2e2e280 80%, transparent 0%) ! important; } .beema-skeleton-pseudo::before, .beema-skeleton-pseudo::after { background: #f7f7f7 ! important; background-image: none ! important; color: transparent ! important; border-color: transparent ! important; border-radius: 0 ! important; } `;
/ * * * *@param ProPath Project path */
export const addSkeletonSCSS = (proPath: string) = > {
const globalScssPath = path.join(proPath, 'src'.'global.scss');
if (fse.existsSync(globalScssPath)) {
let fileContent = fse.readFileSync(globalScssPath, { encoding: 'utf-8' });
if (fileContent.indexOf('beema-skeleton') = = = -1) {
// There is no local skeleton screen style
fileContent += SKELETON_SCSS;
fse.writeFileSync(globalScssPath, fileContent, { encoding: 'utf-8'}); }}};Copy the code
If there is no style class for the skeleton screen in global.scss, it will be injected automatically
This is because as inline elements, the generated skeleton screen code will be large and repetitive, and this is to mention what optimization does
The image processing
import { MAIN_COLOR, SMALLEST_BASE64 } from '.. /constants';
import { setAttributes } from '.. /util';
function imgHandler(node) {
const { width, height } = node.getBoundingClientRect();
setAttributes(node, {
width,
height,
src: SMALLEST_BASE64,
});
node.style.backgroundColor = MAIN_COLOR;
}
export default imgHandler;
Copy the code
export const SMALLEST_BASE64 =
'data:image/gif; base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
Copy the code
Hyperlink processing
function aHandler(node) {
node.href = 'javascript:void(0); ';
}
export default aHandler;
Copy the code
Pseudo-element processing
// Check the element pseudo-class to return the corresponding element and width
export const checkHasPseudoEle = (ele) = > {
if(! ele)return false;
const beforeComputedStyle = getComputedStyle(ele, '::before');
const beforeContent = beforeComputedStyle.getPropertyValue('content');
const beforeWidth = parseFloat(beforeComputedStyle.getPropertyValue('width'), 10) | |0;
consthasBefore = beforeContent && beforeContent ! = ='none';
const afterComputedStyle = getComputedStyle(ele, '::after');
const afterContent = afterComputedStyle.getPropertyValue('content');
const afterWidth = parseFloat(afterComputedStyle.getPropertyValue('width'), 10) | |0;
consthasAfter = afterContent && afterContent ! = ='none';
const width = Math.max(beforeWidth, afterWidth);
if (hasBefore || hasAfter) {
return { hasBefore, hasAfter, ele, width };
}
return false;
};
Copy the code
import { checkHasPseudoEle, addClass } from '.. /util';
import { PSEUDO_CLASS } from '.. /constants';
function pseudoHandler(node) {
if(! node.tagName)return;
const pseudo = checkHasPseudoEle(node);
if(! pseudo || ! pseudo.ele)return;
const { ele } = pseudo;
addClass(ele, PSEUDO_CLASS);
}
export default pseudoHandler;
Copy the code
The pseudo-element style code is shown above in global.scss
General processing
// Remove unwanted elements
Array.from($$(REMOVE_TAGS.join(', '))).forEach((ele) = > removeElement(ele));
// Remove all dom outside the container
Array.from(document.body.childNodes).map((node) = > {
if (node.id !== ROOT_SELECTOR_ID) {
removeElement(node);
}
});
// Remove the non-module element from the container
Array.from($$(` #${ROOT_SELECTOR_ID} .contentWrap`)).map((node) = > {
Array.from(node.childNodes).map((comp) = > {
if (comp.classList && Array.from(comp.classList).includes('compContainer')) {
// Set the module to a white background color
comp.style.setProperty('background'.'#fff'.'important');
} else if (
comp.classList &&
Array.from(comp.classList).includes('headContainer') &&
RETAIN_NAV
) {
console.log('Keep the common header');
} else if (
comp.classList &&
Array.from(comp.classList).join().includes('gradient-bg') &&
RETAIN_GRADIENT
) {
console.log('Retains the gradient background color');
} else{ removeElement(comp); }}); });// Remove the off-screen Node
let totalHeight = 0;
Array.from($$(` #${ROOT_SELECTOR_ID} .compContainer`)).map((node) = > {
const { height } = getComputedStyle(node);
console.log(totalHeight);
if (totalHeight > DEVICE_HEIGHT) {
// all nodes after DEVICE_HEIGHT are deleted
console.log(totalHeight);
removeElement(node);
}
totalHeight += parseFloat(height);
});
// Remove ignore element
Array.from($$(`.${IGNORE_CLASS_NAME}`)).map(removeElement);
Copy the code
Here is a calculation of the off-screen node, i.e., the height of each module in BeeMa is calculated by the user-defined maximum height, and then the sum is calculated. If the height exceeds this height, the subsequent modules will be removed directly, once again to reduce the size of the generated HTML code
use
The basic use
The constraint
Global installation required[email protected] : tnpm i [email protected] –g
After the global installation is complete, the puppeteer automatically searches for the local puppeteer path. If the local puppeteer path is found, the puppeteer is copied to the local puppeteer. Otherwise, you need to manually fill in the path and address. (Once the search is successful, there is no need to fill in the following address and the global Puppeteer package can be deleted)
Currently, only beema architecture source code development is supported
Note ⚠ ️
If the resulting snippet is large, there are two optimizations
1. Reduce the height of skeleton screen (maximum height in configuration interface)
2, in source development, for the first screen of code but not the first screen display of elements addedbeema-skeleton-ignore
The class name of the
Results demonstrate
Effect of ordinary
Generated code size:
Universal header and gradient background color
Auctions off common design elements, which can be seen in the new empty page configuration
The effect is as follows:
Page effect display of complex elements
Default full-screen skeleton screen
Generated code size
No skeleton-ignore invasive optimization, slightly larger 🥺
Another optimization is to reduce the height of the generated skeleton screen!
Half screen skeleton screen
Fast 3G and no throttling network cases
Generated code size
The follow-up to optimize
- Added custom headers for common header styles
- Support skeleton screen style configuration (color, etc.)
- Reduce the reference size of the generated code
- .
- Continually address the use of feedback within the team
The resources
-
page-skeleton-webpack-plugin
-
awesome-skeleton
-
Building Skeleton Screens with CSS Custom Properties
-
Vue page skeleton screen injection practice
-
BeeMa