springboot+vue的先後端分離與合併方案

springboot和vue結合的方案網絡上的主要有如下兩種:javascript

1. 【不推薦】在html中直接使用script標籤引入vue和一些經常使用的組件,這種方式和之前傳統的開發是同樣的,只是能夠很爽的使用vue的雙向數據綁定,這種方式只適合於普通的全棧開發。html

2.【推薦】使用vue官方的腳手架建立單獨的前端工程項目,作到和後端徹底獨立開發和部署,後端單獨部署一個純restful的服務,而前端直接採用nginx來部署,這種稱爲徹底的先後端分離架構開發模式,可是在分離中有不少api權限的問題須要解決,包括部署後的vue router路由須要在nginx中配置rewrite規則。這種先後端徹底分離的架構也是目前互聯網公司所採用的,後端服務器再也不須要處理靜態資源,也能減小後端服務器一些壓力。前端

1、爲何作先後端分離開發合併

    在傳統行業中不少是以項目思想來主導的,而不是產品,一個項目會賣給不少的客戶,而且部署到客戶本地的機房裏。在一些傳統行業裏面,部署實施人員的技術沒法和互聯網公司的運維團隊相比,因爲各類不定的環境也沒法作到自動構建,容器化部署等。所以在這種狀況下儘可能減小部署時的服務軟件需求,打出的包數量也儘可能少。針對這種狀況這裏採用的在開發中作到先後端獨立開發,整個開發方式和上面提到的第二種方式是相同的,可是在後端springboot打包發佈時將前端的構建輸出一塊兒打入,最後只需部署springboot的項目便可,無需再安裝nginx服務器。vue

2、springboot和vue整合的關鍵操做

    實際上本文中這種先後端分離的開發,前端開發好後將build構建好的dist下static中的文件拷貝到springboot的resource的static下,index.html則直接拷貝到springboot的resource的static下。下面是示例圖:java

vue前端項目webpack

springboot項目:nginx

重點:上面這是最簡單的合併方式,可是若是做爲工程級的項目開發,並不推薦使用手工合併,也不推薦將前端代碼構建後提交到springboot的resouce下,好的方式應該是保持先後端徹底獨立開發代碼,項目代碼互不影響,藉助jenkins這樣的構建工具在構建springboot時觸發前端構建並編寫自動化腳本將前端webpack構建好的資源拷貝到springboot下再進行jar的打包,最後就獲得了一個徹底包含先後端的springboot項目了。web

3、整合的核心問題處理

    經過上面的整合後會出現兩個比較大的問題:正則表達式

    1. 沒法正常訪問靜態資源 。spring

    2. vue router路由的路徑沒法正常解析 。

   解決第一個問題,咱們必須從新指定springboot的靜態資源處理前綴,代碼:

@Configuration
public class SpringWebMvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
        super.addResourceHandlers(registry);
    }
}

  解決第二個問題的方式是對vue的路由的路徑作rewrite,交給router來處理,而不是springboot本身處理,rewrite時能夠考慮路由的路徑統一增長後綴,而後在springboot中編寫過濾攔截特定後綴來作請求轉發交給vue的路由處理。如:

const router = new VueRouter({
  mode: 'history',
  base: __dirname,
  routes: [
    {
      path: '/ui/first.vhtml',
      component: First
    },
    {
      path: '/ui/second.vhtml',
      component: secondcomponent
    }
  ]
})

後端攔截到帶有vhtml的都交給router來處理,這種方式在後端寫過濾器攔截後打包是徹底可行的,可是前端開發的直接訪問帶後綴的路徑會有問題。

另一種方式是給前端的路由path統一加個前綴好比/ui,固然就能夠把以前的後綴刪除了,這時後端寫過濾器匹配該前綴,也不會影響前端單獨開發是的路由解析問題。過濾器參考以下:

/**
 * be used to rewrite vue router
 *
 * @author yu on 2017-11-22 19:47:23.
 */
public class RewriteFilter implements Filter {

    /**
     * 須要rewrite到的目的地址
     */
    public static final String REWRITE_TO = "rewriteUrl";

    /**
     * 攔截的url,url通配符以前用英文分號隔開
     */
    public static final String REWRITE_PATTERNS = "urlPatterns";

    private Set<String> urlPatterns = null;//配置url通配符

    private String rewriteTo = null;
    @Override
    public void init(FilterConfig cfg) throws ServletException {
        //初始化攔截配置
        rewriteTo = cfg.getInitParameter(REWRITE_TO);
        String exceptUrlString = cfg.getInitParameter(REWRITE_PATTERNS);
        if (StringUtil.isNotEmpty(exceptUrlString)) {
            urlPatterns = Collections.unmodifiableSet(
                    new HashSet<>(Arrays.asList(exceptUrlString.split(";", 0))));
        } else {
            urlPatterns = Collections.emptySet();
        }
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        String servletPath = request.getServletPath();
        String context = request.getContextPath();
        //匹配的路徑重寫
        if (isMatches(urlPatterns, servletPath)) {
            req.getRequestDispatcher(context+"/"+rewriteTo).forward(req, resp);
        }else{
            chain.doFilter(req, resp);
        }
    }

    @Override
    public void destroy() {

    }

    /**
     * 匹配返回true,不匹配返回false
     * @param patterns 正則表達式或通配符
     * @param url 請求的url
     * @return
     */
    private boolean isMatches(Set<String> patterns, String url) {
        if(null == patterns){
            return false;
        }
        for (String str : patterns) {
            if (str.endsWith("/*")) {
                String name = str.substring(0, str.length() - 2);
                if (url.contains(name)) {
                    return true;
                }
            } else {
                Pattern pattern = Pattern.compile(str);
                if (pattern.matcher(url).matches()) {
                    return true;
                }
            }
        }
        return false;
    }
}

過濾器的註冊:

@SpringBootApplication
public class SpringBootMainApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootMainApplication.class, args);
    }

    @Bean
    public EmbeddedServletContainerCustomizer containerCustomizer() {
        return (container -> {
                ErrorPage error401Page = new ErrorPage(HttpStatus.UNAUTHORIZED, "/errors/401.html");
                ErrorPage error404Page = new ErrorPage(HttpStatus.NOT_FOUND, "/errors/404.html");
                ErrorPage error500Page = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/errors/500.html");
                container.addErrorPages(error401Page, error404Page, error500Page);
        });
    }
    @Bean
    public FilterRegistrationBean testFilterRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new RewriteFilter());//註冊rewrite過濾器
        registration.addUrlPatterns("/*");
        registration.addInitParameter(RewriteFilter.REWRITE_TO,"/index.html");
        registration.addInitParameter(RewriteFilter.REWRITE_PATTERNS, "/ui/*");
        registration.setName("rewriteFilter");
        registration.setOrder(1);
        return registration;
    }
}

這時springboot就能夠將前端的路由資源交給路由來處理了。至此整個完整先後端分離開發合併方案就完成了。這種方式在後期有條件狀況下也能夠很容易作到先後端的徹底分離開發部署。

ps:本方案只是在特定場景下的選擇,若是一切條件容許,強力推薦作徹底的先後端分離

版權申明:轉載請註明出處,不然後果自負

相關文章
相關標籤/搜索