предисловие
В предыдущей статье говорилосьVite: как разрабатывать приложения без веб-пакета, согласно договору, следующее, что нужно сказать热更新Эта проблема,Viteпройти мимо себяWebSocketРеализуйте связь между браузером и сервером, чтобы достичь热更新цель.
Но сначала нам нужно знать热更新Что это значит, в чем разница между этим и прямым обновлением страницы и как это реализовано по-другому? Следующее содержание в основном для справкиИнтенсивная лекция по фронтенд-инжинирингу
Live Reload
Обычно мы используем базовый метод для локальной отладки приложения.webpack-dev-server, а его основная функция — запуск локальной службы.Основная конфигурация кода показана на следующем рисунке:
// webpack.config.js
module.exports = {
//...
devServer: {
contentBase: './dist', //为./dist目录中的静态页面文件提供本地服务渲染
open: true //启动服务后自动打开浏览器网页
}
};
// package.json
"scripts": {
"dev:reload": "webpack-dev-server"
}
воплощать в жизньdev:reloadПосле этого запускается сервер, и браузер автоматически обновляется для перезагрузки при изменении кода.
И его принцип на самом деле черезWebSocketУстановите постоянную связь между веб-страницами и локальными службами. При изменении кода на страницу будет отправлено сообщение, и страница обновит интерфейс для обновления после получения сообщения.
В этом режиме есть определенные дефекты.Например, когда мы отлаживаем во всплывающем окне, когда я автоматически обновляю код каждый раз, когда я модифицирую код, всплывающее окно сразу исчезает. Поскольку браузер обновляется напрямую, состояние теряется, что доставляет небольшие неудобства при отладке. Особенно в случае относительно сложных операций, требующих отладки, дело обстоит еще более убого.
Hot Module Replacement
Hot Module ReplacementУказывает на то, что модуль подвергается горячей замене, о чем все говорили.热更新, чтобы решить проблему потери состояния, вызванную обновлением страницы, упомянутой выше, webpack предлагает концепцию горячей замены модуля. существуетwebpack-dev-server, есть конфигурацияhot, если установленоtrue, он будет автоматически добавлен напрямуюwebpack.HotModuleReplacementPlugin, давайте создадим простой пример, чтобы увидеть конкретный вариант горячего обновления:
// src/index.js
import './index.css'
// src/index.css
div {
background: red;
}
// dist/index.html
...
<script src="./main.js"></script>
...
// webpack.config.js
const path = require("path");
module.exports = {
entry: "./src/index.js",
devServer: {
contentBase: "./dist",
open: true,
hot: true // 开启热更新
},
output: {
filename: "main.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
],
},
};
// package.json
"scripts": {
"dev:reload": "webpack-dev-server"
}
Вот новая конфигурация модуля, которая использует загрузчик стилей и загрузчик css для анализа импортированных файлов CSS. Загрузчик css отвечает за преобразование импортированного файла CSS в модуль для последующей обработки загрузчиком, а загрузчик стилей отвечает за добавление содержимого модуля CSS в тег стиля страницы во время выполнения. После запуска вы можете увидеть:
В это время измените код css, а затем вернитесь в браузер, вы обнаружите, что в центре сетевой панели были созданы два новых запроса:
Однако, если вы измените содержимое файла JS, обновление все равно будет, что касается причины, давайте сначала посмотрим на процесс.style-loaderОбработанный файл примет следующий код:
var api = __webpack_require__(/*! ../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js */ "./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js");
var content = __webpack_require__(/*! !../node_modules/css-loader/dist/cjs.js!./index.css */ "./node_modules/css-loader/dist/cjs.js!./src/index.css");
content = content.__esModule ? content.default : content;
if (typeof content === 'string') {
content = [[module.i, content, '']];
}
var options = {};
options.insert = "head";
options.singleton = false;
var update = api(content, options);
var oldLocals = content.locals;
module.hot.accept(
// 依赖模块
"./node_modules/css-loader/dist/cjs.js!./src/index.css",
// 回调方法
function() {
...
// 修改 style 标签里的内容
update(content);
}
)
}
module.hot.dispose(function() {
// 移除 style 标签
update();
});
}
module.exports = content.locals || {};
Плагин горячей замены модуля
Приведенный выше модуль module.hot на самом деле является базовым плагином из веб-пакета.HotModuleReplacementPlugin, плагин используется как базовый плагин для функции горячей замены, а его методы API экспортируются в свойства module.hot .
-
hot.accept: Когда зависимый модуль изменяется, выполняется вставленный метод обратного вызова. Вышеупомянутое предназначено для обновления содержимого тега стиля. -
hot.dispose: при удалении модуля контекста кода будет выполнен его метод обратного вызова. Вышеупомянутое, чтобы удалить тег стиля
Поэтому, поскольку код для этого плагина не добавлен в js, а парсинг кода не настраивает дополнительный загрузчик, который может вызывать API горячей замены для конкретных кодов, интерфейс будет обновляться сразу после модификации js , Добавьте код, подобный следующему:
./text.js
export const text = 'Hello World'
./index.js
import {text} from './text.js'
const div = document.createElement('div')
document.body.appendChild(div)
function render() {
div.innerHTML = text;
}
render()
if (module.hot) {
// 再 text.js 代码更新后不会重新刷新页面
module.hot.accept('./text.js', function() {
render()
})
}
Реализация Vite Hot обновление
Благодаря вышеизложенному пониманию вы, вероятно, знаете логику и принцип горячего обновления.ViteСпособ достижения горячего обновления также аналогичен, в основном путем созданияWebSocketУстановите браузер для установления связи с сервером и отправки сообщений клиенту, отслеживая изменения файлов, и клиент может обновлять разные файлы, соответствующие различным операциям.
См. следующие частиАнализ принципа Vite
Сервер
Перейти к кодуserver/serverPluginHmr.tsтак же какserver/serverPluginVue.ts
watcher.on('change', (file) => {
if (!(file.endsWith('.vue') || isCSSRequest(file))) {
handleJSReload(file)
}
})
watcher.on('change', (file) => {
if (file.endsWith('.vue')) {
handleVueReload(file)
}
})
handleVueReload
async function handleVueReload(
file: string,
timestamp: number = Date.now(),
content?: string
) {
...
const cacheEntry = vueCache.get(file) // 获取缓存里的内容
// @vue/compiler-sfc 编译 Vue 文件
const descriptor = await parseSFC(root, file, content)
const prevDescriptor = cacheEntry && cacheEntry.descriptor // 获取前一次的缓存
if (!prevDescriptor) {
// 这个文件之前从未被访问过(本次是第一次访问),也就没必要热更新
return
}
// 判断是否需要 rerender
let needRerender = false
// 向客户端发出 reload 消息的方法
const sendReload = () => {
send({
type: 'vue-reload',
path: publicPath,
changeSrcPath: publicPath,
timestamp
})
}
// script 不同则直接 reload
if (
!isEqualBlock(descriptor.script, prevDescriptor.script) ||
!isEqualBlock(descriptor.scriptSetup, prevDescriptor.scriptSetup)
) {
return sendReload()
}
// 如果 template 部分不同则需要 rerender
if (!isEqual(descriptor.template, prevDescriptor.template)) {
needRerender = true
}
// 获取之前的 style 以及下一次(或者说热更新)的 style
const prevStyles = prevDescriptor.styles || []
const nextStyles = descriptor.styles || []
// css module | vars injection | scopes 改变则直接 reload
if (
prevStyles.some((s) => s.module != null) ||
nextStyles.some((s) => s.module != null)
) {
return sendReload()
}
if (
prevStyles.some((s, i) => {
const next = nextStyles[i]
if (s.attrs.vars && (!next || next.attrs.vars !== s.attrs.vars)) {
return true
}
})
) {
return sendReload()
}
if (prevStyles.some((s) => s.scoped) !== nextStyles.some((s) => s.scoped)) {
return sendReload()
}
// 如果以上情况都不是,则 style 改变时 rerender
nextStyles.forEach((_, i) => {
if (!prevStyles[i] || !isEqualBlock(prevStyles[i], nextStyles[i])) {
didUpdateStyle = true
const path = `${publicPath}?type=style&index=${i}`
send({
type: 'style-update',
path,
changeSrcPath: path,
timestamp
})
}
})
// 如果 style 标签及内容删掉了,则需要发送 `style-remove` 的通知
prevStyles.slice(nextStyles.length).forEach((_, i) => {
didUpdateStyle = true
send({
type: 'style-remove',
path: publicPath,
id: `${styleId}-${i + nextStyles.length}`
})
})
// 如果需要 reredner 则发送 vue-rerender
if (needRerender) {
send({
type: 'vue-rerender',
path: publicPath,
changeSrcPath: publicPath,
timestamp
})
}
}
handleJSReload
Что касается перегрузки файла js, то он вызывается напрямую рекурсивноwalkImportChainчтобы узнать, кто его цитирует (импортер), и если вы не можете найти реферера, тоhasDeadEndвернутьtrue;
const hmrBoundaries = new Set<string>() // 引用者如果是 vue 文件则存放在这
const dirtyFiles = new Set<string>() // 引用者如果是 js 文件则存放在这
const hasDeadEnd = walkImportChain(
publicPath,
importers || new Set(),
hmrBoundaries,
dirtyFiles
)
еслиhasDeadEndдляtrue, затем отправить напрямуюfull-reload. При обнаружении файла, требующего горячего обновления, выдается уведомление о горячем обновлении:
if (hasDeadEnd) {
send({
type: 'full-reload',
path: publicPath
})
} else {
const boundaries = [...hmrBoundaries]
const file =
boundaries.length === 1 ? boundaries[0] : `${boundaries.length} files`
send({
type: 'multi',
updates: boundaries.map((boundary) => {
return {
type: boundary.endsWith('vue') ? 'vue-reload' : 'js-update',
path: boundary,
changeSrcPath: publicPath,
timestamp
}
})
})
}
клиент
Как упоминалось выше, сервер опубликует сообщение клиенту после отслеживания изменения, и код перейдет кsrc/client/client.ts, здесь в основном для созданияWebSocketКлиент, а затем прослушайте сообщение, отправленное сервером, чтобы обновить операцию
Основные сильные новости и соответствующие措施в основном включает:
-
connected: соединение WebSocket установлено успешно -
vue-reload: перезагрузка компонента Vue (при изменении содержимого в скрипте) -
vue-rerender: повторный рендеринг компонента Vue (при изменении содержимого в шаблоне) -
style-update: обновление стиля -
style-remove: стиль удален -
js-update: обновление js-файла -
full-reload: резервный механизм, обновление веб-страницы
Обновления здесь в основном черезtimestampОбновите и повторно запросите, чтобы получить обновленный контент, а затем передайте файл vueHMRRuntimeобновление реализации
import { HMRRuntime } from 'vue'
declare var __VUE_HMR_RUNTIME__: HMRRuntime
const socket = new WebSocket(socketUrl, 'vite-hmr')
socket.addEventListener('message', async ({ data }) => {
const payload = JSON.parse(data) as HMRPayload | MultiUpdatePayload
if (payload.type === 'multi') {
payload.updates.forEach(handleMessage)
} else {
handleMessage(payload)
}
})
async function handleMessage(payload: HMRPayload) {
const { path, changeSrcPath, timestamp } = payload as UpdatePayload
switch (payload.type) {
case 'connected':
console.log(`[vite] connected.`)
break
case 'vue-reload':
// 加入到更新队列,之后一起更新
queueUpdate(
// 重新请求
import(`${path}?t=${timestamp}`)
.catch((err) => warnFailedFetch(err, path))
.then((m) => () => {
// 调用 HMRRUNTIME 的方法更新
__VUE_HMR_RUNTIME__.reload(path, m.default)
console.log(`[vite] ${path} reloaded.`)
})
)
break
case 'vue-rerender':
const templatePath = `${path}?type=template`
import(`${templatePath}&t=${timestamp}`).then((m) => {
__VUE_HMR_RUNTIME__.rerender(path, m.render)
console.log(`[vite] ${path} template updated.`)
})
break
case 'style-update':
// check if this is referenced in html via <link>
const el = document.querySelector(`link[href*='${path}']`)
if (el) {
el.setAttribute(
'href',
`${path}${path.includes('?') ? '&' : '?'}t=${timestamp}`
)
break
}
// imported CSS
const importQuery = path.includes('?') ? '&import' : '?import'
await import(`${path}${importQuery}&t=${timestamp}`)
console.log(`[vite] ${path} updated.`)
break
case 'style-remove':
removeStyle(payload.id)
break
case 'js-update':
queueUpdate(updateModule(path, changeSrcPath, timestamp))
break
case 'full-reload':
// 直接刷新页面
if (path.endsWith('.html')) {
// if html file is edited, only reload the page if the browser is
// currently on that page.
const pagePath = location.pathname
if (
pagePath === path ||
(pagePath.endsWith('/') && pagePath + 'index.html' === path)
) {
location.reload()
}
return
} else {
location.reload()
}
}
}
Вот что нужно сказать, когда мы просимtype=cssилиcssфайла, вверху будет специальный запрос на импорт:
import { updateStyle } from "/vite/client" // <--- 这里
const css = "\nimg {\n height: 100px;\n}\n"
// updateStyle 方法用来更新 css 内容,所以对于 css 文件只需要重新请求就可以更新
updateStyle("7ac74a55-0", css)
export default css
а такжеurl:/vite/clientсуществуетserverPluginClientКонфигурация статических файлов выполняется, при возвратеclient/client.jsсодержания, что также обычно называют客户端代码注入
export const clientFilePath = path.resolve(__dirname, '../../client/client.js')
export const clientPlugin: ServerPlugin = ({ app, config }) => {
const clientCode = fs
.readFileSync(clientFilePath, 'utf-8')
...
app.use(async (ctx, next) => {
if (ctx.path === clientPublicPath) {
ctx.type = 'js'
ctx.status = 200
// 返回 client.js 下的内容
ctx.body = clientCode.replace(`__PORT__`, ctx.port.toString())
}
...
})
}
рекламировать
Из-за ограничения на количество слов нельзя использовать специальные темы. Пожалуйста, обратите внимание на официальный аккаунт для получения оригинального текста.学前端来唠嗑, чтобы поделиться взглядами на жизнь поколения после 95-х годов