为什么捐赠
API 浏览器
联系站长
等效 Starter Kit

本指南适用于您想创建一个本质上是"starter kit"的场景——在官方 starter kit 基础上添加内容(/quasar.config 文件配置、文件夹、文件、CLI 钩子)。这允许您让多个项目共享通用的结构/逻辑(只需管理一个包,而不必逐个修改所有项目来匹配通用模式),同时也可以将这些分享给社区。

TIP

要创建 App Extension 项目目录,请先阅读开发指南 > 介绍

完整示例

要查看我们将要构建的完整示例,请前往 MyStarterKit 完整示例,这是一个包含我们在本页构建的 App Extension 的 GitHub 仓库。

我们将创建一个示例 App Extension,它执行以下操作:

  • 提示用户选择要安装的功能
  • 根据用户的回答将文件渲染(复制)到宿主目录中
  • 扩展 /quasar.config 文件
  • 扩展 Vite 配置
  • 使用 App Extension 钩子(onPublish)
  • 在 App Extension 被卸载时删除已添加的文件
  • 使用 prompts 来定义 App Extension 的行为

目录结构

本示例中,我们将创建以下目录结构:

README.md
package.json
README.md
tasks.md
serviceA.js
# (or .ts)
serviceB.js
# (or .ts)
index.js
# (or .ts) Described in Index API
install.js
# (or .ts) Described in Install API
prompts.js
# (or .ts) Described in Prompts API
uninstall.js
# (or .ts) Described in Uninstall API

Prompts 脚本

下面的 prompts 脚本使用了 @clack/prompts,但你可以替换为任何你喜欢的提示库。只需记得安装你使用的提示包(在 /ae 中,添加到 dependencies,因为它是运行时依赖)。

此脚本会在 AE 安装/调用过程中被调用。

File: /ae/src/prompts.js (or .ts)

/**
 * Quasar App Extension prompts script
 * https://quasar.dev/app-extensions/development-guide/prompts-api
 */

import { definePromptsScript } from '@quasar/app-vite'
import { intro, outro, confirm, text, group, cancel } from '@clack/prompts'

export default definePromptsScript(async (/* api */) => {
  intro('Starter Kit App Extension')

  const answers = await group(
    {
      serviceA: () => confirm({ message: 'Do you want service "A"?' }),
      serviceB: () => confirm({ message: 'Do you want service "B"?' }),
      productName: ({ results }) => {
        if (!results.serviceB) return
        return text({
          message: 'Since you want service "B", what is the Product Name?',
          initialValue: 'MyProduct',
          validate(value) {
            if (value.length === 0) return 'Please enter a product name'
          }
        })
      },
      publishService: () =>
        confirm({
          message: 'Do you want the publishing service?',
          initialValue: true
        })
    },
    {
      // On Cancel callback that wraps the group
      // So if the user cancels one of the prompts in the group this function will be called
      onCancel: (/* { results } */) => {
        cancel('Operation cancelled.')
        process.exit(0)
      }
    }
  )

  outro('Thanks for answering the questions!')

  return answers
})

Install 脚本

下面的 install 脚本仅将文件渲染到宿主应用中。注意上面的 src/templates 目录,我们决定在此存放这些模板。

File: /ae/src/install.js (or .ts)

import { defineInstallScript } from '@quasar/app-vite'

export default defineInstallScript(api => {
  // (Optional!)
  // Quasar compatibility check; you may need
  // hard dependencies, as in a minimum version of the "quasar"
  // package or a minimum version of Quasar App CLI
  api.compatibleWith('quasar', '^2.0.0')
  api.compatibleWith('@quasar/app-vite', '^3.0.0-beta.15')

  // We render some files into the hosting project

  if (api.prompts.serviceA) {
    api.render('./templates/serviceA')
  }

  if (api.prompts.serviceB) {
    // we supply interpolation variables
    // to the template
    api.render('./templates/serviceB', {
      productName: api.prompts.productName
    })
  }

  // we always render the following template:
  api.render('./templates/common-files')
})

注意我们使用 prompts 来决定渲染什么到宿主项目中。此外,如果用户选择了"service B",我们还会有一个"productName"可以在渲染 service B 的文件时使用。

Index 脚本

我们在 index 脚本中做了几件事,比如扩展 /quasar.config 文件、挂接到众多 Index API 钩子之一(本例中是 onPublish):

File: /ae/src/index.js (or .ts)

import { defineIndexScript } from '@quasar/app-vite'

export default defineIndexScript(api => {
  // (Optional!)
  // Quasar compatibility check; you may need
  // hard dependencies, as in a minimum version of the "quasar"
  // package or a minimum version of Quasar App CLI
  api.compatibleWith('quasar', '^2.0.0')
  api.compatibleWith('@quasar/app-vite', '^3.0.0-beta.15')

  // Here we extend the /quasar.config file;
  // (extendQuasarConf() will be defined later in this tutorial, continue reading)
  api.extendQuasarConf(extendQuasarConf)

  // Here we register the onPublish hook,
  // only if user answered that he wants the publishing service
  if (api.prompts.publishService) {
    // onPublish() will be defined later in this tutorial, continue reading
    api.onPublish(onPublish)
  }

  api.extendViteConf(extendVite)

  // there's lots more hooks that you can use...
})

下面是 extendQuasarConf 的定义示例:

function extendQuasarConf(conf, api) {
  conf.extras.push('ionicons-v4')
  conf.framework.iconSet = 'ionicons-v4'

  //
  // We register a boot file. User does not need to tamper with it,
  // so we keep it into the App Extension code:
  //

  // make sure my-ext boot file is registered
  conf.boot.push(
    '~quasar-app-extension-my-starter-kit/src/runtime/my-starter-kit-boot.js'
  )
}

onPublish 函数:

function onPublish(api, { arg, distDir }) {
  // this hook is called when "quasar build --publish" is called

  // your publish logic here...
  console.log('We should publish now. But maybe later? :)')

  // are we trying to publish a Cordova app?
  if (api.ctx.modeName === 'cordova') {
    // do something
  }
}

extendVite 函数:

function extendVite(viteConf, { isClient, isServer }, api) {
  // viteConf is a Vite config object generated by Quasar CLI
}

Uninstall 脚本

当 App Extension 被卸载时,我们需要做一些清理工作。但请注意从应用空间中删除什么!某些文件可能仍然需要。如果您决定编写 uninstall 脚本,请格外小心。

import { defineUninstallScript } from '@quasar/app-vite'

// we PNPM added it to our App Extension,
// so we can import the following:
import rimraf from 'rimraf'

export default defineUninstallScript(api => {
  // Careful when you remove folders!
  // You don't want to delete files that are still needed by the Project,
  // or files that are not owned by this app extension.

  // Here, we could also remove the /src/services folder altogether,
  // but what if the user has added other files into this folder?

  if (api.prompts.serviceA) {
    // we added it on install, so we remove it
    rimraf.sync(api.resolve.src('services/serviceA.js'))
  }

  if (api.prompts.serviceB) {
    // we added it on install, so we remove it
    rimraf.sync(api.resolve.src('services/serviceB.js'))
  }

  // we added it on install, so we remove it
  rimraf.sync(api.resolve.app('some-folder'))
  // warning... we've added this folder, but what if the
  // developer added more files into this folder???
})

注意我们引用了 rimraf npm 包。这意味着我们在 App Extension 项目(/ae 目录)中通过 pnpm 安装了它。这里作为示例,让您了解所使用的依赖需要由您的 AE 提供。

或者,您也可以使用 api.removePath