一、前言
大家好!我是sum墨,一個一線的底層碼農,平時喜歡研究和思考一些技術相關的問題並整理成文,限於本人水平,如果文章和程式碼有表述不當之處,還請不吝賜教。
作為一名從業已達六年的老碼農,我的工作主要是開發後端Java業務系統,包括各種管理後臺和小程式等。在這些專案中,我設計過單/多租戶體系系統,對接過許多開放平臺,也搞過訊息中心這類較為複雜的應用,但幸運的是,我至今還沒有遇到過線上系統由於程式碼崩潰導致資損的情況。這其中的原因有三點:一是業務系統本身並不複雜;二是我一直遵循某大廠程式碼規約,在開發過程中儘可能按規約編寫程式碼;三是經過多年的開發經驗積累,我成為了一名熟練工,掌握了一些實用的技巧。
前面的文章都是先說概念,再說怎麼設計和實現,今天我打算換一種寫法,從一個功能需求的實現來講一下靜態資源是如何訪問的?
功能需求如下:
- 現有一個後端應用,預設訪問方式如下:http://47.120.49.119:8080/#/;
- 用電腦、平板、手機等裝置都可以訪問,且不同的裝置樣式要適配,前端做了兩套,但是訪問介面都是同一個;
- 由於沒有使用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