探索 uni-app 下的基于文件名的按平台编译插件
2023-08-21 19:34
我们在 uni-app 中写跨端逻辑无非两种方案:
- 编译时:使用条件语法
#ifdef ... #endif
,推荐的写法,这种方案会在编译时将其他平台的代码直接剔除 - 运行时:使用
if (process.env.UNI_PLATFORM === '...') {...}
,灵活的写法,但是非本平台的代码也会包含在产物中
于我个人而言,运行时的环境变量判断一般只在开发打包插件时使用,如果你也在开发 uni 相关的插件不妨试试 uni-helper/uni-env。
在应用中我总是使用条件语法,大多数情况都很不错。当不同平台的逻辑差异比较多而大时,我们往往使用拆分为多个文件或组件来管理不同平台的逻辑,然后再使用条件语法包裹导入语句。
// #ifdef H5
import Banner from "@/components/h5Banner.vue"
// #endif
// #ifdef MP-WEIXIN
import Banner from "@/components/mpBanner.vue"
// #endif
// #ifdef APP-PLUS
import Banner from "@/components/appBanner.vue"
// #endif
...
components: { Banner }
...
emmm,这没什么问题,确实更好管理不同平台的逻辑了。
缘起
正如前文的代码所示,既然我们都已经按不同文件来管理逻辑了,那么能否实现一次导入然后按文件名自动条件编译呢?
如果你使用过 Nuxt3,那么你对 HighlightedMarkdown.server.vue
、my-directive.client.ts
、setup.global.ts
这种命名风格绝不陌生。这些不同的 suffix 都有对应的功能,*.server.vue
将被视为服务器组件,*.client.ts
将总是只在客户端执行,*.global.ts
的中间件为每个页面都自动注入。
因此,我将文件名命名规则设计为 *.UNI_PLATFORM.*
,将前文的代码将简化为:
import Banner from "@/components/Banner.vue"
...
components: { Banner }
...
当然,对应的文件树为:
components
- Banner.h5.ts # H5 平台
- Banner.mp-weixin.ts # 微信小程序平台
- Banner.app.ts # APP 平台
有了目标,开搞
思路
先看一眼 rollup 的构建流程
看样子只需要定义自定义解析器(resolveId)或者自定义加载器(load)即可。
实现
首先是命名和确定插件顺序
import type { Plugin } from 'vite'
export function VitePluginUniPlatform(): Plugin {
return {
name: 'vite-plugin-uni-platform',
enforce: 'pre',
resolveId() { },
load() { },
}
}
先来看 resolveId
:
async resolveId(source, importer, options) {
// 检查是否为刻意导入带 {platform} 后缀的文件
if (source.includes(`.${platform}`))
return null
const sourceResolution = await this.resolve(source, importer, {
...options,
skipSelf: true, // 避免无限循环
})
if (sourceResolution)
return null
// 无法解析,尝试拼接 platform 后去解析
const platformSource = source.replace(/(.*)\.(.*)$/, `$1.${platform}.$2`)
const resolution = await this.resolve(platformSource, importer, { ...options, skipSelf: true })
// 如果无法解析或是外部引用,则直接返回错误
if (!resolution || resolution.external)
return resolution
const sourceId = normalizePath(resolve(dirname(importer!), source))
const isVue = resolution.id.endsWith('vue')
// 小程序的vue文件直接使用 sourceId,避免生成类似 test.mp-weixin.wxml
// 其他平台的和其他文件直接使用 resolution
return (isMp && isVue) ? sourceId : resolution
}
然后是 load
:
// 自定义加载器,尝试将所有不带 {platform} 后缀的文件拼接 {platform} 后去加载
async load(id) {
let platformId = id
if (!id.includes(`.${platform}`))
platformId = id.replace(/(.*)\.(.*)$/, `$1.${platform}.$2`) // 拼接
// 如果存在的话,读取即可
if (platformId && platformId !== id && existsSync(platformId)) {
return readFileSync(platformId, {
encoding: 'utf-8',
})
}
}
到这里就写完了,先试试页面 pnpm run dev:h5
看看
pages
- index.h5.vue
- index.mp-weixin.vue
- index.app.vue
"pages": [
{
"path": "pages/index",
"type": "home"
},
]
good job!
Hacker
来看看小程序环境 pnpm run dev:mp-weixin
,好家伙直接异常。通过万能的 Javascript 调试终端发现是 @dcloudio/uni-cli-shared
这个包导出的 normalizePagePath
函数,在 Vite 启动前,序列化 pages.json 后,如果对应的文件不存在时直接异常!
好,那么我们复写这个函数
// overwrite uni-cli-shared utils normalizePagePath
import { resolve } from 'node:path'
import { existsSync } from 'node:fs'
// @ts-expect-error ignore
import * as utils from '@dcloudio/uni-cli-shared/dist/utils.js'
// @ts-expect-error ignore
import * as constants from '@dcloudio/uni-cli-shared/dist/constants.js'
import { isApp, inputDir as uniInputDir } from '@uni-helper/uni-env'
// 解决 MP 和 APP 平台页面文件不存在时不继续执行的问题
// @ts-expect-error ignore
utils.normalizePagePath = function (pagePath, platform) {
const absolutePagePath = resolve(uniInputDir, pagePath)
let extensions = constants.PAGE_EXTNAME
if (isApp)
extensions = constants.PAGE_EXTNAME_APP
for (let i = 0; i < extensions.length; i++) {
const extname = extensions[i]
if (existsSync(absolutePagePath + extname))
return pagePath + extname
const withPlatform = `${absolutePagePath}.${platform}${extname}`
if (existsSync(withPlatform))
return pagePath + extname
}
console.error(`${pagePath} not found`)
}
现在,当页面不存在时,检查是否有对应平台的页面,如果不存在,使用 console.error
提示
组件和 utils
试试组件和自定义函数,以 utils 为例
utils
- index.h5.ts # H5 平台
- index.mp-weixin.ts # 微信小程序平台
- index.app.ts # APP 平台
import utils from '@/utils/index'
utils.doSomething()
本以为万事大吉,但现实情况复杂的多,vite:import-analysis
插件貌似会做静态分析,如果 @utils/index.ts
不存在,就会抛出 Cannot find ...
,在我四方 debug 八方查源码最终依然不知道如何解决。
这意味着,除了页面外,必须创建一个真实导入的文件(可以为空)来过 Vite 的静态分析,如果你有好的思路欢迎在评论区讨论哈!
总结
是的,现在所有的带有平台标识符的文件都会被自动替换!不过依然还有一些问题:
- 必须创建一个真实导入的文件
- TypeScript 类型
- 更多测试用例
如果你有解决方法或者思路欢迎评论区一起探讨哈~
已发布到 NPM,使用下面的命令即可安装并使用:
pnpm i -D uni-helper/vite-plugin-uni-platform
完整代码访问 uni-helper/vite-plugin-uni-platform,如果对你有用的话帮忙点个 star.