使用 Javascript 在本地保存 HTML

新手上路,请多包涵

我知道出于明显的安全原因,客户端 Javascript 无法将数据写入本地文件系统。

使用 Javascript 在本地保存数据的唯一方法似乎是使用 cookie, localStorage ,或允许用户下载文件(使用 “保存…” 对话框或浏览器的默认下载文件夹) .

但是, 在非常特殊的情况下,当使用 file:///D:/test/index.html 类的 URL(而不是通过互联网)在 本地 访问文件时, 是否有可能在 本地 写入数据? (没有任何服务器语言,甚至根本没有任何服务器:只是本地浏览 HTML 文件)

例如,是否可以通过单击此处的“ _保存_”:

   <div contenteditable="true" style="height:200px;">Content editable - edit me and save!</div>
  <button>Save</button>

…这样的 HTML 文件(通过 file:///D:/test/index.html 访问)被其新内容覆盖了吗? (即 本地 HTML 文件应在按下“ _保存_”时更新)。


在此处输入图像描述


TL;DR :当在 本地访问 HTML 页面时,是否可以通过 Javascript 保存文件?

注意:我希望能够 静默保存,而不是建议用户必须选择下载位置的下载/保存对话框,然后是“您确定要覆盖吗”等。


编辑:为什么这个问题?我正在做一个浏览器内记事本,我可以在没有任何服务器(没有 Apache,没有 PHP)的情况下在本地运行。我需要能够轻松保存而不必处理对话框“你想在哪里下载文件?”并且必须始终重新浏览到同一文件夹以覆盖当前正在编辑的文件。我想要一个简单的用户体验,就像在任何记事本程序中一样: CTRL+S 完成,当前文件已保存! (例如:每次执行“保存”时,MS Word 都不会要求浏览要保存文件的位置: CTRL+S ,完成!)

原文由 Basj 发布,翻译遵循 CC BY-SA 4.0 许可协议

阅读 624
2 个回答

Chromium 的文件系统访问 API(2019 年推出)

有一个相对较新的非标准 文件系统访问 API (不要与早期的 文件和目录条目 API文件系统 API 混淆)。看起来它是在 20192020 年在 Chromium/Chrome 中引入的,并且在 Firefox 或 Safari 中不受支持。

使用此API时,本地打开的页面 可以 打开/保存其他本地文件并在页面中使用文件的数据。它确实需要初始权限才能保存,但是当用户在页面上时,特定文件的后续保存会“静默”进行。用户还可以授予对特定目录的权限,随后对该目录的读取和写入不需要批准。用户关闭网页的所有选项卡并重新打开页面后,需要再次批准。

您可以在 https://web.dev/file-system-access/ 阅读更多关于这个新 API 的信息。它旨在用于制作更强大的网络应用程序。

有几点需要注意:

  • 默认情况下,它需要安全上下文才能运行。在 https、localhost 或通过 file:// 运行它应该可以。

  • 您可以使用 DataTransferItem .getAsFileSystemHandle 通过拖放文件获取文件句柄

  • 最初读取或保存文件需要用户批准,并且只能通过用户交互启动。之后,后续的读取和保存不需要批准,直到该站点再次打开。

在此处输入图像描述

  • 文件句柄 可以 保存在页面中(因此,如果您正在编辑“path/to/file.html”,并重新加载页面,它将能够引用该文件)。它们似乎无法被字符串化,因此通过 IndexedDB 之类的东西存储(有关更多信息,请参见此 答案)。使用存储的句柄进行读/写需要用户交互和用户批准。

下面是一些简单的例子。它们似乎不在跨域 iframe 中运行,因此您可能需要将它们保存为 html 文件并在 Chrome/Chromium 中打开它们。

打开和保存,拖放(无外部库):

 <body>
<div><button id="open">Open</button><button id="save">Save</button></div>
<textarea id="editor" rows=10 cols=40></textarea>
<script>
let openButton = document.getElementById('open');
let saveButton = document.getElementById('save');
let editor = document.getElementById('editor');
let fileHandle;
async function openFile() {
  try {
    [fileHandle] = await window.showOpenFilePicker();
    await restoreFromFile(fileHandle);
  } catch (e) {
    // might be user canceled
  }
}
async function restoreFromFile() {
  let file = await fileHandle.getFile();
  let text = await file.text();
  editor.value = text;
}
async function saveFile() {
  var saveValue = editor.value;
  if (!fileHandle) {
    try {
      fileHandle = await window.showSaveFilePicker();
    } catch (e) {
      // might be user canceled
    }
  }
  if (!fileHandle || !await verifyPermissions(fileHandle)) {
    return;
  }
  let writableStream = await fileHandle.createWritable();
  await writableStream.write(saveValue);
  await writableStream.close();
}

async function verifyPermissions(handle) {
  if (await handle.queryPermission({ mode: 'readwrite' }) === 'granted') {
    return true;
  }
  if (await handle.requestPermission({ mode: 'readwrite' }) === 'granted') {
    return true;
  }
  return false;
}
document.body.addEventListener('dragover', function (e) {
  e.preventDefault();
});
document.body.addEventListener('drop', async function (e) {
  e.preventDefault();
  for (const item of e.dataTransfer.items) {
    if (item.kind === 'file') {
      let entry = await item.getAsFileSystemHandle();
      if (entry.kind === 'file') {
        fileHandle = entry;
        restoreFromFile();
      } else if (entry.kind === 'directory') {
        // handle directory
      }
    }
  }
});
openButton.addEventListener('click', openFile);
saveButton.addEventListener('click', saveFile);
</script>
</body>

使用 idb-keyval 存储和检索文件句柄:

存储文件句柄可能很棘手,因为它们不能被取消字符串化, 但显然它们可以与 IndexedDB 一起使用,并且主要与 history.state 一起使用。对于此示例,我们将使用 idb-keyval 访问 IndexedDB 以存储文件句柄。要查看它是否正常工作,请打开或保存文件,然后重新加载页面并按“恢复”按钮。此示例使用来自 https://stackoverflow.com/a/65938910/ 的一些代码。

 <body>
<script src="https://unpkg.com/idb-keyval@6.1.0/dist/umd.js"></script>
<div><button id="restore" style="display:none">Restore</button><button id="open">Open</button><button id="save">Save</button></div>
<textarea id="editor" rows=10 cols=40></textarea>
<script>
let restoreButton = document.getElementById('restore');
let openButton = document.getElementById('open');
let saveButton = document.getElementById('save');
let editor = document.getElementById('editor');
let fileHandle;
async function openFile() {
  try {
    [fileHandle] = await window.showOpenFilePicker();
    await restoreFromFile(fileHandle);
  } catch (e) {
    // might be user canceled
  }
}
async function restoreFromFile() {
  let file = await fileHandle.getFile();
  let text = await file.text();
  await idbKeyval.set('file', fileHandle);
  editor.value = text;
  restoreButton.style.display = 'none';
}
async function saveFile() {
  var saveValue = editor.value;
  if (!fileHandle) {
    try {
      fileHandle = await window.showSaveFilePicker();
      await idbKeyval.set('file', fileHandle);
    } catch (e) {
      // might be user canceled
    }
  }
  if (!fileHandle || !await verifyPermissions(fileHandle)) {
    return;
  }
  let writableStream = await fileHandle.createWritable();
  await writableStream.write(saveValue);
  await writableStream.close();
  restoreButton.style.display = 'none';
}

async function verifyPermissions(handle) {
  if (await handle.queryPermission({ mode: 'readwrite' }) === 'granted') {
    return true;
  }
  if (await handle.requestPermission({ mode: 'readwrite' }) === 'granted') {
    return true;
  }
  return false;
}
async function init() {
  var previousFileHandle = await idbKeyval.get('file');
  if (previousFileHandle) {
    restoreButton.style.display = 'inline-block';
    restoreButton.addEventListener('click', async function (e) {
      if (await verifyPermissions(previousFileHandle)) {
        fileHandle = previousFileHandle;
        await restoreFromFile();
      }
    });
  }
  document.body.addEventListener('dragover', function (e) {
    e.preventDefault();
  });
  document.body.addEventListener('drop', async function (e) {
    e.preventDefault();
    for (const item of e.dataTransfer.items) {
      console.log(item);
      if (item.kind === 'file') {
        let entry = await item.getAsFileSystemHandle();
        if (entry.kind === 'file') {
          fileHandle = entry;
          restoreFromFile();
        } else if (entry.kind === 'directory') {
          // handle directory
        }
      }
    }
  });
  openButton.addEventListener('click', openFile);
  saveButton.addEventListener('click', saveFile);
}
init();
</script>
</body>

补充笔记

Firefox 和 Safari 的支持似乎不太可能,至少在短期内是这样。请参阅 https://github.com/mozilla/standards-positions/issues/154https://lists.webkit.org/pipermail/webkit-dev/2020-August/031362.html

原文由 Steve 发布,翻译遵循 CC BY-SA 4.0 许可协议

您可以只使用 Blob 函数:

 function save() {
  var htmlContent = ["your-content-here"];
  var bl = new Blob(htmlContent, {type: "text/html"});
  var a = document.createElement("a");
  a.href = URL.createObjectURL(bl);
  a.download = "your-download-name-here.html";
  a.hidden = true;
  document.body.appendChild(a);
  a.innerHTML = "something random - nobody will see this, it doesn't matter what you put here";
  a.click();
}

您的文件将保存。

原文由 Awesomeness01 发布,翻译遵循 CC BY-SA 3.0 许可协议

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题