29 款精选 Loading,为网页添彩,让等待也成风景

2,282 阅读7分钟

Loading 是开发过程中很常见的场景,在 Client/Server 架构盛行的今天,客户端与服务器之间的数据交换往往需要短暂但关键的时间。这段空白期,若能用一个精心设计、引人入胜的 Loading 组件来填充,能极大缓解用户的等待焦虑,也能为应用增添一丝魅力。

但如何 找到使用 就成了问题。

因此我收集并整理了 29 款各具特色的 Loading 样式,并封装成了易于集成的形式,希望能轻松地将它们融入自己的项目中,提升整体的用户体验。

本文主要包含以下几个部分:

  • Loading组件的展示,有哪些效果
  • 怎么使用:越简单越好,适用范围越大越好
  • 怎么设计的:开放源码,讲述原理,大家都能看懂和学会。

如果大家觉得有所帮助,感谢点一个 star,如有不足之处,欢迎大家评论和反馈,一起学习交流。

一.Loading列表

image.png

为了方便展示,文档里有一个Loading List部分,把鼠标移动上去就能看到动画效果,鼠标移开就会暂停。

20240721212345.gif

其中 左上角 会有每个Loading组件的名字,通过name属性即可展示不同的Loading:

<r-loading name="double-bounce"></r-loading> 
<r-loading name="rotate"></r-loading> 
<r-loading name="stretch"></r-loading> 
<r-loading name="cube"></r-loading>

name属性的值,在每个Loading例子的左上角:

image.png

有些Loading效果,暗黑模式下效果更佳,可以在右上角切换 主题颜色

image.png

一些暗黑模式下,Loading的效果:

20240721212852.gif

二.如何使用

最简单的使用方法就是:

npm i ranui@latest --save

然后在项目中导入:

import 'ranui';

项目也提供了分路径导入

import 'ranui/loading';

然后从文档里,找一个觉得还行的Loading效果,找到对应的name名字。

<r-loading name="double-bounce"></r-loading> 
<r-loading name="rotate"></r-loading> 
<r-loading name="stretch"></r-loading> 
<r-loading name="cube"></r-loading>

就引入完成啦

1.如何自定义颜色,动画,大小等等属性

在使用Loading组件的过程中,可能会遇到需要自定义颜色,大小,动画的情况。以上都属于样式的范畴,可以通过自定义样式的方式去处理。

webcompoents组件中,会把样式放到shadowRoot中(不是必须)。做到完全的样式隔离,不会被外界的样式干扰。同时也不会影响到外部的样式。

在这种情况下,自定义样式就需要一些特殊的方法:

  • CSS 变量:

我们在项目中使用时,可以看到样式:

image.png

其中每一个CSS属性,都有一个CSS变量指代,所以,我们可以用style或者class的方式:

比如默认情况是蓝色:

image.png

  • style的方式:

react中,style必须是一个对象,所以:

<r-loading class="loading" style={{ '--loading-rotate-background': 'red' }} name="rotate"></r-loading>

image.png

但在原生环境中,style是一个字符串,所以:

<r-loading class="loading" style='--loading-rotate-background:red' name="rotate"></r-loading>
  • class的方式:
// index.scss
.loading {
  -loading-rotate-background: red;
}

index.tsx文件中:

    <r-loading class="loading" name="rotate"></r-loading>

因此,也能看到结果:

image.png

也可以采用另一个方式,我们可以在页面元素上看到,元素有一个part属性:

image.png

所以我们可以在CSS中这样使用:

// index.scss

.loading {
  &::part(rotate) {
    background: red;
    width: 100px;
    height: 100px;
  }
}

index.tsx中:

<r-loading class="loading" name="rotate"></r-loading>

然后就会发现,也生效了。

image.png

以上是原生W3C标准提供的CSS scoped方案,适合所有遵循W3C标准的框架。用JavaScript写的框架都可以这么用。

2.在typescript项目中如何使用呢(类型问题)

我们在typescript项目中,可能会遇到这样的问题:

image.png

类型“JSX.IntrinsicElements”上不存在属性“r-loading”。

这是因为JSX类型上,确实没有定义这些元素,对此,我会对JSX类型进行扩展:

image.png

这里为了简单定义,用any做了hack

image.png

如果是直接导入的方式:

import 'ranui'

会自动导入类型文件,就不会有这个问题。

如果是分路径导入的,则肯定不会导入类型文件了,需要手动导入一下,比如按路径导入dist目录下的index.d.ts:

import 'ranui/dist/index.d.ts

三. 架构设计

为了避免出现 vue2 升级 vue3 或其他框架变更可能引发的组件库适配问题,导致公司积累了许久的组件库需要重新调整,这里采用了原生的 webcompoents 去开发。期望做到一次开发,永久受益。

同时期望能支持vuereactpreact,solidjs等各种框架。毕竟工作一段时间就会发现,大部分公司都是vuereact等框架并存的状态。有的项目是用vue,有的是用react

同时由于 Loading 组件也足够简单,也不需要采用Stencil等开发框架,直接原生JS操作即可,可以最小化文件代码体积。

关于 webcompoents的基础知识,可以参考:

1. 避免 SSR 报错

由于webcompoents开发需要依赖HTMLElement,在SSR的情况下会直接抛出Error,因此需要额外的处理下:

export const HTMLElementSSR = (): { new (): HTMLElement; prototype: HTMLElement } | null => {
  if (typeof document !== 'undefined') {
    return HTMLElement;
  }
  return null;
};

在将Loading去依赖这个函数,即可

export class Loading extends (HTMLElementSSR()!) {
  contain: HTMLDivElement;
  static get observedAttributes(): string[] {
    return ['name'];
  }
  constructor() {
    super();
    this.contain = document.createElement('div');
    this.contain.setAttribute('class', 'ran-loading');
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.appendChild(this.contain);
  }
  connectedCallback(): void {
  
  }
  disconnectCallback(): void {
  
  }
  attributeChangedCallback(k: string, o: string, n: string): void {
  
  }
 }

2.设计 Loading 组件的属性

(1). name 属性

首先,Loading组件肯定需要一个类型属性,去区别不同的Loading。我先将这个属性设置为name:

根据不同name属性的值,去生成不同的Loading组件,方便使用和扩展:

首先定义name属性的枚举:

export enum NAME_AMP {
  DOUBLE_BOUNCE = 'double-bounce',
  ROTATE = 'rotate',
  STRETCH = 'stretch',
  CUBE = 'cube',
  DOT = 'dot',
  TRIPLE_BOUNCE = 'triple-bounce',
  SCALE_OUT = 'scale-out',
  CIRCLE = 'circle',
  CIRCLE_LINE = 'circle-line',
  SQUARE = 'square',
  PULSE = 'pulse',
  SOLAR = 'solar',
  CUBE_FOLD = 'cube-fold',
  CIRCLE_FOLD = 'circle-fold',
  CUBE_GRID = 'cube-grid',
  CIRCLE_TURN = 'circle-turn',
  CIRCLE_ROTATE = 'circle-rotate',
  CIRCLE_SPIN = 'circle-spin',
  DOT_BAR = 'dot-bar',
  DOT_CIRCLE = 'dot-circle',
  LINE = 'line',
  DOT_PULSE = 'dot-pulse',
  LINE_SCALE = 'line-scale',
  TEXT = 'text',
  CUBE_DIM = 'cube-dim',
  DOT_LINE = 'dot-line',
  ARC = 'arc',
  DROP = 'drop',
  PACMAN = 'pacman',
}

不同的枚举,对应不同的Loading:

  createLoading = (): void => {
    this.contain.innerHTML = '';
    const NAME_MAP: Record<NAME_AMP, () => void> = {
      [NAME_AMP.STRETCH]: this.stretchLoading,
      [NAME_AMP.ROTATE]: this.rotateLoading,
      [NAME_AMP.DOUBLE_BOUNCE]: this.doubleBounceLoading,
      [NAME_AMP.CUBE]: this.cubeLoading,
      [NAME_AMP.DOT]: this.dotLoading,
      [NAME_AMP.TRIPLE_BOUNCE]: this.tripleBounceLoading,
      [NAME_AMP.SCALE_OUT]: this.scaleOutLoading,
      [NAME_AMP.CIRCLE]: this.circleLoading,
      [NAME_AMP.CIRCLE_LINE]: this.circleLineLoading,
      [NAME_AMP.SQUARE]: this.squareLoading,
      [NAME_AMP.PULSE]: this.pulseLoading,
      [NAME_AMP.SOLAR]: this.solarLoading,
      [NAME_AMP.CUBE_FOLD]: this.cubeFoldLoading,
      [NAME_AMP.CIRCLE_FOLD]: this.circleFoldLoading,
      [NAME_AMP.CUBE_GRID]: this.cubeGridLoading,
      [NAME_AMP.CIRCLE_TURN]: this.circleTurnLoading,
      [NAME_AMP.CIRCLE_ROTATE]: this.circleRotateLoading,
      [NAME_AMP.CIRCLE_SPIN]: this.circleSpinLoading,
      [NAME_AMP.DOT_BAR]: this.dotBarLoading,
      [NAME_AMP.DOT_CIRCLE]: this.dotCircleLoading,
      [NAME_AMP.LINE]: this.lineLoading,
      [NAME_AMP.DOT_PULSE]: this.dotPulseLoading,
      [NAME_AMP.LINE_SCALE]: this.lineScaleLoading,
      [NAME_AMP.TEXT]: this.textLoading,
      [NAME_AMP.CUBE_DIM]: this.cubeDimLoading,
      [NAME_AMP.DOT_LINE]: this.dotLineLoading,
      [NAME_AMP.ARC]: this.arcLoading,
      [NAME_AMP.DROP]: this.dropLoading,
      [NAME_AMP.PACMAN]: this.pacmanLoading,
    };
    const handler = NAME_MAP[this.name];
    handler && handler();
  };

在自定义元素被插入到DOM(文档对象模型)文档树时,执行createLoading函数,生成不同的Loading

  connectedCallback(): void {
    this.createLoading();
  }

同时,还需要处理,在name属性改变时,需要移除当前的Loading组件,添加当前name属性对应的Loading:

 attributeChangedCallback(k: string, o: string, n: string): void {
    if (o !== n) {
      if (k === 'name') {
        this.createLoading();
      }
    }
  }

3.设计导入的方式

这里设计了两种导入方式:

import 'ranui';

或者

import 'ranui/loading';

导入方式上,采用了分路径导入的方式。通过vite进行多入口构建。确保每个路径只导入当前的组件。

按路径导入有什么好处呢?

由于import语法是在静态分析阶段进行导入的,因此,分路径导入能显著减少导入的文件体积。

就算文件中大量的import虽然没有用到,但仍然会实际加载和导入。这点跟require不同,require才是运行时导入。

至于tree shaking,这是在build阶段,构建工具进行依赖分析,拆分出没有用到的部分,不进行打包构建。

但对于某些全局的立即执行函数tree shaking并不能直接分析出会不会用到。比如:

const setTitle = () => {
    document.title = '这是标题'
}
setTitle()

这种情况,没有任何导出,也就不会有任何依赖关系链,但实际上,仍然有作用。这种类似的情况,tree shaking难以发挥作用。但这是正常的,如果这种也被删除了,项目就有问题了。

所以,提供分路径导入的方式,是最好的减少体积的方式。

import 'ranui';这样导入也是没有问题的,没有使用到的部分,在项目最终构建打包时,会通过tree shaking自动帮我们删除没有用的部分。

最后:

其他推荐