使用 YUI-Compressor Maven 插件处理 Spring Boot Web 应用程序
YUI-Compressor maven 插件用于压缩 Spring Boot Web 应用开发的 JS 文件
引言
在我从事 Java 项目时,我一直想知道如何最好地压缩 JavaScript 文件。这是为生产部署强化应用程序的一部分。对我来说,对于个人项目,压缩和不压缩 JavaScript 差别不大,只要应用程序不暴露敏感数据。但对于真实有价值的应用程序,压缩 JavaScript 文件是提高应用程序安全性的最基本措施。我知道这对于 ASP.NET MVC 应用程序来说很容易做到,但在 Java 应用程序开发中,我一直没有时间去研究,直到现在。有很多方法,最常见的方法是在构建过程中使用 Maven 插件来压缩 JavaScript 文件。我决定尝试一下。
问题是我为什么不使用 Node.js 来完成我需要的事情。对我自己的项目来说,Node.js 的功能过于强大。对于开发最前沿技术的应用程序,Node.js 是合适的工具。当处理遗留应用程序和遗留 JavaScript 时,使用 Maven 插件来压缩 JavaScript 文件将是一种理想的方法。将此类插件添加到现有应用程序并使其正常工作所付出的努力将涉及最少的研究和精力。唯一的问题是我对这些插件不够了解。这就是我决定做这样一个教程的原因。令我惊讶的是,我确实发现了一些非常有用的东西。我将在后面的部分讨论这一点。
让我先解释一下示例应用程序的整体架构。它还将涵盖本教程的整体结构。废话不多说,我们进入下一节。
整体架构
坦白说,示例应用程序并没有太多架构。我去年做了不少教程。很容易选择一个现有的示例应用程序,然后添加 Maven 插件来压缩资源。结果发现,在我去年所做的所有教程中,有一个非常适合本教程。那就是关于使用 AngularJS 的 HTML 编辑器教程。 这是我原始教程的链接 - 使用 AngularJS 的可重用 HTML 编辑器控件。本教程中的示例应用程序有三个 JavaScript 文件和一个 CSS 文件。其中一个 JavaScript 文件有 630 多行。很高兴看到这个文件被压缩并在包含到 HTML 后能够正常工作。而且它确实做到了。
既然我已经有了示例应用程序,我只需要找到一个 Maven 插件,并将其添加到项目中。最大的问题是我需要未压缩的资源文件才能进行问题排查,并且我希望在准备部署应用程序时使用压缩后的文件。你可能知道,在运行时或打包时,必须以某种方式决定选择哪种类型的文件包含在 HTML 代码中。我应该选择哪种?结果发现,在运行时选择未压缩和压缩文件之间的区别更容易。ASP.NET MVC 就是这样利用应用程序启动时的 bundle 对象工作的。如果有人知道如何动态加载资源文件,就可以很容易地将其复制到基于 Spring Boot 的应用程序中。这可以使用 Thymeleaf 模板引擎来实现服务器端 HTML 渲染。
我的想法是有一个配置文件。其中有一个属性指示应用程序将运行的环境,然后应用程序将相应地加载资源文件。在构建时,我将配置构建,使压缩后的文件与原始资源文件位于同一文件夹中。然后使用配置文件决定何时加载压缩后的资源文件。下一节将分为两个小节。第一个小节将讨论如何添加 Maven 插件来压缩资源文件。第二个小节将讨论如何使用配置文件加载资源。
在 Spring Boot Web 应用程序中使用压缩后的资源
我已经解释了如何构建和利用压缩后的资源。现在是时候解释这两个步骤是如何完成的了。在下一小节中,我将解释为什么我选择 yui-compressor maven 插件以及如何将其添加到构建配置中。
使用 Yui-Compressor 插件
我选择 yui-compressor 作为压缩 Web 应用程序资源文件的工具的原因是,该工具已经存在很长时间了,而且根据我阅读的评论,它的 maven 插件是一个很棒的插件。它还有详细的文档解释了该插件的用法。我没有查找其他插件获取更多信息。对我来说,我更信任一个文档完善且存在时间长的插件。
将此插件添加到构建过程并不难。难的是配置插件并自定义资源文件压缩过程。在本教程中,我只使用了最基本的配置。对于您读者来说,请查阅相关文档以了解该插件的高级用法。
我在构建中进行的所有更改都可以在我的 Maven POM 文件中找到。如果您不理解下面描述的更改,请参考此文件,它会很清楚。
我做的第一件事是将 yui-compressor 插件依赖项添加到 POM 文件的 dependencies
部分。
...
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>net.alchim31.maven</groupId>
<artifactId>yuicompressor-maven-plugin</artifactId>
<version>1.5.1</version>
</dependency>
</dependencies>
...
在上面的代码片段中,我添加了两个新的依赖项,第一个是用于服务器端渲染 HTML 源代码的 ThymeLeaf starter 依赖项。第二个是加粗显示的,就是我们需要的用于压缩资源文件的 yui-compressor maven 插件。
POM 文件还没有完成。下一步是配置插件,如下所示
...
<build>
<plugins>
...
<plugin>
<groupId>net.alchim31.maven</groupId>
<artifactId>yuicompressor-maven-plugin</artifactId>
<version>1.5.1</version>
<executions>
<execution>
<goals>
<goal>compress</goal>
</goals>
<configuration>
<force>true</force>
<excludes>
<exclude>**/angularjs/**/*.*</exclude>
<exclude>**/bootstrap/**/*.*</exclude>
<exclude>**/jquery/**/*.*</exclude>
</excludes>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
...
这部分可能非常复杂。但我不需要复杂的压缩处理。所以上面的代码片段很容易理解。首先,我将插件配置为在 compress 阶段运行。我猜这是 jar 文件生成阶段。我对这个特定的阶段不太熟悉,无法做更多解释。如果您想了解更多,请自行查找。在插件的 configuration
部分,我将 force
标志设置为 "true
"。这是为了删除先前已压缩的资源文件,以便可以再次压缩原始文件。我测试过,如果不设置这个标志,会有一些已解决的编译警告。警告抱怨最小化文件已存在,需要设置此标志才能删除此警告。据我所知,此标志用于定义压缩器的行为,是保留现有最小化文件还是删除这些最小化文件以便创建新文件。还有一个 excludes
部分,在该部分中,我将这三个目录中的所有文件都添加了进来。原因是这些文件是第三方资源文件,它们已经被压缩过了,如果再次压缩会引发编译错误。所以它们可以被排除在处理之外。
不算太糟,对吧?我不会破坏乐趣。您,读者,将在如何运行示例应用程序部分看到此插件的实际应用。在下一小节中,我将解释如何使用 ThymeLeaf 通过配置加载压缩或未压缩的文件。
通过配置使用压缩/未压缩的资源文件
在我 原始教程 中,index HTML 文件是一个静态文件,位于 resources/static 文件夹中。这意味着 CSS 或 JS 文件等资源文件在此文件中被硬编码。这对于我的示例应用程序不起作用。现在我既有原始资源文件,也有压缩后的资源文件。我不能在同一个文件中同时拥有它们,而必须选择使用哪一个。我需要的是,在开发应用程序时,我想使用未压缩的资源文件。当准备将应用程序部署到生产环境时,应该使用压缩后的资源。这就是为什么我需要一种方法来同时拥有两套资源文件,并在适当的时候加载其中一套而不加载另一套。
此问题的解决方案已通过 ASP.NET MVC 通过应用程序启动时的 bundle 配置提供。我可以在那里使用 .NET 的 ConfigurationManager
加载配置值,然后决定是捆绑未压缩还是压缩的资源文件。我可以在我的 Spring Boot 应用程序中做同样的事情。为了实现这一点,我需要一种在服务器端渲染 HTML 文件的方法。然后我可以自定义要为该页面加载的资源文件。
我已经将 ThymeLeaf 模板引擎依赖项包含在了我的 Maven POM 文件中。要使用它,我需要创建一个 ThymeLeaf HTML 模板。在文件的顶部,我必须使用一个 for
循环来渲染所有 CSS 文件,在底部,我必须使用一个 for
循环来渲染 JS 文件。这是我创建的 HTML 模板文件
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,
initial-scale=1, shrink-to-fit=no">
<title>AngularJS HTML Editor Directive - by Han</title>
<link href="/assets/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link th:each="cssFile: ${allCssFiles}" th:href="${cssFile}" rel="stylesheet">
</head>
<body>
<div class="container" ng-app="testSampleApp"
ng-controller="testSampleController as vm">
<div class="row">
<div class="col-xs-12">
<h3>HTML Editor</h3>
</div>
</div>
<div info-display op-success="vm.opSuccess"
msg-to-show="vm.statusMessage" ></div>
<div class="row">
<div class="col-xs-6 text-right">
<button class="btn btn-info"
ng-click="vm.loadHtmlContent()">Load</button>
</div>
<div class="col-xs-6">
<button class="btn btn-success"
ng-click="vm.saveHtmlContent()">Save</button>
</div>
</div>
<div html-editor html-text="vm.htmlText"></div>
<hr>
<footer class="footer">
<p>© 2019, Han Bo Sun.</p>
</footer>
</div>
<script type="text/javascript" src="/assets/jquery/js/jquery.min.js"></script>
<script type="text/javascript"
src="/assets/bootstrap/js/bootstrap.min.js"></script>
<script type="text/javascript"
src="/assets/angularjs/1.7.5/angular.min.js"></script>
<script type="text/javascript"
src="/assets/angularjs/1.7.5/angular-resource.min.js"></script>
<script type="text/javascript" th:each="jsFile: ${allJsFiles}"
th:src="${jsFile}"></script>
</body>
</html>
这是我之前教程中的 HTML 编辑器索引页。我所做的是将 ThymeLeaf 模板引擎集成到页面渲染中。有三个不同的更改,都用粗体突出显示。这两个是资源文件将被渲染的地方。我所要做的就是在返回页面的 API 方法中提供 CSS 文件名和 JS 文件名(这些文件的 URL)的列表。然后 ThymeLeaf 模板引擎会将这些文件名渲染到构造的页面上。
这是返回索引页面的 API 方法
package org.hanbo.boot.rest.controllers;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class IndexController
extends PageControllerBase
{
@Value("${useMinifiedFiles}")
private Optional<String> useMinifiedFiles;
@GetMapping(value="/index")
public ModelAndView indexPage()
{
boolean useMinifiedFiles = isAppUsingMinifiedFiles();
List<String> allJsFiles = allAppJsFiles(useMinifiedFiles);
List<String> allCssFiles = allAppCssFiles(useMinifiedFiles);
ModelAndView retVal = new ModelAndView();
retVal.setViewName("index");
retVal.addObject("allJsFiles", allJsFiles);
retVal.addObject("allCssFiles", allCssFiles);
return retVal;
}
private boolean isAppUsingMinifiedFiles()
{
boolean retVal = false;
if (useMinifiedFiles.isPresent())
{
retVal = Boolean.parseBoolean(useMinifiedFiles.get());
}
return retVal;
}
}
在上面的类中,我声明了一个名为 useMinifiedFiles
的实例属性。它的值是通过 application.properties 文件注入设置的,因此使用了 @Value
注解。在 application.properties 文件中声明的相应属性的名称与实例属性名称相同,都是 useMinifiedFiles
。启动后,Spring 框架会将此属性的值从 application.properties 文件注入到此实例属性中,并且可以立即使用。我还使用了 Optional
作为包装器。这将允许我在使用值之前检查它是否可用。在 application.properties 文件中,我可以设置值为 "true
"、"false
" 或空字符串作为可注入的值。
您可能会问,allAppJsFiles()
和 allAppCssFiles()
方法在哪里?它们在基类中。我将它们放在名为 PageControllerBase
的基类中,以便这两个方法可以在多个 API 方法中重用。这是 PageControllerBase
类
package org.hanbo.boot.rest.controllers;
import java.util.List;
import java.util.ArrayList;
public class PageControllerBase
{
public List<String> allAppCssFiles(boolean useMinifiedFiles)
{
List<String> retVal = new ArrayList<String>();
if (useMinifiedFiles)
{
retVal.add("/assets/styles/htmlEditor-min.css");
retVal.add("/assets/styles/infoDisplay-min.css");
}
else
{
retVal.add("/assets/styles/htmlEditor.css");
retVal.add("/assets/styles/infoDisplay.css");
}
return retVal;
}
public List<String> allAppJsFiles(boolean useMinifiedFiles)
{
List<String> retVal = new ArrayList<String>();
if (useMinifiedFiles)
{
retVal.add("/assets/app/js/directives/infoDisplay/infoDisplay-min.js");
retVal.add("/assets/app/js/directives/htmlEditor/htmlEditor-min.js");
retVal.add("/assets/app/js/app-min.js");
}
else
{
retVal.add("/assets/app/js/directives/infoDisplay/infoDisplay.js");
retVal.add("/assets/app/js/directives/htmlEditor/htmlEditor.js");
retVal.add("/assets/app/js/app.js");
}
return retVal;
}
}
这很简单。有两个方法,一个用于创建 CSS 文件列表。另一个用于创建 JS 文件列表。两个方法都使用相同的方法来创建列表,接受一个指示是否需要压缩资源文件的参数。然后 if
... else
块将硬编码资源文件 URL 并将其放入列表中。结合加载 useMinifiedFiles
配置值的控制器类,我可以控制何时使用压缩文件以及何时不使用。一旦列表创建完毕,它们就会被打包到一个 ModelAndView
对象中。然后将其传递给由 ThymeLeaf 模板引擎构建的 HTML 视图,以创建最终的页面供浏览器显示。
这是我的 application.properties,它只有一行
useMinifiedFiles=true
我将这个 application.properties 文件移到了项目的根目录,这样我就可以通过命令行参数传递它。这是最后的技巧。我可以有两个不同的 application.properties 文件。一个用于开发用途,一个用于生产部署用途。为了简单起见,我只提供了一个文件,我只是修改了值,然后重新启动应用程序来查看效果。
至此,我已经讨论了本示例应用程序的所有技术细节。在下一节中,我将向您展示如何运行示例应用程序并查看使用压缩资源文件的效果。
如何运行示例应用程序
首先,为了运行这个示例应用程序的构建,请将任何扩展名为 ".sj" 的文件重命名为 ".js"。
接下来,您可以将 useMinifiedFiles
的属性值更改为 "true
"、"false
" 或空 string
。这在 application.properties 文件中完成。根据您的需求,设置为 "true
" 以查看压缩后的资源文件;否则设置为 "false
" 或空字符串。请注意,此文件不会打包到 jar 文件中,因此您可以随时更改其值。更改后,您需要重新启动 Web 应用程序,更改才会生效。
现在是时候构建和打包 Web 应用程序了。在示例项目的根目录下,运行以下命令
maven clean install
我对这一步有一些评论,首先是 YUI-compressor 集成了 JsLint,所以当我们进行打包时,它实际上检查了我的 JS 代码的质量,并发现了 100 多个警告。这是错误的屏幕截图。
结果我拥有的 JavaScript 不如我想象的多。但错误很容易修复。抱怨主要是我使用了多个 var
关键字来声明方法中使用的局部变量。一旦我修复了所有这些,构建过程中的警告就全部消失了。这是一个意外的惊喜。在此之前,我无法检查我的 JavaScript 文件的代码质量,现在我有了方法。我需要做一些进一步的研究,看看是否有方法可以只扫描 JavaScript 文件而不进行压缩。目前,检查和压缩按照我希望的那样进行。
我注意到的另一个观察是压缩进行了两次。我认为第一次是针对 target 文件夹中的文件的压缩。第二次是创建最终 jar 文件的文件的压缩。这不是一个好的解释。我自己都不相信。另一个合理的解释是这个 maven 插件中存在一个 bug,因此它会运行两次压缩。这不会对最终的 jar 文件产生任何负面影响,所以无关紧要。
现在可以运行示例应用程序了。这是启动应用程序的命令
java -jar target/hanbo-js-minify-sample-1.0.1.jar
--spring.config.location=file:./application.properties
与我过去教程中使用的命令不同,这个命令需要一个额外的命令行参数,即 application.properties 文件的位置。这允许我在不重新构建 jar 文件的情况下更改配置。我所要做的就是停止应用程序,更改配置,然后使用相同的命令重新启动应用程序。
application.properties 文件只有一行,我只需要将这一行的值更改为 true
或 false
,然后重新启动应用程序,我就会看到应用程序加载的资源文件的差异。
要查看应用程序的运行情况,请在浏览器中使用以下 URL
https://:8080/index
这是示例应用程序在浏览器中运行的截图
如果我将配置属性设置为 true
并运行应用程序。一旦我打开开发者工具检查源代码,我就可以看到此应用程序正在使用压缩后的资源
如果我将配置属性设置为 false
并运行应用程序。我应该能够看到相同的应用程序。一旦我打开开发者工具检查源代码,我就可以看到此应用程序正在使用未压缩的资源
正如您所见,设置何时使用压缩资源以及何时不使用非常容易。这允许我在开发时进行问题排查,并在准备部署到生产环境时部署压缩资源。
除了压缩/未压缩的资源文件外,我还修复了应用程序中的一个 bug,该 bug 在我选择一段文本并在该选区的开头和结尾应用标记时出现。如果文本是字符串的末尾,则最后一个字符将被排除在标记之外。这是我在原始教程中未修复的一个恼人的 bug。在这里,我能够解决它。
摘要
正如我一开始所说,本教程是一个非常简单的教程。我所做的就是采用一个现有的 AngularJS 应用程序,并在构建过程中添加压缩资源文件。我还解释了如何在运行时加载压缩和未压缩的资源文件。作为奖励,我展示了如何使用命令行参数运行应用程序来定位 application.properties 文件,这样它就不需要打包到 jar 文件中。这个简单的方案允许我设置何时使用压缩资源以及何时不使用。这允许我在开发时进行问题排查,并在准备部署到生产环境时部署压缩资源。
令我感到惊喜的是,YUI-Compressor maven 插件包含了 JsLint 功能,它帮助我发现了我 JavaScript 代码中一些令人不快的问题。这些问题很容易修复。我还修复了原始 JavaScript 代码中的一个 bug,从而完成了示例应用程序。请查看本教程的内容,希望有些内容能令您满意。
展望未来,我将进一步研究如何使用 JsLint 进行语法检查而不是压缩,以及其他用于 ES6 JavaScript 模块文件语法检查和压缩的工具。后者有点复杂,希望这些可以在不久的将来收集到新的教程中。无论如何,我希望您喜欢这个教程,并从中找到一些用处。感谢您的光临。
历史
- 2022 年 8 月 2 日 - 初稿