Skip to content

CDTRSFE/vite-tpl

Repository files navigation

Vite 项目模版

特性

使用

npx degit CDTRSFE/vite-tpl my-project
cd my-project
git init
pnpm i
pnpm dev

目录

.
├── .husky
│   ├── commit-msg                   # commit message 格式检测
│   └── pre-commit                   # git 钩子,commit 之前执行 pnpm lint, pnpm styelint
├── .vscode
├── public                           # 静态资源文件夹
│   └── favicon.ico
├── src
│   ├── App.vue                      # 根组件
│   ├── assets
│   │   ├── fonts                    # 字体文件夹
│   │   ├── icons                    # 图标文件夹
│   │   ├── images                   # 图片文件夹
│   │   └── styles                   # 样式文件夹
│   │       └── main.less            # 全局样式
│   ├── components                   # 全局组件文件夹
│   ├── directives                   # 全局指令文件夹
│   │   └── Focus.js
│   │   └── index.ts                 # 注册指令
│   ├── main.ts                      # 入口文件
│   ├── plugins
│   │   ├── axios.ts                 # axios
│   │   └── loading.ts
│   ├── router
│   │   └── index.ts                 # 路由
│   ├── store                        # 状态管理
│   ├── types
│   │   ├── auto-imports.d.ts        # 自动引入 API 的类型声明
│   │   ├── components.d.ts          # 自动注册组件的类型声明
│   │   ├── global.d.ts              # 全局类型声明
│   │   └── shims.d.ts               # 模块类型声明
│   └── views
│       └── Index.vue
├── .eslintignore
├── .eslintrc.js                     # eslint 配置
├── .eslintrc-auto-import.json       # 自动引入的 API 全局变量配置
├── .gitignore
├── index.html
├── README.md
├── commitlint.config.js             # commitlint 配置
├── stylelint.config.js              # stylelint 配置
├── package.json
├── pnpm-lock.yaml
├── tsconfig.json                    # ts 配置
└── vite.config.ts                   # vite 配置

🚀 axios

项目中引入了 axios,拦截器等相关配置在 src/plugins/axios.ts 中,axios 实例可作为全局变量直接访问,也可通过 Vue 组件的全局属性访问。

<script lang="ts">
export default defineComponent({
    created() {
        this.$axios('/xxx');
    },
});
</script>
<script lang="ts" setup>
window.axios.get('/xxx');
axios.get('/xxx');
</script>

通常情况下,发请求需要显示 loading 动画,所有请求都响应后关闭动画,可在 src/plugins/loading.ts 中修改具体的 loading 逻辑。对于不需要 loading 动画的请求可以在配置中添加 loading 属性:

axios.get('/xxx', { loading: false });

🚀 PNPM

PNPM 是一个快速的,节省磁盘空间的包管理工具。比 npm 安装速度更快,空间占用更少。

1⃣️ 减少磁盘空间占用

所有安装的包都会存储在磁盘上的某一位置,包里的文件会硬链接到这一位置,而不会占用额外的磁盘空间,允许跨项目共享同一版本的依赖。这也提高了安装速度;

如果用到了某依赖项的不同版本,那么只会将有差异的文件添加到仓库。 pnpm update 时只会向存储中心额外添加新文件,而不会复制整新版本包的内容。

2⃣️ 创建非扁平化的 node_modules 文件夹

第二个特点是,使用 pnpm 项目中 node_modules 目录结构不再是扁平化,这种布局的一大好处是只有真正在依赖项中的包才能访问。

💻 CLI 命令

与 npm 命令等效列表:

npm 命令 pnpm 等效
npm install pnpm install
npm i <pkg> [pnpm add <pkg>]
npm run <cmd> [pnpm <cmd>]
npm uninstall <pkg> pnpm remove <pkg> 别名:rm / uninstall / un

当使用一个未知命令时,pnpm 会查找一个具有指定名称的脚本,所以 pnpm run lint 和 pnpm lint相同。

如果没有指定名称的脚本,那么 pnpm 将以 shell 脚本的形式执行该命令,所以可以做类似 pnpm eslint 的事情(查阅 pnpm exec)。在 npm 中需要使用 npx 实现 , npx eslintpnpm eslint

pnpm 默认使用 npm - https://registry.npmjs.org/ 的源,可以设置其他源:

$ nrm use taobao

避免安装时用错包管理器,在 package.json 中添加了 preinstall 脚本,限制只允许使用 pnpm

{
    "scripts": {
        "preinstall": "npx only-allow pnpm"
    }
}

🚀 VueUse

VueUse 是一个基于 Composition API 的工具函数集,同时支持 Vue2 / Vue 3。

VueUse 使用 vue-demi 实现对 Vue2 的支持,vue-demi 内部会对 Vue 版本做判断,如果是 Vue2 则使用 @vue/composition-api ,判断的过程在安装 vue-demi 之前,可以在 package.json 中找到 scripts.postinstall: ‘node ./scripts/postinstall.js’ ,虽然包的入口文件都是 lib/index.cjs, 它会根据项目中安装的 Vue 版本来修改入口文件的内容。

🧐 使用

工具库划分了几个大类:broswer(浏览器相关),Sensors(传感器相关),Animation(动画相关),部分工具可以按 Component 使用,常用的有:

1️⃣  浏览器分类(Browser)

⭐  useClipboard https://vueuse.org/core/useClipboard/

使用 Clipboard API,提供响应剪切板命令(剪切/复制/粘贴)的功能,可以异步地读写系统剪切板。

demo
<template>
    <div class="app">
        <input v-model="val" type="text">
        <button @click="copy(val)">copy</button>
    </div>
</template>

<script setup>
import { ref } from 'vue';
import { useClipboard } from '@vueuse/core';

const val = ref('');
const { copy } = useClipboard({ source: val })
</script>


⭐  useEventListener https://vueuse.org/core/useEventListener/

更容易地使用事件监听器,自动地在 mounted 时注册事件,在 unmounted 时移除事件监听。

demo
import { useEventListener } from '@vueuse/core'

const element = ref<HTMLDivElement>()
useEventListener(element, 'keydown', (e) => { console.log(e.key) })


⭐  useMediaControls https://vueuse.org/core/useMediaControls/

用于 audio 和 video 标签的响应式媒体控制。

demo
import { useMediaControls } from '@vueuse/core'

const video = ref()
const { playing, currentTime, duration, volume } = useMediaControls(video, { 
    src: 'video.mp4',
})

// playing, currentTime, duration, volume 的值都是响应式的
// 改变媒体属性
onMounted(() => {
    volume.value = 0.5
    currentTime.value = 60
    playing.value = true
})


⭐  useScriptTag https://vueuse.org/core/useScriptTag/

script 标签注入。当组件 mounted 时自动加载 script,组件卸载时自动移除。也可通过配置手动控制 script 加载时机。

demo
import { useScriptTag } from '@vueuse/core'

useScriptTag(
    'https://player.twitch.tv/js/embed/v1.js',
    // on script tag loaded.
    (el: HTMLScriptElement) => {
      // do something
    },
)
import { useScriptTag } from '@vueuse/core'

const { scriptTag, load, unload } = useScriptTag(
    'https://player.twitch.tv/js/embed/v1.js',
    () => {
      // do something
    },
    { manual: true },
)

// manual controls
await load()
await unload()


⭐  useUrlSearchParams https://vueuse.org/core/useurlsearchparams

响应式的 URLSearchParams,可以读取或修改 url 参数,支持 history / hash 模式的路由。

demo
import { useUrlSearchParams } from '@vueuse/core'

const params = useUrlSearchParams('history')
// 获取
console.log(params.foo) // 'bar'
// 设置
params.foo = 'bar'
params.vueuse = 'awesome'
// url updated to `?foo=bar&vueuse=awesome`


2️⃣  传感器分类(Sesors)

⭐  onClickOutside https://vueuse.org/core/onClickOutside/

监听元素外部的点击事件,对弹框和下拉框很有用。

demo
<template>
    <div ref="target">Hello world</div>
</template>

<script setup>
import { onClickOutside } from '@vueuse/core'
    
const target = ref(null)
onClickOutside(target, (event) => console.log(event))
</script>

或者使用无无渲染组件 OnClickOutside :

<OnClickOutside @trigger="fn">
    <div>
      Click Outside of Me
    </div>
</OnClickOutside>


⭐  useMouse https://vueuse.org/core/useMouse/

响应式鼠标位置。

demo
import { useMouse } from '@vueuse/core'

const { x, y, sourceType } = useMouse()
// sourceType: "mouse" | "touch" | null


⭐  useScroll https://vueuse.org/core/useScroll/

响应式的滚动位置和状态,返回滚动距离、到达状态、滚动方向、是否在滚动中,可以配置偏移量、截流、滚动回调函数等。

demo
<script setup>
import { useScroll } from '@vueuse/core'

const el = ref(null)
const offset = { top: 30, bottom: 30, right: 30, left: 30 };
const onScroll = e => console.log(e)
const { x, y, isScrolling, arrivedState, directions } = useScroll(el, { offset, onScroll })
</script>

<template>
  <div ref="el"></div>
</template>


3️⃣ 动画分类(Animation)

⭐  useIntervalFn https://vueuse.org/shared/useIntervalFn/

带控制器的 setInterval 包装器,可以调用暂停、继续方法。

demo
import { useIntervalFn } from '@vueuse/core'

const { pause, resume, isActive } = useIntervalFn(() => {
    /* your function */
}, 1000)


⭐  useRafFn https://vueuse.org/core/useRafFn/

带有暂停/继续控制器的 requestAnimationFrame。

demo
import { ref } from 'vue'
import { useRafFn } from '@vueuse/core'

const count = ref(0)

const { pause, resume } = useRafFn(() => {
    count.value++
    console.log(count.value)
})


⭐  useTimeoutFn https://vueuse.org/shared/useTimeoutFn/

带控制器的 setTimeout 包装器,可以调用 stop 手动停止、调用 start 重新开始。

demo
import { useTimeoutFn } from '@vueuse/core'

const { isPending, start, stop } = useTimeoutFn(() => {
    /* ... */
}, 3000)


⭐  useTransition https://vueuse.org/core/useTransition/

值之间的过渡。返回一个被监听的数值,当源数值改变,输出会过度到新值。如果源值在过渡的过程中被改变,一个新的过渡会从中断的地方开始。

demo
import { ref } from 'vue';
import { useTransition, TransitionPresets } from '@vueuse/core'

const source = ref(0)
const output = useTransition(source, {
    duration: 500, // 默认 1000
    transition: TransitionPresets.easeInOutCubic // 默认 linear 线性过渡
})


4️⃣  状态分类(State)

⭐  useStorage https://vueuse.org/core/useStorage/

响应式的 LocalStorage/SessionStorage

demo
import { useStorage } from '@vueuse/core'

// bind object
const state = useStorage('my-store', { hello: 'hi', greeting: 'Hello' })
// bind boolean
const flag = useStorage('my-flag', true) // returns Ref<boolean>
// bind string with SessionStorage
const id = useStorage('my-id', 'some-string-id', sessionStorage) // returns Ref<string>
// delete data from storage
state.value = null


⭐  createGlobalState https://vueuse.org/shared/createGlobalState/

将状态保留在全局范围内,以便在 Vue 实例之间可重用。相当于响应式的全局变量。

demo
// store.js
import { createGlobalState, useStorage } from '@vueuse/core'

export const useGlobalState = createGlobalState(
    () => useStorage('vueuse-local-storage', 'initialValue'),
)
// component.js
import { useGlobalState } from './store'

// 每次调用 useGlobalState 都将返回同一个 state
const state = useGlobalState()


5️⃣  元素分类(Elements)

⭐  useElementBounding https://vueuse.org/core/useElementBounding/

使用了 getBoundingClientRect API 获取一个元素的大小及其相对于视口的位置,页面滚动或者元素改变时会更新返回值。

demo
import { useElementBounding } from '@vueuse/core'

const { x, y, top, right, bottom, left, width, height } = useElementBounding(el)


⭐  useMouseInElement https://vueuse.org/core/useMouseInElement/

鼠标相对于一个元素的位置。

demo
import { useMouseInElement } from '@vueuse/core'

// 鼠标相对于 target 左上角的坐标、鼠标是否在 target 外
const { x, y, isOutside, stop } = useMouseInElement(target)
// 停止监听 target 和鼠标的位置
stop()


⭐  useMutationObserver https://vueuse.org/core/useMutationObserver/

监听 DOM 树的改变。MutationObserver MDN

demo
import { useMutationObserver } from '@vueuse/core'

const el = ref(null)
const messages = ref([])

const cb = mutations => {
    if (!mutations[0]) messages.value.push(mutations[0].attributeName)
}
const { stop } = useMutationObserver(el, cb, { attributes: true })
// 停止监听
stop()


⭐  useWindowScroll https://vueuse.org/core/useWindowScroll/

响应式窗口滚动。

demo
import { useWindowScroll } from '@vueuse/core'

const { x, y } = useWindowScroll()


⭐  useWindowSize https://vueuse.org/core/useWindowSize/

响应式窗口大小。

demo
import { useWindowSize } from '@vueuse/core'

const { width, height } = useWindowScroll()


6️⃣  组件分类(Componet)

⭐  useTemplateRefsList https://vueuse.org/core/useTemplateRefsList/

在 v-for 中绑定 refs 到模版和组件的一个快捷方式,比原本的写法 v-for 中的 Ref 数组 更简洁。

demo
<template>
    <div v-for="i of 5" :key="i" :ref="refs.set"></div>
</template>

<script setup lang="ts">
import { onUpdated } from 'vue'
import { useTemplateRefsList } from '@vueuse/core'

const refs = useTemplateRefsList<HTMLDivElement>()

onUpdated(() => console.log(refs))
</script>


⭐  useVirtualList https://vueuse.org/core/useVirtualList/

轻松创建虚拟列表。虚拟列表可以高效地渲染大量项目。

demo
<template>
    <div v-bind="containerProps" style="height: 300px">
        <div v-bind="wrapperProps">
            <div v-for="item in list" :key="item.index" style="height: 22px">
                Row: {{ item.data }}
            </div>
        </div>
    </div>
</template>

<script setup>
import { ref } from 'vue';
import { useVirtualList } from '@vueuse/core';

const allItems = ref(Array.from(Array(1000).keys()));
const { list, containerProps, wrapperProps } = useVirtualList(allItems, {
    itemHeight: 22,
});
</script>


⭐  useVModel https://vueuse.org/core/useVModel/

v-model 绑定的快捷方式,props + emit -> ref

demo
import { useVModel } from '@vueuse/core'

export default {
    setup(props, { emit }) {
        const data = useVModel(props, 'data', emit)

        console.log(data.value) // props.data
        data.value = 'foo' // emit('update:data', 'foo')
    },
}


7⃣️ 工具分类(Utilities):

⭐  reactivePick https://vueuse.org/shared/reactivePick/

从一个响应式对象中选择字段。

demo
import { reactivePick } from '@vueuse/core'

const obj = reactive({
    x: 0,
    y: 0,
    elementX: 0,
    elementY: 0,
})

const picked = reactivePick(obj, 'x', 'elementX') // { x: number, elementX: number }


⭐  useBase64 https://vueuse.org/core/useBase64/

响应式的 base64 转换,支持纯文本, buffer, 文件, canvas 和 图片。

demo
import { ref, Ref } from 'vue'
import { useBase64 } from '@vueuse/core'

const text = ref('')

const { base64 } = useBase64(text)


⭐  useCycleList https://vueuse.org/core/useCycleList/

环形查看一个项目列表。

demo
import { useCycleList } from '@vueuse/core'

const { state, next, prev } = useCycleList([
    'Dog',
    'Cat',
    'Lizard',
    'Shark',
    'Whale',
    'Dolphin',
    'Octopus',
    'Seal',
])

console.log(state.value) // 'Dog'
prev()
console.log(state.value) // 'Seal'


⭐  useToggle https://vueuse.org/shared/useToggle/

带有工具函数的布尔值切换器。

demo
import { useToggle } from '@vueuse/core'
const [value, toggle] = useToggle()


8⃣️ 配置 https://vueuse.org/guide/config.html#configurations

⭐  事件过滤器 https://vueuse.org/guide/config.html#event-filters

可以更灵活地控制事件触发,提供了防抖截流的功能,也可以让事件暂停/继续触发。

demo
import { throttleFilter, debounceFilter, useLocalStorage, useMouse } from '@vueuse/core'
// 防抖
const storage = useLocalStorage('my-key', { foo: 'bar' }, { eventFilter: throttleFilter(1000) })
// 截流
const { x, y } = useMouse({ eventFilter: debounceFilter(100) })
import { pausableFilter, useDeviceMotion } from '@vueuse/core'
const motionControl = pausableFilter()
const motion = useDeviceMotion({ eventFilter: motionControl.eventFilter })

// 暂停
motionControl.pause() 
// 继续
motionControl.resume()


🚀 组件自动化加载

使用 unplugin-vue-components 自动按需引入组件,也无需注册,使用全局组件和 UI 组件库时更加方便。配置后,项目中放在 'src/components' 目录下的组件可在全局直接使用。

⚙️ 配置

// vite.config.ts

import { defineConfig } from 'vite';
import Components from 'unplugin-vue-components/vite';
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers';

export default defineConfig({
    plugins: [
        Components({
            dirs: ['src/components'],
            extensions: ['vue', 'js', 'ts'],
            include: [/\.vue$/, /\.vue\?vue/],
            dts: 'src/types/components.d.ts',
            resolvers: [
                AntDesignVueResolver(),
            ]
        }),
    ],
})
  • dirs 指定查找组件的相对路径,此目录下的组件并非全局注册。
  • dts: 'src/types/components.d.ts' 表示生成全局类型声明文件,用于 volar 类型提示。
  • AntDesignVueResolver 用于解析 ant-design-vue 组件引入。

🧐 使用

./src
├── components
│   ├── FullLoading.vue
│   └── TabSelect.vue
<template>
    <full-loading></full-loading>
    <a-button type="primary">btn<a/-button>
</template>

通过插件处理后,相当于:

<template>
    <full-loading></full-loading>
    <a-button type="primary">btn</a-button>
</template>

<script setup lang="ts">
import FullLoading from '@/components/FullLoading.vue';
import ElButton from 'ant-design-vue/es/button';
import 'ant-design-vue/es/button/style/css';
</script>

🚀 API 自动引入

通过 unplugin-auto-import 插件自动按需引入所需 API,ref, watch, useRouter 等 API 无需引入可直接使用。

<template>
    <div>{{ name }}</div>
</template>

<script setup lang="ts">
const name = ref('name');
</script>

⚙️ 配置

// vite.config.ts

import { defineConfig } from 'vite';
import AutoImport from 'unplugin-auto-import/vite';

export default defineConfig({
    plugins: [
        AutoImport({
            include: [/\.[tj]sx?$/, /\.vue$/, /\.vue\?vue/],
            imports: [
                'vue',
                'vue-router',
                '@vueuse/core',
            ],
            dts: 'src/types/auto-imports.d.ts',
            eslintrc: {
                enabled: true, // Default `false`
                filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
                globalsPropValue: true, // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
            },
        }),
    ],
});
  • include 指定需要转换的目标文件,自动引入 API 可以在 js, ts, jsx, tsx, vue 文件中使用。
  • imports 添加了 vue, vue-router, @vueuse/core 三个包,使用时无需导入。
  • dts: 'src/types/auto-imports.d.ts' 用于生成类型声明文件。
  • eslintrc 启用此项配置为了解决 ESLint 提示 eslint(no-undef) 的问题,会生成一个 ./.eslintrc-auto-import.json 文件,将自动引入的 API 作为全局变量处理,需要在 ESLint 配置文件中作为扩展添加:
// .eslintrc.js

module.exports = { 
     extends: [
        // ...
        './.eslintrc-auto-import.json',
    ],
}

🚀 Pinia

Pinia 是 Vue 的一个状态管理库,由 Vue 团队成员创建,会成为 Vuex 的继承者,Vue 文档中也推荐使用 Pinia。与 Vuex 相比,Pinia 提供了一个更简单的 API,可以使用 composition-API 风格,在与 Typescript 一起使用时具有可靠的类型推断支持。

和 Vuex ≤ 4 相比,有许多不同:

  • 不再有 mutations,不需要再区分同步异步去使用不同的方法;
  • 不再需要通过复杂的包装器来支持 Typscript;
  • 不再有模块的概念,但同时支持在 store 中使用其他 store;

定义 store

store 使用 defineStore() 定义,有三种传参格式:

defineStore(id, options);
defineStore(options);
defineStore(id, storeSetup, options?);

其中 id 可以通过第一个参数传递,如果不存在,则通过 options.id 获取,作为唯一的名称。传参最主要的区别是 optionsstoreSetupoptions 是一个对象,包含 state, getters, actions,和 Vuex 类似:

import { defineStore } from 'pinia';

export const useCountStore = defineStore('counter', {
    state: () => ({
        counter: 0,
    }),
    getters: {
        doubleCount: (state) => state.counter * 2,
    },
    actions: {
        increment() {
            this.counter++;
        },
    },
})

第三种传参方式,storeSetup 参数是一个函数,和 Vue setup 方法类似,这也是更推荐的写法:

import { defineStore } from 'pinia';

export const useCountStore = defineStore('counter', () => {
    const counter = ref(0);
    const doubleCount = computed(() => counter.value * 2);
    const increment = () => counter.value++;

    return { counter, doubleCount, increment };
})

可以根据需要定义任意多个 store,并且应该放在不同的文件中,如:

src
└── store
    ├── user.ts
    ├── counter.ts
    └── base.ts

使用 store

<template>
    <div>counter: {{ count.counter }}</div>
    <div>double count: {{ count.doubleCount }}</div>
    <button @click="count.increment">increment</button>
</template>

<script lang="ts" setup>
import { useCountStore } from '@/store/counter';

const count = useCountStore();
watchEffect(() => {
    console.log(count.counter);
});
</script>

store 是一个用 reactive 包装的对象,可以使用 storeToRefs 实现不丢失响应性的情况下对返回的对象进行解构/展开:

import { useCountStore } from '@/store/counter';
import { storeToRefs } from 'pinia';

const store = useCountStore();
const { counter } = storeToRefs(store); // store.counter

🚀 图标

通过 vite-plugin-import-icons vite 插件实现图标组件化,可以将本地图标包装成组件,还有自动引入按需加载的功能。

⚙️ 配置

// vite.config.ts
import path from 'path';
import ImportIcons, { ImportIconsResolver } from 'vite-plugin-import-icons';

export default defineConfig({
    plugins: [
        Components({
            resolvers: [
                ImportIconsResolver(),
            ],
        }),
        ImportIcons({
            collections: {
              	icons: path.resolve(__dirname, './src/assets/icons'),
            },
        }),
    ],
});

配置说明:

  • ImportIconsResolver 用于将组件化后的图标组件自动引入,可以直接在 template 中使用。
  • collections 用于加载图标,将 './src/assets/icons' 目录下所有 svg 作为一个图标集。

项目中使用了 Typescript,还需要添加类型声明文件 tsconfig.json:

{
    "compilerOptions": {
        "types": [
            "vite-plugin-import-icons/types",
        ]
    }
}

🧐 使用

只需要把 svg 文件放到 ‘./src/assets/icons’ 目录即可,图标集为 ‘icons’,图标名为文件名,推荐使用小写字母,多个单词用短横线链接(kebab-case):

src
└─ asstes
    └─ icons
        └─ about.svg
<template>
    <icons-about></icons-about>
</template>

图标自动加载和组件自动加载一样,会生成对应的模块声明:

// src/types/components.d.ts

declare module 'vue' {
    export interface GlobalComponents {
        IconsHome: typeof import('~icons/icons/home')['default']
    }
}

如果不使用组件自动加载功能,则需要先 import :

<template>
    <icons-about></icons-about>
</template>
<script setup lang="ts">
import IconsAbout from '~icons/icons/about’;
</script>

引入图标的路径 ~icons/* 是一个虚拟路径,由插件处理后,找到真实的 svg 文件,然后包装成 vue 组件返回。

批量引入

vite-plugin-import-icons 插件提供了一个 import.meta.icons 函数,用于一次性引入多个图标。

<template>
    <component v-for="(icon, name) in icons" :is="icon" :key="name"></component>
</template>
<script>
const icons = import.meta.icons('icons', 'menu-*')
</script>

和 iconfont 字体图标比较

在 iconfont 上把所有需要的图标加入项目,再以 font-class 的方式使用是一个比较常见的做法,不过有以下问题:

  • 首先需要先在平台上添加图标到项目,然后下载图标,最后再替换本地的文件,过程更繁琐;
  • 文件包含所有图标,无法按需加载;
  • 平台账号丢失,只能新建一个图标项目;
  • 多色图标需要换用 svg 或者图片,造成项目中图标使用风格不统一;

🚀 UnoCSS

UnoCSS 是高性能且极具灵活性的即时原子化 CSS 引擎。

组件化 / 原子化

使用 CSS 最原始的方式是:先给标签设置一个 class,再逐条写需要的样式,一些使用率高的样式会重复写很多次,开发效率较低,而且打包生成的 CSS 文件冗余。解决的方法可以使用 CSS 组件化和原子化。

CSS 组件化:

将相同视觉的 UI 封装成公共 class,在需要的标签上直接使用这个类名,一般一个 class 包含多条 CSS 样式,这类似于直接使用 bootstrap 或 element UI。使用组件化的 CSS,可以快速的完成效果,完全不需要动手写样式,缺点是不够灵活。

CSS 原子化:

原子化是一种 CSS 的架构方式,它倾向于小巧且用途单一的 class,并且会以视觉效果进行命名。有些人可能会称其为函数式 CSS,或者 CSS 实用工具。Windi CSS 就是采用原子化方案的框架。

随着构建工具的发展,工程化也是趋势,CSS 框架也会更细化,更工程化。CSS 原子化和组件化有不同的应用场景,两者结合使用可以最大限度的提升开发效率。

⚙️ 配置

UnoCSS 的配置文件写在了 vite.config.ts 中,作为 vite 插件参数传入:

// vite.config.ts

import Unocss from 'unocss/vite';
import { presetAttributify, presetWind } from 'unocss';
import transformerDirective from '@unocss/transformer-directives';

export default (env: ConfigEnv) => {
    return defineConfig({
        plugins: [
            Unocss({
                presets: [
                    presetWind(),
                    presetAttributify(),
                ],
                transformers: [
                    transformerDirective(),
                ],
            }),
        ],
    });
}

以上配置中,使用了 WindiCSS 的预设,开启了属性模式

最后,在项目入口文件中引入相关 CSS:

// main.ts

// 'uno:[layer-name].css'
import 'uno:components.css';
// layers that are not 'components' and 'utilities' will fallback to here
import 'uno.css';
// your own CSS
import './assets/styles/main.less';
// "utilities" layer will have the highest priority
import 'uno:utilities.css';

特性(WindiCSS)

1⃣️ 自动值推导 https://cn.windicss.org/features/value-auto-infer.html

在类名中使用任意值,然后生成相应的样式,任意值可以是数字(表示 rem)、尺寸(px/vw/em/rem)、分数、颜色(rgba/hex)、变量(CSS变量名)。

<div class="w-1/2 p-5px mt-10px bg-#b2a8bb"></div>

生成的 CSS 为:

.bg-#b2a8bb {
    --tw-bg-opacity: 1;
    background-color: rgba(178, 168, 187, var(--tw-bg-opacity));
}
.mt-10px {
    margin-top: 10px;
}
.p-5px {
    padding: 5px;
}
.w-1\/2 {
    width: 50%;
}

2⃣️ 可变修饰组 https://cn.windicss.org/features/variant-groups.html

通过使用括号对相同的工具类进行编组,将其应用于同一可变修饰

<div class="hover:(bg-red-200 font-bold)"></div>

生成的 CSS 为:

.hover\:bg-red-200:hover {
    --darkreader-bg--tw-bg-opacity: 1;
    background-color: rgb(87, 15, 15);
}
.hover\:font-bold:hover {
    font-weight: 700;
}

3⃣️ Shortcuts https://github.com/unocss/unocss#shortcuts

允许把工具类合集组合在一起,不需要重复写。

4⃣️ Important 前缀 https://cn.windicss.org/features/important-prefix.html

可以在任意工具类的前面使用 ! 前缀,使它们变为 !important

<div class="!text-black"></div>

生成的 CSS 为:

.\!text-black {
    --tw-text-opacity: 1 !important;
    color: rgba(0, 0, 0, var(--tw-text-opacity)) !important;
}

5⃣️ 指令 https://cn.windicss.org/features/directives.html

@apply 指令用于应用工具类。可通过选择器复用一组工具类,和 shourtcuts 类似。

<template>
    <button class="btn">btn1</button>
    <button class="btn">btn2</button>
</template>
<style>
.btn {
    @apply font-bold py-4px px-10px rounded;
}
</style>

生成的 CSS 为:

.btn {
    border-radius: 0.25rem;
    font-weight: 700;
    padding-top: 4px;
    padding-bottom: 4px;
    padding-left: 10px;
    padding-right: 10px;
}

其他指令还有:@variants, @screen, @layer, theme()

6⃣️ 属性模式 https://cn.windicss.org/features/attributify.html

为了避免 class 的值太多,造成代码的可读性降低,推荐使用属性化模式,将工具类分组,语法是:

(variant[:-]{1})*key? = "((variant:)*value)*"
<div class="text-#ddd" hover="text-#eee"></div>
<button
    bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"
    text="sm white"
    font="mono light"
    p="y-2 x-4"
    border="2 rounded blue-200"
>
    Button
</button>
生成的 CSS
/* windicss layer utilities */
[bg~="blue-400"] {
    --tw-bg-opacity: 1;
    background-color: rgba(96, 165, 250, var(--tw-bg-opacity));
}
[bg~="hover:blue-500"]:hover {
    --tw-bg-opacity: 1;
    background-color: rgba(59, 130, 246, var(--tw-bg-opacity));
}
.dark [bg~="dark:blue-500"] {
    --tw-bg-opacity: 1;
    background-color: rgba(59, 130, 246, var(--tw-bg-opacity));
}
.dark [bg~="dark:hover:blue-600"]:hover {
    --tw-bg-opacity: 1;
    background-color: rgba(37, 99, 235, var(--tw-bg-opacity));
}
[border~="blue-200"] {
    --tw-border-opacity: 1;
    border-color: rgba(191, 219, 254, var(--tw-border-opacity));
}
[border~="rounded"] {
    border-radius: 0.25rem;
}
[border~="2"] {
    border-width: 2px;
}
[font~="mono"] {
    font-family: ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
}
[font~="light"] {
    font-weight: 300;
}
[text~="sm"] {
    font-size: 0.875rem;
    line-height: 1.25rem;
}
[p~="y-2"] {
    padding-top: 0.5rem;
    padding-bottom: 0.5rem;
}
[p~="x-4"] {
    padding-left: 1rem;
    padding-right: 1rem;
}
[text~="white"] {
    --tw-text-opacity: 1;
    color: rgba(255, 255, 255, var(--tw-text-opacity));
}

属性可能会名称冲突,直接将有冲突的属性改为 class:

<template>
    <a-component class="bg-blue-200" p="x-10px y-4px" bg="xxx"></a-component>
</template>

以上代码中,bg 为组件的 prop,与设置背景的属性冲突。

工具类

工具类的详细介绍可查看文档 https://windicss.org/utilities/,也可以通过搜索快速找到需要的类。

VS Code 插件

UnoCSS Intellisense 通过提供给 Visual Studio Code 用户一些特性的方式来提高 UnoCSS 的开发体验,例如:自动补全、语法高亮。

🚀 样式

项目中依然采用 Less 作为 CSS 预处理器。

./src/assets/styles/main.less 可用于存放公共样式。

🚀 Stylelint

使用 stylelint-config-tpconfig 的规则,可运行 pnpm lint:stylelint 手动检测 src 目录下的样式文件。建议安装编辑器 stylelint 插件,并开启保存时自动修复。

// stylelint.config.js

module.exports = {
    extends: '@trscd/stylelint-config-tpconfig',
    ignoreFiles: ['./public/**/*'],
};

vscode settings.json:

{
    "editor.codeActionsOnSave": {
        "source.fixAll.stylelint": true
    }
}

🚀 ESLint

使用 @trscd/eslint-config 配置,对 JS,TS,Vue 代码做检测。

// .eslintrc.js

module.exports = {
    extends: [
        '@trscd',
        './.eslintrc-auto-import.json',
    ],
};

其中 .eslintrc-auto-import.json 引入的是一些全局变量的配置,为了解决 unplugin-auto-import ESLint 报错的问题

🚀 版本控制

  • 使用 lint-staged 在提交代码前执行 pnpm lintpnpm stylelint,防止不规范的代码推送到远程仓库。
  • 使用 Commitizen + Commitlint 对 commit message 做格式校验,可以使用 git cz  代替 git commit 生成符合规范的 message ,如 feat(api): xxx

需要先全局安装 commitizen:pnpm add -g commitizen

About

✨ Vite + Vue3 + TS template

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published