Vue3 Grid Layout 怎么用才不踩坑?响应式拖拽缩放自由布局这几点没人告诉你吧?
你是不是刚上手Vue3做管理后台、个性化仪表盘、电商装修页这类需要自由布局的项目,搜到一堆教程但要么版本不对要么讲得太浅,改完一放大缩小或者换设备就全乱?甚至连组件安装都有坑?别慌,我上个月刚用它完成了两个SaaS平台的仪表盘模块,踩过的坑能攒一小箩筐,今天就用实操经验给你理明白从安装到优化的全流程,还有三个教程里很少提但超实用的进阶技巧。
第一步先搞懂:Vue3 Grid Layout 到底是啥,它和传统布局有啥区别?
很多人第一次搜这个组件,看到Grid、Layout就以为是CSS Grid的Vue封装——其实完全不是!CSS Grid是静态的、规则式的网格布局,只能靠预设的行列和响应式断点调整,根本做不了用户随意拖拽组件、调整大小、保存布局这种交互;而Vue3 Grid Layout是基于JavaScript计算+DOM操作实现的自由响应式网格布局组件,它的核心是给每个可拖拽缩放的元素分配“网格坐标”(行、列、宽高占的网格数),不管用户怎么操作,组件都会自动贴合网格对齐,不会出现上下左右歪歪扭扭的情况,而且可以非常轻松地把用户自定义的布局存到数据库,下次打开直接还原。
现在主流的Vue3自由布局组件里,它的生态最成熟,更新也比较稳定——不像有些小组件用着用着就没人维护了,不过这里要注意第一个没人直接说的小细节:它有两个分支!一个是最早的vue-grid-layout,官方适配的是Vue2;另一个是vue3-grid-layout(或者叫@jamesdiane/vue3-grid-layout,不过现在很多维护好的库直接用vue3-grid-layout),专门给Vue3用的,千万不要装错,不然会报一堆Vue2/Vue3语法不兼容的错误,连基础Demo都跑不起来。
第二步基础安装配置:避坑第一步,选对正确的包,搞懂核心配置项
刚才说了分支的问题,那具体怎么装呢?打开你的终端,直接输入这行命令就行:
npm install vue3-grid-layout --save
或者用yarn/pnpm也可以,这个包是目前GitHub上star最多的Vue3适配版本,功能和Vue2的原版几乎一致,还修复了原版一些小bug。
装完之后,先别急着写代码,先搞懂它的几个核心必须配置项,不然你的布局要么拖不动,要么缩不了,要么占不满页面:
核心容器配置项(GridLayout组件)
- col-num:整个布局容器横向分成多少个网格,默认是12,这个要根据你的设计稿来,比如管理后台侧边栏占2格、内容区占10格,那可以设成12;如果是电商装修页手机端,设成6或者8比较合适;
- row-height:每个网格的高度是多少px,默认是150,这个也可以调整,不过要注意:行高决定了整个布局的高度计算逻辑,比如某个组件占2行,那它的高度就是row-height 2 + gap (2-1);
- is-draggable:整个容器里的组件能不能拖拽,布尔值,默认是true;
- is-resizable:整个容器里的组件能不能缩放,布尔值,默认是true;
- layout:核心中的核心!这是一个数组,每个元素对应一个可拖拽缩放的子组件,里面必须包含i(唯一标识符)、x(横向网格起始坐标,从0开始)、y(纵向网格起始坐标,从0开始)、w(横向占的网格数)、h(纵向占的网格数)这五个属性,其他属性比如minW、maxW、minH、maxH是可选的,用来限制组件的最小最大宽高;
- vertical-compact:布尔值,默认是true,意思是组件会不会自动“向上吸附”填补空白——比如你把上面的一个组件拖走了,下面的组件如果有空位就会自动填上去;如果是做电商装修页这种需要固定顺序的,这个要设成false!
核心子组件配置项(GridItem组件)
GridItem组件其实没啥必须配置项,你只需要给它套在你要布局的内容外面就行,不过要注意:GridItem的i属性必须和layout数组里对应元素的i属性完全一致!不然会报错组件找不到对应的数据。
第三步快速跑通一个小Demo:从静态布局到可拖拽缩放
现在给你写一个最简单但功能完整的Demo,你可以直接复制到你的项目里试试: 在你的Vue3组件(比如Dashboard.vue)里引入GridLayout和GridItem:
<script setup>
import { ref } from 'vue'
import { GridLayout, GridItem } from 'vue3-grid-layout'
import 'vue3-grid-layout/dist/style.css' // 必须引入样式,不然组件会显示成方块块!
// 定义核心布局数据
const layout = ref([
{ i: 'chart1', x: 0, y: 0, w: 6, h: 2, minW: 3, minH: 1 },
{ i: 'chart2', x: 6, y: 0, w: 6, h: 2, minW: 3, minH: 1 },
{ i: 'table1', x: 0, y: 2, w: 12, h: 3, minW: 6, minH: 2 },
])
</script>
然后写模板部分:
<template>
<div class="dashboard-container">
<h2>我的个性化仪表盘</h2>
<GridLayout
:col-num="12"
:row-height="100"
:is-draggable="true"
:is-resizable="true"
:vertical-compact="true"
:layout="layout"
class="grid-container"
>
<GridItem v-for="item in layout" :key="item.i" :i="item.i" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :minW="item.minW" :minH="item.minH">
<!-- 这里放你自己的内容,比如ECharts图表、表格、卡片 -->
<div class="grid-item-content">{{ item.i }}</div>
</GridItem>
</GridLayout>
</div>
</template>
最后加一点基础的样式:
<style scoped>
.dashboard-container {
padding: 20px;
max-width: 1440px;
margin: 0 auto;
}
.grid-container {
background-color: #f5f7fa;
border-radius: 8px;
padding: 10px;
}
.grid-item-content {
width: 100%;
height: 100%;
background-color: #409eff;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
font-weight: bold;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>
现在你打开浏览器看看,是不是已经能看到三个可拖拽、可缩放、自动向上吸附的组件了?如果不行,检查一下有没有引入样式、i属性对不对、layout是不是ref响应式数据——这三个是新手最容易踩的基础坑。
第四步进阶技巧:三个教程里很少提但90%项目都会用到的功能
刚才的Demo只是入门,真正做项目的时候,你肯定会遇到这三个问题:
进阶技巧1:怎么实现真正的响应式?换设备自动调整col-num和row-height?
很多教程里的响应式只是“说说而已”——只会说col-num可以动态改,但具体怎么监听窗口大小变化、怎么根据断点调整,根本没讲清楚,这里给你一个完整的方案: 定义一个响应式的配置对象,包含不同断点下的col-num和row-height:
const responsiveConfig = ref({
lg: { col: 12, row: 100 }, // 大屏,≥1200px
md: { col: 8, row: 90 }, // 中屏,≥992px且<1200px
sm: { col: 6, row: 80 }, // 小屏,≥768px且<992px
xs: { col: 4, row: 70 }, // 超小屏,<768px
})
const currentBreakpoint = ref('lg')
const currentCol = ref(12)
const currentRow = ref(100)
写一个函数来获取当前的断点:
const getCurrentBreakpoint = () => {
const width = window.innerWidth
if (width >= 1200) return 'lg'
if (width >= 992) return 'md'
if (width >= 768) return 'sm'
return 'xs'
}
写一个函数来更新配置和布局:
const updateLayoutConfig = () => {
const newBreakpoint = getCurrentBreakpoint()
if (newBreakpoint === currentBreakpoint.value) return // 断点没变就不用更新
const oldConfig = responsiveConfig.value[currentBreakpoint.value]
const newConfig = responsiveConfig.value[newBreakpoint]
// 这里是核心!缩放网格的时候,要把原来的布局数据按比例转换
// 比如原来占lg屏12格中的6格,换md屏8格就应该占4格
// 原来占lg屏2行,行高100,换md屏行高90,h要不要变?其实h不用,因为组件会自动适应行高,只需要变x和w
layout.value = layout.value.map(item => ({
...item,
x: Math.round(item.x * (newConfig.col / oldConfig.col)),
w: Math.max(Math.round(item.w * (newConfig.col / oldConfig.col)), item.minW || 1), // 不能小于最小宽度
}))
currentBreakpoint.value = newBreakpoint
currentCol.value = newConfig.col
currentRow.value = newConfig.row
}
在组件挂载的时候添加窗口大小监听事件,在组件卸载的时候移除:
import { onMounted, onUnmounted } from 'vue'
onMounted(() => {
updateLayoutConfig() // 组件挂载的时候先执行一次
window.addEventListener('resize', updateLayoutConfig)
})
onUnmounted(() => {
window.removeEventListener('resize', updateLayoutConfig)
})
然后把模板里的GridLayout的col-num和row-height改成currentCol和currentRow就行啦,这个方案里,我用到了按比例转换x和w的逻辑,这个是Vue3 Grid Layout官方文档里提到的,但很少有教程详细写出来,而且还加了不能小于最小宽度的判断,避免组件缩成看不见。
进阶技巧2:怎么实现布局的保存和还原?存到localStorage或者数据库?
这个功能太常用了——做管理后台的话,用户肯定希望自己调整的仪表盘布局下次打开还能用;做电商装修页的话,商家肯定希望保存装修草稿,实现起来其实非常简单,因为layout数组就是纯JSON数据,直接存就行。 写一个保存布局的函数:
const saveLayout = () => {
// 存到localStorage的例子
localStorage.setItem('dashboardLayout', JSON.stringify(layout.value))
// 存到数据库的例子(伪代码)
// axios.post('/api/save-dashboard-layout', { userId: 123, layout: layout.value })
alert('布局保存成功!')
}
在组件挂载的时候还原布局:
onMounted(() => {
// 先从localStorage里取
const savedLayout = localStorage.getItem('dashboardLayout')
if (savedLayout) {
try {
layout.value = JSON.parse(savedLayout)
} catch (e) {
console.error('布局数据解析失败,使用默认布局', e)
}
}
// 然后再执行响应式配置更新
updateLayoutConfig()
window.addEventListener('resize', updateLayoutConfig)
})
还要在模板里加一个保存按钮:
<template>
<div class="dashboard-container">
<div class="header">
<h2>我的个性化仪表盘</h2>
<button @click="saveLayout" class="save-btn">保存布局</button>
</div>
<!-- 其他内容不变 -->
</div>
</template>
加个header的样式:
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.save-btn {
padding: 8px 16px;
background-color: #67c23a;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.save-btn:hover {
background-color: #5daf34;
}
是不是很简单?不过这里要注意一个小坑:如果你的项目里有用户登录的功能,存localStorage的话,换个账号登录就会看到别人的布局,所以最好存到数据库里,关联用户ID。
进阶技巧3:怎么在GridItem里放ECharts图表?拖拽缩放的时候图表不会变形或者不更新?
这个也是90%做管理后台的人都会遇到的问题——ECharts图表需要一个固定的容器宽高才能正常渲染,但是GridItem的宽高是动态变化的,拖拽缩放之后图表要么变扁变宽,要么完全不显示,解决方法其实就是监听GridItem的宽高变化,然后调用ECharts的resize()方法。 给GridItem里的图表容器加一个ref:
<GridItem v-for="item in layout" :key="item.i" :i="item.i" :x="item.x" :y="item.y" :w="item.w" :h="item.h" :minW="item.minW" :minH="item.minH">
<!-- 把刚才的内容换成图表容器 -->
<div v-if="item.i === 'chart1'" ref="chart1Ref" class="chart-container"></div>
<div v-else-if="item.i === 'chart2'" ref="chart2Ref" class="chart-container"></div>
<div v-else class="grid-item-content">{{ item.i }}</div>
</GridItem>
引入ECharts,定义图表实例和图表容器的ref:
import * as echarts from 'echarts' // 在script setup里,ref的名字要和模板里的一致 const chart1Ref = ref(null) const chart2Ref = ref(null) const chart1Instance = ref(null) const chart2Instance = ref(null)
写一个初始化图表的函数,还有一个监听GridItem宽高变化的函数:
// 初始化chart1的函数,随便写个简单的折线图
const initChart1 = () => {
if (!chart1Ref.value) return
chart1Instance.value = echarts.init(chart1Ref.value)
const option = { { text: '折线图示例' },
tooltip: {},
xAxis: { data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] },
yAxis: {},
series: [{ name: '销量', type: 'line', data: [820, 932, 901, 934, 1290, 1330, 1320] }]
}
chart1Instance.value.setOption(option)
}
// 初始化chart2的函数,随便写个简单的柱状图
const initChart2 = () => {
if (!chart2Ref.value) return
chart2Instance.value = echarts.init(chart2Ref.value)
const option = { { text: '柱状图示例' },
tooltip: {},
xAxis: { data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'] },
yAxis: {},
series: [{ name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20] }]
}
chart2Instance.value.setOption(option)
}
// 监听GridItem宽高变化,调用resize()
const handleGridItemResize = () => {
if (chart1Instance.value) chart1Instance.value.resize()
if (chart2Instance.value) chart2Instance.value.resize()
}
在GridLayout组件上添加resized事件监听——这个事件是Vue3 Grid Layout官方提供的,当任何一个GridItem的宽高变化或者整个容器的大小变化时都会触发:
<GridLayout :col-num="currentCol" :row-height="currentRow" :is-draggable="true" :is-resizable="true" :vertical-compact="true" :layout="layout" @resized="handleGridItemResize" <!-- 加上这个事件 --> class="grid-container" >
在组件挂载的时候初始化图表,在组件卸载的时候销毁图表实例:
onMounted(() => {
// 还原布局和响应式配置的代码不变
const savedLayout = localStorage.getItem('dashboardLayout')
if (savedLayout) {
try {
layout.value = JSON.parse(savedLayout)
} catch (e) {
console.error('布局数据解析失败,使用默认布局', e)
}
}
updateLayoutConfig()
window.addEventListener('resize', updateLayoutConfig)
// 初始化图表——注意要等DOM渲染完成!所以要用nextTick
nextTick(() => {
initChart1()
initChart2()
})
})
onUnmounted(() => {
window.removeEventListener('resize', updateLayoutConfig)
// 销毁图表实例,防止内存泄漏
if (chart1Instance.value) chart1Instance.value.dispose()
if (chart2Instance.value) chart2Instance.value.dispose()
})
还要加一个图表容器的样式:
.chart-container {
width: 100%;
height: 100%;
}
现在你再拖拽缩放chart1或者chart2,图表是不是会自动跟着调整大小,不会变形了?这里的关键有两个:一是用nextTick等DOM渲染完成再初始化图表,二是监听GridLayout的resized事件调用ECharts的resize()方法,还有组件卸载的时候销毁实例防止内存泄漏——这个也是很多教程没讲清楚的。
写在最后:Vue3 Grid Layout的其他小细节和注意事项还有几个小细节要注意:
- 拖拽缩放的体验优化:你可以给GridLayout添加use-css-transforms属性(默认是true),这样组件拖拽缩放的时候会用CSS3的transform,比直接修改DOM的top/left/width/height更流畅;
- 禁止单个组件拖拽缩放:如果你想让某个组件不能拖拽或者不能缩放,可以在layout数组里对应的元素里加static: true属性,这样这个组件就固定了;
- 添加拖拽缩放的手柄:默认情况下,整个GridItem的区域都可以拖拽缩放,如果你想只让某个小区域(比如右上角的三个点)可以拖拽缩放,可以用drag-handle和drag-ignore-from属性,具体用法可以看官方文档;
- 性能优化:如果你的布局里有很多GridItem(比如超过50个),可以考虑用lazy-render属性(默认是false),只渲染可见区域的组件,不过这个功能可能会有一些小bug,要测试清楚再用。
Vue3 Grid Layout是一个非常强大的自由响应式网格布局组件,只要你掌握了上面的基础配置、核心逻辑和进阶技巧,就能快速完成大部分需要自由布局的项目,如果你还有其他问题,或者遇到了我没提到的坑,欢迎在评论区留言交流!
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网



