我处于异地恋。 这意味着我每隔几周就要坐飞机去英国一次,每次我都在飞机上想着,如果能看一些 Reddit 帖子就好了。 我可以找一个 Reddit 应用,让我可以缓存帖子以供离线使用(我相信那里有一个),或者我可以利用这个机会自己写一些东西,并享受使用最新的技术和网络标准的乐趣!
最重要的是,围绕着我称之为“无构建”的主题,有很多讨论,我认为这是一种非常迷人的发展,其中生产项目是在不使用构建过程(如打包程序)的情况下创建的。
这篇文章也是对网络社区中一些很棒的人的致敬,他们让一些伟大的事情成为可能。 当我们继续前进时,我会链接到所有这些内容。 请注意,这不是一个分步教程,但如果你想查看代码,你可以在 GitHub 上找到完成的项目。
我们的最终结果应该看起来像这样

让我们深入了解并安装一些依赖项
npm i @babel/core babel-loader @babel/preset-env @babel/preset-react webpack webpack-cli react react-dom redux react-redux html-webpack-plugin are-you-tired-yet html-loader webpack-dev-server
我开玩笑的。
我们不会使用任何这些东西。
我们将尽量避免使用尽可能多的工具和依赖项,以降低入门门槛。 我们将要使用的是
- LitElement – LitElement 是我们的组件模型。 它易于使用、轻量级、接近底层,并且利用了 Web Components。
- @vaadin/router – 这是一款非常小巧(< 7kb)的路由器,拥有非常棒的开发体验,我强烈推荐它。
- @pika/web – 这将帮助我们将模块组合在一起,方便开发。
- es-dev-server – 这是一款用于现代 Web 开发工作流的简单开发服务器,由我们在 open-wc 创建。 虽然任何 HTTP 服务器都可以工作,但您可以随意使用自己的服务器。
就这样! 我们还将使用一些浏览器标准,即: ES 模块、 Web Components、 导入地图、 KV 存储 和 服务工作者。
让我们继续安装我们的依赖项
npm i -S lit-element @vaadin/router
npm i -D @pika/web es-dev-server
我们还将在我们的 package.json
中添加一个 postinstall
钩子,它将为我们运行 Pika
"scripts": {
"start": "es-dev-server",
"postinstall": "pika-web"
}
🐭 Pika
Pika 是 Fred K. Schott 的一个项目,旨在将 2014 年怀旧的简单性带到 2019 年的 Web 开发中。 Fred 在做各种很棒的事情。 例如,他创建了 pika.dev,它允许您轻松搜索 npm 上的现代 JavaScript 包。 他最近还在 DinosaurJS 2019 上发表了他的演讲 重新构想注册表,我强烈推荐您观看。
Pika 进一步发展了这些。 如果我们运行 pika-web
,它将把我们的依赖项作为单个 JavaScript 文件安装到一个新的 web_modules/
目录中。 如果您的依赖项在其 package.json
清单中导出一个 ES “模块” 入口点,Pika 会支持它。 如果你有任何传递依赖项,Pika 会为你的依赖项之间的任何共享代码创建单独的块。
这意味着在我们的例子中,我们的输出将看起来像
└─ web_modules/
├─ lit-element.js
└─ @vaadin
└─ router.js
太棒了! 就这样。 我们的依赖项已准备就绪,作为单个 JavaScript 模块文件,这将在本文后面为我们带来很大的便利,敬请关注!
📥 导入地图
好的! 现在我们已经整理好了依赖项,让我们开始工作。 我们将创建一个 index.html
,它将看起来像这样
<html>
<!-- head, etc. -->
<body>
<reddit-pwa-app></reddit-pwa-app>
<script src="./src/reddit-pwa-app.js" type="module"></script>
</body>
</html>
以及 reddit-pwa-app.js
import { LitElement, html } from 'lit-element';
class RedditPwaApp extends LitElement {
// ...
render() {
return html`
<h1>Hello world!</h1>
`;
}
}
customElements.define('reddit-pwa-app', RedditPwaApp);
我们已经取得了良好的开端。 让我们尝试在浏览器中查看它现在的效果,因此让我们启动我们的服务器,打开浏览器,然后… 这是什么? 错误?

哦,天哪。
我们才刚刚开始。 好吧,让我们看看。 这里的问题是我们模块标识符是裸的。 它们是裸模块标识符。 这意味着没有指定任何路径,没有文件扩展名,它们只是… 很裸。 我们的浏览器不知道如何处理它,所以它会抛出一个错误。
import { LitElement, html } from 'lit-element'; // <-- bare module specifier
import { Router } from '@vaadin/router'; // <-- bare module specifier
import { foo } from './bar.js'; // <-- not bare!
import { html } from 'https://unpkg.com/lit-html'; // <-- not bare!
当然,我们可以为此使用一些工具,如 webpack 或 rollup,或者一个重写裸模块标识符为浏览器有意义的内容的开发服务器,这样我们就可以加载我们的导入内容。 但这意味着我们必须引入一堆工具,深入配置,而我们正试图保持最小的操作。 我们只想编写代码! 为了解决这个问题,我们将研究 导入地图。
导入地图是一个新的提案,它允许您控制 JavaScript 导入的行为。 使用导入地图,我们可以控制 JavaScript import
语句和 import()
表达式获取哪些 URL,并且允许此映射在非导入上下文中重复使用。 这对以下几个方面非常有用
- 它允许我们的裸模块标识符工作。
- 它提供了一个回退解析,以便
import $ from "jquery";
可以先尝试转到 CDN,但在 CDN 服务器关闭的情况下,回退到本地版本。 - 它能够实现对(或其他控制) 内置模块 的 polyfill。 (稍后会详细介绍,请耐心等待!)
- 解决 嵌套依赖项问题。 (请阅读这篇博客!)
听起来很不错,对吧? 导入地图目前在 Chrome 75+ 中可用,但需要启用一个标志,有了这些知识,让我们转到我们的 index.html
,并将导入地图添加到我们的 <head>
中
<head>
<script type="importmap">
{
"imports": {
"@vaadin/router": "/web_modules/@vaadin/router.js",
"lit-element": "/web_modules/lit-element.js"
}
}
</script>
</head>
如果我们回到我们的浏览器,并刷新我们的页面,我们将不再出现任何错误,并且应该在屏幕上看到我们的 <h1>Hello world!</h1>
。
导入地图是一个非常有趣的新的标准,你一定要关注它。 如果你有兴趣尝试它们,并根据 yarn.lock
文件生成自己的导入地图,你可以尝试我们的 open-wc import-maps-generate 包,并随意玩耍。 我真的很期待看到人们结合导入地图会开发出什么。
📡 服务工作者
好的,我们将提前一点时间。 我们的依赖项已经可以工作了,我们的路由器已经设置好了,我们还做了一些 API 调用来获取 Reddit 的数据并在我们的屏幕上显示它。 详细介绍所有代码超出了本文的范围,但请记住,您可以在 GitHub 仓库 中找到所有代码,如果您想阅读实现细节。
由于我们正在制作这款应用,以便我们可以在飞机上阅读 Reddit 线程,因此如果我们的应用能够离线工作,并且如果我们能够以某种方式保存一些帖子以供阅读,那就太好了。

服务工作者是一种在后台运行的 JavaScript 工作者。 你可以把它想象成位于网页和网络之间的位置。 每当你的网页发出请求时,它都会先经过服务工作者。 这意味着我们可以拦截请求,并对它进行操作! 例如,我们可以让请求通过网络获取响应,并在响应返回时将其缓存,以便以后在可能处于离线状态时使用该缓存数据。 我们还可以使用服务工作者来预缓存我们的资产。 这意味着我们可以预缓存我们的应用可能需要正常工作的所有关键资产,以便能够离线工作。 如果我们没有网络连接,我们可以简单地回退到我们缓存的资产,并且仍然拥有一个可用的(尽管是离线的)应用程序。
如果你有兴趣了解更多关于渐进式 Web 应用和服务工作者的知识,我强烈推荐你阅读由 Jake Archibald 编写的 离线烹饪手册,以及由 Jad Joubran 制作的这个视频教程 系列。
让我们继续实现一个服务工作者。 在我们的 index.html
中,我们将添加以下代码段
<script>
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('./sw.js').then(() => {
console.log('ServiceWorker registered!');
}, (err) => {
console.log('ServiceWorker registration failed: ', err);
});
});
}
</script>
我们还将在项目的根目录中添加一个 sw.js
文件。 因此,我们即将预缓存我们的应用的资产,而 Pika 正是在这里为我们简化了生活。 如果你查看服务工作者文件中安装处理程序
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHENAME).then((cache) => {
return cache.addAll([
'/',
'./web_modules/lit-element.js',
'./web_modules/@vaadin/router.js',
'./src/reddit-pwa-app.js',
'./src/reddit-pwa-comment.js',
'./src/reddit-pwa-search.js',
'./src/reddit-pwa-subreddit.js',
'./src/reddit-pwa-thread.js',
'./src/utils.js',
]);
})
);
});
你会发现我们完全控制着我们的资产,并且有一个干净整洁的文件列表,我们需要这些文件才能离线工作。
📴 离线
好的。现在我们已经缓存了资产以供离线使用,如果我们能够实际保存一些帖子以便离线阅读,那就太好了。通往罗马的路有很多条,但由于我们现在正在尝试一些边缘技术,所以我们将使用:Kv-storage!
📦 内置模块
这里有几件事要讨论。Kv-storage 是一个内置模块。内置模块与普通的 JavaScript 模块非常相似,只是它们与浏览器一起提供。值得注意的是,虽然内置模块与浏览器一起提供,但它们不会暴露在全局作用域中,并且使用std:
命名空间(是的,真的)。这有几个优点:它们不会给启动新的 JavaScript 运行时环境(例如新标签页、工作线程或服务工作线程)增加任何开销,并且除非它们被实际导入,否则它们不会消耗任何内存或CPU,并且可以避免与现有代码的命名冲突。
另一个有趣的,如果不是有点有争议的,作为内置模块的提案是std-toast 元素,以及std-switch 元素。
🗃 Kv-storage
好了,现在我们来谈谈kv-storage。Kv-storage(或“键值存储”)建立在 IndexedDB 之上,与 localStorage 非常相似,只是存在一些主要区别。
Kv-storage 的动机是 localStorage 是同步的,这会导致性能低下和同步问题。它还仅限于字符串键值对。替代方案 IndexedDB 则… 难以使用。之所以很难使用,是因为它早于 Promise,这会导致… 好吧,很糟糕的开发者体验。很不方便。然而,Kv-storage 很有趣、异步,并且易于使用!考虑以下示例
import { storage, /* StorageArea */ } from "std:kv-storage";
(async () => {
await storage.set("mycat", "Tom");
console.log(await storage.get("mycat")); // Tom
})();
注意我们是从 std:kv-storage
导入的吗?这个导入说明符也是裸露的,但在这种情况下是可以的,因为它实际上与浏览器一起提供。
很不错。我们可以完美地使用它来添加一个“离线保存”按钮,并将 Reddit 线程的 JSON 数据简单地存储起来,并在需要时获取它。
// reddit-pwa-thread.js:52:
const savedPosts = new StorageArea("saved-posts");
// ...
async saveForOffline() {
await savedPosts.set(this.location.params.id, this.thread); // id of the post + thread as json
this.isPostSaved = true;
}
因此,现在,如果我们点击“离线保存”按钮,然后转到 DevTools 的“应用程序”选项卡,我们可以看到一个包含此帖子 JSON 数据的 kv-storage:saved-posts
。

如果我们返回搜索页面,我们将看到一个保存的帖子列表,其中包含我们刚刚保存的帖子。

🔮 填充
很棒。但是,我们很快就会遇到另一个问题。活在边缘很有趣,但也危险。我们现在遇到的问题是,截至撰写本文之时,kv-storage
仅在 Chrome 中实现,而且需要使用标志才能启用。这不是很好。幸运的是,有一个 polyfill 可用,同时我们也可以展示 import-maps 的另一个非常有用的功能:填充!
首先,让我们安装 kv-storage-polyfill
npm i -S kv-storage-polyfill
请注意,我们的 postinstall
钩子将再次为我们运行 Pika。
让我们还将以下内容添加到 index.html
中的导入映射中
<script type="importmap">
{
"imports": {
"@vaadin/router": "/web_modules/@vaadin/router.js",
"lit-element": "/web_modules/lit-element.js",
"/web_modules/kv-storage-polyfill.js": [
"std:kv-storage",
"/web_modules/kv-storage-polyfill.js"
]
}
}
</script>
这里发生的事情是,每当请求或导入 /web_modules/kv-storage-polyfill.js
时,浏览器将首先尝试查看 std:kv-storage
是否可用;但是,如果失败,它将改为加载 /web_modules/kv-storage-polyfill.js
。
因此,在代码中,如果我们导入
import { StorageArea } from '/web_modules/kv-storage-polyfill.js';
将会发生以下情况
"/web_modules/kv-storage-polyfill.js": [ // when I'm requested
"std:kv-storage", // try me first!
"/web_modules/kv-storage-polyfill.js" // or fallback to me
]
🎉 结论
现在我们应该有一个简单、功能齐全的,并且依赖项最少的PWA。这个项目有一些我们可能会抱怨的细节,而且它们很可能是合理的。例如,我们可能不需要使用 Pika,但它确实让我们的生活变得非常轻松。你也可以说我们应该添加一个 webpack 配置,但这会偏离主题。这里的重点是创建一个有趣的应用程序,同时使用一些最新功能、抛出一些流行语,并降低入门门槛。正如 Fred Schott 所说:“在 2019 年,你应该使用捆绑器是因为你想使用,而不是因为你需要使用。”
但是,如果你对这些细节感兴趣,你可以阅读关于使用 webpack vs. Pika vs. 无构建的精彩讨论,你将从 webpack 核心团队成员Sean Larkinn 以及 Pika 的创建者Fred K. Schott 那里获得一些宝贵的见解。
我希望你喜欢这篇文章,也希望你学到了一些东西,或者发现了你想要关注的一些有趣的人。这个领域现在正在发生很多激动人心的发展,我希望你也像我一样对它们感到兴奋。如果你有任何问题、评论、反馈或细节,请随时通过推特与我联系,我的推特账号是@passle_ 或@openwc,别忘了查看open-wc.org 😉。
值得一提的人
我想向一些非常有趣的人致敬,他们正在做一些很棒的事情,你可能需要关注一下。
- Guy Bedford,他编写了es-module-shims,它,嗯,对ES 模块和导入映射进行了填充。在我看来,这是一项了不起的壮举,它让我能够实际使用一些尚未在所有浏览器中实现的新技术。
- Luke Jackson 的演讲不要构建那个应用程序! 没有 webpack,不用担心 🤓🤙,正如 Luke 所说。
- 感谢 Benny Powers 和 Lars den Bakker 提供的宝贵评论和反馈。
非常有见地的文章!
我最近一直在探索如何将 es 模块与 TypeScript 一起使用,并遇到了裸导入问题。到目前为止,我的解决方法是包含文件扩展名,这似乎比包含 require.js 或 webpack 等模块加载器更好的权衡。
导入映射(对我来说完全是全新的)似乎正是我一直在寻找的东西,我真的希望这个功能能够在所有浏览器中得到支持。
感谢分享!
太棒了,读起来很有趣。我一开始是运行所有代码,但最后只是读完了剩下的部分。
我想知道你是如何平衡探索和学习不受支持的功能与学习当前已实现的功能的。显然,人们探索那些处于标志后面的功能很好,这样它们最终就可以成为技术栈的一部分,但你如何证明时间上的合理性,因为很可能它们永远不会被实现呢?
嘿,Matt,感谢你的评论!
对我来说,有两个原因
– 我很喜欢它
– 通过早期的实验,我可能会发现一些怪癖,甚至错误,或者只是从用户/开发者角度获得一般性的反馈,这些在标准讨论中总是非常受欢迎的。
很棒的文章!以前不知道所有这些新的标准和提案。很高兴听到这些消息,因为它们看起来很有希望。