Wen zheng/ink

background

At present, the rendering mode of most front-end applications is mainly client rendering, and the general process of rendering is shown in the figure below:

Through this rendering process, we can see that the page will have a blank screen for a period of time if there is no DOM element in the HTML body of the page before js loading is complete. And if the render is dependent on backend data, the screen will be blank for even longer. At present, there are two technical solutions in the industry to deal with this kind of white screen: loading and skeleton screen.

The main advantage of the loading scheme is that it is simple to implement, but the loading experience is not smooth enough for users. The main advantage of the skeleton screen approach is that the loading experience is more user-friendly and smoother, but it has a higher technical implementation cost, often requiring developers to write code by hand. For more information on what a skeleton screen is and why you should use it, see this article on Medium (need to climb the wall) : medium.com/better-prog…

At present, there are also some schemes in the industry to automatically generate the skeleton screen of a page. The core implementation principle is to run the page in node.js service through the headless browser Puppeteer, obtain its DOM structure, and then generate the HTML and CSS of the skeleton screen of the page. The disadvantages of this scheme are:

  • It is difficult to generate a skeleton screen for a single block in the page, especially in the current popular single-page application (SPA) or various micro-front-end frameworks.
  • There is a relatively high engineering cost, need to deploy and maintain the corresponding Node service or add the corresponding Webpack plug-in in the local debugging service
  • The Puppeteer package itself is large, time-consuming to install and update, and can be added during local debugging to reduce development efficiency.

This article introduces the automatic generation of page skeleton screen scheme is mainly achieved through Chrome plug-in, the overall use experience is more lightweight and customized, can perfect solve the above problems, you can first see the specific effect.

Results show

Take my personal GitHub page for example

This Chrome plugin adds a skeleton panel to the browser developer toolbar to control skeleton screen generation. Fill in the style selector of a block to generate skeleton screen code for that block, and you can preview the effect in the console with one click.

Use the pose

I’m not releasing the Chrome plugin to the Chrome plugin market at the moment because it’s expensive to distribute. Developers can clone my GitHub repository and install the extension locally. The steps are as follows:

Step 1 Install the Chrome plug-in locally

  1. Clone repository clone (github.com/NealST/skel…)

    Git clone github.com/NealST/skel…

2. In the Chrome address box, enter Chrome :// Extensions to go to the extension management page

3, open developer mode, then click the Load unzipped extension button in the upper left corner, select the Build folder in the chrome-Skeleton-Extension directory, and click the Select button to complete the installation.

The second step, select the page, open the console to use

Again, take the GitHub page as an example

Step 3, select the element, generate the skeleton screen, copy the skeleton screen code

Enter the selector for the container element as prompted in the panel form, and then click the Generate button to generate the skeleton HTML and CSS for the element. The React component is also provided in the panel. In addition to the optional container elements, you can also customize the color of the skeleton screen.

After the skeleton screen is generated, all you need to do is copy the HTML and CSS code and put it into the HTML template for that page. Or just use its React component.

Realize the principle of

The core of the implementation principle of the whole plug-in lies in three parts, namely, the panel development in browser Devtool, the communication between devTool panel and content script (namely, the content script of the plug-in), and the skeleton screen structure and style of the specified DOM generated by content script.

Devtool panel development

The Chrome plugin provides an API for creating the DevTool panel. All you need to do is generate an HTML rendering for the panel. The core code is as follows:

/ / create a chrome devtool panel. Devtools. Panels. Create (" Skeleton ", and "icon - 34. PNG", "devtools - panel. HTML", (panel) => { panel.onShown.addListener(onPanelShown); panel.onHidden.addListener(onPanelHidden); }); function onPanelShown () { chrome.runtime.sendMessage('skeleton-panel-shown'); } function onPanelHidden() { chrome.runtime.sendMessage('skeleton-panel-hidden'); } // Render the HTML content of the panel import React from 'React '; import { render } from 'react-dom'; import { processMessageFromBg, buildConnectionWithBg } from './utils' import App from './app'; Const Connection = buildConnectionWithBg(); connection.onMessage.addListener((message) => processMessageFromBg(message)); render(<App />, document.getElementById('app-container')); Import React, {useState} from 'React '; // Input the form import SkeletonForm from './components/skeleton-form'; Import SkeletonHtml from './components/ SkeletonHtml '; Import SkeletonCss from './components/ SkeletonCss '; import './app.css'; export default function() { const [ codeInfo, setCodeInfo ] = useState({ html: '', css: '', isMobile: false }); function getSkeletonCode(retCode) { console.log("code info", retCode); setCodeInfo(retCode); } return ( <div className="devtools-panel"> <div className="panel-header"> <SkeletonForm getSkeletonCode={getSkeletonCode} /> </div> <div className="panel-code"> <div className="code-html"> <SkeletonHtml code={codeInfo.html} isMobile={codeInfo.isMobile}/> </div> <div className="code-css"> <SkeletonCss code={codeInfo.css} /> </div> </div> </div> ) }Copy the code

Communication between the Devtool panel and content scripts

The communication between the DevTool panel and the content script is mainly carried out through the background of the Chrome plug-in, which runs in the background of Chrome and runs through the whole life cycle of the plug-in. The background has the highest permission and can almost call all the API of the Chrome plug-in. The core implementation code of the communication mode is as follows:

// devtool-panel.js part let processFnMap = {}; Export const buildConnectionWithBg = function () {const connection = Chrome.runtime.connect ({const buildConnectionWithBg = function () {const connection = Chrome.runtime.connect ({ name: `skeleton-panel-${tabId}`, }); connection.postMessage({ type: "init", tabId, }); return connection; }; Export const processMessageFromBg = function (message) {console.log("get message", message); const processFn = processFnMap[message.type]; processFn && processFn(message.info); }; Export const sendMsgToContent = function (info, cb) { processFnMap[info.type] = processFnMap[info.type] || cb; chrome.runtime.sendMessage({ tabId, isToContent: true, info, }); }; // let connections = {}; chrome.runtime.onConnect.addListener(function (port) { var extensionListener = function (message, sender, SendResponse) {// The original connection event does not contain the tag identifier of the developer tools web page, so we need to send it explicitly. const { type, tabId } = message || {}; if (type == "init") { connections[tabId] = port; return; }}; / / to monitor from the developer tools page message port. The onMessage.. addListener (extensionListener); port.onDisconnect.addListener(function (port) { port.onMessage.removeListener(extensionListener); var tabs = Object.keys(connections); for (var i = 0, len = tabs.length; i < len; i++) { if (connections[tabs[i]] == port) { delete connections[tabs[i]]; break; }}}); }); Chrome / / receiving messages. The runtime. OnMessage. AddListener (function (request, sender, sendResponse) {the console. The log (" request ", request); const { isToContent, tabId, info = {} } = request; If (isToContent) {// If it is passed to the content script chrome.tabs. SendMessage (tabId, info, function (response) { console.log("response from content", response); const thePort = connections[tabId]; thePort.postMessage({ type: info.type, info: response }); }); } // Implement text copy if (info.type === 'copy') {const copyTextarea = document.getelementById ('app-textarea'); copyTextarea.value = info.data; copyTextarea.select(); document.execCommand( 'copy'); const thePort = connections[tabId]; thePort.postMessage({ type: info.type, info: '' }); }}); / / the content - script. Js part chrome. Runtime. OnMessage. AddListener (async function (the req, sender, sendRes) { switch (req.type) { case 'generate': const { containerId } = req.data; queryInfo = req.data; containerEl = document.querySelector(containerId); // If no element is found, null if (! containerEl) { sendRes(null); return } displayStyle = window.getComputedStyle(containerEl).display; clonedContainerEl = getClonedContainerEl(containerEl, containerId); await genSkeletonCss(clonedContainerEl, req.data); const { style, cleanedHtml } = await getHtmlAndStyle(clonedContainerEl); const isMobile = window.navigator.userAgent.toLowerCase().indexOf('mobile') > 0; skeletonInfo = { html: htmlFilter(cleanedHtml), css: style, isMobile }; sendRes(skeletonInfo); break; case 'show': containerEl.style.display = 'none'; clonedContainerEl.style.display = displayStyle; break; case 'hide': containerEl.style.display = displayStyle; clonedContainerEl.style.display = 'none'; break; case 'query': sendRes({ isInPreview: clonedContainerEl && clonedContainerEl.style.display ! == 'none', queryInfo, skeletonInfo }); break; }});Copy the code

ContentScript skeleton screen generation

The Content-script of Chrome plug-in can obtain and manipulate the DOM structure of the current page. Therefore, after obtaining the selector information sent from the Devtool panel, all we need to do is search for the corresponding DOM according to the selector and traverse the content of child elements under the DOM node. The skeleton screen style processing algorithm can be implemented according to different types of DOM nodes. The core code is as follows:

import svgHandler from "./svg-handler"; import textHandler from "./text-handler"; import listHandler from "./list-handler"; import buttonHandler from "./button-handler"; import backgroundHandler from "./background-handler"; import imgHandler from "./img-handler"; import pseudosHandler from "./pseudos-handler"; import grayHandler from "./gray-handler"; import blockTextHandler from './block-text-handler'; import inputHandler from "./input-handler"; import { getComputedStyle, checkHasPseudoEle, checkHasBorder, checkHasTextDecoration, isBase64Img, $$, $, checkIsTextEl, checkIsBlockEl } from "./utils"; import { DISPLAY_NONE, EXT_REG, GRADIENT_REG, MOCK_TEXT_ID, Node, DEFAULT_COLOR } from "./constants"; import { transparent, removeElement, styleCache } from "./dom-action"; Function traverse(containerEl, options) {const {excludes = [], cssUnit = "px", containerId, color } = options; const themeColor = color || DEFAULT_COLOR; const excludesEle = excludes && excludes.length ? Array.from($$(excludes.join(","))) : []; const svg = { color: themeColor, shape: "circle", shapeOpposite: [], }; const text = themeColor; const button = { color: themeColor }; const image = { shape: "rect", color: themeColor, shapeOpposite: [], }; const pseudo = { color: themeColor, shape: "circle", shapeOpposite: [], }; const decimal = 4; const texts = []; const buttons = []; const hasImageBackEles = []; let toRemove = []; const imgs = []; const svgs = []; const inputs = []; const pseudos = []; const gradientBackEles = []; const grayBlocks = []; (function preTraverse(ele) { const styles = getComputedStyle(ele); const hasPseudoEle = checkHasPseudoEle(ele); if ( ! ele.classList.contains(`mz-sk-${containerId}-clone`) && DISPLAY_NONE.test(ele.getAttribute("style")) ) { return toRemove.push(ele); } if (~excludesEle.indexOf(ele)) return false; // eslint-disable-line no-bitwise if (hasPseudoEle) { pseudos.push(hasPseudoEle); } if (checkHasBorder(styles)) { ele.style.border = "none"; } let styleAttr = ele.getAttribute("style"); if (styleAttr) { if (/background-color/.test(styleAttr)) { styleAttr = styleAttr.replace( /background-color:([^;] +); /, "background-color:#fff;" ); ele.setAttribute("style", styleAttr); } if (/background-image/.test(styleAttr)) { styleAttr = styleAttr.replace(/background-image:([^;] +); /, ""); ele.setAttribute("style", styleAttr); } } if (ele.children && ele.children.length > 0 && /UL|OL|TBODY/.test(ele.tagName)) { listHandler(ele); } // If (checkIsTextEl(ele) && checkIsBlockEl(ele)) {blockTextHandler(ele)} if (ele. Children && ele.children.length > 0) { Array.from(ele.children).forEach((child) => preTraverse(child)); } // Set the text color of all elements that have children of textChildNode to the background color so that no text is displayed. if ( ele.childNodes && Array.from(ele.childNodes).some((n) => n.nodeType === Node.TEXT_NODE) ) { transparent(ele); } if (checkHasTextDecoration(styles)) { ele.style.textDecorationColor = TRANSPARENT; } // Hide all SVG elements if (ele. TagName === "SVG ") {return svgs.push(ele); } // INPUT element if (elel.tagname === "INPUT") {return elsion.push (ele); } if ( EXT_REG.test(styles.background) || EXT_REG.test(styles.backgroundImage) ) { return hasImageBackEles.push(ele); } if ( GRADIENT_REG.test(styles.background) || GRADIENT_REG.test(styles.backgroundImage) ) { return gradientBackEles.push(ele); } if (ele.tagName === "IMG" || isBase64Img(ele) || ele.tagName === "FIGURE") { return imgs.push(ele); } if ( ele.nodeType === Node.ELEMENT_NODE && (ele.tagName === "BUTTON" || (ele.tagName === "A" && ele.getAttribute("role") === "button")) ) { return buttons.push(ele); } if (checkIsTextEl(ele)) { return texts.push(ele); } })(containerEl); svgs.forEach((e) => svgHandler(e, svg, cssUnit, decimal)); inputs.forEach(e => inputHandler(e, themeColor)); texts.forEach((e) => textHandler(e, text, cssUnit, decimal)); buttons.forEach((e) => buttonHandler(e, button)); hasImageBackEles.forEach((e) => backgroundHandler(e, image)); imgs.forEach((e) => imgHandler(e, image)); pseudos.forEach((e) => pseudosHandler(e, pseudo)); gradientBackEles.forEach((e) => backgroundHandler(e, image)); grayBlocks.forEach((e) => grayHandler(e, button)); // remove mock text wrapper const offScreenParagraph = $(`#${MOCK_TEXT_ID}`); if (offScreenParagraph && offScreenParagraph.parentNode) { toRemove.push(offScreenParagraph.parentNode); } toRemove.forEach((e) => removeElement(e)); }Copy the code

conclusion

At present, the plug-in has been implemented in our internal workbench application, which improves the page loading experience with visible effects. You are welcome to use it in your project, if you are interested in the plugin or skeleton screen, or have any questions or feedback on new requirements, please feel free to contact me.

Company email: [email protected]

Personal email: [email protected]

Ps: Our business platform – Experience Technology Star Circle team is recruiting talents at the front end and client end. The team is harmonious and friendly, and the technical atmosphere is strong (leaders advocate no code, no BB). The business and technical scenes are also very broad.