Skip to content

封装Loading和AddButton

新增三个组件

loading

tsx
export const Loading: React.FC = () => {
  return <div>加载中……</div>
}

Icon

tsx
export const Icon: React.FC = () => {
  return <div>Icon </div>
}

AddItemFloatButton

tsx
import add from '../assets/icons/add.svg'
export const AddItemFloatButton: React.FC = () => {
  return (
    <button p-4px w-56px h-56px bg="#5C33BE" rounded="50%" b-none text-white
      text-4xl fixed bottom-16px right-16px>
      <img text-red src={add} max-w="100%" max-h="100%" />
    </button>
  )
}

将这三个组件用于Home.tsx页面,详细代码见链接

封装Icon

Icon的处理方法有很多种如下:

  1. 雪碧图
  2. iconfont
  3. svg sprite

其中svg sprite的原理就是通过插件将所有的svg放入一个大的svg内,然后在将这个大的svg放在<body>里,若是要使用则通过如下方式进行使用。

tsx
// react 
<svg>
	<use xlinkHref='pig'></use>
</svg>
// vue
<svg>
	<use xlink:href='pig'></use>
</svg>

引入svgsprites插件

安装svgosvgostore,安装命令pnpm install svgo svgostore

创建vite_plugins文件夹,创建svgsprites.ts文件,一个中间件用于处理svg。

tsx
import path from 'path'
import fs from 'fs'
import store from 'svgstore' // 用于制作 SVG Sprites
import { optimize } from 'svgo' // 用于优化 SVG 文件
import type { Plugin, ViteDevServer } from 'vite'

interface Options {
  id?: string
  inputFolder?: string
  inline?: boolean
}
export const svgsprites = (options: Options = {}): Plugin => {
  const virtualModuleId = `virtual:svgsprites${options.id ? `-${options.id}` : ''}`
  const resolvedVirtualModuleId = `\0${virtualModuleId}`
  const { inputFolder = 'src/assets/icons', inline = false } = options

  const generateCode = () => {
    const sprites = store(options)
    const iconsDir = path.resolve(inputFolder)
    for (const file of fs.readdirSync(iconsDir)) { // 遍历读取的icons的文件夹下的路径
      if (!file.endsWith('.svg')) // 如果文件后缀名不是 .svg 的则跳过
        continue
      const filepath = path.join(iconsDir, file) // 将svg文件的路径拼接成绝对路径
      const svgId = path.parse(file).name // 将路径地址转换为对象取得文件名
      const code = fs.readFileSync(filepath, { encoding: 'utf-8' }) // 读取文件夹的路径并转为编码utf-8
      sprites.add(svgId, code)
    }
    // 优化svg的属性内容,去除无用属性
    const { data: code } = optimize(sprites.toString({ inline }), {
      plugins: [
        'cleanupAttrs', 'removeDoctype', 'removeComments', 'removeTitle', 'removeDesc', 'removeEmptyAttrs',
        { name: 'removeAttrs', params: { attrs: '(data-name|fill)' } },
      ],
    })
    return code
  }
  // 生成或更新svg内容
  const handleFileCreationOrUpdate = (file: string, server: ViteDevServer) => {
    if (!file.includes(inputFolder))
      return
    const code = generateCode()
    server.ws.send('svgsprites:change', { code })
    const mod = server.moduleGraph.getModuleById(resolvedVirtualModuleId)
    if (!mod)
      return
    server.moduleGraph.invalidateModule(mod, undefined, Date.now())
  }

  return {
    name: 'svgsprites',
    configureServer(server) {
      server.watcher.on('add', (file) => {
        handleFileCreationOrUpdate(file, server)
      })
      server.watcher.on('change', (file) => {
        handleFileCreationOrUpdate(file, server)
      })
    },
    resolveId(id: string) {
      if (id === virtualModuleId)
        return resolvedVirtualModuleId
    },
    load(id: string) {
      if (id === resolvedVirtualModuleId) {
        const code = generateCode()
        return `!function(){
  const div = document.createElement('div')
  div.innerHTML = \`${code}\`
  const svg = div.getElementsByTagName('svg')[0]
  const updateSvg = (svg) => {
    if (!svg) { return }
    svg.style.position = 'absolute'
    svg.style.width = 0
    svg.style.height = 0
    svg.style.overflow = 'hidden'
    svg.setAttribute("aria-hidden", "true")
  }
  const insert = () => {
    if (document.body.firstChild) {
      document.body.insertBefore(div, document.body.firstChild)
    } else {
      document.body.appendChild(div)
    }
  }
  updateSvg(svg)
  if (document.body){
    insert()
  } else {
    document.addEventListener('DOMContentLoaded', insert)
  }
  if (import.meta.hot) {
    import.meta.hot.on('svgsprites:change', (data) => {
      const code = data.code
      div.innerHTML = code
      const svg = div.getElementsByTagName('svg')[0]
      updateSvg(svg)
    })
  }
}()`
      }
    },
  }
}

由于是自行导入的插件,需要自行在tsconfig.node.json文件内将vite_plugins归属于配置文件。

json
{
  "compilerOptions": {
    "composite": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "allowSyntheticDefaultImports": true
  },
  "include": ["vite.config.ts", "vite_plugins"]
}

然后在vite.config.ts中导入插件。

typescript
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import Unocss from 'unocss/vite'
import { viteMockServe } from 'vite-plugin-mock'
import { svgsprites } from './vite_plugins/svgsprites'

// https://vitejs.dev/config/
export default defineConfig(({ command }) => ({
  define: {
    isDev: command === 'serve'
  },
  plugins: [
    Unocss(),
    react(),
    viteMockServe(),
    svgsprites()
  ]
}))

然后将其全局导入在main.tsx中。

typescript
import React from 'react'
import ReactDOM from 'react-dom/client'
import { RouterProvider } from 'react-router-dom'
import { router } from './routes/router'
import 'virtual:uno.css'
import './global.scss'
import 'virtual:svgsprites'

const div = document.getElementById('root') as HTMLElement

const root = ReactDOM.createRoot(div)
root.render(
  <React.StrictMode>
    <RouterProvider router={router}/>
  </React.StrictMode>
)

然后在AddItemFloatButton.tsx中使用svg插件,详细代码见链接

typescript
export const AddItemFloatButton: React.FC = () => {
  return (
    <button p-4px w-56px h-56px bg="#5C33BE" rounded="50%" b-none text-white
    fixed bottom-16px right-16px>
    <svg style={{ fill: 'red', width: '1.2em', height: '1.2em' }}>
      <use xlinkHref='#menu' />
    </svg>
    </button>
  )
}

封装Icon组件

typescript
import c from 'classnames'
import s from './Icon.module.scss'
interface Props {
  className?: string
  name: string
}
export const Icon: React.FC<Props> = ({ name, className }) => {
  return (
    <svg className={c(className, s.icon)}>
      <use xlinkHref={`#${name}`}></use>
    </svg>
  )
}

添加Icon的默认样式,使用了新出的伪类,可以将属性优先级降至最低。这个伪类可能过新,会导致有些浏览器会出现问题。

typescript
:where(.icon){
  fill: currentColor;
  width: 1.2em;
  height: 1.2em;
}

添加样式属性配置,详细代码见链接

typescript
import * as React from 'react'
declare module 'react' {
  /** 省略 **/
  interface SVGProps<T> extends SVGAttributes<T>, ClassAttributes<T> {
    w?: string
    h?: string
  }
}

unocsslayers可以用于调整css属性。

即将原有css样式放入layer的层级 ,即css样式放置最前,修改uni.config.ts文件,详细代码见链接

tsx
import {
  defineConfig, presetAttributify, presetIcons,
  presetTypography, presetUno, transformerAttributifyJsx
} from 'unocss'

export default defineConfig({
  theme: {
  },
  shortcuts: {
  },
  safelist: [],
  preflights: [
    {
      layer: 'components',
      getCSS: () => `
        .j-icon{
          fill: currentColor;
          width: 1.2em;
          height: 1.2em;
        }
      `
    },
  ],
  presets: [
    presetUno(),
    presetAttributify(),
    presetIcons({
      extraProperties: { 'display': 'inline-block', 'vertical-align': 'middle' },
    }),
    presetTypography(),
  ],
  transformers: [
    transformerAttributifyJsx()
  ],
})

返璞归真,在main.ts文件中将css样式文件引入修改至引入unocss样式文件之前,详细代码见链接

封装loading组件

在iconfont找到一个loading的的svg,然后使用styled添加动画,使其旋转。

tsx
import styled from 'styled-components'
import c from 'classnames'
import { Icon } from './Icon'

const Div = styled.div`
  @keyframes spin {
    0% {
      transform: rotate(0deg);
    }
    100% {
      transform: rotate(360deg);
    }
  }
  svg {
    animation: spin 1.25s linear infinite;
  }
`

interface Props {
  className?: string
  message?: string
}

export const Loading: React.FC<Props> = ({ className, message }) => {
  return (
    <Div className={c('flex flex-col justify-center items-center', className)}>
      <Icon name="loading" className='w-128px h-128px' />
      <p p-8px text-lg>{message || '加载中……'}</p>
    </Div>
  )
}

然后修改mock文件中/api/v1/me接口的timeout时间,然后在文件中导入,详细代码见链接