- 引入 create-vite-app
yarn global add create-vite-app
cva 项目名
yarn dev
- 将 main.js 改为 main.ts,报错:can not find module
解决方法:src 文件夹建文件 shims-vue.d.ts
declare module '*.vue' {
import { ComponentOptions } from 'vue'
const componentOptions: ComponentOptions
export default componentOptions
}
- 引入 vue-router,新建 history 对象,新建 router 对象,app.use(router)
npm info vue-router versions
yarn add vue-router@4.0.0
import {createWebHashHistory,createRouter} from "vue-router"
const history = createWebHashHistory()
const router = createRouter({
history:history,
routes:[
{path:'/',component:Home},
{
path:'/doc',
component:Doc,
//嵌套路由
children:[
//默认路径
{path:'',component:DocDemo},
{path:'switch',component:SwitchDemo},
{path:'button',component:ButtonDemo},
{path:'dialog',component:DialogDemo},
{path:'tabs',component:TabsDemo}
]
}
]
})
const app = createApp(App)
app.use(router)
app.mount('#app')
- 使用 router-link / router-view
<template>
<div>导航栏 |
<router-link to="/">Frank</router-link> |
<router-link to="/xxx">Frank2</router-link>
</div>
<hr>
<router-view />
</template>
- 实现 aside 切换,provide / inject / ref
// App.vue,provide 标记值
import {provide, ref} from 'vue';
export default {
name: 'App',
setup(){
//根据屏幕宽度,设置初始值
const width = document.documentElement.clientWidth
const menuVisible = ref(width <= 500 ? false : true)
provide('menuVisible',menuVisible)
}
}
//Doc.vue,inject 获取值,根据值的不同切换显示/隐藏
<div v-if="menuVisible"> </div>
import {inject,Ref} from 'vue';
export default {
components: {Topnav},
setup(){
// 类型为 Ref ,值为 boolean
const menuVisible = inject<Ref<boolean>>('menuVisible')
// return {} 之后,template 里面才能取到这个值
return {menuVisible}
}
}
//Topnav.vue,inject 获取值,点击后改变值
<div class="logo" @click="toggleMenu">LOGO</div>
setup(){
const menuVisible = inject<Ref<boolean>>('menuVisible')
const toggleMenu = ()=>{
menuVisible.value = !menuVisible.value
}
return {toggleMenu}
}
- VsCode 插件报错 SwitchDemo.vue 文件引入 Switch 组件时,Vetur 插件报错
解决方法:
-
打开 shims-vue.d.ts 文件,然后关闭;
-
或者打开 Switch.vue(被引入的文件),添加
export default {},然后返回当前文件 SwitchDemo.vue; -
将 import 语句删掉,然后重新 import ;
- props / emit / context
// SwitchDemo.vue ,
<Switch :value="y" @input="y=$event"/>
components:{Switch},
setup(){
const y = ref(false)
return {y}
}
//Switch.vue ,
<button :class="{checked:value}" @click="toggle">
props:{
value:Boolean,
},
setup(props,context) {
const toggle = () => {
//setup 里面不能用 this.$emit,methods 里面可以用 this.$emit
context.emit('input',!props.value)
};
return {toggle};
}
子组件调用 context.emit(事件名,事件参数),通过 input 事件,将最新的值传给父组件,父组件监听 input 事件,通过 $event 获得最新值(emit 的第二个参数)。事件名 input 可自定义,只要父子组件的事件名相同即可。
- v-model
-
属性名任意,假设为 x ;
-
事件名必须为 'update:x'
-
<Switch :value="y" @update:value = "y=$event"可以简写为<Switch v-model:value="y"
- slot
//父组件
<Button>你好</Button>
//子组件
<button>
<slot />
</button>
- 绑定属性 v-bind
- 默认所有属性都绑定到根元素;
//父组件
<Button disabled>禁用按钮</Button>
//Button 组件,disabled 会自动绑定到根元素 <button> 上。
<template>
<button>
<slot/>
</button>
</template>
//当 Button 组件用 props 接收 disabled 时,不会自动绑定,需要手动绑定
<Button disabled>禁用按钮</Button>
// 或者 <Button :disabled='false'>禁用按钮</Button>
<template>
<button disabled>
<slot/>
</button>
</template>
export default {
props:{
disabled:{
type:String,
default:false
}
}
}
-
子组件使用 inheritAttrs:false ,可以取消默认绑定;
-
template 中使用 $attrs ,setup 中使用 context.attrs 获取所有属性;
-
使用
v-bind="$attrs"批量绑定属性; -
使用
const {size,...xxx}=context.attrs将属性分开
//父组件
<Button @click="onClick"
@focus="onClick"
@mouseover="onClick"
size="small">
</Button>
//子组件,
<template>
<div :size="size">
<button v-bind="rest">
<slot/>
</button>
</div>
</template>
export default {
inheritAttrs:false,
setup(props,context){
//const {size,onClick,onMouseOver} = context.attrs 注意事件名的变化
const {size,...rest} = context.attrs
return {size,rest}
}
}
props 和 attrs 的区别
vue3 组件传值之 props 与 attrs 的区别 - 小船二 - 博客园 (cnblogs.com)
-
props 要先声明才能取值,attrs 不用先声明;
-
props 声明过的属性,attrs 里不会再出现;
-
props 不包含事件,attrs 包含;
-
props 支持 String 以外的类型,attrs 只有 String 类型;
-
attrs 中的事件名称,要在父组件的事件名前面加 on
- UI 库的 CSS 注意事项
-
不能使用 scoped:
因为 data-v-xxx 中的 xxx 每次运行可能不同;
必须输出稳定不变的 class 选择器,方便使用者覆盖;
-
类名必须加前缀:
.button 不行,很容易被使用者覆盖;
.pixu-button 可以,不太容易被覆盖;
.theme-link 不行,很容易被使用者覆盖;
.pixu-theme-link 可以,不太容易被覆盖;
//当 theme 的值为 undefined 时,这个 class 不存在;
//当 theme 有值时,例 theme=‘button’,class 的值为 pixu-theme-button
:class = "{[`pixu-theme-${theme}`]: theme}"
11、CSS 最小影响原则:
自己的 CSS 绝对不能影响库使用者。
//新建 pixu.scss 文件,将影响到库的样式,从 index.scss 复制到这个文件
//以 'pixu-' 开头的样式和以 ' pixu-' 开头的样式
[class^='pixu-'],[class*=' pixu-']{
padding:0;
margin:0;
box-sizing:border-box;
font-size:16px;
font-family: -apple-system, "Noto Sans", "Helvetica Neue", Helvetica, "Nimbus Sans L", Arial, "Liberation Sans", "PingFang SC", "Hiragino Sans GB", "Noto Sans CJK SC", "Source Han Sans SC", "Source Han Sans CN", "Microsoft YaHei", "Wenquanyi Micro Hei", "WenQuanYi Zen Hei", "ST Heiti", SimHei, "WenQuanYi Zen Hei Sharp", sans-serif;
}
- 加载小动画
> .pixu-loadingIndicator{
width: 14px;
height: 14px;
display: inline-block;
margin-right:4px;
border-radius:8px;
border-color:$blue $blue $blue transparent;
border-style:solid;
border-width:2px;
animation:pixu-spin 1s infinite linear;
}
@keyframes pixu-spin {
0%{transform:rotate(0deg)}
100%{transform:rotate(360deg)}
}
- 具名插槽
<template>
<Dialog>
<template v-slot:title>
<strong>加粗的标题</strong>
</template>
<template v-slot:content>
<div>hi</div>
<div>hi2</div>
</template>
</Dialog>
</template>
//Dialog.vue
<div class="pixu-dialog">
<header>
<slot name="title"/>
</header>
<main>
<slot name="content"/>
</main>
</div>
- 使用内置组件 ,移动组件内的内容,将 Dialog 移到 body 下面,防止 Dialog 因为层级上下文的关系,被其它的页面遮盖。
//Dialog.vue 页面
<template>
<teleport to="body">
<div>
<footer>
<Button>OK</Button>
</footer>
</div>
</teleport>
</template>
- 使用函数和事件的方式渲染 Dialog
- 之前使用 Dialog 组件时,直接将参数写在组件标签里面;
<Dialog v-model:visible="x"
:closeOnClickOverlay="false"
:ok="f1" :cancel="f2">
</Dialog>
- 动态挂载组件 利用函数,执行 openDialog 函数时,渲染 Dialog.
//使用 Dialog 组件的地方
<Button @click="showDialog">show</Button>
import {openDialog} from '../lib/openDialog';
setup(){
const showDialog = ()=>{
openDialog({
title:'标题',
content:'你好',
ok(){
console.log('ok')
},
cancel(){
console.log('false')
},
closeOnClickOverlay:true
})
}
}
// 建立 openDialog.ts 文件
import Dialog from './Dialog.vue';
import {createApp, h} from 'vue';
export const openDialog = (options) => {
const {title, content,ok,cancel,closeOnClickOverlay} = options;
//建立临时 div ,放置渲染出来的 Dialog 组件,Dialog 组价不能直接放在 body 里面,否则 body 内的东西会被清除;
const div = document.createElement('div');
document.body.appendChild(div);
const close = () => {
// @ts-ignore
app.unmount(div);
div.remove();
};
const app = createApp({
render() {
return h(Dialog, {
//第二个参数内的所有东西,都是第一个参数的属性
visible: true,
'onUpdate:visible': (newVisible) => {
//关闭 Dialog 组件时,直接移除即可
if (newVisible === false) {close();}
},
ok,cancel,closeOnClickOverlay
}, {
//插槽的内容放第三个参数(子代虚拟节点)
title, content
});
}
});
app.mount(div);
};
- 如何在运行时确认子组件的类型?
用 JS 获取插槽内容:
context.slots.default()数组,数组内容是外部传给该组件的子内容console.log({...context})//setup() 里面,context 可以解构,props 不行,因为 props 是响应式的。
// TabsDemo.vue
<template>
<Tabs>
//标签名不是 Tab 时,会报错。
<div title="导航1">内容1</div>
<Tab title="导航2">内容2</Tab>
</Tabs>
</template>
//Tabs.vue
<template>
<div>
这是 Tabs 组件
<component :is="defaults[0]"/>
<component :is="defaults[1]"/>
</div>
</template>
<script lang="ts">
import Tab from './Tab.vue'
export default {
setup(props,context){
const defaults = context.slots.default()
defaults.forEach((tag)=>{
//判断子组件的标签是不是 Tab
if(tag.type !== Tab){
throw new Error('Tabs 字标签必须是 Tab')
}
})
return {defaults}
}
}
//如果生产环境下该方法报错,可以改变下思路,在 Tab 组件上做一个标记,添加一个 name 属性,只需要对比 tag 的 name 和 Tab 的 name 是否相等就行,不用对比是不是 Tab 标签
//在 Tab.vue 文件
export default {
name:'xxx'
}
//在 Tabs.vue 中
defaults.forEach((tag)=>{
//判断子组件的标签是不是 Tab
@ts-ignore
if(tag.type.name !== Tab.name){
throw new Error('Tabs 字标签必须是 Tab')
}
})
return {defaults}
- 嵌套插槽的渲染 tabs 中插入了 Tab,Tab 中插入了内容。
//Tabs.vue
<div>
<div v-for="(t,index) in titles" :key='index'>{{t}}</div>
<component v-for="(c,index) in defaults" :key="index" :is="c"/>
</div>
// <slot/> 不能直接在 Tabs 里面写slot,这样会将所有内容直接显示出来,而不是一个一个获取子组件的内容,应该用上面的 component v-for。
//Tab.vue
<template>
<div>
//直接用插槽将所有内容显示出来
<slot/>
</div>
</template>
- Tabs 动态挂载组件 Tab v-if 和 v-for 不能同时使用;
动态挂载组件时,需要加 :key,否则 Tab 内容一直不变。
//Tabs.vue
<div :class="{selected:t===selected}"
@click="select(t)" class="pixu-tabs-nav-item"
v-for="(t,index) in titles" :key='index'}">
{{ t }}
</div>
<component :is="current" :key="current.props.title" /> //动态挂载组件时,需要加 :key,否则 Tab 内容一直不变。
setup(){
const select = ()=>{context.emit('update:selected',title)}
}
const titles = defaults.map(tag=>tag.props.title)
//获取被选中的 Tabs 需要展示的 Tab,current 的值随着 selected 的变化而变化,所以要用 computed 计算出来
const current = computed(()=>{
return defaults.filter(()=>{
return tag.props.title === props.selected
})
})
- 动态设置 div 的宽度
// template 的 v-for 中使用 ref,动态获取当前 v-for 出来的组件
<div v-for="(t,index) in titles" :key='index'
:ref="(el) => { if(el) navItems[index] = el }">{{ t }} </div>
<div ref="indicator"></div>
const navItems = ref<HTMLDivElement[]>([]);
const indicator = ref<HTMLDivElement>(null)
onMounted(()=>{
//获取被选中的 div
const result = navItems.value.filter((item)=>{
return item.classList.contains('selected')
})[0]
//获取被选中 div 的宽度
const {width} = result.getBoundingClientRect()
//动态设置目标 div 的宽度
indicator.value.style.width = width + 'px'
})
- 动态设置 div 的位置 解构赋值的变量重命名:
const container = ref<HTMLDivElement>(null);
//将获取到的 left 重新命名为 left1
const {left: left1} = container.value.getBoundingClientRect();
const {left: left2} = result.getBoundingClientRect();
const left = left2 - left1;
indicator.value.style.left = left + 'px';
- watchEffect 的调用时机
// 如副作用中涉及到DOM的操作和ref的获取,需要放到mounted周期中执行
onMounted(() => {
watchEffect(() => { // .. 操作dom或ref等 })
})
// 也可以用 flush 参数,默认为 pre,在更新前执行
watchEffect(()=>{
console.log(x) },
{ flush:'post' //在组件更新完成之后操作 })
- TS 泛型
//<>里的值,表示的是函数参数 () 的类型
const container = ref<HTMLDivElement>(null)
- clip-path 实现圆弧
clip-path: ellipse(80% 60% at 50% 40%);
- 选中的路由高亮
vue3 中,选中的路由会自动添加两个class:
router-link-active和router-link-exact-active
//a标签是行内元素,变成块元素后填满 <li>,然后将 padding 加在 a 标签上
li {
> a {
display: block;
padding: 4px 16px;
text-decoration:none;
}
.router-link-active{
background-color: white;
}
}
- 引入 github 样式
yarn add github-markdown-css
//main.ts 引入
import 'github-markdown-css'
//添加 class="markdown-body" 即可
<article class="markdown-body">
- ts 不认识 .md 和 .vue 文件,项目根目录下,新建 shims-vue.d.ts 文件,告诉 ts 这些后缀对应的文件是什么东西就行
declare module '*.vue' {
import { ComponentOptions } from 'vue'
const componentOptions: ComponentOptions
export default componentOptions
}
declare module '*.md' {
const str: String
export default str
}
27.引入 markdown 格式, 项目根目录下创建 plugins/md.ts 和 vite.config.ts
md.ts 文件:将 markdown 格式转换为 js 格式
// @ts-nocheck
import path from 'path'
import fs from 'fs'
import marked from 'marked'
const mdToJs = str => {
const content = JSON.stringify(marked(str))
return `export default ${content}`
}
export function md() {
return {
configureServer: [ // 用于开发
async ({ app }) => {
app.use(async (ctx, next) => { // koa
if (ctx.path.endsWith('.md')) {
ctx.type = 'js'
const filePath = path.join(process.cwd(), ctx.path)
ctx.body = mdToJs(fs.readFileSync(filePath).toString())
} else {
await next()
}
})
},
],
transforms: [{ // 用于 rollup // 插件
test: context => context.path.endsWith('.md'),
transform: ({ code }) => mdToJs(code)
}]
}
}
vite.config.ts 文件: 调用了 md.ts 文件,然后将 md.ts 文件传给了 plugins。
// @ts-nocheck
import { md } from "./plugins/md";
export default {
plugins: [md()]
};
-
将 svg 变为本地文件 将 iconfont 生成的 JS 地址在浏览器打开,保存文件,然后将文件复制到项目 lib 目录下,在 main.js 中引用该文件。
-
展示源代码:使用 Vue loader 的 Custom blocks
//新建 .vue 文件,放用户需要拷贝的代码
//加 demo 标签(自定义的标签),用于 vite.config.ts 文件做识别
<demo>
常规用法
</demo>
<template>
<Switch v-model:value="bool"/>
</template>
<script lang="ts">
import Switch from '../lib/Switch.vue';
import {ref} from 'vue';
export default {
components: {
Switch
},
setup() {
const bool = ref(false)
return {bool}
}
}
</script>
//更改 vite.config.ts 的配置,识别 demo 标签,将源代码放在 Component.__sourceCode 里
// @ts-nocheck
import fs from 'fs'
import {baseParse} from '@vue/compiler-core'
export default {
vueCustomBlockTransforms: {
demo: (options) => {
const { code, path } = options
const file = fs.readFileSync(path).toString()
const parsed = baseParse(file).children.find(n => n.tag === 'demo')
const title = parsed.children[0].content
const main = file.split(parsed.loc.source).join('').trim()
return `export default function (Component) {
Component.__sourceCode = ${
JSON.stringify(main)
}
Component.__sourceCodeTitle = ${JSON.stringify(title)}
}`.trim()
}
}
};
// 在需要展示源代码的地方引用组件的 __sourceCode,两种方式都行
<pre>{{Switch1Demo.__sourceCode}}</pre>
<pre v-text="Switch1Demo.__sourceCode"></pre>
- 在 export default{} 里面 import,此时是异步加载,import() 不能直接得到值,setup()外面不能写 async所以需要用 .then 方法异步获取值。
import xx from 'xxx'只能写在export default外面。
props:{path:{type:String,required:true}}
setup(){
const content = ref(null)
import(props.path).then(result=>{
content.value = result.default
})
return {content}
}
- 更改 vite 的 build path 在 vite.config.ts 里面:
export default{
base:'./',
将默认的 _assets 改为 assets,否则 github 找不到含有下划线的路径
assetsDir:'assets',
}
Vue 2 和 Vue 3 的区别
1、Vue 3 的 template 支持多个根标签,Vue 2 不支持;
2、Vue 3 有 createApp() , 而 Vue 2 的是 new Vue();
3、createApp(组件),new Vue({template,render});
4、Vue 3 的 v-model 代替以前的 v-model 和 .sync;
5、新增 context.emit,与 this.$emit 作用相同,setup(){} 里面只能用 context.emit()