上海网站建设哪,培训网页设计吗,本地服务器 wordpress,郑州官网制做Vue3 vite Ts pinia 实战 源码 electron 仓库地址#xff1a;https://gitee.com/szxio/vue3-vite-ts-pinia 视频地址#xff1a;小满Vue3#xff08;课程导读#xff09;_哔哩哔哩_bilibili 课件地址#xff1a;Vue3_小满zs的博客-CSDN博客 初始化Vue3项目
方式一
…Vue3 vite Ts pinia 实战 源码 electron 仓库地址https://gitee.com/szxio/vue3-vite-ts-pinia 视频地址小满Vue3课程导读_哔哩哔哩_bilibili 课件地址Vue3_小满zs的博客-CSDN博客 初始化Vue3项目
方式一
npm init vitelatest生成的目录结构
vite-demo
├── .vscode
│ └── extensions.json
├── public
│ └── vite.svg
├── src
│ ├── assets
│ │ └── vue.svg
│ ├── components
│ │ └── HelloWorld.vue
│ ├── App.vue
│ ├── main.ts
│ ├── style.css
│ └── vite-env.d.ts
├── README.md
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts启动
npm run dev方式二
npm init vuelatest生成的目录结构
vue-demo
├── .vscode
│ └── extensions.json
├── public
│ └── favicon.ico
├── src
│ ├── assets
│ │ ├── base.css
│ │ ├── logo.svg
│ │ └── main.css
│ ├── components
│ │ ├── __tests__
│ │ ├── icons
│ │ ├── HelloWorld.vue
│ │ ├── TheWelcome.vue
│ │ └── WelcomeItem.vue
│ ├── router
│ │ └── index.ts
│ ├── stores
│ │ └── counter.ts
│ ├── views
│ │ ├── AboutView.vue
│ │ └── HomeView.vue
│ ├── App.vue
│ └── main.ts
├── .eslintrc.cjs
├── .prettierrc.json
├── README.md
├── env.d.ts
├── index.html
├── package.json
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── tsconfig.vitest.json
├── vite.config.ts
└── vitest.config.ts用这种方式生成的项目会全一点
启动
npm run dev自动生成路由
添加 gen-router.js 文件
var fs require(fs);
const readline require(readline);
const os require(os);const vueDir ./src/views/;fs.readdir(vueDir, function (err, files) {if (err) {console.log(err);return;}let routers ;// 对文件进行排序let sortFiles files.sort((a,b){return a.split(_)[0] - b.split(_)[0]});for (const filename of sortFiles) {if (filename.indexOf(.) 0) {continue;}var [name, ext] filename.split(.);if (ext ! vue) {continue;}let routerName nullconst contentFull fs.readFileSync( ${vueDir}${filename}, utf-8 );var match /\\!\-\-\s*(.*)\s*\-\-\/g.exec(contentFull.split(os.EOL)[0]);if (match) {routerName match[1];}routers {path: /${name root ? : encodeURIComponent(name)},name:${name}, component: () import(/* webpackChunkName: ${name} */ /views/${filename}) ${ routerName ? ,name: routerName : } },\n;}const result
import { createRouter, createWebHistory } from vue-router
import Layout from /layout/index.vueconst router createRouter({history: createWebHistory(import.meta.env.BASE_URL),routes: [{path: /,name: index,component: Layout,redirect: /index,children:[${routers}]},]
})export default router
// console.log(result);fs.writeFile(./src/router/index.ts,result, utf-8,(err) {if (err) throw err;});
});修改 package.json 中的启动命令
scripts: {dev: node gen-router.js vite,
},这样每次新建完一个文件后需要重启一下服务然后会自动生成路由文件配置菜单动态显示即可
Ref全家桶
ref
接受一个内部值并返回一个可变响应式的 ref 对象。ref 对象仅有一个 .value property指向该内部值。
templatediv{{ product }}/divhrbutton clickchange点击/button
/templatescript setup langtsimport {ref} from vue;const product ref({id:001,name:小米手机
})const change () {product.value.name 华为手机console.log(product)
}/script调试小技巧
我们打印 ref 对象时需要点开两层才能看到信息如下 可以打开 启用自定义格式化程序 之后打印就会直接展示具体的信息 isRef
判断一个对象是否是响应式对象
import { ref, isRef } from vue;const product ref({id: 001,name: 小米手机
})const change () {product.value.name 华为手机// isRef判断一个对象是否是响应式对象console.log(isRef(product)) // true
}shallowRef
创建一个跟踪自身 .value 变化的 ref但不会使其值也变成响应式的
import { ref, isRef, shallowRef } from vue;const shaRef shallowRef({price: 100
})const change () {// product.value.name 华为手机// isRef判断一个对象是否是响应式对象console.log(isRef(product)) // trueshaRef.value.price 200console.log(shaRef.value);
}上面的例子中页面不会发生变化
triggerRef
强制更新页面
import { ref, isRef, shallowRef, triggerRef } from vue;const product ref({id: 001,name: 小米手机
})const shaRef shallowRef({price: 100
})const change () {// product.value.name 华为手机// isRef判断一个对象是否是响应式对象console.log(isRef(product)) // trueshaRef.value.price 200console.log(shaRef.value);triggerRef(shaRef)
}需要传入一个要更新的对象
customRef
自定义一个ref响应式数据
import { customRef } from vue;function myRefT(value: T) {return customRef((track, trigger) {return {get() {track()return value},set(newVal) {value newValtrigger()},}})
}const song1 myRef(123)const change () {song1.value 456}Reactive全家桶
Reactive
用来绑定复杂的数据类型 例如 对象 数组
源码中限定只能传入类型是Object的数据 templatediv{{ form }}/divbutton clickchange改变/buttonhrulli v-foritem in list.value{{ item }}/li/ulbutton clickgetList获取/button
/templatescript setup langts nameReactive
import { reactive, } from vue;let form reactive({name: 张三,age: 18
})
function change() {form.age
}let list reactive({value: [lisi, wangwu]
})
function getList() {setTimeout(() {let res [Anly, Jack]// 直接给reactive赋值会破坏原有的响应式list.value resconsole.log(list);}, 1000);
}
/scriptReadonly
将一个对象设置为只读
import { reactive, readonly } from vue;let form reactive({name: 张三,age: 18
})
let readOnlyForm readonly(form)
function change() {readOnlyForm.age
}shallowReactive
浅层的响应式
import { shallowReactive } from vue;let shaReactive shallowReactive({a: {b: 123}
})
function chageSha() {shaReactive.a.b 456 // 页面不会发生改变console.log(shaReactive); // 打印的数据发生改变
}to系列全家桶
toRef
将对象中的某个属性变成响应式的
如果原始数据是非响应式的则经过 toRef 之后也不会更新视图但是数据会发生变化
templatediv{{ student }}/divdivlikeRef:{{ likeRef }}/divbutton clickchange修改/button
/template
script setup langts
import { toRef } from vueconst student {name: Jack,age: 18,like: 画画
}let likeRef toRef(student, like)function change() {// 如果源数据是非响应式的则经过toRef后也不会触发页面更新likeRef.value 足球console.log(student);console.log(likeRef);}/script如果源数据就是响应式的则会触发页面更新
templatediv{{ student }}/divdivlikeRef:{{ likeRef }}/divbutton clickchange修改/button
/template
script setup langts
import { toRef, reactive } from vueconst student reactive({name: Jack,age: 18,like: 画画
})let likeRef toRef(student, like)function change() {// 如果源数据是非响应式的则经过toRef后也不会触发页面更新likeRef.value 足球console.log(student);console.log(likeRef);}/scripttoRefs
将对象的所有数据都变成响应式数据
import { toRef, toRefs, toRaw, ref, reactive } from vueconst student reactive({name: Jack,age: 18,like: 画画,code: [1, 2]
})// 自实现toRefs
function myToRefsT extends Object(object: T) {let map: any {}for (const key in object) {map[key] toRef(object, key)}return map
}
function refs() {console.log(myToRefs(student)); // 打印结果如下图
}// 使用场景对象解构
let { name, age, code } toRefs(student)
function fun1() {name.value Timage.value 16code.value.push(3)
}myToRefs 打印结果 toRaw
返回对象的原始信息
function fun2() {console.log(toRaw(student));
}打印 Vue3响应式源码实现
初始化项目结构
vue-proxy
├── effect.js
├── effect.ts
├── index.html
├── index.js
├── package.json
├── reactive.js
├── reactive.ts
└── webpack.config.jsreactive.ts
import { track, trigger } from ./effect// 判断是否是对象
const isObject (target) target ! null typeof target object// 泛型约束只能传入Object类型
export const reactive T extends object(target: T) {return new Proxy(target, {get(target, key, receiver) {console.log(target);console.log(key);console.log(receiver);let res Reflect.get(target, key, receiver)track(target, key)if (isObject(res)) {return reactive(res)}return res},set(target, key, value, receiver) {let res Reflect.set(target, key, value, receiver)console.log(target, key, value);trigger(target, key)return res}})}effect.ts
// 更新视图的方法
let activeEffect;
export const effect (fn: Function) {const _effect function () {activeEffect _effect;fn()}_effect()
}// 收集依赖
const targetMap new WeakMap()
export const track (target, key) {let depsMap targetMap.get(target)if (!depsMap) {depsMap new Map()targetMap.set(target, depsMap)}let deps depsMap.get(key)if (!deps) {deps new Set()depsMap.set(key, deps)}deps.add(activeEffect)
}// 触发更新
export const trigger (target, key) {const depsMap targetMap.get(target)const deps depsMap.get(key)deps.forEach(effect effect())
}测试
执行 tsc 转成 js 代码没有 tsc 的全局安装 typescript
npm install typescript -g新建 index.js分别引入 effect.js 和 reactive.js
import { effect } from ./effect.js;
import { reactive } from ./reactive.js;let data reactive({name: lisit,age: 18,foor: {bar: 汽车}
})effect(() {document.getElementById(app).innerText 数据绑定${data.name} -- ${data.age} -- ${data.foor.bar}
})document.getElementById(btn).addEventListener(click, () {data.age
})新建index.html
!DOCTYPE html
html langenheadmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0titleDocument/title
/headbodydiv idapp/divbutton idbtn按钮/button
/body然后再根目录执行
npm init -y安装依赖
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin -D然后新建 webpack.config.js
const path require(path)
const HtmlWebpakcPlugin require(html-webpack-plugin)module.exports {entry: ./index.js,output: {path: path.resolve(__dirname, dist)},plugins: [new HtmlWebpakcPlugin({template: path.resolve(__dirname, ./index.html)})],mode: development,// 开发服务器devServer: {host: localhost, // 启动服务器域名port: 3000, // 启动服务器端口号open: true, // 是否自动打开浏览器},
}执行命令启动项目
npx webpack servecomputed的简单使用
templatedivtable border width600 cellspacing0 cellpadding0theadth名称/thth价格/thth数量/thth总价/thth操作/th/theadtbodytr v-for(item,index) in choosList styletext-align: centertd{{ item.name }}/tdtd{{ item.price }}/tdtd{{ item.count }}/tdtdel-button typeprimary clickitem.count---/el-button{{ item.price * item.count }}el-button typeprimary clickitem.count/el-button/tdtdel-button typedanger clickremove删除/el-button/td/tr/tbodytfoot alignrighttrtd colspan5总价{{total}}/td/tr/tfoot/table/div
/templatescript setup
import {reactive,computed} from vue;let choosList reactive([{name:裤子,price:100,count:1,},{name:衣服,price:200,count:1,},{name:鞋子,price:300,count:1,},{name:帽子,price:400,count:1,}
])
let total computed((){let total 0;choosList.forEach(item{total item.price * item.count;})return total;
})function remove(index){choosList.splice(index,1);
}
/scriptstyle scoped/stylecomputed源码实现
effect.ts
// 更新视图方法
let activeEffect
export const effect (fn:Function,options) {console.log(effect触发)const _effect function () {activeEffect _effectreturn fn()}_effect.options options_effect()return _effect
}// 依赖收集
const targetMap new WeakMap()
export const track (target, key) {let depsMap targetMap.get(key)if (!depsMap) {depsMap new Map()targetMap.set(target, depsMap)}let deps depsMap.get(key)if (!deps) {deps new Set()depsMap.set(key, deps)}deps.add(activeEffect)
}// 触发更新
export const trigger (target, key) {const depsMap targetMap.get(target)const deps depsMap.get(key)deps.forEach(effect {if (effect.options.scheduler){effect.options.scheduler()}else{effect()}})
}reactive.ts
import {track, trigger} from ./effect
// 判断是否是对象类型
const isObject (target) typeof target object target ! nullexport const reactive (target) {return new Proxy(target, {get(target, key, receiver) {console.log(reactive.get-,key)const res Reflect.get(target, key, receiver)// 收集依赖track(target, key)// 递归return isObject(res) ? reactive(res) : res},set(target, key, value, receiver) {console.log(reactive.set-,key)const res Reflect.set(target, key, value, receiver)// 触发依赖trigger(target, key)return res}})
}computed.ts
import {effect} from ./effectexport const myComputed (getter:Function){let _value effect(getter,{scheduler:(){_dirty true}})// 判断是否需要重新计算结果let _dirty true// 缓存结果let catchValueclass ComputedRefImpl{get value(){if(_dirty){console.log(依赖发生变化时执行)catchValue _value()_dirty false}return catchValue}}return new ComputedRefImpl()
}watch监听器
监听单属性值
let name ref(李四)watch(name,(newValue,oldValue){console.log(newValue,oldValue)
})同时监听多个属性
let name ref(李四)
let age ref(20)watch([name,age],(newValue,oldValue){console.log(newValue,oldValue)
})深度监听
let obj ref({foo:{bar:{name:张三}}
})watch(obj,(newValue,oldValue){console.log(obj.value.foo.bar.name)
},{deep:true, // 深度监听immediate:true, // 立即执行
})监听对象中的某一个属性
let obj ref({foo:{bar:{name:张三,age:18}}
})// 监听某个属性是要传入一个函数来返回要监听的属性值
watch(()obj.value.foo.bar.age,(newValue,oldValue){console.log(obj.value.foo.bar.age)
},{immediate:true
})watchEffect
简介
watchEffect不需要传入任何参数它是一个函数当依赖变化时这个函数就会执行它内部会根据响应式数据的依赖关系自动执行监听函数
使用
templateel-input idmsg1 v-modelmsg1 placeholderplaceholder/el-inputel-input v-modelmsg2 placeholderplaceholder/el-inputel-button typeprimary clickstopWatch停止监听/el-button
/templatescript setup
import {ref, watchEffect,nextTick} from vuelet msg1 ref(msg1)
let msg2 ref(msg2)// watchEffect不需要传入任何参数它是一个函数当依赖变化时这个函数就会执行
// 它内部会根据响应式数据的依赖关系自动执行监听函数
const stop watchEffect((){console.log(msg1.value)console.log(msg2.value)
})function stopWatch(){// 停止监听stop()
}/scriptBEM架构和Layout布局
Layout目录结构
layout
├── Content
│ └── index.vue
├── Header
│ └── index.vue
├── Menu
│ └── index.vue
├── css
│ └── bem.scss
└── index.vue新建 bem.scss
$namespace: zx !default;
$block-sel:- !default;
$element-sel:__ !default;
$modifier-sel:-- !default;mixin bfc{height:100%;overflow: hidden;
}mixin b($block){// 拼接的结果为:zx-xxx$B:$namespace $block-sel $block;.#{$B}{content;}
}mixin e($element){// 拼接的结果为:zx-xxx__xxx$selector:;at-root {$E:$selector $element-sel $element;#{$E}{content;}}
}mixin m($modifier){// 拼接的结果为:zx-xxx--xxx$selector:;at-root {$M:$selector $modifier-sel $modifier;#{$M}{content;}}
}配置全局生效
import { fileURLToPath, URL } from node:urlimport { defineConfig } from vite
import vue from vitejs/plugin-vue
import vueJsx from vitejs/plugin-vue-jsx
import AutoImport from unplugin-auto-import/vite
import Components from unplugin-vue-components/vite
import { ElementPlusResolver } from unplugin-vue-components/resolvers// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),vueJsx(),AutoImport({resolvers: [ElementPlusResolver()],}),Components({resolvers: [ElementPlusResolver()],}),],resolve: {alias: {: fileURLToPath(new URL(./src, import.meta.url))}},css: {preprocessorOptions: {// 配置全局CSSscss: {additionalData: import ./src/layout_v2/css/bem.scss;}}}
})index.vue
templatediv classzx-boxdiv classzx-box__menuMenu//divdiv classzx-box__mainHeader/Content//div/div
/templatescript setupimport Menu from /layout_v2/Menu/index.vue;
import Header from /layout_v2/Header/index.vue;
import Content from /layout_v2/Content/index.vue;
/scriptstyle scoped langscss
include b(box){height: 100%;display: flex;include e(menu){width: 250px;height: 100%;border-right: 1px solid #ebebeb;}include e(main){flex: 1;display: flex;flex-direction: column;}
}/styleMenu/index
templatedivMenu/div
/templatescript setup/scriptHeader/index.vue
template
div classzx-headerHeader
/div
/templatescript setup
/scriptstyle scoped langscss
include b(header){width: 100%;height: 60px;line-height: 60px;border-bottom: 1px solid #ccc;
}
/styleContent/index.vue
templatediv classzx-contentdiv v-foritem in 50 classzx-content__item{{item}}/div/div
/templatescript setup/scriptstyle scoped langscss
include b(content){height: 100%;overflow: auto;include e(item){height: 60px;line-height: 60px;text-align: center;border-radius: 5px;border: 1px solid pink;margin: 10px;}
}
/style布局效果 父子组件传值
简单使用
定义父组件
templatediv classparent-box父组件div子组件传过来的值{{count}}/divbutton clickgetSubInfo获取子组件的所有属性和方法/buttonSubComponent refsubCom :valuetitle changeCountchangeCount//div
/templatescript setup langts
import SubComponent from /components/SubComponent.vue;
import {ref} from vue;let title 给儿子传值;
let count refnumber()// 定义组件类型
let subCom refInstanceTypetypeof SubComponent()// 子组件触发的父组件方法
const changeCount (newVal) {count.value newVal;
}const getSubInfo () {// 调用子组件的实例方法subCom.value.open()// 获取子组件的属性console.log(subCom.value.order)
}
/scriptstyle scoped
.parent-box{width: 300px;height: 300px;border: 1px solid #ccc;padding: 30px;
}
/style子组件
templatediv classchildren-box父组件传递的值 {{ value }}button clickchangeParentCount改变父组件的值/button/div
/templatescript setup langts
import {defineProps, defineEmits, defineExpose, ref} from vue;// 父组件传过来的值,带个问号表示可选
const props defineProps{value: string
}()// JS中获取父组件传过来的值
console.log(props.value)// 点击按钮触发父组件的自定义事件
const changeParentCount () {emit(changeCount, 10)
}// 触发父组件的自定义事件
const emit defineEmits([changeCount])// 定义对外暴露的属性
let order ref(10)
const open () {console.log(open)
}
// 使用defineExpose暴露出去
defineExpose({order,open
})
/scriptstyle scoped
.children-box{border: 1px solid #ccc;padding: 30px;
}
/style实现瀑布流布局
父组件
templateWaterfallFlow :listlist/
/templatescript setup langtsimport WaterfallFlow from /components/WaterfallFlow.vue;
import {reactive} from vue;
type listType {height:number,color:string
}
// 随机生成100个高度和颜色的对象
let list reactivelistType[]([...Array.from({length:100},()({height:Math.floor(Math.random()*250)50,color:rgb(${Math.floor(Math.random()*255)},${Math.floor(Math.random()*255)},${Math.floor(Math.random()*255)})}))])
/script子组件
templatediv classwrapsdiv v-foritem in list classitem :style{left: item.left px,top: item.top px,height: item.height px,backgroundColor: item.color,}/div/div
/templatescript setup langts
import {defineProps, onMounted} from vueconst props defineProps{list: any[]
}()const initLayout () {// 上下左右间隙距离let margin 10// 每个元素的宽度let elWidth 120 margin// 每行展示的列数let colNumber Math.floor(document.querySelector(.app-content).clientWidth / elWidth)// 存放元素高度的listlet heightList []// 遍历所有元素for (let i 0; i props.list.length; i) {let el props.list[i]// i小于colNumber表示第一行元素if(i colNumber){el.top 0el.left elWidth * iheightList.push(el.height)}else{// 找出最小的高度let minHeight Math.min(...heightList)// 找出最小高度的索引let minHeightIndex heightList.indexOf(minHeight)// 设置元素的位置el.left elWidth * minHeightIndexel.top minHeight margin// 更新高度集合heightList[minHeightIndex] minHeight el.height margin}}
}// 监听app-content元素的宽度变化
window.onresize () {initLayout()
}onMounted(() {initLayout()
})
/scriptstyle scoped langscss
.wraps{height: 100%;position: relative;.item{position: absolute;width: 120px;}
}
/style效果展示 组件递归
实现一个如下的东西 父组件
templateTreeVue :treeDatatreeData/
/templatescript setupimport {reactive} from vue
import TreeVue from /components/TreeVue.vue;let treeData reactive([{label:1,checked:false,children:[{label:1-1,checked:false,},{label: 1-2,checked:true,}]},{label:2,checked:false,children: [{label: 2-1,checked:false,children:[{label: 2-1-1,checked:false,children:[{label: 2-1-1-1,checked:false,}]}]}]},{label:3,checked:false,}
])/scriptTreeVue.vue
templatediv v-foritem in treeData stylemargin-left: 15px click.stopgetCurrNode(item,$event)input typecheckbox v-modelitem.checked/span{{item.label}}/spanTreeVue v-ifitem.children :tree-dataitem.children//div
/templatescript setup
import {defineProps} from vue
defineProps([treeData])const getCurrNode (currNode,event) {console.log(currNode)console.log(event)
}
/script控制台打印的东西 动态组件 templatediv styledisplay: flex;gap: 15pxdiv v-for(item,index) in tabData :keyindexclasstab-item:class{active:active index}clickswitchCom(item,index)div{{item.tab}}/div/div/divcomponent :iscurrCom/component
/templatescript setup
import {reactive, ref, shallowRef,markRaw} from vue
import ComA from /components/13/ComA.vue
import ComB from /components/13/ComB.vue
import ComC from /components/13/ComC.vue// 使用shallowRef避免深层相应
let currCom shallowRef(ComA)
let active ref(0)let tabData reactive([{tab:组件A,// 使用markRaw使组件不会被vue进行响应式处理提高性能com:markRaw(ComA)},{tab:组件B,com:markRaw(ComB)},{tab:组件C,com:markRaw(ComC)}
])const switchCom (item,index) {currCom.value item.comactive.value index
}
/scriptstyle scoped
.tab-item{padding: 5px 15px;border: 1px solid black;
}
.active{background-color: deepskyblue;
}
/style插槽
定义子组件
templatediv classboxdiv classheaderslot nameheader/slot/divdiv classmain!--默认插槽--slot :linklink :ageage/slot/divdiv classfooterslot namefooter/slot/div/div
/template
script setup langts
import {ref} from vue;const link ref(Tome)
const age ref(18)
/scriptstyle scoped langscss
// 父元素高度100%
.box{height: 100%;display: flex;flex-direction: column;
}
.header {height: 100px;background: pink;width: 100%;
}
.main{flex: 1;background-color: #c6e2ff;
}
.footer{height: 100px;background: blueviolet;width: 100%;
}
/style定义父组件
templateDialogtemplate #header具名插槽-header/templatetemplate #default{link,age}这是默认插槽{{link}} -- {{age}}/templatetemplate #footer具名插槽-footer/template/Dialog
/templatescript setup langts
import Dialog from /components/14/Dialog.vue
/script效果 异步组件
添加骨架屏组件
Skeleton.vue
templateel-skeleton style--el-skeleton-circle-size: 100pxtemplate #templateel-skeleton-item variantcircle //template/el-skeletonbr /el-skeleton /
/template效果是这个样子 添加新闻组件
添加新闻数据在 public 文件夹中添加 newinfo.json
[{title: 秋粮陆续成熟 多措并举保粮食丰收,description: 眼下从南到北各地秋粮陆续成熟。人们全力以赴抓好秋粮生产多措并举保粮食丰收。\n\n金秋时节安徽水稻主产区无为市85万亩水稻进入收割期当地组织机械作业服务队帮助农民机耕机收颗粒归仓。今年安徽计划投入各类农机具240万台套力争玉米、大豆、中晚稻机收水平达八成以上。,url: https://baijiahao.baidu.com/s?id1777244368223895628,image: https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/picgo/img.png}
]引入 axios请求这个文件
src/api/index.js
import axios from axiosexport function getNewDataFun(){return axios(../public/newinfo.json)
}编写组件 NewCar.vue
templatediv v-foritem in newData classnew-boxdiv classimageimg :srcitem.image alt//divdiv classcontentdiv classtitle{{item.title}}/divdiv classdesc{{item.description}}/div/div/div
/templatescript setup langts
import {onMounted, reactive, ref} from vue;
import {getNewDataFun} from /api/index;type dataType {title:string,description:string,url:string,image:string,
}const newData refdataType[]([])await getNewDataFun().then(res{setTimeout((){newData.value res.data},2000)
})
/scriptstyle scoped langscss
.new-box{display: flex;gap: 15px;.image{width: 200px;border-radius: 5px;overflow: hidden;img{width: 100%;}}.content{width: 80%;display: flex;flex-direction: column;gap: 10px;.title{font-weight: 700;font-size: 16px;}}
}
/style效果展示 使用异步组件
Suspense 是vue内置的一个组件有两个插槽
default默认插槽展示等待结果返回后的组件fallback等待过程中展示的组件
templatediv stylepadding: 20pxSuspensetemplate #defaultNewCar//templatetemplate #fallbackSkeleton//template/Suspense/div
/templatescript setup langts
import {defineAsyncComponent} from vueimport Skeleton from /components/15/Skeleton.vueconst NewCar defineAsyncComponent(()import(/components/15/NewCar.vue))
/script异步组件必须使用 defineAsyncComponent 函数来导入接收一个回调函数 TelePore传送组件
自定义一个弹框组件
templatediv classzx-dialogslot/slotdiv classzx-dialog__footerslot namefooter/slot/div/div
/templatestyle scoped langscss
include b(dialog){width: 200px;height: 200px;position: absolute;left: 50%;top: 50%;margin-left: -100px;margin-top: -100px;border: 1px solid #ccc;background-color: #c6e2ff;include e(footer){position: absolute;bottom: 0;left: 0;right: 0;height: 50px;line-height: 50px;text-align: right;}
}
/style使用TelePore
父组件使用这个组件
templatediv classboxel-button typeprimary clickswitchDialog !switchDialog打开Dialog/el-button!--使用teleport将组件渲染到body标签下面避免受到父组件的position: absolute;定位影响--teleport tobody v-ifswitchDialogMyDialogtemplate #default弹框内容/templatetemplate #footerel-button clickswitchDialog !switchDialog关闭/el-button/template/MyDialog/teleport/div
/templatescript setup
import {ref} from vue
import MyDialog from /components/MyDialog.vue;let switchDialog ref(false);/scriptstyle scoped
.box{width: 100%;height: 50%;background-color: gold;position: absolute;}
/style效果 KeepAlive
可以缓存组件内容
默认使用
切换组件显示后组件内容不会丢失
templatedivdivel-button typeprimary clickswitchFlag切换组件/el-button/divkeep-aliveAliveA v-ifflag/AliveB v-else//keep-alive/div
/templatescript setup
import {ref} from vue;
import AliveA from /components/AliveA.vue;
import AliveB from /components/AliveB.vue;let flag ref(true)const switchFlag () {flag.value !flag.value
}
/scriptincludes
只缓存AliveA组件
keep-alive :include[AliveA]AliveA v-ifflag/AliveB v-else/
/keep-aliveexclude
不缓存AliveA组件
keep-alive :exclude[AliveA]AliveA v-ifflag/AliveB v-else/
/keep-alivemax
最多缓存的组件个数
keep-alive :max10AliveA v-ifflag/AliveB v-else/
/keep-alivekeep-alive的钩子函数
script langts setup
import { ref,onMounted,onActivated,onDeactivated,onUnmounted, } from vue// 组件显示时只会触发一次
onMounted((){console.log(mounted)
})// 组件显示时触发
onActivated((){console.log(activated)
})
// 组件隐藏时触发
onDeactivated((){console.log(deactivated)
})
// 被keepalive包裹时组件销毁不会触发unmounted
onUnmounted((){console.log(unmounted)
})transition
基本用法
在进入/离开的过渡中会有 6 个 class 切换。
v-enter-from定义进入过渡的开始状态。在元素被插入之前生效在元素被插入之后的下一帧移除。
v-enter-active定义进入过渡生效时的状态。在整个进入过渡的阶段中应用在元素被插入之前生效在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间延迟和曲线函数。
v-enter-to定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from 被移除)在过渡/动画完成之后移除。
v-leave-from定义离开过渡的开始状态。在离开过渡被触发时立刻生效下一帧被移除。
v-leave-active定义离开过渡生效时的状态。在整个离开过渡的阶段中应用在离开过渡被触发时立刻生效在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间延迟和曲线函数。
v-leave-to离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from 被移除)在过渡/动画完成之后移除。
template
divel-button typeprimary clickflag !flag切换/el-buttontransition namefadediv classbox v-ifflag/div/transition
/div
/templatescript setup
import {ref} from vue;let flag ref(true)
/scriptstyle scoped langscss
.box{width: 200px;height: 200px;background-color: red;
}//开始过度.fade-enter-from{background:red;width:0px;height:0px;transform:rotate(360deg)}
//开始过度了.fade-enter-active{transition: all 1s ease;}
//过度完成.fade-enter-to{background:yellow;width:200px;height:200px;}
//离开的过度.fade-leave-from{width:200px;height:200px;transform:rotate(360deg)}
//离开中过度.fade-leave-active{transition: all 1s linear;}
//离开完成.fade-leave-to{width:0px;height:0px;}
/style结合animate
安装
npm install animate.css -D官网中有很多动画示例 Animate.css | A cross-browser library of CSS animations.
templatediv classroot-boxdiv classapp-menuMenu //divdiv classapp-content!-- 路由出口 --!-- 路由匹配到的组件将渲染在这里 --router-view v-slot{ Component,route }transition enter-active-classanimate__animated animate__fadeInUp!-- 这里加一个div是防止页面没有根组件时动画失效 --div :keyroute.name styleheight: 100%component :isComponent //div/transition/router-view/div/div
/template
script setup
import Menu from ./Menu.vue
import animate.css;
// 设置所有动画的时间在0.3秒内完成
document.documentElement.style.setProperty(--animate-duration, 0.3s);
/scriptstyle scoped langless
.root-box {display: flex;width: 100%;height: 100vh;.app-menu {width: 200px;height: 100vh;background-color: #a18cd1;text-overflow: ellipsis;white-space: nowrap;overflow: auto;}.app-content {flex: 1;height: 100vh;background-color: white;overflow: auto;}
}
/styletranstion生命周期
transition before-enterbeforeEnter enterenter leaveleavediv v-ifgsapFlag classgsap-box/div
/transitionbefore-enterbeforeEnter //对应enter-fromenterenter//对应enter-activeafter-enterafterEnter//对应enter-toenter-cancelledenterCancelled//显示过度打断before-leavebeforeLeave//对应leave-fromleaveleave//对应enter-activeafter-leaveafterLeave//对应leave-toleave-cancelledleaveCancelled//离开过度打断结合gsap
安装官网https://greensock.com/
npm install gsap使用
html
el-button typeprimary clickgsapFlag !gsapFlag切换/el-button
transition before-enterbeforeEnter enterenter leaveleavediv v-ifgsapFlag classgsap-box/div
/transitionjs
script setup
import gsap from gsap;
import {ref} from vue;let gsapFlag ref(true)const beforeEnter (el) {console.log(显示之前)gsap.set(el,{width:0,height:0,background:green})
}
const enter (el,done) {gsap.to(el,{width:200px,height:200px,background:red,rotate:360dge,duration:1, // 动画时长单位是秒onComplete:done, // 动画完成后的回调函数})
}
const leave (el,done) {gsap.to(el,{width:0,height:0,background:green,rotate:-360dge,duration:1, // 动画时长单位是秒onComplete:done})
}效果 appear属性
在 transtion 组件中添加 appear 可以在进入页面时就触发对应的样式代码
appear-class初始样式appear-to-class结束样式appear-active-class动画曲线
transition appear appear-class appear-to-class appear-active-classanimate__animated animate__rubberBand namefadediv classbox v-ifflag/div
/transition结合animate__animated实现一个进入页面就执行的一个动画效果 transition-group
在遍历数组的时候可以给每一个元素添加过度动画生命周期和transition一致我们结合animate来实现一个列表的动画效果 divel-button typeprimary clickaddadd/el-buttonel-button typedanger clickpoppop/el-button/divdiv classwarptransition-groupenter-active-classanimate__animated animate__bounceInLeftleave-active-classanimate__animated animate__fadeOutRightdiv v-foritem in groupList :keyitem classitem{{item}}/div/transition-group/divimport {ref,reactive} from vue;
import animate.cssconst groupList reactive([1,2,3,4,5])const add () {groupList.push(groupList.length 1)
}
const pop () {groupList.pop()
}动画效果 实现一个炫酷的动画效果
安装lodash库 Lodash 简介 | Lodash中文文档 | Lodash中文网 (lodashjs.com)
npm i --save lodash实现代码
div stylemargin-top: 20px平面动画过度效果/div
el-button typeprimary clickshuffle动画/el-button
div classnum-wraptransition-group move-classmove-classdiv v-foritem in numList :keyitem.id classnum-item{{item.value}}/div/transition-group
/divimport {ref,reactive} from vue;
import _ from lodashlet numList ref(Array.apply(null, {length: 81}).map((_,index){return {id:index,value:(index % 9) 1}
}))const shuffle () {// shuffle 用来创建一个被打乱值的集合numList.value _.shuffle(numList.value)
}$numWidth:60px;.move-class{transition: all 1s ease;
}
.num-wrap{display: flex;flex-wrap: wrap;width: calc(#{$numWidth} * 9 5px * 8);gap: 5px;.num-item{width: $numWidth;height: $numWidth;line-height: $numWidth;text-align: center;border: 1px solid #ccc;}
}实现效果 使用gsap实现数字滚动
div stylemargin-top: 20px;height: 2px使用gsap实现数字滚动/div
el-input v-modelrolling.num placeholderplaceholder stylewidth: 200px/el-input
h1{{rolling.numRul.toFixed(0)}}
/h1import gsap from gsap;
import {ref,reactive,watch} from vue;let rolling reactive({num:10,numRul:10
})
watch(()rolling.num,(newVal){gsap.to(rolling,{numRul:newVal,duration:1,})
})依赖注入provide和inject
爷爷组件
templateh1爷爷组件/h1el-button typeprimary clicksetColor(red)红色/el-buttonel-button typeprimary clicksetColor(blue)蓝色/el-buttonel-button typeprimary clicksetColor(pink)粉色/el-buttondiv classbox/divhrProvideA/hrProvideB//templatescript setup langts
import {provide, inject, ref} from vue
import ProvideA from /components/ProvideA.vue;
import ProvideB from /components/ProvideB.vue;let color ref(red)provide(color,color)const setColor (c) {color.value c
}/scriptstyle scoped
.box{width: 100px;height: 100px;background-color: v-bind(color);
}
/styleProvideA
templatedivh1爸爸组件/h1div classbox/div/div
/templatescript setup langts
import {inject} from vue
import type {Ref} from vue
let color:Refstring inject(color)
/scriptstyle scoped
.box{width: 100px;height: 100px;background-color: v-bind(color);
}
/styleProvideB
templatedivh1孙子组件/h1div classbox/divbutton clicksetColor设置粉色/button/div
/templatescript setup langts
import {inject} from vue
import type {Ref} from vue
let color:Refstring inject(color)const setColor () {color.value pink
}
/scriptstyle scoped
.box{width: 100px;height: 100px;background-color: v-bind(color);
}
/style实现效果 兄弟传参
Mitt
安装
npm install mitt局部使用
添加一个JS文件导出
utils/mitt.js
import mitt from mitt
export default mitt()使用分别定义 A B两个组件
BusA
templatediv classbox我是A组件el-button typeprimary clickchangeFlag改变/el-button/div
/templatescript setup
import mitt from ../utils/mitt
import {ref} from vue;let flag ref(false)
const changeFlag () {flag.value !flag.valuemitt.emit(changeFlag,flag.value)
}/scriptBusB
templatediv classbox我是B组件{{flag}}/div
/templatescript setup
import mitt from ../utils/mitt;
import {ref,onBeforeUnmount} from vue;
let flag ref(false)mitt.on(changeFlag, data{flag.value data
})onBeforeUnmount((){mitt.off(changeFlag)
})
/script在父组件引入
templateBusA/BusB/
/templatescript setup
import BusA from /components/BusA.vue;
import BusB from /components/BusB.vue;/script效果 全局使用
main文件添加
import ./assets/main.css
import { createApp } from vue
import { createPinia } from pinia
import ElementPlus from element-plus
import App from ./App.vue
import router from ./router
import element-plus/dist/index.css
import zhCn from element-plus/dist/locale/zh-cn.min.js
import dayjs/locale/zh-cn import mitt from mittconst Mitt mitt()const app createApp(App)
app.use(createPinia())
app.use(router)
app.use(ElementPlus, { locale: zhCn })
app.mount(#app) declare module vue{export interface ComponentCustomProperties {$Bus: typeof Mitt}}app.config.globalProperties.$bus Mitt文件内部通过从 vue 中导出 getCurrentInstance 方法获取当前实例获取定义的全局变量使用
BusA
templatediv classbox我是A组件el-button typeprimary clickchangeFlag改变/el-button/div
/templatescript setup
import {ref,getCurrentInstance} from vue;let flag ref(false)
let instance getCurrentInstance()const changeFlag () {flag.value !flag.valueinstance?.proxy?.$bus.emit(changeFlag,flag.value)
}
/scriptBusB
templatediv classbox我是B组件{{flag}}/div
/templatescript setup
import {ref, onBeforeUnmount, getCurrentInstance} from vue;
let flag ref(false)
let instance getCurrentInstance()instance?.proxy?.$bus.on(changeFlag, data{flag.value data
})onBeforeUnmount((){instance?.proxy?.$bus.off(changeFlag)
})
/script手写Bus
class MyBus{constructor() {this.list {}}emit(event, ...args){let funs this.list[event]funs.forEach((fun) {fun.apply(this,args)})}on(event, callback){let funs this.list[event]if(funs){funs.push(callback)}else{funs [callback]}this.list[event] funs}off(event){delete this.list[event]}
}
export default new MyBus()jsx插件
安装
npm in stall vitejs/plugin-vue-jsx -D在 vite.config.js 中使用
import { fileURLToPath, URL } from node:urlimport { defineConfig } from vite
import vue from vitejs/plugin-vue
import vueJsx from vitejs/plugin-vue-jsx
import AutoImport from unplugin-auto-import/vite
import Components from unplugin-vue-components/vite
import { ElementPlusResolver } from unplugin-vue-components/resolvers// https://vitejs.dev/config/
export default defineConfig({module:es2022,plugins: [vue(),vueJsx(),AutoImport({resolvers: [ElementPlusResolver()],}),Components({resolvers: [ElementPlusResolver()],}),],resolve: {alias: {: fileURLToPath(new URL(./src, import.meta.url))}},css: {preprocessorOptions: {scss: {additionalData: import ./src/layout_v2/css/bem.scss;}}}
})新建 JsxCom.tsx
import {defineComponent, reactive, ref} from vue
import {ElButton} from element-plusinterface propType {msg?:string
}export default defineComponent({props:{msg:String,},emits:[],setup(prop:propType,{emit,attrs,slots,expose}){let flag ref(false)const chagneFlag () {flag.value true}let list reactive([1,2,3,4,5])return () {/*遍历循环*/}{list.map(item h1{item}/h1)}hr/{/*按钮事件使用οnclick{()chagneFlag()}*/}ElButton typeprimary οnclick{()chagneFlag()}改变这个值/ElButton{flag.value h1改变后的值/h1}hr/div父组件传递的值{prop.msg}/div/},
})在vue中可以把这个当成普通的组件使用
templateJsxCom msgHello Jsx/
/template
script setup
import JsxCom from ../components/JsxCom
/script页面效果 自动引入插件
安装
npm istall unplugin-auto-import/vite配置
import AutoImport from unplugin-auto-import/viteexport default defineConfig({module:es2022,plugins: [vue(),vueJsx(),AutoImport({resolvers: [ElementPlusResolver()],imports: [vue, vue-router], // 自动引入vue和vue-router相关dts: src/auto-imports.d.ts // 自动生成的依赖文件}),Components({resolvers: [ElementPlusResolver()],}),],})保存后查看 src/auto-imports.d.ts 内容 里面自动的帮我们了引入
然后再组件中不需要手动的导入 vue就可以使用vue中的各种声明
templateel-button typeprimary clickflag !flagbuttonCont/el-buttondiv{{flag}}/div
/template
script setup
let flag ref(false)
/scriptv-model在组件中的使用
基本使用
vue3中在组件上绑定v-model时默认的prop变成了modelValue
子组件 Vmodel
templatedivel-input v-modelinput placeholderplaceholder inputchangeValue stylewidth: 200px/el-inputel-button关闭/el-button/div
/templatescript setup langts
import {defineProps,defineEmits} from vueconst props defineProps{modelValue:string,
}()
// 更新model绑定的值固定写法: update:modelValue
const emit defineEmits([update:modelValue])let input ref()onMounted((){input.value props.modelValue
})const changeValue (e) {// 修改父组件的值emit(update:modelValue,e)
}/script父组件
template父组件的值{{value}}div classboxVmodel v-modelvalue //div
/templatescript setup langts
import Vmodel from /components/Vmodel.vue;let value ref(你好)
/scriptstyle scoped
.box{border: 2px solid black;padding: 30px;
}
/style绑定多个v-model
父组件
template父组件的值{{value}}el-button clickisShow !isShow切换显示/el-buttondiv classboxVmodel v-modelvalue v-model:isShowisShow//div
/templatescript setup langts
import Vmodel from /components/Vmodel.vue;let value ref(你好)
let isShow ref(true)
/scriptstyle scoped
.box{border: 2px solid black;padding: 30px;
}
/style子组件
templatediv v-ifisShowel-input v-modelinput placeholderplaceholder inputchangeValue stylewidth: 200px/el-inputel-button clickclose关闭/el-button/div
/templatescript setup langts
import {defineProps,defineEmits} from vueconst props defineProps{modelValue:string,isShow:boolean
}()
// 更新model绑定的值固定写法: update:modelValue
const emit defineEmits([update:modelValue,update:isShow])let input ref()onMounted((){input.value props.modelValue
})const changeValue (e) {// 修改父组件的值emit(update:modelValue,e)
}const close () {emit(update:isShow,false)
}
/script自定义指令
自定义指令的声明周期
templatediv classbox v-resizeonResize/div
/templatescript setup langts
import { Directive } from vueconst onResize () {console.log(宽高变化)
}// 声明一个局部自定义指令必须以v开头
const vResize:Directive {created(){console.log(created)},beforeMount(){console.log(beforeMount)},mounted(...arg){console.log(mounted)console.log(arg)},beforeUpdate(){console.log(beforeUpdate)},updated(){console.log(updated)},beforeUnmount(){console.log(beforeUnmount)},unmounted(){console.log(unmounted)}
}
/scriptstyle scoped
.box{height: 100%;background-color: #f5f5f5;
}
/style在任意一个钩子函数头能拿到自定义指令绑定的参数我们通过打印 arg 看看参数有什么 我们利用这两个参数实现监听元素宽高变化的指令当元素宽高发生变化时调用绑定的函数
mounted(el,bindings){console.log(mounted)// 监听元素宽高变化const resizeObserver new ResizeObserver(entries {let width entries[0].contentRect.width;let height entries[0].contentRect.height;console.log(元素宽度${width},元素高度${height})bindings.value()});resizeObserver.observe(el);
},修改 mounted 钩子的内容通过observe 观察 el然后调用 bindings.value 自定义指令的简写方式
我们也可以通过函数的方式来自定义指令
templateel-button v-has-showorder:add typeprimary新增/el-buttonel-button v-has-showorder:update typewarning修改/el-buttonel-button v-has-showorder:delete typedanger删除/el-button
/templatescript setup langts
import { Directive } from vue
//permission
localStorage.setItem(userId,songzx)//mock后台返回的数据
const permission [// songzx:order:add,songzx:order:update,songzx:order:delete
]const userId localStorage.getItem(userId) as stringconst vHasShow:DirectiveHTMLElement,string (el,binding){if(!permission.includes(${userId}:${binding.value})){// 直接移除这个元素比使用 el.style.display none 更安全el.remove()}
}/script上面的例子是一个按钮级别权限的demo
鼠标拖动元素案例
templatediv classrootdiv classbox v-movediv classheader/div/div/div/templatescript setup langts
import { Directive } from vueconst vMove:DirectiveHTMLElement (el){let moveEl:HTMLElement el.querySelector(.header)const mousedown (e:MouseEvent) {// 鼠标按下时获取当前鼠标的位置和移动物体相对于浏览器的位置let X e.x - el.offsetLeftlet Y e.y - el.offsetTop// 移动const move (e:MouseEvent){// 在移动物体时需要减去偏移量el.style.left e.clientX - X pxel.style.top e.clientY - Y px}document.addEventListener(mousemove, move)document.addEventListener(mouseup, (){document.removeEventListener(mousemove, move)})}// 鼠标按下头部时触发moveEl.addEventListener(mousedown,mousedown)
}/scriptstyle scoped langscss
.root{position: relative;
}
.box{position: absolute;width: 200px;height: 200px;left: 100px;top: 100px;border: 2px solid black;.header{width: 100%;height: 20px;background-color: black;}
}
/style图片懒加载案例
templatediv classrootimg v-foritem in arr v-lazyitem stylewidth: 100%;//div/templatescript setup langts
import { Directive,DirectiveBinding } from vue// import.meta.glob 引入目标路径中的所有文件返回一个对象默认使用module引入
// 添加了eager: true则变成同步引入
let imgList import.meta.glob(../assets/images/*.*,{eager: true})
// 得到所有图片地址
let arr Object.values(imgList).map(itemitem.default)// 自定义懒加载指令
const vLazy:Directive async (el:HTMLImageElement,binding:DirectiveBinding){// 先给一个默认值const def await import(../assets/logo.svg)el.src def.default// 判断元素是否在可视范围内const intersection new IntersectionObserver((e){// 判断是否在可视范围内if(e[0].intersectionRatio 0){setTimeout((){el.src binding.value},500)intersection.unobserve(el)}})intersection.observe(el)
}/scriptstyle scoped langscss
.root{width: 361px;height: 800px;overflow: auto;
}
/style自定义Hook
好用的第三方库
vueuse
npm i vueuse/core网址Get Started | VueUse — 开始使用 |Vueuse
图片转base64
新建 useImgToBase64.ts
import {onMounted} from vuetype optionsType {el:String
}export default function (options:optionsType):Promisestring{return new Promise((resolve, reject) {onMounted((){let img:HTMLImageElement document.querySelector(options.el)img.onload (){resolve(toBase64(img))}const toBase64 (img:HTMLImageElement) {let canvas:HTMLCanvasElement document.createElement(canvas)let ctx:CanvasRenderingContext2D canvas.getContext(2d)canvas.width img.widthcanvas.height img.heightctx.drawImage(img, 0, 0, canvas.width, canvas.height)return canvas.toDataURL(image/jpeg)}})})
}使用
templateimg src../assets/images/1.jpeg/
/templatescript setup langts
import useImgToBase64 from /utils/useImgToBase64;useImgToBase64({el:img}).then(res{console.log(res)
})
/script自定义Vite库并发布到NPM
封装useResize
用于监听绑定元素的宽高变化当元素宽高发生变化时触发回调并获取最新的宽高
新建项目
结合上面学到的 Hook 和 自定义指令封装一个监听元素宽高变化的指令并发布到 npm
项目结构
useResize
├── src
│ └── index.ts
├── README.md
├── index.d.ts
├── package-lock.json
├── package.json
├── tsconfig.json
└── vite.config.tssrc/index.ts
import type {App} from vue;/*** 自定义Hook* param el* param callback*/
const weakMap new WeakMapHTMLElement, Function();
const resizeObserver new ResizeObserver((entries) {for (const entry of entries) {const handle weakMap.get(entry.target as HTMLElement);handle handle(entry)}
})function useResize(el: HTMLElement, callback: Function) {if (weakMap.get(el)) {return}weakMap.set(el, callback)resizeObserver.observe(el)
}/*** 定义vite插件时vue会在底层调用插件的install方法* param app*/
function install(app: App) {app.directive(resize, {mounted(el: HTMLElement, binding: { value: Function }) {useResize(el, binding.value)}})
}useResize.install installexport default useResizevite.config.ts
import {defineConfig} from viteexport default defineConfig({build:{lib:{// 打包入口文件entry:src/index.ts,// namename:useResize},rollupOptions:{// 忽略打包的文件external:[vue],output:{globals:{useResize:useResize}}}}
})index.d.ts
declare const useResize:{(element:HTMLElement, callback:Function):voidinstall:(app:any) void
}export default useResizepackage.json
{name: v-resize-songzx,version: 1.0.0,description: ,main: dist/v-resize-songzx.umd.js,module: dist/v-resize-songzx.mjs,scripts: {test: echo \Error: no test specified\ exit 1,build: vite build},keywords: [],author: songzx,files: [dist,index.d.ts],license: ISC,devDependencies: {vue: ^3.3.4},dependencies: {vite: ^4.4.9}
}pachage.json 文件属性说明
name对应打包后生成的包名也就是上传到npm上面的包名不能包含数字和特殊符号version包的版本号main对应打包后的 umd.js 文件在使用 app.use 时会访问使用文件module使用import、require等方式引入时会使用 mjs 文件files指定那些文件需要上传
打包
npm run build登录npm
npm login发布
npm publish打开 npm 网站搜索查看是否发布成功 使用自己的库
安装
npm i v-resize-songzx使用方式一
全局注册 v-resze 指令
main.ts 引入
import useResize from v-resize-songzx;const app createApp(App)app.use(useResize)
app.mount(#app)templatediv classresize v-resizegetNewWH/div
/templatescript setup langts
const getNewWH (e) {console.log(e.contentRect.width, e.contentRect.height);
}/scriptstyle scoped
/*把一个元素设置成可以改变宽高的样子*/
.resize {resize: both;width: 200px;height: 200px;border: 1px solid;overflow: hidden;
}
/style使用方式二
使用Hook的方式
templatediv classresize/div
/templatescript setup langtsimport useResize from v-resize-songzx;onMounted(() {useResize(document.querySelector(.resize), e {console.log(e.contentRect.width, e.contentRect.height);})
})/scriptstyle scoped
/*把一个元素设置成可以改变宽高的样子*/
.resize {resize: both;width: 200px;height: 200px;border: 1px solid;overflow: hidden;
}
/style定义全局变量和方法
在 main.ts 中添加
import dayjs from dayjs
import mitt from mittconst Mitt mitt()// 定义全局变量
app.config.globalProperties.$bus Mitt
app.config.globalProperties.$BaseUrl http://localhost
app.config.globalProperties.$formatDate (date: Date) dayjs(date).format(YYYY-MM-DD HH:mm:ss)// 定义声明文件
declare module vue {export interface ComponentCustomProperties {$bus: typeof Mitt,$BaseUrl: string,$formatDate: Date}
}在任何组件中都可以使用
templatediv{{ $BaseUrl }}/div
/templatescript setup langts
import {getCurrentInstance} from vue
// 获取当前实例
const instance getCurrentInstance()console.log(instance.proxy.$BaseUrl) // http://localhost
console.log(instance.proxy.$formatDate(new Date())) // 2023-09-25 13:51:23/script自定义插件之全局Loading
ElementPlus的默认全局Loading
如果完整引入了 Element Plus那么 app.config.globalProperties 上会有一个全局方法$loading同样会返回一个 Loading 实例。
名称说明类型默认targetLoading 需要覆盖的 DOM 节点。 可传入一个 DOM 对象或字符串 若传入字符串则会将其作为参数传入 document.querySelector以获取到对应 DOM 节点string / HTMLElementdocument.bodybody同 v-loading 指令中的 body 修饰符booleanfalsefullscreen同 v-loading 指令中的 fullscreen 修饰符booleantruelock同 v-loading 指令中的 lock 修饰符booleanfalsetext显示在加载图标下方的加载文案string—spinner自定义加载图标类名string—background遮罩背景色string—customClassLoading 的自定义类名string—
指令的方式使用
templatediv classbox v-loadingisLoadingcontent/divel-button typeprimary clickshowDivLoading显示loading/el-button
/templatescript setup langts
// 显示局部loading
let isLoading ref(false)const showDivLoading () {isLoading.value !isLoading.value
}/scriptstyle scoped
.box {width: 200px;height: 200px;border: 1px solid;
}
/style函数式调用
templateel-button typeprimary clickshowLoadingshowLoading/el-button
/templatescript setup langts
import {getCurrentInstance} from vue
// 获取当前实例
const {proxy} getCurrentInstance()// 显示全局loading
const showLoading () {const loading proxy.$loading()setTimeout(() {loading.close()}, 2000)
}
/script自定义全局Loading
我们自己动手来实现一个和ElementPlus的Loading同时支持函数调用和指令调用
添加MyLoading.vue
templatetransition enter-active-classanimate__animated animate__fadeInleave-active-classanimate__animated animate__fadeOutdiv classroot-box v-ifshowdiv classwrapimg src../assets/images/loading.gif//div/div/transition
/templatescript setup
let show ref(false)const showLoading () {show.value true
}
const hideLoading (callback) {show.value falsecallback setTimeout(() callback(), 500)
}defineExpose({show,showLoading,hideLoading
})/scriptstyle scoped langscss
.animate__animated.animate__fadeIn {--animate-duration: 0.5s;
}.animate__animated.animate__fadeOut {--animate-duration: 0.5s;
}.root-box {position: absolute;left: 0;top: 0;bottom: 0;right: 0;margin: 0;background-color: rgba(255, 255, 255, 0.9);z-index: 2000;display: flex;justify-content: center;align-items: center;.wrap {width: 100px;height: 100px;display: flex;justify-content: center;align-items: center;overflow: hidden;img {width: 100%;transform: scale(2.5);}}
}
/style添加MyLoading.ts
import type {App, VNode,} from vue
import {createVNode, render, cloneVNode} from vue
import MyLoading from /components/MyLoading.vueexport default {install(app: App) {// 使用vue底层的createVNode方法将组件渲染为虚拟节点const VNode: VNode createVNode(MyLoading)// 使用render函数将组件挂载到body中render(VNode, document.body)// 定义全局方法设置组件的显示和隐藏app.config.globalProperties.$showLoading VNode.component?.exposed.showLoadingapp.config.globalProperties.$hideLoading VNode.component?.exposed.hideLoadingconst weakMap new WeakMap()// 自定义Loading指令app.directive(zx-loading, {mounted(el) {if (weakMap.get(el)) return// 记录当前绑定元素的positionweakMap.set(el, window.getComputedStyle(el).position)},updated(el: HTMLElement, binding: { value: Boolean }) {const oldPosition weakMap.get(el);// 如果不是position: relative或者absolute就设置为relative// 这里的目的是确保loading组件正确覆盖当前绑定的元素if (oldPosition ! absolute oldPosition ! relative) {el.style.position relative}// 克隆一份loading元素,// 作用是当页面上有多个zx-loading时每个dom都维护一份属于自己的loading不会冲突const newVNode cloneVNode(VNode)// 挂载当前节点render(newVNode, el)// 判断绑定的值if (binding.value) {newVNode.component?.exposed.showLoading()} else {newVNode.component?.exposed.hideLoading(() {// 还原布局方式el.style.position oldPosition})}}})}
}在上面的文件中定义了两个全局函数和一个自定义指令
$showLoading全局显示一个Loading$hideLoading关闭全局的Loadingzx-loading自定义指令
在main.ts中挂载
在 main.ts 中去挂载我们自定义的 Loading
import {createApp} from vue
import MyLoading from /utils/MyLoading;const app createApp(App)
// 引入自定义的全局Loading
app.use(MyLoading)app.mount(#app)使用方法一函数式使用
调用全局方法弹出Loading
template!--自定义全局loading--el-button typeprimary clickshowMyLoading显示自定义的全局loading/el-button
/templatescript setup langts
import {getCurrentInstance} from vue
// 获取当前实例
const {proxy} getCurrentInstance()// 全局显示自定义loading
const showMyLoading () {proxy.$showLoading()setTimeout(() {proxy.$hideLoading()}, 2000)
}
/script使用方法二指令式使用
templatediv!--自定义的loading指令使用--div classbox v-zx-loadingisLoading指令的方式使用/divel-button typeprimary clickshowDivLoading显示loading/el-button!--自定义的loading指令使用-- div classparentdiv classchild v-zx-loadingchildLoading/div/divel-button typeprimary clickshowChildLoading显示childLoading/el-button/div
/templatescript setup langts
// 显示局部loading
let isLoading ref(false)
const showDivLoading () {isLoading.value !isLoading.value
}const childLoading ref(false)
const showChildLoading () {childLoading.value !childLoading.value
}
/scriptstyle scoped langscss
.box {width: 200px;height: 200px;border: 1px solid;
}.parent {position: relative;width: 300px;height: 300px;border: 1px solid;padding: 30px;.child {position: absolute;right: 30px;bottom: 30px;width: 200px;height: 200px;border: 1px solid;}
}
/styleuse函数源码实现
添加 MyUse.ts
import type {App} from vue
import {app} from /main// 定义一个接口声明install方法必传
interface Use {install: (app: App, ...options: any[]) void
}const installList new Set()export default function myUseT extends Use(plugin: T, ...options: any[]) {// 判断这个插件是否已经注册过了如果注册过了则报错if (installList.has(plugin)) {console.error(Plugin already installed)return}// 调用插件身上的install方法并传入main.ts导出的appplugin.install(app, ...options)installList.add(plugin)
}使用自定义的myUse方法注册我们自定义的Loading
import {createApp} from vue// 自定义全局Loading
import MyLoading from /utils/MyLoading;
// 自定义app.use方法
import myUse from /utils/MyUse;export const app createApp(App)
// 引入自定义的全局Loading
myUse(MyLoading)app.mount(#app)CSS选择器
:deep
使用 :deep() 将选择器包裹起来可以将第三方库的样式进行修改
templatedivel-input placeholderplaceholder v-modelname//div
/templatescript setup
let name ref()
/scriptstyle scoped langscss
.el-input{:deep(.el-input__inner) {background-color: red;}
}
/style:slotted
使用 :slotted() 将插槽中的类名包裹起来可以修改插槽中的元素样式
SlotTestCom.vue
templatediv父组件slot/slot/div
/templatestyle scoped
:slotted(.msg) {font-weight: bold;color: red;
}
/styleSlotTestComdiv classmsg私人订制DIV/div
/SlotTestCom:global
使用 :global() 用于设置全局样式
:global(div){font-size: 17px;color: #222222;
}全局设置div的样式
css中使用v-bind
let color ref(pink)
// 随机一个颜色
const randomColor () {color.value rgb(${Math.random() * 255},${Math.random() * 255},${Math.random() * 255})
}使用 v-bind() 将JS中变量包裹起来即可使用
.el-input {width: 300px;:deep(.el-input__inner) {background-color: v-bind(color);}
}Vue3集成Tailwind CSS
官网地址Tailwind CSS 中文文档 - 无需离开您的HTML即可快速建立现代网站。
安装
npm install -D tailwindcsslatest postcsslatest autoprefixerlatest生成配置文件
npx tailwindcss init -p修改配置文件 tailwind.config.js
2.6版本
module.exports {purge: [./index.html, ./src/**/*.{vue,js,ts,jsx,tsx}],theme: {extend: {},},plugins: [],
}3.0版本
module.exports {content: [./index.html, ./src/**/*.{vue,js,ts,jsx,tsx}],theme: {extend: {},},plugins: [],
}新建 index.css 并在 main.ts 中引入
tailwind base;
tailwind components;
tailwind utilities;基础使用
详细类名见文档https://www.tailwindcss.cn/docs/font-family
templatediv classh-full flex justify-center items-center bg-teal-400div classtext-8xl text-rose-700font-bold text-whiteHello Word/div/div
/templatenextTick vue 中更新DOM操作是异步的但是JS程序是同步的所以当遇到操作DOM时可能会出现延迟更新的情况vue 也给了一个解决方案就是可以将操作 DOM 的代码放在 nextTick 中执行nextTick 会执行一个 Promise 函数去更新DOM来实现同步更新DOM的操作 这样做的好处是可以提高程序性能例如执行一个for循环每次循环会改变变量的值然后吧这个变量输出到页面上。用一个watch去监听这个变量watch函数并不会触发多次而是只会执行一次 下面是一个小案例
templatediv classbox refboxdiv classitem v-for(item,index) in msgList :keyindex{{ item.msg }}/div/divel-input v-modelmsg stylewidth: 200px/el-button typeprimary clicksend发送/el-button
/templatescript setup langts
import {nextTick, ref, reactive} from vuelet msgList reactive([{msg: Hello world}
])
let msg ref()
let box refHTMLDivElement()const send () {msgList.push({msg: msg.value})nextTick(() {// 发送完消息后自动滚动到底部box.value.scrollTop box.value.scrollHeight})
}
/scriptstyle scoped langscss
.box {width: 300px;border: 2px solid #ddd;height: 400px;overflow: auto;.item {height: 30px;line-height: 30px;padding-left: 1em;background-color: #dddddd;margin: 2px;}
}
/styleVue3开发安卓和IOS
参照博客https://xiaoman.blog.csdn.net/article/details/131507483
安装安卓开发工具 安装完成后打开 首次运行需要安装一些SDK ionic安装
npm install -g ionic/cli初始化项目
ionic start app tabs --type vueapp 项目名称tabs 使用的预设–type vue 使用的是vue就写vuereact就写react 启动项目
npm run dev打包和构建
先执行打包命令
npm run build再执行构建命令将程序打包成Android包
ionic capacitor copy android运行成功后会自动多一个android文件夹 然后运行下面命令进行预览
ionic capacitor open android会自动打开安卓编辑器
等待项目加载完成后点击绿色的箭头即可启动 H5适配
添加meat信息
meta nameviewport contentwidthdevice-width, initial-scale1.0清除默认样式
stylehtml,body,#app{height: 100%;overflow: hidden;}*{padding: 0;margin: 0;}
/style圣杯布局
templatediv classheaderdiv/divdiv/divdiv/div/div
/templatestyle scoped langscss
.header{width: 100%;height: 50px;line-height: 50px;display: flex;div:nth-child(1),div:nth-child(3){width: 100px;background-color: deepskyblue;}div:nth-child(2){flex: 1;background-color: pink;}
}
/style使用postCSS将px单位转成vh和vw 百分比是相对于父元素 vw和vh相对于视口 编写postCSS插件
新建 plugins/PxToVwVh.ts
import {Plugin} from postcsslet Options {defaultWidth: 390,defaultHeight: 844,
}
interface OptionsTypes {defaultWidth?:number,defaultHeight?:number,
}export function PxToVwVh(options:OptionsTypesOptions):Plugin{let opt Object.assign({}, options)return {postcssPlugin:px-to-vw-vh,// 钩子函数Declaration(node){if(node.value.includes(px)){const num parseFloat(node.value)if(node.prop.includes(width)){node.value ${((num / opt.defaultWidth) * 100).toFixed(2)}vw}else if(node.prop.includes(height)){node.value ${((num / opt.defaultHeight) * 100).toFixed(2)}vh}}}}
}在 tsconfig.node.json 中引入
{extends: tsconfig/node18/tsconfig.json,include: [vite.config.*,vitest.config.*,cypress.config.*,nightwatch.conf.*,playwright.config.*,plugins/**/*],compilerOptions: {composite: true,module: ESNext,moduleResolution: Bundler,types: [node],noImplicitAny: false}
}include中添加 plugins/**/*noImplicitAny 允许隐式的使用any
使用插件
在 vite.config.ts 中使用
import { fileURLToPath, URL } from node:urlimport { defineConfig } from vite
import vue from vitejs/plugin-vue
import {PxToVwVh} from ./plugins/PxToVwVh;// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),],css: {postcss: {plugins: [PxToVwVh()]},},resolve: {alias: {: fileURLToPath(new URL(./src, import.meta.url))}}
})效果展示
我们通过编写插件实现了将PX单位转换成相对于视口这样保证了在不同尺寸的屏幕上都会有一个相同的展示布局 全局控制字体大小
设置全局CSS变量
:root{--font-size:16px;
}然后全局可以通过 var(–font-size) 使用
templatediv classheaderdiv返回/divdivH5适配/divdiv取消/div/divbutton clickchangeFontSize(15)默认/buttonbutton clickchangeFontSize(24)大/buttonbutton clickchangeFontSize(36)特大/button
/templatescript setupimport {onMounted} from vue;onMounted((){document.documentElement.style.setProperty(--font-size,localStorage.getItem(fontSize) || 16px)
})const changeFontSize (size) {document.documentElement.style.setProperty(--font-size,size px)localStorage.setItem(fontSize,size px);
}
/scriptstyle scoped langscss
.header{width: 100%;height: 50px;line-height: 50px;display: flex;text-align: center;font-size: var(--font-size);div:nth-child(1),div:nth-child(3){width: 100px;background-color: deepskyblue;}div:nth-child(2){flex: 1;background-color: pink;}
}button{margin-right: 10px;
}
/style点击按钮可以实现字体大小切换 unoCss原子化
官网https://unocss.dev/
什么是css原子化
CSS原子化的优缺点
1.减少了css体积提高了css复用
2.减少起名的复杂度
3.增加了记忆成本 将css拆分为原子之后你势必要记住一些class才能书写哪怕tailwindcss提供了完善的工具链你写background也要记住开头是bg
安装
npm i -D unocss配置插件
// vite.config.ts
import UnoCSS from unocss/vite
import { defineConfig } from viteexport default defineConfig({plugins: [UnoCSS(),],
})创建一个 uno.config.js 文件
// uno.config.js
import { defineConfig } from unocssexport default defineConfig({// 自定义规则rules:[[red,{ color:red,font-size:25px }]]
})
在 main.ts 文件中添加
// main.ts
import virtual:uno.css使用
直接在页面中使用类名即可
div classredHello Word
/div动态配置类名
rules: [[/^m-(\d)$/, ([, d]) ({ margin: ${Number(d) * 10}px })],[flex, { display: flex }]
]使用
div classred m-10Hello Word
/div使用预设
修改 uno.config.js
// uno.config.js
import { defineConfig,presetIcons,presetAttributify,presetUno } from unocssexport default defineConfig({// 自定义规则rules:[[/^m-(\d)$/, ([, d]) ({ margin: ${Number(d) * 10}px })],[red,{ color:red,font-size:25px }],],// 使用预设presets:[presetIcons(),presetAttributify(),presetUno()]
})presetIcons 这个是图标 presetAttributify 这个是美化CSS presetUno 预设实验阶段是一系列流行的原子化框架的 通用超集包括了 Tailwind CSSWindi CSSBootstrapTachyons 等。 例如ml-3Tailwindms-2Bootstrapma4Tachyonsmt-10pxWindi CSS均会生效。
使用图标
在官网中找到自己需要的图标https://icones.js.org/
然后选中后安装 查看页面路径上的单词然后安装
npm i -D iconify-json/svg-spinners点击某个要使用的图标复制类名即可 div classi-svg-spinners-bars-fade font-size-50px color-pink/divVue编译宏 首先vue版本必须是3.3及以上版本 子组件
templateel-button typeprimary clickadd添加/el-buttonulli v-foritem in props.nameList{{item}}/li/ul
/templatescript setup langts
import {defineProps,defineOptions,defineEmits,defineSlots} from vue// defineProps,可以定义类型
const props defineProps{nameList:string[]
}()const add () {emit(addName,Tome)
}
// defineEmits,可以定义事件
// 第一个参数是事件名称第二个参数是事件参数类型问号表示可选
const emit defineEmits{(event:addName,args?:any):void
}()// defineOptions常用来定义组件名字
defineOptions({name:DefineComponents
})/script父组件
templateDefineComponents :nameListnameList addNameaddName/
/templatescript setup langts
import DefineComponents from /components/DefineComponents.vue;let nameList:string[] reactive([张三,李四, 王五])const addName (args) {nameList.push(args)
}/script函数名称含义defineProps接收父组件传递过来的参数defineEmits定义事件名称defineOptions配置组件名称和其他信息
Vue环境变量
在项目根目录新建两个文件分别表示开发环境配置、生成环境配置 注意设置环境变量时必须以 VITE_ 开头否则不生效 .env.development
# .env.development
VITE_APIhttp://localhost:8080.env.production
# .env.production
VITE_API/prod-api修改 package.json 中的运行命令,在启动dev是设置mode是development表示读取开发环境配置名称可以自定义但是要和上面新建的配置文件后缀名保持一致
scripts: {dev: vite --mode development,
},然后在 vue 文件中通过下面方式获取配置项
console.log(import.meta.env)这里是开发环境读取到的 VITE_API 是 http://localhost:8080
然后打包项目再看一下打印结果 在 vite.config.ts 中获取环境变量时通过如下方式获取
import { defineConfig,loadEnv } from vitelet {VITE_API} loadEnv(process.env.NODE_ENV,process.cwd())console.log(VITE_API)控制台会打印出定义的环境变量 Webpack从0到1构建Vue3工程
项目结构
webpack-vue
├── config
│ ├── webpack.dev.js
│ └── webpack.prod.js
├── src
│ ├── App.vue
│ └── Child.vue
├── index.html
├── main.js
├── package.json
└── pnpm-lock.yamlpackage.json
{name: webpack-vue,version: 1.0.0,description: ,main: index.js,scripts: {test: echo \Error: no test specified\ exit 1,build: webpack --config config/webpack.prod.js,dev: webpack serve --config config/webpack.dev.js},keywords: [],author: ,license: ISC,dependencies: {vue/compiler-sfc: ^3.3.4,clean-webpack-plugin: ^4.0.0,css-loader: ^6.8.1,friendly-errors-webpack-plugin: ^1.7.0,html-webpack-plugin: ^5.5.3,less: ^4.2.0,less-loader: ^11.1.3,style-loader: ^3.3.3,typescript: ^5.2.2,vue: ^3.3.4,vue-loader: ^17.3.0,webpack: ^5.89.0,webpack-cli: ^5.1.4,webpack-dev-server: ^4.15.1}
}webpack.dev.js
const path require(path)
const HtmlWebpackPlugin require(html-webpack-plugin);
const {CleanWebpackPlugin} require(clean-webpack-plugin);
const {VueLoaderPlugin} require(vue-loader);module.exports {mode:development,entry: ./main.js,output: {filename: js/[name].[contenthash:10].js,path: path.resolve(__dirname, dist)},module: {rules: [{test:/\.vue$/,use: vue-loader},{test: /\.css$/, //解析cssuse: [style-loader, css-loader],},{test:/\.less/,use: [style-loader,css-loader, less-loader],}]},resolve: {alias: {/: path.resolve(__dirname, ./src) // 别名},extensions: [.js, .json, .vue, .ts, .tsx] //识别后缀},plugins: [new CleanWebpackPlugin(),new VueLoaderPlugin(),new HtmlWebpackPlugin({template: ./index.html,}),],devServer: {port: 8088,open: true,host: localhost,historyApiFallback: true, // 解决vue-router刷新404问题proxy: {/api: {changeOrigin: true,pathRewrite: {^/api: }}}}
}webpack.prod.js
const path require(path)
const HtmlWebpackPlugin require(html-webpack-plugin);
const {CleanWebpackPlugin} require(clean-webpack-plugin);
const {VueLoaderPlugin} require(vue-loader);module.exports {mode:production,entry: ./main.js,output: {filename: js/[name].[contenthash:10].js,path: path.resolve(__dirname, ../dist)},module: {rules: [{test:/\.vue$/,use: vue-loader},{test: /\.css$/, //解析cssuse: [style-loader, css-loader],},{test:/\.less/,use: [style-loader,css-loader, less-loader],}]},resolve: {alias: {: path.resolve(__dirname, ./src) // 别名},extensions: [.js, .json, .vue, .ts, .tsx] //识别后缀},plugins: [new CleanWebpackPlugin(),new VueLoaderPlugin(),new HtmlWebpackPlugin({template: ./index.html,}),],
}Vite性能优化
打包优化
vite.config.js 添加 build 配置项
import { fileURLToPath, URL } from node:urlimport { defineConfig,loadEnv } from vite
import vue from vitejs/plugin-vue
import vueJsx from vitejs/plugin-vue-jsx
import AutoImport from unplugin-auto-import/vite
import Components from unplugin-vue-components/vite
import { ElementPlusResolver } from unplugin-vue-components/resolvers
import unocss from unocss/vitelet {VITE_API} loadEnv(process.env.NODE_ENV,process.cwd())console.log(VITE_API)// https://vitejs.dev/config/
export default defineConfig({module:es2022,plugins: [vue(),vueJsx(),AutoImport({resolvers: [ElementPlusResolver()],imports: [vue, vue-router],dts: src/auto-imports.d.ts}),Components({resolvers: [ElementPlusResolver()],}),unocss(),],resolve: {alias: {: fileURLToPath(new URL(./src, import.meta.url))}},css: {preprocessorOptions: {scss: {additionalData: import ./src/layout_v2/css/bem.scss;}}},build:{minify:esbuild, // esbuild打包速度最快terser 打包体积最小cssCodeSplit:true,// 拆分CSS文件chunkSizeWarningLimit:2000, // 单文件超过2000kb警告assetsInlineLimit:1024*10, // 静态资源文件低于10KB时自动转Base64}
})Pinia
安装
npm install pinia在 main.ts 中引入
import {createApp} from vue
import {createPinia} from piniaexport const app createApp(App)
app.use(createPinia())app.mount(#app)基本使用
userInfoStore.js
import {defineStore} from piniaexport const useUserInfoStore defineStore(userInfo, {state: () {return {name: 李斯特,age: 18}},getters: {userMsg() {return this.name --- this.age}},actions: {setName(newName) {console.log(this.name)this.name newName}}
})actions 中的函数也是支持异步的this 指向指向的是 state 中返回的对象地址所以可以通过this来获取到 state 中的属性值
vue文件中使用方法
templatedivulli{{ userInfoStore.name }}/lili{{ userInfoStore.age }}/lili{{ userInfoStore.userMsg }}/li/ulel-button typeprimary clickchangechange/el-button/div
/templatescript setup
import {useUserInfoStore} from /stores/userInfoStore;const userInfoStore useUserInfoStore()const change () {userInfoStore.setName(张三丰)
}/scriptPinia的一些API
$reset 重置数据$subscribe 监听数据变化$onAction 监听 action 数据变化
import {useUserInfoStore} from /stores/userInfoStore;const userInfoStore useUserInfoStore()const change () {userInfoStore.setName(张三丰)
}// $reset 重置数据
const reset () {userInfoStore.$reset()
}// $subscribe 监听数据变化
userInfoStore.$subscribe((mutation, state) {console.log(mutation, state)
})// $onAction 监听 action 数据变化
userInfoStore.$onAction((action, state) {console.log(action, state)
})Pinia持久化缓存
安装
npm install pinia-plugin-persistedstate配置
import {createApp} from vue
import {createPinia} from pinia
import PiniaPluginPersistedstate from pinia-plugin-persistedstateexport const app createApp(App)
// 配置Pinia并设置持久化缓存
const Pinia createPinia()
Pinia.use(PiniaPluginPersistedstate)app.use(Pinia)
app.mount(#app)然后在需要设置持久化缓存的pinia文件中开启persist配置
import {defineStore} from piniaexport const useUserInfoStore defineStore(userInfo, {state: () {return {name: 李斯特,age: 18}},getters: {userMsg() {return this.name --- this.age}},actions: {setName(newName) {console.log(this.name)this.name newName}},// 开启数据持久化persist: true
})效果展示 它原理是将pinia数据保存到 localStorage 缓存中刷新页面后优先从缓存中读取如果缓存中没有则再从代码中读取
Echarts展示地图
效果图 安装
npm install echarts默认安装的是 5.x 版本
在这个版本中的引入方式必须是下面这种方法
import * as echarts from echarts源码
首先要下载好地图数据 china.js
下载地址https://szx-bucket1.oss-cn-hangzhou.aliyuncs.com/picgo/china.js下载到本地使用即可
地图实现源码
templatediv classh-full flex justify-center items-centerdiv idmapDom classh-full w-full/div/div
/templatescript setup
import { onMounted } from vue
import * as echarts from echarts
import ../assets/china
import { getCityPositionByName } from /assets/cityPostion// 模拟10条数据
let mockData [{ name: 北京, value: 500 },{ name: 天津, value: 200 },{ name: 河南, value: 300 },{ name: 广西, value: 300 },{ name: 广东, value: 300 },{ name: 河北, value: 300 },
]onMounted(() {let data mockData.map(i {let cityPosition getCityPositionByName(i.name).valuereturn {name: i.name,value: cityPosition.concat(i.value),}})let initMap echarts.init(document.querySelector(#mapDom))initMap.setOption({backgroundColor: transparent, // 设置背景色透明// 必须设置tooltip: {show: false,},// 地图阴影配置geo: {map: china,// 这里必须定义不然后面series里面不生效tooltip: {show: false,},label: {show: false,},zoom: 1.03,silent: true, // 不响应鼠标时间show: true,roam: false, // 地图缩放和平移aspectScale: 0.75, // scale 地图的长宽比itemStyle: {borderColor: #0FA3F0,borderWidth: 1,areaColor: #070f71,shadowColor: rgba(1,34,73,0.48),shadowBlur: 10,shadowOffsetX: -10,shadowOffsetY: 10,},select: {disabled: true,},emphasis: {disabled: true,areaColor: #00F1FF,},// 地图区域的多边形 图形样式 阴影效果// z值小的图形会被z值大的图形覆盖top: 10%,left: center,// 去除南海诸岛阴影 series map里面没有此属性regions: [{name: 南海诸岛,selected: false,emphasis: {disabled: true,},itemStyle: {areaColor: #00000000,borderColor: #00000000,},}],z: 1,},series: [// 地图配置{type: map,map: china,zoom: 1,tooltip: {show: false,},label: {show: true, // 显示省份名称color: #ffffff,align: center,},top: 10%,left: center,aspectScale: 0.75,roam: false, // 地图缩放和平移itemStyle: {borderColor: #3ad6ff, // 省分界线颜色 阴影效果的borderWidth: 1,areaColor: #17348b,opacity: 1,},// 去除选中状态select: {disabled: true,},// 控制鼠标悬浮上去的效果emphasis: { // 聚焦后颜色disabled: false, // 开启高亮label: {align: center,color: #ffffff,},itemStyle: {color: #ffffff,areaColor: #0075f4,// 阴影效果 鼠标移动上去的颜色},},z: 2,data: data,},{type: scatter,coordinateSystem: geo,symbol: pin,symbolSize: [50, 50],label: {show: true,color: #fff,formatter(value) {return value.data.value[2]},},itemStyle: {color: #e30707, //标志颜色},z: 2,data: data,},],})
})
/scriptcityPostion.js 文件代码这个文件主要是通过省份名称获取经纬度
const positionArr [{ name: 北京, value: [116.3979471, 39.9081726] },{ name: 上海, value: [121.4692688, 31.2381763] },{ name: 天津, value: [117.2523808, 39.1038561] },{ name: 重庆, value: [106.548425, 29.5549144] },{ name: 河北, value: [114.4897766, 38.0451279] },{ name: 山西, value: [112.5223053, 37.8357424] },{ name: 辽宁, value: [123.4116821, 41.7966156] },{ name: 吉林, value: [125.3154297, 43.8925629] },{ name: 黑龙江, value: [126.6433411, 45.7414932] },{ name: 浙江, value: [120.1592484, 30.265995] },{ name: 福建, value: [119.2978134, 26.0785904] },{ name: 山东, value: [117.0056, 36.6670723] },{ name: 河南, value: [113.6500473, 34.7570343] },{ name: 湖北, value: [114.2919388, 30.5675144] },{ name: 湖南, value: [112.9812698, 28.2008247] },{ name: 广东, value: [113.2614288, 23.1189117] },{ name: 海南, value: [110.3465118, 20.0317936] },{ name: 四川, value: [104.0817566, 30.6610565] },{ name: 贵州, value: [106.7113724, 26.5768738] },{ name: 云南, value: [102.704567, 25.0438442] },{ name: 江西, value: [115.8999176, 28.6759911] },{ name: 陕西, value: [108.949028, 34.2616844] },{ name: 青海, value: [101.7874527, 36.6094475] },{ name: 甘肃, value: [103.7500534, 36.0680389] },{ name: 广西, value: [108.3117676, 22.8065434] },{ name: 新疆, value: [87.6061172, 43.7909393] },{ name: 内蒙古, value: [111.6632996, 40.8209419] },{ name: 西藏, value: [91.1320496, 29.657589] },{ name: 宁夏, value: [106.2719421, 38.4680099] },{ name: 台湾, value: [120.9581316, 23.8516062] },{ name: 香港, value: [114.139452, 22.391577] },{ name: 澳门, value: [113.5678411, 22.167654] },{ name: 安徽, value: [117.2757034, 31.8632545] },{ name: 江苏, value: [118.7727814, 32.0476151] },
]export function getCityPositionByName(name) {return positionArr.find(item item.name name)
}Vue-Router
安装
npm install vue-router安装完成后检查一下安装的版本是否是 4.x 版本确保在 vue3 中可以使用 定义路由和404
新建 router/index.js
import {createRouter,createWebHashHistory} from vue-routerconst router createRouter({// 定义路由模式哈希模式history:createWebHashHistory(),routes:[{path:/,component:()import(../views/home.vue)},{path:/about,component:()import(../views/about.vue)},// 匹配404页面当所有路径都匹配不到时就跳转到404{path: /:pathMatch(.*),component: ()import(../views/404.vue),},]
})// 导出路由
export default router注册路由
main.js
import { createApp } from vue
import App from ./App.vue
import router from ./routerconst app createApp(App)app.use(router)app.mount(#app)定义路由出口
App.vue
templaterouter-view/
/template路由跳转
方式一router-link
router-link classmr-10 to/home/router-link
router-link to/aboutabout/router-linkrouter-link是vue-router内置的组件通过to属性定义要跳转的地址属性值要和路由中的 path 相对应
方式二通过js的方式跳转
定义两个按钮点击按钮实现跳转
button classmr-10 clicktoPath(/)home/button
button clicktoPath(/about)about/buttonjs方法
import {useRouter} from vue-routerconst router useRouter()const toPath (url) {router.push({path:url})
}控制路由返回与前进
定义两个按钮分别实现返回和前进
button classmr-10 clickback()返回/button
button classmr-10 clickadvance()前进/button实现两个方法
const back () {// 方式一// router.go(-1)// 方式二router.back()
}const advance () {router.go(1)
}replace
默认通过 push 的方式跳转会留下历史记录。如果不想留下历史记录可以通过 replace 这种方法跳转。
例如在登录成功后就可以使用 replace 来跳转
在 router-link 标签上添加 replace 属性
router-link replace classmr-10 to/home/router-link
router-link replace classmr-10 to/aboutabout/router-link或者通过 router.replace
const toPath (url) {router.replace({path:url})
}这种跳转方式不会留下历史记录
路由传参
通过添加 query 参数来实现传参
const toPath (url) {router.push({path:url,query:{id:1,name:李四,}})
}通过如下方法接收路由参数
template我是详情页接收到的路由参数是{{route.query}}
/templatescript setup
import {useRoute} from vue-router;const route useRoute()console.log(route.query)/script接收到到的是一个对象
动态URL
我们也可以将参数作为页面URL的一部分
首先定义路由 注意 这里要多定义一个参数name动态路由跳转时需要通过 name 来跳转 使用 /dyDetail/:xxx/:xxx 这种方式定义动态参数名称 {path:/dyDetail/:id/:name,name:DyDetail,component:()import(../views/dyDetail.vue)
},添加跳转方法
const toDyDetail () {router.push({// 这里使用name来跳转name名称也要和路由中定义的name一致name:DyDetail,// 这里传递的属性名必须和路由中定义的参数名一致params:{id:1,name:张三}})
}获取动态路由参数方法通过 route.params 方法获取
templatedivid{{route.params.id}}/divdivname{{route.params.name}}/div
/templatescript setup
import {useRoute} from vue-router;const route useRoute()console.log(route.params)/script这里观察地址栏中的显示方式直接将参数获取url的一部分来显示
路由嵌套
定义路由
{path:/system,component:()import(../views/system/index.vue),children:[{path:menu,component:()import(../views/system/menu.vue)},{path:role,component:()import(../views/system/role.vue)},]
}system/index.vue
templatediv classparentbutton clicktoPath(menu)菜单管理/buttonbutton clicktoPath(role)角色管理/button/divrouter-view/
/templatescript setup
import {useRouter} from vue-router;const router useRouter()
const toPath (url) {router.push({path:/system/${url}})
}/scriptstyle scoped
.parent{height: 45px;background-color: pink;display: flex;gap: 15px;align-items: center;justify-content: center;
}
/style跳转到子路由时需要加上父路由地址 重定向
{path:/system,// 重定向到第一个子菜单redirect:/system/menu,component:()import(../views/system/index.vue),children:[{path:menu,component:()import(../views/system/menu.vue)},{path:role,component:()import(../views/system/role.vue)},]
}路由守卫
全局前置路由守卫
// 全局前置路由守卫
router.beforeResolve((to,from,next){console.log(to) // 去哪个页面console.log(from) // 从哪个页面来next() // 下一步必须要写否则无法跳转
})全局后置路由守卫
// 全局后置路由守卫
router.afterEach((to,from){console.log(to) // 去哪个页面console.log(from) // 从哪个页面来
})局部路由守卫
{path:menu,component:()import(../views/system/menu.vue),// 局部前置路由守卫beforeEnter:((to,from,next){console.log(to,局部前置路由守卫)console.log(from,局部前置路由守卫)next()})
},滚动行为
import {createRouter,createWebHashHistory} from vue-routerconst router createRouter({// 定义路由模式哈希模式history:createWebHashHistory(),// 滚动模式scrollBehavior:(to,from,savedPosition){if(savedPosition){// 如果有滚动的位置则重新回到之前滚动的位置return savedPosition}else{// 否则页面滚动到顶部return {x:0,y:0}}},routes:[{path:/,component:()import(../views/home.vue)},{path:/about,component:()import(../views/about.vue)},{path:/detail,component:()import(../views/detail.vue)},]
})// 导出路由
export default router动态路由
在后台管理系统中常见的场景根据不同的角色显示不同的菜单
编写方法根据不同的账号名返回不同的菜单
export function getDynamicRouting(name){return new Promise((resolve,reject){// root角色登录if(name admin){resolve([{path:/about,component:about.vue},{path:/detail,component:detail.vue},{path:/system,redirect:/system/menu,component:system/index.vue,children:[{path:menu,component:system/menu.vue,},{path:role,component:system/role.vue},],},])}// 普通人员登录if(name tome){resolve([{path:/about,component:about.vue},{path:/detail,component:detail.vue},])}})
}login.vue
登录成功后根据返回的路由信息添加路由
templatedivinput placeholder请输入账号 v-modelname/input placeholder请输入密码 typepassword v-modelpwd/button clicklogin登录/button/div
/templatescript setup
import {ref} from vue;
import router from ../router
import {getDynamicRouting} from ../../mock/mockRouter.js;let name ref()
let pwd ref()const login () {getDynamicRouting(name.value).then(routers{let dyRouter setDyRouter(routers)// 只需要添加一级路由信息即可dyRouter.forEach(rootRouter{router.addRoute(rootRouter)})})
}const setDyRouter (routers,parentPath) {routers.forEach(item{item.component import(../views/${item.component})if(!item.path.startsWith(/)){item.path ${parentPath}/${item.path}}if(item.children){setDyRouter(item.children,item.path)}})return routers
}
/script测试
首先用admin登录然后点击菜单管理可以正常返回 然后刷新页面使用tome登录然后点击菜单管理发现是404 上面的例子只是简单的实现了一个动态路由实际开发中我们会根据接口返回的路由数据渲染不同的菜单来显示
MarkDown语法高亮
安装
npm install marked highlight.js --save
or
pnpm add marked highlight.js --save注册
import ./assets/main.css
import { createApp } from vue
import { createPinia } from pinia
import App from ./App.vue
import router from ./router
import highlight from highlight.js
import highlight.js/styles/atom-one-dark.cssconst app createApp(App)app.use(createPinia())
app.use(router)app.directive(highlight,function(el){let blocks el.querySelectorAll(pre code);blocks.forEach((block){highlight.highlightBlock(block);})
})app.mount(#app)使用
div v-highlight v-htmlcontent/divscript
import { marked } from marked
const content ref()
// 需要使用marked方法吧语法转成html页面
content marked(content)
/script效果