前段時間公司網路部署的演示環境全部轉到內網環境中去,所有對外演示的環境都需要申請網路對映才能訪問某個服務。我用一個網路地址 www.a.com 對映到一個內網地址 http://ip:port,然後在這個地址 http://ip:port 用 nginx 做代理轉發到各個組的專案 http://ipn:portn 上去,其中也遇到一些靜態資源 404,主要是是解決這個 404 問題。
最近又做了一個專案,考慮到使用者的體驗,減少部署的複雜性,我想了一個辦法用 SpringBoot 做 web 伺服器對映前端資源為 web 資源 。
條件允許或者對效能要求比較高,推薦是前後端分離部署,nginx 做 web 伺服器,後端只提供介面服務
以前部署的專案 A 網路訪問地址是 http://ip1:8080,網路對映後只能訪問 http://ip/app1 ,以前專案 B 網路訪問地址是 http://ip1:8081 ,專案訪問地址是 http://ip/app2 。這也算是一個不大不小的變動,但是切換之後遇到的第一個問題就是靜態資源轉發導致 404。
比如以前專案 A 訪問地址是 http://ip1:8080 它是沒有上下文的。
而現在 A 的訪問地址為 http://ip/app1 ,有一個上下文 app1 在這裡,導致有一些資源 404。
比如說:原來 http://ip1:8080 請求到了 index.html 資源,現在只能 http://ip/app1 請求到 index.html。
前端使用 vue 編寫,html 中的靜態資源路徑可以很好解決,修改 webpack 打包即可。
本文內容Nginx 部署 vue 專案,怎麼能友好處理靜態資源的丟失SpringBoot 提供 web 伺服器的功能對映 vue 專案為 web 資源,並處理 vue 路由轉發 index.html 問題。演示程式碼地址
https://github.com/zhangpanqin/vue-springboot複製程式碼
Nginx 部署 Vue 專案
server { listen 8087; # 它的作用是不重定向地址,比如瀏覽器輸入 /app1 訪問,也可以訪問到 /app1/ ,而瀏覽器地址是不改變的 /app1 。沒辦法,強迫症 location / { try_files $uri $uri/; } root /Users/zhangpanqin/staic/; location ~ /(.*)/ { index index.html /index.html; try_files $uri $uri/ /$1/index.html; }}複製程式碼
/Users/zhangpanqin/staic/ 放部署的專案,比如 app 的專案資源放到 /Users/zhangpanqin/staic/app 下。 訪問地址為 http://ip/8087/app
import Vue from 'vue';import VueRouter from 'vue-router';Vue.use(VueRouter);const routes = [ { path: '/', name: 'Home', component: () => import('@/views/Home.vue'), }, { path: '/blog', name: 'Blog', component: () => import('@/views/Blog.vue'), }, { // 匹配不到路由的時候跳轉到這裡 path: '*', name: 'Error404', component: () => import('@/views/Error404.vue'), }];const router = new VueRouter({ // 主要是修改這裡,可以根據 vue mode 環境來取值。 // http://127.0.0.1/vhost/conf/img_echo.php?w=640&h=398&src=https://cli.vuejs.org/zh/guide/mode-and-env.html // http://127.0.0.1/vhost/conf/img_echo.php?w=640&h=398&src=https://router.vuejs.org/zh/api/#base base: process.env.VUE_APP_DEPLOY_PATH, mode: 'history', routes,});export default router;複製程式碼
http://localhost:8087/app/index.css 為 css 的真實地址。所以想辦法為這些不以 /app 開頭的資源加上 /app 就可以了,想了想只有 cookie 能做到。
x_vue_path 記錄每個專案的路徑,然後靜態資源去這個路徑下尋找,$cookie_x_vue_path/$uri
下面這個配置使用了 try_files 內部重定向資源,是不會在瀏覽器端發生重定向的。
下面這個是重定向的配置
server { listen 8087; root /Users/zhangpanqin/staic/; location ~ /(.*)/.*/? { index index.html /index.html; add_header Set-Cookie "x_vue_path=/$1;path=/;"; try_files $uri $uri/ /$1/index.html @404router; } location ~ (.css|js)$ { # 匹配到 /app/index.css 的資源,直接訪問 rewrite ^($cookie_x_vue_path)/.* $uri break; # 訪問的資源 /index.css 302 臨時重定向到 /app/index.css rewrite (.css|js)$ $cookie_x_vue_path$uri redirect; } location @404router { return 404; }}複製程式碼SpringBoot 部署 Vue 專案
Nginx 走通了,SpringBoot 依葫蘆畫瓢就行了,還是 java 寫的舒服,能 debug,哈哈。
SpringBoot 對映靜態資源@Configurationpublic class VueWebConfig implements WebMvcConfigurer { /** * 對映的靜態資源路徑 * file:./static/ 路徑是相對於 user.dir 路徑,jar 包同級目錄下的 static */ private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {"file:./static/", "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/"}; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 新增靜態資源快取 CacheControl cacheControl = CacheControl.maxAge(5, TimeUnit.HOURS).cachePublic(); registry.addResourceHandler("/**").addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS).setCacheControl(cacheControl); } @Override public void addInterceptors(InterceptorRegistry registry) { // 配置要攔截的資源,主要用於 新增 cookie registry.addInterceptor(new VueCookieInterceptor()).addPathPatterns("/test/**"); } // vue 路由轉發使用的,也做 介面請求找不到的 @Bean public VueErrorController vueErrorController() { return new VueErrorController(new DefaultErrorAttributes()); }}複製程式碼
專案靜態資源路徑新增 cookiepublic class VueCookieInterceptor implements HandlerInterceptor { public static final String VUE_HTML_COOKIE_NAME = "x_vue_path"; public static final String VUE_HTML_COOKIE_VALUE = "/test"; /** * 配置請求資源路徑 /test 下全部加上 cookie */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { final Cookie cookieByName = getCookieByName(request, VUE_HTML_COOKIE_NAME); if (Objects.isNull(cookieByName)) { final Cookie cookie = new Cookie(VUE_HTML_COOKIE_NAME, VUE_HTML_COOKIE_VALUE); // 專案下的 url 都帶能帶上 cookie.setPath("/"); cookie.setHttpOnly(true); response.addCookie(cookie); } return true; } public static Cookie getCookieByName(HttpServletRequest httpServletRequest, String cookieName) { final Cookie[] cookies = httpServletRequest.getCookies(); if (Objects.isNull(cookieName) || Objects.isNull(cookies)) { return null; } for (Cookie cookie : cookies) { final String name = cookie.getName(); if (Objects.equals(cookieName, name)) { return cookie; } } return null; }}複製程式碼
請求出現錯誤做資源的轉發訪問錯誤的跳轉要分清楚 介面請求和靜態資源的請求,通過 accept 可以判斷。
@RequestMapping("/error")public class VueErrorController extends AbstractErrorController { private static final String ONLINE_SAIL = VUE_HTML_COOKIE_NAME; private static final String ERROR_BEFORE_PATH = "javax.servlet.error.request_uri"; public VueErrorController(DefaultErrorAttributes defaultErrorAttributes) { super(defaultErrorAttributes); } @Override public String getErrorPath() { return "/error"; } @RequestMapping public ModelAndView errorHtml(HttpServletRequest httpServletRequest, HttpServletResponse response, @CookieValue(name = ONLINE_SAIL, required = false, defaultValue = "") String cookie) { final Object attribute = httpServletRequest.getAttribute(ERROR_BEFORE_PATH); if (cookie.length() > 0 && Objects.nonNull(attribute)) { response.setStatus(HttpStatus.OK.value()); String requestURI = attribute.toString(); // 訪問的路徑沒有以 vue 部署的路徑結尾,補充上路徑轉發去訪問 if (!requestURI.startsWith(cookie)) { ModelAndView modelAndView = new ModelAndView(); modelAndView.setStatus(HttpStatus.OK); // 靜態資源不想轉發,重定向的話,修改為 redirect String viewName = "forward:" + cookie + requestURI; modelAndView.setViewName(viewName); return modelAndView; } } ModelAndView modelAndView = new ModelAndView(); modelAndView.setStatus(HttpStatus.OK); modelAndView.setViewName("forward:/test/index.html"); return modelAndView; } // 處理請求頭為 accept 為 application/json 的請求,就是介面請求返回json 資料 @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { HttpStatus status = getStatus(request); if (status == HttpStatus.NO_CONTENT) { return new ResponseEntity<>(status); } final Map<String, Object> errorAttributes = getErrorAttributes(request, true); return new ResponseEntity<>(errorAttributes, status); }複製程式碼
首頁跳轉@Controllerpublic class IndexController { @RequestMapping(value = {"/test", "/test"}) public String index() { return "forward:/test/index.html"; }}