
简介一套基于Vue框架的企业官网H5设计源码面向具备一定前端基础、希望快速搭建现代化企业站点的开发者同时也适合学习Vue组件化开发的项目实践。资源完整覆盖企业官网常见模块如导航、页头页脚、首页、产品服务、解决方案、案例等并包含路由与状态管理配置可直接在此结构上继续扩展。包体共49个文件压缩包约3.38MB核心由11个Vue组件、7个TypeScript/JavaScript脚本、4个JSON配置、14张PNG图片及2个ICO图标等构成。组件层承担页面区块复用脚本层处理交互与逻辑JSON文件管理路由、构建及依赖图片与图标提供基础视觉素材目录组织清晰便于维护。已有308人学习/下载。通过阅读源码可熟悉VueTypeScript项目搭建方式了解组件划分、路由配置、状态管理及测试配置的实际用法同时可作为企业官网模板帮助节省从零搭建的时间快速产出具备H5响应式体验的正式页面。1. 企业官网H5项目里为什么我坚持用 Vue 和 TypeScript 打底不少团队做企业官网时习惯从网上下一个 HTML 模板改改文字就上线等后面要加产品分类、做 SEO、加埋点才发现模板里的全局 JS 变量和样式互相污染。这份基于 Vue 框架的企业官网 H5 设计源码把官网拆成了一个个带作用域的.vue组件并用 TypeScript 约束数据类型解决的是“日常维护成本和页面迭代速度”的问题。它包含了 49 个文件其中 11 个 Vue 组件、7 个 JS/TS 脚本以及路由、Vuex、构建配置和 E2E 测试适合两类人一是想快速交付官网的前端开发者可以拿它当脚手架二是想学习 Vue 工程化组织方式的学生或初级工程师能直接看到组件、路由、状态管理在真实项目里如何配合。整个项目不依赖后端也能先跑起来非常适合作为企业站点的起步工程。2. 先把工程结构拆开别急着看代码先搞懂文件各自干什么拿到一个 Vue 源码包先别急着跑npm install我一般先看根目录的配置文件确认这个项目是纯 Webpack 还是 Vue CLI 3 的封装。这个包里同时有vue.config.js、tsconfig.json、cypress.json和.eslintrc.js说明它是一套完整的 CLI TypeScript E2E 测试工程不是那种只有一个 HTML 的 demo。越早看清这些文件后面对接和维护就越少踩坑。2.1 根目录配置现代 Vue 工程的“基建层”package.json声明了所有运行时依赖和开发依赖package-lock.json把依赖版本精确锁定到具体 commit避免换一台机器安装后行为不一致。tsconfig.json控制 TypeScript 编译规则企业官网这类项目通常会把strict打开宁可编译时报错也不把隐患留到线上。vue.config.js是 Vue CLI 的扩展配置入口后面要配置代理、输出目录、压缩方式都改这里。cypress.json配置的是端到端测试eslintrc.js统一代码风格.gitignore让 node_modules、dist 这些生成目录不进版本库。下面这张表把根目录文件与职责对应起来了文件作用改动的关键点package.json依赖与脚本管理scripts 里的 dev/build/testtsconfig.jsonTS 编译规则strict、paths 路径别名vue.config.js构建与开发服务器配置devServer.proxy、publicPathcypress.jsonE2E 测试基地址与超时baseUrl 需匹配 dev 端口.eslintrc.jsLint 规则规则冲突时在此关闭或调整这些配置都不是摆设。比如package.json里的vue和vue-router版本会影响路由 API 的写法如果是 Vue 2 Vue Router 3用new Router()如果是 Vue 3 Vue Router 4则是createRouter()。而这份源码里有main.ts、shims-vue.d.ts可以判断它是基于类的组件写法还是 Options API。没看代码前先确认这条技术线能省去不少编译报错时间。2.2 src 目录入口、视图与组件如何分工源码里最值得研究的是src下的分层。main.ts作为编译入口会在这个文件里引入全局组件、插件和样式App.vue是根组件它只负责整体骨架比如把Header、Menu、router-view组合到一起views目录放页面级组件比如firstPage.vue是首页case.vue是案例页productServices.vue是产品服务页solution.vue是解决方案页components目录则放可复用片段Header.vue、Footer.vue、Menu.vue都是典型。我用一个目录树来表示这种组织方式src/ ├── main.ts ├── App.vue ├── components/ │ ├── Concat.vue │ ├── Footer.vue │ ├── Header.vue │ └── Menu.vue ├── views/ │ ├── Home.vue │ ├── firstPage.vue │ ├── case.vue │ ├── productServices.vue │ ├── solution.vue │ └── advantage.vue ├── router/ │ └── index.ts ├── store/ │ └── index.ts ├── assets/ │ └── images/ └── EvenBus.ts这种分层的好处是页面之间的切换只是路由跳转组件内部状态自己管。EvenBus.ts作为全局事件总线适合跨页面传递轻量级消息。但要提醒一点事件总线在组件卸载时容易造成重复监听用它之前先想清楚是不是真的不能用 props 和 Vuex 替代。在 views 里同时存在Home.vue和firstPage.vue这可能是历史版本或者多入口切换的预留。实际开发时不要把两个语义接近的页面都留着建议只保留一个作为首页避免路由表混乱。2.3 assets 与 public静态资源该放哪assets/images下的 14 张 PNG 是构建时会被处理的图片经过 base64 内联或者加 hash 文件名public目录下的favicon-bac.ico、logo.ico和index.html则直接复制到打包根目录。很多初学者分不清这两个目录简单区分是要求被 webpack 处理、需要压缩和版本号的放assets不需要处理、要保证绝对路径引用的放public。企业官网里的 logo 和 favicon 通常放public首页展示的产品图放assets。这样部署到 CDN 时也能明确哪些文件需要参与指纹更新。3. 组件不是模板套壳Header、Menu、Footer 的职责边界与事件传递很多 Vue 初学者写官网组件喜欢把数据一股脑写在 template 里导航菜单写死点击事件直接改当前组件这样页面一多就失控。这份源码的components目录把Concat.vue、Footer.vue、Header.vue、Menu.vue剥离开本质上是在用组件化思路做布局每个组件只回答“我长什么样”和“我有什么事件向外传”不关心外部怎么消费。3.1 布局组件的 props 与 emit不要把数据写在模板里Header 和 Footer 这类组件在官网里看起来简单但容易出现“所有数据都写死在 template”的坏味道。企业官网的导航、联系方式、版权信息通常后台可配所以更好的做法是在组件上声明props由父组件或 Vuex 把数据传进来。比如 Header 组件接收一个navLists数组数组里的每一项包含name和path点击菜单通过this.$emit(change, path)把事件抛给父组件去处理路由跳转。这样组件自身不关心数据从哪来只负责渲染。template header classsite-header nav v-foritem in navLists :keyitem.path a :hrefitem.path :class{ active: currentPath item.path } click.preventhandleNav(item.path) {{ item.name }} /a /nav /header /template script langts import { Vue, Component, Prop } from vue-property-decorator Component export default class SiteHeader extends Vue { Prop({ type: Array, default: () [] }) navLists!: NavItem[] Prop({ type: String, default: / }) currentPath!: string private handleNav(path: string): void { this.$emit(nav-change, path) } } /script这段代码里Prop是 TypeScript 下声明 props 的常见方式类型是Array默认空数组。handleNav内部不直接调用router.push而是向外抛出nav-change事件这样 Header 组件在单元测试时只需要 mock 一个回调不必真正挂载路由。currentPath由父层传入是为了让高亮状态跟随路由变化。3.2 菜单展开与全局状态Vuex 还是 EventBus源码中同时有 Vuex store 和 EvenBus.ts。对于菜单这种全局 UI 状态我会选择 Vuex移动端点开菜单后需要锁定 body 滚动不同组件都要读取菜单开合状态事件总线在多个组件同时订阅时很容易出现回调没被销毁的问题。Vuex 里定义isMenuOpenHeader 通过 dispatch 修改App.vue 通过 mapState 响应变化。EventBus 更适合页面间一次性通知比如首页触发“打开联系客服”的弹窗。表格对比一下方案适用场景缺点props / emit父子组件直接交互层级过深时穿透麻烦Vuex全局 UI 状态需要额外维护 mutation 名称EventBus无关组件间的轻量通知监听器易泄露调试困难用 Vuex 写菜单状态的常见写法是state.menu.menuOpenmutation里TOGGLE_MENU组件里用computed绑定到 class 上。这样不管菜单是在 Header 里触发还是右侧悬浮按钮触发状态源都是同一个。3.3 企业官网 H5 的响应式布局细节Views 下的firstPage.vue是整个官网的门面这一屏的 banner 图、主标题和 CTA 按钮必须适配不同宽度屏幕。常见做法是外层容器不写死宽度用max-width: 1440px加margin: 0 auto图片不直接用width: 100%而是配合object-fit: cover处理裁切文字字号在移动端用rem而非px配合postcss-pxtorem自动转换。这套源码里没有强制指定 UI 框架说明它更依赖手写 CSS反而更适合学习栅格和媒体查询。如果后续要插入轮播图不要只做自动播放还要处理手势滑动和指示器切换。4. 路由、状态管理与构建配置把页面串起来的关键代码企业官网本质上是一个多页面展示系统路由、状态管理和构建配置三者配合不当会直接导致首屏白屏、页面刷新 404、跨域报错。这一章我们按实际项目里的常见做法把源码中router/index.ts、store/index.ts、vue.config.js拆开看。4.1 路由表设计与懒加载企业官网页面多如果main.ts里同步引入所有 views打包产物体积会随着页面数量线性增长。源码里的router/index.ts应该用动态 import 来拆包。我一般会把首页设置为最高优先级直接 import其他页面用() import()形式路由切换时按需加载。import Vue from vue import VueRouter, { RouteConfig } from vue-router import Home from /views/Home.vue Vue.use(VueRouter) const routes: RouteConfig[] [ { path: /, name: Home, component: Home }, { path: /product-services, name: ProductServices, component: () import(/views/productServices.vue) }, { path: /case, name: Case, component: () import(/views/case.vue) } ] const router new VueRouter({ mode: history, routes, scrollBehavior(to, from, savedPosition) { return savedPosition || { x: 0, y: 0 } } }) export default router代码说明component: () import()返回异步组件webpack 会自动根据该模块生成独立 chunk。mode: history让 URL 更贴近静态站点但生产环境需要服务器把未知路径重写到 index.html否则刷新会 404。scrollBehavior保证从详情页返回列表页时位置是对的这对官网的体验很重要。参数方面RouteConfig类型来自vue-router如果使用 Vue 3 要把类型改为RouteRecordRaw。加载方式首屏体积加载时机配置成本同步 import大打开首页即加载低动态 import小路由切换时加载中预加载 prefetch视浏览器策略空闲时提前加载高需谨慎动态 import 不是越细越好切得太碎会产生大量几 KB 的小 chunkHTTP/2 下可接受HTTP/1.1 下反而增加请求数。对于企业官网页面级懒加载就够用组件不用拆。4.2 Vuex store 的组织方式不要所有 state 堆在一个文件store/index.ts是 Vuex 的入口但不要把所有 state、mutation、action 全写在这个文件里。一个官网的 store 至少包含 navigation、contact、global每个模块有自己的权限隔离。为什么要拆因为企业官网后续会有表单提交、用户留言等状态全部放在 index.ts 会导致同一个文件几百行团队协作时冲突不断。使用namespaced: true避免不同模块里的 mutation 命名冲突。import Vue from vue import Vuex from vuex Vue.use(Vuex) const navigation { namespaced: true, state: () ({ menuOpen: false, activeMenu: / }), mutations: { SET_MENU_OPEN(state: { menuOpen: boolean }, payload: boolean) { state.menuOpen payload }, SET_ACTIVE_MENU(state: { activeMenu: string }, path: string) { state.activeMenu path } }, actions: { toggleMenu({ commit }: { commit: any }, open?: boolean) { commit(SET_MENU_OPEN, typeof open boolean ? open : !this.state.navigation.menuOpen) } } } export default new Vuex.Store({ modules: { navigation } })逻辑说明namespaced: true后组件里读取需要写this.$store.state.navigation.menuOpen分发 action 是this.$store.dispatch(navigation/toggleMenu)。用 namespace 虽然写起来长一点但能防止 mutation 冲突。在组件中最好用mapState和mapMutations不要直接在页面里写上几十处$store.commit出现 bug 时很难定位是谁改的。使用 Vue 3 项目时这套逻辑要改成createStore和useStore但模块化划分原则是一样的。4.3 vue.config.js代理和构建优化开发环境解决跨域生产环境解决静态资源路径。.env文件经常和vue.config.js配合使用。这里的publicPath是最容易踩坑的配置如果官网部署在域名的根目录可以设为/部署在子目录时不设置就会导致 CSS、JS 404。const path require(path) module.exports { publicPath: process.env.NODE_ENV production ? /official-site/ : /, outputDir: dist, devServer: { port: 8080, proxy: { /api: { target: https://api.example.com, changeOrigin: true, pathRewrite: { ^/api: } } } }, configureWebpack: { resolve: { alias: { : path.resolve(__dirname, src) } } } }publicPath直接决定资源在服务器上的访问前缀子目录部署时设置成子目录名否则绝对路径/js/chunk.js会导致静态资源 404。devServer.proxy把/api开头的请求转发到目标服务器pathRewrite去掉前缀这样后端接口不需要包含/api。alias配置指向 src代码里import Header from /components/Header.vue才能正常工作。如果官网使用了web-vitals做性能监控还需要在configureWebpack里把这类第三方库放进splitChunks避免它们被打进每个页面 chunk。5. H5 交付前实测Cypress 回归、移动端视口与微信内置浏览器缓存官网 H5 交付前我习惯用源码里自带的 Cypress 做一次关键路径回归。源码里有cypress.json和e2e/specs只要在cypress.json里把baseUrl改成http://localhost:8080然后写一个打开首页并验证导航高亮的用例就能在本地跑通整套交互闭环。describe(官方首页导航跳转, () { it(访问首页后点击产品服务跳转, () { cy.visit(/) cy.get(.site-header nav a) .contains(产品服务) .click() cy.url().should(include, /product-services) cy.get(.page-title).should(contain, 产品服务) }) })这个用例里的cy.visit建立会话get配合contains定位文本点击后检查路由和页面标题。Cypress 的优势在于每一步都自动等待元素出现不需要手动sleep比单纯用浏览器 F12 刷新验证稳定得多。跑测试时注意cypress.json里的baseUrl必须和vue.config.js的devServer.port保持一致否则会报连接失败。移动端 H5 翻车点集中在这几个位置交付前逐个确认检查项预期结果常见失败原因视口 meta移动端不出现整体缩放缺少viewport或initial-scale错误菜单开合点击 menu 后 body 锁滚动未处理touchmove默认事件图片裁切不同宽高比 banner 不拉伸object-fit未设置或低版本浏览器兼容返回路由从详情页返回保持滚动位置没有scrollBehavior和组件缓存最后说一个真机调试技巧用电脑 Chrome 的 device toolbar 模拟 iPhone 宽度只能验证布局不能替代真机把 H5 页面生成二维码用手机浏览器直接访问 dev server 的局域网地址检查下拉刷新、软键盘弹出、微信内置浏览器缓存这三类场景。微信内置浏览器经常缓存旧资源扫码调试时在 URL 后加?v${Date.now()}可以强制刷新这招在企业官网 H5 联调里很实用。本文还有配套的精品资源点击获取