Как перейти с Webpack (Vue CLI) на Vite?

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.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *