Я хочу экспортировать HTML-контент со страниц Confluence. Они могут содержать теги <img> с атрибутами src, которые представляют собой обычные гиперссылки. Поскольку я тоже хочу их экспортировать, я решил заменить содержимое src соответствующими URL-адресами данных, чтобы было src = "data:image/png;base64,AEFJEFEF…".
Конечно, для этого требуется получение изображений через HTTP, и это можно сделать только асинхронно. Кроме того, он содержит множество «вложенных» асинхронных вызовов.
Это мой код:
/**
* @param {HTMLTableCellElement | undefined} cell
*/
asynC#getCellHtml(cell) {
if (!cell) return undefined;
const srcMap = {}
for await (const imgElement of cell.querySelectorAll('img')) {
if ("attachment" !== imgElement.dataset.linkedResourceType) {
return;
}
const imgUrl =
new URL(imgElement.src, imgElement.dataset.baseUrl);
await fetch(imgUrl)
.then(response => response.blob())
.then(blob => blob.arrayBuffer())
.then(arrayBuffer => {
srcMap[imgElement.src] =
`data:${imgElement.dataset.linkedResourceContentType};base64,`
+ Buffer.from(arrayBuffer).toString('base64');
});
}
const cellHtml = cell.innerHTML;
Object.entries(srcMap).forEach(([imgSrc, dataUrl]) => {
cellHtml.replace(imgSrc, dataUrl)
})
return cellHtml;
}
Для справки, такой HTML выглядит следующим образом:
<p style = "text-align: left;"><br/></p>
<p style = "text-align: left;"><span
class = "confluence-embedded-file-wrapper confluence-embedded-manual-size"><img
class = "confluence-embedded-image" draggable = "false" width = "639"
src = "/confluence/download/attachments/2345432345/image-2024-7-11_16-48-22-1.png?version=1&modificationDate=1720709302000&api=v2"
data-image-src = "/confluence/download/attachments/235432345/image-2024-7-11_16-48-22-1.png?version=1&modificationDate=1720709302000&api=v2"
data-unresolved-comment-count = "0" data-linked-resource-id = "345654345"
data-linked-resource-version = "1" data-linked-resource-type = "attachment"
data-linked-resource-default-alias = "image-2024-7-11_16-48-22-1.png"
data-base-url = "https://suite.acme.com/confluence"
data-linked-resource-content-type = "image/png"
data-linked-resource-container-id = "1491043790"
data-linked-resource-container-version = "1" alt = ""/></span></p>
<p style = "text-align: left;"><br/></p>
<p style = "text-align: left;"><br/></p>
Моя цель — перебрать все элементы <img>, найти соответствующие теги <img>, получить данные их изображений и собрать массив замены. После этого я бы просто заменил все результаты соответствующими URL-адресами данных.
Я думаю, что мне бы хотелось чего-то вроде этого:
cell.querySelectorAll('img').map(cell => {
// return a Promise that combines all the fetching etc.
// so that it resolves() with returning the base64 string(!).
return new Promise()…
});
После того, как я map()перенес этот массив в Promises, я мог Promise.all()и затем выполнить замену HTML.
Я понятия не имею, как «вернуть» это последнее обещание, после того как все остальные уже выполнены. Должен ли мой код использовать вызовы await, а не .then(), чтобы я не попадал в контекст обратного вызова?



![Безумие обратных вызовов в javascript [JS]](https://i.imgur.com/WsjO6zJb.png)


Несколько замечаний по вашему текущему коду
for await (const imgElement of cell.querySelectorAll('img'): поскольку querySelectorAll нет async, вам не нужна for await (...) простая for (...) петля, все в порядке.
if ("attachment" !== imgElement.dataset.linkedResourceType) { return; } выйдет из метода для первого элемента, не отвечающего этому условию, и оставит все остальные элементы необработанными. Более того, уже загруженные изображения не будут заменены, потому что вы никогда не доберетесь до кода после цикла. Используйте continue вместо return, чтобы пропустить текущий элемент и перейти к следующему элементу в списке.
Не следует смешивать async/await с then/catch, если вы точно не знаете, что делаете. Потому что это вызовет путаницу и, возможно, приведет к неожиданному поведению.
При этом я бы реорганизовал ваш код следующим образом.
Поскольку ваш asynC#getCellHtml(cell) асинхронный, я бы полностью переключился на await и отказался от всего .then(...)
Замените цикл for, перебирающий все элементы, на Promise.all(). Вам действительно не нужен результат этого Promise.all, потому что, если он не выдаст, вы знаете, все обещания успешно решены. И поскольку каждый обратный вызов устанавливает соответствующее значение в объекте srcMap, вы знаете, что после разрешения Promise.all() все изображения будут загружены.
...
let srcMap = {};
await Promise.all(cell.querySelectorAll('img').map(async c => {
if ("attachment" !== c.dataset.linkedResourceType) {
//ignore wrong resource types and do nothing
return;
};
//for correct resourcetype load the images and update the `srcMap` object
const
imgUrl = new URL(c.src, c.dataset.baseUrl),
resp = await fetch(imgUrl),
blob = await resp.blob(),
buff = await blob.arrayBuffer();
scrMap[c.src] = ...
});
const cellHtml = cell.innerHTML;
...
Конечно, этот код не имеет никакой обработки ошибок. Поэтому, если, например, одно изображение не загружается, весь процесс завершается. Но я позволю вам включить эту обработку ошибок в качестве упражнения.
ах да, спасибо за ваши замечания, 2) это была ошибка, так как изначально у меня был блок кода в .forEach(), поэтому return вместо continue. Также спасибо за подсказку, как не смешивать async/await с then/catch; и в целом за подробное объяснение. Это определенно помогает мне встать на правильный путь!
«Я понятия не имею, как «вернуть» это последнее обещание, после того как все остальные уже выполнены». - Я не понимаю вопроса. Разве в вашем коде уже нет обещания? Теперь просто не делайте
awaitэто в цикле, а верните его из обратного вызоваmap.