告别构建工具:在Django中使用原生JavaScript模块

本文探讨了如何利用现代浏览器对原生JavaScript模块和CSS特性的支持,在Django项目中摒弃复杂的构建工具链(如Webpack、Vite),直接进行前端开发,并提供了具体的代码示例和实践建议。

过去十多年,我们一直在捆绑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的polls教程。我们将使用现代JS模块,且不需要构建系统。 为了让我们的JS有用武之地,让我们给投票应用添加一个新需求:允许用户添加自己的建议,而不仅仅是对现有选项进行投票。我们更新表单,在选择代码下方添加一个新选项:

1
or add your own <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("You didn't select a choice or provide a new one.");
    }

    if (hasChecked && hasText) {
      e.preventDefault();
      alert("You can't select a choice and also provide a new option.");
    }
  });
}

注意我们在上面的代码中如何使用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来尝试修复所有这些问题。核心工作是将CSS和JS的处理切换到基于词法分析器,使用Ned Batchelder的JsLex(Django之前使用过)。通过与Claude Code合作,完成涵盖语法的繁琐工作,它被扩展到覆盖现代JS和CSS。 它还改用拓扑排序来查找依赖关系,而之前我们采用的是更暴力的方法,即重复处理直到看不到更多变化,这导致了更多的工作,特别是在使用网络的存储上。这也意味着我们无法处理循环依赖。 为了验证其有效性,我在50多个项目上运行了性能基准测试,它经过了问题测试,并具有相当(通常是改进的)性能。平均而言,它大约快了30%。

尽管这些改进会受到欢迎,但今天就开始在你的Django项目中尝试无需构建的JavaScript和CSS吧!现代浏览器使得无需复杂的构建工具就能创造出优秀的前端体验成为可能。

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