preface

There was a new project recently where the UI guy somehow found a GIF and threw it into blue Lake,

Said as a global page loading, but I thought about it, or choose to draw one.

At first, I thought about using SVG and Canvas, but finally I chose CSS3 + JS to achieve this effect.

GIF has many disadvantages. As for why SVG and Canvas are excluded, it is because CSS3 + JS has stronger controllability.

Whether it is size or color, or responsive (my project goes VH, VW) that set to fit;


The effect

GIF provided by UI guy

Codesandbox preview

  • Support ring color change and overall display size
  • Support inloadingDisplays text at the bottom and controls its style

Implementation approach

This one mainly uses these few points to achieve the full effect;

  • flexandpositionTo layout
  • Color inheritance of pseudo-classes (currentColor)
  • Border combined with rounded corners to achieve the ring
  • With thetransformandanimationTo achieve the whole transition

Now that we know how to implement the effect, all that’s left is the function points we need to implement;

Because it is mobile oriented, these general things should also be considered

  • Mask layer is controllable
  • Prevent clicks from penetrating the scrollbody
  • The component supports function method calls

The source code

Loading.vue

<template>
  <div id="loading-wrapper">
    <div class="loading-ring" :style="ringStyle">
      <div class="outer" />
      <div class="middle" />
      <div class="inner" />
    </div>
    <div class="text" :style="textStyle" v-if="text">
      {{ text }}
    </div>
  </div>
</template>

<script>
export default {
  name: "Loading".props: {
    text: {
      type: String.default: ""
    },
    textStyle: {
      type: Object.default: function() {
        return {
          fontSize: "14px".color: "#fff"}; }},ringStyle: {
      type: Object.default: function() {
        return {
          width: "100px".height: "100px".color: "#407af3"}; }}},methods: {
    preventDefault(e) {
      // Disallow body scrolling
      console.log(e);
      e.preventDefault();
      e.stopPropagation();
    }
  },
  mounted() {
    document
      .querySelector("body")
      .addEventListener("touchmove".this.preventDefault);
  },
  destroyed() {
    document
      .querySelector("body")
      .removeEventListener("touchmove".this.preventDefault); }};</script>

<style lang="scss" scoped>#loading-wrapper { position: fixed; left: 0; top: 0; height: 100vh; width: 100vw; Background: rgba(0, 0, 0, 0.25); display: flex; justify-content: center; align-items: center; flex-direction: column; .loading-ring { position: relative; width: 200px; height: 200px; .outer, .inner, .middle { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); color: currentColor; &::after { content: ""; display: block; width: 100%; height: 100%; border-radius: 100%; border-left: 10px solid currentColor; border-right: 10px solid currentColor; border-top: 10px solid currentColor; border-bottom: 10px solid transparent; } } .outer { width: 100%; height: 100%; &::after {animation: anticlockwise 1.5s infinite linear; }}. Inner {width: calc(100 * 0.6); Height: calc * 0.6 (100%); &::after {animation: anticlockwise 1.5s infinite linear; }}. Middle {width: calc(100% * 0.8); Height: calc * 0.8 (100%); &:: After {animation: 1.5s infinite linear; } } } .text { color: #fff; font-size: 14px; padding: 30px; width: 250px; text-align: center; } } @keyframes clockwise { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } @keyframes anticlockwise { 0% { transform: rotate(0deg); } 100% { transform: rotate(-360deg); }}</style>


Copy the code

index.js

import Loading from "./Loading.vue";
// to maintain the instance, singleton mode
let instance;
let el;

Loading.install = function(Vue, options = {}) {
  const defaultOptions = {
    text: "".textStyle: {
      fontSize: "14px".color: "#fff"
    },
    ringStyle: {
      width: "100px".height: "100px".color: "#407af3"
    },
    ...options
  };
  Vue.prototype.$loading = {
    show(options = {}) {
      if(! instance) {let LoadingInstance = Vue.extend(Loading);
        el = document.createElement("div");
        document.body.appendChild(el);
        instance = new LoadingInstance({
          propsData: { defaultOptions, ... options } }).$mount(el); }else {
        return instance;
      }
    },
    hide() {
      if (instance) {
        document.body.removeChild(document.getElementById("loading-wrapper"));
        instance = undefined; }}}; };export default Loading;


Copy the code

Options and Usage

options

    text: {  // This is not empty but displays text under loading
      type: String.default: ""
    },
    textStyle: {  // Loading text style, color, and font size
      type: Object.default: function() {
        return {
          fontSize: "14px".color: "#fff"}; }},ringStyle: {  // The size of the outermost ring is proportional to the size of the inner ring.
      type: Object.default: function() {
        return {
          width: "100px".height: "100px".color: "#407af3"}; }}Copy the code

usage

Use in the main entrance can be used globally

In addition to normal import usage, function calls are supported and a $loading is mounted.


this.$loading.show({
        text: "loading".textStyle: {
          fontSize: "18px".color: "#f00"}});let st = setTimeout((a)= > {
        clearTimeout(st);
        this.$loading.hide();
    }, 1000);

Copy the code

conclusion

The props pass is not an incremental merge (recursive assignment for each key), but a shallow copy of the merge.

For the component function of the comprehensiveness, extensibility, size need to weigh;

At this point, we have a widget that our business needs, and all the functions we need.

If there is something wrong, please leave a message and we will correct it in time.