如何启用脚本合并(Script Bundling)?

在 Serene 模板中,_LayoutHead.cshtml 默认包含了 3MB+ 的 javascript 引用文件。

这可能导致系统出现一些带宽和性能问题,特别是使用移动设备访问基于 Serenity 的站点。

有几种方式可以处理这些问题,如压缩文件以减少脚本大小;合并打包脚本到较少的文件,从而减少请求的数量。

你可能更喜欢使用工具,如 Webpack、Grunt、Gulp、UglifyJS 等,但如果你想要更简单有效且更少手工操作的解决方案,Serenity 自带的脚本合并和压缩系统可开箱即用。

请注意,该功能要求 Serenity 2.0.13+ 。

ScriptBundles.json

首先,你需要在 MyProject.Web/scripts/site 文件夹下添加一个文件 ScriptBundles.json。ScriptBundles.json 配置启用合并时,合并脚本将包含这些文件。

在 Serene 模板 2.0.13+ 中默认包含该文件,它看起来像:

除非你想要向包中添加一些自定义的脚本,否则你不需要修改此文件。

  1. {
  2. "Libs": [
  3. "~/Scripts/pace.js",
  4. "~/Scripts/rsvp.js",
  5. "~/Scripts/jquery-{version}.js",
  6. "~/Scripts/jquery-ui-{version}.js",
  7. "~/Scripts/jquery-ui-i18n.js",
  8. "~/Scripts/jquery.validate.js",
  9. "~/Scripts/jquery.blockUI.js",
  10. "~/Scripts/jquery.cookie.js",
  11. "~/Scripts/jquery.json.js",
  12. "~/Scripts/jquery.autoNumeric.js",
  13. "~/Scripts/jquery.colorbox.js",
  14. "~/Scripts/jquery.dialogextendQ.js",
  15. "~/Scripts/jquery.event.drag.js",
  16. "~/Scripts/jquery.scrollintoview.js",
  17. "~/Scripts/jsrender.js",
  18. "~/Scripts/select2.js",
  19. "~/Scripts/toastr.js",
  20. "~/Scripts/SlickGrid/slick.core.js",
  21. "~/Scripts/SlickGrid/slick.grid.js",
  22. "~/Scripts/SlickGrid/slick.groupitemmetadataprovider.js",
  23. "~/Scripts/SlickGrid/Plugins/slick.autotooltips.js",
  24. "~/Scripts/SlickGrid/Plugins/slick.headerbuttons.js",
  25. "~/Scripts/SlickGrid/Plugins/slick.rowselectionmodel.js",
  26. "~/Scripts/SlickGrid/Plugins/slick.rowmovemanager.js",
  27. "~/Scripts/bootstrap.js",
  28. "~/Scripts/Saltarelle/mscorlib.js",
  29. "~/Scripts/Saltarelle/linq.js",
  30. "~/Scripts/Serenity/Serenity.CoreLib.js",
  31. "~/Scripts/Serenity/Serenity.Script.UI.js",
  32. "~/Scripts/Serenity/Serenity.Externals.js",
  33. "~/Scripts/Serenity/Serenity.Externals.Slick.js",
  34. "~/Scripts/jquery.cropzoom.js",
  35. "~/Scripts/jquery.fileupload.js",
  36. "~/Scripts/jquery.iframe-transport.js",
  37. "~/Scripts/jquery.slimscroll.js",
  38. "~/Scripts/mousetrap.js",
  39. "~/Scripts/fastclick/fastclick.js"
  40. ],
  41. "Site": [
  42. "~/Scripts/adminlte/app.js",
  43. "~/Scripts/Site/Serene.Script.js",
  44. "~/Scripts/Site/Serene.Web.js"
  45. ]
  46. }

在这里我们定义两个不同的合并配置,LibsSite,对应于动态脚本文件:

Bundle.Site.js 配置为包含这三个 JS 文件(在列出的顺序):

  1. "~/Scripts/adminlte/app.js",
  2. "~/Scripts/Site/Serene.Script.js",
  3. "~/Scripts/Site/Serene.Web.js"

Bundle.Libs.js 包含所有其他 javascript 文件。

默认情况下,我们这里使用 2 个合并包(bundles),但如果你需要不同的配置,只要注意依赖关系,也可以使用一个、三个或更多的合并包。

在这里,合并包(bundle)内的顺序是非常重要的。必须按在 _LayoutHead.cshtml 出现的顺序包含脚本。

在你添加另外的客户脚本时,请确保把它放在所有依赖脚本后面。

例如,如果在 jquery 前加载 jquery 插件,你将得到错误。

同时,也要确保在两个合并包(bundles)中不能包含同一文件。

Enabling Bundling

你应该只在生产发布时启用合并(特别是压缩)。否则 Javascript 将会难以调试。

为了启用合并,只需要把 web.config 中 ScriptBundlingEnabled 属性改为 true:

  1. <add key="ScriptBundling" value="
  2. { Enabled: true, Minimize: false, UseMinJS: false }" />

Enabled 是 false (默认情况)时,系统不会做任何事,并且你包含的脚本是未经压缩的。你的页面源代码看起来是这样的:

  1. <script src="/Scripts/pace.js?v=..."></script>
  2. <script src="/Scripts/rsvp.js?v=..."></script>
  3. <script src="/Scripts/jquery-2.2.3.js?v=..."></script>
  4. <script src="/Scripts/jquery-ui-1.11.4.js?v=..."></script>
  5. <script src="/Scripts/jquery-ui-i18n.js?v=..."></script>
  6. ...
  7. ...
  8. ...
  9. <script src="/Scripts/adminlte/app.js?v=..."></script>
  10. <script src="/Scripts/Site/Serene.Script.js?v=..."></script>
  11. <script src="/Scripts/Site/Serene.Web.js?v=..."></script>
  12. ...

Enabled 为 true 时,脚本将被合并成为:

  1. <script src="/DynJS.axd/Bundle.Libs.js?v=..."></script>
  2. <script src="/DynJS.axd/Bundle.Site.js?v=..."></script>
  3. ...

这两个包在内存中生成,并包含所有在 ScriptBundles.json 文件配置的脚本。

他们同样使用 GZIP 压缩并在内存中缓存(以压缩的形式),因此我们的脚本将消耗更少的带宽和服务请求。

现在,我们的脚本文件压缩到 600KB,而不是之前的 3000KB,减少了 80% 的大小。

Enabling Minification

在启用合并之后,你也可以在 web.config 设置中启用脚本压缩,把 Minimize 属性设置为 true:

  1. <add key="ScriptBundling" value="
  2. { Enabled: true, Minimize: true, UseMinJS: false }" />

请注意,Minimize 属性只有同时启用合并属性才能工作。

UglifyJS 库是用来压缩 js 文件。它被用在 合并/压缩 之前,因此我们的合并将会减少 40%,但是更难以阅读,因此只在生产发布时启用压缩设置。

现在我们合并及压缩后的脚本文件将变为 375 KB,而不是之前的 3000 KB,与初始大小相比降低了 87% 或 1/8。

UseMinJS Setting

压缩操作将花点时间,并且第一次请求站点时,根据服务器的速度,可能需要花费 5-40 秒甚至更长时间。

由于压缩只在应用程序启动时执行一次,所以不会影响其他请求。

无论如何,如果在第一个请求时,你还需要更高的性能,你可以要求 Serenity 重用在磁盘上可用的已压缩文件。

UseMinJS 设置为 true:

  1. <add key="ScriptBundling" value="
  2. { Enabled: true, Minimize: true, UseMinJS: true }" />

当该设置为 ON 时,在压缩文件前,例如 jquery-ui-1.11.4.js,Serenity 将首先检查 jquery-ui-1.11.4.min.js 文件是否存在磁盘上。如果存在,将直接使用该文件,而不再使用 UglifyJS 压缩文件。否则它将运行 UglifyJS 压缩文件。

Serene 包含所有脚本库的压缩版本,也包括 Serenity 的脚本,因此该设置将加速初始启动时间。

还有一个你应该小心避免的风险。如果你手动修改脚本库,请确保你手动压缩并同时修改它的 .min.js 文件,否则启用合并时,生产环境可能会运行旧版本的脚本。

Serenity 如何修改 _LayoutHead.cshtml 包含的脚本文件?

如果查看 _LayoutHead.cshtml,你可能会注意到文件中的这些代码:

  1. @Html.Script("~/Scripts/jquery.cropzoom.js")
  2. @Html.Script("~/Scripts/jquery.fileupload.js")
  3. @Html.Script("~/Scripts/jquery.iframe-transport.js")

当禁用合并时,这些声明生成如下 HTML 代码:

  1. <script src="/Scripts/jquery.cropzoom.js"></script>
  2. <script src="/Scripts/jquery.fileupload.js"></script>
  3. <script src="/Scripts/jquery.iframe-transport.js"></script>

Html.Script 是 Serenity 的扩展方法,因此当开启合并时,Serenity 将首先检查合并内容是否包含该脚本,而不是生成 HTML 代码。

假设合并包含 Bundle.Lib.js 脚本,Serenity 将生成下面的代码:

  1. <script src="/DynJS.axd/Bundle.Libs.js?v=..."></script>

但是,对于同一合并中包括其他 Html.Script 调用,Serenity 将不会生成任何东西。因此,即使你调用 Html.Script 50 次,在页代码中只会得到一个 <script> 的输出。

脚本标签中的 v=p53uqJ… 是什么?

这是脚本的版本号或哈希值。不管是否启用合并,当你使用 Html.Script 时,都会在包含的脚本中添加这些哈希值。该哈希值允许浏览器缓存脚本直到哈希值发生变化。当脚本内容更改时,其哈希值也会变化,所以浏览器不会缓存并使用较旧版本的脚本。

这就是使用 Serenity 应用程序不会有脚本缓存问题的原因。