Winse Blog

走走停停, 熙熙攘攘, 忙忙碌碌, 不知何畏.

Maven压缩js/css功能实践

为了节约网络带宽,一般在发布项目时对资源(js/css)文件进行压缩(去掉空行、精简代码等)。但是要做到兼容开发与生产还是的下一番功夫才行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ls -l src/main/webapp/static/assets/js/ | head
total 3120
-rwxrwxr--+ 1 winse None  24804 Aug 10 17:40 bootbox.js
-rwxrwxr--+ 1 winse None  71315 Aug 10 17:40 bootstrap.js
-rwxrwxr--+ 1 winse None  13905 Aug 10 17:40 bootstrap-colorpicker.js
-rwxrwxr--+ 1 winse None  49319 Aug 10 17:40 bootstrap-multiselect.js
...

$ ls -l target/dist/js/ | head
total 1368
-rwxrwx---+ 1 winse None   8943 Aug 19 16:53 bootbox-min.js
-rwxrwx---+ 1 winse None   8057 Aug 19 16:53 bootstrap-colorpicker-min.js
-rwxrwx---+ 1 winse None  38061 Aug 19 16:53 bootstrap-min.js
-rwxrwx---+ 1 winse None  18232 Aug 19 16:53 bootstrap-multiselect-min.js
...

项目中原本使用dist(压缩)、assets目录放置js/css等资源,在部署的时刻替换dist为assets,有点麻烦。首先想到的用nginx进行url重写,但是需要增加一个服务有点麻烦,能不能直接用spring来实现呢?

  • 自定义一个handler类

查看Spring的 mvc:resources 实现,相当于注册了一个 location -> ResourceHttpRequestHandler 的映射。 第一种尝试自动化的方式就是自定义handler类来进行资源的定位。增加 StaticRequestHandler 的处理类,增加配置 location 和 compressLocation 的配置:首先去查找压缩文件([NAME]-min.js),找不到然后再找源文件([NAME].js)位置。

主要修改 getResource 方法,具体完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
## java
public class StaticRequestHandler extends ResourceHttpRequestHandler {

  private final static Log logger = LogFactory.getLog(ResourceHttpRequestHandler.class);

  private String location;
  private String compressLocation;

  private Resource locationResource;
  private Resource compressLocationResource;

  public void setLocation(String location) {
      this.location = location;
  }

  public void setCompressLocation(String compressLocation) {
      this.compressLocation = compressLocation;
  }

  @Override
  public void afterPropertiesSet() throws Exception {
      super.afterPropertiesSet();

      this.locationResource = getWebApplicationContext().getResource(location);
      super.setLocations(Collections.singletonList(this.locationResource));

      this.compressLocationResource = getWebApplicationContext().getResource(compressLocation);
  }

  @Override
  protected Resource getResource(HttpServletRequest request) {
      String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
      if (path == null) {
          throw new IllegalStateException("Required request attribute '"
                  + HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set");
      }

      if (!StringUtils.hasText(path) || isInvalidPath(path)) {
          if (logger.isDebugEnabled()) {
              logger.debug("Ignoring invalid resource path [" + path + "]");
          }
          return null;
      }

      Resource res = null;
      if (path.endsWith(".css")) {
          res = findResource(compressLocationResource, path.substring(0, path.length() - 4) + ".min.css");
      } else if (path.endsWith(".js")) {
          res = findResource(compressLocationResource, path.substring(0, path.length() - 3) + ".min.js");
      }

      if (res == null) {
          res = findResource(locationResource, path);
      }

      return res;
  }

  private Resource findResource(Resource location, String path) {
      try {
          if (logger.isDebugEnabled()) {
              logger.debug("Trying relative path [" + path + "] against base location: " + location);
          }
          Resource resource = location.createRelative(path);
          if (resource.exists() && resource.isReadable()) {
              if (logger.isDebugEnabled()) {
                  logger.debug("Found matching resource: " + resource);
              }
              return resource;
          } else if (logger.isTraceEnabled()) {
              logger.trace("Relative resource doesn't exist or isn't readable: " + resource);
          }
      } catch (IOException ex) {
          logger.debug("Failed to create relative resource - trying next resource location", ex);
      }

      return null;
  }

}

## spring config
  <!-- 静态资源 -->
  <!-- <mvc:resources mapping="/static/**" location="/static/" /> -->

  <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
      <property name="mappings">
          <value>
              /static/assets/**=staticRequestHandler
          </value>
      </property>
  </bean>
  <bean id="staticRequestHandler" class="com.hotel.servlet.resource.StaticRequestHandler">
      <property name="location" value="/static/assets/" />
      <property name="compressLocation" value="/static/dist/" />
  </bean>

这种方式实现了自动定位压缩资源 min.js 的功能,但是压缩还是不能自动化而且不能实时的更新(min要单独压缩产生),并且调试和生产环境还是需要手动的修改配置来切换。

有没有更好的自动化的实现开发环境和生产环境分开呢?

  • Maven打包时压缩然后替换源文件

使用 yuicompressor-maven-plugin 插件压缩资源,然后把压缩资源打包放置到assets目录下。

注意: yuicomressor 插件的 nosuffix 配置为 true ! 这样压缩后的文件名和源文件名称才一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
## spring config
  <!-- 静态资源 -->
  <mvc:resources mapping="/static/**" location="/static/" />

## maven pom.xml
      <profile>
          <id>release</id>

          <build>
              <plugins>
                  <!-- http://alchim.sourceforge.net/yuicompressor-maven-plugin/compress-mojo.html -->
                  <plugin>
                      <groupId>net.alchim31.maven</groupId>
                      <artifactId>yuicompressor-maven-plugin</artifactId>
                      <version>1.3.2</version>
                      <executions>
                          <execution>
                              <id>compress_js_css</id>
                              <phase>process-resources</phase>
                              <goals>
                                  <goal>compress</goal>
                              </goals>
                          </execution>
                      </executions>
                      <configuration>
                          <encoding>UTF-8</encoding>
                          <nosuffix>true</nosuffix>
                          <skip>false</skip>

                          <jswarn>false</jswarn>
                          <nomunge>false</nomunge>
                          <preserveAllSemiColons>false</preserveAllSemiColons>

                          <sourceDirectory>src/main/webapp/static/assets</sourceDirectory>
                          <outputDirectory>${project.build.directory}/dist</outputDirectory>
                      </configuration>
                  </plugin>

                  <plugin>
                      <artifactId>maven-war-plugin</artifactId>
                      <version>2.6</version>
                      <configuration>
                          <webResources>
                              <resource>
                                  <directory>${project.build.directory}/dist</directory>
                                  <targetPath>static/assets</targetPath>
                                  <filtering>false</filtering>
                              </resource>
                          </webResources>
                      </configuration>
                  </plugin>
                  
              </plugins>
          </build>
      </profile>

war插件添加了自定义webResources资源,首先把压缩的文件拷贝到对应目录,maven发现文件已经存在就不会再拷贝同名的文件。这样源文件就相当于被替换成压缩的资源了。

总结

使用maven插件压缩打包,完美的解决js/css压缩导致的开发和生产不兼容问题。

后记

jsp使用了tag的地方总是会产生很多的空行,看着挺烦的。其实可以通过在jsp开头添加 trimDirectiveWhitespaces 属性来去掉空行:

1
<%@ page language="java" trimDirectiveWhitespaces="true" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>

–END

Comments