Оригинальная статья впервые опубликована в моемблог, Добро пожаловать в гости.
Прошло много времени с тех пор, как я вел блог. В этой статье не говорится о Vue3.0, я полагаю, что об этом уже написано много статей. А некоторое время назад особенно с открытым исходным кодомViteЭто то, что мне больше нравится, его общая идея очень хороша, а стоимость изучения раннего исходного кода относительно невелика, поэтому я воспользовался отпуском, чтобы поучиться.
Эта статья была написана в версии Vite-0.9.1.
Что такое Вите
Заимствуя оригинальные слова автора:
Vite, сервер разработки, основанный на импорте ES из браузера. Используйте браузер для анализа импорта, компиляции и возврата по запросу на стороне сервера, полностью пропуская концепцию упаковки, и сервер можно использовать по мере необходимости. При этом он не только поддерживает файлы Vue, но и обрабатывает горячие обновления, причем скорость горячих обновлений не будет замедляться по мере увеличения количества модулей. Для производственных сред тот же код можно упаковать с помощью свертки. Несмотря на то, что пока это относительно грубо, я думаю, что у этого направления есть потенциал, и если оно будет сделано хорошо, оно может полностью решить проблему полдня горячих обновлений, таких как изменение строки кода.
Обратите внимание на две вещи:
- Во-первых, основным соответствующим сценарием Vite является режим разработки, Принцип состоит в том, чтобы перехватить запрос на импорт ES, выданный браузером, и соответствующим образом обработать его. (Производственный режим упакован с накопительным пакетом)
- Во-первых, Vite не нужно упаковывать в режиме разработки, ему нужно только скомпилировать файл, соответствующий HTTP-запросу, отправленному браузером, поэтому скорость горячего обновления очень высока.
Поэтому для достижения вышеперечисленных целей необходимо требовать в проекте только нативные ES-импорты, в случае использования require он будет невалидным, поэтому использовать его для полной замены Webpack нереально. Как упоминалось выше, упаковка в производственном режиме не предоставляется самим Vite, поэтому по-прежнему можно использовать Webpack для упаковки в производственном режиме. С этой точки зрения Vite может быть скорее заменой webpack-dev-server.
модуль модулей
Реализация Vite неотделима от нативной поддержки современных браузеров.Функция модуля. следующее:
<script type="module">
import { a } from './a.js'
</script>
при объявленииscript
Тип этикеткиmodule
, браузер будетimport
цитирование инициированоHTTP
Запрос на получение содержимого модуля. Например, выше, браузер инициируетHOST/a.js
HTTP-запрос выполняется после получения содержимого.
Vite перехватывает эти запросы и выполняет соответствующую обработку на сервере (например, разбивает файл Vue наtemplate
,style
,script
три части) перед возвратом в браузер.
Поскольку браузер будет инициировать HTTP-запросы только к используемым модулям, Vite не нужно сначала упаковывать все файлы в проекте, а затем возвращать их, а только компилировать модули, которые браузер инициирует HTTP-запросы. Это что-то вроде загрузки по требованию?
Разница между компиляцией и упаковкой
Увидев это, у некоторых друзей могут возникнуть вопросы, а в чем разница между компиляцией и упаковкой? Почему Vite утверждает, что «скорость горячего обновления не снижается по мере увеличения количества модулей»?
В качестве простого примера есть три файлаa.js
,b.js
,c.js
// a.js
const a = () => { ... }
export { a }
// b.js
const b = () => { ... }
export { b }
// c.js
import { a } from './a'
import { b } from './b'
const c = () => {
return a() + b()
}
export { c }
Если в качестве записи используется файл c, упаковка будет выглядеть следующим образом (результат упрощается): (Предположим, что имя файла пакета равноbundle.js
)
// bundle.js
const a = () => { ... }
const b = () => { ... }
const c = () => {
return a() + b()
}
export { c }
Стоит отметить, что упаковка также требует этапа компиляции.
Принцип горячего обновления Webpack заключается в том, что при возникновении зависимости (например,a.js
) изменить, поместите эту зависимость вmodule
обновить и вставить новыйmodule
Отправьте его в браузер для повторного выполнения. Поскольку мы попали только в одинbundle.js
, так что если будет горячее обновление, то тоже перепечатает этоbundle.js
. Представьте, что если зависимостей становится все больше и больше, даже если модифицируется только один файл, теоретически скорость горячего обновления будет становиться все медленнее и медленнее.
А что, если это похоже на Vite, который только компилируется, а не упаковывается?
Если вы просто скомпилируете, окончательный вывод по-прежнемуa.js
,b.js
,c.js
Три файла, требующие только времени для компиляции. Так как вход естьc.js
, браузер разрешаетimport { a } from './a'
, будет сделан HTTP-запросa.js
(То же самое и для b), даже если вам не нужно его паковать, вы можете загрузить нужный код, тем самым сэкономив время на слияние кода.
Во время горячего обновления, еслиa
изменилось, просто нужно обновитьa
и использоватьa
изc
. так какb
Ничего не изменилось, поэтому Vite не нужно перекомпилировать.b
, вы можете получить скомпилированный результат прямо из кеша. Таким образом, измените файлa
, будет только перекомпилировать этот файлa
и браузер в данный момент использует этот файлa
файлы, а остальные файлы перекомпилировать не нужно. Так что теоретически скорость горячего обновления не будет замедляться по мере увеличения файла.
Конечно, есть ли какие-либо недостатки в этом? Да, если браузер запрашивает слишком много модулей во время инициализации, это также приведет к проблемам с производительностью инициализации. Однако, если вы можете столкнуться с проблемой медленной инициализации, я считаю, что скорость горячего обновления многое компенсирует. Конечно, я верю, что You Da решит эту проблему в будущем.
Реализация работающего веб-приложения Vite
Упомянутое выше предзнаменование может быть недостаточно интуитивным, поэтому мы можем сначала запустить проект Vite, чтобы увидеть его.
По инструкции на официальном сайте можно ввести следующую команду (<project-name>
Вы можете использовать имя каталога, которое вы хотите)
$ npx create-vite-app <project-name>
$ cd <project-name>
$ npm install
$ npm run dev
Если все хорошо, вы будете вlocalhost:3000
(Порт с сервера Vite) См. этот интерфейс:
И получить следующую структуру кода:
.
├── App.vue // 页面的主要逻辑
├── index.html // 默认打开的页面以及 Vue 组件挂载
├── node_modules
└── package.json
Перехватывать HTTP-запросы
Далее поговорим о ядре реализации Vite — перехвате запросов браузера на модули и возврате обработанных результатов.
Мы знаем это, потому чтоlocalhost:3000
Откройте веб-страницу, поэтому первый запрос, инициированный браузером, естественно, является запросомlocalhost:3000/
, после того как этот запрос будет отправлен на серверную часть Vite, он будет обработан сервером статических ресурсов, а затем будет запрошен/index.html
, Vite начинает перехватывать и обрабатывать этот запрос.
Во-первых,index.html
Исходный код выглядит следующим образом:
<div id="app"></div>
<script type="module">
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
</script>
Но в браузере это выглядит так:
Заметили разницу? Да,import { createApp } from 'vue'
заменяетсяimport { createApp } from '/@modules/vue
.
Здесь я должен сказать, что браузер правimport
Некоторые ограничения, когда модуль инициирует запрос, обычно пишем код, если не модуль ссылается на относительный путь, а на ссылкуnode_modules
модули, непосредственноimport xxx from 'xxx'
, и такие инструменты, как Webpack, помогают нам найти конкретный путь к этому модулю. Но браузер не знает, что ваш проектnode_modules
, он может найти модули только по относительному пути.
Поэтому в перехваченном запросе Vite напрямую ссылается наnode_modules
Модули были заменены путем, заменены на/@modules/
и вернуться. Затем, после того, как браузер получит его, он инициирует ответ на/@modules/xxx
Затем запрос снова перехватывается Vite, и Vite получает внутренний доступ к реальному модулю и возвращает полученный контент в браузер после повторного выполнения той же обработки.
импорт заменяет
Обычная замена импорта JS
Упомянутая выше замена происходит отsrc/node/serverPluginModuleRewrite.ts
:
// 只取关键代码:
// Vite 使用 Koa 作为内置的服务器
// 如果请求的路径是 /index.html
if (ctx.path === '/index.html') {
// ...
const html = await readBody(ctx.body)
ctx.body = html.replace(
/(<script\b[^>]*>)([\s\S]*?)<\/script>/gm, // 正则匹配
(_, openTag, script) => {
// also inject __DEV__ flag
const devFlag = hasInjectedDevFlag ? `` : devInjectionCode
hasInjectedDevFlag = true
// 替换 html 的 import 路径
return `${devFlag}${openTag}${rewriteImports(
script,
'/index.html',
resolver
)}</script>`
}
)
// ...
}
если не вscript
Пишите прямо внутри этикеткиimport
, но используйтеsrc
Форма цитируется следующим образом:
<script type="module" src="/main.js"></script>
Затем браузер инициируетmain.js
Обработка по запросу:
// 只取关键代码:
if (
ctx.response.is('js') &&
// ...
) {
// ...
const content = await readBody(ctx.body)
await initLexer
// 重写 js 文件里的 import
ctx.body = rewriteImports(
content,
ctx.url.replace(/(&|\?)t=\d+/, ''),
resolver,
ctx.query.t
)
// 写入缓存,之后可以从缓存中直接读取
rewriteCache.set(content, ctx.body)
}
логика заменыrewriteImports
Не расширяется, используетсяes-module-lexer
грамматический анализ для полученияimports
массив, а затем выполните замену.
Замена файлов *.vue
еслиimport
да.vue
файл, который будет производить дальнейшие замены:
оригинальныйApp.vue
Файл выглядит так:
<template>
<h1>Hello Vite + Vue 3!</h1>
<p>Edit ./App.vue to test hot module replacement (HMR).</p>
<p>
<span>Count is: {{ count }}</span>
<button @click="count++">increment</button>
</p>
</template>
<script>
export default {
data: () => ({ count: 0 }),
}
</script>
<style scoped>
h1 {
color: #4fc08d;
}
h1, p {
font-family: Arial, Helvetica, sans-serif;
}
</style>
После замены выглядит так:
// localhost:3000/App.vue
import { updateStyle } from "/@hmr"
// 抽出 script 逻辑
const __script = {
data: () => ({ count: 0 }),
}
// 将 style 拆分成 /App.vue?type=style 请求,由浏览器继续发起请求获取样式
updateStyle("c44b8200-0", "/App.vue?type=style&index=0&t=1588490870523")
__script.__scopeId = "data-v-c44b8200" // 样式的 scopeId
// 将 template 拆分成 /App.vue?type=template 请求,由浏览器继续发起请求获取 render function
import { render as __render } from "/App.vue?type=template&t=1588490870523&t=1588490870523"
__script.render = __render // render 方法挂载,用于 createApp 时渲染
__script.__hmrId = "/App.vue" // 记录 HMR 的 id,用于热更新
__script.__file = "/XXX/web/vite-test/App.vue" // 记录文件的原始的路径,后续热更新能用到
export default __script
Таким образом, исходный.vue
Файл разбит на три запроса (соответствующихscript
,style
иtemplate
), браузер сначала получитscript
логическийApp.vue
, который затем анализируется наtemplate
иstyle
После установки пути снова будет инициирован HTTP-запрос для запроса соответствующего ресурса, В это время Vite перехватит его, снова обработает и вернет соответствующий контент.
следующее:
Я должен сказать, что эта идея очень умна.
Разделение на этом шаге происходит отsrc/node/serverPluginVue.ts
, основная логика заключается в выполнении различной обработки в соответствии с параметром запроса URL-адреса (упрощенный анализ выглядит следующим образом):
// 如果没有 query 的 type,比如直接请求的 /App.vue
if (!query.type) {
ctx.type = 'js'
ctx.body = compileSFCMain(descriptor, filePath, publicPath) // 编译 App.vue,编译成上面说的带有 script 内容,以及 template 和 style 链接的形式。
return etagCacheCheck(ctx) // ETAG 缓存检测相关逻辑
}
// 如果 query 的 type 是 template,比如 /App.vue?type=template&xxx
if (query.type === 'template') {
ctx.type = 'js'
ctx.body = compileSFCTemplate( // 编译 template 生成 render function
// ...
)
return etagCacheCheck(ctx)
}
// 如果 query 的 type 是 style,比如 /App.vue?type=style&xxx
if (query.type === 'style') {
const index = Number(query.index)
const styleBlock = descriptor.styles[index]
const result = await compileSFCStyle( // 编译 style
// ...
)
if (query.module != null) { // 如果是 css module
ctx.type = 'js'
ctx.body = `export default ${JSON.stringify(result.modules)}`
} else { // 正常 css
ctx.type = 'css'
ctx.body = result.code
}
}
@modules/* разрешение пути
Вышеизложенное включает только логику замены, а логика синтаксического анализа исходит изsrc/node/serverPluginModuleResolve.ts
. Этот шаг относительно прост, основная логика состоит в том, чтобы перейти кnode_modules
Если есть какой-либо соответствующий модуль, верните его, если нет, сообщите 404: (Опущено много логики, например, правильныйweb_modules
обработка, обработка кэша и т.д.)
// ...
try {
const file = resolve(root, id) // id 是模块的名字,比如 axios
return serve(id, file, 'node_modules') // 从 node_modules 中找到真正的模块内容并返回
} catch (e) {
console.error(
chalk.red(`[vite] Error while resolving node_modules with id "${id}":`)
)
console.error(e)
ctx.status = 404 // 如果没找到就 404
}
Внедрение горячего обновления Vite
Выше было сказано, как Vite запускает веб-приложение, в том числе как перехватывать запросы, заменять контент и возвращать обработанные результаты. Далее поговорим о реализации горячего обновления Vite, тоже очень умной.
Мы знаем, что если должно быть реализовано горячее обновление, то браузер и сервер должны установить какой-то механизм связи, чтобы браузер мог получать уведомление о горячем обновлении. Вите закончилсяWebSocket
для достижения горячей связи обновления.
клиент
Код клиента находится вsrc/client/client.ts
, в основном для созданияWebSocket
Клиентская сторона прослушивает push-сообщения HMR со стороны сервера.
Клиент Vite WS в настоящее время прослушивает следующие типы сообщений:
-
connected
: соединение WebSocket установлено успешно -
vue-reload
: перезагрузка компонента Vue (при изменении содержимого в скрипте) -
vue-rerender
: повторный рендеринг компонента Vue (при изменении содержимого в шаблоне) -
style-update
: обновление стиля -
style-remove
: стиль удален -
js-update
: обновление js-файла -
full-reload
: резервный механизм, обновление веб-страницы
Некоторые обновления для самого компонента Vue можно вызывать напрямуюHMRRuntime
Предлагаемый метод очень удобен. Остальная логика обновления в основном используетсяtimestamp
Обновите кеш и повторно выполните метод для достижения цели обновления.
Основная логика выглядит следующим образом, я чувствую себя очень ясно:
import { HMRRuntime } from 'vue' // 来自 Vue3.0 的 HMRRuntime
console.log('[vite] connecting...')
declare var __VUE_HMR_RUNTIME__: HMRRuntime
const socket = new WebSocket(`ws://${location.host}`)
// Listen for messages
socket.addEventListener('message', ({ data }) => {
const { type, path, id, index, timestamp, customData } = JSON.parse(data)
switch (type) {
case 'connected':
console.log(`[vite] connected.`)
break
case 'vue-reload':
import(`${path}?t=${timestamp}`).then((m) => {
__VUE_HMR_RUNTIME__.reload(path, m.default)
console.log(`[vite] ${path} reloaded.`) // 调用 HMRRUNTIME 的方法更新
})
break
case 'vue-rerender':
import(`${path}?type=template&t=${timestamp}`).then((m) => {
__VUE_HMR_RUNTIME__.rerender(path, m.render)
console.log(`[vite] ${path} template updated.`) // 调用 HMRRUNTIME 的方法更新
})
break
case 'style-update':
updateStyle(id, `${path}?type=style&index=${index}&t=${timestamp}`) // 重新加载 style 的 URL
console.log(
`[vite] ${path} style${index > 0 ? `#${index}` : ``} updated.`
)
break
case 'style-remove':
const link = document.getElementById(`vite-css-${id}`)
if (link) {
document.head.removeChild(link) // 删除 style
}
break
case 'js-update':
const update = jsUpdateMap.get(path)
if (update) {
update(timestamp) // 用新的时间戳加载并执行 js,达到更新的目的
console.log(`[vite]: js module reloaded: `, path)
} else {
console.error(
`[vite] got js update notification but no client callback was registered. Something is wrong.`
)
}
break
case 'custom':
const cbs = customUpdateMap.get(id)
if (cbs) {
cbs.forEach((cb) => cb(customData))
}
break
case 'full-reload':
location.reload()
}
})
Сервер
Реализация сервера находится наsrc/node/serverPluginHmr.ts
. Суть в том, чтобы отслеживать изменения в файлах проекта, а затем по разным типам файлов (на данный момент толькоvue
иjs
) для выполнения различной обработки:
watcher.on('change', async (file) => {
const timestamp = Date.now() // 更新时间戳
if (file.endsWith('.vue')) {
handleVueReload(file, timestamp)
} else if (file.endsWith('.js')) {
handleJSReload(file, timestamp)
}
})
заVue
Что касается горячего обновления файлов, то это, в основном, перекомпиляцияVue
файл, проверитьtemplate
,script
,style
Если есть какие-либо изменения, соответствующий запрос на горячее обновление будет инициирован через сервер WS.
Простой анализ исходного кода выглядит следующим образом:
async function handleVueReload(
file: string,
timestamp: number = Date.now(),
content?: string
) {
const publicPath = resolver.fileToRequest(file) // 获取文件的路径
const cacheEntry = vueCache.get(file) // 获取缓存里的内容
debugHmr(`busting Vue cache for ${file}`)
vueCache.del(file) // 发生变动了因此之前的缓存可以删除
const descriptor = await parseSFC(root, file, content) // 编译 Vue 文件
const prevDescriptor = cacheEntry && cacheEntry.descriptor // 获取前一次的缓存
if (!prevDescriptor) {
// 这个文件之前从未被访问过(本次是第一次访问),也就没必要热更新
return
}
// 设置两个标志位,用于判断是需要 reload 还是 rerender
let needReload = false
let needRerender = false
// 如果 script 部分不同则需要 reload
if (!isEqual(descriptor.script, prevDescriptor.script)) {
needReload = true
}
// 如果 template 部分不同则需要 rerender
if (!isEqual(descriptor.template, prevDescriptor.template)) {
needRerender = true
}
const styleId = hash_sum(publicPath)
// 获取之前的 style 以及下一次(或者说热更新)的 style
const prevStyles = prevDescriptor.styles || []
const nextStyles = descriptor.styles || []
// 如果不需要 reload,则查看是否需要更新 style
if (!needReload) {
nextStyles.forEach((_, i) => {
if (!prevStyles[i] || !isEqual(prevStyles[i], nextStyles[i])) {
send({
type: 'style-update',
path: publicPath,
index: i,
id: `${styleId}-${i}`,
timestamp
})
}
})
}
// 如果 style 标签及内容删掉了,则需要发送 `style-remove` 的通知
prevStyles.slice(nextStyles.length).forEach((_, i) => {
send({
type: 'style-remove',
path: publicPath,
id: `${styleId}-${i + nextStyles.length}`,
timestamp
})
})
// 如果需要 reload 发送 `vue-reload` 通知
if (needReload) {
send({
type: 'vue-reload',
path: publicPath,
timestamp
})
} else if (needRerender) {
// 否则发送 `vue-rerender` 通知
send({
type: 'vue-rerender',
path: publicPath,
timestamp
})
}
}
Для горячих обновленийjs
Для файлов он будет рекурсивно находить ссылки на этот файлimporter
. напримерVue
Документ ссылается на этоjs
, будет найдено. Если в конце концов обнаружится, что реферер не может быть найден, он вернетhasDeadEnd: true
.
const vueImporters = new Set<string>() // 查找并存放需要热更新的 Vue 文件
const jsHotImporters = new Set<string>() // 查找并存放需要热更新的 js 文件
const hasDeadEnd = walkImportChain(
publicPath,
importers,
vueImporters,
jsHotImporters
)
еслиhasDeadEnd
заtrue
, затем отправить напрямуюfull-reload
. еслиvueImporters
илиjsHotImporters
Если в файле будет обнаружен файл, требующий горячего обновления, будет выдано уведомление о горячем обновлении:
if (hasDeadEnd) {
send({
type: 'full-reload',
timestamp
})
} else {
vueImporters.forEach((vueImporter) => {
send({
type: 'vue-reload',
path: vueImporter,
timestamp
})
})
jsHotImporters.forEach((jsImporter) => {
send({
type: 'js-update',
path: jsImporter,
timestamp
})
})
}
Внедрение клиентской логики
Написав здесь, есть еще одна проблема, которую мы не ввели в наш код.HRM
изclient
код, как Vite ставитclient
Как насчет внедрения кода?
Возвращаясь к картинке выше, Вите переписываетApp.vue
Содержимое файла и при возврате:
Обратите внимание на первое предложение области кода на этой картинке.import { updateStyle } from '/@hmr'
, а в списке запросов слева тоже есть пара@hmr
запрос файла. Что это за просьба?
Можно обнаружить, что этот запрос является клиентской логикой, упомянутой выше.client.ts
Содержание.
существуетsrc/node/serverPluginHmr.ts
, Есть@hmr
Обработка парсинга файлов:
export const hmrClientFilePath = path.resolve(__dirname, './client.js')
export const hmrClientId = '@hmr'
export const hmrClientPublicPath = `/${hmrClientId}`
app.use(async (ctx, next) => {
if (ctx.path !== hmrClientPublicPath) { // 请求路径如果不是 @hmr 就跳过
return next()
}
debugHmr('serving hmr client')
ctx.type = 'js'
await cachedRead(ctx, hmrClientFilePath) // 返回 client.js 的内容
})
До сих пор был проанализирован общий процесс горячего обновления.
резюме
В последнее время этот проект итерируется с невероятной скоростью, поэтому, если я скоро вернусь к этой статье, код и реализация могут устареть. Тем не менее, общая идея Vite очень хороша, когда на ранней стадии не так много исходного кода, это хороший выигрыш, чтобы иметь возможность узнать что-то более близкое к первоначальной идее автора. Я надеюсь, что эта статья поможет вам в изучении Vite, и вы можете указать на любые ошибки.