使用Javascript将HTML本地保存

34

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

似乎使用cookie、localStorage或允许用户下载文件(通过"保存..."对话框或浏览器默认下载文件夹)是JavaScript保存数据到本地的唯一方式。

但是,在特定情况下,当文件通过类似file:///D:/test/index.html这样的本地URL进行访问时(而不是通过Internet),是否有可能在本地浏览HTML文件时写入数据?(不需要任何服务器语言,甚至没有任何服务器:只是本地浏览HTML文件)

例如,点击这里的保存按钮,是否可能:

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

如何才能使HTML文件(通过file:///D:/test/index.html访问)被其新内容覆盖?(即在按下SAVE时应更新本地HTML文件)。


enter image description here


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

注:我想要能够静默保存,而不是提供一个下载/保存对话框,在该对话框中,用户必须选择下载位置,然后出现"您确定要覆盖吗"等提示。


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


2
我相信你唯一能做到的方法就是将内容提交给某些服务器端处理(例如PHP,Python等),并让本地脚本更新文件。 - Jacob Ewing
1
@JacobEwing 而且没有服务器端处理?我的意思是本地浏览HTML文件,即浏览器打开c:\myproject\index.html,没有Apache服务器/没有PHP服务器。 - Basj
1
如果您在浏览器中执行脚本(即使从本地计算机加载),那么就无法告诉它修改本地文件(这是浏览器防止这种情况发生,而不是服务器)。您可能可以使用其他软件来执行它,在这种情况下,我不知道还有什么限制。https://dev59.com/qnA85IYBdhLWcg3wF_m0 中有一些更多的信息可能会有所帮助。 - Jacob Ewing
1
@Basj:关于node-webkit,它可以生成可在终端机上运行而无需安装node-webkit本身的程序。HTA适用于Windows系统,并且您可以使用FSO读写文件,只要文件有一个<APPLICATION>标签即可。还有Chrome打包应用和Firefox应用,以及Cordova。 - dandavis
1
@kzaiwo 我刚刚发起了一个悬赏,看看是否有新的方法。 - Basj
显示剩余16条评论
13个回答

30
你可以直接使用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();
}

并且您的文件将会被保存。

保存为怎么样?我喜欢这个解决方案,很整洁。谢谢。 - Symbolic
2
最好添加一个htmlContent示例。 var htmlContent = [ "<head><meta charset='utf-8'><title>测试</title></head>", "<style>.container{max-width: 940px;margin: 0 auto;}</style>", "<body><div class="container">'这里是内容'</div></body>" ]; - Oni
2
仍然要求我确认下载。 - Rexcirus

9
来自W3C文件API标准的规范答案:

用户代理应提供一个API,向脚本公开上述功能。每当与文件系统进行交互时,用户都会通过UI得到通知,从而使用户完全能够取消或中止事务。用户将被通知任何文件选择,并可以取消这些选择。没有调用这些API会在没有用户干预的情况下静默发生。

基本上,由于安全设置的原因,每次下载文件时,浏览器都会确保用户确实想要保存该文件。浏览器并不真正区分您计算机上的JavaScript和Web服务器上的JavaScript。唯一的区别是浏览器如何访问文件,因此将页面存储在本地不会有任何区别。

解决方法: 然而,你可以将<div>的innerHTML存储在cookie中。当用户回来时,你可以从cookie中重新加载它。虽然它不是真正意义上将文件保存到用户的计算机上,但它应该具有覆盖文件的效果。当用户回来时,他们将看到他们上次输入的内容。缺点是,如果用户清除了他们的网站数据,他们的信息将丢失。由于忽略用户请求清除本地存储也是一个安全问题,所以确实没有办法绕过它。

然而,你也可以这样做:

  • 使用Java小程序
  • 使用其他类型的小程序
  • 创建桌面(非基于Web的)应用程序
  • 只需记得在清除网站数据时保存文件。当你退出页面时,你可以创建一个弹出窗口提醒你,甚至为你打开保存窗口。

使用cookie:可以在本地页面上使用JavaScript cookie。只需将其放入文件中并在浏览器中打开即可。

<!DOCTYPE html>
<html>

<head>
</head>

<body>
  <p id="timesVisited"></p>
  <script type="text/javascript">
    var timesVisited = parseInt(document.cookie.split("=")[1]);
    if (isNaN(timesVisited)) timesVisited = 0;
    timesVisited++;
    document.cookie = "timesVisited=" + timesVisited;
    document.getElementById("timesVisited").innerHTML = "You ran this snippet " + timesVisited + " times.";
  </script>
</body>

</html>

好的,谢谢,这正是我所想的(由于安全原因不可能)。如果可能的话,我会为个人使用制作一个简单的本地HTML记事本:http://gget.it/59b78rl1/NFBF33C.HTML(当然还有一些JavaScript的功能)。由于无法在一个点击中保存,所以这个项目是不可能完成的 :( - Basj
到目前为止,我确实使用了 localStorage。但是失去所有数据的事实,并且必须记住在清除历史记录时导出记事本(带有某些下载文件功能),使其实际上变得不太有趣和无用...不幸的是... - Basj
@Basj 还有很多其他选项,我会在我的答案中添加它们。 - James Westman
是的@kittycat3141,我想添加自己特定的功能,请查看我的项目:http://bigpicture.bi/demo。它完全作为在线服务运行(当然有服务器端语言等),但我想尝试将其发布为100%离线工具:这就是为什么我需要能够本地“保存”的原因。有什么想法吗? - Basj
@Basj 如果你想要创建一个离线工具,你可能需要考虑使用Java应用程序等方式。此外,这样你可以将图像和其他资源放在同一个文件中。ZIP文件等东西对于用户来说有点混乱。 - James Westman
显示剩余6条评论

8

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

相对较新的非标准文件系统访问API(不要与早期的文件和目录条目API文件系统API混淆)似乎是在2019/2020年在Chromium / Chrome中引入的,并且在Firefox或Safari中没有支持。

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

您可以在https://web.dev/file-system-access/阅读更多关于这个新API的内容。它旨在用于创建更强大的Web应用程序。

关于此API有几点需要注意:

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

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

  • 最初读取或保存文件需要用户批准,并且只能通过用户交互来启动。之后,除非再次打开网站,否则不需要批准即可进行后续读取和保存。

    enter image description here

  • 文件的句柄可以保存在页面中(因此,如果您正在编辑本地文件'/path/to/file.txt'并重新加载页面,则可以引用该文件)。它们似乎无法被字符串化,因此通过类似IndexedDB的方式进行存储(有关更多信息,请参见this answer)。使用存储的句柄进行读写操作需要用户交互和用户批准。

这里有一些简单的例子。它们似乎不能在跨域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://dev59.com/I1EG5IYBdhLWcg3wcN3q#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>

附加说明

火狐和Safari的支持似乎在短期内不太可能。请参见https://github.com/mozilla/standards-positions/issues/154https://lists.webkit.org/pipermail/webkit-dev/2020-August/031362.html


1
很棒的解决方案@Steve!最后一个问题,你知道是否可以通过拖放文件到浏览器窗口来打开文件吗?这将使其成为在浏览器中创建文本编辑器等完美解决方案。 - Basj
2
@Basj,通过DataTransferItem.getAsFileSystemHandle(在这里也提到了https://web.dev/file-system-access/#drag-and-drop-integration),应该是可以实现的。我有机会测试后会更新我的答案。 - Steve

3

是的,这是可能的。

在您的示例中,您已经使用了ContentEditable,而大多数该属性的教程都有某种形式的localStrorage示例,例如http://www.html5tuts.co.uk/demos/localstorage/

在页面加载时,脚本应检查localStorage是否存在数据,如果是,则填充元素。单击保存按钮(或在链接示例中自动使用模糊和聚焦)可将内容中的任何更改保存在localStorage中。此外,您可以使用此代码段检查用户的在线或离线状态,并根据状态修改逻辑:

// check if online/offline
// http://www.kirupa.com/html5/check_if_internet_connection_exists_in_javascript.htm
function doesConnectionExist() {
    var xhr = new XMLHttpRequest();
    var file = "http://www.yoursite.com/somefile.png";
    var randomNum = Math.round(Math.random() * 10000);

    xhr.open('HEAD', file + "?rand=" + randomNum, false);

    try {
        xhr.send();

        if (xhr.status >= 200 && xhr.status < 304) {
            return true;
        } else {
            return false;
        }
    } catch (e) {
        return false;
    }
}

编辑:更高级的localStorage版本是Mozilla localForage,它允许存储字符串以外的其他类型数据。


我已经使用localStorage进行了测试,但是当您清除浏览器历史记录时,修改内容会丢失...因此,对于在本地运行没有任何服务器(没有apache,没有php)的HTML/JS记事本的目的来说,这不是一个真正的解决方案。 - Basj
1
@Basj 好的,我现在看到了你对kittycat3141答案的评论。如上所述,任何浏览器存储技术(cookies、localStorage或appcache)都可以离线工作,但如果用户决定清除缓存和浏览数据,则不会持久化。也许你应该考虑将第三方存储服务集成到你的应用程序中,并使用它来存储数据文件?Dropbox(https://www.dropbox.com/developers/dropins)和Google Drive(https://developers.google.com/drive/web/)都有非常好的API可供使用。 - Teo Dragovic

3
您可以使用FileSystem-API和webkit保存文件并使其持久化。您需要使用Chrome浏览器,这不是标准技术,但我认为它正好能满足您的需求。以下是一个很棒的教程,展示了如何实现这一点:http://www.noupe.com/design/html5-filesystem-api-create-files-store-locally-using-javascript-webkit.html
而且,为了证明它与主题相关,它开始展示如何使文件保存持久化...
window.webkitRequestFileSystem(window.PERSISTENT , 1024*1024, SaveDatFileBro);

1
使用这种技术和API,可以在Chrome中打开file:///D:/test/index.html并写入到file:///D:/test/test.txt吗? - Basj

1
将您的HTML内容转换为数据URI字符串,并将其设置为锚元素的href属性。不要忘记指定一个文件名作为download属性。
以下是一个简单的例子:
<a>click to download</a>
<script>
    var anchor = document.querySelector('a');
    anchor.setAttribute('download', 'example.html');
    anchor.setAttribute('href', 'data:text/html;charset=UTF-8,<p>asdf</p>');
</script>

只需在您的浏览器中尝试,无需服务器。


1
谢谢@LeoDeng,但是我想要能够静默保存,而不是弹出一个下载对话框让用户选择下载位置。还有其他的想法吗? - Basj
那取决于用户的浏览器设置。如果你打算绕过它,据我所知是不可能的。 - Leo
1
刚刚解决了一个编码方面的小问题,值得在这里提一下。在 data url 中的 html 之前,添加 escape('\xEF\xBB\xBF'),这样它就真正变成了 UTF-8,语言特定的字符(在我的情况下是土耳其语)将会正确显示。 - Erdogan Kurtur

1

看一下这个 :) 使用Javascript/jQuery下载文件 里面应该有你需要的一切。如果你还需要帮助,或者这不是你需要的解决方案,请告诉我 ;)


谢谢,但这不是我需要的。我正在寻找一些东西,而不必显示下载对话框。出于特定原因,请参阅我在kittycat3141答案中的最后评论。 - Basj

1

是的,这是可能的。以下是一个示例:

TiddlyFox:允许通过附加组件修改本地文件。(源代码) (扩展页面):

TiddlyFox 是 Mozilla Firefox 的一个扩展,它使 TiddlyWiki 直接保存更改到文件系统。

Todo.html:一个可以保存编辑内容的HTML文件。目前仅在Internet Explorer中可用,并且在第一次打开文件时需要确认某些安全对话框。(源代码) (功能演示)。

确认 Todo.html 实际上将更改保存到本地的步骤:

  1. todo.html保存到本地硬盘。
  2. 使用Internet Explorer打开。接受所有安全提示框。
  3. 输入命令todo add TEST(todo.html模拟了todo.txt-CLI的命令行界面)。
  4. 检查todo.html文件是否添加了“TEST”。

注意事项:没有跨平台的方法。我不确定这些方法还能存在多久。当我开始我的todo.html项目时,有一个名为twFile的jQuery插件,可以使用四种不同的方法(ActiveX、Mozilla XUL、Java applet、Java Live Connect)在各种浏览器中加载/保存本地文件。除了ActiveX,由于安全问题,浏览器已禁用了所有这些方法。


1
如果您不介意代码在默认浏览器范围之外运行,并且只支持Windows,那么HTA可以轻松满足静默保存而不提示的要求。
下面的代码并没有使用太多HTA特定的功能,但仍然使用了微软特定的东西,比如ActiveXObject("Scripting.FileSystemObject")
<html>

<head>
  <title>Simple Notepad</title>
  <meta http-equiv="X-UA-Compatible" content="IE=9">
  <script>
    document.addEventListener('keydown', function (event) {
      if (event.ctrlKey) {
        if (event.key == 's') {
          var FSo = new ActiveXObject("Scripting.FileSystemObject");
          //see https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/opentextfile-method
          var thisFile = FSo.OpenTextFile(window.location.pathname, 2, true, -1);
          thisFile.Write(document.getElementsByTagName("html")[0].outerHTML);
          thisFile.Close();
          // Comment out the below alert to get truly silent saving.
          alert('Saved Successfully');
          if (event.preventDefault) event.preventDefault();
          return false;
        }
      }
    }, false);
  </script>

</head>

<body contentEditable="true">
  <h1>Press <kbd>CTRL + S</kbd> To Save</h1>
</body>

</html>

它也不是非常丰富的编辑体验,但我认为可以通过添加一些按钮或键盘快捷键来解决。例如,使用CTRL + B使所选文本加粗。它目前没有任何安全检查,但绑定一个事件处理程序到beforeunload应该可以防止意外关闭程序导致的数据丢失。
HTA也有其他缺点。它们不支持ES6(虽然转译是一个选择)。
虽然它有点过时,但如果你不想使用现代Web功能,我认为你会同意它非常实用和可用。
编辑 我忘了提到,HTA必须使用.hta文件扩展名保存,以便将mshta.exe注册为其文件类型处理程序。这是必需的,这样您就可以在Windows资源管理器中双击它轻松打开它。
另请参见 MSDN上的HTML应用介绍 MSDN上的HTML应用参考

只是为了澄清,这不在像浏览器标签那样的沙盒/受限环境中运行,因此不应该出现任何关于它尝试使用“ActiveXObjects”、“FileSystemObject”等的提示。 - Gian Singh Sarao
1
HTA不能通过MS Edge渲染。它们使用MSHTML渲染引擎(与IE使用的相同),因此受限于IE 11的功能。然而,上面的代码将在IE 5文档模式下运行,因为它缺少任何DOCTYPEX-UA-Compatible声明。添加适当的声明以在IE 9、10或11模式下运行 - LesFerch
但问题在于寻找一种方法从沙盒环境的网页中访问本地文件系统。HTA并不能做到这一点。它只是另一种编写本地应用程序的方法,只是碰巧使用HTML/CSS作为其GUI。 - LesFerch
是的。抱歉,我的错误。HTA无法使用MSEdge(即使使用注册表修改)。我现在已经编辑过了。出于某种原因,我认为mshta.exe可以选择要使用的引擎,就像cscript.exe一样,您可以在命令行上使用progId,或者修改注册表以修改默认设置。 - Gian Singh Sarao

0
使用 jsPDF -> https://github.com/MrRio/jsPDF
<div id="content">
     <h3>Hello, this is a H3 tag</h3>
    <p>a pararaph</p>
</div>
<div id="editor"></div>
<button id="cmd">generate PDF</button>

JavaScript

  var doc = new jsPDF();
  var specialElementHandlers = {
      '#editor': function (element, renderer) {
          return true;
      }
  };

  $('#cmd').click(function () {
      doc.fromHTML($('#content').html(), 15, 15, {
          'width': 170,
              'elementHandlers': specialElementHandlers
      });
      doc.save('sample-file.pdf');
  });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接