web development
October 1

Установка и настройка Quasar Framework (Vue)

Установка и настройка frontend на основе Quasar Framework (https://quasar.dev/).

Часть проекта User Dashboard Base (Quasar + Node.js + SQLite).

ОС: Ubuntu 24.01

Стек: pnpm, Quasar Framework, Typescript, CamelCase, git.

1. Устанавливаем Quasar CLI глобально:

# Установка Quasar CLI
npm install -g @quasar/cli

# Проверяем установку
quasar --version

2. Создаем Quasar проект в папке frontend:

# Переходим в корень проекта
cd quasar-node-usercabinet

# Удаляем старую папку frontend (если создали)
rm -rf frontend

# Создаем новый Quasar проект
npm init quasar

При создании проекта выберите следующие опции:

✔ What would you like to build? › App with Quasar CLI, let's go!
✔ Project folder: … frontend
✔ Pick script type: › Typescript
✔ Pick Quasar App CLI variant: › Quasar App CLI with Vite
✔ Package name: … quasar-node-usercabinet-frontend
✔ Project product name: (must start with letter if building mobile apps) … User Cabinet
✔ Project description: … Multilingual user cabinet with authentication
✔ Author: … [ваше имя]
✔ Pick a Vue component style: › Composition API with <script setup>
✔ Pick your CSS preprocessor: › Sass with SCSS syntax
✔ Check the features needed for your project: › Linting (vite-plugin-checker + ESLint + vue-tsc), State Management (Pinia), vue-i18n
✔ Add Prettier for code formatting? … yes
✔ Install project dependencies? (recommended) › No, I will handle that myself

3. Проверяем установку:

cd frontend
pnpm install
pnpm run lint — --fix
quasar dev

4. Обновляем `frontend/.env.example`:

# API Configuration
VITE_API_URL=http://localhost:3000/api
VITE_API_TIMEOUT=30000

# App Configuration
VITE_APP_NAME="User Cabinet"
VITE_APP_VERSION=1.0.0

# Feature Flags
VITE_ENABLE_DEBUG=true
VITE_AUTO_REFRESH_INTERVAL=60000

# Default Language (en, ru, sr)
VITE_DEFAULT_LANGUAGE=en

5. Копируем `.env.example` в `.env`:

cp .env.example .env

6. Обновляем `frontend/package.json` (добавляем несколько полезных пакетов):

# Устанавливаем дополнительные зависимости
pnpm add @vueuse/core dayjs jwt-decode
pnpm add -D @types/node
pnpm add axios

7. Создаем базовую структуру папок для нашего проекта:

# Создаем дополнительные папки в src
mkdir -p src/api
mkdir -p src/types
mkdir -p src/utils
mkdir -p src/composables
mkdir -p src/services

Версиях Quasar с TypeScript используется `quasar.config.ts`. Настроим конфигурацию `frontend/quasar.config.ts`:

/* eslint-env node */

// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js

import { configure } from 'quasar/wrappers';
import path from 'node:path';

export default configure(function (/* ctx */) {
return {
eslint: {
warnings: true,
errors: true,
},

// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: ['i18n', 'axios'],

// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ['app.scss'],

// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
// 'ionicons-v4',
// 'mdi-v5',
// 'fontawesome-v6',
// 'eva-icons',
// 'themify',
// 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!

'roboto-font', // optional, you are not bound to it
'material-icons', // optional, you are not bound to it
'mdi-v7',
],

// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
build: {
target: {
browser: ['es2019', 'edge88', 'firefox78', 'chrome87', 'safari13.1'],
node: 'node16',
},

vueRouterMode: 'history', // available values: 'hash', 'history'

env: {
API_URL: process.env.VITE_API_URL || 'http://localhost:3000/api',
APP_VERSION: process.env.VITE_APP_VERSION || '1.0.0',
},

// viteVuePluginOptions: {},

vitePlugins: [
[
'@intlify/unplugin-vue-i18n/vite',
{
// if you want to use Vue I18n Legacy API, you need to set `compositionOnly: false`
// compositionOnly: false,

// if you want to use named tokens in your Vue I18n messages, such as 'Hello {name}',
// you need to set `runtimeOnly: false`
// runtimeOnly: false,

// you need to set i18n resource including paths !
include: [path.resolve(__dirname, './src/i18n/**')],
},
],
],

alias: {
'@': path.join(__dirname, './src'),
},

// https://v2.quasar.dev/quasar-cli-vite/handling-vite
extendViteConf(viteConf) {
// Add any custom Vite configuration here if needed
},
},

// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: {
// https: true
open: true, // opens browser window automatically
port: 9000,
proxy: {
// proxy all requests starting with /api
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
},
},
},

// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
framework: {
config: {
brand: {
primary: '#1976d2',
secondary: '#26a69a',
accent: '#9c27b0',
dark: '#1d1d1d',
'dark-page': '#121212',
positive: '#21ba45',
negative: '#c10015',
info: '#31ccec',
warning: '#f2c037',
},

notify: {
position: 'top',
timeout: 3000,
textColor: 'white',
actions: [{ icon: 'close', color: 'white' }],
},

loading: {
delay: 200,
message: 'Loading...',
spinnerSize: 60,
spinnerColor: 'primary',
},
},

// iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack

// For special cases outside of where the auto-import strategy can have an impact
// (like functional components as one of the examples),
// you can manually specify Quasar components/directives to be available everywhere:
//
// components: [],
// directives: [],

// Quasar plugins
plugins: [
'Notify',
'Loading',
'Dialog',
'LocalStorage',
'SessionStorage',
],
},

// animations: 'all', // --- includes all animations
// https://v2.quasar.dev/options/animations
animations: 'all',

// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#sourcefiles
// sourceFiles: {
// rootComponent: 'src/App.vue',
// router: 'src/router/index',
// store: 'src/store/index',
// registerServiceWorker: 'src-pwa/register-service-worker',
// serviceWorker: 'src-pwa/custom-service-worker',
// pwaManifestFile: 'src-pwa/manifest.json',
// electronMain: 'src-electron/electron-main',
// electronPreload: 'src-electron/electron-preload'
// },

// https://v2.quasar.dev/quasar-cli-vite/developing-ssr/configuring-ssr
ssr: {
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
// will mess up SSR

// extendSSRWebserverConf (esbuildConf) {},
// extendPackageJson (json) {},

pwa: false,

// manualStoreHydration: true,
// manualPostHydrationTrigger: true,

prodPort: 3000, // The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)

middlewares: [
'render', // keep this as last one
],
},

// https://v2.quasar.dev/quasar-cli-vite/developing-pwa/configuring-pwa
pwa: {
workboxMode: 'generateSW', // or 'injectManifest'
injectPwaMetaTags: true,
swFilename: 'sw.js',
manifestFilename: 'manifest.json',
useCredentialsForManifestTag: false,
// useFilenameHashes: true,
// extendGenerateSWOptions (cfg) {}
// extendInjectManifestOptions (cfg) {},
// extendManifestJson (json) {}
// extendPWACustomSWConf (esbuildConf) {}
},

// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-cordova-apps/configuring-cordova
cordova: {
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
},

// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true,
},

// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/configuring-electron
electron: {
// extendElectronMainConf (esbuildConf)
// extendElectronPreloadConf (esbuildConf)

inspectPort: 5858,

bundler: 'packager', // 'packager' or 'builder'

packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options

// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',

// Windows only
// win32metadata: { ... }
},

builder: {
// https://www.electron.build/configuration/configuration

appId: 'quasar-node-usercabinet',
},
},

// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
bex: {
contentScripts: ['my-content-script'],

// extendBexScriptsConf (esbuildConf) {}
// extendBexManifestJson (json) {}
},
};
});

### Также обновим `frontend/src/boot/i18n.ts`:

import { boot } from 'quasar/wrappers';
import { createI18n } from 'vue-i18n';
import messages from 'src/i18n';

export type MessageLanguages = keyof typeof messages;
export type MessageSchema = typeof messages['en'];

/* eslint-disable @typescript-eslint/no-empty-interface */
declare module 'vue-i18n' {
export interface DefineLocaleMessage extends MessageSchema {}
export interface DefineDateTimeFormat {}
export interface DefineNumberFormat {}
}
/* eslint-enable @typescript-eslint/no-empty-interface */

// Get saved language from LocalStorage or use default
const savedLanguage = localStorage.getItem('userLanguage') ||
import.meta.env.VITE_DEFAULT_LANGUAGE ||
'en';

const i18n = createI18n({
locale: savedLanguage,
fallbackLocale: 'en',
legacy: false,
globalInjection: true,
messages,
});

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

export { i18n };

Добавим `frontend/src/boot/axios.ts`:

import { boot } from 'quasar/wrappers';
import axios, { AxiosInstance } from 'axios';

declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$axios: AxiosInstance;
$api: AxiosInstance;
}
}

//
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api',
timeout: parseInt(import.meta.env.VITE_API_TIMEOUT || '30000'),
headers: {
'Content-Type': 'application/json',
},
});

// Request interceptor for auth
api.interceptors.request.use(
(config) => {
const token = localStorage.getItem('accessToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);

// Response interceptor for error handling
api.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;

if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;

try {
const refreshToken = localStorage.getItem('refreshToken');
if (refreshToken) {
const response = await api.post('/auth/refresh', {
refreshToken,
});

const { accessToken, refreshToken: newRefreshToken } = response.data;
localStorage.setItem('accessToken', accessToken);
localStorage.setItem('refreshToken', newRefreshToken);

originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return api(originalRequest);
}
} catch (refreshError) {
// Refresh failed, redirect to login
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);

export default boot(({ app }) => {
app.config.globalProperties.$axios = axios;
app.config.globalProperties.$api = api;
});

export { api };

Обновим `frontend/src/env.d.ts`:

/* eslint-disable */

/// <reference types="vite/client" />

declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: string;
VUE_ROUTER_MODE: 'hash' | 'history' | 'abstract' | undefined;
VUE_ROUTER_BASE: string | undefined;
}
}

interface ImportMetaEnv {
readonly VITE_API_URL: string;
readonly VITE_API_TIMEOUT: string;
readonly VITE_APP_NAME: string;
readonly VITE_APP_VERSION: string;
readonly VITE_ENABLE_DEBUG: string;
readonly VITE_AUTO_REFRESH_INTERVAL: string;
readonly VITE_DEFAULT_LANGUAGE: string;
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}

Теперь зафиксируем изменения в основной ветке main (корневой коммит):

# Добавляем изменения
git add .

# Создаем коммит
git commit -m "fix: update Quasar configuration to TypeScript format
- Update quasar.config.ts
- Update boot files for TypeScript support
- Configure axios with interceptors for auth
- Setup i18n with TypeScript types
- Update environment variable types"