简介:这份基于Vue 2打造的H5结婚请帖前端源码,面向需要快速搭建婚礼、宴会二维码邀请页的前端开发者和婚庆从业者,以现代交互形式替代传统纸质请帖,覆盖邀请展示、祝福留言、时间线回顾等典型场景。压缩包内共87个文件,体积约34.07MB,包含14个JavaScript逻辑脚本、6个Vue组件、24张PNG与31张JPG图片素材,另提供HTML入口、CSS样式、环境变量、依赖锁文件及license说明;图片资源覆盖背景、装饰及地址图标,脚本与组件则分别承担路由、接口请求、音乐播放和页面结构等职责,目录清晰,便于二开。项目还集成音乐控制、留言板、时间线、地图地址、阿里巴巴OSS上传等模块,并封装了axios请求与正则校验工具,适合系统学习Vue全家桶在移动端H5中的工程化实践。当前已有500人学习,对希望直接复用完整邀请模板并研究组件拆分、路由配置及环境切换的开发者来说,具备不错的参考价值。
1. 基于Vue的H5结婚请帖:一个前端需求的完整落地样本
微信群里点开一张电子请帖,音乐响起、照片缓缓滚动、倒计时跳动,最后弹出地图导航。这就是基于Vue的H5结婚请帖前端源码要解决的事:用 Vue 组件化一套移动端网页,把邀请函做成可直接部署的单页应用。这类需求在个人接单、婚庆外包和公司活动页里出现频率不低,难点不在于业务复杂度,而在于移动端适配、路由传参、动效性能和微信生态兼容。以下内容按工程初始化、页面交互、分享集成、构建排错顺序展开,面向有 Vue 基础、想直接接手或改造一套 H5 请帖源码的读者。
2. 技术选型与工程初始化:Vue 3 + Vite 还是 Vue 2 + Webpack
2.1 版本选型:为什么新项目优先 Vue 3
结婚请帖页面虽然不大,但组件拆分、路由管理、定时器清理、图片懒加载这些点一个不少。Vue 3 的组合式 API 让这部分逻辑天然聚合,一个setup函数里就能把倒计时、音乐播放、滚动监听的逻辑管理清楚,不会出现 Vue 2 选项式写法里 data、methods、watch 各处分散的问题。
Vue 3 对应的构建链也成熟了。Vite 的开发服务器秒级启动,HMR 快,对改样式和调动画非常友好。Vue 2 + Webpack 不是不能用,但如果是新起项目,面对 2026 年这时间点,Vue 3 已是默认。唯一需要犹豫的场景是:客户运营后台还在用老版 WebView 内核,且明确要求兼容 Android 7 以下的老机器。遇到这种情况,先把目标机器的浏览器内核版本摸清,Vue 3 本身支持到 IE 11 以上的现代浏览器,真正卡住你的往往不是 Vue 版本,而是 ES2015+ 语法和 CSS 变量。解决方式是构建时开启@vitejs/plugin-legacy,而不是退回 Vue 2。
2.2 create-vite 初始化工程与依赖安装
命令行操作直接给出来,项目名用wedding-invite:
npm create vite@latest wedding-invite -- --template vue cd wedding-invite npm install npm install vue-router@4 npm run dev--template vue拉下来的是 Vue 3 + Vite 的最小骨架,默认不带 TypeScript,适合快速改造。vue-router 单独装,是因为注意包版本要与 Vue 3 匹配,vue-router 4.x 才是对应 Vue 3 的版本。装完依赖后npm run dev,浏览器打开http://localhost:5173,就可以看到默认页面。
安装依赖过程中常见的报错是peerDependencies冲突,一般发生在 node 版本过旧或 npm 版本不兼容。遇到这种情况,执行node -v确认版本在 18 以上,然后删除node_modules和package-lock.json重新安装。网络不好时把 npm 源切到国内镜像执行npm config set registry https://registry.npmmirror.com,这两步能解决九成安装问题。
2.3 移动端适配:rem 与 vw 双方案的取舍
H5 请帖主要跑在微信内置浏览器里,屏幕从 320px 到 430px 不等,适配是第一道坎。当前主流做法是 vw 方案和 rem 方案并存,我一般按团队习惯选,但推荐新项目直接用 vw。
| 方案 | 核心计算 | 优点 | 坑点 |
|---|---|---|---|
| rem | 根字号 = 屏幕宽度 / 设计稿宽度 | 老项目标准方案,配合 postcss-pxtorem 自动换算 | 根字号会被系统字体设置影响,需额外处理 |
| vw | 直接把 px 换算成 vw | 原生支持、不依赖根字号 | 1px 边框和字体大小需要单独控制 |
vw 方案里,设计稿按 750px 出图,那么 1px 对应100vw / 750 ≈ 0.1333vw。手写太累,交给 postcss 插件处理:
// postcss.config.js export default { plugins: { 'postcss-px-to-viewport': { viewportWidth: 750, // 设计稿宽度 unitPrecision: 5, // 换算后保留小数位 viewportUnit: 'vw', minPixelValue: 1, // 小于等于 1px 不转换 exclude: /node_modules/ } } }这个配置会把样式文件里所有 px 自动转成 vw,写代码时仍然按设计稿 750 的尺寸写 px,不会有换算负担。minPixelValue: 1是为了保留 1px 的细边框,防止某些机器上边框直接消失。exclude很重要,node_modules 里的第三方样式不用转换。
2.3.1 字体与安全区域的边界
字号不建议全部交给 vw 转换,标题、正文等关键文字我习惯单独写媒体查询或使用clamp()。原因很简单:vw 是按屏幕宽度等比缩放,但人眼阅读习惯里,手机上字太小、平板上字太大的问题必须手动兜底。一个常用的方案是:
body { font-size: clamp(14px, 3.2vw, 18px); }clamp的三参数分别是最小值、首选值、最大值,这样屏幕宽度变化时字号被限制在 14px 到 18px 之间。此外,iPhone 的刘海屏和底部横条会遮挡内容,需要在页面最底部加安全区域适配:
.safe-bottom { padding-bottom: env(safe-area-inset-bottom); }2.4 目录结构与全局样式
请帖项目不需要复杂的目录层级,清晰即可。一个常规划分如下:
src/ ├── assets/ # 静态图片、音乐 ├── components/ # 相册、倒计时、留言等组件 ├── router/ # 路由配置 ├── views/ # 打开邀请函、详情页、祝福页 ├── utils/ # 格式化、分享等工具函数 ├── App.vue └── main.js全局样式里首先要做 reset。微信浏览器默认样式比标准浏览器更不可控,特别是-webkit-tap-highlight-color这个属性,不处理的话点击任何元素都会闪一下灰色遮罩。另外请帖页都是全屏滚动,所以html, body的高度和滚动行为要显式声明。
html, body { margin: 0; padding: 0; height: 100%; overflow-x: hidden; -webkit-tap-highlight-color: transparent; }禁掉横向滚动是关键,图片错位偶尔会造成横向溢出,一旦出现横向滚动条,整个页面体验就很廉价。给overflow-x: hidden是兜底,但布局上还是要保证每个区块宽度不超过视口宽度。
3. 邀请函页面与路由:URL 传参、倒计时与滚动动效
3.1 路由模式:hash 还是 history
H5 请帖一般部署在对象存储或 nginx 下,进入路径是https://域名/invite/index.html这种形式。路由模式上我建议直接用createWebHashHistory。原因很直接:hash 路由不需要后端配置重写规则,部署到任何静态服务器都不会出现刷新后 404 的问题。history 路由在本地开发时很舒服,但一旦部署到 CDN 或 OSS,刷新非根路径就大概率白屏,彼时需要配置一大段重写规则,为一个小项目不值当。
路由结构本身非常简单,三个页面足够:打开邀请函首页、婚礼详情页、宾客留言页。
// src/router/index.js import { createRouter, createWebHashHistory } from 'vue-router' const routes = [ { path: '/', name: 'invite', component: () => import('@/views/InvitePage.vue') }, { path: '/detail', name: 'detail', component: () => import('@/views/DetailPage.vue') }, { path: '/message', name: 'message', component: () => import('@/views/MessagePage.vue') } ] const router = createRouter({ history: createWebHashHistory(), routes }) export default routercreateWebHashHistory()会让地址栏带上#/,例如https://域名/index.html#/detail。很多不熟悉 H5 的人会觉得带#不美观,但换来的是部署时零配置,这个取舍在移动端小项目里非常划算。
3.2 嘉宾入口与 URL 参数传递
有一个高频业务需求:新人发出去的请帖,希望知道每位宾客是否打开过。常见做法是把宾客姓名拼到 URL 上,例如index.html#/detail?guest=张伟&type=friend。这样页面打开时就能在代码里读到参数,展示不同的欢迎语,也可以把访问行为上报给后端。
<script setup> import { useRoute } from 'vue-router' const route = useRoute() const guestName = route.query.guest || '朋友' const inviteType = route.query.type || 'friend' // guest 参数为空时的兜底文案 const greeting = guestName !== '朋友' ? `亲爱的 ${guestName},诚邀您参加我们的婚礼` : '亲爱的朋友,诚邀您参加我们的婚礼' </script>这里有两个注意点。第一,route.query拿到的是字符串,参数值里如果包含中文或空格,URL 上会显示编码后的%E5%BC%A0%E4%BC%9F,但不影响取值,Vue Router 会自动解码。第二,参数缺失要有兜底,否则显示空名字很尴尬。更严谨的做法是把参数校验封装成一个工具函数,保证每个入口页面拿到的都是合法值。
3.2.1 传多个参数的拼装方法
如果你是给运营或者新人做分享链接,最常用的拼装方式有两种:手动拼字符串,或者用 Vue Router 内置的router.push方法。前者直观但容易漏掉编码,后者更安全:
import { useRouter } from 'vue-router' const router = useRouter() function generateShareUrl(guestName, type) { router.push({ path: '/detail', query: { guest: guestName, type: type, from: 'wechat' } }) }调用generateShareUrl('李婷', 'colleague')后,地址栏会生成#/detail?guest=李婷&type=colleague&from=wechat。Vue Router 内部会把中文自动编码,后台解码后即可解析。这里强调使用router.push而不是直接修改window.location,为的是保留 SPA 内部状态,避免整个页面刷新。
3.3 倒计时组件:时间计算与定时器清理
婚礼主题页几乎都有一个倒计时。倒计时看起来简单,但写错的人不少,常见问题包括:日期写死导致每年都要改、组件卸载后定时器未清理、时区导致计算偏差。
<script setup> import { ref, onMounted, onUnmounted, computed } from 'vue' const targetTime = new Date('2026-10-01T10:00:00+08:00').getTime() const currentTime = ref(Date.now()) let timer = null const countdown = computed(() => { const diff = targetTime - currentTime.value if (diff <= 0) { return { days: 0, hours: 0, minutes: 0, seconds: 0 } } return { days: Math.floor(diff / (1000 * 60 * 60 * 24)), hours: Math.floor((diff / (1000 * 60 * 60)) % 24), minutes: Math.floor((diff / (1000 * 60)) % 60), seconds: Math.floor((diff / 1000) % 60) } }) onMounted(() => { timer = setInterval(() => { currentTime.value = Date.now() }, 1000) }) onUnmounted(() => { clearInterval(timer) }) </script>核心逻辑是维护一个currentTime的 ref,每秒更新一次,倒计时结果通过computed推导。这里有个容易被忽视的点:不要直接修改targetTime,也不要拿系统时间减本地时间,应该统一使用+08:00这种带时区的 ISO 字符串。很多人踩过这个坑,部署在海外服务器的前端拿到的时间跟北京时间不一样,倒计时就错了。
onUnmounted里clearInterval是必须的,否则组件切换后定时器还在跑,性能和内存都会被拖累。如果你用 Vue 2 的选项式写法,则对应destroyed钩子。不管哪种框架,定时器清理都是倒计时组件的安全红线。
3.4 滚动动效:IntersectionObserver 比滚动监听更优
请帖页面通常是一张长图流,从封面滑到相册再到地图。早期做法是监听scroll事件,判断元素距视口位置后添加动画类,但scroll事件触发频率极高,容易造成掉帧。推荐使用IntersectionObserver,它是浏览器原生 API,只在元素进入视口时触发回调,性能和可维护性都更好。
<template> <section ref="sectionRef" class="fade-section"> <h2>我们的故事</h2> <p>从相识到相守,一路走来的每个瞬间</p> </section> </template> <script setup> import { ref, onMounted, onUnmounted } from 'vue' const sectionRef = ref(null) let observer = null onMounted(() => { observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible') observer.unobserve(entry.target) } }) }, { threshold: 0.3 }) if (sectionRef.value) { observer.observe(sectionRef.value) } }) onUnmounted(() => { if (observer) observer.disconnect() }) </script> <style scoped> .fade-section { opacity: 0; transform: translateY(30px); transition: opacity 0.6s ease, transform 0.6s ease; } .fade-section.visible { opacity: 1; transform: translateY(0); } </style>threshold: 0.3表示元素有 30% 的面积进入视口时才触发动画。先设opacity: 0加translateY(30px)做隐藏态,添加.visible类后过渡到显示态,这组参数能做出很常见的上浮浮现效果。有一点要注意:初始隐藏的动画元素如果 JS 执行失败或观察器未生效,页面会出现大面积空白。解决办法是在onMounted里给根元素加一个兜底类,确保 JS 报错时元素仍然可见。
4. 背景音乐、相册与微信分享:H5 交互集成的几个关键点
4.1 背景音乐自动播放策略
用户打开请帖就想起音乐,这是产品方的执念,但移动端浏览器对自动播放限制非常严格。微信浏览器里,audio.play()只有在用户触发过一次触摸或点击事件后才会被允许。常见的默认策略是:页面首次加载时显示一个有音乐按钮的遮罩层,用户点击进入后才开始播放音乐,这样既保证有声音,也满足了浏览器的交互要求。
<script setup> import { ref } from 'vue' const audioRef = ref(null) const isPlaying = ref(false) const showCover = ref(true) function enterInvite() { showCover.value = false const audio = audioRef.value if (audio) { audio.play().then(() => { isPlaying.value = true }).catch(() => { // 自动播放被拦截时,等待用户下一次手势 isPlaying.value = false }) } } function toggleMusic() { const audio = audioRef.value if (isPlaying.value) { audio.pause() isPlaying.value = false } else { audio.play() isPlaying.value = true } } </script> <template> <div> <audio ref="audioRef" src="/music/wedding.mp3" loop></audio> <div v-if="showCover" @click="enterInvite" class="cover-mask">点击进入</div> <button @click="toggleMusic" class="music-btn">{{ isPlaying ? '暂停' : '播放' }}</button> </div> </template>audio.play()返回一个 Promise,then里设置播放状态,catch里处理被拦截的情况。这个细节值得写清楚,因为很多人在初学阶段会直接audio.play()后不管,结果在 Safari 或微信里按钮反应异常,其实是捕获到异常后没有回退处理。音乐文件建议压缩到 1MB 以内,长音频用 AAC 编码,用户流量成本和时间成本都低。
4.2 图片懒加载与相册预览
请帖页的图片数量动辄十几张,全量加载会让首屏时间暴涨。移动端 4G 网络环境下,单张 200KB 的图片 20 张就是 4MB,用户打开一秒内看不到内容就会关掉。懒加载是必须项。
可以直接用loading="lazy"属性,这是现代浏览器的原生能力,零成本。但要注意:微信浏览器内置 X5 内核对新属性的支持并不一致,兼容起见我仍会选择基于IntersectionObserver的自定义懒加载指令。
<script setup> const vLazy = { mounted(el, binding) { const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting) { el.src = binding.value observer.unobserve(el) } }) observer.observe(el) } } </script> <template> <img v-for="item in photos" :key="item.id" v-lazy="item.url" alt="婚礼照片"> </template>指令里把真实的图片地址放在binding.value上,初始<img>标签不写src或使用一个 1x1 像素占位图。当图片进入视口时再把真实地址赋给src。图片加载失败的情况也需要兜底,el.onerror的时候替换成一张默认图,否则页面上会出现碎图图标。
相册预览用原生方式做最简单:点击图片打开一个全屏遮罩层,里面展示大图并支持左右滑动。如果不想引入 swiper 这类重型库,可以只做单张全屏展示,左右切换通过监听touchstart和touchend的坐标差实现,逻辑量大约 30 行,足够应付请帖场景。
4.3 留言与祝福表单:校验与提交
留言功能通常需要后端配合,源码里一般只做前端部分:表单收集、校验、提交,以及提交后的成功反馈。为了不依赖具体后端地址,常见的做法是把接口地址抽到环境变量里,用import.meta.env.VITE_API_BASE配置。
<script setup> import { ref } from 'vue' const nickname = ref('') const message = ref('') const submitting = ref(false) async function submitMessage() { if (!nickname.value.trim() || !message.value.trim()) { alert('请填写昵称和祝福语') return } submitting.value = true try { const res = await fetch(`${import.meta.env.VITE_API_BASE}/api/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nickname: nickname.value.trim(), message: message.value.trim(), createdAt: Date.now() }) }) if (!res.ok) throw new Error('submit failed') alert('祝福成功,感谢您的留言') nickname.value = '' message.value = '' } catch (e) { alert('提交失败,请稍后再试') } finally { submitting.value = false } } </script>校验只做非空判断是最低标准。如果要做字数限制,建议把maxlength直接写在<input>和<textarea>上,从源头控制。submitting状态用来防重复提交,按钮点击后立即禁用,避免用户连点导致同一条留言重复入库。使用fetch而不是 axios,是考虑到大多数留言接口极其简单,引入 axios 只是增加包体积。
4.4 微信 JSSDK 分享配置
请帖在微信里传播的核心是分享卡片。用户在微信右上角分享,默认展示的是链接标题和缩略图,这不够体面,也体现不出婚礼氛围。接入微信 JS-SDK 后,可以自定义分享标题、描述和分享图标。
import wx from 'weixin-js-sdk' function initWechatShare(config) { wx.config({ debug: false, appId: config.appId, timestamp: config.timestamp, nonceStr: config.nonceStr, signature: config.signature, jsApiList: ['updateAppMessageShareData', 'updateTimelineShareData'] }) wx.ready(function () { const shareData = { title: '诚邀您参加我们的婚礼', desc: '这是一份来自新郎新娘的邀请', link: window.location.href, imgUrl: config.shareImage } wx.updateAppMessageShareData(shareData) wx.updateTimelineShareData(shareData) }) wx.error(function (res) { // 签名失败,需重新获取签名 console.error('wx config error', res) }) }wx.config所需的四个参数全部由后端接口获取。常见做法是前端在路由加载时请求一次签名接口,传入当前页面的window.location.href.split('#')[0],后端用这块 URL 生成签名。这里有个典型坑:link参数如果填了带#的完整地址,部分安卓机的分享卡片会打不开。必须去掉#之后的部分,让用户点开卡片时重新落到正确的 hash 路由上。
如果后端还没就绪,前端可以用<meta>标签做降级方案。微信支持og:title、og:description、og:image三个 meta 属性,虽然灵较度不如 JSSDK,但至少分享出去有标题有图,不裸奔。
5. 构建部署与真机调试:打包路径、布局异常和验证清单
5.1 base 路径与静态资源定位
部署到子目录是最常见的场景,比如https://example.com/wedding/。如果打包时不做任何配置,Vite 默认资源路径是根目录/,部署到子目录后所有 JS、CSS、图片都会 404。解决办法是在vite.config.js里设置base,或者在执行构建命令时通过环境变量控制。
// vite.config.js export default { base: process.env.VITE_BASE_PATH || './' }打包时执行VITE_BASE_PATH=/wedding/ npm run build,产物里的资源路径就会自动带上/wedding/前缀。如果部署在 nginx 的根路径,直接用默认/即可。base设为'./'也可以,它让资源加载变成相对路径,适合放在任何目录,但对路由和懒加载模块的路径解析有兼容风险,不推荐在vue-router的 hash 模式下混用相对路径,容易出问题。
5.2 打包后布局异常的常见原因
开发环境一切正常,npm run build部署上去后发现布局乱了,这是 Vue 打包后布局异常的高频场景。原因集中在三处。
第一处是样式文件的顺序。开发环境里 Vite 按依赖图动态注入样式,打包时按照模块顺序合并,如果组件里的 scoped 样式和全局样式发生覆盖关系,两者在产物里的顺序可能和预期不同。解决方法是只把真正的公共样式放全局,组件内的关键样式全部加scoped,并且不要全局去改第三方组件的内部类名,比如vant这类 UI 库的样式覆盖会非常痛苦。
第二处是图片路径问题。打包后的 CSS 里引用的图片,如果被编译成 base64 嵌入,体积会变大;如果通过相对路径引用,一旦部署路径和预期不符就白屏。出现图片丢失时,第一时间打开 DevTools 看 Network 面板里图片资源的完整 URL,判断是路径问题还是 404。
第三处是字体文件。移动端 H5 通常用到自定义字体,但字体文件体积大且跨域限制多。建议只用一种字重,用font-display: swap避免阻塞渲染,把字体格式转为 woff2,裁剪掉不需要的字形子集。简单来说,字体少用,用了就压。
5.3 用 vConsole 做真机调试
开发环境有 DevTools,真机调试就得靠 vConsole。它是一个移动端可引入的控制台面板,能看 console、网络请求和 cookie,微信里打开特别方便。
<script src="https://unpkg.com/vconsole"></script> <script> var vConsole = new VConsole(); </script>生产环境需要调试时,可以在 URL 加参数控制开启条件,避免用户看到控制台按钮。另一个方式是写进代码里:
import VConsole from 'vconsole' if (location.hash.includes('debug')) { new VConsole() }这样分享出去的链接只要带上#debug,开发者打开就是调试模式,用户正常打开不受影响。vConsole 里能直观看到 fetch 请求的报错信息、图片加载失败对象,以及页面上 JS 报错的堆栈。我用它在微信里排查过很多次签名失败和路由 404 的问题,是 H5 项目的常备工具。还需要强调一个问题:vConsole 只适合开发自测,正式版应移除这段代码或用条件语句隔离,不要在产品环境裸奔。
5.4 上线前验证清单
上线前最后一件事,打开手机微信扫一扫本地构建产物,按以下顺序过一遍:首屏加载速度,要求在 3 秒内出现主体内容,超过这个时间用户基本流失,检查图片是否压缩、音乐是否过大;倒计时是否与北京时间一致,尤其跨时区用户看到的数字;分享卡片标题、描述、图片是否正确,曾在生产环境出现过分享卡片用的是上一次签名的旧缓存,清除微信缓存后立即恢复的情况;路由刷新后是否正常,hash 模式下一般没问题;底部按钮是否被 iPhone 横条遮挡,加env(safe-area-inset-bottom)解决。
最后验证一个最容易被忽略的点:用 Chrome DevTools 的 Device Toolbar 把 UA 设成 iPhone,Network 面板勾选 Fast 3G,刷新页面复现一遍用户第一次打开时的体验,重点看首屏图片和背景音乐是否按预期加载,这比模拟器更接近真实情况。
本文还有配套的精品资源,点击获取