Правка по хэшам

This commit is contained in:
NikitaKalitov
2026-08-25 11:15:04 +03:00
parent 6e420470b3
commit 1a9f24e53f
5 changed files with 80 additions and 30 deletions
+8 -4
View File
@@ -13,11 +13,14 @@
- `VITE_BASE_PATH` — подпуть, под которым будет отдаваться приложение. Должен совпадать с тем, куда положите `dist/` на шаге 2.
- `VITE_BITRIX_ORIGIN` здесь **не нужен**: приложение и портал на одном домене, origin хоста приложение берёт из `window.location.origin` в рантайме. Задавать переменную нужно только если хост на другом origin — например, локальная разработка (dev-сервер хоста и `vite preview` этого приложения на разных портах) или деплой отдельным контейнером (см. `bitrix-integration.md`, раздел 7, вариант B).
2. Скопировать содержимое `dist/` в `local/diagram-app/` внутри каталога сайта на сервере (тот же каталог, где лежит остальной код портала — `bitrix_www`/аналог на проде).
3. Готово: приложение доступно по `https://ВАШ-ДОМЕН-ПОРТАЛА/local/diagram-app/index.html`.
3. Готово — но с оговоркой: приложение доступно **не** по фиксированному `index.html`.
Если схему нужно только показать, без правки, — добавьте к `src` iframe `?mode=view`: редактор откроется в режиме просмотра, переключатель режимов будет скрыт.
**Имя `index.html` в `dist/` — с хешем содержимого**, например `index-2fddeca6.html`, а не просто `index.html` (см. `vite.config.ts`, плагин `hashIndexHtml` — хеширует и сам HTML-файл, не только JS/CSS, которые Vite и так хеширует по умолчанию). Хеш меняется при каждой значимой пересборке и не меняется, если содержимое не менялось. Значит:
- **`src` у iframe нельзя хардкодить строкой** — нужно на своей стороне подставлять актуальное имя файла (например, взять единственный `index-*.html` в `local/diagram-app/`, или через отдельный метод получения актуального имени — решение за бэкендом).
- Ручной cache-busting (`?v=...`) для JS/CSS/HTML **больше не нужен**: раз URL меняется вместе с содержимым, старый закэшированный файл просто никогда больше не запрашивается, как только известно новое имя.
- **После каждого редеплоя старые `index-<hash>.html`/`index-<hash>.js`/`index-<hash>.css` от прошлых версий стоит убирать** из `local/diagram-app/` — они не удаляются сами, и если способ поиска актуального файла — «взять единственный файл по маске», их накопление сломает эту логику.
**При каждом повторном деплое** (обновили React-приложение, положили новый `dist/` поверх старого) — добавляйте к `src` iframe cache-busting параметр (`?v=<дата/хэш билда>`). У `.html`-файлов на большинстве Bitrix-стендов стоит `Cache-Control` на 30 дней — без версии в URL браузер администратора может продолжать открывать старый `index.html` со старыми хэшами JS/CSS ещё месяц.
Если схему нужно только показать, без правки, — добавьте к `src` iframe `?mode=view`: редактор откроется в режиме просмотра, переключатель режимов будет скрыт (параметр дописывается к уже вычисленному актуальному имени файла).
## 2. Как сформировать HTML
@@ -25,7 +28,8 @@
```html
<input type="hidden" id="DIAGRAM-DATA" name="DIAGRAM-DATA" value="<?= htmlspecialcharsbx($currentValue) ?>">
<iframe id="diagram-frame" src="/local/diagram-app/index.html" style="width:100%;height:600px;border:1px solid #d7dae0;border-radius:6px"></iframe>
<iframe id="diagram-frame" src="/local/diagram-app/<?= htmlspecialcharsbx($actualIndexFileName) ?>" style="width:100%;height:600px;border:1px solid #d7dae0;border-radius:6px"></iframe>
<!-- $actualIndexFileName — актуальное имя index-<hash>.html, см. раздел 1 -->
<script>
(function () {
+11 -8
View File
@@ -44,8 +44,12 @@ class DiagramProperty
// Вариант A — рекомендуемый, приложение лежит внутри дерева портала (см. раздел 7):
// appOrigin — это просто origin текущего запроса, appUrl — конкретный файл.
// ВАЖНО: имя файла — не константа. dist/index.html выходит из сборки как
// index-<hash>.html (хеш меняется при каждой значимой пересборке, см.
// раздел 7) — 'index.html' строкой здесь писать нельзя, нужно получать
// актуальное имя каким-то способом на своей стороне (см. раздел 7).
$appOrigin = (CMain::IsHTTPS() ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'];
$appUrl = $appOrigin . '/local/diagram-app/index.html';
$appUrl = $appOrigin . '/local/diagram-app/' . GetActualDiagramAppIndexFile(); // пример, не готовая функция
// Вариант B — если приложение развёрнуто отдельным контейнером (см. раздел 7),
// appUrl/appOrigin вместо этого указывают на его отдельный адрес, например:
// $appOrigin = 'http://localhost:5178';
@@ -209,14 +213,13 @@ Backend не обязан ничего валидировать глубже «
Что это даёт технически:
- Nginx-конфиг стенда отдаёт файлы по расширению (`js|css|html|svg|png|...`) напрямую с диска — это срабатывает раньше, чем запрос попадёт в PHP-роутер Bitrix. Любой статический файл под корнем сайта отдаётся сам, никаких правок nginx не требуется.
- `local/` уже существует как каталог для кастомного кода (`ai`, `components`, `modules`, `php_interface`, `templates` — по факту на дев-стенде). Новый подкаталог, например `local/diagram-app/`, вписывается в эту конвенцию и не пересекается с `local/modules`/`local/php_interface` — они у nginx явно помечены `internal` (заблокированы для прямого внешнего доступа), новый подкаталог под этот блок не попадает.
- Нюанс: `index` на уровне сервера — `index.php`, не `index.html`. Открыть голый `/local/diagram-app/` (без имени файла) не выйдет — nginx попытается найти там `index.php` и не найдёт. Не проблема: PHP-класс (раздел 3) сам подставляет `src` у iframe, поэтому в `$appUrl` достаточно явно указывать файл — `/local/diagram-app/index.html`.
- Нюанс: `index` на уровне сервера — `index.php`, не `index.html`. Открыть голый `/local/diagram-app/` (без имени файла) не выйдет — nginx попытается найти там `index.php` и не найдёт. PHP-класс (раздел 3) сам подставляет `src` у iframe — но, в отличие от более ранней версии этого документа, **больше нельзя просто написать `/local/diagram-app/index.html` строкой** — см. следующий пункт.
- Так как приложение и портал теперь на одном домене, `VITE_BITRIX_ORIGIN`/`appOrigin` — это просто origin самого портала, а не адрес отдельного сервиса.
- **Важный нюанс, найден живым тестом на дев-стенде:** тот же regex-location в nginx, что раздаёт статику, ставит `.html`-, `.js`- и `.css`-файлам одинаковый `Cache-Control: public` с `expires 30d`. Это касается **двух независимых уровней**, и решать их нужно по отдельности — закрыть только один недостаточно:
- **Имена файлов в `dist/` — с хешем содержимого, включая сам `index.html`.** `index.js`/`index.css`/шрифты хеширует сам Vite при сборке (стандартное поведение, как в любом Vite-проекте). Кроме того, в `vite.config.ts` есть небольшой плагин (`hashIndexHtml`), который **дополнительно хеширует и сам `index.html`** — на выходе получается что-то вроде `index-2fddeca6.html`, а не просто `index.html`. Имя меняется всякий раз, когда меняется что-то значимое (сама HTML-обёртка либо то, что она подключает), и не меняется при пересборке без реальных изменений.
1. **Кэш самого `index.html`.** После повторного деплоя (обновили React-приложение, положили новый `dist/` поверх старого) браузер, уже открывавший `$appUrl` раньше, может до 30 дней получать из кэша старый `index.html` вместо нового. Решение: тот, кто формирует `src` у iframe (PHP-класс из раздела 3, либо любой другой хост), должен сам добавлять к нему cache-busting query-параметр, привязанный к версии сборки (например, `?v=<git-hash>` или `?v=<дата билда>`), а не отдавать голый `/local/diagram-app/index.html` без параметров.
2. **Кэш `index.js`/`index.css`, на которые ссылается сам `index.html`.** Тут пункт 1 не помогает: даже если браузер честно перезапросит свежий `index.html` (URL с новым `?v=` не совпал с закэшированным), внутри этого свежего файла ссылки на `index.js`/`index.css` — те же самые, неизменные имена (в приложении нет хеша в имени файла, см. `vite.config.ts` — сделано намеренно, чтобы имена были человекочитаемыми). Если эти конкретные URL уже лежат в кэше браузера с прошлого визита, он отдаст их с диска, даже не сходив на сервер, — HTML окажется свежим, а исполняемый код внутри него старым.
Из этого следует главное отличие от более ранней версии данного документа: **`$appUrl` в PHP-классе (раздел 3) больше нельзя хардкодить как строку** — конкретное имя `index-<hash>.html` меняется при каждой пересборке приложения. Бэкенду нужен свой способ узнавать актуальное имя файла на момент рендера страницы (например, взять единственный `index-*.html` из каталога деплоя, или через отдельный метод/эндпоинт, который вернёт актуальное имя, — механизм на усмотрение бэкенда, здесь не фиксирован, уточняйте на момент внедрения).
Пока это **не автоматизировано** и остаётся на усмотрение того, кто деплоит: либо вручную версировать `index.js`/`index.css` внутри задеплоенного `dist/index.html` при каждом обновлении, либо настроить на стороне nginx `Cache-Control` для этих файлов иначе (например, `no-cache`/`must-revalidate`). Если проблема будет всплывать на практике — можно вернуться к автоматизации на стороне сборки.
Взамен это полностью снимает необходимость в отдельном cache-busting query-параметре (`?v=...`) — раньше он был обязателен и для самого `index.html`, и (до появления хеша у самого файла) для `index.js`/`index.css` внутри него. Теперь URL меняется вместе с содержимым: старый закэшированный `index-OLDHASH.html` просто никогда больше не запрашивается, как только хост узнаёт новое имя — что убирает разом оба уровня кэш-проблемы, описанной в более ранней версии этого раздела.
Порядок действий:
@@ -227,8 +230,8 @@ Backend не обязан ничего валидировать глубже «
`VITE_BITRIX_ORIGIN` в этом варианте не задаётся — см. примечание в конце раздела.
`VITE_BASE_PATH` — путь, под которым будет отдаваться приложение (нужен, чтобы ссылки на JS/CSS внутри `dist/index.html` были корректными под этим подпутём; см. `vite.config.ts`). По умолчанию, если переменная не задана, `base` — `/` (не влияет на локальную разработку, `npm run dev`).
2. Скопировать содержимое `dist/` в `local/diagram-app/` внутри host-каталога, который смонтирован как `bitrix_www` (на текущем дев-стенде это `/home/nikit/bitrix/www/local/diagram-app/` — точный путь для конкретного стенда уточняется у того, кто его поднимал/администрирует).
3. В PHP-классе (раздел 3): `$appUrl = $appOrigin . '/local/diagram-app/index.html';`, `$appOrigin` — origin портала (без пути).
4. Обновление после правок в React-приложении — тот же `npm run build` + копирование поверх старых файлов. Пересборка/перезапуск контейнеров Bitrix не нужны.
3. В PHP-классе (раздел 3): `$appUrl = $appOrigin . '/local/diagram-app/' . $actualIndexFileName;`, `$appOrigin` — origin портала (без пути), `$actualIndexFileName` — актуальное имя `index-<hash>.html` в `local/diagram-app/`, полученное способом на усмотрение бэкенда (см. врезку про кэш выше — хардкодить `index.html` строкой уже нельзя).
4. Обновление после правок в React-приложении — тот же `npm run build` + копирование поверх старых файлов. Пересборка/перезапуск контейнеров Bitrix не нужны. Старый `index-<hash>.html` от предыдущей сборки при этом не удаляется автоматически — если способ поиска актуального файла (шаг 3) предполагает «взять единственный файл по маске», после нескольких деплоев в каталоге стоит убирать файлы прошлых версий, иначе маска перестанет быть однозначной.
### Вариант B — отдельный Docker-контейнер
+4 -1
View File
@@ -78,7 +78,10 @@ function getLockedMode(): "view" | undefined {
const lockedMode = getLockedMode();
const isProd = import.meta.env.PROD;
const adapter = isProd ? createBitrixAdapter() : createDevAdapter();
const adapter =
isProd ?
createBitrixAdapter({ ignoreConfiguredOrigin: lockedMode === 'view' })
: createDevAdapter();
// In Bitrix, no value means a brand-new element — start blank, not with dev demo content.
const fallbackDocument = isProd ? EMPTY_DOCUMENT : SAMPLE_DOCUMENT;
+15 -6
View File
@@ -21,17 +21,26 @@ const CHANGE_MESSAGE = "diagram:change";
* разработка (dev-сервер хоста и `vite preview` этого приложения на разных
* портах) или деплой отдельным контейнером (§7, вариант B).
*
* Требование «никогда не `postMessage('*')`» соблюдено в обоих случаях:
* `ignoreConfiguredOrigin` — режим просмотра (`?mode=view`, см. `App.tsx`)
* всегда игнорирует VITE_BITRIX_ORIGIN, даже если он задан. Один и тот же
* билд встраивается из двух разных мест с разным origin: админка Bitrix
* (фиксированный адрес, туда и печётся переменная) и portal-front в режиме
* просмотра (свой адрес, может не совпадать с зашитым в сборку) — если бы
* переменная действовала и там, просмотр молча ломался бы всякий раз, когда
* portal-front открыт не с того origin, что был на момент сборки конструктора.
*
* Требование «никогда не `postMessage('*')`» соблюдено во всех случаях:
* значение всегда конкретный origin. Если хост окажется на другом домене,
* а переменная не задана, обмен просто не состоится — это безопасный отказ,
* а не утечка сообщений куда попало.
* а переменная не задана (или проигнорирована), обмен просто не состоится —
* это безопасный отказ, а не утечка сообщений куда попало.
*/
function getBitrixOrigin(): string {
function getBitrixOrigin(ignoreConfiguredOrigin: boolean): string {
if (ignoreConfiguredOrigin) return window.location.origin;
return import.meta.env.VITE_BITRIX_ORIGIN || window.location.origin;
}
export function createBitrixAdapter(): HostAdapter {
const targetOrigin = getBitrixOrigin();
export function createBitrixAdapter(options: { ignoreConfiguredOrigin: boolean }): HostAdapter {
const targetOrigin = getBitrixOrigin(options.ignoreConfiguredOrigin);
return {
onReady(callback) {
+42 -11
View File
@@ -1,6 +1,39 @@
import { defineConfig, loadEnv } from 'vite'
import { createHash } from 'node:crypto'
import { existsSync, readFileSync, renameSync } from 'node:fs'
import path from 'node:path'
import { defineConfig, loadEnv, type Plugin } from 'vite'
import react from '@vitejs/plugin-react'
// Vite always writes the HTML entry under its source name (index.html stays
// index.html) — it only content-hashes what that HTML references (JS/CSS),
// never the HTML file itself. The backend now reads the actual deployed
// filename at deploy time instead of hardcoding it on the main portal, so
// index.html needs a unique name too.
//
// index.html isn't part of the same bundle object the JS/CSS live in
// (confirmed empirically — it never showed up in generateBundle's `bundle`),
// so this hashes and renames the file directly on disk after the whole build
// has finished writing it, rather than fighting the bundler's own emit step.
function hashIndexHtml(): Plugin {
let outDir = 'dist'
return {
name: 'hash-index-html',
apply: 'build',
configResolved(config) {
outDir = config.build.outDir
},
closeBundle() {
const htmlPath = path.resolve(outDir, 'index.html')
if (!existsSync(htmlPath)) return
// Hashing the final content (already contains the hashed JS/CSS refs)
// means this name only changes when something meaningful actually did.
const hash = createHash('md5').update(readFileSync(htmlPath)).digest('hex').slice(0, 8)
renameSync(htmlPath, path.resolve(outDir, `index-${hash}.html`))
},
}
}
// https://vite.dev/config/
export default defineConfig(({ mode }) => {
// VITE_BASE_PATH is read here (config/Node context), so it needs an explicit
@@ -13,19 +46,17 @@ export default defineConfig(({ mode }) => {
// inside the Bitrix docroot) instead of domain root — see
// docs/bitrix-integration.md §7.
base: env.VITE_BASE_PATH || '/',
plugins: [react()],
plugins: [react(), hashIndexHtml()],
build: {
// No content hashes in output filenames — cache-busting on redeploy is
// handled at the hosting/nginx level instead (see docs/bitrix-integration.md).
// Flat layout (no "assets/" subfolder): one deployment target's own
// folder is literally named "assets" (local/modules/vniigaz.bpmn/assets/),
// and nesting output under another "assets/" there doubled the path
// segment (.../assets/assets/index.js, 404). Flat works everywhere.
// Same content hash Vite computes by default — just without its default
// "assets/" subfolder prefix. The deploy target's own folder is itself
// named assets (local/modules/vniigaz.bpmn/assets/), so keeping Vite's
// default pattern would nest output at .../assets/assets/index-*.js (404).
rolldownOptions: {
output: {
entryFileNames: '[name].js',
chunkFileNames: '[name].js',
assetFileNames: '[name].[ext]',
entryFileNames: '[name]-[hash].js',
chunkFileNames: '[name]-[hash].js',
assetFileNames: '[name]-[hash][extname]',
},
},
},