Перспективный инструмент для сборки интерфейса — vite

Vue.js Webpack
Перспективный инструмент для сборки интерфейса — vite

предисловие

Если вы недавно следили за динамикой Vue, вы найдете новые инструменты, с которыми авторы Vue в последнее время возятся.vite. vite 1.0 теперь входит в версию rc, и скоро будет официально выпущена версия 1.0. Несколько месяцев назад Ю Юйси уже представил vite на Weibo, сервере разработки, основанном на собственном ESM браузера.

尤雨溪微博

Когда Webpack впервые появился, чтобы решить проблему, заключающуюся в том, что браузеры с низкими версиями не поддерживают модуляризацию ESM, он объединил различные разбросанные модули JavaScript в один файл и объединил несколько файлов сценариев JavaScript в один файл, чтобы уменьшить количество HTTP-запросов. , что помогает повысить скорость первого посещения страницы. На более позднем этапе Webpack воспользовался победой и представил механизмы загрузчика и плагина для предоставления различных возможностей, связанных с созданием (побег от Babel, слияние css, сжатие кода), заменив Browserify и Gulp в тот же период.

Сегодня, с преобладанием HTTP/2 и предстоящим выпуском HTTP/3 в сочетании с коммерческим использованием сетей 5G, уменьшение количества HTTP-запросов мало что дает, и новая версия браузера в основном поддерживает ESM (<script module>).

JavaScript modules

начать

Вите пришло вместе со своей исторической миссией. Поскольку процесс упаковки опущен, можно сказать, что при первом запуске vite его можно открыть за считанные секунды. Вы можете взглянуть на Gif, который я записал, и вы можете войти в разработку, не дожидаясь вообще.

启动 vite

Если вы хотите попробовать vite, вы можете напрямую передать следующую команду:

$ npm init vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev

npm init vite-appкоманда будет выполненаnpx create-vite-app, взято из нпмcreate-vite-appмодуль, а затем сгенерировать файл шаблона в указанную папку через соответствующий шаблон.

{
  "name": "vite-app",
  "version": "0.0.1",
  "scripts": {
    "dev": "vite",
    "build": "vite build"
  },
  "dependencies": {
    "vue": "^3.0.0-rc.1"
  },
  "devDependencies": {
    "vite": "^1.0.0-rc.1",
    "@vue/compiler-sfc": "^3.0.0-rc.1"
  }
}

В настоящее время vite используется с vue 3. Если вы хотите использовать vite в vue 2, по оценкам, вам придется дождаться выпуска официальной версии. Конечно, независимо от того, можете ли вы перейти на vue 3 или vue 3, vue 3 намного превосходит vue 2 с точки зрения производительности, размера пакета и благословения ts. В дополнение к vue, vite также предоставляет шаблоны, связанные с реакцией и предварительными настройками.

其他模板

Структура каталогов сгенерированного проекта vue выглядит следующим образом.

目录结构

Заявка на проект естьindex.html, собственный ESM браузера (type="module") способность. Для ознакомления с возможностями браузера ESM вы можете прочитать мою предыдущую статью«Нынешняя жизнь фронтальной модуляризации».

<script type="module" src="/src/main.js"></script>

После того, как все файлы js будут обработаны vite, путь к модулю их импорта будет изменен, добавьте впереди/@modules/. Когда браузер запрашивает модуль импорта, vitenode_modulesНайдите соответствующий файл и верните его.

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')

请求

Таким образом, процесс упаковки исключается, что значительно повышает эффективность разработки. Конечно, vite также предоставляет производственный режим, в котором для сборки используется Rollup.

говорить о снежном покрове

Первым инструментом, использующим встроенные в браузер возможности ESM, является не vite, а инструмент под названиемsnowpackИнструмент. До того, как Snowpack был выпущен 1.0, он назывался@pika/web.

snowpack rename

pikaКоманда сделала снежный покров, потому что pika стремится ускорить работу веб-приложений на 90%.

pika

Поскольку многие современные веб-приложения построены на основе разных модулей с открытым исходным кодом, и эти модули с открытым исходным кодом упаковываются в один пакет с помощью инструментов упаковки, таких как веб-пакет, если все эти модули с открытым исходным кодом находятся с одного адреса CDN и поддерживают междоменное кэширование, то эти модули с открытым исходным кодом нужно загрузить только один раз, и другие веб-сайты используют те же модули с открытым исходным кодом, их не нужно загружать снова, а локальный кеш считывается напрямую.

Например, Taobao и Tmall разработаны на основе реакции + редукса + antd + loadsh.После того, как я открою Taobao, мне не нужно повторно загружать эти модули с открытым исходным кодом при входе в Tmall, мне нужно только загрузить некоторые службы, связанные на страницу Tmall. Подойдет код. С этой целью pika построила CDN (skypack) используется для загрузки некоторых модулей esm в npm.

Позже, когда был освобожден SnowPack, команда Pika опубликовала статью под названием «Путь».«Будущее без Webpack»В статье всем рассказывается, что можно попробовать отказаться от webpack и изменить жизнь webpack.

snowpack

В README vite также упоминается, что в некоторых аспектах упоминается снежный покров, и перечислены сходства и различия между vite и снежным покровом.

Different

Snowpack теперь выпущен для v2, мы можем найти исходный код периода v1, чтобы увидеть раннюю реализацию Snowpack.

Анализ исходного кода

На github можно найти версию snowpack v1.0.0 по тегу git.После скачивания он немного глючит.При чтении исходного кода рекомендуется переходить на v1.2.0(GitHub.com/pikapikalight/скажи нет…).

существуетpackage.jsonКак видно в@pika/packДля упаковки этот инструмент передает процесс упаковки, который чем-то похож на gulp. Если вам интересно, вы можете узнать об этом. Здесь основное внимание уделяется принципу снежного покрова.

{
  "scripts": {
    "build": "pika build"
  },
  // snowpack 的构建工具
  "@pika/pack": {
    "pipeline": [
      [
        "@pika/plugin-ts-standard-pkg"
      ],
      [
        "@pika/plugin-copy-assets"
      ],
      [
        "@pika/plugin-build-node"
      ],
      [
        "@pika/plugin-simple-bin",
        {
          // 通过 snowpack 运行命令
          "bin": "snowpack"
        }
      ]
    ]
  }
}

Здесь мы берем проект vue в качестве примера и используем Snowpack для запуска проекта vue 2. Структура каталогов следующая:

目录结构

Если вы хотите внедрить снежный покров в проект, вам нужно добавить снежный покров в проектpackage.jsonдобавить конфигурацию, связанную снегопадом, тем более важной конфигурацией этоsnowpack.webDependencies, указывающий на зависимости текущего проекта, эти два файла будут упакованы Snowpack вweb_modulesсодержание.

{
  "scripts": {
    "build": "snowpack",
    "start": "serve ./"
  },
  "dependencies": {
    "http-vue-loader": "^1.4.2",
    "vue": "^2.6.12"
  },
  "devDependencies": {
    "serve": "^11.3.2",
    "snowpack": "~1.2.0"
  },
  "snowpack": {
    "webDependencies": [
      "http-vue-loader",
      "vue/dist/vue.esm.browser.js"
    ]
  }
}

бегатьnpm run buildПосле этого новыйweb_modulesкаталог, файлы в этом каталоге - это то, что у нас есть вsnowpack.webDependenciesДва файла js, объявленные в .

npm run build

web_modules

Когда снежный покров запущен, он вызывает sourcesrc/index.tsСпособ CLI In, сокращенная версия метода выглядит следующим образом:

// 精简了部分代码,如果想看完整版建议去 github
// https://github.com/pikapkg/snowpack/blob/v1.2.0/src/index.ts
const cwd = process.cwd();

export async function cli(args: string[]) {
  // 解析命令行参数
  const { dest = 'web_modules' } = yargs(args);
  // esm 脚本文件的输出目录,默认为 web_modules
  const destLoc = path.resolve(cwd, dest);
  // 获取 pkg.json
  const pkgManifest: any = require(path.join(cwd, 'package.json'));
  // 获取 pkg.json 中的依赖模块
  const implicitDependencies = [
    ...Object.keys(pkgManifest.dependencies || {}),
    ...Object.keys(pkgManifest.peerDependencies || {}),
  ];
  // 获取 pkg.json 中 snowpack 相关配置
  const { webDependencies } = pkgManifest['snowpack'] || {
    webDependencies: undefined
  };

  const installTargets = [];
  // 需要被安装的模块,如果没有该配置,会尝试安装所有 dependencies 内的模块
  if (webDependencies) {
    installTargets.push(...scanDepList(webDependencies, cwd));
  } else {
    installTargets.push(...scanDepList(implicitDependencies, cwd));
  }
  // 模块安装
  const result = await install(installTargets, installOptions);
}

Этот метод считывает элементpackage.jsonфайл, если естьsnowpack.webDependenciesКонфигурация, будет установлена ​​первойsnowpack.webDependenciesМодуль, объявленный в , если такой конфигурации нет, то поставитdependenciesа такжеdevDependenciesмодули установлены. Все имена модулей будут переданы черезscanDepList, преобразуется в определенный формат и будетglobИмя модуля грамматики послеglobВосстановить в один файл.

import path from 'path';

function createInstallTarget(specifier: string): InstallTarget {
  return {
    specifier,
    named: [],
  };
}

export function scanDepList(depList: string[], cwd: string): InstallTarget[] {
  // 获取 node_modules 路径
  const nodeModules = path.join(cwd, 'node_modules');
  return depList
    .map(whitelistItem => {
    	// 判断文件名是否为 glob 语法 (e.g. `vue/*.js`)
      if (!glob.hasMagic(whitelistItem)) {
        return [createInstallTarget(whitelistItem)];
      } else {
        // 转换 glob 路径
        return scanDepList(glob.sync(whitelistItem,{cwd: nodeModules}), cwd);
      }
    })
  	// 将所有文件合并成一个数组
    .reduce((flat, item) => flat.concat(item), []);
}

Наконец, все модули будут установлены через install.

install

// 移除 .js、.mjs 后缀
function getWebDependencyName(dep: string): string {
  return dep.replace(/\.m?js$/i, '');
}

// 获取模块的类型以及绝对路径
function resolveWebDependency(dep: string): {
  type: 'JS' | 'ASSET';
  loc: string;
} {
  var packagePattern = new RegExp('^(?:@([^/]+?)[/])?([^/]+?)$')
  // 如果带有扩展名,且非 npm 模块,直接返回
  if (path.extname(dep) && !packagePattern.test(dep)) {
    const isJSFile = ['.js', '.mjs', '.cjs'].includes(path.extname(dep));
    return {
      type: isJSFile ? 'JS' : 'ASSET',
      // 还原绝对路径
      loc: require.resolve(dep, {paths: [cwd]}),
    };
  }
  // 如果是 npm 模块,需要查找模块对应的 package.json 文件
  const manifestPath = `${cwd}/node_modules/${dep}/package.json`;
  const manifestStr = fs.readFileSync(manifestPath, {encoding: 'utf8'});
  const depManifest = JSON.parse(manifestStr);
  // 然后读取 package.json 中的 module属性、browser属性
  let foundEntrypoint: string =
    depManifest['browser:module'] || depManifest.module || depManifest.browser;
  if (!foundEntrypoint) {
    // 如果都不存在就取 main 属性
    foundEntrypoint = depManifest.main || 'index.js';
  }
  return {
    type: 'JS',
    // 还原绝对路径
    loc: path.join(`${cwd}/node_modules/${dep}`, foundEntrypoint),
  };
}

// 模块安装
function install(installTargets, installOptions) {
  const {
    destLoc
  } = installOptions;
  // 使用 set 将待安装模块进行一次去重
  const allInstallSpecifiers = new Set(installTargets.map(dep => dep.specifier));
  
  // 模块查找转化
  for (const installSpecifier of allInstallSpecifiers) {
    // 移除 .js、.mjs 后缀
    const targetName = getWebDependencyName(installSpecifier);
    // 获取文件类型,以及文件绝对路径
    const {type: targetType, loc: targetLoc} = resolveWebDependency(installSpecifier);
    if (targetType === 'JS') {
      // 脚本文件
      const hash = await generateHashFromFile(targetLoc);
      // 添加到脚本依赖对象
      depObject[targetName] = targetLoc;
      importMap[targetName] = `./${targetName}.js?rev=${hash}`;
      installResults.push([installSpecifier, true]);
    } else if (targetType === 'ASSET') {
      // 静态资源
      // 添加到静态资源对象
      assetObject[targetName] = targetLoc;
      installResults.push([installSpecifier, true]);
    }
  }
  
  if (Object.keys(depObject).length > 0) {
    // 通过 rollup 打包文件
    const packageBundle = await rollup.rollup({
    	input: depObject,
      plugins: [
        // rollup 插件
        // 这里可以进行一些 babel 转义、代码压缩之类的操作
        // 还可以将一些 commonjs 的模块转化为 ESM 模块
      ]
    });
    // 文件输出到 web_modules 目录
    await packageBundle.write({
    	dir: destLoc,
    });
  }

  // 拷贝静态资源
  Object.entries(assetObject).forEach(([assetName, assetLoc]) => {
    mkdirp.sync(path.dirname(`${destLoc}/${assetName}`));
    fs.copyFileSync(assetLoc, `${destLoc}/${assetName}`);
  });

  return true;
}

Основной принцип проанализирован, давайте посмотрим на реальный случай. Переходим в htmltype="module"Введен тег scriptindex.jsкак входной файл.

<!DOCTYPE html>
<html lang="en">
  <title>snowpack-vue-httpvueloader</title>
  <link rel="stylesheet" href="./assets/style.css">

  <body>
    <h1>snowpack - Vue Example</h1>
    <div id="app"></div>
    <script type="module" src="./js/index.js"></script>
  </body>
</html>

затем вindex.js, импорт вwebDependeniesДва js-файла, объявленные и добавленные перед/web_modules.

import Vue from '/web_modules/vue/dist/vue.esm.browser.js'
import httpVueLoader from '/web_modules/http-vue-loader.js'

Vue.use(httpVueLoader)

new Vue({
  el: '#app',
  components: {
    app: 'url:./components/app.vue',
  },
  template: '<app></app>',
})

наконец прошлоnpm run start ,использоватьserveЗапустите службу узла, и вы сможете получить к ней обычный доступ.

Видно, что функции SnowPack V1 являются относительно простыми в целом, но модули, которые необходимо зависеть от, извлекаются из Node_Modules в Web_Modules, а компиляция выполняется через свертание в середине. Rollup представлен здесь в основном для сжимания и оптимизации кода JS и преобразовать некоторые модули Commonjs в модули ESM.

Но в итоге для запуска службы узла нужно использовать сторонние модули.В то время официалы тоже с энтузиазмом рассказывали, какие сторонние модули можно выбрать для предоставления услуг.

server

Версия v2 уже поддерживает включение внутреннего сервера узла для разработки, не прибегая к горячим обновлениям. Конечно, версия v2 обеспечивает поддержку модулей css в дополнение к модулям js.

принцип жизни

Разобравшись с исходным кодом Snowpack v1, давайте вернемся к принципу vite. Или так же, как раньше, возвращаясь кvite v0.1.1, когда количество кода невелико, присмотритесь к идее vite.

При запуске vite внутри запускается http-сервер для перехвата файла сценария страницы.

// 精简了热更新相关代码,如果想看完整版建议去 github
// https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/server.ts
import http, { Server } from 'http'
import serve from 'serve-handler'

import { vueMiddleware } from './vueCompiler'
import { resolveModule } from './moduleResolver'
import { rewrite } from './moduleRewriter'
import { sendJS } from './utils'

export async function createServer({
  port = 3000,
  cwd = process.cwd()
}: ServerConfig = {}): Promise<Server> {
  const server = http.createServer(async (req, res) => {
    const pathname = url.parse(req.url!).pathname!
    if (pathname.startsWith('/__modules/')) {
      // 返回 import 的模块文件
      return resolveModule(pathname.replace('/__modules/', ''), cwd, res)
    } else if (pathname.endsWith('.vue')) {
      // 解析 vue 文件
      return vueMiddleware(cwd, req, res)
    } else if (pathname.endsWith('.js')) {
      // 读取 js 文本内容,然后使用 rewrite 处理
      const filename = path.join(cwd, pathname.slice(1))
      const content = await fs.readFile(filename, 'utf-8')
      return sendJS(res, rewrite(content))
    }

    serve(req, res, {
      public: cwd,
      // 默认返回 index.html
      rewrites: [{ source: '**', destination: '/index.html' }]
    })
  })

  return new Promise((resolve, reject) => {
    server.on('listening', () => {
      console.log(`Running at http://localhost:${port}`)
      resolve(server)
    })

    server.listen(port)
  })
}

При доступе к сервису vite по умолчанию возвращается index.html.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <link rel="icon" href="/favicon.ico" />
  <title>Vite App</title>
</head>
<body>
  <div id="app"></div>
  <script type="module" src="/src/main.js"></script>
</body>
</html>

обрабатывать js-файлы

html файл запросит/src/main.js, когда служба vite вернет файл js, она будет использоватьrewriteМетод заменяет содержимое файла js один раз.

if (pathname.endsWith('.js')) {
  // 读取 js 文本内容,然后使用 rewrite 处理
  const filename = path.join(cwd, pathname.slice(1))
  const content = await fs.readFile(filename, 'utf-8')
  return sendJS(res, rewrite(content))
}
// 精简了部分代码,如果想看完整版建议去 github
// https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/moduleRewriter.ts
import { parse } from '@babel/parser'

export function rewrite(source: string, asSFCScript = false) {
  // 通过 babel 解析,找到 import from、export default 相关代码
  const ast = parse(source, {
    sourceType: 'module',
    plugins: [
      'bigInt',
      'optionalChaining',
      'nullishCoalescingOperator'
    ]
  }).program.body

  let s = source
  ast.forEach((node) => {
    if (node.type === 'ImportDeclaration') {
      if (/^[^\.\/]/.test(node.source.value)) {
        // 在 import 模块名称前加上 /__modules/
        // import { foo } from 'vue' --> import { foo } from '/__modules/vue'
        s = s.slice(0, node.source.start) 
          + `"/__modules/${node.source.value}"`
        	+ s.slice(node.source.end) 
      }
    } else if (asSFCScript && node.type === 'ExportDefaultDeclaration') {
      // export default { xxx } -->
      // let __script; export default (__script = { xxx })
      s = s.slice(0, node.source.start)
        + `let __script; export default (__script = ${
      		s.slice(node.source.start, node.declaration.start) 
   			})`
        + s.slice(node.source.end) 
      s.overwrite(
        node.start!,
        node.declaration.start!,
        `let __script; export default (__script = `
      )
      s.appendRight(node.end!, `)`)
    }
  })

  return s.toString()
}

запрос файла html/src/main.jsПосле лечения вите результаты следующие:

- import { createApp } from 'vue'
+ import { createApp } from '/__modules/vue'
import App from './App.vue'

createApp(App).mount('#app')

main.js

Работа с модулями npm

После того, как браузер проанализирует main.js, он прочитает модуль импорта и сделает запрос. запрошенный файл, если/__modules/В начале указано, что это модуль npm, который будет использовать viteresolveModuleспособ обработки.

// fetch /__modules/vue
if (pathname.startsWith('/__modules/')) {
  // 返回 import 的模块文件
  return resolveModule(pathname.replace('/__modules/', ''), cwd, res)
}
// 精简了部分代码,如果想看完整版建议去 github
// https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/moduleResolver.ts
import path from 'path'
import resolve from 'resolve-from'
import { sendJSStream } from './utils'
import { ServerResponse } from 'http'

export function resolveModule(id: string, cwd: string, res: ServerResponse) {
  let modulePath: string
  modulePath = resolve(cwd, 'node_modules', `${id}/package.json`)
  if (id === 'vue') {
    // 如果是 vue 模块,返回 vue.runtime.esm-browser.js
    modulePath = path.join(
      path.dirname(modulePath),
      'dist/vue.runtime.esm-browser.js'
    )
  } else {
    // 通过 package.json 文件,找到需要返回的 js 文件
    const pkg = require(modulePath)
    modulePath = path.join(path.dirname(modulePath), pkg.module || pkg.main)
  }

  sendJSStream(res, modulePath)
}

Обрабатывать vue-файлы

В дополнение к получению рамочного кода Main.js также импортирует Vue компонент. если.vueконец файла, vite пройдетvueMiddlewareспособ обработки.

if (pathname.endsWith('.vue')) {
  // 解析 vue 文件
  return vueMiddleware(cwd, req, res)
}
// 精简了部分代码,如果想看完整版建议去 github
// https://github.com/vitejs/vite/blob/a4f093a0c3/src/server/vueCompiler.ts

import url from 'url'
import path from 'path'
import { parse, SFCDescriptor } from '@vue/compiler-sfc'
import { rewrite } from './moduleRewriter'

export async function vueMiddleware(
  cwd: string, req, res
) {
  const { pathname, query } = url.parse(req.url, true)
  const filename = path.join(cwd, pathname.slice(1))
  const content = await fs.readFile(filename, 'utf-8')
  const { descriptor } = parse(content, { filename }) // vue 模板解析
  if (!query.type) {
    let code = ``
    if (descriptor.script) {
      code += rewrite(
        descriptor.script.content,
        true /* rewrite default export to `script` */
      )
    } else {
      code += `const __script = {}; export default __script`
    }
    if (descriptor.styles) {
      descriptor.styles.forEach((s, i) => {
        code += `\nimport ${JSON.stringify(
          pathname + `?type=style&index=${i}`
        )}`
      })
    }
    if (descriptor.template) {
      code += `\nimport { render as __render } from ${JSON.stringify(
        pathname + `?type=template`
      )}`
      code += `\n__script.render = __render`
    }
    sendJS(res, code)
    return
  }
  if (query.type === 'template') {
    // 返回模板
  }
  if (query.type === 'style') {
    // 返回样式
  }
}

После анализа,.vueКогда файл будет возвращен, он будет разделен на три части: сценарий, стиль и шаблон.

// 解析前
<template>
  <div>
    <img alt="Vue logo" src="./assets/logo.png" />
    <HelloWorld msg="Hello Vue 3.0 + Vite" />
  </div>
</template>

<script>
import HelloWorld from "./components/HelloWorld.vue";

export default {
  name: "App",
  components: {
    HelloWorld
  }
};
</script>
// 解析后
import HelloWorld from "/src/components/HelloWorld.vue";

let __script;
export default (__script = {
    name: "App",
    components: {
        HelloWorld
    }
})

import {render as __render} from "/src/App.vue?type=template"
__script.render = __render

Содержимое в шаблоне будет преобразовано в метод рендеринга с помощью vue. О том, как шаблоны vue компилируются в методы рендеринга, вы можете прочитать в другой моей статье:"Принцип компиляции шаблона Vue".

import {
  parse,
  SFCDescriptor,
  compileTemplate
} from '@vue/compiler-sfc'

export async function vueMiddleware(
  cwd: string, req, res
) {
  // ...
  if (query.type === 'template') {
    // 返回模板
    const { code } = compileTemplate({
      filename,
      source: template.content,
    })
    sendJS(res, code)
    return
  }
  if (query.type === 'style') {
    // 返回样式
  }
}

模板

И стиль шаблона

import {
  parse,
  SFCDescriptor,
  compileStyle,
  compileTemplate
} from '@vue/compiler-sfc'

export async function vueMiddleware(
  cwd: string, req, res
) {
  // ...
  if (query.type === 'style') {
    // 返回样式
    const index = Number(query.index)
    const style = descriptor.styles[index]
    const { code } = compileStyle({
      filename,
      source: style.content
    })
    sendJS(
      res,
      `
  const id = "vue-style-${index}"
  let style = document.getElementById(id)
  if (!style) {
    style = document.createElement('style')
    style.id = id
    document.head.appendChild(style)
  }
  style.textContent = ${JSON.stringify(code)}
    `.trim()
    )
  }
}

Обработка стиля не сложная, получите содержимое тега стиля, а затем js добавит стиль в тег заголовка, создав тег стиля.

резюме

Вот простой анализ того, как vite перехватывает запрос и потом возвращает нужный файл, опуская код горячего обновления. В дополнение к запуску сервисов для разработки, vite v1, который будет выпущен, также поддерживает накопительную упаковку и возможность вывода кода рабочей среды.

Суммировать

Когда vite был только что выпущен, его можно было использовать только как вспомогательный инструмент для vue, а теперь он поддерживает ряд возможностей, таких как JSX, TypeScript, Web Assembly, PostCSS и т. д. Давайте просто спокойно дождемся выхода официальных версий vue3 и vite.Сможем ли мы произвести революцию в веб-пакете или нет, зависит от воли Божией.

Кстати, vite, как и vue, происходит от французского, а по-китайски означает «быстрый».

vite翻译