《最佳化介面設計的思路》系列:第十篇—網站的靜態資源怎麼獲取?

sum墨發表於2024-04-18

一、前言

大家好!我是sum墨,一個一線的底層碼農,平時喜歡研究和思考一些技術相關的問題並整理成文,限於本人水平,如果文章和程式碼有表述不當之處,還請不吝賜教。

作為一名從業已達六年的老碼農,我的工作主要是開發後端Java業務系統,包括各種管理後臺和小程式等。在這些專案中,我設計過單/多租戶體系系統,對接過許多開放平臺,也搞過訊息中心這類較為複雜的應用,但幸運的是,我至今還沒有遇到過線上系統由於程式碼崩潰導致資損的情況。這其中的原因有三點:一是業務系統本身並不複雜;二是我一直遵循某大廠程式碼規約,在開發過程中儘可能按規約編寫程式碼;三是經過多年的開發經驗積累,我成為了一名熟練工,掌握了一些實用的技巧。

前面的文章都是先說概念,再說怎麼設計和實現,今天我打算換一種寫法,從一個功能需求的實現來講一下靜態資源是如何訪問的?

功能需求如下:

  1. 現有一個後端應用,預設訪問方式如下:http://47.120.49.119:8080/#/
  2. 用電腦、平板、手機等裝置都可以訪問,且不同的裝置樣式要適配,前端做了兩套,但是訪問介面都是同一個;
  3. 由於沒有使用cdn,介面渲染的時候需要用到的字型庫、圖示庫和一些圖片也放在了後端應用;

光文字講需求可能不太直觀,我放一些圖片就好理解了,如下:

使用電腦訪問http://47.120.49.119:8080/#/出現的介面

使用手機或平板訪問http://47.120.49.119:8080/#/出現的介面

可以看到這兩個的樣式不一樣,元件也不一樣,但是介面都是同一個。

前端的資源如下

檔案有html、js、css,還有一些特殊的檔案如字型,檔案型別還是比較豐富的,且這樣的資源有兩份,一份是電腦端,一份是移動端。由於沒有使用cdn,我們需要透過後端服務來訪問這些資源。
那麼說到這裡不知道大家有沒有理解這個需求呢?其實簡單理解就是SpringBoot的介面訪問靜態資源,下面就開始講一下我是如何實現這個功能的。

二、功能實現

1. 從訪問一個index.html開始

(1)建立一個SpringBoot專案

這個我就不囉嗦了,使用ide或者https://start.spring.io/網站建立一個即可。

(2)引入SpringBoot模板引擎— Thymeleaf

pom.xml引入

<!-- thymeleaf -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

(3)在resources目錄下建立一個static資料夾,在static資料夾建立一個index.html

(4)application.properties新增如下配置

spring.thymeleaf.prefix=classpath:/static/
spring.thymeleaf.suffix=.html
spring.thymeleaf.mode=HTML

(5)寫一個IndexController.java

package com.summo.file.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {

  @GetMapping("/")
  public String index() {
    return "index";
  }
}

(6)訪問一下index.html

這個還是比較簡單的,我已經成功了,大家成功了嗎?如果發現返回的不是HTML而是字串,檢查一下IndexController的註解,是@Controller而不是@RestController,方法上也不要加 @ResponseBody。

2. 判斷當前的請求來自於電腦還是手機

(1)判斷邏輯分析

這個聽起來很難,其實很簡單,前端在訪問後端介面的時候,會攜帶一個USER-AGENT的請求頭,透過這個USER-AGENT請求頭就可以區分訪問來源,判斷邏輯程式碼如下:

/**
   * 校驗是否手機端
   *
   * @param request
   * @return
   */
public static boolean isFromMobile(HttpServletRequest request) {
  //1. 獲得請求UA
  String userAgent = request.getHeader("USER-AGENT").toLowerCase();
  //2.宣告手機和平板的UA的正規表示式
  // \b 是單詞邊界(連著的兩個(字母字元 與 非字母字元) 之間的邏輯上的間隔),
  // 字串在編譯時會被轉碼一次,所以是 "\\b"
  // \B 是單詞內部邏輯間隔(連著的兩個字母字元之間的邏輯上的間隔)
  String phoneReg = "\\b(ip(hone|od)|android|opera m(ob|in)i" + "|windows (phone|ce)|blackberry"
            + "|s(ymbian|eries60|amsung)|p(laybook|alm|rofile/midp" + "|laystation portable)|nokia|fennec|htc[-_]"
            + "|mobile|up.browser|[1-4][0-9]{2}x[1-4][0-9]{2})\\b";
  String tableReg = "\\b(ipad|tablet|(Nexus 7)|up.browser" + "|[1-4][0-9]{2}x[1-4][0-9]{2})\\b";

  // 3.移動裝置正則匹配:手機端、平板
  Pattern phonePat = Pattern.compile(phoneReg, Pattern.CASE_INSENSITIVE);
  Pattern tablePat = Pattern.compile(tableReg, Pattern.CASE_INSENSITIVE);
  if (null == userAgent) {
    userAgent = "";
  }
  // 4.匹配
  Matcher matcherPhone = phonePat.matcher(userAgent);
  Matcher matcherTable = tablePat.matcher(userAgent);
  if (matcherPhone.find() || matcherTable.find()) {
    //來自手機或者平板
    return true;
  } else {
    //來自PC
    return false;
  }
}

(2)新建兩個資料夾,將手機端和電腦端的index介面區分開來

(3)IndexController程式碼改一下

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class IndexController {

    @GetMapping("/")
    public String index(HttpServletRequest request) {
        if (isFromMobile(request)) {
            return "mp/index";
        } else {
            return "web/index";
        }
    }

    /**
     * 校驗是否手機端
     *
     * @param request
     * @return
     */
    public static boolean isFromMobile(HttpServletRequest request) {
        //1. 獲得請求UA
        String userAgent = request.getHeader("USER-AGENT").toLowerCase();
        //2.宣告手機和平板的UA的正規表示式
        // \b 是單詞邊界(連著的兩個(字母字元 與 非字母字元) 之間的邏輯上的間隔),
        // 字串在編譯時會被轉碼一次,所以是 "\\b"
        // \B 是單詞內部邏輯間隔(連著的兩個字母字元之間的邏輯上的間隔)
        String phoneReg = "\\b(ip(hone|od)|android|opera m(ob|in)i" + "|windows (phone|ce)|blackberry"
            + "|s(ymbian|eries60|amsung)|p(laybook|alm|rofile/midp" + "|laystation portable)|nokia|fennec|htc[-_]"
            + "|mobile|up.browser|[1-4][0-9]{2}x[1-4][0-9]{2})\\b";
        String tableReg = "\\b(ipad|tablet|(Nexus 7)|up.browser" + "|[1-4][0-9]{2}x[1-4][0-9]{2})\\b";

        // 3.移動裝置正則匹配:手機端、平板
        Pattern phonePat = Pattern.compile(phoneReg, Pattern.CASE_INSENSITIVE);
        Pattern tablePat = Pattern.compile(tableReg, Pattern.CASE_INSENSITIVE);
        if (null == userAgent) {
            userAgent = "";
        }
        // 4.匹配
        Matcher matcherPhone = phonePat.matcher(userAgent);
        Matcher matcherTable = tablePat.matcher(userAgent);
        if (matcherPhone.find() || matcherTable.find()) {
            //來自手機或者平板
            return true;
        } else {
            //來自PC
            return false;
        }
    }
}

電腦端訪問

手機端訪問

到這裡,一個介面區分電腦端和手機端的功能實現了,由於index.html引入了一些js、css、ttf、woff 等靜態資源,這些檔案如何訪問呢?繼續看。

3. 其他型別的靜態資源如何訪問

mp目錄下檔案結構如下

index.html想要引入js和css目錄下的檔案,路徑應該這樣寫

<!DOCTYPE html>
<html>
<head>
    <meta charset=utf-8>
    <meta name=viewport content="width=device-width,initial-scale=1">
    <title>summo-sbmy-front-mp</title>
    <link href=/mp/css/app.css rel=stylesheet>
</head>
<body>
<div id=app></div>
<script type=text/javascript src=/mp/js/manifest.js></script>
<script type=text/javascript src=/mp/js/vendor.js></script>
<script type=text/javascript src=/mp/js/app.js></script>
</body>
</html>

這樣就可以訪問到指定目錄的資源了

整體邏輯其實很簡單,我覺得最容易出問題的地方在路徑的設定,有時候檔案和程式碼都弄好了,但就是載入不出來,基本上就是路徑設定錯了。所以大家要是自己實現的話,儘量先按照我的檔案路徑來,先搞成功再說。

三、擴充套件知識

1. 給介面傳遞動態值

在Spring MVC中,將變數傳遞到檢視通常透過Model物件或使用ModelAndView物件進行,舉個例子:
IndexController.java程式碼如下

package com.summo.file.controller;

// 確保引入相關的包
import org.springframework.ui.Model;

@Controller
public class IndexController {

  @GetMapping("/")
  public String index(HttpServletRequest request, Model model) {
    // 新增屬性到model中
    model.addAttribute("message", "歡迎訪問我們的網站!");
    // 設定一個版本號
    model.addAttribute("version", "1.0.0"); 

    // 根據客戶端型別選擇檢視
    if (isFromMobile(request)) {
      return "mp/index"; // 返回移動端頁面
    } else {
      return "web/index"; // 返回桌面端頁面
    }
  }
}

index.html程式碼如下

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>首頁</title>
  <script type="text/javascript" th:src="'/mp/js/manifest-' + ${version} + '.js'"></script>
</head>
<body>
<h1>Hello World,我是電腦端</h1>
<!-- 在Thymeleaf中使用的表示式 -->
<p th:text="${message}"></p>
</body>
</html>

訪問之後可以看到引數已經替換掉了

動態傳值在實際開發中經常使用到,比如我們一般在配置檔案中維護好js、css 的版本號,然後將版本號傳給index.html達到動態控制前端版本。

2. 專案打包靜態資源沒有打進去

正常情況下,打完包後靜態資原始檔會在static資料夾下,但是上次我打包就發現靜態資原始檔沒有打包進去,後來才知道需要在pom.xml檔案裡面配置一下,具體配置如下:

<build>
    <finalName>summo-sbmy</finalName>
    <resources>
      <resource>
        <!-- 指定配置檔案所在的resource目錄 -->
        <directory>src/main/resources</directory>
        <includes>
          <include>**/*.html</include>
          <include>**/*.js</include>
          <include>**/*.css</include>
        </includes>
        <filtering>true</filtering>
      </resource>
      <resource>
        <!-- 指定配置檔案所在的resource目錄 -->
        <directory>src/main/resources</directory>
        <includes>
          <include>**/*.woff</include>
          <include>**/*.ttf</include>
        </includes>
        <filtering>false</filtering>
      </resource>
    </resources>
  </build> 

這裡需要注意的是,.woff和.ttf這類檔案比較特殊,需要單獨開一塊並且設定filtering為false。這個配置說明對這類檔案不執行過濾,因為過濾可能破壞檔案內容,字型檔案應該以其原始形式包含在構建產物中。

四、總結一下

訪問靜態資源的介面大家接觸的不多,主要是因為現在前後端分離了,前端自己使用CDN放資源,後端只用維護一個index.html檔案,其他的資源都透過CDN訪問,已經變得很簡單了。但是有時候想要用卻不知道從哪裡開始,希望這篇文章可以給大家一個大概的思路,還有就是處理靜態資源的框架很多,最常見的就是Thymeleaf、Velocity,這兩個都可以實現上面的效果,但建議不要混用。

文末小彩蛋,自己花一個星期做的小網站,放出來給大家看看,網址如下:http://47.120.49.119:8080

相關文章