07.02.22 Vue 3 стал основной версией фреймворка. Для многих авторов библиотек, которые думали, что этот день еще не наступил, это, возможно, стало шоком. Но я хочу обратить ваше внимание на другой случай — возможно, вы участвуете в проекте, который построен на Vue 3, но был запущен с помощью Vue CLI — потому что на момент запуска это было очень хорошим решением. Но сегодня Эван Ты проповедует совсем другую истину — простой способ начать проект на Vue 3 — это инструмент: create-vue
, а результат — совершенно другой набор инструментов.
Прошли месяцы, ваш проект уже внедрен в производство, но он продолжает развиваться по мере поступления новых клиентов и запросов на новые функции. Возможно, вы думаете: все работает хорошо, зачем что-то менять? Если вы опытный разработчик, вы наверняка знаете ответ на этот вопрос — это технологический долг. При построении любого решения за значительное время мы полагаемся на работу других разработчиков, содержащуюся в различных библиотеках. В конце концов, именно поэтому мы используем Vue, множество других библиотек и инструментов разработки, таких как Webpack, Jest, Cypress, ESLint, Stylelint, Prettier и т.д. и т.п.
Три причины для изменений
Наш мир JS развивается очень быстро. В популярной шутке о том, что JS-разработчики каждую неделю осваивают новый фреймворк, есть доля правды. Направление развития задают две силы, управляющие экосистемой JS: TC-39 — т.е. комитет, ответственный за создание последовательных версий стандарта ECMAScript, который является основой для реализаций, которые находят свой путь в среды выполнения (т.е. браузеры, NodeJS, Deno и т.д.) и сообщества — т.е. нас, разработчиков, которые создают библиотеки, инструменты или делятся шаблонами для различных решений.
Вторая причина изменений — это среда Vue, которая также эволюционировала. Эван Вы создали замечательный бандлер Vite, который в настоящее время является основной отправной точкой для всех новых проектов в Vue 3. То, что последует за этим, не просто смена инструмента. Дело не в типе: вот молоток с обычной ручкой, а вот молоток с эргономичной ручкой. Это изменение гораздо более глубокое, поскольку Vite основан не на системе модулей Node (CommonJS), а на собственных модулях ECMAScript (ESM). Эти различия приводят к множеству других изменений и в то же время открывают новые возможности.
Поэтому проект, основанный на Vite, хотя и не обязательно заставит перестроить наш код, изменит некоторые из инструментов, которые мы используем до сих пор, или их конфигурацию, или то, как мы их используем. Именно поэтому все больше авторов библиотек стараются включать в свои сборки обе версии модулей, а некоторые переходят исключительно на модули ESM. Вот почему не стоит откладывать миграцию, именно здесь действительно применима поговорка — если вы стоите на месте, вы идете назад.
Третья причина для перемен проста и, как мне кажется, наиболее заметна в повседневной работе. Я имею в виду скорость запуска. Пример проекта, который я упомянул, не придуман. В компании Evionica одним из проектов является W&B 2.0, который находится в разработке уже более 10 месяцев и внедрен в производство у нескольких клиентов. Он был запущен с помощью Vue-CLI v4.5., т.е. работает на Webpack 4. Следовательно, скорость запуска этого проекта… 33 s. Это обычное время для Webpack 4 — ничего поразительного. Однако после быстрой попытки перенести этот проект на Vite, скорость запуска версии разработки падает до <1 с. Так что, вероятно, есть за что бороться.
Давайте перенесем Webpack!
Теперь, в следующих шагах, я покажу, как может выглядеть миграция большого проекта, работающего с VueCLI, собранного с помощью Webpack, на Vite. Каков план?
- мы устанавливаем и настраиваем Vite
- исследуем конфигурацию Webpack и находим элементы конфигурации и плагины, которые необходимо перенести
- Перенос конфигурации
- Найдите аналогичные плагины или решения с тем же эффектом
Установка и настройка Vite
yarn add -D vite @vitejs/plugin-vue
Создайте файл vite.config.ts
с базовой конфигурацией
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
});
Теперь нам нужно найти эквиваленты для вещей, которые мы имеем в текущем проекте и которые являются плагином для Webpack или частью конфигурации проекта (например, пользовательские псевдонимы). Начнем с псевдонимов — вы найдете их в конфигурационном файле webpack, а также в конфигурации TypeScript.
{
"compilerOptions": {
// TYPICAL TSCONFIG NOTHING FANCY
"paths": {
"@/*": [
"src/*"
],
"@np/*": [
"src/modes/narrow-pax/*"
],
"@wf/*": [
"src/modes/wide-freighter/*"
],
"~~/*": [
"tests/*"
]
}
}
}
Из того, что мы видим, достаточно следующих псевдонимов: @
, @np
, @wf
и ~~
. Поэтому давайте настроим их в Vite.
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
import { fileURLToPath } from 'url'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
'@np': fileURLToPath(new URL('./src/modes/narrow-pax', import.meta.url)),
'@wf': fileURLToPath(new URL('./src/modes/wide-freighter', import.meta.url)),
'~~': fileURLToPath(new URL('./tests', import.meta.url)),
},
},
})
Теперь давайте рассмотрим конфигурацию webpack.
/* eslint-disable @typescript-eslint/no-var-requires */
const clearTestId = require('unplugin-clear-testid/webpack');
const webpack = require('webpack');
const childProcess = require('child_process');
const fs = require('fs');
const path = require('path');
const manifestOptions = require('./manifest');
const packageJson = fs.readFileSync('./package.json').toString();
const mapSizeToDimensions = {
['A4']: {
width: '210mm',
height: '297mm',
},
['letter']: {
width: '215.9mm', // 8.5in
height: '279.4mm', // 11in
},
};
let gitLastCommitHash = '';
try {
gitLastCommitHash = childProcess
.execSync('git rev-parse --short HEAD')
.toString()
.trim();
} catch (e) {
console.error(e);
}
/* eslint-disable @typescript-eslint/camelcase */
module.exports = {
publicPath: '/',
productionSourceMap: process.env.NODE_ENV !== 'production',
configureWebpack: {
resolve: {
alias: {
'@np': path.resolve(__dirname, 'src/modes/narrow-pax'),
'@wf': path.resolve(__dirname, 'src/modes/wide-freighter'),
},
},
plugins: [
new webpack.DefinePlugin({
'process.env': {
VUE_APP_VERSION: JSON.stringify(JSON.parse(packageJson).version || 0),
VUE_APP_COMMIT_HASH: JSON.stringify(gitLastCommitHash),
},
}),
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1,
}),
clearTestId.default({
attrs: ['data-testid', 'data-cy'],
testing: process.env.NODE_ENV === 'testing',
}),
],
output: {
chunkFilename: '[id].js',
},
},
css: {
loaderOptions: {
sass: {
additionalData: `$DEFAULT_PAGE_SIZE: ${process.env.VUE_APP_DEFAULT_PAGE_SIZE}; $PAGE_HEIGHT: ${
mapSizeToDimensions[process.env.VUE_APP_DEFAULT_PAGE_SIZE].height
}; $PAGE_WIDTH: ${mapSizeToDimensions[process.env.VUE_APP_DEFAULT_PAGE_SIZE].width};`,
},
},
},
};
Есть несколько предметов, которые нам нужно переместить сюда:
- создать sourceMap в режиме dev
- плагин
clearTestId
- ограничение на создание чанков
- конфигурация sass
- пользовательский плагин, внедряющий две переменные окружения
Карты источников в режиме dev
Шаг за шагом, давайте начнем с карты источника. В Vite за это отвечает опция build.sourcemap
, которая по умолчанию имеет значение false
— так что мы имеем то же самое из коробки. Все прошло гладко 🙂
Плагин clearTestId
.
Следующее — unplugin-clear-testid
. Когда вы видите, что название плагина начинается со слов unplugin
, это означает, что автор полагается на решение, которое позволяет собрать плагин один раз в нескольких версиях для разных бандлеров: Webpack, Rollup и Vite. Так что все, что нам нужно сделать, это:
import vue from '@vitejs/plugin-vue';
import clearTestId from 'unplugin-clear-testid/vite'
import { defineConfig } from 'vite';
import { fileURLToPath } from 'url';
export default defineConfig({
plugins: [vue(), clearTestId()],
// pozostała część konfiguracji
});
Ограничить количество создаваемых чанков
Еще одна вещь в списке — ограничение количества чанков. В роллапе, который Vite использует для создания производственной версии нашего приложения, мы можем добиться этого без использования какого-либо плагина. Важно понять, что вызывает появление чанков — в подавляющем большинстве случаев они возникают из-за асинхронного импорта модулей, обычно компонентов. В подавляющем большинстве случаев это действие является максимально правильным. Однако есть и исключения — для автономных приложений важно, чтобы весь код приложения загружался с самого начала. Потому что пользователь, войдя в систему и загрузив приложение, может выйти в автономный режим и использовать его в полном объеме. Итак, давайте изменим настройки рулонов:
// vite.config.ts
export default defineConfig({
// ...
build: {
// wyłączamy dzielenie plików CSS
cssCodeSplit: false,
// wyłączamy blokady wielkościowe przed łączeniem kodu
assetsInlineLimit: 100000000,
chunkSizeWarningLimit: 100000000,
rollupOptions: {
output: {
// inline dynamicznych importów
inlineDynamicImports: true,
// wyłączamy dzielenie na chunki
manualChunks: undefined
},
},
},
});
Настройка переменных Sass
Следующим пунктом в списке является внедрение переменных в файлы sass
. Здесь нам нужно сделать дополнительный шаг, потому что существуют различия в том, что нужно webpack и что нужно Vite для работы с sass. Webpack требует специального загрузчика (sass-loader
и библиотека node-sass
), тогда как Vite нужна только библиотека sass
. Так что давайте добавим его:
yarn add -D sass
Теперь мы перенесем параметры конфигурации sass из webpack. Vite позволяет аналогичным образом передавать параметры конфигурации:
export default defineConfig({
// ...
css: {
preprocessorOptions: {
sass: {
additionalData: `$DEFAULT_PAGE_SIZE: ${process.env.VUE_APP_DEFAULT_PAGE_SIZE}; $PAGE_HEIGHT: ${
mapSizeToDimensions[process.env.VUE_APP_DEFAULT_PAGE_SIZE].height
}; $PAGE_WIDTH: ${mapSizeToDimensions[process.env.VUE_APP_DEFAULT_PAGE_SIZE].width};`,
},
}
}
});
Однако, чтобы это работало, нам нужно получить доступ к переменным окружения внутри конфигурации Vite. Для этого мы изменим использование функции defineConfig
, которая сейчас получает объект, и будем получать функцию, которая возвращает объект — таким образом, внутри функции мы сможем выполнять определенные операции и обращаться к переменным, которые получаются в результате.
import vue from '@vitejs/plugin-vue';
import clearTestId from 'unplugin-clear-testid/vite'
import { defineConfig, loadEnv } from "vite";
import { fileURLToPath } from 'url';
const mapSizeToDimensions = {
// bez zmian
};
export default defineConfig(({ command, mode }) => {
const env = loadEnv(process.env.NODE_ENV ?? "", process.cwd(), 'VUE_APP')
return {
// ...
envPrefix: 'VUE_APP',
css: {
preprocessorOptions: {
sass: {
additionalData: `$DEFAULT_PAGE_SIZE: ${env.VUE_APP_DEFAULT_PAGE_SIZE}; $PAGE_HEIGHT: ${mapSizeToDimensions[env.VUE_APP_DEFAULT_PAGE_SIZE].height
}; $PAGE_WIDTH: ${mapSizeToDimensions[env.VUE_APP_DEFAULT_PAGE_SIZE].width};`,
}
}
}
}
});
Еще одним отличием, вытекающим из дизайна Webpack и Vite и их подхода к модулям (CommonJS/ESM), является работа с переменными окружения внутри кода приложения.
При использовании webpack мы обращаемся к process.env.
Node, тогда как при использовании Vite мы используем родной import.meta.env.
. Второе отличие заключается в том, что имена переменных окружения в проекте на базе Vue-CLI должны начинаться с VUE_APP
, а в Vite — с VITE
. В то же время — мы можем легко изменить это и избавить себя от множества изменений файлов. Более того, мы сделали это при внедрении системных переменных в конфигурацию sass
. Таким образом, все, что нам нужно сделать в нашем проекте, это быстрое действие «поиск и замена»: process.env.
-> import.meta.env.
.
Еще один случай, связанный с переменными окружения, это использование NODE_ENV
— у нас не будет этой переменной, но Vite предоставляет 3 другие, которые могут быть полезны:
Примечание: обратите внимание на некоторые файлы конфигурации инструментов (включая даже файл vite.config.ts
), которые запускаются из консоли (и, следовательно, NodeJS), и они могут использовать process.env.
.
Если вы используете TypeScript в своем проекте (как в моем случае), то хорошей идеей для вас будет добавить определение системных переменных в локальный файл типов. Возможно, у вас уже есть файл типа shims-vue.d.ts
, поэтому вы можете изменить его на env.d.ts
или оставить это имя, но стоит добавить в него несколько вещей:
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VUE_APP_API_URL: string
readonly VUE_APP_API_HEALTH_CHECK: string
readonly VUE_APP_AWS_URL: string
readonly VUE_APP_DEFAULT_PAGE_SIZE: string
}
Ввод переменных окружения
Следующее в списке — внедрение плагина в две переменные окружения. На первый взгляд это может показаться странным — в конце концов, их можно определить в файле .env
. Можно, но тогда они не смогут иметь динамические значения, как в данном случае. Общая идея заключается в том, чтобы тайно передать в код номер версии из package.json и хэш последнего git-коммита.
В Vite мы можем использовать опцию define
в конфиге и определить глобальные константы (не в смысле JS, опция define
!== const
), которые будут подменяться во время сборки, а также во время работы сервера разработки. Поэтому давайте добавим это в конфигурацию:
let gitLastCommitHash = ''
try {
gitLastCommitHash = childProcess
.execSync('git rev-parse --short HEAD')
.toString()
.trim()
} catch (e) {
console.error(e)
}
const packageJson = fs.readFileSync('./package.json').toString()
let version = JSON.parse(packageJson).version
export default defineConfig(({ command, mode }) => {
// ...
return {
// ...
define: {
__APP_VERSION__: version,
__COMMIT_HASH__: gitLastCommitHash,
},
//...
}
})
Теперь все, что нам нужно сделать, — это еще один «поиск и замена»: в тех местах кода, где мы ссылались на process.env.VUE_APP_VERSION
и process.env.VUE_APP_COMMIT_HASH
, теперь мы ссылаемся на __APP_VERSION__
и __COMMIT_HASH__
соответственно.
Для запуска Vite нам все еще нужна отправная точка — т.е. простой файл index.html
, в котором находится <div id="app"></div>
, и в который выводится наше приложение. Давайте создадим его в корневом каталоге проекта:
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="alternate icon" href="/favicon.ico" type="image/png" sizes="16x16" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" />
<link rel="mask-icon" href="/favicon.svg" color="#FFFFFF" />
<meta name="theme-color" content="#ffffff" />
</head>
<body class="h-full">
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
И это все?
Что касается простого переноса конфигурации и плагинов из Webpack в конфигурацию в Vite, что достигает того же эффекта, то да. Однако, возможно, нам еще предстоит проделать некоторую работу в нашем коде. Как я уже говорил, Webpack и Vite — это два разных мира, когда речь идет об использовании модулей. Это также имеет значение при модульном тестировании. Если проект, который вы хотите перенести на Vite, использует Jest — будьте готовы к некоторым проблемам, особенно если вы начнете использовать плагины vite, которые (облегчая жизнь) отвечают за автоимпорт компонентов (unplugin-vue-components), или автомаршрутизацию по примеру Nuxta (vite-plugin-pages), или отвечают за PWA (vite-plugin-pwa). В них часто используются виртуальные импорты или пути, и Jest просто не может с этим справиться.
Эта проблема, конечно, может быть решена с помощью заглушек и правильной конфигурации Jest или путем замены программы для запуска тестов на ту, которая является ESM-первой, например vitest
.