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 vueReact)
  • 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 basedBeeMaarchitecture
  • 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 onBeeMaSkeleton page support for SkeletonScreen height,ignoreHeight/width, universal header and background color retention, etc
  • Based on thePuppeteerGet the pre-release page (login support)
  • Functionality is encapsulated in the BeeMa Framework plug-in
  • The skeleton plate just spits outHTMLStructure, style based on the user to automaticallyCSSInModelThe style of the
  • Skeleton screen style, precipitate to projectglobal.scssTo 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-ignoreThe 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