vue3 + Quasar 框架开发问题记录

Quasar 这个框架,又爱又恨。Material Design 风格,pc 和移动都可以用,集成 ssr、pwa、electron 等模式,算是一站全包的框架。但用起来又确实不顺手,因为用的人不多,遇到问题也比较折腾。

QMarkdown 组件

Quasar 通过扩展的形式支持 markdown,但是按照文档走下来并不顺利。

这个组件有两种包,一个是 @quasar/quasar-app-extension-qmarkdown,还有一个是 @quasar/quasar-ui-qmarkdown。

前一个包是对第二个包的封装,可以通过 quasar ext add @quasar/qmarkdown 命令自动安装。我好像是安装失败了。所以用了第二个包。

yarn add @quasar/quasar-ui-qmarkdown@next

然后根据网上的教程,写 boot 文件:

// src/boot/q-markdown.ts
import { boot } from 'quasar/wrappers';
import Plugin from '@quasar/quasar-ui-qmarkdown';
import '@quasar/quasar-ui-qmarkdown/dist/index.css';

export default boot(({ app }) => {
  app.use(Plugin);
});

但是,Plugin 提示报错,类型不匹配。所以放弃了 boot 文件注入的方式,改成需要的页面自行引入。

import { QMarkdown } from '@quasar/quasar-ui-qmarkdown';

全局注册 component 我也尝试了,好像无法解析,页面里就是 q-markdown 这个标签。

scss 图片引入

之前 vue 项目在配置了 alias 的情况下,css 图片的引入使用 url(~@/assets/xxx) 这种形式就可以了。但是,quasar 项目里没用。

文档里倒是有相关介绍。https://quasar.dev/quasar-cli-vite/handling-assets#regular-assets-src-assets

可惜的是,url(./images/xxx) 这种写法依然无效。

后来,阴差阳错,把 ~ 去掉就好了。url(~@/assets/xxx)

路由参数匹配限制

页面需要适配多语言,我选择用路由参数的方式识别:

path: `/:lang(${config.langIsoList.join('|')})/`

一开始是没有括号中的内容的,直接就是: path: ‘/:lang’。但这就导致一个问题,这个 lang 可以是任意字符串。比如路径是 /about,这个 about 就被理解为 lang。但实际上的页面路径应该是 /zh-CN/about 这样。所以就需要把 lang 的取值限制在一个范围,其余的值直接匹配 404 页面。

ssr 下的 api proxy

对于开发环境的接口代理,直接在 quasar.config.js 里配置 proxy 就可以了:

devServer: {
      // https: true
      open: true, // opens browser window automatically
      proxy: {
        '/api': {
          target: 'http://localhost:8570',
          changeOrigin: true,
          rewrite: (path) => path.replace(/^\/api/, ''),
        },
      },
    },

但是,ssr 环境该怎么弄呢?devServer 里的配置对 ssr 里的 express 环境不起作用啊。然后在网上搜索了一番,又摸索了一阵,才弄出个可以生效的配置。

首先,安装 http-proxy-middleware 这个包。

yarn add http-proxy-middleware

然后,src-ssr/middlewares 目录下添加一个中间件:

// src-ssr/middlewares/proxy.ts
import { createProxyMiddleware } from 'http-proxy-middleware';
import { ssrMiddleware } from 'quasar/wrappers';

export default ssrMiddleware(({ app }) => {
  app.use(
    '/api',
    // @ts-ignore
    createProxyMiddleware({
      target: 'http://localhost:8570',
      changeOrigin: true,
      pathRewrite: (path) => path.replace(/^\/api/, ''),
    })
  );
});

这里添加 ts-ignore 是因为提示类型不匹配,但实际上是可以运行的。createProxyMiddleware 这个函数返回的就是 RequestHandler 类型的函数,但就是提示不匹配,我也不知道为什么。

接着,quasar.config.js 文件 ssr 部分的 middlewares 字段添加 ‘proxy’。代码里设置 baseUrl 的部分写成这样:

baseUrl: process.env.SERVER ? 'http://localhost:8570' : '/api',

为什么不直接用 /api?因为 ssr 渲染时服务端请求接口无法走代理。上面的 ssr 代理配置是用户端向服务端 ssr 端口发请求时用的。

ssr 下 token 的处理

前端通过 axios 发送 cookie 给后端可以通过配置 withCredentials 实现,但 ssr 下又该如何处理呢?ssr 是 node 环境,不是浏览器,没有 cookie。而从请求的结果来看,ssr 也确实没有默认把 cookie 转发给后端。也许有方法配置,但我没找到,所以只能换个方式——通过 headers 传递 token。

前端在 axios 的拦截器里获取 cookie 里的 token,给拼接到 headers 里发送给后端,ssr 环境下也会把 headers 里的值转给后端,这样不管哪种环境,鉴权都可以完成。

pm2 配置文件拷贝

打包出 ssr 文件后,可以直接安装依赖,用 node 启动服务。但是,为了更好的体验,比如错误自动重启之类的,还是习惯用 pm2 启动。但这就需要打包的时候把 pm2 配置文件——ecosystem.config.js 也打包到 dist/ssr 目录。这个文件我放在根目录下,然后配置 quasar.config.js 的 build 部分,添加 afterBuild 钩子。

afterBuild: async () => {
  fs.copyFileSync(
    './ecosystem.config.js',
    './dist/ssr/ecosystem.config.js'
  );
},

在 setup 之外使用 i18n

vue-i18n 的 useI18n 钩子只能在 setup 顶层使用,但实际项目中,我又需要在 setup 之外使用 i18n,比如 axios 统一拦截器里对报错进行处理。为此,我只能在 i18n 的 boot 文件里导出实例,然后 axios 里引入。

// src/boot/i18n.ts
const i18n = createI18n({
  locale: 'zh-CN',
  legacy: false,
  messages,
})

export default boot(({ app }) => {
  // Set i18n instance on app
  app.use(i18n)
})

export { i18n }

// src/boot/axios.ts
import { i18n } from './i18n'

errorNotify(
  {
    message: msg,
  },
  {
    t: i18n.global.t,
  }
)

这个解决方案来自 quasar 的 issue:“How to use i18n in js file”。

在 worker 中使用 quasar 自带的 colors 方法会报错–document is not defined

quasar 自带了颜色处理函数–colors。

import { colors } from 'quasar'

但是,这个 colors 不能在 worker 中使用,因为其中的部分方法用到了 document。

// 源码
export function getPaletteColor (colorName) {
  if (typeof colorName !== 'string') {
    throw new TypeError('Expected a string as color')
  }

  const el = document.createElement('div')

  el.className = `text-${ colorName } invisible fixed no-pointer-events`
  document.body.appendChild(el)

  const result = getComputedStyle(el).getPropertyValue('color')

  el.remove()

  return rgbToHex(textToRgb(result))
}