为什么要捐赠
API 资源管理器
升级指南
新增!
quasar.config 文件
转换为使用 Webpack 的 CLI
浏览器兼容性
支持 TypeScript
目录结构
命令列表
CSS 预处理器
路由
延迟加载 - 代码分割
处理资源
启动文件
预取功能
API 代理
处理 Webpack
处理 process.env
使用 Pinia 进行状态管理
使用 Vuex 进行状态管理
代码检查器
测试与审计
开发移动应用
Ajax 请求
向公众开放开发服务器
使用 Webpack 的 Quasar CLI - @quasar/app-webpack
使用 Webpack 的 Quasar CLI 升级指南

@quasar/app-webpack v4(测试版)

CLI 目前处于测试阶段

  • 请帮助测试 CLI,以便我们能够将其从beta状态中移除。感谢您的帮助!
  • 虽然我们不打算添加任何进一步的重大更改,但根据您的反馈,我们仍然可能会被迫进行一些细微的更改。

警告

所有其他文档页面都将参考旧版 @quasar/app-webpack 版本 (v3) 的规范。目前只有此页面提到了如何使用 v4 测试版。

致应用扩展所有者

您可能希望发布新版本的 Quasar 应用扩展,以支持新的 @quasar/app-webpack。如果您没有修改 quasar.config 配置,那么只需更改以下内容即可

api.compatibleWith(
  '@quasar/app-webpack',
- '^3.0.0'
+ '^3.0.0 || ^4.0.0-beta.1'
)

显著的重大更改

  • Node.js 最小版本现已更新至 18.12。
  • 我们已将整个 Quasar 项目文件夹转向 ESM 样式,因此许多默认项目文件现在需要 ESM 代码(尽管可以使用 .cjs 作为这些文件的扩展名,但如果您不希望更改任何内容,则很可能需要重命名扩展名)。一个示例是 /quasar.config.js 文件,现在也假设它是 ESM(因此如果您仍然想要一个 CommonJs 文件,请从 .js 更改为 .cjs)。
  • 从 @quasar/app-vite 移植并适配了更优秀的 devserver 实现,用于所有 Quasar 模式。好处巨大。
  • 从 @quasar/app-vite 移植了 SSR、PWA、Electron 和 BEX 模式的更优秀实现。我们将在本文档页面上详细说明每个 Quasar 模式的更改。
    • SSR - 一些显著的改进
      • 改进的可靠性:相同的服务器代码在开发和生产环境中运行
      • 更多目标 Web 服务器选项:您可以用您正在使用的任何其他内容替换 express()。
      • 性能:当更改 /src-ssr 中的代码时,客户端代码不再需要从头重新编译。
      • /src-ssr 中文件的编译速度更快且更好(现在使用 Esbuild 而不是 Webpack 构建)。
    • PWA - 一些显著的改进
      • 许多新的配置选项(同时删除了许多旧的选项)
      • /src-pwa 中文件的编译速度更快且更好(现在使用 Esbuild 而不是 Webpack 构建)。
    • Electron
      • 现在编译为 ESM(从而也利用了 ESM 格式的 Electron)。
      • /src-electron 中文件的编译速度更快且更好(现在使用 Esbuild 而不是 Webpack 构建)。
      • 支持多个预加载脚本。
    • BEX - 一些显著的改进
      • 移植了 @quasar/app-vite 中更优秀的实现,这也意味着当您启动此模式时,可以在扩展清单 v2 和清单 v3 之间进行选择。
      • 清单现在保存在它自己的文件中(/src-pwa/manifest.json),而不是在 /quasar.config 文件中。
  • Webpack 现在只会编译 /src 文件夹的内容,而其余部分(/src-pwa、/src-electron 等)现在由 Esbuild 处理。这转化为更快的构建速度和对 Node.js 格式的处理。
  • 由于 @quasar/testing-* 包的最新更新,已删除“test”命令。请参阅 此处
  • “clean”命令已重新设计。在您升级的 Quasar 项目文件夹中键入“quasar clean -h”以获取更多信息。
  • Typescript 检测基于 quasar.config 文件是否为 TS 格式(quasar.config.ts)以及 tsconfig.json 文件是否存在。
  • 我们将在下面详细说明每个 Quasar 模式的更多重大更改。.

新增功能亮点

以下某些工作已回传到旧的 @quasar/app-webpack v3,但在此处发布以供读者了解。

  • feat(app-webpack):能够同时运行多个 quasar dev/build 命令(例如:可以同时运行“quasar dev -m capacitor”和“quasar dev -m ssr”以及“quasar dev -m capacitor -T ios”)。
  • feat(app-webpack):支持多种格式的 quasar.config 文件(.js、.mjs、.ts、.cjs)。
  • feat(app-webpack):总体上更好的 TS 类型定义。
  • feat(app-webpack):升级到 Typescript v5;删除 fork-ts-checker。
  • feat(app-webpack):改进 quasarConfOptions,为其生成类型,改进文档(修复:#14069)(#15945)
  • feat(app-webpack):如果 quasar.config 文件中的某个导入发生更改,则重新加载应用程序。
  • feat(app-webpack):TS 检测也应考虑 quasar.config 文件的格式(quasar.config.ts)。
  • feat(app-webpack):简写 CLI 命令“quasar dev/build -m ios/android”现在目标是 Capacitor 模式而不是 Cordova(4.0.0-beta.13+)。
  • feat(app-webpack):env dotfiles 支持 #15303
  • feat(app-webpack):新的 quasar.config 文件属性:build > envFolder(字符串)和 envFiles(字符串[])。
  • feat(app-webpack):支持多种格式的 postcss 配置文件:postcss.config.cjs、.postcssrc.js、postcss.config.js、postcss.config.mjs、.postcssrc.cjs、.postcssrc.mjs。
  • feat(app-webpack):支持多种格式的 babel 配置文件:babel.config.cjs、babel.config.js、babel.config.mjs、.babelrc.js、.babelrc.cjs、.babelrc.mjs、.babelrc。
  • feat(app-webpack):通过 quasar.config 文件更改应用程序 URL 时重新打开浏览器(如果已配置)。
  • feat(app-webpack):从 q/app-vite 移植 quasar.config 文件 > electron > inspectPort 属性。
  • feat(app-webpack):从 q/app-vite 移植 quasar.config 文件 > build > rawDefine。
  • feat&perf(app-webpack):更快且更准确的确定要使用的节点包管理器算法。
  • feat(app-webpack):高度提高 SSR 性能 + 内存使用率(尤其是在生产环境中);ssr-helpers 的主要重构;还包括来自 q/app-vite 的 renderPreloadTag()。
  • feat(app-webpack):支持使用 HTTPS 进行 SSR 开发。
  • feat(app-webpack):SSR - 能够用任何其他类似 connect 的 Web 服务器替换 express()。
  • feat(app-webpack):SSR - 更改 /src-ssr 中的代码时不再重新编译所有内容。
  • feat(app-webpack):升级依赖项。
  • feat(app-webpack):删除 CLI 模板中 Electron 6-8 中错误的解决方法(#15845)。
  • feat(app-webpack):删除 Capacitor v5+ 的 bundleWebRuntime 配置。
  • feat(app-webpack):默认使用 workbox v7。
  • feat(app-webpack):quasar.config > build > htmlMinifyOptions。
  • feat+refactor(app-webpack):能够同时运行多个模式 + dev/build。
  • feat(app-webpack):查找正在使用的 vue devtools 的开放端口;能够使用 vue devtools 运行多个 cli 实例。
  • perf(app-webpack):仅为“dev”命令验证 quasar.conf 服务器地址。
  • feat(app-webpack):为每个实例选择新的 electron inspect 端口。
  • refactor(app-webpack):AE 支持 - 更好、更高效的算法。
  • feat(app-webpack):对 ESM 格式的支持。
  • feat(app-webpack):对 TS 格式的支持(通过构建步骤)。
  • feat(app-webpack):AE API 新方法 -> hasTypescript() / hasLint() / getStorePackageName() / getNodePackagerName()。
  • feat(app-webpack):AE -> 提示 API(以及提示默认导出函数为异步的能力)。
  • feat(app-webpack):更智能的应用程序文件验证。
  • refactor(app-webpack):“clean”命令现在的工作方式有所不同,因为 CLI 可以在同一项目文件夹中以多个实例运行(开发或构建中的多个模式)。
  • feat(app-webpack):支持 Bun 作为包管理器 #16335
  • feat(app-webpack):对于默认的 /src-ssr 模板 -> 生产 ssr -> 发生错误时,如果使用调试功能构建,则打印错误堆栈。
  • fix(app-webpack):electron 预加载脚本触发“模块未找到”。
  • feat(app-webpack):升级到 webpack-dev-server v5。

升级过程的开始

建议

如果您不确定是否不会错误地跳过任何建议的更改,您可以随时使用 @quasar/app-webpack v4 beta 构建一个新的项目文件夹,然后轻松地从那里开始移植您的应用程序。大部分更改是指不同的项目文件夹配置文件,主要不是指您的 /src 文件。


$ yarn create quasar

当被要求“选择 Quasar App CLI 变体”时,请回答:“带有 Webpack 的 Quasar App CLI(BETA | 下一个主要版本 - v4)”。

准备工作

  • 如果使用 Quasar CLI 的全局安装(@quasar/cli),请确保您拥有最新版本。这是由于支持多种格式的 quasar.config 文件。

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

  • /package.json 上的 @quasar/app-webpack 条目中进行编辑,并将其赋值为 ^4.0.0-beta.1

    /package.json

    "devDependencies": {
    - "@quasar/app-webpack": "^3.0.0",
    + "@quasar/app-webpack": "^4.0.0-beta.1"
    }

    然后 yarn/npm/pnpm/bun install。

  • 将您的 /quasar.config.js 文件转换为 ESM 格式(建议这样做,否则将文件扩展名重命名为 .cjs 并使用 CommonJs 格式)。

    /quasar.config.js 文件

    import { configure } from 'quasar/wrappers'
    export default configure((/* ctx */) => {
      return {
        // ...
      }
    })

    关于 Typescript 的提示

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

  • 为了与 @quasar/app-vite 保持一致(以及在 @quasar/app-webpack 和它之间轻松切换),将 /src/index.template.html 移动到 /index.html 并进行以下更改。

    /index.html

    <body>
    - <!-- DO NOT touch the following DIV -->
    - <div id="q-app"></div>
    + <!-- quasar:entry-point -->
    </body>

  • (可选,但建议)为了将来证明某些工具配置文件,请重命名以下文件(在根项目文件夹中)。

    旧名称新名称
    postcss.config.jspostcss.config.cjs
    .eslintrc.js.eslintrc.cjs
    babel.config.jsbabel.config.cjs

  • 您可能希望将以下内容添加到您的 /.gitignore 文件中。当您的 /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

  • 如果您有 linting,请查看您的 /.eslintignore 文件。

    /.eslintignore

    /dist
    /src-capacitor
    /src-cordova
    /.quasar
    /node_modules
    .eslintrc.cjs
    babel.config.cjs
    /quasar.config.*.temporary.compiled*

  • 如果您使用 Typescript,请确保您的 /tsconfig.json 文件如下所示。

    {
      "extends": "@quasar/app-webpack/tsconfig-preset",
      "compilerOptions": {
        "baseUrl": "."
      },
      "exclude": [
        "./dist",
        "./.quasar",
        "./node_modules",
        "./src-capacitor",
        "./src-cordova",
        "./quasar.config.*.temporary.compiled*"
      ]
    }

  • 功能标志文件必须从您的项目文件夹中删除。它们需要重新生成(将自动发生)。


    # in project folder root:
    $ npx rimraf -g ./**/*-flag.d.ts
    $ quasar build # or dev

SPA / Capacitor / Cordova 模式更改

无需更改 /src/src-capacitor/src-cordova 文件夹中的任何内容。

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

- import { precacheAndRoute } from 'workbox-precaching'

- // Use with precache injection
- precacheAndRoute(self.__WB_MANIFEST)

+ import { clientsClaim } from 'workbox-core'
+ import { precacheAndRoute, cleanupOutdatedCaches, createHandlerBoundToURL } from 'workbox-precaching'
+ import { registerRoute, NavigationRoute } from 'workbox-routing'

+ self.skipWaiting()
+ clientsClaim()

+ // Use with precache injection
+ precacheAndRoute(self.__WB_MANIFEST)

+ cleanupOutdatedCaches()

+ // Non-SSR fallbacks to index.html
+ // Production SSR fallbacks to offline.html (except for dev)
+ if (process.env.MODE !== 'ssr' || process.env.PROD) {
+  registerRoute(
+    new NavigationRoute(
+      createHandlerBoundToURL(process.env.PWA_FALLBACK_HTML),
+      { denylist: [new RegExp(process.env.PWA_SERVICE_WORKER_REGEX), /workbox-(.)*\.js$/] }
+    )
+  )
+ }

创建文件 /src-pwa/manifest.json 并将 /quasar.config 文件 > pwa > manifest 从那里移动到此文件。以下是如何显示示例。

{
  "orientation": "portrait",
  "background_color": "#ffffff",
  "theme_color": "#027be3",
  "icons": [
    {
      "src": "icons/icon-128x128.png",
      "sizes": "128x128",
      "type": "image/png"
    },
    {
      "src": "icons/icon-192x192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "icons/icon-256x256.png",
      "sizes": "256x256",
      "type": "image/png"
    },
    {
      "src": "icons/icon-384x384.png",
      "sizes": "384x384",
      "type": "image/png"
    },
    {
      "src": "icons/icon-512x512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ]
}

/quasar.config 文件中也有一些细微的更改。

/quasar.config 文件

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: {
- workboxPluginMode?: "GenerateSW" | "InjectManifest";
+ workboxMode?: "GenerateSW" | "InjectManifest";

  /**
   * Full option list can be found
   *  [here](https://developers.google.com/web/tools/workbox/modules/workbox-webpack-plugin#full_generatesw_config).
   */
- workboxOptions?: object;
  /**
   * Extend/configure the Workbox GenerateSW options
   */
+ extendGenerateSWOptions?: (config: GenerateSWOptions) => void;
  /**
   * Extend/configure the Workbox InjectManifest options
   */
+ extendInjectManifestOptions?: (config: InjectManifestOptions) => void;

- // Now the contents for this held in a new file: /src-pwa/manifest.json
- // and its replaced by extendManifestJson below:
- manifest?: PwaManifestOptions;
  /**
   * Should you need some dynamic changes to the /src-pwa/manifest.json,
   * use this method to do it.
   */
+ extendManifestJson?: (json: PwaManifestOptions) => void;

  /**
   * PWA manifest filename to use on your browser
   * @default manifest.json
   */
+ manifestFilename?: string;

  /**
   * Does the PWA manifest tag requires crossorigin auth?
   * @default false
   */
+ useCredentialsForManifestTag?: boolean;

  /**
   * Webpack config object for the custom service worker ONLY (`/src-pwa/custom-service-worker`)
   *  when pwa > workboxPluginMode is set to InjectManifest
   */
- extendWebpackCustomSW?: (config: WebpackConfiguration) => void;
  /**
   * Equivalent to `extendWebpackCustomSW()` but uses `webpack-chain` instead,
   *  for the custom service worker ONLY (`/src-pwa/custom-service-worker`)
   *  when pwa > workboxPluginMode is set to InjectManifest
   */
- chainWebpackCustomSW?: (chain: WebpackChain) => void;
  /**
   * Extend the Esbuild config that is used for the custom service worker
   * (if using it through workboxMode: 'InjectManifest')
   */
+ extendPWACustomSWConf?: (config: EsbuildConfiguration) => void;

- /**
-  * @default
-  * ```typescript
-  * {
-  *    appleMobileWebAppCapable: 'yes';
-  *    appleMobileWebAppStatusBarStyle: 'default';
-  *    appleTouchIcon120: 'icons/apple-icon-120x120.png';
-  *    appleTouchIcon180: 'icons/apple-icon-180x180.png';
-  *    appleTouchIcon152: 'icons/apple-icon-152x152.png';
-  *    appleTouchIcon167: 'icons/apple-icon-167x167.png';
-  *    appleSafariPinnedTab: 'icons/safari-pinned-tab.svg';
-  *    msapplicationTileImage: 'icons/ms-icon-144x144.png';
-  *    msapplicationTileColor: '#000000';
-  * }
-   * ```
-  */
- metaVariables?: {
-   appleMobileWebAppCapable: string;
-   appleMobileWebAppStatusBarStyle: string;
-   appleTouchIcon120: string;
-   appleTouchIcon180: string;
-   appleTouchIcon152: string;
-   appleTouchIcon167: string;
-   appleSafariPinnedTab: string;
-   msapplicationTileImage: string;
-   msapplicationTileColor: string;
- };
- metaVariablesFn?: (manifest?: PwaManifestOptions) => PwaMetaVariablesEntry[];
+ /**
+  * Auto inject the PWA meta tags?
+  * If using the function form, return HTML tags as one single string.
+  * @default true
+  */
+ 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 模式更改

警告

可分发文件(您的生产代码)将编译为 ESM 格式,从而也利用 ESM 格式的 Electron。

提示

您可能希望将 electron 包升级到最新版本,以便它可以处理 ESM 格式。

大多数更改是指编辑您的 /src-electron/electron-main.js 文件。

图标路径

+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
    // ...
  })
预加载脚本

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)
+     )
    }
  })

警告

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

/quasar.config 文件

sourceFiles: {
- electronPreload?: string;
},

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

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

最后,新文件应如下所示。

新的 /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

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-webpack/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) {
    mainWindow.loadURL(process.env.APP_URL)
  } else {
    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()
  }
})

/quasar.config 文件中也有一些更改。

/quasar.config 文件 > electron

electron: {
  /** Webpack config object for the Main Process ONLY (`/src-electron/electron-main`) */
- extendWebpackMain?: (config: WebpackConfiguration) => void;
  /**
   * Equivalent to `extendWebpackMain()` but uses `webpack-chain` instead,
   *  for the Main Process ONLY (`/src-electron/electron-main`)
   */
- chainWebpackMain?: (chain: WebpackChain) => void;
  /**
   * Extend the Esbuild config that is used for the electron-main thread
   */
+ extendElectronMainConf?: (config: EsbuildConfiguration) => void;

  /** Webpack config object for the Preload Process ONLY (`/src-electron/electron-preload`) */
- extendWebpackPreload?: (config: WebpackConfiguration) => void;
  /**
   * Equivalent to `extendWebpackPreload()` but uses `webpack-chain` instead,
   *  for the Preload Process ONLY (`/src-electron/electron-preload`)
   */
- chainWebpackPreload?: (chain: WebpackChain) => void;
  /**
   * Extend the Esbuild config that is used for the electron-preload thread
   */
+ extendElectronPreloadConf?: (config: EsbuildConfiguration) => void;

  /**
   * The list of content scripts (js/ts) that you want embedded.
   * Each entry in the list should be a filename (WITHOUT its extension) from /src-electron/
   *
   * @default [ 'electron-preload' ]
   * @example [ 'my-other-preload-script' ]
   */
+ preloadScripts?: string[];

  /**
   * Specify the debugging port to use for the Electron app when running in development mode
   * @default 5858
   */
+ inspectPort?: number;

  /**
   * Specify additional parameters when yarn/npm installing
   * the UnPackaged folder, right before bundling with either
   * electron packager or electron builder;
-  * Example: [ '--ignore-optional', '--some-other-param' ]
+  * Example: [ 'install', '--production', '--ignore-optional', '--some-other-param' ]
   */
  unPackagedInstallParams?: string[];
}

SSR 模式更改

/src-ssr/production-export.js 的支持已被删除(删除它)。相同的 SSR Web 服务器现在同时用于开发和生产,因此使用以下内容创建一个 /src-ssr/server.js

/src-ssr/server.js

/**
 * More info about this file:
 * https://v2.quasar.dev/quasar-cli-webpack/developing-ssr/ssr-webserver
 *
 * Runs in Node context.
 */

/**
 * Make sure to yarn add / npm install (in your project root)
 * anything you import here (except for express and compression).
 */
import express from 'express'
import compression from 'compression'
import {
  ssrClose,
  ssrCreate,
  ssrListen,
  ssrServeStaticContent,
  ssrRenderPreloadTag
} from 'quasar/wrappers'

/**
 * Create your webserver and return its instance.
 * If needed, prepare your webserver to receive
 * connect-like middlewares.
 *
 * Can be async: ssrCreate(async ({ ... }) => { ... })
 */
export const create = ssrCreate((/* { ... } */) => {
  const app = express()

  // attackers can use this header to detect apps running Express
  // and then launch specifically-targeted attacks
  app.disable('x-powered-by')

  // place here any middlewares that
  // absolutely need to run before anything else
  if (process.env.PROD) {
    app.use(compression())
  }

  return app
})

/**
 * You need to make the server listen to the indicated port
 * and return the listening instance or whatever you need to
 * close the server with.
 *
 * The "listenResult" param for the "close()" definition below
 * is what you return here.
 *
 * For production, you can instead export your
 * handler for serverless use or whatever else fits your needs.
 *
 * Can be async: ssrListen(async ({ app, devHttpsApp, port }) => { ... })
 */
export const listen = ssrListen(({ app, devHttpsApp, port }) => {
  const server = devHttpsApp || app
  return server.listen(port, () => {
    if (process.env.PROD) {
      console.log('Server listening at port ' + port)
    }
  })
})

/**
 * Should close the server and free up any resources.
 * Will be used on development only when the server needs
 * to be rebooted.
 *
 * Should you need the result of the "listen()" call above,
 * you can use the "listenResult" param.
 *
 * Can be async: ssrClose(async ({ listenResult }) => { ... })
 */
export const close = ssrClose(({ listenResult }) => {
  return listenResult.close()
})

const maxAge = process.env.DEV
  ? 0
  : 1000 * 60 * 60 * 24 * 30

/**
 * 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: ssrServeStaticContent(async ({ app, resolve }) => {
 * Can return an async function: return async ({ urlPath = '/', pathToServe = '.', opts = {} }) => {
 */
export const serveStaticContent = ssrServeStaticContent(({ app, resolve }) => {
  return ({ urlPath = '/', pathToServe = '.', opts = {} }) => {
    const serveFn = express.static(resolve.public(pathToServe), { maxAge, ...opts })
    app.use(resolve.urlPath(urlPath), serveFn)
  }
})

const jsRE = /\.js$/
const cssRE = /\.css$/
const woffRE = /\.woff$/
const woff2RE = /\.woff2$/
const gifRE = /\.gif$/
const jpgRE = /\.jpe?g$/
const pngRE = /\.png$/

/**
 * Should return a String with HTML output
 * (if any) for preloading indicated file
 */
export const renderPreloadTag = ssrRenderPreloadTag((file/* , { ssrContext } */) => {
  if (jsRE.test(file) === true) {
    return `<script src="${file}" defer crossorigin></script>`
  }

  if (cssRE.test(file) === true) {
    return `<link rel="stylesheet" href="${file}" crossorigin>`
  }

  if (woffRE.test(file) === true) {
    return `<link rel="preload" href="${file}" as="font" type="font/woff" crossorigin>`
  }

  if (woff2RE.test(file) === true) {
    return `<link rel="preload" href="${file}" as="font" type="font/woff2" crossorigin>`
  }

  if (gifRE.test(file) === true) {
    return `<link rel="preload" href="${file}" as="image" type="image/gif" crossorigin>`
  }

  if (jpgRE.test(file) === true) {
    return `<link rel="preload" href="${file}" as="image" type="image/jpeg" crossorigin>`
  }

  if (pngRE.test(file) === true) {
    return `<link rel="preload" href="${file}" as="image" type="image/png" crossorigin>`
  }

  return ''
})

对于无服务器方法,以下是“listen”部分的外观。

/src-ssr/server.js > listen

export const listen = ssrListen(({ 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 }
  }
})

如果您有 /src-ssr/middlewares/compression.js 文件,请将其删除,因为此代码现在已嵌入到 /src-ssr/server.js 中。然后编辑您的 /quasar.config 文件以删除对旧文件的引用。

/quasar.config 文件

ssr: {
  middlewares: [
-   ctx.prod ? 'compression' : '',
    'render' // keep this as last one
  ]
}

/src-ssr/middlewares/render.js 文件内容示例

/src-ssr/middlewares/render.js

import { ssrMiddleware } from 'quasar/wrappers'

// This middleware should execute as last one
// since it captures everything and tries to
// render the page with Vue

export default ssrMiddleware(({ app, resolve, render, serve }) => {
  // we capture any other Express route and hand it
  // over to Vue and Vue Router to render our page
  app.get(resolve.urlPath('*'), (req, res) => {
    res.setHeader('Content-Type', 'text/html')

    render(/* the ssrContext: */ { req, res })
      .then(html => {
        // now let's send the rendered html to the client
        res.send(html)
      })
      .catch(err => {
        // oops, we had an error while rendering the page

        // we were told to redirect to another URL
        if (err.url) {
          if (err.code) {
            res.redirect(err.code, err.url)
          } else {
            res.redirect(err.url)
          }
        } else if (err.code === 404) {
          // hmm, Vue Router could not find the requested route

          // Should reach here only if no "catch-all" route
          // is defined in /src/routes
          res.status(404).send('404 | Page Not Found')
        } else if (process.env.DEV) {
          // well, we treat any other code as error;
          // if we're in dev mode, then we can use Quasar CLI
          // to display a nice error page that contains the stack
          // and other useful information

          // serve.error is available on dev only
          serve.error({ err, req, res })
        } else {
          // we're in production, so we should have another method
          // to display something to the client when we encounter an error
          // (for security reasons, it's not ok to display the same wealth
          // of information as we do in development)

          // Render Error Page on production or
          // create a route (/src/routes) for an error page and redirect to it
          res.status(500).send('500 | Internal Server Error')

          if (process.env.DEBUGGING) {
            console.error(err.stack)
          }
        }
      })
  })
})

对于 TS 开发人员,您还应该对您的 /src-ssr/middlewares 文件进行一些小的更改,如下所示。

对于 TS 开发人员

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

/quasar.config 文件中还有一些其他更改。

/quasar.config 文件

ssr: {
  // ...

  /**
   * If a PWA should take over or just a SPA.
   * When used in object form, you can specify Workbox options
   *  which will be applied on top of `pwa > workboxOptions`.
   *
   * @default false
   */
- pwa?: boolean | object;
+ pwa?: boolean;

  /**
   * 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;
- // now part of the /src-ssr/server.js code

  /**
   * 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;

  /**
   * Webpack config object for the Webserver
   * which includes the SSR middleware
   */
- extendWebpackWebserver?: (config: WebpackConfiguration) => void;
  /**
   * Equivalent to `extendWebpackWebserver()` but uses `webpack-chain` instead.
   * Handles the Webserver webpack config ONLY which includes the SSR middleware
   */
- chainWebpackWebserver?: (chain: WebpackChain) => void;
  /**
   * Extend the Esbuild config that is used for the SSR webserver
   * (which includes the SSR middlewares)
   */
+ extendSSRWebserverConf?: (config: EsbuildConfiguration) => void;
}

Bex 模式更改

BEX 模式的实现已从 @quasar/app-vite 移植,因此当您启动此 Quasar 模式时,它现在会询问您想要哪种扩展清单版本(v2 或 v3)。

但这同时也意味着您的 /src-bex 文件夹的文件夹和文件结构发生了重大变化。最好先将您的 /src-bex 文件夹临时复制到安全位置,然后删除并重新添加 BEX 模式

$ quasar mode remove bex
$ quasar mode add bex

然后,尝试理解新的结构并将您的旧 /src-bex 移植到其中。很遗憾,没有其他方法。

但首先,您需要注意一些 /quasar.config 文件的更改

/quasar.config 文件

sourceFiles: {
+ bexManifestFile: 'src-bex/manifest.json',
  // ...
},

bex: {
- builder: {
-   directories: {
-     input: cfg.build.distDir,
-     output: path.join(cfg.build.packagedDistDir, 'Packaged')
-   }
- }
}

一些更改,例如将后台脚本从 /js/background.js 直接移动到根文件夹,是由于外部因素为了扩展结构的未来发展而必需的。

提示

临时,直到此版本的 @quasar/app-webpack 退出测试版状态,最好查看 Quasar CLI with Vite 文档中关于 BEX 的内容,因为它们现在大多会匹配。

点击下面的块展开并查看旧的和新的文件夹结构

旧的文件夹结构
content-css.css
# 通过 manifest.json 自动注入到使用网页中的 CSS 文件
icon-16x16.png
# 16px x 16px 的图标文件
icon-48x48.png
# 48px x 48px 的图标文件
icon-128x128.png
# 128px x 128px 的图标文件
background.js
# 标准的后台脚本 BEX 文件 - 通过 manifest.json 自动注入
background-hooks.js
# 带有 BEX 通信层挂钩的后台脚本
content-hooks.js
# 带有 BEX 通信层挂钩的内容脚本脚本
content-script.js
# 标准的内容脚本 BEX 文件 - 通过 manifest.json 自动注入
dom-hooks.js
# 注入到 DOM 中并带有 BEX 通信层挂钩的 JS 文件
www/
# 编译后的 BEX 源代码 - 从 /src(Quasar 应用程序)编译而来
manifest.json
# 生产环境的主线程代码
新的文件夹结构
content.css
# 通过 manifest.json 自动注入到使用网页中的 CSS 文件
background.js
# 标准的后台脚本 BEX 文件(通过 manifest.json 自动注入)
dom.js
# 注入到 DOM 中并带有 BEX 通信层挂钩的 JS 文件
icon-128x128.png
# 128px x 128px 的图标文件
icon-16x16.png
# 16px x 16px 的图标文件
icon-48x48.png
# 48px x 48px 的图标文件
_locales/
# 您可能在清单中定义的可选 BEX 本地化文件
manifest.json
# 浏览器扩展程序清单文件
my-content-script.js
# 标准的内容脚本 BEX 文件 - 通过 manifest.json 自动注入(您可以有多个脚本)

其他 /quasar.config 文件更改

/quasar.config 文件中的 ctx 具有一些额外的属性(vueDevtoolsappPaths

import { configure } from 'quasar/wrappers'
export default configure((ctx) => ({
  // ctx.vueDevtools & ctx.appPaths is available

ctx.vueDevtools 的定义如下

/** True if opening remote Vue Devtools in development mode. */
vueDevtools: boolean;

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;
}

Typescript 检测基于 quasar.config 文件是否为 TS 格式(quasar.config.ts)以及 tsconfig.json 文件是否存在,因此请删除以下内容

/quasar.config

- /**
-  * Add support for TypeScript.
-  *
-  * @default false
-  */
- supportTS?: boolean | { tsLoaderConfig: object; tsCheckerConfig: object };

/quasar.config 文件 > sourceFiles 的定义有一些更改

/quasar.config > sourceFiles

sourceFiles: {
  rootComponent?: string;
  router?: string;
  store?: string;
  indexHtmlTemplate?: string;

- registerServiceWorker?: string;
- serviceWorker?: string;
+ pwaRegisterServiceWorker?: string;
+ pwaServiceWorker?: string;
+ pwaManifestFile?: string;

  electronMain?: string;
- electronPreload?: string;
- ssrServerIndex?: string;

+ bexManifestFile?: string;
}

有一个用于代码风格检查的新属性

/quasar.config > eslint(新增!)

eslint: {
  /**
   * Should it report warnings?
   * @default false
   */
  warnings?: boolean;

  /**
   * Should it report errors?
   * @default false
   */
  errors?: boolean;

  /**
   * Fix on save.
   * @default false
   */
  fix?: boolean;

  /**
   * Raw options to send to ESLint for Esbuild
   */
  rawEsbuildEslintOptions?: Omit<
    ESLint.Options,
    "cache" | "cacheLocation" | "fix" | "errorOnUnmatchedPattern"
  >;

  /**
   * Raw options to send to ESLint Webpack plugin
   */
  rawWebpackEslintPluginOptions?: WebpackEslintOptions;

  /**
   * Files to include (can be in glob format; for Esbuild ESLint only)
   */
  include?: string[];

  /**
   * Files to exclude (can be in glob format).
   * Recommending to use .eslintignore file instead.
   * @default ['node_modules']
   */
  exclude?: string[];

  /**
   * Enable or disable caching of the linting results.
   * @default true
   */
  cache?: boolean;

  /**
   * Formatter to use
   * @default 'stylish'
   */
  formatter?: ESLint.Formatter;
}
/quasar.config > build

build: {
  /**
   * Transpile JS code with Babel
   *
   * @default true
   */
- transpile?: boolean;
+ webpackTranspile?: boolean;

  /**
   * Add dependencies for transpiling with Babel (from node_modules, which are by default not transpiled).
   * It is ignored if "transpile" is not set to true.
   * @example [ /my-dependency/, 'my-dep', ...]
   */
- transpileDependencies?: (RegExp | string)[];
+ webpackTranspileDependencies?: (RegExp | string)[];

  /**
   * Add support for also referencing assets for custom tags props.
   *
   * @example { 'my-img-comp': 'src', 'my-avatar': [ 'src', 'placeholder-src' ]}
   */
- transformAssetsUrls?: Record<string, string | string[]>;
  // use vueLoaderOptions instead

  /** Show a progress bar while compiling. */
- showProgress?: boolean;
+ webpackShowProgress?: boolean;

  /**
   * Source map [strategy](https://webpack.js.cn/configuration/devtool/) to use.
   */
- devtool?: WebpackConfiguration["devtool"];
+ webpackDevtool?: WebpackConfiguration["devtool"];

  /**
   * Sets [Vue Router mode](https://router.vuejs.ac.cn/guide/essentials/history-mode.html).
   * History mode requires configuration on your deployment web server too.
   *
   * @default 'hash'
   */
+ vueRouterMode?: "hash" | "history";
  /**
   * Sets Vue Router base.
   * Should not need to configure this, unless absolutely needed.
   */
+ vueRouterBase?: string;

  /**
   * When using SSR+PWA, this is the name of the
   * PWA index html file.
   *
   * Do NOT use index.html as name as it will mess SSR up!
   *
   * @default 'offline.html'
   */
- ssrPwaHtmlFilename?: string;
- // Moved to ssr > pwaOfflineHtmlFilename

  /** Options to supply to `ts-loader` */
+ tsLoaderOptions?: object;

  /**
   * Esbuild is used to build contents of /src-pwa, /src-ssr, /src-electron, /src-bex
   * @example
   *    {
   *      browser: ['es2022', 'firefox115', 'chrome115', 'safari14'],
   *      node: 'node20'
   *    }
   */
+ esbuildTarget?: EsbuildTargetOptions;
+ // please check below for the EsbuildTargetOptions interface

  /**
   * Defines constants that get replaced in your app.
   * Unlike `env`, you will need to use JSON.stringify() on the values yourself except for booleans.
   * Also, these will not be prefixed with `process.env.`.
   *
   * @example { SOMETHING: JSON.stringify('someValue') } -> console.log(SOMETHING) // console.log('someValue')
   */
+ rawDefine?: { [index: string]: string | boolean | undefined | null };

  /**
   * 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[];
}

interface EsbuildTargetOptions {
  /**
   * @default ['es2022', 'firefox115', 'chrome115', 'safari14']
   */
  browser?: string[];
  /**
   * @example 'node20'
   */
  node?: string;
}

由于 @quasar/app-webpack v4.0.0-beta.3 中将 webpack-dev-server 升级到 v5

/quasar.config > devServer

devServer: {
- proxy: {
-   "/api": {
-     target: "http://localhost:3000",
-     changeOrigin: true,
-   },
- }
+ proxy: [
+   {
+     context: ["/api"],
+     target: "http://localhost:3000",
+     changeOrigin: true,
+   },
+ ]
}

env 点文件支持

详细介绍一下 env 点文件支持。这些文件将被检测并使用(顺序很重要)

.env                                # loaded in all cases
.env.local                          # loaded in all cases, ignored by git
.env.[dev|prod]                     # loaded for dev or prod only
.env.local.[dev|prod]               # loaded for dev or prod only, ignored by git
.env.[quasarMode]                   # loaded for specific Quasar CLI mode only
.env.local.[quasarMode]             # loaded for specific Quasar CLI mode only, ignored by git
.env.[dev|prod].[quasarMode]        # loaded for specific Quasar CLI mode and dev|prod only
.env.local.[dev|prod].[quasarMode]  # loaded for specific Quasar CLI mode and dev|prod only, ignored by git

…其中“git 忽略”假设在发布此软件包后创建的默认项目文件夹,否则将 .env.local* 添加到您的 /.gitignore 文件中。

您还可以配置以上文件以从不同的文件夹中获取,甚至可以向列表中添加更多文件

/quasar.config 文件

build: {
  envFolder: './' // absolute or relative path to root project folder
  envFiles: [
    // Path strings to your custom files --- absolute or relative path to root project folder
  ]
}