preface

When visiting station B, I happened to find that Village head Young and teacher Kagol recruited friends to participate in the construction of Vue3 component library. This article will tell me how I developed Skeleton components, from a little white grow to understand so little white.

Technology stack

Vite+Vue3+TypeScript+JSX

What does the skeleton screen do

In my opinion, before implementing a component, one should first understand what problems the component solves, and then refer to how other mature component libraries are implemented and what apis they provide. This chapter is taken from “A Solution for Automatically generating skeleton screens”.

Evolution of the first screen loading

Let’s start with two authoritative studies.

One, from Akamai, interviewed a total of about 1,048 online shoppers and concluded:

  • About 47% of users expect their pages to load in less than two seconds.
  • If the page takes longer than 3s to load, about 40% of users choose to leave or close the page.

Another test TagMan conducted with Glasses Direct, an eyewear retailer, looked at the relationship between page loading speed and final conversion:

In the test report and found the page load rate and conversion rate showed obvious negative correlation, in the page load time is 1 ~ 2 seconds when the conversion rate is the highest, and when the loading time continue to grow, conversion rate began to present a downward trend, every increase 1 s about page load time conversion rate dropped by 6.7%.

Typically, a progress bar or spinning Spinner is displayed on the page on the first screen or when fetching data.

  • Progress bar: We use progress bars when we know exactly how long an interaction will take, or when we know an approximate value.
  • Spinner: There is no way to predict how long it will take to fetch data or open a page.

Having a progress bar or Spinner tells the user at least two things:

  • The operation you are doing will take some time to wait.
  • Secondly, appease users and let them wait patiently.

Beyond that, the progress bar and Spinner don’t do anything else, either to make the user feel that the page loads faster or to give the user a focus to focus on and know that that focus is about to show what the user is interested in.

Skeleton screens are a better solution than progress bars and Spinners.

Why do WE need a skeleton screen?

  • At the beginning in the study of MIT in 2014 already mentioned, the user will probably be within 200 ms access to specific concerns of interface, data acquisition or page load to complete before, to the user first show the skeleton screen, frame screen style, layout, and is consistent with real data rendered page, so that users get to focus in the skeleton screen, You can also anticipate where text will be displayed and where images will be displayed, so you can shift the focus of attention to the place of interest. When the real data acquisition, replace with real data rendered page frame screen, if the process within 1 s, users almost not perceive the data loading process and the final rendered page replacement frame screen, and on the user’s perception, a skeleton screen capturing the data have a moment, and then just data progressive rendering. The user perceives the page to load faster.
  • If you look at the current front-end framework,React,Vue,AngularMost front-end applications on the market are based on these three frameworks or libraries. These three frameworks have a common feature, they are JS-driven, and the page will not show any content before the COMPLETION of THE JS code parsing, which is the so-called white screen. Users hate to see a blank screen that shows nothing, and they are likely to suspect that something is wrong with the network or application. Vue, for example, passes state values in DATA and computed in the component when the application is startedObject.definePropertyMethods are converted to set and GET access properties to listen for data changes. All of this is done at startup time, which inevitably results in a slower page startup than a non-JS-driven (such as jQuery) page.

Implement skeleton screen

Implement basic styles

The basic skeleton screen effect is shown below:

The DOM structure is very simple, and I split it into headings (the first line) and paragraphs (the last three lines) so that I can control the presentation separately through the API later.

<div class="devui-skeleton devui-skeleton-animated"> <div class="devui-skeleton__item__group"> <div class="devui-skeleton__title" style="width: 40%;" > </div> <div class="devui-skeleton__paragraph"> <div class="devui-skeleton__item"> </div> <div class="devui-skeleton__item"></div> <div class="devui-skeleton__item"> </div> </div> </div> </div>Copy the code

Animation effect with CSS to achieve, refer to the minimum class “CSS skeleton screen effect”, here only write the most critical animation effect. The end result is similar to Element-Plus, while Vant’s skeleton screen is closer to fade-in and fade-out.

.devui-skeleton__title, .devui-skeleton__item { @keyframes skeletonLoading { to { background-position-x: -20%; } } background: Linear-gradient (100DEg, Rgba (255, 255, 255, 0) 40%, Rgba (255, 255, 255, 0.5) 50%, Rgba (255, 255, 255, 0) 60%) #f2f2f2;  background-size: 200% 100%; background-position-x: 180%; animation: 2s skeletonLoading ease-in-out infinite; }Copy the code

PS: Wrapping styles in component styles when it comes to changing common styles is something I think should be optimized through configuration later on.

Full effect

The final complete DOM structure of skeleton screen is as follows:

<div class="devui-skeleton devui-skeleton-animated"> <div class="devui-skeleton__avatar"> <div class="avatar" style="width: 40px;" ></div> </div> <div class="devui-skeleton__item__group"> <div class="devui-skeleton__title" style="width: 40%;" ></div> <div class="devui-skeleton__paragraph"> <div class="devui-skeleton__item" style="width: 100%;" ></div> <div class="devui-skeleton__item"></div> <div class="devui-skeleton__item"></div> </div> </div> </div>Copy the code

The CSS is as follows:

.devui-skeleton { display: flex; justify-content: space-between; .devui-skeleton__avatar { display: flex; flex: 1; justify-content: center; padding-right: 16px; .avatar { width: 40px; height: 40px; background-color: #f2f2f2; } } .devui-skeleton__item__group { flex: 11; .devui-skeleton__item, .devui-skeleton__title { width: 100%; height: 16px; background-color: #f2f2f2; } .devui-skeleton__title { margin-top: 24px; } .devui-skeleton__paragraph { margin-top: 12px; } .devui-skeleton__item:last-child { width: 60%; } } } .devui-skeleton-animated > .devui-skeleton__item__group > .devui-skeleton__title, .devui-skeleton-animated > .devui-skeleton__avatar > .avatar, .devui-skeleton-animated > .devui-skeleton__item__group > div > .devui-skeleton__item { @keyframes skeletonLoading { to { background-position-x: -20%; } } background: Linear-gradient (100DEg, Rgba (255, 255, 255, 0) 40%, Rgba (255, 255, 255, 0.5) 50%, Rgba (255, 255, 255, 0) 60%) #f2f2f2;  background-size: 200% 100%; background-position-x: 180%; animation: 2s skeletonLoading ease-in-out infinite; } .devui-skeleton__avatar > .avatar, .devui-skeleton__item__group > div > .devui-skeleton__item { margin-top: 12px; }.devui-skeleton-skeleton >. Devui-skeleton__avatar >. }Copy the code

To actually use the skeleton screen, just wrap the content around the skeleton, as follows:

<template> <div class="skeleton-btn-groups"> <div class="skeleton-btn"> <d-switch V-model :checked="loading" /> </div> <div class="skeleton- BTN "> <d-switch V-model :checked="animate" /> </div> <div class="skeleton- BTN "> < D-switch V-model :checked="avatar" /> </div> <div class="skeleton- BTN "> <d-switch V-model :checked="title" /> </div> <div class="skeleton- BTN "> <d-switch V-model :checked="paragraph" /> </div> <div class="skeleton- BTN "> <d-switch V-model :checked="roundAvatar" /> </div> <div class="skeleton- BTN ">  <d-switch v-model:checked="round" /> </div> </div> <d-skeleton :row="3" :animate="animate" :avatar="avatar" :avatar-shape="roundAvatar? '':'square'" :title="title" :paragraph="paragraph" :loading="loading" :round="round"> <div> <div>row one</div> <div>row two</div> <div>row three</div> <div>row four</div> </div> </d-skeleton> </template> <script> import { defineComponent, ref } from 'vue' export default defineComponent({ setup () { const loading = ref(true) const animate = ref(true) const avatar = ref(true) const title = ref(true) const paragraph = ref(true) const roundAvatar = ref(true) const round = ref(false) return { loading, animate, avatar, title, paragraph, roundAvatar, round } } }) </script> <style> .skeleton-btn-groups{ display: flex; margin-bottom: 1rem; } .skeleton-btn{ display: flex; flex-direction: column; justify-content: space-between; } </style>Copy the code

The final effect is as follows:

parameter

The variable code is as follows:

import type { ExtractPropTypes, PropType } from 'vue'
​
export type ModelValue = number | string
​
export const skeletonProps = {
  row: {
    type: Number,
    default: 0
  },
  animate: {
    type: Boolean,
    default: true
  },
  round: {
    type: Boolean,
    default: false
  },
  loading: {
    type: Boolean,
    default: true
  },
  avatar: {
    type: Boolean,
    default: false
  },
  title: {
    type: Boolean,
    default: true
  },
  paragraph: {
    type: Boolean,
    default: true
  },
  avatarSize: {
    type: [String, Number] as PropType<ModelValue>,
    default: '40px'
  },
  avatarShape: {
    value: String as PropType<'round' | 'square'>,
    default: 'round'
  },
  titleWidth: {
    type: [String, Number] as PropType<ModelValue>,
    default: '40%'
  },
  rowWidth: {
    type: [Number, String, Array] as PropType<number | string | Array<number | string>>,
    default: ['100%']
  }
} as const
​
export type SkeletonProps = ExtractPropTypes<typeof skeletonProps>
​
Copy the code

API implementation

Usually in Vue projects, we use V-for to traverse elements in template code and V-if to determine whether or not to render elements. In JSX, for v-for, you can use a for loop,array.map, and for V-if, you can use a ternary expression.

import './skeleton.scss' import { defineComponent } from 'vue' import { skeletonProps, SkeletonProps } from './skeleton-types' export default defineComponent({ name: 'DSkeleton', props: skeletonProps, setup(props: SkeletonProps, ctx) { const { slots } = ctx; function renderAnimate(isAnimated) { return isAnimated ? 'devui-skeleton-animated' : '' } function renderBorderRadius(isRound) { return isRound ? 'border-radius: 1em; ': '' } function renderParagraph(isShown, rowNum, rowWidth, round) { const arr = [] function pushIntoArray(type) { for (let index = 0; index < rowNum; index++) { arr.push({ width: type }) } } (function handleRowWidth() { if (rowWidth instanceof Array) { for (let index = 0; index < rowNum; index++) { if (rowWidth[index]) { switch (typeof rowWidth[index]) { case 'string': arr.push({ width: rowWidth[index] }) break case 'number': arr.push({ width: `${rowWidth[index]}px` }) } } else { arr.push({ width: 1 }) } } } else { switch (typeof rowWidth) { case 'string': pushIntoArray(rowWidth) break case 'number': pushIntoArray(`${rowWidth}px`) break } } })() return <div class="devui-skeleton__paragraph" v-show={isShown}>{ arr.map(item => { return <div class="devui-skeleton__item" style={round ? 'border-radius: 1em; ' : '' + `width: ${item.width}`} /> }) }</div> } function renderAvatarStyle(avatarSize, avatarShape) { function renderAvatarSize(avatarSize) { switch (typeof avatarSize) { case 'string': return `width:${avatarSize}; height:${avatarSize}; ` case 'number': return `width:${avatarSize}px; height:${avatarSize}px; ` } } function renderAvatarShape(avatarShape) { return avatarShape === 'square' ? '' : 'border-radius:50%; ' } return (renderAvatarSize(avatarSize) + renderAvatarShape(avatarShape)) } function renderTitle(isVisible, titleWidth, isRound) { function renderTitleWidth(titleWidth) { switch (typeof titleWidth) { case 'string': return `width: ${titleWidth}; ` case 'number': return `width: ${titleWidth}px; ` } } function renderTitleVisibility(isVisible) { return isVisible ? null : 'visibility: hidden; ' } return (renderTitleWidth(titleWidth) + renderBorderRadius(isRound) + renderTitleVisibility(isVisible)) } function renderSkeleton(isLoading) { if (isLoading) { return <> <div class="devui-skeleton__avatar" v-show={props.avatar}> <div class="avatar" style={renderAvatarStyle(props.avatarSize, props.avatarShape)} /> </div> <div class="devui-skeleton__item__group"> <div class="devui-skeleton__title" style={renderTitle(props.title, props.titleWidth, props.round)} /> {renderParagraph(props.paragraph, props.row, props.rowWidth, props.round)} </div> </> } return <>{slots.default?.()}</> } return () => { return <div class={`devui-skeleton ${renderAnimate(props.animate)}`}> {renderSkeleton(props.loading)} </div> } } })Copy the code

The main reason I learned why I chose JSX is that component library code is much more dynamic than business code, you have a lot of flexibility to control dynamic DOM fragments with JSX, and you can enjoy type cues for props.

Existing deficiencies

  1. In TSX, I put too much logic in style, and the mature approach would have been to control class for better performance and more intuitive DOM reflection.
  2. It may seem too large to look at a single TSX file after completion. It might be better to start by dividing components into features, then one file for each feature, inherited into Setup via the Composition API.
  3. The rowWidth argument is passed in as an array by default, whereas the Vant only needs to pass in Number or String for the same function. Since Elment does not provide the ability to control each row individually, comparisons are not made here.

Unit testing

Another skill I learned in the process of writing components is unit testing. If you don’t know much about it, I recommend you to read vue3’s test guide to get started quickly.

Components need unit testing for three main reasons:

  1. Unit tests are performed to prove that the code behaves as expected
  2. Sufficient unit testing is the only way to improve software quality and reduce development cost
  3. Repeatable unit tests after developers make changes can avoid those unpleasant side effects

Since I’m relatively inexperienced, my single test is usually to verify that the DOM structure is as expected and contains the expected class name. Here is my single test code:

import { mount } from '@vue/test-utils'; import { ref } from 'vue'; import DSkeleton from '.. /src/skeleton'; Describe ('skeleton ', () => {it('render basic skeleton successfully', () => {const row = ref(4); const wrapper = mount({ components: { DSkeleton }, template: `<d-skeleton :row="row" />`, setup() { return { row }; }}); expect(wrapper.classes()).toContain('devui-skeleton') expect(wrapper.classes()).toContain('devui-skeleton-animated') Expect (wrapper. Element. ChildElementCount). Place (1) / / apply colours to a drawing number shall be identical with the number of incoming row expect(wrapper.element.children[0].childElementCount).toBe(4) }) it('render skeleton without animate', () => { const animate = ref(false); const wrapper = mount({ components: { DSkeleton }, template: `<d-skeleton :animate="animate" />`, setup() { return { animate }; }}); expect(wrapper.classes()).toContain('devui-skeleton-no-animated') }) it('render skeleton with avatar', () => { const avatar = ref(true); const wrapper = mount({ components: { DSkeleton }, template: `<d-skeleton :avatar="avatar" />`, setup() { return { avatar }; }}); expect(wrapper.element.childElementCount).toBe(2) expect(wrapper.element.children[0].innerHTML).toBe('<div class="avatar"></div>') }) it('hide skeleton and show real content', () => { const row = ref(4); const loading = ref(false); const wrapper = mount({ components: { DSkeleton }, template: ` <d-skeleton :row="4" :loading="loading"> <div> <div>content1</div> <div>content2</div> <div>content3</div> <div>content4</div> </div> </d-skeleton>`, setup() { return { row, loading }; }}); expect(wrapper.classes()).toContain('devui-skeleton') expect(wrapper.element.children[0].innerHTML).toBe('<div>content1</div><div>content2</div><div>content3</div><div>conten t4</div>') }) })Copy the code

As for the unit test, I asked Teacher Kagol again, and his understanding was the test of component rendering, event triggering and useXXX logic.

As a result, since the skeleton screen is mainly for data display, I tend to test the DOM structure without any problems. If you are doing Toast, you should detect event triggers more.

conclusion

I’ve been working on this summary for some time, but I’ve polished it up and added some thought. In addition, DevUI has recently released a number of components for those interested in participating.

Temporary preview address

DevUI Skeleton components

reference

  1. DevUI document
  2. A scheme for automatically generating skeleton screen
  3. [CSS] Skeleton screen effect
  4. Learn to use Vue JSX, a car full of Laoganma is yours
  5. Vue3 test guide
  6. How to do front-end unit testing