ASP.NET Core 中的捆绑和缩小静态资产

最近在B站上看到杨旭老师的ASP.NET Core 3.x 入门视频(完结)的第三节的ASP.NET视频教程,里面提到到ASP.NET Core 中的捆绑和缩小静态资产,可以在微软官方文档ASP.NET Core 中的捆绑和缩小静态资产,特此记录一下,感兴趣的可以直接查看官方文档。

ASP.NET Core 中的捆绑和缩小静态资产

2020/09/02
作者:Scott AddieDavid Pine
本文介绍应用捆绑和缩小的好处,包括如何在 ASP.NET Core Web 应用中使用这些功能。

什么是捆绑和缩小

捆绑和缩小是可以在 Web 应用中应用的两个不同的性能优化。 捆绑和缩小一起使用,可减少服务器的请求数并减小请求的静态资产的大小,从而提高性能。
捆绑和缩小主要缩短第一个页面请求加载时间。 请求网页后,浏览器会缓存静态资产(JavaScript、CSS 和图像)。 因此,在请求相同资产的同一站点上请求相同的一个或多个页面时,捆绑和缩小不会提高性能。 如果未在资产上正确设置 expires 标头,且未使用捆绑和缩小,则浏览器的新鲜度启发会在几天后将资产标记为过期。 此外,浏览器还需要对每个资产进行验证请求。 在这种情况下,即使在第一个页面请求后,捆绑和缩小仍能提高性能。

捆绑

捆绑将多个文件合并到单个文件中。 捆绑可减少呈现 Web 资产(如网页)所需的服务器请求数。 可以专门为 CSS、JavaScript 等创建任意数量的单个捆绑。文件越少,从浏览器到服务器或从提供应用程序的服务的 HTTP 请求就越少。 这会提高第一页加载性能。

缩小

缩小在不更改功能的情况下从代码中删除不必要的字符。 因此,请求的资产(如 CSS、图像和 JavaScript 文件)的大小大幅减小。 缩小的常见副作用包括将变量名称缩短为一个字符、删除注释和不必要的空格。
考虑以下 JavaScript 函数:

AddAltToImg = function (imageTagAndImageID, imageContext) {
    ///<signature>
    ///<summary> Adds an alt tab to the image
    // </summary>
    //<param name="imgElement" type="String">The image selector.</param>
    //<param name="ContextForImage" type="String">The image context.</param>
    ///</signature>
    var imageElement = $(imageTagAndImageID, imageContext);
    imageElement.attr('alt', imageElement.attr('id').replace(/ID/, ''));
}

缩小将函数缩减为以下内容:

AddAltToImg=function(t,a){var r=$(t,a);r.attr("alt",r.attr("id").replace(/ID/,""))};

除了删除注释和不必要的空格外,还进行了以下参数和变量名称重命名:

原始 重命名
imageTagAndImageID t
imageContext a
imageElement r

捆绑和缩小的影响

操作 使用捆绑/缩小 不使用捆绑/缩小 更改
文件请求 7 18 157%
传输的 KB 156 264.68 70%
加载时间(毫秒) 885 2360 167%

对于 HTTP 请求标头,浏览器非常详细。 捆绑时,已发送的总字节数指标明显减少。 加载时间显示了显著改进,但本示例在本地运行。 将捆绑和缩小与通过网络传输的资产结合使用时,可实现更高的性能提升。

选择捆绑和缩小策略

MVC 和 Razor Pages 项目模板提供了一种用于捆绑和缩小的解决方案,它们构成 JSON 配置文件。 第三方工具(如 Grunt 任务运行程序)以更复杂的方式完成相同的任务。 开发工作流需要捆绑和缩小之外的其他处理(如 linting 和图像优化)时,第三方工具非常适用。 通过使用设计时捆绑和缩小,在应用部署之前创建缩小文件。 在部署之前进行捆绑和缩小具有减少服务器负载的优点。 但是,必须认识到,设计时捆绑和缩小会增加生成的复杂性,并且仅适用于静态文件。

配置捆绑和缩小

备注

需要将 BuildBundlerMinifier NuGet 包添加到项目中使其正常工作。
在 ASP.NET Core 2.1 或更高版本中,将名为 bundleconfig.json 的新 JSON 文件添加到 MVC 或 Razor Pages 项目根目录。 在该文件中包含以下 JSON 作为起点:

[
  {
    "outputFileName": "wwwroot/css/site.min.css",
    "inputFiles": [
      "wwwroot/css/site.css"
    ]
  },
  {
    "outputFileName": "wwwroot/js/site.min.js",
    "inputFiles": [
      "wwwroot/js/site.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    "sourceMap": false
  }
]

bundleconfig.json 文件定义每个捆绑的选项。 在前面的示例中,为自定义 JavaScript (wwwroot/js/site.js) 和样式表 (wwwroot/css/site.css) 文件定义了单一捆绑配置 。

配置选项包括:

  • outputFileName:要输出的捆绑文件的名称。 可包含 bundleconfig.json 文件中的相对路径。 (必需)
  • inputFiles:要捆绑在一起的文件数组。 这些是配置文件的相对路径。 可以选择使用空值,*这将导致输出文件为空。 支持 glob 模式。 - minify:输出类型的缩小选项。 可选,默认值 - minify: { enabled: true }
  • sourceMap:指示是否为捆绑的文件生成源映射的标记。 可选,默认值 - false
  • sourceMapRootPath:用于存储所生成的源映射文件的根路径。

向工作流添加文件

假设添加了额外的 custom.css 文件,类似于以下内容:

.about, [role=main], [role=complementary] {
    margin-top: 60px;
}

footer {
    margin-top: 10px;
}

若要缩小 custom.css 并将其与 site.css 捆绑到 site.min.css 文件中,请将相对路径添加到 bundleconfig.json :

[
  {
    "outputFileName": "wwwroot/css/site.min.css",
    "inputFiles": [
      "wwwroot/css/site.css",
      "wwwroot/css/custom.css"
    ]
  },
  {
    "outputFileName": "wwwroot/js/site.min.js",
    "inputFiles": [
      "wwwroot/js/site.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    "sourceMap": false
  }
]

备注

或者,可以使用以下通配模式:

"inputFiles": ["wwwroot/**/!(*.min).css" ]

此通配模式匹配所有 CSS 文件,并排除缩小的文件模式。

生成应用程序。 打开 site.min.css 并注意 custom.css 的内容将追加到文件末尾 。

基于环境的捆绑和缩小

最佳做法是,应在生产环境中使用应用的捆绑文件和缩小文件。 在开发过程中,原始文件可简化应用的调试。
使用视图中的环境标记帮助程序指定要包含在页面中的文件。 环境标记帮助程序仅在特定环境中运行时呈现其内容。
以下 environment 标记将在 Development环境中运行时呈现未处理的 CSS 文件:

<environment include="Development">
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</environment>

以下 environment标记将在非 Development 环境中运行时呈现捆绑的和缩小的 CSS 文件。 例如,在 ProductionStaging 中运行将触发这些样式表的呈现:

<environment exclude="Development">
    <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
          asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
          asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
    <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>

从 Gulp 使用 bundleconfig.json

在某些情况下,应用的捆绑和缩小工作流需要额外处理。 示例包括图像优化、缓存清除和 CDN 资产处理。 为了满足这些要求,可以将捆绑和缩小工作流转换为使用 Gulp。

手动转换捆绑和缩小工作流以使用 Gulp

将 package.json 文件(包含以下 devDependencies)添加到项目根:
警告

gulp-uglify 模块不支持 ECMAScript (ES) 2015/ES6 和更高版本。 安装 gulp-terser 而不是 gulp-uglify 来使用 ES2015/ES6 或更高版本。

"devDependencies": {
  "del": "^3.0.0",
  "gulp": "^4.0.0",
  "gulp-concat": "^2.6.1",
  "gulp-cssmin": "^0.2.0",
  "gulp-htmlmin": "^3.0.0",
  "gulp-uglify": "^3.0.0",
  "merge-stream": "^1.0.1"
}

通过在与 package.json 相同的级别运行以下命令来安装依赖项:

npm i

安装 Gulp CLI 作为全局依赖项:

npm i -g gulp-cli

将以下 gulpfile.js 文件复制到项目根:

'use strict';

var gulp = require('gulp'),
    concat = require('gulp-concat'),
    cssmin = require('gulp-cssmin'),
    htmlmin = require('gulp-htmlmin'),
    uglify = require('gulp-uglify'),
    merge = require('merge-stream'),
    del = require('del'),
    bundleconfig = require('./bundleconfig.json');

const regex = {
    css: /\.css$/,
    html: /\.(html|htm)$/,
    js: /\.js$/
};

gulp.task('min:js', async function () {
    merge(getBundles(regex.js).map(bundle => {
        return gulp.src(bundle.inputFiles, { base: '.' })
            .pipe(concat(bundle.outputFileName))
            .pipe(uglify())
            .pipe(gulp.dest('.'));
    }))
});

gulp.task('min:css', async function () {
    merge(getBundles(regex.css).map(bundle => {
        return gulp.src(bundle.inputFiles, { base: '.' })
            .pipe(concat(bundle.outputFileName))
            .pipe(cssmin())
            .pipe(gulp.dest('.'));
    }))
});

gulp.task('min:html', async function () {
    merge(getBundles(regex.html).map(bundle => {
        return gulp.src(bundle.inputFiles, { base: '.' })
            .pipe(concat(bundle.outputFileName))
            .pipe(htmlmin({ collapseWhitespace: true, minifyCSS: true, minifyJS: true }))
            .pipe(gulp.dest('.'));
    }))
});

gulp.task('min', gulp.series(['min:js', 'min:css', 'min:html']));

gulp.task('clean', () => {
    return del(bundleconfig.map(bundle => bundle.outputFileName));
});

gulp.task('watch', () => {
    getBundles(regex.js).forEach(
        bundle => gulp.watch(bundle.inputFiles, gulp.series(["min:js"])));

    getBundles(regex.css).forEach(
        bundle => gulp.watch(bundle.inputFiles, gulp.series(["min:css"])));

    getBundles(regex.html).forEach(
        bundle => gulp.watch(bundle.inputFiles, gulp.series(['min:html'])));
});

const getBundles = (regexPattern) => {
    return bundleconfig.filter(bundle => {
        return regexPattern.test(bundle.outputFileName);
    });
};

gulp.task('default', gulp.series("min"));

运行 Gulp 任务

若要在 Visual Studio 中生成项目之前触发 Gulp 缩小任务:

  1. 安装 BuildBundlerMinifier NuGet 包。
  2. 将以下 MSBuild 目标添加到项目文件:
<Target Name="MyPreCompileTarget" BeforeTargets="Build">
  <Exec Command="gulp min" />
</Target>

在此示例中,MyPreCompileTarget 目标内定义的所有任务在预定义的 Build 目标之前运行。 Visual Studio 的输出窗口中显示类似于以下内容的输出:

1>------ Build started: Project: BuildBundlerMinifierApp, Configuration: Debug Any CPU ------
1>BuildBundlerMinifierApp -> C:\BuildBundlerMinifierApp\bin\Debug\netcoreapp2.0\BuildBundlerMinifierApp.dll
1>[14:17:49] Using gulpfile C:\BuildBundlerMinifierApp\gulpfile.js
1>[14:17:49] Starting 'min:js'...
1>[14:17:49] Starting 'min:css'...
1>[14:17:49] Starting 'min:html'...
1>[14:17:49] Finished 'min:js' after 83 ms
1>[14:17:49] Finished 'min:css' after 88 ms
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========

其他资源

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,158评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,600评论 1 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,785评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,655评论 0 220
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,075评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,002评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,146评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,918评论 0 211
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,671评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,838评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,318评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,636评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,343评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,187评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,982评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,126评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,934评论 2 279

推荐阅读更多精彩内容