发布于 2026-01-06 5 阅读
0

无建筑模式 无建筑模式

无需建造

无需建造

无需建造

大家好👋

我谈着异地恋,这意味着我每隔几周就要飞去英国。每次坐飞机的时候,我都会想,要是能看看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
Enter fullscreen mode Exit fullscreen mode

我开玩笑的。我们不会用那些东西。我们会尽量避免使用各种工具/依赖项,并降低准入门槛。

我们使用的方法是:

  • LitElement
    在这个项目中,我们将使用 LitElement 作为组件模型。它易于使用、轻量级、接近底层架构,并且充分利用了 Web 组件。

  • @vaadin/router
    Vaadin 路由器体积非常小(< 7kb),开发者体验极佳,我强烈推荐。

  • snowpack
    Snowpack 将帮助我们把各个模块整合在一起,以便于后续开发。

  • es-dev-server
    这是一个由open-wc团队开发的简单易用的开发服务器,适用于现代 Web 开发工作流程。当然,任何 HTTP 服务器都可以,您也可以使用自己的服务器。

就是这样。我们还会使用一些浏览器标准,即:es 模块web 组件import-mapskv-storageservice-worker

那么接下来我们就来安装依赖项:

npm i -S lit-element @vaadin/router
npm i -D snowpack es-dev-server
Enter fullscreen mode Exit fullscreen mode

我们还会添加一个postinstall钩子,package.json用来运行 Snowpack:

  "scripts": {
    "start": "es-dev-server",
    "prepare": "snowpack"
  }
Enter fullscreen mode Exit fullscreen mode

🐭 鼠兔 - 积雪

Pika 是Fred K. Schott的一个项目,旨在将 2014 年那种令人怀念的简洁风格带入 2019 年的 Web 开发中。Fred 做了很多很棒的事情,比如他创建了pika.dev,让你可以轻松地在 npm 上搜索现代 JavaScript 包。他最近还在 DinosaurJS 2019 大会上发表了题为“重塑注册表”的演讲,我强烈推荐大家观看。

snowpackSnowpack 更进一步。如果我们运行 `npm install` snowpack,它会将依赖项作为单个 JavaScript 文件安装到新web_modules/目录中。如果你的依赖项在其package.json清单文件中导出 ES“module”入口点,Snowpack 也支持它。如果你有任何传递依赖项,Snowpack 会为依赖项之间共享的任何代码块创建单独的代码块。简直易如反掌。它甚至还支持tree shaking

这意味着,在我们的例子中,输出结果将类似于:

└─ web_modules/
   ├─ lit-element.js
   └─ @vaadin
        └─ router.js
Enter fullscreen mode Exit fullscreen mode

太棒了!就是这样。我们已经将所有依赖项打包成单独的 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>
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

开局不错。我们来看看目前在浏览器中的显示效果如何,启动服务器,打开浏览器……这是什么?报错了?

错误

我的天哪。

我们才刚刚开始。好了,让我们来看看。问题在于我们的模块说明符是空的。它们是空的模块说明符。这意味着没有指定路径,没有文件扩展名,它们只是……非常空。我们的浏览器不知道该如何处理这种情况,所以会抛出错误。

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,或者一个开发服务器,它可以将裸模块说明符重写成浏览器能理解的形式,从而加载我们的导入。但这也就意味着我们需要引入一大堆工具,深入研究配置,而我们想尽量保持简洁。我们只想写代码!为了解决这个问题,我们将来要了解一下导入映射(import maps)

import导入映射(Import Maps)是一项新提案,它允许您控制 JavaScript 导入的行为。通过使用导入映射,我们可以控制 JavaScript语句和表达式获取哪些 URL import(),并且允许在非导入上下文中重用此映射。这在以下几个方面非常有用:

  • 允许我们的裸模块规范执行
  • 提供备用方案,以便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>
Enter fullscreen mode Exit fullscreen mode

如果我们回到浏览器并刷新页面,就不会再出现错误,我们应该能<h1>Hello world!</h1>在屏幕上看到我们的结果。

导入映射是一个非常有趣的新标准,绝对值得您密切关注。如果您有兴趣尝试使用导入映射,并基于yarn.lock文件生成自己的导入映射,可以试试我们的open-wc import-maps-generate包,尽情探索。我非常期待看到大家将如何结合导入映射进行开发。

📡 服务人员

好了,我们稍微跳过一些步骤。我们已经配置好了依赖项,路由器也设置好了,并且调用了一些 API 从 Reddit 获取数据并将其显示在屏幕上。详细讲解所有代码超出了这篇博文的范围,但请记住,如果您想了解实现细节,可以在GitHub 代码库中找到所有代码。

因为我们开发这款应用是为了在飞机上阅读 Reddit 帖子,所以如果我们的应用能够离线运行,并且能够保存一些帖子以便阅读,那就太好了。

服务人员

Service Worker 是一种在后台运行的 JavaScript Worker。你可以把它想象成位于网页和网络之间。每当你的网页发出请求时,它都会首先经过 Service Worker。这意味着我们可以拦截请求并对其进行处理!例如,我们可以让请求通过网络获取响应,并在响应返回后将其缓存,以便在离线状态下使用缓存的数据。我们还可以使用 Service Worker 来预缓存资源。这意味着我们可以预缓存应用程序离线运行所需的任何关键资源。如果没有网络连接,我们可以回退到已缓存的资源,从而确保应用程序仍然可以运行(尽管是离线的)。

如果你对渐进式 Web 应用和 Service Worker 感兴趣,我强烈推荐你阅读Jake Archibald《离线 Cookbook》,以及Jad Joubran的这套视频教程

接下来,我们来实现一个 Service Worker。在我们的代码中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>
Enter fullscreen mode Exit fullscreen mode

我们还会sw.js向项目根目录添加一个文件。接下来,我们将预缓存应用程序的资源,而 Snowpack 让这一切变得非常简单。请看 Service Worker 文件中的安装处理程序:

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',
      ]);
    })
  );
});
Enter fullscreen mode Exit fullscreen mode

你会发现我们完全掌控着自己的资产,而且我们还有一份清晰明了的文件清单,列出了离线工作所需的文件。

📴 即将离线

没错。既然我们已经缓存了资源以便离线使用,那么如果我们能保存一些文章以便在离线时阅读,那就太好了。条条大路通罗马,但既然我们想冒险尝试一下,那就选择:Kv-storage!

📦 内置模块

这里有几点需要说明。Kv-storage 是一个内置模块。内置模块与普通的 JavaScript 模块非常相似,区别在于它们随浏览器一起发布。值得注意的是,虽然内置模块随浏览器一起发布,但它们并不暴露在全局作用域中,而是使用命名空间进行隔离std:(没错,就是这样)。这样做有几个优点:它们不会增加启动新的 JavaScript 运行时环境(例如新标签页、工作线程或 Service Worker)的开销,除非被实际导入,否则不会占用任何内存或 CPU 资源,并且还能避免与现有代码发生命名冲突。

另一个有趣(尽管有些争议)的内置模块提案是std-toast元素和std-switch元素。

🗃 Kv-存储

好了,说完这些,我们来谈谈kv-storage。kv -storage(或称“键值存储”)与localStorage非常相似,只有一些主要区别,它是在IndexedDB之上构建的。

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
})();
Enter fullscreen mode Exit fullscreen mode

注意我们是如何从……导入的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;
}
Enter fullscreen mode Exit fullscreen mode

现在,如果我们点击“保存以供离线使用”按钮,然后转到开发者工具的“应用程序”选项卡,我们可以看到一个kv-storage:saved-posts包含此帖子的 JSON 数据的文件:

节省

如果我们返回搜索页面,就会看到一个已保存帖子的列表,其中就包括我们刚刚保存的帖子:

已保存

🔮 聚酯纤维填充物

太好了。不过,我们接下来会遇到另一个问题。冒险固然刺激,但也危险重重。我们遇到的问题是,截至撰写本文时,kv-storage 在 Chrome 中仅通过一个标志位启用。这显然不太理想。幸运的是,我们有一个 polyfill 可用,同时我们还能借此机会展示 import-maps 的另一个实用功能:polyfill!

首先,我们来安装 kv-storage-polyfill:

npm i -S kv-storage-polyfill

请注意,我们的prepare钩子会再次为我们运行 Snowpack。

让我们将以下内容添加到我们的导入映射中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>
Enter fullscreen mode Exit fullscreen mode

所以这里发生的情况是,每当/web_modules/kv-storage-polyfill.js请求或导入时,浏览器会首先尝试查看是否std:kv-storage可用;但是,如果失败,则会加载/web_modules/kv-storage-polyfill.js其他内容。

所以在代码中,如果我们导入:

import { StorageArea } from '/web_modules/kv-storage-polyfill.js';
Enter fullscreen mode Exit fullscreen mode

接下来会发生这样的事:

"/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
]
Enter fullscreen mode Exit fullscreen mode

🎉 结论

现在我们应该拥有一个简单易用、依赖项极少的 PWA。这个项目有一些可以挑剔的地方,而且这些挑剔可能都很有道理。例如,我们或许可以不用 Snowpack,但它的确让我们的工作轻松了不少。你也可以提出添加一个简单的 Webpack 配置,但那样就偏离了重点。重点在于打造一个有趣的应用程序,同时使用一些最新功能,避免使用一些时髦的术语,并降低入门门槛。正如 Fred Schott 所说:“在 2019 年,你应该因为想用而使用打包工具,而不是因为需要而使用它。”

不过,如果你对吹毛求疵感兴趣,你可以阅读这篇关于使用 Webpack、Pika 和 buildless 的精彩讨论,你会从Webpack 核心团队的Sean Larkinn以及Pika 的创建者Fred K. Schott那里获得一些很棒的见解。

希望您喜欢这篇博文,也希望您从中有所收获,或者发现了一些值得关注的新朋友。目前这个领域正在发生许多令人兴奋的变化,我希望这篇文章能让您和我一样感到激动。如果您有任何问题、评论、反馈或建议,欢迎随时在推特上联系我,我的账号是@passle_@openwc。也别忘了访问open-wc.org 😉。

荣誉提名

在结束这篇博客之前,我想特别推荐几位非常有趣的人,他们正在做一些很棒的事情,你们不妨关注一下。

首先要感谢Guy Bedford,他编写了es-module-shims,顾名思义,它为 ES 模块和导入映射提供了兼容层。在我看来,这真是一项了不起的成就,它让我能够使用一些尚未在所有浏览器上实现的新技术。

如果你对更多类似内容感兴趣,你绝对应该看看Luke Jackson的演讲《别构建那个应用!没有 webpack,不用担心 🤓🤙》,正如 Luke 会说的那样。

我还要感谢Benny PowersLars den Bakker的宝贵意见和反馈。

文章来源:https://dev.to/open-wc/on-the-bleeding-edge-3cb8