[Web翻译]无构建开发(1):简介

241 阅读14分钟

原文地址:dev.to/open-wc/dev…

原文作者:dev.to/larsdenbakk…

发布时间:2019年8月4日 ・12分钟阅读

无构建开发:简介 本文是关于无构建开发系列的一部分。

  1. 导言(本文)
  2. es-dev-server
  3. 测试(即将推出)

在这篇文章中,我们探讨了为什么以及是否应该在没有构建步骤的情况下进行开发,并概述了当前和未来的浏览器API,使之成为可能。在后续的文章中,我们将探讨es-dev-server如何帮助我们实现这一点以及如何处理测试。

现代Web开发

在网站开发的早期,我们只需要一个简单的文件编辑器和一个网络服务器。新人很容易理解这个过程并开始制作自己的网页。从那时起,Web开发已经发生了很大的变化:我们用于开发的工具的复杂性和我们在网络上构建的东西的复杂性一样增长。

想象一下,如果你是完全陌生的Web开发进来是什么样子的。

  • 你首先需要学习很多不同的工具 并了解每一个工具是如何改变你的代码的 在它能真正在浏览器中运行之前。
  • 你的IDE和linter很可能不理解这个由朋友推荐给你的框架的语法,所以你需要找到合适的插件组合,让它发挥作用。
  • 如果你想有机会在浏览器中调试你的代码,源码图需要为链中的所有工具正确配置。让它们与你的测试一起工作是另一个故事。
  • 你决定保持简单,不使用typecript。你按照教程去做,但却无法让这个装饰器工作,错误信息也没有帮助。原来您没有按照正确的顺序配置您的babel插件......

这听起来可能很夸张,我知道有非常好的入门项目和教程,但这种经历是很多开发者的共同经历。你自己可能也跳过类似的圈子。

我觉得这真的很可惜。网络的关键卖点之一就是它是一种简单而开放的形式。它应该很容易就能马上上手,不需要很多配置和仪式。

我并不是在批评构建工具本身,它们都有自己的作用和目的。而且在很长一段时间里,使用构建是真正在网络上创建复杂应用的唯一真正方式。Web标准和浏览器的实现只是不支持现代Web开发。构建工具确实帮助推动了Web开发的发展。

但是,浏览器在过去的几年里已经有了很大的改进,在不久的将来会有很多令人兴奋的事情发生。我认为现在是一个很好的时机来考虑我们是否可以取消很大一部分工具的复杂性,至少在开发过程中。也许还不是所有类型的项目,但让我们看看我们能走多远。

在浏览器中加载模块

这不是一个循序渐进的教程,但你可以使用任何Web服务器来跟随任何一个例子。例如来自npm的http-server。使用-c-1运行它来禁用基于时间的缓存。

npx http-server -o -c-1

装入模块

模块可以在浏览器中使用type="module "属性的常规脚本标签加载。我们可以直接内联编写模块代码。

<!DOCTYPE html>
<html>
<head></head>

<body>
  <script type="module">
    console.log('hello world!');
  </script>
</body>

</html>

从这里我们可以使用静态导入来加载其他模块。

<script type="module">
  import './app.js';

  console.log('hello world!');
</script>

请注意,我们需要使用一个显式的文件扩展名,否则浏览器不知道要请求哪个文件。

如果我们使用src属性,同样的事情也会发生。

<script type="module" src="./app.js"></script>

加载依赖关系

我们的代码不会只写在一个文件里。导入初始模块后,我们可以导入其他模块。例如,让我们创建两个新文件。

src/app.js:

import { message } from './message.js';

console.log(`The message is: ${message}`);

src/message.js:

export const message = 'hello world';

将这两个文件放在src目录下,并从index.html中导入app.js。

<!DOCTYPE html>
<html>
<head></head>

<body>
  <script type="module" src="./src/app.js"></script>
</body>

</html>

如果你运行这个并检查网络面板,你会看到两个模块都被加载了。因为导入是相对解析的,所以app.js可以使用相对路径来引用message.js

这看起来微不足道,但它非常有用,也是我们之前使用经典脚本所没有的。我们不再需要在某个中心的地方协调依赖关系,也不需要维护一个基础URL。模块可以声明他们自己的依赖关系,我们可以导入任何模块而不知道他们的依赖关系是什么。浏览器会负责请求正确的文件。

动态导入

当构建任何严肃的Web应用程序时,我们通常需要做某种形式的懒惰加载以获得最佳性能。像我们之前看到的静态导入不能有条件地使用,它们总是需要存在于顶层。

例如,我们不能写

if (someCondition) {
  import './bar.js';
}

这就是动态导入的作用。动态导入可以在任何时候导入一个模块。它返回一个Promise,与导入的模块进行解析。

例如,让我们更新上面创建的app.js例子。

window.addEventListener('click', async () => {
  const module = await import('./message.js');

  console.log(`The message is: ${module.message}`);
});

现在我们并不是马上导入消息模块,而是将其延迟到用户点击页面上的任何地方。我们可以等待导入返回的承诺,并与返回的模块进行交互。任何导出的成员都可以在模块对象上使用。

懒惰评估

这就是没有捆绑程序的开发有很大好处的地方。如果你在向浏览器提供应用程序之前就将其捆绑,那么捆绑程序就需要评估你所有的动态导入,以进行代码分割并输出独立的块。对于具有大量动态导入的大型应用程序,这可能会增加大量的开销,因为整个应用程序是在你在浏览器中看到任何东西之前构建和捆绑的。

当服务于未捆绑的模块时,整个过程都是懒惰的。浏览器只做必要的工作来加载实际被请求的模块。

最新版本的Chrome、Safari和Firefox都支持动态导入。当前版本的Edge不支持,但基于Chromium的新Edge将支持。

在MDN阅读更多关于动态导入的信息

非亲属请求

并非所有的浏览器API都会解析相对于模块位置的请求。例如在使用fetch或在页面上渲染图片时。

为了处理这些情况,我们可以使用import.meta.url来获取当前模块的位置信息。

import.meta是一个特殊的对象,它包含了当前执行模块的元数据。url是这里暴露的第一个属性,它的工作原理很像NodeJS中的__dirname

import.meta.url指向模块被导入的url。

console.log(import.meta.url); // logs http://localhost:8080/path/to/my/file.js

我们可以使用URL API来轻松构建URL。例如,请求一个JSON文件。

const lang = 'en-US';

// becomes http://localhost:8080/path/to/my/translations/en-US.json
const translationsPath = new URL(`./translations/${lang}.json`, import.meta.url);

const response = await fetch(translationsPath);

在MDN上阅读更多关于import.meta的内容。

加载其他软件包

当构建一个应用程序时,你很快就会遇到不得不从npm中包含其他包的情况。这在浏览器中也能正常工作。例如,让我们安装并使用 lodash。

npm i -P lodash-es
import kebabCase from '../node_modules/lodash-es/kebabCase.js';

console.log(kebabCase('camelCase'));

Lodash是一个非常模块化的库,kebabCase函数依赖于很多其他模块。这些依赖性是自动处理的,浏览器会帮你解析和导入它们。

把显式路径写到你的节点模块文件夹里,这有点不寻常。虽然它是有效的,而且它可以工作,但大多数人习惯于编写所谓的裸露的导入指定器。

import { kebabCase } from 'lodash-es';
import kebabCase from 'lodash-es/kebabCase.js';

这样你就不需要特别说明一个包的位置,只需要说明它的名字。NodeJS经常使用这种方式,它的解析器会在文件系统中寻找node_modules文件夹和该名称的包。它读取package.json来知道要使用哪个文件。

浏览器无法承受发送一堆请求,直到它不再收到404s,那就太贵了。开箱即用,浏览器看到一个裸露的导入就会抛出一个错误。有一个新的浏览器API叫做import maps,它可以让你指导浏览器如何解决这些导入。

<script type="importmap">
  {
    "imports": {
      "lodash-es": "./node_modules/lodash-es/lodash.js",
      "lodash-es/": "./node_modules/lodash-es/"
    }
  }
</script>

目前它在chrome的标志后面实现了,并且很容易在其他浏览器上使用es-module-shims来实现。在我们得到广泛的浏览器支持之前,这可能是开发过程中一个有趣的选择。

对于导入地图来说,现在还为时过早,对于大多数人来说,它们可能还有点过于血腥。如果你对这个工作流程感兴趣,我推荐你阅读这篇文章

在正确支持导入地图之前,推荐的方法是使用一个web服务器,在向浏览器提供模块之前,它可以在飞行过程中把裸露的导入重写成显式路径。有一些服务器可以做到这一点。我推荐es-dev-server,我们将在下一篇文章中探讨。

缓存

因为我们没有将所有的代码捆绑到几个文件中,所以我们不必设置任何复杂的缓存策略。如果文件没有变化,你的Web服务器可以使用文件系统的最后修改时间戳来返回一个304。

你可以在浏览器中通过关闭Disable cache和刷新来测试这一点。

非js模块

到目前为止,我们只研究了javascript模块,故事看起来很完整。看起来我们已经具备了大规模编写javascript所需要的大部分东西。但是在网络上,我们不仅仅是在写javascript,我们还需要处理其他语言。

好消息是,HTML、CSS和JSON模块有了具体的建议,而且所有主要的浏览器厂商似乎都支持这些模块。

坏消息是,它们现在还不能使用,而且还不清楚什么时候可以使用。我们必须在这期间寻找一些解决方案。

JSON

在Node JS中,可以从javascript中导入JSON文件。这些文件会成为javascript对象。在web项目中,这也是经常使用的。有很多构建工具插件可以实现这一点。

在浏览器支持JSON模块之前,我们可以直接使用javascript模块来导出对象,或者使用fetch来检索JSON文件。请参阅import.meta.url部分,了解一个使用fetch的例子。

HTML模块

随着时间的推移,Web框架已经用不同的方式解决了HTML模板,例如将HTML放在javascript字符串里面。JSX是一种非常流行的格式,用于在javascript里面嵌入动态HTML,但如果不进行某种转换,它是不会在浏览器中原生运行的。

如果你真的想在HTML文件中编写HTML,在我们得到HTML模块之前,你可以使用fetch下载你的HTML模板,然后再使用你使用的任何渲染系统。我不推荐这样做,因为它很难优化生产。你想要的东西可以通过捆绑器进行静态分析和优化,这样你就不会在生产中产生大量的请求。

幸运的是,有一个很好的选择。在es2015/es6中,我们可以使用标签化的模板字符串字元将HTML嵌入到JS中,并利用它来进行高效的DOM更新。因为HTML模板往往带有很多动态性,我们可以使用javascript来表达这些,而不是学习一个全新的元语法,这其实是一个很大的好处。它可以在浏览器中原生运行,有很好的开发者体验,并与你的模块图集成,因此可以为生产优化。

有一些非常好的生产准备和功能完整的库可以用于此。

对于语法高亮,你可能需要配置你的IDE或安装一个插件。

CSS

对于HTML和JSON,有足够的选择。不幸的是,对于CSS来说,它更加复杂。就其本身而言,CSS并不是模块化的,因为它会影响整个页面。一个常见的抱怨是,这就是CSS难以扩展的原因。

有很多不同的方法来编写CSS,这超出了本文的范围,无法一一研究。如果你在index.html中加载常规的样式表,就可以正常工作。如果你使用的是某种CSS预处理器,你可以在运行Web服务器之前运行它,只需加载CSS输出即可。

如果库中发布了你可以导入的es模块格式,许多JS中的CSS解决方案也应该可以工作。

影子dom

对于真正的模块化CSS,我推荐大家研究一下Shadow dom,它解决了CSS的许多范围和封装问题。我已经在许多不同类型的项目中成功地使用了它,但很高兴地指出,它还不是一个完整的故事。标准中还有一些缺失的功能正在被解决,所以它可能还不是所有场景下的正确解决方案

这里值得一提的是lit-element库,它在编写模块化CSS时提供了很好的开发者体验,无需构建步骤,lit-element为你完成了大部分的重任。你可以使用标记的模板字元来编写CSS,这只是创建可构建样式表的语法糖。这样你就可以在你的组件之间编写和共享CSS。

当CSS模块出厂时,这个系统也会和它们很好的集成。我们可以通过使用fetch来模拟CSS模块,但正如我们在HTML上看到的那样,这很难在生产使用中进行优化。我不喜欢在JS中使用CSS,但lit-element的解决方案与众不同,非常优雅。你在JS文件中编写CSS,但它仍然是有效的CSS语法。如果你喜欢把事情分开,你可以直接创建一个my-styles.css.js文件,并使用默认导出的只是一个样式表。

库支持

幸运的是,运送es模块格式的库数量正在稳步增长。但仍有一些流行的库只运UMD或CommonJS。如果没有某种代码转换,这些库是无法工作的。我们能做的最好的事情就是在这些项目上公开问题,让他们知道有多少人对支持原生模块语法感兴趣。

我认为这是一个会比较快消失的问题,尤其是在Node JS完成他们的es模块实现之后。许多项目已经使用es模块作为他们的创作格式,我不认为有人真的喜欢不得不发布多种不完美的模块格式。

最后的想法

本文的目标是探索我们不需要为开发做任何构建的工作流程,我认为我们已经证明了存在真正的可能性。对于很多用例,我认为我们可以放弃大部分的开发工具。在其他情况下,我认为它们仍然可以是有用的。但我认为我们的出发点应该反过来。我们不应该试图让我们的生产构建在开发过程中发挥作用,而是应该编写标准代码,在浏览器中原样运行,只有在我们认为有必要的情况下才进行轻量级的转换。

需要重申的是,我不认为构建工具是邪恶的,我也不是说这是每个项目的正确方法。这是每个团队应该根据自己的需求为自己做出的选择。

es-dev-server

你几乎可以用任何一台普通的Web服务器来完成本文所描述的一切。尽管如此,还是有一些Web服务器的功能可以真正帮助我们获得开发体验。特别是当我们想在旧的浏览器上运行我们的应用程序时,我们可能需要一些帮助。

open-wc,我们创建了es-dev-server,这是一个可组合的web服务器,它专注于开发者在开发时的生产力,而不需要构建步骤。

请看我们下一篇文章,看看我们如何设置它。

开始使用

要想在没有任何构建工具的情况下开始开发,你可以使用open-wc项目的脚手架来设置基本功能。

npm init @open-wc

它用 lit-element 这个 web 组件库来设置项目,你可以把它换成你选择的任何库,设置不是针对 web 组件的。你可以把它换成任何你选择的库,这个设置并不针对web组件。


通过www.DeepL.com/Translator (免费版)翻译