无需构建即可进行开发 (1):简介
无需构建即可进行开发:简介
无需构建即可进行开发:简介
本文是无构建开发系列文章的一部分:
- 简介(本文)
- es-dev-服务器
- 测试(即将推出!)
在本文中,我们将探讨为什么以及是否应该在开发过程中不包含构建步骤,并概述当前和未来支持构建的浏览器 API。在后续文章中,我们将探讨如何es-dev-server
实现构建步骤以及如何进行测试。
现代 Web 开发
在 Web 开发的早期,我们只需要一个简单的文件编辑器和一个 Web 服务器。新手很容易理解整个流程并开始制作自己的网页。从那时起,Web 开发发生了巨大的变化:我们用于开发的工具的复杂性与我们在 Web 上构建的内容的复杂性一样增长。
想象一下,如果你对 Web 开发完全陌生,那会是什么样子:
- 首先,您需要学习许多不同的工具,并了解每种工具如何改变您的代码,然后才能在浏览器中实际运行。
- 您的 IDE 和 linter 可能不理解朋友向您推荐的这个框架的语法,因此您需要找到使其正常工作的正确插件组合。
- 如果你希望在浏览器中调试代码,则需要为链中的所有工具正确配置源映射。如何让它们与你的测试协同工作则是另一回事。
- 你决定简化流程,不使用 TypeScript。你按照教程操作,却无法使用装饰器,错误信息也帮不上忙。原来是你没有按照正确的顺序配置 Babel 插件……
这听起来可能有些夸张,我知道市面上有很多很棒的入门项目和教程,但这种经历对很多开发者来说都很常见。你自己可能也经历过类似的困境。
我觉得这真的很可惜。Web 的一个关键卖点就是它简单开放。它应该很容易上手,无需繁琐的配置和繁琐的流程。
我并非批评构建工具本身,它们都有各自的作用和用途。长期以来,使用构建工具是真正在 Web 上创建复杂应用的唯一途径。当时的 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
目录中并app.js
从 index.html 导入:
<!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,该 Promise 会解析导入的模块。
例如,让我们更新app.js
上面创建的示例:
window.addEventListener('click', async () => {
const module = await import('./message.js');
console.log(`The message is: ${module.message}`);
});
现在我们不会立即导入消息模块,而是将其延迟到用户点击页面上的任何位置。我们可以等待导入返回的promise,并与返回的模块进行交互。所有导出的成员都可以在模块对象上使用。
惰性求值
这正是不使用打包器进行开发的一个显著优势。如果您在将应用程序提供给浏览器之前就进行打包,打包器需要评估所有动态导入,以进行代码拆分并输出单独的代码块。对于包含大量动态导入的大型应用程序来说,这可能会增加大量的开销,因为整个应用程序在浏览器中显示任何内容之前就已经构建并打包完毕。
当提供非捆绑模块时,整个过程是惰性的。浏览器只执行加载实际请求的模块所需的工作。
最新版本的 Chrome、Safari 和 Firefox 均支持动态导入。当前版本的 Edge 尚不支持此功能,但基于 Chromium 的新版 Edge 将会支持此功能。
非亲属请求
并非所有浏览器 API 都会解析相对于模块位置的请求。例如,使用 fetch 或在页面上渲染图像时。
为了处理这些情况,我们可以使用import.meta.url
来获取有关当前模块位置的信息。
import.meta
是一个特殊对象,包含有关当前正在执行的模块的元数据。是这里公开的第一个属性,其工作方式与 NodeJS 中的url
非常相似。__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);
加载其他包
在构建应用程序时,你很快就会遇到需要从 npm 引入其他包的情况。这在浏览器中也同样适用。例如,让我们安装并使用 lodash:
npm i -P lodash-es
import kebabCase from '../node_modules/lodash-es/kebabCase.js';
console.log(kebabCase('camelCase'));
Lodash 是一个非常模块化的库,其kebabCase
功能依赖于许多其他模块。这些依赖项会自动处理,浏览器会为您解析并导入它们:
明确写入 Node 模块文件夹的路径有点不寻常。虽然这样做是有效的,而且也能正常工作,但大多数人习惯于使用所谓的“裸导入说明符”来写:
import { kebabCase } from 'lodash-es';
import kebabCase from 'lodash-es/kebabCase.js';
这样,您就无需具体说明包的位置,只需说明它的名称。NodeJS 经常使用这种方法,它的解析器会遍历文件系统,查找node_modules
具有该名称的文件夹和包。它会读取文件,package.json
以确定要使用哪个文件。
浏览器无法承受发送大量请求,直到不再收到 404 错误为止,这代价太高了。默认情况下,浏览器在遇到裸导入时会抛出错误。现在有一个名为导入映射的新浏览器 API,可以让你指示浏览器如何解析这些导入:
<script type="importmap">
{
"imports": {
"lodash-es": "./node_modules/lodash-es/lodash.js",
"lodash-es/": "./node_modules/lodash-es/"
}
}
</script>
它目前在 Chrome 中通过 flag 实现,并且可以通过 es-module-shims轻松在其他浏览器上进行 shim 。在我们获得广泛的浏览器支持之前,这可能是开发过程中一个有趣的选择。
现在导入地图还为时过早,对大多数人来说,这可能还太过前沿。如果你对这个工作流程感兴趣,我推荐你阅读这篇文章
在导入映射得到正确支持之前,推荐的方法是使用 Web 服务器,该服务器会在将模块提供给浏览器之前,动态地将裸导入重写为显式路径。有一些服务器可以做到这一点。我推荐es-dev-server,我们将在下一篇文章中探讨它。
缓存
由于我们不会将所有代码打包到几个文件中,因此无需设置任何复杂的缓存策略。如果文件未发生更改,您的 Web 服务器可以使用文件系统的上次修改时间戳来返回 304 错误。
您可以通过关闭Disable cache
并刷新在浏览器中进行测试:
非 js 模块
到目前为止,我们只研究了 JavaScript 模块,目前为止,一切都已经相当完善。看起来,我们已经具备了大规模编写 JavaScript 所需的大部分条件。但在 Web 上,我们不仅要编写 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 来表达这些特性,而不是学习一整套新的元语法,这实际上是一个非常大的优势。它原生运行在浏览器中,拥有良好的开发者体验,并且能够与模块图集成,因此可以针对生产环境进行优化。
有一些非常好的生产就绪且功能齐全的库可用于此目的:
- htm,使用模板字符串的 JSX。可与使用 JSX 的库(例如 React)配合使用。
- lit-html,一个 HTML 模板库
- lit-element,将 lit-html 与 Web 组件集成
- haunted,一个带有类似 React Hooks 的函数式 Web 组件库
- hybrids,另一个函数式 Web 组件库
- hyperHTML,一个 HTML 模板库
为了语法高亮,您可能需要配置您的 IDE 或安装插件。
CSS
对于 HTML 和 JSON,有很多替代方案。遗憾的是,CSS 更加复杂。CSS 本身并不模块化,因为它会影响整个页面。一个常见的抱怨是,这正是 CSS 难以扩展的原因。
编写 CSS 的方法有很多种,本文无法一一详述。常规样式表如果在 index.html 中加载,就可以正常工作。如果您使用了某种 CSS 预处理器,则可以在运行 Web 服务器之前运行它,然后直接加载 CSS 输出。
如果库发布了可以导入的 es 模块格式,那么 JS 解决方案中的许多 CSS 也应该可以工作。
影子王国
对于真正的模块化 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。如果不进行某种代码转换,这些库就无法正常工作。我们能做的最好的事情就是在这些项目上公开 issue,让他们了解有多少人有兴趣支持原生模块语法。
我认为这个问题很快就会消失,尤其是在 Node.js 完成 es 模块实现之后。许多项目已经使用 es 模块作为其编写格式,我认为没有人真的喜欢交付多种不完善的模块格式。
最后的想法
本文的目标是探索无需进行任何开发构建的工作流程,我认为我们已经证明了这确实存在可能性。在很多用例中,我认为我们可以省去大部分开发工具。在其他情况下,我认为它们仍然有用。但我认为我们的出发点应该反过来。与其在开发过程中努力让生产构建能够正常工作,不如编写在浏览器中运行的标准代码,并且只在我们认为必要时进行少量转换。
需要重申的是,我并不认为构建工具是邪恶的,我也不是说它适合所有项目。每个团队都应该根据自身需求做出选择。
es-dev-服务器
任何常规的 Web 服务器几乎都可以完成本文所述的所有功能。话虽如此,Web 服务器的一些功能仍然能够真正提升开发体验。尤其是当我们想在老版本的浏览器上运行应用程序时,我们可能需要一些帮助。
open-wc
我们创建了es-dev-server ,这是一个可组合的 Web 服务器,专注于提高开发人员在开发过程中无需构建步骤的工作效率。
查看我们的下一篇文章,了解我们如何进行设置!
入门
要在没有任何构建工具的情况下开始开发,您可以使用open-wc
项目脚手架来设置基础知识:
npm init @open-wc
它会使用 Web 组件库( )来设置项目lit-element
。您可以将其替换为您选择的任何库,此设置并不特定于 Web 组件。