源码图是将你的最小化的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中,当你打开调试器时,你可以看到这个动作,它显示了源码文件。
你可以对在浏览器中运行的精简后的代码进行调试,就像它是原始的源代码一样。 例如,如果你在源文件中设置一个断点,它就会在适当的位置暂停运行的代码。 惊人的是!
此外,堆栈跟踪将使用源文件名、行号和列号。 如果你添加了一个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() 调用。 我为源码图引用贡献了一些额外的修正。
- Django 4.0中的JavaScript源码地图(Ticket #32383,感谢Carlton Gibson和Mariusz Felisiak的审核)。
- 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 ,提醒你将其删除。我喜欢这样做,以确保事情被整理好。