Featured image of post 原生JavaScript模块:告别构建工具,拥抱现代Web开发

原生JavaScript模块:告别构建工具,拥抱现代Web开发

本文探讨了如何在现代Django项目中,利用原生JavaScript模块(ES6模块)和现代浏览器特性,完全摆脱复杂的构建工具链(如Webpack、Vite)。文章提供了详细的代码示例,展示了如何在不使用任何构建流程的情况下实现模块化JavaScript,并讨论了在生产环境中使用Django的ManifestStaticFilesStorage进行静态文件版本化的注意事项与未来改进方向。

在过去十多年里,我们一直在打包CSS和JavaScript文件。这些构建工具使我们能够在利用CSS和JS新特性的同时,继续支持旧版浏览器。它们还有助于优化客户端网络性能,将内容压缩到尽可能小,并将文件合并成一个大包以减少网络握手。在这个过程中,我们经历了大量构建工具的迭代:从 Grunt (2012) 到 Gulp (2013),再到 Webpack (2014)、Parcel (2017),以及 esbuild (2020) 和 Vite (2020)。 然而,随着现代浏览器技术的发展,对这些构建工具的需求已大大减少。

现代CSS原生支持了许多当初需要构建工具才能实现的功能。例如用于组织代码的CSS嵌套、变量,以及用于特性检测的@supports。 JavaScript ES6 / ES2015是一个巨大的进步,此后这门语言一直在稳步发展。它现在通过import/export关键字提供了原生的模块支持。 与此同时,HTTP/2的性能改进使得可以在同一连接上进行并行请求,从而消除了HTTP/1.x协议的限制。

这些构建过程非常复杂,特别是对于Django初学者而言。相关的工具和最佳实践更新迅速。需要学习的内容很多,并且你需要了解如何在Django项目中使用它们。你可以建立一个工作流,将构建结果存储在静态文件夹中,但Django核心本身并不支持构建流水线,因此这很大程度上需要从众多第三方包中进行选择,并将它们集成到项目中。 这种复杂性带来的好处已不再那么明显,尤其是对初学者来说。构建工具仍然有其优势,但你完全可以在不使用或学习任何构建流程的情况下,创造出专业的结果。

无构建JavaScript教程

为了展示现代浏览器的能力,让我们用一些更新的JavaScript来扩展Django的投票教程。我们将使用现代JS模块,并且不需要构建系统。 为了让我们的应用需要JavaScript,我们为投票功能增加一个新需求:允许用户添加自己的选项,而不仅仅是投票给现有选项。我们更新表单,在选择代码下方添加一个新选项:

1
或者添加你自己的 <input type="text" name="choice_text" maxlength="200" />

现在,如果现有选项不合适,我们的用户可以为投票添加自己的选项。我们可以更新投票视图来处理这个新选项,并进行更多验证:

  • 如果没有选择投票选项,我们就处理添加新选项
  • 如果没有选择投票选项也没有新的choice_text,则显示错误
  • 如果两者都选择了,也显示错误。

随着逻辑变得越来越复杂,如果我们有一些JavaScript来处理会更好。我们可以编写一个脚本来为我们处理部分表单验证。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 注意"export default",这使得此函数可用于其他模块。
export default function initFormValidation() {
  document.getElementById("polls").addEventListener("submit", function (e) {
    const choices = this.querySelectorAll('input[name="choice"]');
    const choiceText = this.querySelector('input[name="choice_text"]');

    const hasChecked = [...choices].some(r => r.checked);
    const hasText = choiceText?.value.trim() !== "";

    if (!hasChecked && !hasText) {
      e.preventDefault();
      alert("你既没有选择选项,也没有提供新的选项。");
    }

    if (hasChecked && hasText) {
      e.preventDefault();
      alert("你不能同时选择一个选项并提供新选项。");
    }
  });
}

注意上面代码中我们如何使用export default。这意味着form_validation.js是一个JavaScript模块。当我们创建main.js文件时,可以用import语句导入它:

1
2
3
import initFormValidation from "./form_validation.js";

initFormValidation();

最后,我们使用Django常用的静态模板标签将脚本添加到details.html文件的底部。注意type="module",这是告诉浏览器我们将使用import/export语句所必需的。

1
<script type="module" src="{% static 'polls/js/main.js' %}"></script>

就是这样!我们获得了现代JavaScript的模块化优势,而无需任何构建过程。浏览器会为我们处理模块加载。并且得益于HTTP/2的并行请求,即使有很多模块,也不会造成性能损失。

生产环境部署

为了部署,我们只需要Django对收集静态文件到一个位置的支持,以及其对文件名添加哈希值的支持。在生产环境中,使用ManifestStaticFilesStorage存储后端是个好主意。它会通过将文件内容的MD5哈希值附加到文件名上来存储所处理的文件名。这允许你设置很远的未来缓存过期时间,这对性能有好处,同时还能确保新版本的文件能够送达用户的浏览器。 此外,这个后端也能够更新import语句中对form_validation.js的引用,使其使用新的带版本号的文件名。

未来的工作

ManifestStaticFilesStorage可以工作,但其很多实现细节带来了不便。对开发者来说,它可以更容易使用。

  • 对带哈希文件的import/export支持不够健壮。
  • CSS中引用文件的注释可能导致collectstatic时出错。
  • 无法处理CSS/JS中的循环依赖。
  • 当文件缺失时,collectstatic过程中的错误信息不总是清晰。
  • StaticFilesStorageManifestStaticFilesStorage的实现差异可能导致生产环境中出现开发环境没有的错误(例如关于前导斜杠的#26329号问题)。
  • 配置常见选项意味着需要子类化存储后端,而我们本可以使用现有的OPTIONS字典。
  • 如果使用并行化,收集静态文件可以更快(拉取请求:#19935 使用线程池并行化collectstatic)。

我们在Django on the Med 🏖️冲刺活动中讨论了这些可能的改进,我希望我们能取得进展。 我构建了django-manifeststaticfiles-enhanced来尝试修复所有这些问题。核心工作是基于Ned Batchelder的JsLex(Django以前用过)为CSS和JS切换到词法分析器。通过与Claude Code合作处理语法覆盖的繁重工作,它被扩展以支持现代JS和CSS。 它还改用拓扑排序来查找依赖关系,而之前我们使用的是更暴力的方法——反复处理直到看不到变化为止,这导致了更多的工作量,尤其是在使用网络存储的后端上。这也意味着我们无法处理循环依赖。 为了验证其有效性,我在50多个项目上运行了性能基准测试,它经过了问题测试,并且性能相当(通常是更好的)。平均而言,它大约快30%。

虽然这些改进会受欢迎,但你现在就可以在Django项目中尝试无构建的JavaScript和CSS!现代浏览器使得无需复杂工具就能创建出色的前端体验成为可能。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计