前言
在软件开发过程中,频繁的版本迭代和更新是常态。无论是在测试还是生产环境中,新代码的发布若未被用户及时察觉,可能会引发一系列问题,这些问题包括但不限于:
- 功能不一致性:用户可能错失新版本中引入的功能,从而影响整体的用户体验。
- 安全隐患:如果新版本中修复了安全漏洞,那些未能及时更新的用户可能会暴露于风险之中。
- 性能缺失:新版本可能包含性能上的优化,而未更新的用户则无法享受到这些改进。
- 兼容性问题:新版本可能修复了与第三方服务或组件的兼容性问题,未更新的用户可能会遭遇兼容性错误。
- 数据不一致性:如果新版本对数据模型进行了更改,未更新的用户可能会导致数据不一致。
- UI/UX不一致性:新版本可能对用户界面进行了改进,未更新的用户可能会遇到不一致的交互体验。
过去,在项目发布新版本时,我们通常会发送专门的更新通知,提醒业务人员清除缓存。然而,由于种种原因,业务人员可能未能及时更新至最新版本,导致了一系列问题的出现,并将这些问题反馈给开发团队,让我们感到无奈。为了解决这一问题,我们一直在寻找一种解决方案,即在新版本发布后能够自动提示用户更新,而无需反复在群组中提醒业务人员清除缓存。幸运的是,在新部门中,我们发现了一款名为 plugin-web-update-notification
的插件,它专门用于处理版本更新问题。经过长时间的使用,我们发现这款插件确实非常好用。
为了更深入地理解这款插件,我花了一些时间研究了它的源代码,从中获益良多。本文旨在记录我对这款插件源代码的分析。在阅读源代码的过程中,我发现了许多之前未曾使用过的 API,包括 Node.js 和 Vite 的 API。本文将主要基于 Vite 对这款插件进行介绍。
文章将分为五个部分:第一部分为概述,包括版本更新的理念和需要解决的问题;第二、三部分将分别介绍插件中使用的 Node.js 和 Vite API,为后续的源码解读打下基础;第四、五部分将分别介绍插件的 JavaScript 脚本的核心代码块以及如何在 Vite 中进行配置。
通过这篇文章,我们希望能够为那些同样面临版本更新挑战的开发者提供一些有价值的见解和解决方案。
概述
在我们深入探索这款插件之前,先来思考一个核心问题:为什么浏览器不自动更新到最新版本的代码?
这难道不是一个一劳永逸的解决方案吗?
浏览器不自动更新代码的原因主要涉及以下几个方面:
-
缓存机制:为了提升性能和用户体验,浏览器会对网页资源进行缓存。这意味着,即便服务器端的代码已经更新,浏览器也可能继续使用缓存中的旧版本代码。要避免这种情况,通常需要采取特定措施,比如在文件名中加入版本号或时间戳,或者通过服务器配置来禁止特定文件的缓存。
-
兼容性与稳定性:自动更新所有代码可能会引入兼容性问题,尤其是在新代码与旧版本不兼容的情况下。此外,自动更新可能会带来新的错误或问题,从而影响用户体验和网站稳定性。
-
控制权与安全性:用户和网站管理员往往希望掌控代码更新的时机,以确保更新不会干扰正在进行的工作或业务流程。自动更新可能会在不适当的时刻发生,导致服务中断或数据丢失。
-
技术实现:实现浏览器自动更新最新代码需要复杂的技术方案,包括版本检测、服务器推送技术(如 Server-Sent Events, SSE)、WebSocket 实时通信等。这些技术不仅需要服务器端的配合,还可能增加服务器的负担。
-
用户选择:用户可能基于个人偏好或特定需求选择不更新到最新版本的代码。例如,某些用户可能依赖于特定版本的功能或插件。
-
开发和部署流程:网站和应用程序的开发和部署通常遵循严格的流程,包括测试和审核新代码。自动更新可能会绕过这些流程,增加风险。
因此,浏览器不自动更新最新版本的代码,是为了确保用户体验、兼容性、控制权和安全性,同时考虑到技术实现的复杂性和开发部署流程的需要。网站开发者通常会采取其他措施,如版本控制和缓存管理,以确保用户能够及时获取更新。这种平衡是为了在提供最新功能的同时,保护用户免受潜在问题的影响。
插件 plugin-web-update-notification
的出现正是基于这样的背景:我们需要一种机制来确保用户能够及时地接收到前端代码的更新。接下来,让我们探讨第二个问题:如何判断前端代码是否已经更新?更新的评判标准又是什么?
判断前端代码更新的标准可以从以下几个方面来考量:
- 版本号变化:前端项目通常会维护一个版本号,这个版本号可能是基于 Git 的提交哈希(git commit hash)、
package.json
中的版本信息、构建时间戳(build timestamp),或者是自定义的版本标识(custom)。当这些版本号发生变动时,我们可以认为前端代码已经经历了更新。 - 文件哈希值变化:在构建过程中,为每个文件生成一个唯一的哈希值是一种常见做法。通过比较当前页面加载的文件哈希值与服务器上最新文件的哈希值,如果存在不一致,这通常意味着前端代码已经更新。
- 资源加载变化:通过定期获取服务器上的前端资源,并匹配其中的
<script>
标签,对比这些标签是否发生变化,可以帮助我们检测是否有新版本发布。 - 等等
这些标准不仅帮助开发者和用户确认前端代码是否已经更新,还指导用户在必要时采取行动(例如刷新页面)以获取最新代码。通过这些细致的评判标准,我们可以更精确地管理和控制前端代码的更新流程,确保用户始终拥有最佳的浏览体验。
这里我们以版本号变化作为评判标准,我们接下来探讨如何获取、保存和比较这些版本号
。这些细节将在后续章节中详细介绍,目前我们只需了解基本概念。
现在,我们需要明确一个关键点:在新版本发布之后,用户的浏览器中可能同时存在新旧两个版本的代码,旧版本的代码通常以缓存的形式保留。通过重新加载浏览器,可以覆盖旧代码以加载新代码。但这就引出了我们的第四个问题:在未更新之前,浏览器请求的资源都是已经缓存的,我们如何获取最新的版本号来进行比较呢?
一种有效的解决方案是使用 window.fetch
API,并在请求的 URL 后面添加一个查询参数,例如时间戳或版本号。这种做法可以绕过浏览器缓存,从而获取最新的资源。这种方法的工作原理是:
- 唯一URL请求:通过在请求的 URL 后面添加一个唯一的查询参数(如当前的时间戳),每次请求的 URL 都会有所不同,因此浏览器会将其视为一个新的请求,而不是从缓存中读取旧文件。
以下是具体的实现步骤:
-
添加查询参数:
window.fetch(`/path/to/version.json?t=${Date.now()}`) .then(response => response.json()) .then(data => { console.log(data); // 处理获取到的数据 }) .catch(error => { console.error('Fetching version file failed:', error); });
在这个例子中,
Date.now()
用于生成一个时间戳,确保每次请求的 URL 都是唯一的。 -
处理响应: 使用
.then()
方法处理响应,将响应体转换为 JSON 格式,并处理获取到的数据。 -
错误处理: 使用
.catch()
方法捕获请求过程中可能发生的错误,如网络问题或文件不存在等。
通过这种方式,我们可以确保即使在浏览器缓存的情况下,也能够获取到服务器上的最新版本号文件,从而与当前页面的版本号进行比较,判断是否需要更新。这种方法简单而有效,能够确保我们的应用能够及时地检测到版本更新,为用户提供最新的内容。
如果在原生的html页面,该怎么实现版本更新提示呢?
在原生HTML项目中实现全局页面监控版本是否更新升级,可以通过以下步骤来完成
:
-
版本控制:
- 首先,你需要有一个版本控制系统。通常,这可以通过在服务器上维护一个版本号文件(例如
version.txt
),或者直接在HTML文件中嵌入版本号。
- 首先,你需要有一个版本控制系统。通常,这可以通过在服务器上维护一个版本号文件(例如
-
获取当前版本:
- 在HTML页面加载时,获取当前页面的版本号。这可以通过JavaScript读取嵌入在HTML中的数据属性或通过AJAX请求获取服务器上的版本号。
-
检查更新:
- 使用JavaScript定期检查是否有新版本。可以通过定时器(如
setInterval
)或在页面加载时立即检查。
- 使用JavaScript定期检查是否有新版本。可以通过定时器(如
-
版本比较:
- 将当前版本号与服务器上的最新版本号进行比较。这可以通过简单的字符串比较或数字比较来实现。
-
弹出提示:
- 如果发现有新版本,弹出一个确认框给用户。
-
页面刷新:
- 用户点击确认后,刷新页面以加载新版本。
以下是一个简单的实现示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Version Check Example</title>
<script>
// 假设当前页面版本号
const currentVersion = '1.0.1';
// 函数来检查更新
function checkForUpdate() {
fetch('version.txt') // 假设服务器上有一个文件包含最新版本号
.then(response => response.text())
.then(serverVersion => {
if (serverVersion > currentVersion) {
if (confirm('发现新版本,是否立即更新?')) {
location.reload(); // 用户确认后刷新页面
}
}
})
.catch(error => console.error('Error checking for updates:', error));
}
// 页面加载时检查更新
window.onload = checkForUpdate;
// 每隔一段时间检查更新(例如每小时)
setInterval(checkForUpdate, 3600000);
</script>
</head>
<body>
<h1>Welcome to My Page</h1>
<p>Current Version: <span id="version"></span></p>
<script>
document.getElementById('version').textContent = currentVersion;
</script>
</body>
</html>
通过上述介绍,我们了解了plugin-web-update-notification
插件的原理和实现步骤。
但是上面也有一些问题亟待解决:
-
plugin-web-update-notification插件是如何获取当前的版本号?如何在vite打包构建过程中讲版本信息进行保存和比较的?
-
plugin-web-update-notification插件是如何打包插件的css和js资源,并最后在index.html中注入的?
带着上面的问题,我们会在源码章节进行介绍。在介绍源码之前,我们先介绍一些插件源码中使用的node
和vite
api。
预备知识-Node API
plugin-web-update-notification
插件在使用 Node.js API 时,涉及到的属性和钩子函数主要包括以下几个:
1. readFileSync (fs 模块)
- 作用:同步读取文件内容。
- 用法示例:
import fs from 'fs' const content = fs.readFileSync(filePath, 'utf-8')
- 注意:由于
readFileSync
是同步操作,它会阻塞事件循环直到文件被完全读取。在大文件或高并发场景下,可能需要考虑使用异步的fs.promises.readFile
或fs.createReadStream
。
2. fileURLToPath (url 模块)
- 作用:将
file://
URL 转换为文件路径。 - 用法示例:
import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url)
- 注意:这个函数主要用于处理 ES 模块中的文件路径,特别是在
import.meta.url
场景下。
3. execSync (child_process 模块)
- 作用:同步执行 shell 命令。
- 用法示例:
import { execSync } from 'child_process' const output = execSync('git rev-parse HEAD', { encoding: 'utf-8' }).trim()
- 注意:
execSync
也会阻塞事件循环直到命令执行完成。对于耗时的命令,可能需要考虑使用异步的exec
或spawn
。
4. copyFileSync (fs 模块)
- 作用:同步复制文件。
- 用法示例:
import fs from 'fs' fs.copyFileSync(sourcePath, destinationPath)
- 注意:
copyFileSync
会覆盖目标文件,如果目标文件已存在。
5. writeFileSync (fs 模块)
- 作用:同步写入文件。
- 用法示例:
import fs from 'fs' fs.writeFileSync(filePath, content, 'utf-8')
- 注意:
writeFileSync
会创建文件(如果文件不存在),或截断文件(如果文件存在但较小),然后写入数据。
6. findUpSync (find-up 包)
- 作用:向上查找文件或目录。
- 用法示例:
import { findUpSync } from 'find-up' const filePath = findUpSync('package.json')
- 注意:
findUpSync
不是 Node.js 核心 API 的一部分,需要通过 npm 安装find-up
包来使用。
这些 API 的使用展示了 plugin-web-update-notification
插件如何与文件系统交互、执行系统命令、处理文件路径,以及在项目结构中定位文件。这些操作对于实现版本检查、文件更新通知等功能是必要的。
预备知识- Vite Hooks
Vite 插件中常用的属性和钩子函数包括以下内容:
核心属性
- name:插件的名称,用于标识插件。
- enforce:用于强制插件的执行顺序,可以是
'pre'
或'post'
。'pre'
表示插件在默认顺序之前执行,'post'
表示在默认顺序之后执行。 - apply:指定插件应用的环境,可以是
'build'
或'serve'
。这决定了插件是在构建时应用还是在服务时应用。
构建钩子(Build Hooks)
- options:在 Rollup 配置生成之前,允许插件修改 Vite 的内部 Rollup 配置。
- buildStart:在构建开始时调用,可以用来进行初始化操作。
- resolveId:在模块解析阶段调用,允许插件自定义模块的解析逻辑。
- load:在模块加载阶段调用,允许插件自定义模块的加载逻辑。
- transform:在模块转换阶段调用,允许插件对模块代码进行转换。
- buildEnd:在构建结束时调用,可以用来进行清理或收尾工作。
- closeBundle:在 Rollup 生成 bundle 后,但在 Vite 完成构建之前调用。
配置钩子(Config Hooks)
- config:允许插件修改 Vite 的配置对象。
- configResolved:在 Vite 配置解析完成后调用,此时配置已经完全解析,可以用来进行基于配置的进一步操作。
通用钩子
- transformIndexHtml:允许插件转换项目的
index.html
文件。
虚拟模块相关
- resolveId 和 load:这两个钩子也用于处理虚拟模块,即那些不存在于文件系统中的模块。
输出生成钩子
- generateBundle:在 Rollup 生成 bundle 时调用,允许插件操作最终的输出。
- writeBundle:在 Rollup 输出文件写入磁盘后调用,可以用来进行额外的文件操作。
上述hooks的详细介绍和其他的hooks介绍详情可直接查看vite hooks官网。
插件核心源码解读
获取项目版本号 getHostProjectPkgVersion
export function getHostProjectPkgVersion() {
try {
// 尝试从环境变量中获取项目版本号
return process.env.npm_package_version as string;
} catch (err) {
// 如果获取失败,输出警告并抛出错误
console.warn(`
======================================================
[plugin-web-update-notice] cannot get the version of the host project's package.json file!
======================================================`);
throw err;
}
}
这个函数尝试从环境变量 process.env.npm_package_version
中获取宿主项目的版本号。这个环境变量通常在构建过程中由打包工具(如 Vite 或 Webpack)设置。如果获取失败,函数会输出一条警告信息,并重新抛出错误。
获取当前 Git 提交哈希 getGitCommitHash
export function getGitCommitHash() {
try {
// 尝试执行 Git 命令获取当前分支的简短提交哈希值
return execSync('git rev-parse --short HEAD').toString().replace('\n', '').trim();
} catch (err) {
// 如果执行失败,输出警告并抛出错误
console.warn(`
======================================================
[plugin-web-update-notice] Not a git repository!
======================================================`);
throw err;
}
}
这个函数使用 Node.js 的 execSync
方法执行 Git 命令 git rev-parse --short HEAD
,以获取当前 Git 仓库的最新提交的简短哈希值。如果当前目录不是一个 Git 仓库,execSync
会抛出错误,函数会捕获这个错误,输出一条警告信息,并重新抛出错误。
这两个函数为 plugin-web-update-notice
插件提供了版本检测的基础设施。getHostProjectPkgVersion
函数用于获取项目版本号,而 getGitCommitHash
函数用于获取当前 Git 提交的哈希值。这些信息对于插件来说至关重要,因为它们可以用来确定是否需要提示用户刷新页面以获取最新的代码更新。如果这些信息无法获取,插件会通过控制台警告提醒开发者,并确保错误能够被适当地处理。
获取最新版本信息
const checkSystemUpdate = () => {
window
.fetch(`${injectFileBase}${DIRECTORY_NAME}/${JSON_FILE_NAME}.json?t=${Date.now()}`)
.then((response) => {
if (!response.ok)
throw new Error(`Failed to fetch ${JSON_FILE_NAME}.json`)
return response.json()
})
.then(({ version: versionFromServer, silence }: VersionJSON) => {
if (silence)
return
latestVersion = versionFromServer
if (window.pluginWebUpdateNotice_version !== versionFromServer) {
// dispatch custom event
document.body.dispatchEvent(new CustomEvent(CUSTOM_UPDATE_EVENT_NAME, {
detail: {
options,
version: versionFromServer,
},
bubbles: true,
}))
const dismiss = localStorage.getItem(`${LOCAL_STORAGE_PREFIX}${versionFromServer}`) === 'true'
if (!hasShowSystemUpdateNotice && !hiddenDefaultNotification && !dismiss)
showNotification(options)
}
})
.catch((err) => {
console.error('[pluginWebUpdateNotice] Failed to check system update', err)
})
}
用于检查系统更新的函数 checkSystemUpdate
,它使用 window.fetch
来请求服务器上的版本信息文件,并根据返回的版本号判断是否需要提示用户更新。
其职责是检查系统是否有可用的更新。该函数执行以下步骤:
- 构建请求 URL:通过拼接基础路径、目录名和 JSON 文件名,并附加一个时间戳查询参数,以确保每次请求都能绕过浏览器缓存,获取最新的版本信息。
- 检查响应状态:对服务器的响应进行检查,如果响应状态码表明请求失败,则抛出错误。
- 解析 JSON 数据:将响应体解析为 JSON 格式,以获取服务器上的最新版本信息。
- 处理版本信息:如果服务器返回的
silence
属性为true
,则不进行任何操作。否则,将服务器版本号赋值给latestVersion
变量,并与本地存储的版本号进行比较。 - 触发自定义事件:如果本地版本号与服务器版本号不一致,触发一个自定义事件,以便其他部分的代码可以响应这一更新。
- 显示更新通知:检查本地存储中是否已经显示过更新通知或用户是否已忽略此版本。如果没有,并且没有隐藏默认通知,则调用
showNotification
函数来向用户显示更新提示。 - 错误处理:如果在整个过程中发生错误,如网络请求失败或 JSON 解析错误,捕获这些错误并记录到控制台。
轮询方式
function limit(fn: Function, delay: number) {
let pending = false
return function (this: any, ...args: any[]) {
if (pending)
return
pending = true
fn.apply(this, args)
setTimeout(() => {
pending = false
}, delay)
}
}
/**
* polling check system update
*/
const pollingCheck = () => {
if (checkInterval > 0)
intervalTimer = setInterval(checkSystemUpdate, checkInterval)
}
pollingCheck()
const limitCheckSystemUpdate = limit(checkSystemUpdate, 5000)
window.pluginWebUpdateNotice_.checkUpdate = limitCheckSystemUpdate
// when page visibility change, check system update
window.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
pollingCheck()
if (checkOnWindowFocus)
limitCheckSystemUpdate()
}
if (document.visibilityState === 'hidden')
intervalTimer && clearInterval(intervalTimer)
})
// when page focus, check system update
window.addEventListener('focus', () => {
if (checkOnWindowFocus)
limitCheckSystemUpdate()
})
if (checkOnLoadFileError) {
// listener script resource loading error
window.addEventListener(
'error',
(err) => {
const errTagName = (err?.target as any)?.tagName
if (errTagName === 'SCRIPT')
checkSystemUpdate()
},
true,
)
}
}
这段代码通过节流和定时器实现了一个智能的系统更新检查机制,它能够在页面可见、获得焦点或资源加载失败时检查更新,同时限制了检查的频率,以避免过度的网络请求。这种机制有助于确保用户及时了解到系统的最新更新,同时保持了良好的性能和用户体验。
插件vite源码解读
css和js等资源注入
function injectPluginHtml(
html: string,
version: string,
options: Options,
{ cssFileHash, jsFileHash }: { jsFileHash: string; cssFileHash: string },
) {
const { customNotificationHTML, hiddenDefaultNotification, injectFileBase = '' } = options
const versionScript = `<script>window.pluginWebUpdateNotice_version = '${version}';</script>`
const cssLinkHtml = customNotificationHTML || hiddenDefaultNotification ? '' : `<link rel="stylesheet" href="${injectFileBase}${DIRECTORY_NAME}/${INJECT_STYLE_FILE_NAME}.${cssFileHash}.css">`
let res = html
res = res.replace(
'<head>',
`<head>
${cssLinkHtml}
<script src="${injectFileBase}${DIRECTORY_NAME}/${INJECT_SCRIPT_FILE_NAME}.${jsFileHash}.js"></script>
${versionScript}`,
)
if (!hiddenDefaultNotification) {
res = res.replace(
'</body>',
`<div class="${NOTIFICATION_ANCHOR_CLASS_NAME}"></div></body>`,
)
}
return res
}
这里是使用正则表达式进行替换的,方法比较简单,这里不做进一步的解读。
在plugin-web-update-notification插件的vite配置中一共使用了6个vite的属性和hooks,分别是name,apply,enforce,configResolved,generateBundle和transformIndexHtml。前三个属性还是比较容易理解的,generateBundle也比较好理解,用来打包版本json文件、css和js脚本文件,但是在打包这三个文件时,包含版本信息的json文件名称是固定的,但css和js脚本资源打包文件会在文件名后面加一个hash值。
generateBundle(_, bundle = {}) {
if (!version)
return
// inject version json file
bundle[JSON_FILE_NAME] = {
// @ts-expect-error: for Vite 3 support, Vite 4 has removed `isAsset` property
isAsset: true,
type: 'asset',
name: undefined,
source: generateJSONFileContent(version, silence),
fileName: `${DIRECTORY_NAME}/${JSON_FILE_NAME}.json`,
}
// inject css file
bundle[INJECT_STYLE_FILE_NAME] = {
// @ts-expect-error: for Vite 3 support, Vite 4 has removed `isAsset` property
isAsset: true,
type: 'asset',
name: undefined,
source: cssFileSource,
fileName: `${DIRECTORY_NAME}/${INJECT_STYLE_FILE_NAME}.${cssFileHash}.css`,
}
// inject js file
bundle[INJECT_SCRIPT_FILE_NAME] = {
// @ts-expect-error: for Vite 3 support, Vite 4 has removed `isAsset` property
isAsset: true,
type: 'asset',
name: undefined,
source: jsFileSource,
fileName: `${DIRECTORY_NAME}/${INJECT_SCRIPT_FILE_NAME}.${jsFileHash}.js`,
}
},
这又是什么原因呢?简单来说直接固定文件名不行吗?
在 plugin-web-update-notification
插件的 Vite 配置中,包含版本信息的 JSON 文件名称是固定的,而 CSS 和 JS 脚本资源打包文件会在文件名后面加上一个 hash 值,这是出于以下几个原因:
-
缓存控制:浏览器会缓存静态资源以提高性能。固定的 JSON 文件名使得插件能够在每次构建时生成一个包含新版本信息的文件,而无需更改文件名。这样,即使浏览器缓存了旧版本的 JSON 文件,插件也能通过固定的文件名来获取最新版本信息。
-
避免缓存冲突:对于 CSS 和 JS 文件,每次构建时生成的 hash 值是唯一的,这确保了即使文件内容只发生了微小的变化,文件名也会不同,从而避免了浏览器缓存旧版本的文件。这种策略称为“缓存破坏”或“指纹命名”,是前端构建过程中常用的一种方法来确保用户总是加载最新版本的资源。
-
版本控制:JSON 文件中包含的版本信息用于标识当前构建的唯一性。如果 JSON 文件名发生变化,可能会导致客户端无法正确识别和比较版本,从而影响更新检测的准确性。
-
灵活性和兼容性:固定 JSON 文件名的同时使用带有 hash 值的 CSS 和 JS 文件名,结合了版本控制和缓存控制的需要。这种方法既确保了版本信息的一致性,又利用了 Vite 的资产(asset)处理能力,确保了资源的最新性。
简而言之,固定 JSON 文件名是为了确保版本信息的一致性和可比性,而 CSS 和 JS 文件名后添加 hash 值则是为了有效地控制缓存,确保用户总是加载到最新版本的资源。这种方法结合了版本控制和缓存控制的优势,提高了应用的更新效率和用户体验。
归根到底还是浏览器缓存的问题,那版本json文件为啥后面不加hash值呢?其实也很简单,上文我们提到过,我们需要在旧版本页面中获取这个版本json文件,使用window.fetch()方法,该方法内部是要传具体文件地址,如果打包的时候json文件名后面拼上hash值,那我们由于不知道这个hash值,进而得不到这个这个版本信息,那后面的流程也就走不下去了,那拿版本数据时,如何避免浏览器缓存的影响呢?上文也讲到了,是在请求后面加一个当前时间戳参数,来避免浏览器缓存带来的影响。
总结来说,版本 JSON 文件的名称不包含哈希值是为了确保我们能够在旧版本的页面中准确地获取到新版本的信息,并通过添加时间戳参数来避免浏览器缓存的影响。这种方法既保证了版本的可检测性,也有效地控制了缓存,确保用户能够及时接收到更新。
transformIndexHtml:
// if the viteVersion is undefined, we assume that vite is less than v3.0(after v3.0, vite export version)
// viteVersion === undefined
// ? {
{
order: 'post',
handler(html: string, { chunk }) {
if (version && chunk)
return injectPluginHtml(html, version, options, { jsFileHash, cssFileHash })
return html
},
enforce: 'post', // deprecated since Vite 4
async transform(html: string) { // deprecated since Vite 4
if (version)
return injectPluginHtml(html, version, options, { jsFileHash, cssFileHash })
return html
},
},
}
这段代码是 Vite 插件配置中的一部分,具体是针对 transformIndexHtml
钩子的配置。这个钩子用于在 Vite 构建过程中处理 index.html
文件之后、写入磁盘之前修改它。代码的目的是将特定的 HTML、CSS 和 JavaScript 资源注入到 index.html
文件中,这对于需要添加监控或通知脚本的插件来说是一个常见的需求。
transformIndexHtml
这个hooks其实更加容易理解了,我们的最终目的就是在index.html中注入最新的css和js脚本资源,以及最新的版本信息,来实现前端版本更新的功能。
但我们发现在generateBundle
和 transformIndexHtml
这两个hooks中,出现了几个比较重要的参数,分别是version、cssFileHash和jsFileHash,版本信息version比较容易理解,我们可以通过上一章节的获取版本信息的方法来获取当前的版本信息。但cssFileHash和jsFileHash要如何生成呢?这里是通过readFileSync 读取文件的全部内容,并将其转换为字符串,利用
export function getFileHash(fileString: string) {
return md5(fileString).slice(0, 8)
}
方法生成的文件hash。只不过在js脚本中,添加了一些Vite 配置被解析后访问和使用已解析的配置项,进行一些后续的操作,才使用到configResolved这个hooks。
其实看别人的代码容易理解,又不容易理解,容易理解就是写的方法不难,使用的hooks和方法相对容易理解,不容易理解就是这个插件是一步步进行完善升级的,我们不过是解读代码,其中的思路和优化还是相对不容易考虑到的。解读源码,一方面是了解他们所使用的方法,另一方面就是参考了解其中的流程和思路。
解读他人的代码确实是一个既直观又复杂的过程,它涉及到对代码的直接理解以及对代码背后思路和逻辑的深入洞察。以下是对这个过程的一些补充:
代码的直接理解
- 技术实现:直接阅读代码可以帮助我们理解作者使用了哪些技术、框架和库,以及如何应用这些工具来实现特定的功能。
- 方法和钩子:通过查看代码,我们可以学习到特定的方法和钩子是如何被调用和实现的,这对于我们在自己的项目中应用类似的技术非常有帮助。
- 代码结构:观察代码的结构和组织方式可以让我们了解如何模块化代码,以及如何维护和扩展代码库。
代码背后的思路和逻辑
- 问题解决:理解代码背后的思路意味着要弄清楚作者试图解决什么问题,以及为什么选择特定的技术方案。
- 设计决策:每个代码库都是一系列设计决策的结果。理解这些决策背后的逻辑可以帮助我们在自己的项目中做出更好的设计选择。
- 优化和改进:随着项目的发展,代码库会经历多次优化和改进。了解这些变化的过程可以帮助我们学习如何迭代和改进自己的代码。
- 上下文理解:代码往往是在特定的上下文中编写的,理解这些上下文(如业务需求、技术限制等)对于完全理解代码至关重要。
源码解读的价值
- 学习最佳实践:通过解读源码,我们可以学习到行业内的最佳实践,这些实践可以帮助我们提高自己的编码水平。
- 启发新思路:他人的代码可能会激发我们的新思路,帮助我们在面对类似问题时找到创新的解决方案。
- 避免重复错误:通过学习他人的代码,我们可以避免犯相同的错误,特别是在处理复杂的技术问题时。
总之,解读源码不仅是学习具体技术的过程,也是对编程思维和问题解决策略的深入探索。这种活动有助于我们成为更全面的开发者,不仅在技术上,也在思考和解决问题的方式上。