引子
现在决定就走前端的这条道路了,当然更希望 2026 年考公上岸。这周一直在巩固 VUE,在仓库里看见了这个去年暑假学习VUE的时候练习的一个Demo,发现挺不错的,打算写一篇博客。
这个Demo,或许看起来平平无奇,但它深深凹印着VUE的基础篇章:
- props emit 绘制了一条神秘的密码,实现了父子组件间的暗号交流
- 开启了slot插槽的大门,使得组件灵活性,复用性更高 ⭐⭐⭐⭐⭐
- 全局自定义指令的封装
- 使用$nextTick演示了如何优雅的应对异步DOM更新,感觉就像是有了掌控时间的超能力
- 巧用v-model,简洁地优化了父子组件之间的通信 ⭐⭐⭐⭐⭐
- 触发事件的事件源event
- ref 、$refs 的绑定和使用
- 原生HTML5 Drag and Drop API 的使用
预览
项目文件结构
-db 数据库的存放位置 |- index.json 组织和管理数据库中的数据 -node_modules 包含了通过 npm 或 yarn 安装的所有依赖包 -public 这是公共资源目录,其中的文件和内容会被直接复制到构建输出的根目录 |- favicon.ico 网页的图标,显示在浏览器的标签页上 |- index.html 这是项目的入口HTML文件,用于加载Vue应用 -src 源代码目录,包含了项目的所有源代码文件 |- assets 存放所有静态资源文件,如图片、样式文件等 |- logo.png 项目的Logo图片 -components 存放所有的Vue组件 |- MyTable.vue 一个自定义的Vue表格组件 |- MyTag.vue 一个自定义的Vue标签组件 -directives 存放所有的全局Vue指令 |- globalDirectives.js 全局Vue指令的定义和注册 -store Vuex存储管理,用于管理应用的状态 |- index.js Vuex存储的入口文件,定义和配置了整个存储系统 -utils 工具函数和实用程序的集合 -App.vue 应用的根组件 -main.js 应用的入口文件,通常在这里初始化Vue应用并挂载到DOM中 -.browserslistrc 定义了Babel和Browserify的浏览器兼容性目标 -.editorconfig 定义了不同编辑器的代码风格和格式 -.eslintrc.js ESLint的配置文件,用于代码质量检查和静态代码分析 -.gitignore Git版本控制系统忽略的文件和目录列表 -babel.config.js Babel的配置文件,用于转译ES6+代码到ES5 -package.json 包含了项目的元信息和依赖包列表 -README.md 项目说明文档 -vue.config.js Vue CLI项目的配置文件,可以进行各种自定义配置 -yarn.lock Yarn依赖包的锁定文件,确保依赖包的版本一致性
数据准备
{ "goods": [ { "id": 1, "picture": "https://static.nike.com.cn/a/images/t_PDP_864_v1/f_auto,b_rgb:f5f5f5/a43f1f52-6850-4cab-837f-b93ff752f16d/ja-1-ep-%E5%B0%8F%E9%87%91%E9%BE%99%E9%BE%99%E5%B9%B4%E6%AC%BE%E5%AE%9E%E6%88%98%E8%B4%BE%E8%8E%AB%E5%85%B0%E7%89%B9%E7%94%B7%E5%AD%90%E7%AF%AE%E7%90%83%E9%9E%8B-ZLQQx9.png", "name": "“小金龙”龙年款实战贾莫兰特男子篮球鞋", "tag": "篮球鞋" }, { "id": 2, "picture": "https://static.nike.com.cn/a/images/t_PDP_864_v1/f_auto,b_rgb:f5f5f5,u_126ab356-44d8-4a06-89b4-fcdcc8df0245,c_scale,fl_relative,w_1.0,h_1.0,fl_layer_apply/dcb6b305-9d83-43b8-8b93-d7761ab4d6a8/air-jordan-legacy-312-%E9%9D%92%E9%BE%99%E7%94%B7%E5%AD%90%E8%BF%90%E5%8A%A8%E9%9E%8B-bssr37.png", "name": "Air Jordan Legacy 312 “青龙”男子运动鞋", "tag": "运动鞋" }, { "id": 3, "picture": "https://static.nike.com.cn/a/images/t_PDP_864_v1/f_auto,b_rgb:f5f5f5/406bd965-8f3e-4af8-bfe1-c0375cd4fd19/custom-sabrina-1-by-you.png", "name": "Sabrina 1 By You 专属定制篮球鞋", "tag": "定制" }, { "id": 4, "picture": "https://static.nike.com.cn/a/images/t_PDP_864_v1/f_auto,b_rgb:f5f5f5/ba3a7f48-77d9-49aa-ad2b-24d0df830bac/lebron-21-ep-%E7%94%B7%E5%AD%90%E7%AF%AE%E7%90%83%E9%9E%8B-wK6QND.png", "name": "LeBron XXI EP 男子篮球鞋", "tag": "人物系列" } ] }
MyTable.vue
- 可自定义表头和表体,通过插槽的方式进行传入。
- 支持拖拽排序功能,通过dragstart,drop事件实现元素的拖拽排序功能。
<template> <div class="table-case"> <table class="my-table"> <thead> <tr> <slot name="head"></slot> </tr> </thead> <tbody> <tr v-for="(item, index) in data" :key="index" @dragstart="dragStart(index)" @drop="drop(index)" @dragover.prevent> <slot name="body" :item="item" :index="index"></slot> </tr> </tbody> </table> </div> </template> <script> export default { name: 'MyTable', props: { data: { type: Array, require: true } }, data () { return { draggedIndex: 0, endIndex: 0 } }, methods: { dragStart (index) { this.draggedIndex = index }, drop (index) { this.endIndex = index const obj = { start: this.draggedIndex, end: this.endIndex } this.$emit('swapThem', obj) } } } </script> <style lang="less" scoped> .table-case { width: 1000px; margin: 50px auto; img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; } .my-table { width: 100%; border-spacing: 0; tr { transition: background-color .3s; &:hover { background-color: rgba(0,0,0,.4); color: #fff; cursor: pointer; } } img { width: 100px; height: 100px; object-fit: contain; vertical-align: middle; } th { background: #000; color:#fff; border-bottom: 2px solid #ccc; } td { border-bottom: 1px dashed #ccc; } td, th { text-align: center; padding: 10px; transition: all 0.5s; &.red { color: red; } } .none { height: 100px; line-height: 100px; color: #999; } } } </style>
MyTag.vue
- 双击标签即可编辑,编辑时显示输入框,失焦或按下 Enter 键即可提交修改。
- 使用了自定义指令v-focus来实现输入框聚焦功能。
<template> <div class="my-tag"> <input v-if="isEdit" v-focus @blur="isEdit = false" ref="inp" class="input" type="text" placeholder="输入标签" :value="value" @keyup.enter="handleEnter" /> <div v-else class="text" @dblclick="handleClick" > {{ value }} </div> </div> </template> <script> export default { name: 'MyTag', props: { value: { type: String } }, data () { return { isEdit: false } }, methods: { handleClick () { // 切换显示状态 (Vue是异步Dom更新) this.isEdit = true // 立刻获取焦点 // this.$nextTick(() => { // console.log(this.$refs) // this.$refs.inp.focus() // }) }, handleEnter (e) { this.$emit('input', e.target.value) this.isEdit = false } } } </script> <style lang="less"> .my-tag { cursor: pointer; .input { appearance: none; outline: none; border: 1px solid #ccc; width: 100px; height: 40px; box-sizing: border-box; padding: 10px; color: #666; &::placeholder { color: #666; } } } </style>
App.vue
<template> <div class="table-case"> <MyTable :data="goodList" v-on:swapThem="swapThem"> <template #head> <th>编号</th> <th>商品名</th> <th>商品展示</th> <th width="100px"></th> </template> <!-- 解构也是可以的 #body="{ item, index }" --> <template #body="slotProps"> <td>{{ slotProps.index + 1 }}</td> <td>{{ slotProps.item.name }}</td> <td> <img :src="slotProps.item.picture" /> </td> <td> <MyTag v-model="slotProps.item.tag"></MyTag> </td> </template> </MyTable> </div> </template> <script> import MyTable from './components/MyTable.vue' import MyTag from './components/MyTag.vue' import axios from 'axios' export default { name: 'TableCase', components: { MyTable, MyTag }, data () { return { goodList: [] } }, created () { this.fetchListItem() }, methods: { async fetchListItem () { axios.get('http://localhost:3000/goods').then((res) => { this.goodList = res.data }) }, swapThem (value) { const { start, end } = value // 交换元素位置 const removedStart = this.goodList.splice(start, 1)[0] const removedEnd = this.goodList.splice(end - 1, 1, removedStart)[0] this.goodList.splice(start, 0, removedEnd) console.log(this.goodList) } } } </script> <style lang="less" scoped> </style>
自定义指令
globalDirectives.js
import Vue from 'vue' // 全局指令 focus Vue.directive('focus', { inserted (el, binding) { el.focus() } })
小结
很简单的一个 Demo