一、vue項目開發方案
使用vue官方的腳手架創建單獨的前端工程項目,做到和後端完全獨立開發和部署,後端單獨部署一個純restful的服務,而前端直接採用nginx來部署,這種稱爲完全的前後端分離架構開發模式,但是在分離中有很多api權限的問題需要解決,包括部署後的vue router路由需要在nginx中配置rewrite規則。這種前後端完全分離的架構也是目前互聯網公司所採用的,後端服務器不再需要處理靜態資源,也能減少後端服務器一些壓力。
二、爲什麼做前後端分離開發合併
在傳統行業中很多是以項目思想來主導的,而不是產品,一個項目會賣給很多的客戶,並且部署到客戶本地的機房裏。在一些傳統行業裏面,部署實施人員的技術無法和互聯網公司的運維團隊相比,由於各種不定的環境也無法做到自動構建,容器化部署等。因此在這種情況下儘量減少部署時的服務軟件需求,打出的包數量也儘量少。針對這種情況這裏採用的在開發中做到前後端獨立開發,整個開發方式和上面提到的vue項目開發方案是相同的,但是在後端springboot打包發佈時將前端的構建輸出一起打入,最後只需部署springboot的項目即可,無需再安裝nginx服務器。
三、springboot和vue整合的關鍵操作
實際上本文中這種前後端分離的開發,前端開發好後將build構建好的dist下static中的文件和index.html直接拷貝到springboot的resource/static下,下面是示例圖:
vue前端項目:
springboot項目:
上面這是最簡單的合併方式,但是如果作爲工程級的項目開發,並不推薦使用手工合併,也不推薦將前端代碼構建後提交到springboot的resouce下,好的方式應該是保持前後端完全獨立開發代碼,項目代碼互不影響,藉助jenkins這樣的構建工具在構建springboot時觸發前端構建並編寫自動化腳本將前端webpack構建好的資源拷貝到springboot下再進行jar的打包,最後就得到了一個完全包含前後端的springboot項目了。
三、整合的核心問題處理
通過上面的整合後會出現兩個比較大的問題:
1. 無法正常訪問靜態資源 。
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就可以將前端的路由資源交給路由來處理了。至此整個完整前後端分離開發合併方案就完成了。這種方式在後期有條件情況下也可以很容易做到前後端的完全分離開發部署。