preface

Skeleton screen is not a new concept, should have ten years ago, is the way we generate and use slightly different way, essential purpose is to in the premise of not obvious influence on the performance page, enhance the user experience, of course, the user experience is a subjective point of view, may show a loading when some people feel bad is better than frame screen experience, I’d rather just leave the screen blank until the page starts rendering elements. Ok, let’s move on

Why semi-automatic?

Automatically, in my opinion, is the premise of the user does not need extra operation, simply introduce tools/plugin page can generate skeleton screen, but this is not realistic, because the skeleton screen is simply to show the main page layout, main layout is determined by the subjective point of view, you make different UI design with a page frame screen, more or less will be a little difference, The skeleton screen can not be achieved in one step as everyone wants, so it needs to be generated by user configuration of some rules + general identification logic, so it is called semi-automatic

Train of thought

The main idea is to traverse the entire page elements and match the current elements with predefined rules, such as identifying different modules, such as images, buttons, text… , and then use the module conversion logic to convert the previously identified modules into color blocks (div), and then splicken the color blocks together according to the geometric position of the elements, so that the skeleton screen is generated, so we need to do the following:

  • Module definition, i.e., whatDOMWhat module does the element belong to
  • Module processing logic, how does the current module restore the originalDOMElement geometry attributes under the premise of using color blocks instead
  • Reassemble the top color blocks together to form a skeleton screen

So this is the process of identification. Before we start the process of identification, another question is how do we start our identification logic? Since we are analyzing the DOM elements of the page to generate the skeleton screen, we need to get the page elements first, so we need to provide a way to inject recognition logic into the page, and finally, we need to provide a visual interface to show the results of the previous parsing. Now, let’s look at the process step by step

Module identification and generation

Mainstream of modules on the market to identify all the same, in this paper, the same logic, logic here can speak, but generated a bit different, hungry? (this plug-in to give me a lot of thinking, thank 🙏 development of bosses) open source plug-in generation logic is reuse CSS, which generated the same CSS color piece can use with a DOM element, so can achieve consistent layout, However, in this paper, FIXED layout is adopted. I will calculate the width, height, top and left of elements, and then use a fixed DIV to replace the original elements. Instead of using the original CSS, the styles will be completely rewritten.

  • Reuse CSS:
    • Advantages: style generation is relatively simple, after all, the style is ready-made, so it can generate a skeleton screen consistent with the structure of the page, after the skeleton screen generation, if you want to debug the results are also more friendly, becauseDOMThe structure andCSSIt is as simple as debugging your own page.
    • Disadvantages: Skeleton skeleton block linkage between serious, screen does not need to convert all the elements of the page, so will be part of the element is not transformation, but the layout of the elements is dependent on each other, such as layout, the right of the element is the element on the left into the right, at this time if we want to keep the right element, then the left elements has been removed, lead to the right element into the left elements, of course, This can also be control elements, but to the left elements set to transparent state, so the effect is ok, but at the same time lead to more frame screen some redundant elements, lead to frame screen code bigger volume, optimizing CSS is bad, it’s hard to compare thoroughly remove those who do not affect the outcome of the style, because of the different combination of style as a result of the difference is very big, It’s hard to tell which styles are useless for skeleton screens
  • fixed
    • Advantages:CSSOptimization is simple, after all, CSS is generated by ourselves, the rules are also defined by ourselves, we can extract the common class, for example, when the top value of multiple elements is consistent, you can write a separate example.top64 { top: 64vw }Style, this can reduce the size of the CSS, the linkage between the elements, because it is based on fixed layout, so there will not be the above left and right layout missing its temporarily caused by embarrassment 😅.
    • Disadvantages: For the overall movement of the scene debugging is difficult, for example, we want to make a row of color blocks collective move down, but because the layout is fixed, so can only be changed one by one, not like the first way, change their parent elements. Locating elements is also not as easy as the first method.

This paper does not discuss which one is optimal. The second one is adopted in this paper, and three representative scenarios are given as examples

Identify the image

Image recognition is relatively simple, when the recognition toTag then it’s an image, and then it passesgetBoundingClientRect()You can obtain the position and width of the image, so that you can use a div to replace the image as the color block of the skeleton screen, pseudo-code is as follows

// Get the position and width
const { left, top, width, height } = imgNode.getBoundingClientRect()
let style = `
    position: fixed;
    top: ${top};
    left: ${left};
    width: ${width};
    height: ${height};
`
let div = document.createElement('div')
div.setAttribute('style', style)
Copy the code

Identify the text

Text is the most complex, after all, it is divided into single-line text and multi-line text. Single-line text is easy to understand, which is to obtain the geometric attributes of the text itself to set a div instead, but the multi-line text is expected to have the following effects:You can see, multiline text is not a simple alternative with a colors, but in a similar article at a zebra crossing in color, the color of the produce can use gradient, because in linear gradient, if we set the starting point for the linear gradient less than the starting point of the previous color, gradient effect will disappear, replaced by two different colors of color, Any line of text is actually composed of three parts:So we need for any line of copy, article parsed into three color, text above is a blank, a text itself, at the bottom of the text is also a blank, so calculate the rate of two points, namely the text at the top of the position with the position of the text at the bottom, so you know that three parts separately in a row height ratio, and the height of a row can be divided into three parts. We can simply assume that the height of the upper and lower whitespace is the same, the height of the text is fontSize, then the two points can be calculated:

const textHeightRatio = fontSize / parseFloat(lineHeight) // The ratio of text to a line
const firstColorPoint = ((1 - textHeightRatio) / 2 * 100).toFixed(4)
const secondColorPoint = (((1 - textHeightRatio) / 2 + textHeightRatio) * 100).toFixed(4)

const commonRule = {
    'background-image': `linear-gradient(transparent ${firstColorPoint}%, #EFEFEF 0%, #EFEFEF  ${secondColorPoint}%, transparent 0%); `.'background-size': 100% `${lineHeight}` // lineHeight represents the height of a line, that is, the sum of the heights of the three parts above
}
Copy the code

Background-image = background-size = image; background-size = image; background-image = image; background-size = image;

.skeleton__text {
    width: 200px;
    height: 300px;
    background-image: linear-gradient(transparent 12%, #EFEFEF 0%, #EFEFEF 76%, transparent 0%);
    background-size: 100% 30px;
}

<div class="skeleton__text"></div>
Copy the code

Other scenarios

Other, buttons, such as identification of SVG, pseudo element defines the background picture, gradient, etc., as logically consistent picture, just not as identification rules, and the generated color piece color according to the individual subjective requirements set to don’t like it, like to use a background image, the gradient of elements, so it is as a general background, That is, there are other elements to be identified above it. At this time, the function of this element is to divide the area and make the structure of the page clear enough. Therefore, when identifying this element, it should be replaced by a color block of another color, otherwise it will overlap with other elements that are resolved above it. Because this article is about the idea, so the analysis of this part will not be introduced.

The process to deal with

Injection parsing script

Began to have our first parse the script into the page, you can’t get access to the DOM, injected scripts can be monitored HTML – webpack – plugin htmlWebpackPluginBeforeHtmlProcessing events to modify our code entry documents, If we were to inject our parsing script into index.html, of course, it would be a bit inelegant to insert a large section of JS directly into the page. I would insert the script link, load the script after the page opens, and then provide a service that responds to the request script and returns the script, so start a simple Node service locally. I’m a simple service that uses the Express build. Of course, this service does more than that, and I’ll talk about it later. The pseudocode is as follows

class Server {
    constructor(){... server.listen()// Start the service
    }
    listen() { // Start the service
        this.app = express()
        this.listenServer = http.createServer(this.app)

        this.app.all(The '*'.(req, res, next) = > {
          res.header('Access-Control-Allow-Origin'.The '*')
          res.header('Access-Control-Allow-Headers'.'content-type')
          res.header('Access-Control-Allow-Methods'.'PUT,POST,GET,DELETE,OPTIONS')
          next()
        })
        this.initRouters()
        this.listenServer.listen(this.port, () = > {
          this.log.info(`page-skeleton server listen at port: The ${this.port}`)})return Promise.resolve()
    }
    initRouters() { // Set the route
         const { app, staticPath } = this
         // Get the script request accordingly
         app.get(` /${staticPath}/index.bundle.2.js`.(req, res) = > {
              res.setHeader('Content-Type'.'application/javascript')
              fs.createReadStream(path.join(__dirname, '.. / '.'client'.'index.bundle.2.js')).pipe(res)
         })
    }
}
Copy the code

/${staticPath}/index.bundle.2.js and return the local parsing script.

Result visualization

From above, we can get the final skeleton screen, but at this point, the result is only stored in a variable in the parsing script to save the result, we can not see the actual look, so we need to be able to automatically display the result immediately after parsing. Let’s take a look at my visualization:

As above, provides a frame screen visualization, in real time with the right code editing features, on the right to edit the code skeleton on the left side of the screen will be updated in real time, of course, edit function can not provide, you can use the local editor to change the code to also go, although less updated in real time, but with friendly, debugging experience will have code hinting 😁 after all. The skeleton screen on the left is actually implemented through iframe. After the above script is parsed, I will send the parsing result to the local Node service and save it to a variable. Then I will open the visual interface through window.open. With the skeleton screen code on the right, we sent the result of the parse to the Node service, so we can directly request the skeleton screen code stored in the Node service in the visual interface. What about links? The link is also sent along with the skeleton screen code, and the problem is to generate the link. My processing is that I will assemble the parsed result saved by Node into a complete page, and then save it in memory through memory-fs. The file name is a summary of the content survived by hasha, and the pseudo-code is as follows:

try {
      const pathName = path.join(__dirname, '__webpack_page_skeleton__/skeleton') // Set the directory
      let fileName = await hasha(html, { algorithm: 'md5' }) // Generate the file name
      fileName += '.html'
      myFs.mkdirpSync(pathName, fileName) // Create directory
      await promisify(myFs.writeFile.bind(myFs))(path.join(pathName, fileName), html, 'utf8') // Write the file to the directory above
      return `http://localhost:The ${this.port}/The ${this.staticPath}/skeleton/${fileName}` // Returns the link to get the file
} catch (err) {
  console.log(err)
}
Copy the code

Ok, so we saved the result in memory as an.html file, so I just need to write one more node service in response to the above linked request, as follows:

app.get(` /${staticPath}/skeleton/:filename`.async (req, res) => {
      const { filename } = req.params
      if (!/\.html$/.test(filename)) return false
      let html = ' '
      try {
        html = await promisify(myFs.readFile.bind(myFs))(path.resolve(__dirname, `${staticPath}/skeleton/${filename}`), 'utf-8') // Read the page saved in memory
      } catch (err) {
      }
      res.send(html)
 })
Copy the code

You can also save the file on your hard disk and open it directly in the local absolute path. You don’t need to go through node service, but I need to do some extra operations (for example, I actually respond to requests through websocket). But these have nothing to do with the skeleton screen generation idea, so I won’t introduce it.

Updated in real time

Since the visual interface is written by Vue, the editor plug-in is used vue-codemirror. Therefore, how to modify the left side of the code to update in real time is actually very simple. At first, I thought it was complicated. The webSocket service restores the results in memory, generates HTML links, sends the new links to the visual interface, and finally updates the SRC attribute of the iframe. This is one of the reasons I mentioned above that I use webSocket locally, because I need to actively push the results to the visual interface. But that would cause the left side to refresh, flickering, which is not friendly, so I thought, why not just use postMessage? Using this API, I pass the edited code to the iframe in real time. The iframe responds to the result via window.addeventListener (‘message’, ()=>{}), and then replaces the HTML content of the iframe as follows:

// Editor:
onCmCodeChange (newCode) {
      let reg = /\<head\>([\s\S])*\<\/body\>/ // Get the style and HTML
      let code = newCode.match(reg)
      // Get the iframe instance and send it
      document.getElementsByTagName('iframe') [0].contentWindow.postMessage(JSON.stringify({ code }), 'http://localhost:7006')}Copy the code
// iframe
  window.addEventListener('message'.(rsp) = > {
    let data = JSON.parse(rsp.data)
    const { code } = data
    document.getElementsByTagName('html') [0].innerHTML = code / / replace HTML
  })
Copy the code

This way the contents of the iframe are updated in real time, and since only a dozen DOM are being re-rendered at a time, there is no render lag. Here, skeleton screen code injection -> parsing -> visualization is completed, you only need to provide a few buttons in the visual interface to copy the corresponding HTML and CSS, for you to copy into the formal project.

The results of

The size of the skeleton screen generated through the above increases with the increase of parsing elements. The code volume in the above visual interface is about 8K after compression and optimization, which is 24.5% smaller than the previous direct use of images (10.6K). The results will vary depending on personal optimization, mainly CSS optimization, such as extraction of public CSS.

Now that the process is over, let’s mention what else we need to pay attention to in addition to the above, so we won’t discuss it here:

  • Provides configuration capabilities, such as the ability to build a configuration file locally to support elements that can be parsed into specific skeleton blocks, which elements to leave unparsed, parsed styles, Node ports, and so on
  • How cross-platform? How to supportTaro3, vue - cliAnd translate the results into something that these frameworks can recognize
  • Optimization, take a closer look at the existing CSS, you can actually find that a lot of CSS can be extracted into common CSS to reduce the size of the code. So there have to be optimization rules.
  • When does parsing start? I start parsing by listening for keyboard combination events in the script
  • Note the parsing boundary, for example, the above text parsing is based on the copytext default left sort, then if settext-align: center?

As an aside: Skeleton screens have existed for so long, are they still necessary? With all the server-side rendering technologies out there, and all the technologies derived from server-side rendering, is it okay to get rid of the skeleton screen? The way I see it is that you can do away with skeleton screens if you use some sort of server-side rendering technology, but how much does it cost to do that? Technology with skeleton screen above the difference is that the cost of the former 100, make the product experience rose 25 points, the latter only one can make the cost of the product experience ascension 10, so when considering the cost, technology is useful to frame screen as a transition, perhaps in the future network with hardware level up a step, All of our optimizations are unnecessary because the page loads & renders too fast 🏄♀️