如何用Django设置源代码地图

302 阅读4分钟

源码图是将你的最小化的CSS或JavaScript映射到原始代码的文件。 它们允许你使用浏览器的开发工具来调试最小化的代码,就像它是原始代码一样。 还有一些错误捕获工具,如Sentry,可以使用源码图来报告原始代码的错误。

在这篇文章中,我们将看看源码图如何融入Django的静态文件基础设施,包括我最近对Django所做的一些改变,以更好地支持它们。 我们将看看使用JavaScript源码图,但一切都同样适用于CSS源码图。 好吧,让我们开始吧。

目录布局

让我们看看一个使用源代码地图的项目实例,看看它们是如何融入Django的基础设施的。 这个项目展示了一个静态文件设置,对大多数项目来说都很有效。

该项目有三个不同角色的 "静态文件 "目录,都在资源库的根目录下。

  • frontend/Django完全不使用这个目录。 前端工具,比如bundlers,应该处理frontend/ 中的源文件,并将输出文件放在下一个目录,static/ 。这提供了一个清晰的 "移交"。

  • static/,其中包含了供Django管理的静态文件。这是在STATICFILES_DIRS 的设置。

    STATICFILES_DIRS = [BASE_DIR / "static"]
    
  • static_root/,它是由Django的collectstatic 命令创建的。这是由STATIC_ROOT 设置提到的。

    STATIC_ROOT = BASE_DIR / "static_root"
    

    对于非开发环境,你可以运行collectstatic ,用来自STATICFILES_DIRS 的静态文件和应用程序内的静态文件填充这个目录。静态文件存储也可以在这个阶段处理这些文件。

总的来说,静态文件管道看起来像。

+-----------+               +---------+                       +--------------+
| frontend/ | --- build --> | static/ | --- collectstatic --> | static_root/ |
+-----------+               +---------+                       +--------------+

使用源码图构建

有很多前端构建工具可以生成源码图,比如Webpack和Parcel。 本例项目使用esbuild保持简单,这是一个非常快速和低配置的工具。

frontend/app.js ,该示例应用程序的代码是最小的,只处理进入和退出全屏模式

document.addEventListener("keydown", (event) => {
  if (event.key == "f") {
    document.documentElement.requestFullscreen();
  } else if (event.key == "Escape") {
    document.exitFullscreen();
  }
});

esbuild 将代码最小化,并通过 中的npm脚本输出为源码图。package.json

{
  "dependencies": {
    "esbuild": "^0.14.12"
  },
  "scripts": {
    "build": "esbuild --minify --sourcemap frontend/app.js --outdir=static/"
  }
}

它是这样调用的。

$ npm run build

> build
> esbuild --minify --sourcemap frontend/app.js --outdir=static/


  static/app.js      174b
  static/app.js.map  428b

⚡ Done in 10ms

两个输出文件是最小化的代码,app.js ,和源码图,app.js.map 。这里是app.js

document.addEventListener("keydown",e=>{e.key=="f"?document.documentElement.requestFullscreen():e.key=="Escape"&&document.exitFullscreen()});
//# sourceMappingURL=app.js.map

第二行是源码图参考,这是一个特殊格式的注释(根据源码图规范)。 当你打开浏览器的开发工具时,它将从这些注释中加载源码图,然后你就可以调试你的原始源代码。

一个源码图是一个JSON文件,里面有一些数据,这里是app.js.map

{
  "version": 3,
  "sources": ["../frontend/app.js"],
  "sourcesContent": ["document.addEventListener(\"keydown\", (event) => {\n  if (event.key == \"f\") {\n    document.documentElement.requestFullscreen();\n  } else if (event.key == \"Escape\") {\n    document.exitFullscreen();\n  }\n});\n"],
  "mappings": "AAAA,SAAS,iBAAiB,UAAW,AAAC,GAAU,CAC9C,AAAI,EAAM,KAAO,IACf,SAAS,gBAAgB,oBAChB,EAAM,KAAO,UACtB,SAAS",
  "names": []
}

你可以看到。

  • version 是源码图的版本。
  • sources 包含原始的源文件名称。
  • sourcesContent 包含每个文件的原始源代码。
  • mappings 包含压缩的引用,将输出的代码映射回源文件。

由于源码图包含完整的原始源代码和更多的内容,它们可能会变得相当大。 但这并不是一个性能问题,因为它们只在开发工具打开时被加载。

开发中的源码图

在开发中,runserver ,直接从你的静态目录中提供静态文件--这里是static/ 。这意味着源码图的引用如期进行。 在Firefox中,当你打开调试器时,你可以看到这个动作,它显示了源码文件。

image.png 你可以对在浏览器中运行的精简后的代码进行调试,就像它是原始的源代码一样。 例如,如果你在源文件中设置一个断点,它就会在适当的位置暂停运行的代码。 惊人的是!

此外,堆栈跟踪将使用源文件名、行号和列号。 如果你添加了一个delibarate错误。

 document.addEventListener("keydown", (event) => {
   if (event.key == "f") {
     document.documentElement.requestFullscreen();
   } else if (event.key == "Escape") {
     document.exitFullscreen();
   }
+  throw "woops";
 });

那么火狐会在app.js ,第7行第8列报告这个问题。

Uncaught woops 2 app.js:7:8
    <anonymous> app.js:7
    (Async: EventListener.handleEvent)
    <anonymous> app.js:1

这很好,因为真的很难读懂最小化版本的单行😅。

生产中的源代码地图

源代码图在你的生产环境中是非常有用的,因为它们可以让你更容易追踪到bug。 所以确保它们包含在你的静态文件中是一个好主意。

(有些人认为,为了安全,或者为了防止人们复制你的代码,不要把源码图放到生产环境中。 我不相信这两个理由--"unminifier "工具已经允许人们在没有源码图的情况下阅读你的代码。)

在生产中,Django的collectstatic 步骤会将你的源码图和其他静态文件一起处理。这取决于你在STATICFILES_STORAGE 设置中选择的存储类。

如果你使用的是默认的存储类,StaticFilesStorage ,那么你不需要做任何事情。源码图会和你的其他文件一起被按原样提供,而且一切都会很顺利。但是StaticFilesStorage 在生产中一般是没有用的,因为它不兼容在静态文件上设置缓存头。

对于大多数生产设置,你会想使用Django的 ManifestStaticFilesStorage它通过添加一个短的哈希值来转换文件。 这实现了缓存破坏模式,如果一个文件的内容改变了,它的文件名也会改变。 你可能想使用ManifestStaticFilesStorage 本身,或者它来自Whitenoise的子类,叫做CompressedManifestStaticFilesStorage 。(Whitenoise通过直接从Django提供静态文件,使其设置非常容易。 我认为它很适合大多数项目。

下面是在Whitenoise中使用collectstatic 的情况。

$ ./manage.py collectstatic -v 2
Copying '/Users/chainz/tmp/source-maps/static/app.js.map'
Copying '/Users/chainz/tmp/source-maps/static/app.js'
Post-processed 'app.js.map' as 'app.js.8dee7203977f.map'
Post-processed 'app.js' as 'app.ba8df8fcb9ec.js'
Post-processed 'app.js.8dee7203977f.map' as 'app.js.8dee7203977f.map.br'
Post-processed 'app.js.8dee7203977f.map' as 'app.js.8dee7203977f.map.gz'
Post-processed 'app.ba8df8fcb9ec.js' as 'app.ba8df8fcb9ec.js.br'
Post-processed 'app.ba8df8fcb9ec.js' as 'app.ba8df8fcb9ec.js.gz'

2 static files copied to '/Users/chainz/tmp/source-maps/static_root', 6 post-processed.

散列步骤将app.js 重命名为app.ba8df8fcb9ec.js ,将app.js.map 重命名为app.js.8dee7203977f.map 。这对破坏缓存很有好处,但它破坏了源地图的关系!回顾一下注释。

//# sourceMappingURL=app.js.map

...它需要更新以使用重命名的源地图。

//# sourceMappingURL=app.js.8dee7203977f.map

(如果没有这个更新,源码图可能仍然有效。香草式的ManifestStaticFilesStorage ,而Whitenoise没有 WHITENOISE_KEEP_ONLY_HASHED_FILES的情况下,都会将未哈希的文件留在原地,这可以让源码图加载。 但这将是没有缓存的,所以你可能会得到一个旧的源码图,从而调试出旧的源码--巨大的混乱!)

值得庆幸的是,ManifestStaticFilesStorage 可以为你更新这些源码图的引用。作为散列步骤的一部分,它应用了一些基于正则表达式的修正来更新文件引用,例如CSS文件中的url() 调用。 我为源码图引用贡献了一些额外的修正。

  1. Django 4.0中的JavaScript源码地图(Ticket #32383,感谢Carlton Gibson和Mariusz Felisiak的审核)。
  2. Django 4.1中的CSS源码图(预计2022年8月)。(票号33446,感谢Mariusz Felisiak的审核。

在较早的Django版本中,你可以使用下节的backport。

无论是使用backport还是更新的Django版本,你都可以看到源码图的引用被更新了。 例如,app.ba8df8fcb9ec.js 包含。

document.addEventListener("keydown",e=>{throw e.key=="f"?document.documentElement.requestFullscreen():e.key=="Escape"&&document.exitFullscreen(),"woops"});
//# sourceMappingURL=app.js.8dee7203977f.map

现在你可以调试掉那些生产中的麻烦了

未来的回传

更新(2022-03-03):**我发现CSS源码图有更灵活的空白要求,所以我向Django做了一个PR,并更新了下面的CSS源码图正则表达式。

这是针对旧版Django的Backport。将该类放在你的项目中,并更新你的STATICFILES_STORAGE 设置以使用它,或者与你现有的自定义存储类合并。这个版本是针对Whitenoise的--如果你使用普通的ManifestStaticFilesStorage ,请根据情况将其作为基础类。

import django

from whitenoise.storage import CompressedManifestStaticFilesStorage


class CustomStaticFilesStorage(CompressedManifestStaticFilesStorage):
    if django.VERSION < (4, 0):
        patterns = CompressedManifestStaticFilesStorage.patterns + (
            (
                "*.js",
                (
                    (
                        r"(?m)^(//# (?-i:sourceMappingURL)=(.*))$",
                        "//# sourceMappingURL=%s",
                    ),
                ),
            ),
            (
                "*.css",
                (
                    (
                        r"(?m)^(/\*#[ \t](?-i:sourceMappingURL)=(.*)[ \t]*\*/)$",
                        "/*# sourceMappingURL=%s */",
                    ),
                ),
            ),
        )
    elif django.VERSION < (4, 1):
        # Django 4.0 switched to named patterns
        patterns = CompressedManifestStaticFilesStorage.patterns + (
            (
                "*.css",
                (
                    (
                        r"(?m)^(?P<matched>/\*#[ \t](?-i:sourceMappingURL)=(?P<url>.*)[ \t]*\*/)$",
                        "/*# sourceMappingURL=%(url)s */",
                    ),
                ),
            ),
        )
    else:
        raise AssertionError(
            "The above backported custom patterns are no longer required."
        )

在Django 4.1之后,Backport就不是必需的了,所以它会引发一个AssertionError ,提醒你将其删除。我喜欢这样做,以确保事情被整理好。