引入 Vite 让 Flutter Web 支持 App Flavors
很遗憾的是,截止 2026 年 5 月 31 日(Flutter 3.44),Flutter 官方仍未支持在 Web 端创建 App Flavors,因此只能自己手动来实现。
App Flavors 用中文来说可以理解为应用变体。简单来说,Flavors 让你可以从同一份 Flutter 代码库构建出多个不同配置的应用(比如开发版、测试版、生产版,或者免费版、付费版),每个版本可以有不同的:
- 应用名称(如“[QA]我的 App”)
- Bundle ID / App ID(如
com.example.app.dev) - 应用图标、启动图
- 后端环境地址
- 以及其他原生层面的自定义
在 Web 端,由于 Flutter Web 默认提供的 web 文件夹基本就是一些静态文件,并且在 Flutter Build 阶段也仅仅就是将这些文件直接复制进 build 目录,如果想要简单实现 App Flavors,只能通过 HTML 内脚本的方式在运行时阶段去“动态”修改应用名称、图标和启动图。这种做法不支持动态修改 PWA 相关的设置——这些设置就是一个固定的 JSON 文件(manifest.json)——并且对 SEO 也不算友好,搜索引擎可能会抓取到动态修改名称之前的那些配置。
所以要想在 Flutter Web 上支持 App Flavors,那就得动态输出这个 web 目录,也就很自然的选择使用 Vite 来构建整个 web 目录。
- 使用
npm create vite@latest命令创建vite目录,选择vanilla-ts模板。 - 将原
web目录内有用的文件内容迁移到vite目录,并且删除vite初始化生成的无用内容 - 移除
web目录,并且将web添加到.gitignore文件
+# web platform source folder (this folder should be generated by vite)
+web.gitignore 文件
.
├── analysis_options.yaml
├── android
├── devtools_options.yaml
├── ios
├── lib
├── linux
├── macos
├── pubspec.yaml
├── test
+├── vite
-├── web
└── windows项目根目录
.
├── .gitignore
+├── env # web 端环境变量
+│ ├── .env.prod
+│ └── .env.qa
├── index.html
├── package-lock.json
├── package.json
├── src
│ └── main.ts
+├── static # web 端变体分别使用到的静态文件
+│ ├── prod
+│ │ ├── manifest.json
+│ │ ├── apple-touch-icon.png
+│ │ ├── favicon-96x96.png
+│ │ ├── favicon.ico
+│ │ ├── web-app-manifest-192x192.png
+│ │ └── web-app-manifest-512x512.png
+│ └── qa
+│ ├── manifest.json
+│ ├── apple-touch-icon.png
+│ ├── favicon-96x96.png
+│ ├── favicon.ico
+│ ├── web-app-manifest-192x192.png
+│ └── web-app-manifest-512x512.png
├── tsconfig.json
└── vite.config.tsvite 目录
import { createLogger, defineConfig } from 'vite'
import { Target, viteStaticCopy } from 'vite-plugin-static-copy'
export default defineConfig((env) => {
// 屏蔽无意义的警告日志
const logger = createLogger()
logger.warn = (message, options) => {
if (message.includes('flutter_bootstrap.js')) return
console.warn(message, options)
}
return {
build: {
emptyOutDir: true,
outDir: '../web',
},
envDir: 'env',
customLogger: logger,
plugins: [
viteStaticCopy({
targets: [
// 根据 MODE 复制不同的 manifest.json 文件
...copyManifestFiles(env.mode),
// 根据 MODE 复制不同的 Favicon 文件
...copyFaviconFiles(env.mode),
]
})
]
}
})
const copyManifestFiles = (mode: string): Target[] => {
return [{
src: `static/${mode}/manifest.json`,
dest: '',
rename: { stripBase: true }
}]
}
const copyFaviconFiles = (mode: string): Target[] => {
const fileList = [
'apple-touch-icon.png',
'favicon-96x96.png',
'favicon.ico',
'web-app-manifest-192x192.png',
'web-app-manifest-512x512.png',
]
return fileList.map(file => ({
src: `static/${mode}/${file}`,
dest: '',
rename: { stripBase: true }
}))
}vite.config.ts 文件
Member discussion