为什么捐赠
API 浏览器
联系站长
Quasar CLI with Vite - @quasar/app-vite
Quasar CLI with Vite 升级指南

@quasar/app-vite v2

App Extensions 开发者注意事项

你可能需要发布新版本的 Quasar App Extensions 以支持新的 @quasar/app-vite。如果你没有修改 quasar.config 配置,那么只需要更改以下内容即可:

api.compatibleWith(
  '@quasar/app-vite',
- '^1.0.0'
+ '^1.0.0 || ^2.0.0'
)

重要的破坏性变更

  • Node.js 最低版本要求现在是 22(主要是因为 Vite 8)
  • 我们已经将整个 Quasar 项目文件夹转向 ESM 风格,因此许多默认的项目文件现在需要 ESM 代码(虽然支持使用 .cjs 作为这些文件的扩展名,但如果你不想更改任何内容,你很可能需要重命名扩展名)。例如,/quasar.config.js 文件现在也默认按 ESM 处理(所以如果你仍然想要一个 CommonJs 文件,请将扩展名从 .js 改为 .cjs)。
  • 由于 @quasar/testing-* 包的最新更新,“test” 命令已被移除。参见此处
  • “clean” 命令已被重新设计。在你升级后的 Quasar 项目文件夹中输入 “quasar clean -h” 获取更多信息。
  • TypeScript 检测基于 quasar.config 文件是否为 TS 形式(quasar.config.ts)以及 tsconfig.json 文件是否存在。
  • TypeScript tsconfig.json 预设已被自动生成的 .quasar/tsconfig.json 文件替代。这更加灵活并带来了新功能,下面会详细介绍。
  • feat+refactor(app-vite):支持同时运行多个模式 + dev/build(巨大的工程!)
  • SSR 和 Electron 模式现在以 ESM 格式构建。
  • 全新的 BEX 模式,具有显著的新功能和更好的易用性(现在支持 Chrome 的 HMR!)。
  • 放弃了对内部 linting 系统的支持(quasar.config 文件 > eslint)。应该使用 vite-plugin-checker 代替。
  • 放弃了对 Vuex 的支持。Pinia 已经成为 Vue 3 的官方状态管理库有一段时间了。Vuex 在 app-vite v1 中已被弃用,并且在新结构中存在问题,所以现在已被彻底移除。你仍然可以像使用任何 Vue 插件一样使用 Vuex,但你需要自己管理所有内容(安装 store、hydration、boot 文件中没有 store 参数等),并且不会收到 Quasar CLI 的任何支持。你可能需要修补 Vuex 以使其与 TypeScript 一起工作。我们建议迁移到 Pinia。
  • 我们将在下面详细说明每个 Quasar 模式的更多破坏性变更

新功能亮点

以下一些工作已经被移植到旧的 @quasar/app-vite v1,但在此列出以供读者了解。

  • feat(app-vite):升级到 Vite 8
  • feat(app-vite):支持同时运行多个 quasar dev/build 命令(例如:可以同时运行 “quasar dev -m capacitor” 和 “quasar dev -m ssr” 和 “quasar dev -m capacitor -T ios”)
  • feat(app-vite):整体更好的 TS 类型支持
  • refactor(app-vite):将 CLI 移植到 ESM 格式(重大工程!特别是为了支持 Vite 8 和 SSR)
  • feat(app-vite):支持多种格式的 quasar.config 文件(.js、.mjs、.ts、.cjs)
  • feat(app-vite):改进 quasarConfOptions,为其生成类型,改进文档(fix: #14069)(#15945)
  • feat(app-vite):当 quasar.config 文件中的某个导入发生变化时,自动重新加载应用
  • feat(app-vite):TS 检测也会考虑 quasar.config 文件格式(quasar.config.ts)
  • feat(app-vite):CLI 命令简写 “quasar dev/build -m ios/android” 现在默认对应 Capacitor 模式而不是 Cordova(2.0.0-beta.12+)
  • feat(app-vite):支持使用 HTTPS 进行 SSR 开发
  • feat(app-vite):env dotfiles 支持 #15303
  • feat(app-vite):新的 quasar.config 文件属性:build > envFolder(字符串)和 envFiles(字符串数组)
  • feat(app-vite):通过 quasar.config 文件更改应用 url 时自动重新打开浏览器(如果配置了的话)
  • feat&perf(app-vite):更快且更准确的算法来确定要使用的 node 包管理器
  • feat(app-vite):升级依赖
  • feat(app-vite):移除 CLI 模板中对 Electron 6-8 bug 的临时解决方案(#15845)
  • feat(app-vite):移除 Capacitor v5+ 的 bundleWebRuntime 配置
  • feat(app-vite):默认使用 workbox v7
  • feat(app-vite):quasar.config > pwa > injectPwaMetaTags 现在也可以是一个函数:(({ pwaManifest, publicPath }) => string);
  • feat(app-vite):quasar.config > build > htmlMinifyOptions
  • feat(app-vite):当使用 vue devtools 时自动查找可用端口;支持同时运行多个带有 vue devtools 的 CLI 实例
  • perf(app-vite):根据宿主项目,以特定的 esm 或 cjs 形式进行 SSR render-template;通过变量进行插值
  • perf(app-vite):仅对 “dev” 命令验证 quasar.conf 服务器地址
  • feat(app-vite):为每个实例选择新的 electron inspect 端口
  • feat(app-vite):Electron - 现在可以加载多个预加载脚本
  • refactor(app-vite):AE 支持 - 更好和更高效的算法
  • feat(app-vite):AE 支持 ESM 格式
  • feat(app-vite):AE 支持 TS 格式(通过构建步骤)
  • feat(app-vite):AE API 新方法 -> hasTypescript() / hasLint() / getStorePackageName() / getNodePackagerName()
  • feat(app-vite):AE -> Prompts API(以及 prompts 默认导出函数可以是异步的)
  • refactor(app-vite):“clean” 命令现在的工作方式不同了,因为 CLI 可以在同一个项目文件夹上运行多个实例(在 dev 或 build 上同时使用多个模式)
  • feat(app-vite):支持 Bun 作为包管理器 #16335
  • feat(app-vite):对于默认的 /src-ssr 模板 -> prod ssr -> 出错时,如果启用了调试构建则打印错误堆栈
  • feat(app-vite):扩展 build > vitePlugins 形式(额外的 { server?: boolean, client?: boolean } 参数)
  • feat+refactor(app-vite):BEX -> 完全重写和重新设计了 Quasar Bridge(带有大量新功能);自动从 bex manifest 本身推断后台脚本文件和内容脚本文件;支持编译其他 js/ts 文件以便你动态加载/注入;打开弹出窗口时不再有 3 秒延迟;不再有 “dom” 脚本(直接使用内容脚本);Bridge 在 App(/src)中通过 $q 对象或 window.QBexBridge 全局可用
  • feat(app-vite):Chrome 的 BEX 支持 HMR(热模块重载)
  • feat(app-vite):支持从 build > extendViteConf 返回覆盖配置

开始升级过程

TIP

如果你不确定是否会不小心跳过任何推荐的更改,你可以随时使用 @quasar/app-vite v2 搭建一个新的项目文件夹,然后从那里轻松地移植你的应用。大部分更改都涉及不同的项目文件夹配置文件,主要不是你的 /src 文件。


$ yarn create quasar

当被问到 "Pick Quasar App CLI variant" 时,选择:"Quasar App CLI with Vite"。

准备工作:

  • 如果使用全局安装的 Quasar CLI(@quasar/cli),请确保你用的是最新版本。这是因为需要支持多种格式的 quasar.config 文件。

  • 再次强调,Node.js 的最低支持版本现在是 v22(始终使用 Node.js 的 LTS 版本 - 版本越高越好)。

  • 编辑你的 /package.json 中的 @quasar/app-vite 条目,将其设置为 ^2.0.0

    /package.json

    "devDependencies": {
    - "@quasar/app-vite": "^1.0.0",
    + "@quasar/app-vite": "^2.0.0"
    }

    然后运行 yarn/npm/pnpm/bun install。

  • 将你的 /quasar.config.js 文件转换为 ESM 格式(推荐做法,否则将文件扩展名重命名为 .cjs 并使用 CommonJs 格式)。另外注意 wrappers 导入的变化,稍后会详细介绍。

    /quasar.config.js file

    - const { configure } = require('quasar/wrappers')
    + import { defineConfig } from '#q-app/wrappers'
    
    - module.export = configure((ctx) => {
    + export default defineConfig((ctx) => {
        return {
          // ...
        }
      })

    TypeScript 提示

    如果你愿意,现在也可以用 TS 编写这个文件(将 /quasar.config.js 重命名为 /quasar.config.ts – 注意 .ts 文件扩展名)。

  • 在你的 /package.json 中设置 typemodule。不要忽略这一步!

    /package.json

    {
    + "type": "module"
    }

    如果 postcss.config.js 还不是 ESM 格式,将其转换为 ESM。

    /postcss.config.js

    import autoprefixer from "autoprefixer";
    // import rtlcss from 'postcss-rtlcss'
    
    export default {
      plugins: [
        // https://github.com/postcss/autoprefixer
        autoprefixer({
          overrideBrowserslist: [
            "last 4 Chrome versions",
            "last 4 Firefox versions",
            "last 4 Edge versions",
            "last 4 Safari versions",
            "last 4 Android versions",
            "last 4 ChromeAndroid versions",
            "last 4 FirefoxAndroid versions",
            "last 4 iOS versions",
          ],
        }),
    
        // https://github.com/elchininet/postcss-rtlcss
        // If you want to support RTL css, then
        // 1. yarn/pnpm/bun/npm install postcss-rtlcss
        // 2. optionally set quasar.config.js > framework > lang to an RTL language
        // 3. uncomment the following line (and its import statement above):
        // rtlcss()
      ],
    };

  • 你可能想要在 /.gitignore 文件中添加以下内容。/quasar.config.*.temporary.compiled* 条目是指当你的 /quasar.config 文件出问题时留下的用于检查的文件(可以通过 quasar clean 命令删除):

    /.gitignore

    .DS_Store
    .thumbs.db
    node_modules
    
    # Quasar core related directories
    .quasar
    /dist
    /quasar.config.*.temporary.compiled*
    
    # local .env files
    .env.local*
    
    # Cordova related directories and files
    /src-cordova/node_modules
    /src-cordova/platforms
    /src-cordova/plugins
    /src-cordova/www
    
    # Capacitor related directories and files
    /src-capacitor/www
    /src-capacitor/node_modules
    
    # Log files
    npm-debug.log*
    yarn-debug.log*
    yarn-error.log*
    
    # Editor directories and files
    .idea
    *.suo
    *.ntvs*
    *.njsproj
    *.sln

  • 确保更新你的 /quasar.config 文件以满足最新的规范和类型要求。请检查下面的所有章节。

  • 如果你安装了 dotenv 包并在 quasar.config 文件中使用它,请卸载它并使用我们 CLI 原生的 dotenv 支持

    /quasar.config file

    - build: {
    -  env: require('dotenv').config().parsed
    - }

  • 如果你有 linting,请通过访问 Linter 页面来检查你的设置。你需要:

    1. 卸载所有当前的 linting 包
    2. /.eslintrc.cjs 重命名为 /eslint.config.js(查看上面的链接了解新文件应该是什么样子)
    3. /.eslintignore 迁移到新的 /eslint.config.js
    4. 删除 /.eslintignore
    5. 安装新的依赖(查看上面的链接)。
    6. 编辑你的 /package.json > scripts > lint:

    /package.json

    "scripts": {
    -  "lint": "eslint --ext .js,.ts,.vue ./"
    
    // for non-TS projects:
    +  "lint": "eslint -c ./eslint.config.js \"./src*/**/*.{js,cjs,mjs,vue}\""
    // for TS projects:
    +  "lint": "eslint -c ./eslint.config.js \"./src*/**/*.{ts,js,cjs,mjs,vue}\""
    }

  • 类型功能标志文件现在将在 .quasar 文件夹中自动生成。所以,你必须删除它们:


    # in project folder root:
    $ npx rimraf -g ./src*/*-flag.d.ts
    $ quasar prepare

  • 我们已经弃用了所有来自 quasar/wrappers 的导入。你仍然可以使用它们,但我们强烈建议切换到新的 #q-app/wrappers,如下所示:

    The wrapper functions

    - import { configure } from 'quasar/wrappers'
    + import { defineConfig } from '#q-app/wrappers'
    
    - import { boot } from 'quasar/wrappers'
    + import { defineBoot } from '#q-app/wrappers'
    
    - import { preFetch } from 'quasar/wrappers'
    + import { definePreFetch } from '#q-app/wrappers'
    
    - import { route } from 'quasar/wrappers'
    + import { defineRouter } from '#q-app/wrappers'
    
    - import { store } from 'quasar/wrappers'
    + import { defineStore } from '#q-app/wrappers'
    
    - import { ssrMiddleware } from 'quasar/wrappers'
    + import { defineSsrMiddleware } from '#q-app/wrappers'
    
    - import { ssrCreate } from 'quasar/wrappers'
    + import { defineSsrCreate } from '#q-app/wrappers'
    
    - import { ssrListen } from 'quasar/wrappers'
    + import { defineSsrListen } from '#q-app/wrappers'
    
    - import { ssrClose } from 'quasar/wrappers'
    + import { defineSsrClose } from '#q-app/wrappers'
    
    - import { ssrServeStaticContent } from 'quasar/wrappers'
    + import { defineSsrServeStaticContent } from '#q-app/wrappers'
    
    - import { ssrRenderPreloadTag } from 'quasar/wrappers'
    + import { defineSsrRenderPreloadTag } from '#q-app/wrappers'

  • 对于非 TS 项目,更新你的 /jsconfig.json 文件。是的,里面包含 tsconfig 字样,这没有错。

    /jsconfig.json

    {
      "extends": "./.quasar/tsconfig.json"
    }

  • 对于 TypeScript 项目@quasar/app-vite/tsconfig-preset 已被移除,所以更新你的 /tsconfig.json 文件以继承新的自动生成的 .quasar/tsconfig.json 文件。除非你确实知道自己在做什么,否则去掉其他所有配置,只保留 extends 作为文件中唯一的选项。

    /tsconfig.json

    {
    +  "extends": "./.quasar/tsconfig.json"
    -  "extends": "@quasar/app-vite/tsconfig-preset",
    -  "compilerOptions": {
    -    "baseUrl": "."
    -  },
    - "include": [ ... ],
    - "exclude": [ ... ]
    }

    底层配置现在已经不同了,所以请查看生成的文件中的新选项,看看是否需要对你的 tsconfig.json 文件做进一步调整。以下是生成的 tsconfig(非严格模式)示例供参考:

    /.quasar/tsconfig.json

    {
      "compilerOptions": {
        "esModuleInterop": true,
        "skipLibCheck": true,
        "target": "esnext",
        "allowJs": true,
        "resolveJsonModule": true,
        "moduleDetection": "force",
        "isolatedModules": true,
        "module": "preserve",
        "noEmit": true,
        "lib": [
          "esnext",
          "dom",
          "dom.iterable"
        ],
        "paths": { ... }
      },
      "exclude": [ ... ]
    }

    如果你使用 ESLint,我们建议在 ESLint 配置中启用 @typescript-eslint/consistent-type-imports 规则。如果你没有设置 linting,我们建议在 tsconfig.json 文件中使用 verbatimModuleSyntax 作为替代方案(与 ESLint 规则不同,它不支持自动修复)。这些更改将帮助你统一常规导入和仅类型导入的写法。更多信息和设置方法请阅读 typescript-eslint 博客 - Consistent Type Imports and Exports: Why and How。示例如下:

    /eslint.config.js

    rules: {
      // ...
      '@typescript-eslint/consistent-type-imports': [
        'error',
        { prefer: 'type-imports' },
      ],
      // ...
    }

    你可以使用 quasar.config file > build > typescript 来控制 TypeScript 相关的行为。在你的配置中添加以下部分:

    /quasar.config.ts

    build: {
    +  typescript: {
    +    strict: true, // (recommended) enables strict settings for TypeScript
    +    vueShim: true, // required when using ESLint with type-checked rules, will generate a shim file for `*.vue` files
    +    extendTsConfig (tsConfig) {
    +      // You can use this hook to extend tsConfig dynamically
    +      // For basic use cases, you can still update the usual tsconfig.json file to override some settings
    +    },
    +  }
    }

    大多数严格选项在之前的预设中已经启用了。所以,你应该能够将 strict 选项设为 true 而不会遇到太多问题。但如果你确实遇到了问题,你可以选择更新代码以满足更严格的规则,或者在 tsconfig.json 文件中将"有问题的"选项暂时设为 false,直到你有时间修复它们。

    src/quasar.d.tssrc/shims-vue.d.ts 文件现在将在 .quasar 文件夹中自动生成。所以,你必须删除这些文件:


    # in project folder root:
    $ npx rimraf src/quasar.d.ts src/shims-vue.d.ts

    如果你使用 ESLint 的类型检查规则,启用 vueShim 选项可以保持之前 shim 文件的行为。如果你的项目不启用这个选项也能正常工作,那就不需要启用它。

    /quasar.config.ts

    build: {
      typescript: {
    +    vueShim: true // required when using ESLint with type-checked rules, will generate a shim file for `*.vue` files
      }
    }

    得益于这一改动,Capacitor 依赖现在能正确链接到项目的 TypeScript 配置中。这意味着你不再需要重复安装依赖——以前要在 /src-capacitor 和根目录各装一次。所以,你可以从根目录的 package.json 文件中移除 Capacitor 依赖。今后只需要在 /src-capacitor 文件夹中安装 Capacitor 依赖即可。

    这一改动的另一个好处是文件夹别名(quasar.config file > build > alias)会被 TypeScript 自动识别。所以,你可以移除 tsconfig.json > compilerOptions > paths。如果你之前使用了 vite-tsconfig-paths 之类的插件,你可以卸载它,改用 quasar.config file > build > alias 作为唯一的真实来源。

    要正确运行类型检查和 linting,需要 .quasar/tsconfig.json 文件存在。该文件会在运行 quasar devquasar build 命令时自动生成。但作为一个轻量级的替代方案,有一个新的 CLI 命令 quasar prepare,它会生成 .quasar/tsconfig.json 文件和一些类型文件。这在 CI/CD 流水线中特别有用。

    $ quasar prepare

    你可以将它添加为 postinstall 脚本,确保在安装依赖后自动运行。这在有人第一次拉取项目时会很有帮助。

    /package.json

    {
      "scripts": {
        "postinstall": "quasar prepare"
      }
    }

    如果你使用 Pinia,我们现在会在 .quasar/pinia.d.ts 中自动增强 router 属性。所以,你可以从 src/stores/index.ts 文件的 PiniaCustomProperties 接口中移除 router 属性。它仍然会像以前一样工作,但建议移除以避免混淆。

    /src/stores/index.ts

    import { defineStore } from '#q-app/wrappers'
    import { createPinia } from 'pinia'
    - import { type Router } from 'vue-router';
    
    /*
     * When adding new properties to stores, you should also
     * extend the `PiniaCustomProperties` interface.
    - * @see https://pinia.vuejs.org/core-concepts/plugins.html#typing-new-store-properties
    + * @see https://pinia.vuejs.org/core-concepts/plugins.html#Typing-new-store-properties
     */
    declare module 'pinia' {
      export interface PiniaCustomProperties {
    -    readonly router: Router;
    +    // add your custom properties here, if any
      }
    }

Capacitor / Cordova 模式变更

UI 代码(/src)现在可以使用 process.env.TARGET(值为 “ios” 或 “android”)。

PWA 模式变更

register-service-worker 依赖不再由 CLI 自动提供。你需要自己在项目文件夹中安装它。


$ yarn add register-service-worker@^1.0.0

编辑你的 /src-pwa/custom-service-worker.js 文件:

/src-pwa/custom-service-worker.js

- if (process.env.MODE !== 'ssr' || process.env.PROD) {
+ if (process.env.PROD) {
  registerRoute(
    new NavigationRoute(
      createHandlerBoundToURL(process.env.PWA_FALLBACK_HTML),
-     { denylist: [/sw\.js$/, /workbox-(.)*\.js$/] }
+     { denylist: [new RegExp(process.env.PWA_SERVICE_WORKER_REGEX), /workbox-(.)*\.js$/] }
    )
  )
}

/quasar.config 文件中也有一些细微的变更:

/quasar.config file

sourceFiles: {
- registerServiceWorker: 'src-pwa/register-service-worker',
- serviceWorker: 'src-pwa/custom-service-worker',
+ pwaRegisterServiceWorker: 'src-pwa/register-service-worker',
+ pwaServiceWorker: 'src-pwa/custom-service-worker',
+ pwaManifestFile: 'src-pwa/manifest.json',
  // ...
},

pwa: {
- workboxMode?: "generateSW" | "injectManifest";
+ workboxMode?: "GenerateSW" | "InjectManifest";

- // useFilenameHashes: false,
+ // Moved to quasar.config > build > useFilenameHashes

  /**
   * Auto inject the PWA meta tags?
   * If using the function form, return HTML tags as one single string.
   * @default true
   */
- injectPwaMetaTags?: boolean;
+ injectPwaMetaTags?: boolean | ((injectParam: InjectPwaMetaTagsParams) => string);
+ // see below for the InjectPwaMetaTagsParams interface

  // ...
}

// additional types for injectPwaMetaTags
interface InjectPwaMetaTagsParams {
  pwaManifest: PwaManifestOptions;
  publicPath: string;
}
interface PwaManifestOptions {
  id?: string;
  background_color?: string;
  categories?: string[];
  description?: string;
  // ...
}

Electron 模式变更

WARNING

生产构建产物将以 ESM 格式编译,从而充分利用 Electron 对 ESM 格式的支持。

TIP

你可能想要将 electron 包升级到最新版本,以便它能处理 ESM 格式。

大部分变更涉及编辑你的 /src-electron/electron-main.js 文件:

Icon path

+import { fileURLToPath } from 'node:url'

+const currentDir = fileURLToPath(new URL('.', import.meta.url))

function createWindow () {
  mainWindow = new BrowserWindow({
-   icon: path.resolve(__dirname, 'icons/icon.png'), // tray icon
+   icon: path.resolve(currentDir, 'icons/icon.png'), // tray icon
    // ...
  })
Preload script

import { fileURLToPath } from 'node:url'

const currentDir = fileURLToPath(new URL('.', import.meta.url))

function createWindow () {
  mainWindow = new BrowserWindow({
    // ...
    webPreferences: {
-     preload: path.resolve(__dirname, process.env.QUASAR_ELECTRON_PRELOAD)
+     preload: path.resolve(
+       currentDir,
+       path.join(process.env.QUASAR_ELECTRON_PRELOAD_FOLDER, 'electron-preload' + process.env.QUASAR_ELECTRON_PRELOAD_EXTENSION)
+     )
    }
  })

WARNING

编辑 /quasar.config.js 以指定你的预加载脚本:

/quasar.config file

sourceFiles: {
- electronPreload?: string;
},

electron: {
+ // Electron preload scripts (if any) from /src-electron, WITHOUT file extension
+ preloadScripts: [ 'electron-preload' ],
}

如你所见,你现在可以在需要时指定多个预加载脚本。
- function createWindow () {
+ async function createWindow () {
   // ...
-  mainWindow.loadURL(process.env.APP_URL)
+  if (process.env.DEV) {
+    await mainWindow.loadURL(process.env.APP_URL)
+  } else {
+    await mainWindow.loadFile('index.html')
+  }

最终,新文件应该是这样的:

The new /src-electron/electron-main.js

import { app, BrowserWindow } from "electron";
import path from "node:path";
import os from "node:os";
import { fileURLToPath } from "node:url";

// needed in case process is undefined under Linux
const platform = process.platform || os.platform();

const currentDir = fileURLToPath(new URL(".", import.meta.url));

let mainWindow;

async function createWindow() {
  /**
   * Initial window options
   */
  mainWindow = new BrowserWindow({
    icon: path.resolve(currentDir, "icons/icon.png"), // tray icon
    width: 1000,
    height: 600,
    useContentSize: true,
    webPreferences: {
      contextIsolation: true,
      // More info: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/electron-preload-script
      preload: path.resolve(
        currentDir,
        path.join(
          process.env.QUASAR_ELECTRON_PRELOAD_FOLDER,
          "electron-preload" + process.env.QUASAR_ELECTRON_PRELOAD_EXTENSION,
        ),
      ),
    },
  });

  if (process.env.DEV) {
    await mainWindow.loadURL(process.env.APP_URL);
  } else {
    await mainWindow.loadFile("index.html");
  }

  if (process.env.DEBUGGING) {
    // if on DEV or Production with debug enabled
    mainWindow.webContents.openDevTools();
  } else {
    // we're on production; no access to devtools pls
    mainWindow.webContents.on("devtools-opened", () => {
      mainWindow.webContents.closeDevTools();
    });
  }

  mainWindow.on("closed", () => {
    mainWindow = null;
  });
}

app.whenReady().then(createWindow);

app.on("window-all-closed", () => {
  if (platform !== "darwin") {
    app.quit();
  }
});

app.on("activate", () => {
  if (mainWindow === null) {
    createWindow();
  }
});

SSR 模式变更

WARNING

生产构建产物将以 ESM 格式编译。

/src-ssr/middlewares/*

- import { ssrMiddleware } from 'quasar/wrappers'
+ import { defineSsrMiddleware } from '#q-app/wrappers'

- export default ssrMiddleware({
+ export default defineSsrMiddleware(({
  app,
  port,
  resolve,
  publicPath,
  folders,
  render,
  serve
}) => {
  // something to do with the server "app"
})

其他变更涉及编辑你的 /src-ssr/server.js 文件。由于你现在在开发应用时也可以使用 HTTPS,因此需要对文件做以下更改:

/src-ssr/server.js > listen

- import { ssrListen } from 'quasar/wrappers'
+ import { defineSsrListen } from '#q-app/wrappers'

- export const listen = ssrListen(async ({ app, port, isReady }) => {
+ // notice: devHttpsApp param which will be a Node.js httpsServer (on DEV only) and if https is enabled
+ // notice: no "isReady" param (starting with 2.0.0-beta.16+)
+ // notice: defineSsrListen() param can still be async (below it isn't)
+ export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
-   await isReady()
-   return app.listen(port, () => {
+   const server = devHttpsApp || app
+   return server.listen(port, () => {
      if (process.env.PROD) {
        console.log('Server listening at port ' + port)
      }
    })
  })

最终,它应该是这样的:

/src-ssr/server.js > listen

import { defineSsrListen } from "#q-app/wrappers";
export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
  const server = devHttpsApp || app;
  return server.listen(port, () => {
    if (process.env.PROD) {
      console.log("Server listening at port " + port);
    }
  });
});

对于 serverless 方案,“listen” 部分应该是这样的:

/src-ssr/server.js > listen

export const listen = defineSsrListen(({ app, devHttpsApp, port }) => {
  if (process.env.DEV) {
    const server = devHttpsApp || app;
    return server.listen(port, () => {
      console.log("Server listening at port " + port);
    });
  } else {
    // in production
    // return an object with a "handler" property
    // that the server script will named-export
    return { handler: app };
  }
});

接下来,serveStaticContent 函数有变化:

/src-ssr/server.js > serveStaticContent

- import { serveStaticContent }
+ import { defineSsrServeStaticContent } from '#q-app/wrappers'

- export const serveStaticContent = ssrServeStaticContent((path, opts) => {
-  return express.static(path, { maxAge, ...opts })
- })

+ /**
+ * Should return a function that will be used to configure the webserver
+ * to serve static content at "urlPath" from "pathToServe" folder/file.
+ *
+ * Notice resolve.urlPath(urlPath) and resolve.public(pathToServe) usages.
+ *
+ * Can be async: defineSsrServeStaticContent(async ({ app, resolve }) => {
+ * Can return an async function: return async ({ urlPath = '/', pathToServe = '.', opts = {} }) => {
+ */
+ export const serveStaticContent = defineSsrServeStaticContent(({ app, resolve }) => {
+  return ({ urlPath = '/', pathToServe = '.', opts = {} }) => {
+    const serveFn = express.static(resolve.public(pathToServe), { maxAge, ...opts })
+    app.use(resolve.urlPath(urlPath), serveFn)
+  }
+ })

此外,renderPreloadTag() 函数现在可以接收一个额外的参数(ssrContext):

/src-ssr/server.js

- import { ssrRenderPreloadTag } from 'quasar/wrappers'
+ import { defineSsrRenderPreloadTag } from '#q-app/wrappers'

+ export const renderPreloadTag = defineSsrRenderPreloadTag((file, { ssrContext }) => {
+  // ...
+ })

对于 TS 开发者,你还需要对 /src-ssr/middlewares 文件做一个小改动:

For TS devs

+ import { type Request, type Response } from 'express';
// ...
- app.get(resolve.urlPath('*'), (req, res) => {
+ app.get(resolve.urlPath('*'), (req: Request, res: Response) => {

/quasar.config 文件也有一些新增内容:

/quasar.config file

ssr: {
  // ...

  /**
   * When using SSR+PWA, this is the name of the
   * PWA index html file that the client-side fallbacks to.
   * For production only.
   *
   * Do NOT use index.html as name as it will mess SSR up!
   *
   * @default 'offline.html'
   */
- ssrPwaHtmlFilename?: string;
+ pwaOfflineHtmlFilename?: string;

  /**
   * Tell browser when a file from the server should expire from cache
   * (the default value, in ms)
   * Has effect only when server.static() is used
   */
- maxAge?: number;

  /**
   * Extend/configure the Workbox GenerateSW options
   * Specify Workbox options which will be applied on top of
   *  `pwa > extendGenerateSWOptions()`.
   * More info: https://developer.chrome.com/docs/workbox/the-ways-of-workbox/
   */
+ pwaExtendGenerateSWOptions?: (config: object) => void;

  /**
   * Extend/configure the Workbox InjectManifest options
   * Specify Workbox options which will be applied on top of
   *  `pwa > extendInjectManifestOptions()`.
   * More info: https://developer.chrome.com/docs/workbox/the-ways-of-workbox/
   */
+ pwaExtendInjectManifestOptions?: (config: object) => void;
}

BEX 模式变更

有许多改进:

  • BEX 模式现在支持 HMR(热模块重载)了!!!(仅限 Chrome)
  • 完全重写和重新设计了 Quasar Bridge,支持:
    • 在你的 bex 的任何部分(app、内容脚本、后台脚本)之间直接发送/接收消息
    • 可以完全不使用 bridge
    • 通过 bridge 发送和接收消息时的错误处理
    • 更好地处理内部资源以避免内存泄漏(之前的实现中存在一些边界情况)
    • 调试模式(所有 bridge 通信都会输出到浏览器控制台)
    • 破坏性变更要点:后台和内容脚本的 bridge 初始化方式;bridge.on() 调用的响应方式;bridge.send() 调用
    • Bridge 现在在 /src/ 中的 App 里全局可用(无论使用哪个文件:boot 文件、路由初始化、App.vue、任何 Vue 组件……)通过 $q 对象window.QBexBridge 访问
  • 一个统一的 manifest 文件,可以从中分别提取 chrome 和 firefox 的版本。
  • 自动从 BEX manifest 文件推断后台脚本文件和内容脚本文件。
  • 支持编译其他 js/ts 文件,以便你动态加载/注入。
  • 打开弹出窗口时不再有 3 秒延迟。
  • “dom” 脚本支持已被移除。只需将你的逻辑从那里移到一个内容脚本中即可。
  • 全新的、更简洁的后台/内容脚本 API。

依赖

不再需要 events 依赖。如果你已经安装了它,请卸载:


$ yarn remove events

CLI 命令

quasar devquasar build 命令现在需要显式指定目标浏览器(chrome 或 firefox)。如果你想同时为两者开发,可以启动两个 quasar dev 命令。

$ quasar dev -m bex -T <chrome|firefox>
$ quasar dev -m bex --target <chrome|firefox>

$ quasar build -m bex -T <chrome|firefox>
$ quasar build -m bex --target <chrome|firefox>

注意,/src/src-bex 中的代码现在可以使用 process.env.TARGET(值为 “chrome” 或 “firefox”)。

Chrome 的 HMR

开发体验的重大提升:

  • devtools/options/popup 页面完整的 HMR 支持
  • 当修改后台脚本时,扩展会自动重新加载。
  • 当修改内容脚本时,扩展会自动重新加载,并且使用这些内容脚本的标签页会自动刷新。

quasar.config 文件

/quasar.config file

sourceFiles: {
+ bexManifestFile: 'src-bex/manifest.json',
  // ...
},
bex: {
- contentScripts: [] // no longer needed as scripts are
-                    // now extracted from the manifest file
+ extraScripts: []
}

BEX manifest 文件

我们现在提供了一种方式来区分每个目标浏览器(chrome 和 firefox)的 manifest。

注意,manifest 文件现在包含三个根属性:allchromefirefox。chrome 的 manifest 是由 all+chrome 深度合并生成的,而 firefox 的 manifest 是由 all+firefox 生成的。你甚至可以为每个目标设置不同的 manifest 版本。

{
  "all": {
    "manifest_version": 3,

    "icons": {
      "16": "icons/icon-16x16.png",
      "48": "icons/icon-48x48.png",
      "128": "icons/icon-128x128.png"
    },

    "permissions": ["storage", "tabs", "activeTab"],

    "host_permissions": ["*://*/*"],
    "content_security_policy": {
      "extension_pages": "script-src 'self'; object-src 'self';"
    },
    "web_accessible_resources": [
      {
        "resources": ["*"],
        "matches": ["*://*/*"]
      }
    ],

    "action": {
      "default_popup": "www/index.html"
    },

    "content_scripts": [
      {
        "matches": ["<all_urls>"],
        "css": ["assets/content.css"],
        "js": ["my-content-script.js"]
      }
    ]
  },

  "chrome": {
    "background": {
      "service_worker": "background.js"
    }
  },

  "firefox": {
    "background": {
      "scripts": ["background.js"]
    }
  }
}

TS 开发者注意

你的后台脚本和内容脚本使用 .ts 扩展名。在 manifest.json 文件中也使用该扩展名!例如:“background.ts”、“my-content-script.ts”。虽然浏览器厂商只支持 .js 扩展名,但 Quasar CLI 会自动转换文件扩展名。

脚本文件

Background script

/**
 * Importing the file below initializes the extension background.
 *
 * Warnings:
 * 1. Do NOT remove the import statement below. It is required for the extension to work.
 *    If you don't need createBridge(), leave it as "import '#q-app/bex/background'".
 * 2. Do NOT import this file in multiple background scripts. Only in one!
 * 3. Import it in your background service worker (if available for your target browser).
 */
import { createBridge } from '#q-app/bex/background'

/**
 * Call createBridge() to enable communication with the app & content scripts
 * (and between the app & content scripts), otherwise skip calling
 * createBridge() and use no bridge.
 */
const bridge = createBridge({ debug: false })
Content script

/**
 * Importing the file below initializes the content script.
 *
 * Warning:
 *   Do not remove the import statement below. It is required for the extension to work.
 *   If you don't need createBridge(), leave it as "import '#q-app/bex/content'".
 */
import { createBridge } from '#q-app/bex/content'

// The use of the bridge is optional.
const bridge = createBridge({ debug: false })
/**
 * bridge.portName is 'content@<path>-<number>'
 *   where <path> is the relative path of this content script
 *   filename (without extension) from /src-bex
 *   (eg. 'my-content-script', 'subdir/my-script')
 *   and <number> is a unique instance number (1-10000).
 */

// Attach initial bridge listeners...

/**
 * Leave this AFTER you attach your initial listeners
 * so that the bridge can properly handle them.
 *
 * You can also disconnect from the background script
 * later on by calling bridge.disconnectFromBackground().
 *
 * To check connection status, access bridge.isConnected
 */
bridge.connectToBackground()
  .then(() => {
    console.log('Connected to background')
  })
  .catch(err => {
    console.error('Failed to connect to background:', err)
  })
App (/src/...) vue components

<template>
  <div />
</template>

<script setup>
import { useQuasar } from 'quasar'
const $q = useQuasar()

// Use $q.bex (the bridge)
// $q.bex.portName is "app"
</script>

请注意 devtools/popup/options 页面的 portName 是 app

全新的 BEX bridge

Bex Bridge messaging

// Listen to a message from the client
bridge.on('test', message => {
  console.log(message)
  console.log(message.payload)
  console.log(message.from)
})

// Send a message and split payload into chunks
// to avoid max size limit of BEX messages.
// Warning! This happens automatically when the payload is an array.
// If you actually want to send an Array, wrap it in an object.
bridge.send({
  event: 'test',
  to: 'app',
  payload: [ 'chunk1', 'chunk2', 'chunk3', ... ]
}).then(responsePayload => { ... }).catch(err => { ... })

// Send a message and wait for a response
bridge.send({
  event: 'test',
  to: 'background',
  payload: { banner: 'Hello from content-script' }
}).then(responsePayload => { ... }).catch(err => { ... })

// Listen to a message from the client and respond synchronously
bridge.on('test', message => {
  console.log(message)
  return { banner: 'Hello from a content-script!' }
})

// Listen to a message from the client and respond asynchronously
bridge.on('test', async message => {
  console.log(message)
  const result = await someAsyncFunction()
  return result
})
bridge.on('test', message => {
  console.log(message)
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({ banner: 'Hello from a content-script!' })
    }, 1000)
  })
})

// Broadcast a message to app & content scripts
bridge.portList.forEach(portName => {
  bridge.send({ event: 'test', to: portName, payload: 'Hello from background!' })
})

// Find any connected content script and send a message to it
const contentPort = bridge.portList.find(portName => portName.startsWith('content@'))
if (contentPort) {
  bridge.send({ event: 'test', to: contentPort, payload: 'Hello from background!' })
}

// Send a message to a certain content script
bridge
  .send({ event: 'test', to: 'content@my-content-script-2345', payload: 'Hello from a content-script!' })
  .then(responsePayload => { ... })
  .catch(err => { ... })

// Listen for connection events
// (the "@quasar:ports" is an internal event name registered automatically by the bridge)
// --> ({ portList: string[], added?: string } | { portList: string[], removed?: string })
bridge.on('@quasar:ports', ({ portList, added, removed }) => {
  console.log('Ports:', portList)
  if (added) {
    console.log('New connection:', added)
  } else if (removed) {
    console.log('Connection removed:', removed)
  }
})

// Current bridge port name (can be 'background', 'app', or 'content@<name>-<xxxxx>')
console.log(bridge.portName)

注意!发送大量数据

所有浏览器扩展对通信消息的数据量都有硬性限制(例如:50MB)。如果你的 payload 超出了该限制,可以发送分块数据(payload 参数应该是一个数组)。


bridge.send({
  event: "some.event",
  to: "app",
  payload: [chunk1, chunk2, ...chunkN],
});

在计算 payload 大小时,请注意 payload 会被包裹在 Bridge 构建的消息中,其中还包含一些其他属性,也会占用一些字节。所以你的分块大小应该比浏览器的阈值小几个字节。

注意!发送数组时的性能问题

如上面的提示所述,如果 payload 是数组,bridge 会为数组的每个元素分别发送一条消息。当你实际上想发送一个数组(而不是将 payload 拆分为分块)时,这会非常低效。


解决方案是将你的数组包裹在一个对象中(这样只会发送一条消息):


bridge.send({
  event: "some.event",
  to: "background",
  payload: {
    myArray: [
      /*...*/
    ],
  },
});

如果你在 BEX 各部分之间发送消息时遇到问题,可以为感兴趣的 bridge 启用调试模式。启用后,通信内容也会输出到浏览器控制台:

Bridge debug mode

// Dynamically set debug mode
bridge.setDebug(true); // boolean

// Log a message on the console (if debug is enabled)
bridge.log("Hello world!");
bridge.log("Hello", "world!");
bridge.log("Hello world!", { some: "data" });
bridge.log("Hello", "world", "!", { some: "object" });
// Log a warning on the console (regardless of the debug setting)
bridge.warn("Hello world!");
bridge.warn("Hello", "world!");
bridge.warn("Hello world!", { some: "data" });
bridge.warn("Hello", "world", "!", { some: "object" });

其他 /quasar.config 文件变更

/quasar.config 文件中的 ctx 有一个新增属性(appPaths):

import { defineConfig } from '#q-app/wrappers'
export default defineConfig((ctx) => ({
  // ctx.appPaths is available

ctx.appPaths 的定义由 QuasarAppPaths TS 类型描述如下:

export interface IResolve {
  cli: (dir: string) => string;
  app: (dir: string) => string;
  src: (dir: string) => string;
+ public: (dir: string) => string;
  pwa: (dir: string) => string;
  ssr: (dir: string) => string;
  cordova: (dir: string) => string;
  capacitor: (dir: string) => string;
  electron: (dir: string) => string;
  bex: (dir: string) => string;
}

export interface QuasarAppPaths {
  cliDir: string;
  appDir: string;
  srcDir: string;
+ publicDir: string;
  pwaDir: string;
  ssrDir: string;
  cordovaDir: string;
  capacitorDir: string;
  electronDir: string;
  bexDir: string;

  quasarConfigFilename: string;
+ quasarConfigInputFormat: "esm" | "cjs" | "ts";
+ quasarConfigOutputFormat: "esm" | "cjs";

  resolve: IResolve;
}
/quasar.config > sourceFiles

sourceFiles: {
+ bexManifestFile?: string;
}
/quasar.config > framework

framework: {
  /**
   * Auto import - how to detect components in your vue files
   *   "kebab": q-carousel q-page
   *   "pascal": QCarousel QPage
   *   "combined": q-carousel QPage
   * @default 'kebab'
   */
  autoImportComponentCase?: "kebab" | "pascal" | "combined";

  /**
   * Auto import - which file extensions should be interpreted as referring to Vue SFC?
   * @default [ 'vue' ]
   */
+ autoImportVueExtensions?: string[];

  /**
   * Auto import - which file extensions should be interpreted as referring to script files?
   * @default [ 'js', 'jsx', 'ts', 'tsx' ]
   */
+ autoImportScriptExtensions?: string[];

  /**
   * Treeshake Quasar's UI on dev too?
   * Recommended to leave this as false for performance reasons.
   * @default false
   */
+ devTreeshaking?: boolean;
+ // was previously under /quasar.conf > build
}
/quasar.config > build

build: {
  /**
   * Treeshake Quasar's UI on dev too?
   * Recommended to leave this as false for performance reasons.
   * @default false
   */
- devTreeshaking?: boolean;
- // moved under /quasar.conf > framework

  /**
   * Should we invalidate the Vite and ESLint cache on startup?
   * @default false
   */
- rebuildCache?: boolean;

  /**
   * Automatically open remote Vue Devtools when running in development mode.
   */
+ vueDevtools?: boolean;

  /**
   * Folder where Quasar CLI should look for .env* files.
   * Can be an absolute path or a relative path to project root directory.
   *
   * @default project root directory
   */
+ envFolder?: string;
  /**
   * Additional .env* files to be loaded.
   * Each entry can be an absolute path or a relative path to quasar.config > build > envFolder.
   *
   * @example ['.env.somefile', '../.env.someotherfile']
   */
+ envFiles?: string[];
}

其他注意事项

你可能想要从 @intlify/vite-plugin-vue-i18n 升级/切换到更新的 @intlify/unplugin-vue-i18n

卸载旧包并安装新包后,按如下方式更新你的 /quasar.config 文件:

/quasar.config

- import path from 'node:path'
+ import { fileURLToPath } from 'node:url'

export default defineConfig((ctx) => {
  return {
    build: {
      vitePlugins: [
-       ['@intlify/vite-plugin-vue-i18n', {
+       ['@intlify/unplugin-vue-i18n/vite', {
-         include: path.resolve(__dirname, './src/i18n/**')
+         include: [ fileURLToPath(new URL('./src/i18n', import.meta.url)) ],
+         ssr: ctx.modeName === 'ssr'
        }]
      ]
    }
  }
})

env dotfiles 支持

这里详细展开 env dotfiles 支持。以下文件会被检测和使用(顺序很重要):

.env                                # 所有情况下都会加载
.env.local                          # 所有情况下都会加载,被 git 忽略
.env.[dev|prod]                     # 仅在 dev 或 prod 模式下加载
.env.local.[dev|prod]               # 仅在 dev 或 prod 模式下加载,被 git 忽略
.env.[quasarMode]                   # 仅在特定的 Quasar CLI 模式下加载
.env.local.[quasarMode]             # 仅在特定的 Quasar CLI 模式下加载,被 git 忽略
.env.[dev|prod].[quasarMode]        # 仅在特定的 Quasar CLI 模式和 dev|prod 下加载
.env.local.[dev|prod].[quasarMode]  # 仅在特定的 Quasar CLI 模式和 dev|prod 下加载,被 git 忽略

……其中"被 git 忽略"假设项目文件夹是在此包发布后创建的,否则请将 .env.local* 添加到你的 /.gitignore 文件中。

你还可以配置上述文件从不同的文件夹读取,或者向列表中添加更多文件:

/quasar.config file

build: {
  /**
   * Folder where Quasar CLI should look for .env* files.
   * Can be an absolute path or a relative path to project root directory.
   *
   * @default project root directory
   */
  envFolder?: string;

  /**
   * Additional .env* files to be loaded.
   * Each entry can be an absolute path or a relative path to quasar.config > build > envFolder.
   *
   * @example ['.env.somefile', '../.env.someotherfile']
   */
  envFiles?: string[];

  /**
   * Filter the env variables that are exposed to the client
   * through the env files. This does not account also for the definitions
   * assigned directly to quasar.config > build > env prop.
   *
   * Requires @quasar/app-vite v2.0.3+
   */
  envFilter?:
    (env: { [index: string]: string | boolean | undefined | null })
      => { [index: string]: string | boolean | undefined | null };
}

请记住,你可以使用 build > envFilter 来过滤掉不需要的 key,甚至修改 key 的值:

/quasar.config file

build: {
  // @quasar/app-vite v2.0.3+
  envFilter (originalEnv) {
    const newEnv = {}
    for (const key in originalEnv) {
      if (/* ...decide if it goes in or not... */) {
        newEnv[ key ] = originalEnv[ key ]
      }
    }

    // remember to return your processed env
    return newEnv
  }
}