序

自成一派的 H5 框架
Fantastic-mobile 不是一个新项目,我在 2024 年 6 月的时候就发布了第一个版本,只是一直没正式做过宣传。
经过近一年的陆续迭代,这次正式从 0.x 版本升级到了 1.0 版本,觉得也是时候宣传一波了。
有哪些轮子
如果你有在关注 Vue 生态的 H5 模板,相信你一定见过下面这几个项目:
它们是社区里人气比较高的项目,也包括前几个月刚发布的 MobVue。
但它们好像没什么区别?
它们几乎都用到了:
- Vite / Vue 3 / Vant 4 / Pinia / UnoCSS / TypeScript
- 基于 Eslint / Stylelint 实现代码规范
- 基于 unplugin-vue-router 实现的文件系统路由
- 基于 unplugin-auto-import 实现 API 自动引入
- 基于 unplugin-vue-components 实现组件自动引入
- 基于 vue-i18n 实现多语言支持
- 基于 postcss-mobile-forever 实现移动端尺寸单位自动转换
- 基于 devtools 实现开发环境调试
- ......
近乎于 100% 相同的技术栈,似乎只是将这些插件/库集成进一个 Vue 工程中,并进行了一些初始化的配置,然后就形成了各自的模板。
这真的有意义么?
毋庸置疑,当然是有意义的。即便是让一个经验丰富的 Vue 开发者从零开始,自行将上述这些插件/库自行集成进一个空白的 Vue 工程,可能也需要花费 2-3 天的时间,更别说新手小白了。
但就只能做这些了么?
为什么重新造轮子(痛点)
在没有开发 Fantastic-mobile 之前,我也用过这些人气模板进行项目开发。
使用过程中,最大的痛点就是,顶部导航栏和底部标签栏需要我自行实现。
虽然有些模板在页面上层提供了一个 Layout
布局组件(也就是二级路由中一级路由的组件,类似于父组件),在 Layout
组件中封装了顶栏和底栏,但依旧很难使用。
比如有个表单页,它的提交按钮是需要放在顶栏右侧的,要如何实现这个按钮?(以掘金发布沸点这个页面做示例)

是在 Layout
组件中实现么?那就会增加很多业务代码。首先需要根据路由判断按钮是否显示;其次按钮有 disabled
状态,这个状态肯定是从子组件里来的,要怎么拿到也是个问题;然后还得处理按钮点击后和子组件的通信。
如果只是一两个页面还好,但如果这样的页面很多,Layout
组件的代码量就会变得很大,而且不利于维护。
也有模板没有二级路由,不存在 Layout
布局组件,它直接封装了两个组件,一个 Header
组件,一个 Footer
组件。需要用到的页面引入这两个组件就行,并且组件提供了一些插槽,方便做一些扩展。
但也带来另外一个问题,比如顶栏通常是 fixed
固定在页面顶部的,那当前页面就需要手动设置 padding-top
或者 margin-top
,不然页面内容就可能会被遮挡。如果顶栏高度是动态的,那可能还得动态去计算 padding-top
或 margin-top
的值。
再比如顶栏默认是显示的,当向下滑动时需要隐藏,向上滑动时则显示,这就需要在业务页面里手动实现这些逻辑,并且多数情况下会与业务代码耦合在一起。
虽然这些实现起来可能并不复杂,但多少还是会增加开发心智,因为一旦对顶栏或者底栏进行的改动,必须检查下是否对页面会造成影响。
Fantastic-mobile 是怎么做的
针对上面提到的痛点,我做了一些方案尝试,比如通过 <teleport>
组件将子组件里的按钮发送到 Layout
组件中提供的位置,但存在一些边缘情况的bug。
最终确定的方案就是提供一个 FmPageLayout
组件,它并没有采用二级路由那种方案,而是直接应用在具体页面中的一个组件。它的使用方式就是在页面最外层包裹一层 FmPageLayout
组件就行。
html
<template>
<FmPageLayout :navbar="false" tabbar copyright>
<!-- 页面内容 -->
</FmPageLayout>
</template>
那这个组件是怎么解决上面提到的痛点呢?我们一个个来看。
顶栏插槽
通过提供预设的插槽,可以很方便的实现顶栏的定制,并且完全不需要考虑组件通信的问题,因为它都在当前页面里。
html
<template>
<FmPageLayout navbar>
<template #navbar-start>
<FmSwitch v-model="checked" />
</template>
<template #navbar-end>
<FmButton size="sm" @click="show = true">
操作按钮
</FmButton>
<van-action-sheet v-model:show="show" :actions="actions" @select="onSelect" />
</template>
<div class="flex flex-col gap-4 p-4">
<div>
Switch: {{ checked }}
</div>
<FmButton @click="router.back()">
返回
</FmButton>
</div>
</FmPageLayout>
</template>

顶栏模式
提供了多种顶栏展示模式。
html
<template>
<FmPageLayout navbar navbar-mode="static">
<!-- 页面内容 -->
</FmPageLayout>
</template>
<template>
<FmPageLayout navbar navbar-mode="fixed">
<!-- 页面内容 -->
</FmPageLayout>
</template>
<template>
<FmPageLayout navbar navbar-mode="show-hide-fixed">
<!-- 页面内容 -->
</FmPageLayout>
</template>
<template>
<FmPageLayout navbar navbar-mode="sticky">
<!-- 页面内容 -->
</FmPageLayout>
</template>

更多玩法
因为有了 FmPageLayout
组件,就可以扩展出很多玩法特性。
导航栏预设按钮
提供了一些常用的按钮,比如"主页"、"返回"、"多语言切换"等。并且可以根据业务需要,自行增加这些预设按钮。
html
<template>
<FmPageLayout navbar :navbar-start-side="['home']" :navbar-end-side="['i18n']">
<!-- 页面内容 -->
</FmPageLayout>
</template>

导航栏样式

自定义导航栏
可以完全自定义导航栏,比如导航栏的背景色、导航栏的高度、导航栏的标题等。并且当高度变化时,不再需要手动计算 padding-top
或 margin-top
的值。
html
<template>
<FmPageLayout navbar @scroll="onScroll">
<template #navbar>
<div
class="h-[80px] flex flex-center gap-2 bg-([url('https://picsum.photos/375/60')] cover center no-repeat) text-light text-shadow text-shadow-color-dark shadow transition-all transition-all-500" :class="{
'h-[60px]!': scrollTop > 50,
}"
>
头部导航
</div>
</template>
<!-- 页面内容 -->
</FmPageLayout>
</template>

底部标签栏
我提供了一个全局的配置文件,可以将底部标签栏提前配置好。
它很像小程序在
pages.json
中配置的tabBar
选项,没错,这块就是借鉴了小程序的设计。
ts
const globalSettings: Settings.all = {
tabbar: {
list: [
{
path: '/feature/',
icon: 'i-ic:sharp-auto-awesome',
activeIcon: 'i-ic:twotone-auto-awesome',
text: '特色',
},
{
path: '/',
icon: 'i-ic:sharp-home',
activeIcon: 'i-ic:twotone-home',
text: '主页',
},
{
path: '/user/',
icon: 'i-ic:baseline-person',
activeIcon: 'i-ic:twotone-person',
text: '我的',
},
],
},
}
然后在需要启用底栏的页面开启即可。
html
<template>
<FmPageLayout tabbar>
<!-- 页面内容 -->
</FmPageLayout>
</template>

多套底部标签栏
与小程序设计不同的是,我提供了多套底部标签栏的配置,可以为不同的页面设置不同的底部标签栏。
ts
const globalSettings: Settings.all = {
tabbar: {
list: [
{
name: 'default',
list: [
{
path: '/feature/',
icon: 'i-ic:sharp-auto-awesome',
activeIcon: 'i-ic:twotone-auto-awesome',
text: $t('tabbar.default.feature'),
},
{
path: '/',
icon: 'i-ic:sharp-home',
activeIcon: 'i-ic:twotone-home',
text: $t('tabbar.default.index'),
},
{
path: '/user/',
icon: 'i-ic:baseline-person',
activeIcon: 'i-ic:twotone-person',
text: $t('tabbar.default.user'),
},
],
},
{
name: 'second',
list: [
{
path: '/',
icon: 'i-mdi:flower',
text: $t('tabbar.second.flower'),
},
{
path: '/',
icon: 'i-mdi:grass',
text: $t('tabbar.second.grass'),
},
],
},
],
},
}
然后在页面指定使用哪套即可。
html
<template>
<FmPageLayout tabbar tabbar-name="second">
<!-- 页面内容 -->
</FmPageLayout>
</template>

自定义标签栏
甚至可以完全自定义顶部标签栏,比如控制按钮显示、设置毛玻璃效果等。
html
<template>
<FmPageLayout tabbar tabbar-class="bg-[hsl(var(--background))]/80 backdrop-blur-sm">
<template #tabbar>
<div class="flex-center flex-1">
<FmIcon name="https://fantastic-admin.hurui.me/logo.svg" class="text-8" />
</div>
<div v-show="checked" class="flex-center flex-1">
<FmIcon name="https://fantastic-mobile.hurui.me/logo.png" class="text-8" />
</div>
<div class="flex-center flex-1">
<FmIcon name="https://one-step-admin.hurui.me/logo.png" class="text-8" />
</div>
</template>
<!-- 页面内容 -->
</FmPageLayout>
</template>

除此之外
除了顶栏和底栏,FmPageLayout
组件还提供了返回顶部、版权信息、记忆当前滚动位置等特性。
也是可以通过 props 传入一键开启。
html
<template>
<FmPageLayout back-top copyright saved-position>
<!-- 页面内容 -->
</FmPageLayout>
</template>

无限可能
最重要的一点是,FmPageLayout
这个组件并非三方插件,所以完全可以直接修改源码实现二次扩展,满足开发者更多场景需求。
锦上添花的特性
介绍完了 Fantastic-mobile 最核心的特性,来介绍一些锦上添花的特性。
技术栈统一
与文章开头提到的那些模板,保持了基本一致的技术栈,如果你使用过那些模板,那么上手 Fantastic-mobile 会非常容易。
或者说,迁移到 Fantastic-mobile 也会变得很容易🤣
可替换三方组件库
Fantastic-mobile 默认集成了 Vant ,但并不强制要求必须使用 Vant ,你可以轻松替换为其他组件库。例如 Varlet 或 NutUI 。
该特性得益于 shadcn-vue ,框架封装了部分 shadcn-vue 组件,以便满足框架自身的需要。


大量内建组件
除了封装 shadcn-vue 组件,还提供了一些业务中常用的组件,比如数字动画、跑马灯、弹簧抽屉等。




篇幅有限
还有更多特性无法一一介绍,感兴趣的可以前往 Fantastic-mobile 并访问演示站点自行体验。
最后
引用官网里的一句话:
是模板,更是框架
Fantastic-mobile 与市面上大部分移动端 H5 模板不同之处在于,它针对通用场景提供了一套标准且易于扩展设计,通过简单的配置即可轻松完成页面的设计和布局。同时也提供了一些常用的组件和工具函数,让开发者可以更加专注于业务逻辑的开发。
这也是为什么我自称为「框架」,而不仅仅是「模板」的原因。
当然如果你在使用过 Fantastic-mobile 后,认为它还达不到你心中框架的标准,也请告诉我有哪些可改进的地方,因为最终的目标是希望 Fantastic-mobile 能够成为你的得力助手,让你的开发工作高效且愉快。