在网上找了很久都没有找到解决类似问题的文章,所以就自己实现了一种的解决方案,发出来供大家参考批评。
背景
在很多网站中,用移动端浏览器和 PC 端浏览器访问同一个路由时,展示的页面是不一样的。比如说下面的中国大学 MOOC:
在 Nuxt 中,实现这一功能常见的做法是这样的:
- 将 PC 端、移动端 两套页面内容抽离出来,分别放到 components 文件夹下的两个组件内
- 在 pages 文件夹对应的页面文件中,使用
<component>
组件,根据设备类型动态引入相应的组件,如下所示:
vue
<component :is="isMobile ? MobileHomeComponent : PcHomeComponent">
这样做很简单,但会让目录结构不太优雅,随着项目复杂度的增加,维护难度会非常大。
为了避免这些问题,在一个项目中优雅地集成 PC 端、移动端多套页面,我实现了另外一种解决的方法。
最终效果
在 pages 文件夹下新建了 mobile、pc 两个目录。使用移动端访问时,应用会自动直接访问 mobile 目录下的页面;使用 PC 端访问时,应用会自动直接访问 pc 目录下的页面。同时 pc 和 mobile 这两个字符串不会出现在路由中。
在最终效果中,上图中的目录结构对应的路由如下:
路由 移动端显示的页面 PC端显示的页面 / pages/mobile/index.vue pages/pc/index.vue /aaa pages/mobile/aaa.vue pages/pc/aaa.vue /bbb pages/mobile/bbb.vue pages/pc/bbb.vue
实现步骤
第一步:创建一个获取设备类型的组合函数
在 composables 文件夹下,创建 useDeviceType.ts 文件,在其中通过 UA 来判断设备类型。代码如下:
ts
export const useDeviceType = () => {
let UA: string
if (process.client)
// 如果是在客户端执行,则通过 navigator 获取 user-agent
UA = navigator.userAgent
else
// 如果是在服务端执行,则通过请求头获取 user-agent
UA = useRequestHeader('user-agent') as string
const type = ref<'mobile' | 'pc'>()
console.log(UA)
// 通过 UA 来判断设备类型是 pc 还是 mobile
if (/(Android|webOS|iPhone|iPod|tablet|BlackBerry|Mobile)/i.test(UA))
type.value = 'mobile'
else
type.value = 'pc'
return type
}
由于 Nuxt 应用会在服务端和客户端两个环境中执行,所以获取 UA 时需要根据所在环境分别获取
第二步:通过 router options 动态修改路由
一般情况下,Nuxt 使用的是基于文件的路由系统,Nuxt 默认会扫描 pages 目录下的所有文件,并根据目录结构生成路由。如果直接按照最终效果所示的方式,新建了 pc、mobile 两个文件夹,那么 pc、mobile 这两个字符串势必会分别出现在这两个文件夹下所有页面的路由中,路由会变成这样子:
路由 | 显示的页面 |
---|---|
/pc/ | pages/pc/index.vue |
/pc/aaa | pages/pc/aaa.vue |
/pc/bbb | pages/pc/bbb.vue |
/mobile/ | pages/mobile/index.vue |
/mobile/aaa | pages/mobile/aaa.vue |
/mobile/bbb | pages/mobile/bbb.vue |
在这样的路由中,两端的页面分别需要通过 /pc 和 /mobile 前缀才能访问到,这显然不是我们想要的,我们需要对这些路由进行修改。
通过仔细阅读 官方文档 可以知道,Nuxt3 中,修改路由一共有三种方式:
修改方式 | 可行性 | 原因 |
---|---|---|
修改 router options | 可行✅ | 实例化 Router 时的配置项,可以获取设备类型并动态修改路由 |
使用 pages:extend 钩子 | 不可行❌ | 该钩子只在构建时被调用,无法获取设备类型 |
使用 Nuxt Module | 不可行❌ | 经测试,无法使用 useDeviceType 组合函数获取设备类型 |
三种方式中只有修改 router options 的方式才能获取设备类型并动态修改路由,下面是具体的操作方法:
新建 app/router.options.ts 文件,在其中根据设备类型动态导出路由配置项,代码如下:
ts
import type { RouterConfig } from '@nuxt/schema'
import type { RouteRecordRaw } from 'vue-router'
// 删除路由 path 中指定前缀
function deletePrefixInPath(prefix: '/mobile' | '/pc', routes: RouteRecordRaw[]) {
const newRoutes: RouteRecordRaw[] = []
for (let i = 0; i < routes.length; i++) {
routes[i].path = routes[i].path.replace(prefix, '') === '' ? '/' : routes[i].path.replace(prefix, '')
}
return newRoutes
}
// 重点!
// 导出路由配置项
export default <RouterConfig> {
routes: (_routes) => {
// 获取设备类型
const deviceType = useDeviceType()
const targetRoutes = ref<RouteRecordRaw[]>([]) // 存储匹配当前设备类型的页面路由
const notTargetRoutes = ref<RouteRecordRaw[]>([]) // 存储不匹配当前设备类型的页面路由
// 如果是移动端访问,则给移动端页面删除路由前缀 /mobile ,给PC端页面添加路由前缀 /pc
if (deviceType.value === 'mobile') {
targetRoutes.value = _routes.filter(item => item.path.startsWith('/mobile')).length ? _routes.filter(item => item.path.startsWith('/mobile')) : _routes.filter(item => !item.path.startsWith('/pc'))
targetRoutes.value = deletePrefixInPath('/mobile', targetRoutes.value)
notTargetRoutes.value = _routes.filter(item => item.path.startsWith('/pc'))
notTargetRoutes.value = notTargetRoutes.value.map((item) => {
if (!item.path.startsWith('/pc'))
item.path = `/pc${item.path}`
return item
})
// console.log([...targetRoutes.value, ...notTargetRoutes.value])
return [...targetRoutes.value, ...notTargetRoutes.value]
}
// 如果是PC端访问,则给PC端页面删除路由前缀 /pc ,给移动端页面添加路由前缀 /mobile
else if (deviceType.value === 'pc') {
targetRoutes.value = _routes.filter(item => item.path.startsWith('/pc')).length ? _routes.filter(item => item.path.startsWith('/pc')) : _routes.filter(item => !item.path.startsWith('/mobile'))
targetRoutes.value = deletePrefixInPath('/pc', targetRoutes.value)
notTargetRoutes.value = _routes.filter(item => item.path.startsWith('/mobile'))
notTargetRoutes.value = notTargetRoutes.value.map((item) => {
if (!item.path.startsWith('/mobile'))
item.path = `/mobile${item.path}`
return item
})
// console.log([...targetRoutes.value, ...notTargetRoutes.value])
return [...targetRoutes.value, ...notTargetRoutes.value]
}
},
}
实现的动态修改效果如下图:
第三步:优雅地分别为 PC 端、移动端 写页面
所有的移动端页面都放到 pages/mobile 文件夹中;所有的 PC 端页面都放到 pages/pc 文件夹中。
需要注意的是,如果 PC 端和 移动端 的页面数量不完全一致,比如说 PC 端有一个 pages/pc/abc.vue 页面文件,而移动端没有这个文件,那么使用移动端访问 /abc 时就会出现 404 的错误,而 PC 端可以正常显示。这一点应该很好理解。
总结
整个实现过程的核心其实只有两步:
- 创建获取设备类型的组合函数
- 通过 app/router.options.ts 文件实现动态修改路由
同时,标题的表述其实不太恰当,准确地说应该是实现了,根据设备类型的不同,使用 pages 目录下不同的文件夹作为路由的 "根目录",因此也就可以实现在同一路由下,根据设备类型,动态切换显示对应的页面。
此外值得一提的是, 由于我们是通过实例化 Router 时的配置项来动态修改路由,所以只有在初次或以刷新的方式进入页面时,路由的动态修改才会生效,也就是说:如果在中途通过开发者工具修改了设备类型,这种情况是不会动态切换到相匹配的页面的,必须要刷新页面,路由才会动态修改。
以上就是全部内容了,如果你有其他更好的实现方法或者思路,欢迎在评论区补充。如果你发现了什么 Bug 或者有其它问题,也可以在评论区提出来。
如果这篇文章有帮到你,还请点一个不要钱的赞,感谢支持 ❤️