1.ESLint配置文件
- 优先级(同一级下):
.eslintrc.js.eslintrc.cjs.eslintrc.yaml.eslintrc.yml.eslintrc.jsonpackage.json
ESLint 会自动要检查文件的目录中寻找它们,并在其直系父目录中寻找,直到文件系统的根目录(/)、当前用户的主目录(~/)或指定 root: true 时停止
eslint rules
{
"root": true,// 是否是根目录
"env": {
"node": true // 环境
},
"extends": [// 扩展
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended"
],
"parserOptions": {
"ecmaVersion": 2020 // ES版本
},
"rules": {// 自定义规则
"eqeqeq": "warn",
"quotes": ["error", "double"]
},
"overrides": [// 覆盖规则
{
"files": ["./src/test/*"],
"rules": {
"quotes": ["error", "single"]
}
}
]
},
2.插件具体是如何实现扩展配置的
通过extends设置的配置包加载的时候,是递归的形式去查找配置文件一步步派生继承的。
例如: extends: ["test"],然后对应的eslint-config-test有plugins: ["children"], ESLint 会先找到 ./node_modules/ 下的eslint-plugin-children, (而不是 ./node_modules/eslint-config-foo/node_modules/),更不会从祖先目录去查找。
"plugin:vue/vue3-essential":有 plugin: 前缀,显然要先去找./node_modules/eslint-plugin-vue3,然后看它导出的配置里的essential属性。在./node_modules/eslint-plugin-vue/lib/index.js 里找到了
"eslint:recommended": 没有eslint-config-eslint插件,说明是插件名/路径,就直接到eslint的npm包下去找了,它封装得比较复杂,没找到在哪里导出配置和configs 关键字,只知道"eslint:recommended" 的配置规则在 ./node_modules/@eslint/js/configs/eslint-recommended.js
"@vue/typescript/recommended":同上没有相关插件,直接到@vue包里找,,找到eslint-config-typescript;再到里面找到recommended.js
3.扩展配置文件
module.exports = {
// ...
"rules": {
"eqeqeq": "warn",
}
};
要改变规则的设置,你必须把规则 ID 设置为这些值之一:
-
"off"或0- 关闭规则 -
"warn"或1- 启用并视作警告(不影响退出)。 -
"error"或2- 启用并视作错误(触发时退出代码为 1) -
改变一个继承的规则的严重程度,而不改变其选项。
- 基本配置:
"eqeqeq": ["error", "allow-null"] - 派生配置:
"eqeqeq": "warn" - 产生的实际配置:
"eqeqeq": ["warn", "allow-null"]
- 基本配置:
-
覆盖基础配置中的规则选项:
- 基本配置:
"quotes": ["error", "single", "avoid-escape"] - 派生配置:
"quotes": ["error", "single"] - 产生的实际配置:
"quotes": ["error", "single"]
- 基本配置:
4.覆盖配置文件
{
"rules": {
"eqeqeq": "warn",
"quotes": ["error", "double"]
},
"overrides": [
{
"files": ["./src/test/*"],
"rules": {
"quotes": ["error", "single"]
}
}
]
}
VitePress
1.markdown基础语法

]()
 // “半角空格”
 // “全角空格”
2.VitePress基础配置
-
Markdown 扩展
-
首页配置
---
layout: home
hero:
name: Docs Name #文档标题
text: Vite & Vue powered static site generator. #文档副标题
tagline: Lorem ipsum... #文档标语
image:
src: /logo.png
alt: 首页的logo图标
# type ThemeableImage =
# | string
# | { src: string; alt?: string }
# | { light: string; dark: string; alt?: string }
actions:
- theme: brand #按钮的主题
text: 开始使用 #按钮的文字
link: /guide/start #按钮的链接
- theme: alt
text: 在 GitHub 上查看
link: https://github.com/ox4f5da2
features:
- icon: 🛠️ #盒子的图标
title: Simple and minimal, always #盒子的标题
details: Lorem ipsum... #详细描述
---
- 主题配置
import { DefaultTheme, defineConfig } from 'vitepress'
const nav: DefaultTheme.NavItem[] = [
{ text: '指南', link: '/guide/' },
{ text: '源码', link: 'https://vitepress.vuejs.org/guide/theme-home-page' },
]
const sidebar: DefaultTheme.Sidebar = {
'/guide': [
{
text: '指南',
items: [
{ text: '组件库介绍', link: '/guide/' },
{ text: '快速开始', link: '/guide/start' },
]
}
],
}
module.exports = {
title: 'Test VitePress',
description: 'Just playing around.',
lang: 'cn-ZH',
base: '/',
lastUpdated: true,
markdown: {
lineNumbers: true // md文件中的代码启用行号
},
config: (md) => {
// 使用markdown-it插件
md.use(require('markdown-it-xxx'))
},
themeConfig: {
logo: '/logo.png',// 图标
siteTitle: '组件库标题',// 标题
outline: 10,
socialLinks: [
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' }
],
nav,
sidebar
}
}
<pre>{{ test }}</pre>
<script setup>
const test = 'just for test'
</script>
vue组件封装及测试
<template>
<div>
<van-form>
<van-cell-group inset>
<van-field
v-for="item in props.schema"
v-bind="item.fieldProps"
:key="item.field"
v-model="formData[item.fieldProps.field]"
>
<template #input v-if="item.compotent">
<component
:is="item.compotent"
v-bind="item.compotentProps"
v-model="formData[item.fieldProps.field]"
>
</component>
</template>
</van-field>
</van-cell-group>
</van-form>
</div>
</template>
<script lang="ts" setup>
import { nextTick, onMounted, PropType, ref } from 'vue'
// eslint-disable-next-line no-undef
const props = defineProps({
schema: {
type: Array as PropType<Record<string, any>[]>,
default: () => [],
},
})
const formData = ref<Record<string, any>>({})
const setFormData = (values: any) => {
nextTick(() => Object.assign(formData.value, values))
console.log(formData)
}
onMounted(() => {
props.schema.forEach((item) => {
formData.value[item.fieldProps.field] = item.compotentProps?.value
})
})
// eslint-disable-next-line no-undef
defineExpose({
setFormData,
})
</script>
<style lang="less" scoped></style>
<template>
<div>
<van-popup :show="props.showPicker" position="bottom">
<van-picker
:columns="(props.currentOption as any)"
@confirm="onConfirm"
@cancel="onCancel"
/>
</van-popup>
<slot name="action"> </slot>
</div>
</template>
<script setup lang="ts">
// eslint-disable-next-line no-undef
const props = defineProps({
showPicker: {
type: Boolean,
default: false,
},
currentOption: {
type: Array,
default: () => [],
},
})
// eslint-disable-next-line no-undef
const emit = defineEmits(['update:showPicker', 'setSelectValue'])
const onConfirm = ({ selectedOptions }) => {
emit('setSelectValue', selectedOptions)
emit('update:showPicker', false)
}
const onCancel = () => {
emit('update:showPicker', false)
}
</script>
<template>
<div>
<CustomForm :schema="schema" ref="customFormRef" />
<CustomPicker
:currentOption="currentOption"
v-model:showPicker="showPicker"
@setSelectValue="setSelectValue"
/>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import CustomForm from './customForm.vue'
import CustomPicker from './customPicker.vue'
const schema = [
{
fieldProps: {
field: 'switch',
name: 'switch',
rules: [],
label: '开关',
},
compotent: 'van-switch',
compotentProps: {
defaultValue: 0,
},
},
{
fieldProps: {
field: 'rate',
name: 'rate',
rules: [],
label: '开关',
},
compotent: 'van-rate',
compotentProps: {
value: 3,
},
},
{
fieldProps: {
field: 'stepper',
name: 'stepper',
rules: [],
label: '步进器',
},
compotent: 'van-stepper',
compotentProps: {
value: 3,
},
},
{
fieldProps: {
field: 'picker',
label: '选择器',
onclick: () => {
openPicker('picker', [
{
text: 'test-1',
value: 1,
},
{
text: 'test-2',
value: 2,
},
])
},
},
compotent: '',
compotentProps: {
value: 3,
['is-link']: true,
readonly: true,
label: '选择器',
},
},
]
const customFormRef = ref<any>(null)
const currentOption = ref<Array<Record<string, any>>>([])
const currentSelectField = ref('')
const showPicker = ref(false)
const openPicker = (field: string, Options: Array<Record<string, any>>) => {
currentOption.value = Options
currentSelectField.value = field
showPicker.value = true
}
const setSelectValue = (selectedOptions) => {
customFormRef.value.setFormData({
[currentSelectField.value]: selectedOptions[0].text,
})
}
</script>
<style lang="less" scoped></style>
Vitest+Vue Test Utils组件测试
- vitest.config.ts配置
// vitest.config.js
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
test: {
clearMocks: true,
environment: "happy-dom",//使用 happy-dom 模拟 DOM
transformMode: {
web: [/\.[jt]sx$/],
},
deps: {
// 使用UI库
inline: ['vant']
}
},
})
- 基础测试
// 组件
<template>
<div @click="add">{{ count }}</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
// eslint-disable-next-line no-undef
const emit = defineEmits(['change'])
const add = () => {
emit('change', 123)
count.value++
}
</script>
// 测试文件
import { it, describe, expect } from 'vitest'
import { mount } from '@vue/test-utils'
// import CustomPicker from './customPicker.vue'
import counter from './counter.vue'
import exp from 'constants'
describe('基础测试', () => {
const wapper = mount(counter)
it('测试组件是否正确渲染', () => {
expect(wapper.html()).toBe('<div>0</div>')
})
it('测试vue语法正确渲染', () => {
expect(wapper.html()).toBe('{{count}}')
})
})
- 模拟用户交互
import { it, describe, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
// import CustomPicker from './customPicker.vue'
import counter from './counter.vue'
describe('模拟交互', () => {
it('点击事件', () => {
const wapper = mount(counter)
wapper.trigger('click')
expect(wapper.html()).toBe('<div>1</div>')
})
it('点击事件-await', async () => {
const wapper = mount(counter)
await wapper.trigger('click')
expect(wapper.html()).toBe('<div>1</div>')
})
it('点击事件-nextTick', () => {
const wapper = mount(counter)
wapper.trigger('click')
nextTick(() => {
expect(wapper.html()).toBe('<div>1</div>')
})
})
})
- 测试props
import { it, describe, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import { nextTick } from 'vue'
import CustomPicker from './customPicker.vue'
describe('设置props', () => {
const wapper = mount(CustomPicker)
it('查看props', () => {
expect(wapper.props('showPicker')).toBe(true)
})
it('设置节点渲染', () => {
const picker = wapper.find('van-picker')
expect(picker.isVisible()).toBe(true)
})
it('设置props', async () => {
wapper.setProps({ showPicker: true })
nextTick(() => {
const picker = wapper.find('van-picker')
expect(wapper.props('showPicker')).toBe(true)
expect(picker.isVisible()).toBe(true)
})
})
})
- 测试emit事件
import { it, describe, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
describe('测试emit事件', async () => {
const wapper = mount(Counter)
wapper.trigger('click')
it('测试emit事件', () => {
expect(wapper.emitted().change).toMatchObject([[12]])
})
})
- 测试插槽
// 路由文件
import { it, describe, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
import CustomPicker from './customPicker.vue'
describe('设置props', async () => {
const wapper = mount(CustomPicker, {
slots: {
action: '<button class="action-btn">确定</button>',
error: '<button class="error">错误</button>'
}
})
it('测试slot-1', async () => {
expect(wapper.find('.action-btn').text()).toBe('确定')
})
it('测试slot-2', async () => {
expect(wapper.find('.error').text()).toBe('错误')
})
})
- router
import { createRouter, createWebHistory } from 'vue-router'
import customButton from '@/components/customButton/index.vue'
import routerPageOne from '@/client/routerPageOne.vue'
import routerPageTwo from '@/client/routerPageTwo.vue'
// 路由按需引入有问题
export const routes = [
{
path: '/routerTest',
component: routerPageOne
},
{
path: '/routerSkip',
component: routerPageTwo
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
// 测试文件
//页面1
<template>
<div>just for test router</div>
</template>
<script lang="ts" setup></script>
<style lang="less" scoped></style>
// 页面二
<template>
<div>just for test router skip</div>
</template>
<script lang="ts" setup></script>
<style lang="less" scoped></style>
// 主页面
<template>
<div>
<router-link to="/routerSkip">skip</router-link>
<router-view />
</div>
</template>
<script lang="ts" setup>
</script>
<style lang="less" scoped>
</style>
// 测试文件
import { describe, it, expect } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import customButton from '../components/customButton/index'
import customField from '../components/customField/index'
import App from './App.vue'
import testRouter from './testRouter.vue'
import { createRouter, createWebHistory } from 'vue-router'
import router from "@/router/index"
import { nextTick } from 'vue'
it('是否正确渲染', async () => {
router.push('/routerTest')
await router.isReady()
const wrapper = mount(testRouter, {
global: {
plugins: [router]
}
})
console.log(111, wrapper.html())
expect(wrapper.html()).contains('just for test router')
await wrapper.find('a').trigger('click')
await flushPromises()
console.log(111, wrapper.html())
expect(wrapper.html()).contains('just for test router skip')
})
- mock数据
// 模拟接口
export const getList = () => new Promise((resolve,reject) => {
resolve({
data: { name: '李四' }
});
})
// 组件
<template>
<div>
<van-popup :show="props.showPicker" position="bottom">
<van-picker
v-if="props.showPicker"
:columns="(props.currentOption as any)"
@confirm="onConfirm"
@cancel="onCancel"
/>
</van-popup>
<div @click="onConfirm">
<slot name="action"> </slot>
</div>
<div @click="getData">
<slot name="getData"></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { getList } from '../api/request'
// eslint-disable-next-line no-undef
const props = defineProps({
showPicker: {
type: Boolean,
default: false,
},
})
let currentOption = ref({})
// eslint-disable-next-line no-undef
const emit = defineEmits(['update:showPicker', 'setSelectValue'])
const onConfirm = async () => {
emit('setSelectValue', currentOption.value)
emit('update:showPicker', false)
}
const onCancel = () => {
emit('update:showPicker', false)
}
const getData = async () => {
const res = await getList()
currentOption.value = res
}
</script>
// 测试代码
import { it, describe, expect, vi } from 'vitest'
import { mount } from '@vue/test-utils'
import Counter from './Counter.vue'
import CustomPicker from './customPicker.vue'
import flushPromises from 'flush-promises';
import { nextTick } from 'vue';
vi.doMock('../api/request');
describe('测试接口', async () => {
const wapper = mount(CustomPicker, {
slots: {
action: '<button class="action-btn">确定</button>',
getData: '<button class="getData"> 请求数据</button>'
}
})
it('测试接口', async () => {
await wapper.find('.getData').trigger('click')
await flushPromises()
wapper.find('.action-btn').trigger('click')
expect(wapper.emitted().setSelectValue).toMatchObject([[{
name: '11',
},]])
})
})
//自定义按钮
<template>
<div>
<van-button class="custom-btn" @click="emitClick" v-bind="$attrs">
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</van-button>
</div>
</template>
<script setup lang="ts">
const emit = defineEmits(['onClick'])
const emitClick = () => {
emit('onClick', 'you has clicked it')
}
</script>
<style></style>
// 自定义输入框
<template>
<div>
<van-field class="custom-field" v-bind="$attrs" @focus="onFocus($attrs.label)" v-model="msg">
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
<slot :name="item" v-bind="data || {}"></slot>
</template>
</van-field>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
const emit = defineEmits(['onFocus'])
const msg = ref('')
const onFocus = (label: any) => {
emit('onFocus', label)
}
</script>
<style></style>
// 测试vue文件
<template>
<div>
<CustomButton :text="'test'">
<template #icon>
<img class="icon" :src="'https://fastly.jsdelivr.net/npm/@vant/assets/user-active.png'" />
</template>
</CustomButton>
<CustomField :label="'测试'" :placeholder="'请输入'" @onFocus="onFocus"/>
<span class="notify">{{ label }}已聚焦</span>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import CustomButton from '../components/customButton/index'
import CustomField from '../components/customField/index'
const label = ref('')
const onFocus = (value: any) => {
label.value = value
}
</script>
<style>
.icon {
width: 20px;
height: 20px;
}
</style>
// 测试文件
import { describe,it,expect} from 'vitest'
import { mount } from '@vue/test-utils'
import customButton from '../components/customButton/index'
import customField from '../components/customField/index'
import App from './App.vue'
describe('测试App是否正确渲染',() => {
const wrapper = mount(App)
const node = wrapper.findComponent(customButton)
it('测试按钮文字',async () => {
console.log(1111, node.text())
expect(node.text()).toBe('test')
})
it('测试按钮插槽',async () => {
console.log(node.find('.icon'))
expect(node.find('.icon').isVisible()).toBe(true)
})
})
**import { describe,it,expect} from 'vitest'
import { mount } from '@vue/test-utils'
import customButton from '../components/customButton/index'
import customField from '../components/customField/index'
import App from './App.vue'
describe('测试App是否正确渲染',() => {
const wrapper = mount(App)
const node = wrapper.findComponent(customField).find('input')
it('测试输入框',async () => {
await node.setValue(1111)
console.log(111, wrapper.find('.notify').text())
expect(wrapper.find('.notify').text()).toBe('1111已聚焦')
})
})
组件打包
// 组件ts文件
// 按钮
import zButton from "./index.vue";
zButton.install = function (Vue: any) {
Vue.component(zButton.name, zButton);
};
export default zButton;
// 输入框
import zField from './index.vue';
zField.install = function (Vue: any) {
Vue.component(zField.name, zField);
};
export default zField;
// main.ts
// 引入封装好的组件
import zButton from "./components/customButton/index";
import zField from "./components/customField/index"
const components = [zButton,zField]; // 如果有多个其它组件,都可以写到这个数组里
// 批量组件注册
const install = function (Vue: any) {
components.forEach((com) => {
Vue.component(com.name, com);
});
};
export default {
install,
zButton,
zField
}; // 这个方法使用的时候可以被use调用
// vite.config.ts
export default defineConfig({
plugins: [
vue(),
vueJsx(),
Components({
resolvers: [VantResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
},
build: {
// 到处文件目录,zmhu-ui 用于存放package.json,避免被覆盖
// 这里不设置也是默认dist
outDir:"dist",
// 兼容
target: "es2015",
lib: {
// Could also be a dictionary or array of multiple entry points
entry: resolve(__dirname, "src/main.ts"),
name: "zmh-ui",
// the proper extensions will be added
// 如果不用format文件后缀可能会乱
fileName: (format) => `zmh-ui.${format}.js`,
},
rollupOptions: {
// 确保外部化处理那些你不想打包进库的依赖
external: ["vue", "vant"],
output: {
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
globals: {
vue: "Vue",
vant: "Vant",
},
},
},
},
})
// package.json
"version": "2.0.0", // 版本,每次发包不能相同
"private": false,// 是否私有
"files": ["dist"],// 别人npm 下载的文件
"main": "./dist/zmh-ui.umd.js",
"module": "./dist/zmh-ui.es.js",
"name": "zmh-ui",// 包名
"exports": {
".": {
"import": "./dist/zmh-ui.es.js",
"require": "./dist/zmh-ui.umd.js"
},
"./dist/style.css": {
"import": "./dist/style.css",
"require": "./dist/style.css"
}
},
使用
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './assets/main.css'
import customUI from 'zmh-ui'
import "zmh-ui/dist/style.css"
const app = createApp(App)
app.use(router).use(customUI)
app.mount('#app')
// vue-shim.d.ts
declare module 'zmh-ui'
// App.vue
<script setup lang="ts">
import customUI from 'zmh-ui'
const { zButton } = customUI
</script>
<template>
<zButton :text="'测试'">
<template #icon>
<img class="btn-icon" :src="'../public/favicon.ico'">
</template>
</zButton>
</template>
<style scoped>
:deep(.btn-icon) {
width: 20px;
height: 20px;
}
</style>