再談先後端分離開發和部署

先後端分離開發已成爲業界的共識,但分離的同時也帶來了部署的問題。傳統web模式下,前端和後端同屬一個項目,模板的渲染理所固然由後端渲染。然而隨着node的流行,以及webpack的模塊化打包方案,讓前端在開發階段徹底有能力脫離後端環境:經過本地node啓動一個服務器,搭配Mock數據,立刻就能夠進行業務開發了。javascript

可是到了部署階段,問題也就顯現出來:前端最後打包出來的js,css以及index.html,到底放在哪裏?靜態文件js,css或者圖片,咱們還能夠在CI階段上傳到cdn服務器上,可是最後的html模板index.html必定須要放在一個服務器上,然而這個服務器到底由前端仍是後端維護?css

前端維護HTML

若是html模板由前端維護,那麼前端就須要本身使用一個靜態服務器:提供HTML的渲染和API接口的轉發。常見的單頁應用,也是推薦使用Nginx進行部署。html

使用Nginx部署,這裏又分兩種狀況:前端

  • 靜態資源徹底由Nginx託管,也就是js,css和index.html放在同一個dist目錄裏。在這種狀況下,webpack的publicPath通常不用特別設置,使用默認的/便可。
  • 靜態資源上傳CDN,Nginx只提供index.html。在這種狀況下,webpack的publicPath要設置成cdn的地址,例如://static.demo.cn/activity/share/。但這又會引起一個問題,因爲qa環境,驗證環境,生產環境的cdn地址一般不一樣,爲了讓index.html能夠引入正確的靜態文件路徑,你須要打包三次,僅僅爲了生成三份引用了不一樣路徑的html(即便三次打包的js內容徹底同樣)

nginx配置java

server {
    listen       80;
    server_name  localhost;

    location / {
        root   /app/dist; # 打包的路徑
        index  index.html index.htm;
        try_files $uri $uri/ /index.html; # 單頁應用防止重刷新返回404,若是是多頁應用則無需這條命令
    }

    location /api {
        proxy_pass https://anata.me; #後臺轉發地址
        proxy_set_header   X-Forwarded-Proto $scheme;
        proxy_set_header   X-Real-IP         $remote_addr;
    }
}

理論上qa,yz,prod環境的接口轉發地址也不一樣,所以你還須要三份nginx.conf配置node

後端維護HTML

不少狀況下,咱們須要渲染的頁面帶上後端注入的動態數據,又或者頁面須要支持SEO,這種狀況下,咱們只能把模板交給後端渲染。那麼後端維護的html模板怎麼獲取打包後的hash值呢?webpack

  • 前端打包後的index.html直接發給後端(簡單粗暴,並不推薦)
  • 前端打包時經過插件webpack-manifest-plugin後生成一個manifest.json文件,該文件實際上是個key-value的鍵值對,key表明了資源名稱,value記錄了資源的hash
{
  "common.css": "/css/common/common-bundle.804a717f.css",
  "common.js": "/js/common/common-bundle.fcb76db9.js",
  "manifest.js": "/js/manifest/manifest-bundle.551ff423.js",
  "vendor.js": "/js/vendor/vendor-bundle.d99dc0e4.js",
  "page1.css": "/css/demo/demo-bundle.795bbee4.css",
  "page1.js": "/js/demo/demo-bundle.e382801f.js",
}

後端的index.htmlnginx

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>demo</title>
  <link href="<%= manifest['page1.css']%>" rel="stylesheet">
</head>

<body>
  <h1>demo</h1>
  <script src="<%= manifest['page1.js'] %>"></script>
</body>
</html>

後端經過讀取這個json文件,就能夠動態渲染出文件的引用路徑。git

若是你曾經用過百度的打包工具FIS,它最後打包產出的map.json就是相似的資源文件列表。github

使用這種方法還有一個好處:前面咱們說過,若是文件上傳至cdn,那麼前端維護的html可能須要打包三次,由於不一樣環境的cdn地址不一樣。如今html交給後端維護了,那麼這個問題就很好解決,前端只須要打包一次,不一樣環境的cdn地址可讓後端動態拼接生成。

固然,使用這種方法也會帶來一個問題,這個json文件,後端怎麼獲取?

  • 把這個json文件和其餘靜態資源一塊兒打包上傳到cdn上,後端服務器每次啓動時,先到cdn上獲取這個json文件,而後存到內存裏
wget --tries=3 --quiet -O manifest.json http://static.demo.cn/demo/manifest.json?`date +%s` ## 防止緩存

方案的優勢:簡單方便,每次前端打包,manifest.json就會自動更新,上傳到cdn同時覆蓋前一個版本。
方案的缺點:若是manifest.json更新了,後端則須要重啓服務以便獲取新的配置,當集羣多的時候,重啓耗費的代價可能很大。

  • manifest.json的內容放在配置中心裏,後端則須要接入配置中心。每次CI打包後,調用配置中心更新接口,後端就能自動獲取最新的配置。

在我平時工做項目中,這兩種方案均有實現。

Node中間層

使用Nginx部署時,爲了解決跨域問題,咱們通常須要配置proxy_pass指向提供api的後端服務。

後端採用了SOA,微服務的架構時,proxy_pass指向的api服務器,其實本質也是一個轉發服務。

前端ajax請求

// 獲取商品列表
ajax.get('/api/queryProductList')

// 獲取價格列表
ajax.get('/api/queryPriceList')

Nginx轉發

location /api {
    proxy_pass https://demo.com; #後臺轉發地址
    proxy_set_header   X-Forwarded-Proto $scheme;
    proxy_set_header   X-Real-IP         $remote_addr;
}

接口轉發到
https://demo.com/api/queryProductList
https://demo.com/api/queryPriceList

查詢商品列表和查詢價格列表實際上是由兩個不一樣的soa服務提供:

查詢商品: product.soa.neko.com
查詢價格: price.soa.neko.com

所以,本質上https://demo.com這個服務也只是用來轉發接口,同時對數據作部分的組裝。那麼這個服務,就能夠用Node中間層來替代。使用了Node中間層,模板的渲染也能夠從Nginx轉移到Node了。

固然,多了一層Node,對於前端的綜合要求也隨之提升,後端的部署,監控,日誌,性能等等問題也隨之而來,全(幹)工程師應運而生。

工做現狀

我司大部分to C的前端項目,都是採用Node層渲染模板加轉發接口的開發模式,還有少許項目採用Java tomcat渲染html模板。

大部分頁面都是多頁應用,並非典型的單頁應用。

Node層渲染模板,又分兩種狀況:

  • 須要支持SEO,則採用傳統的模板渲染,填充展現數據。可是JS的業務代碼,依舊先後端分離,並不在Node項目裏。這類頁面,通常都是採用Jquery+webpack模塊化打包。
  • 不須要支持SEO,則Node只渲染一個空html模板,頁面內容徹底由JS生成。這類頁面,通常採用最新的前端MVC框架,好比Vue和React。

固然近幾年比較流行的SSR方案,讓Node渲染模板時能夠直接使用Vue和React的同構組件,直出頁面後,用戶的交互體驗又如單頁應用般流暢,只能說:歷史老是驚人的類似。

從某種程度上說,SSR是一種向傳統模式的迴歸,不過這種迴歸並非倒退,而是一種螺旋式的發展。

實戰

理論知識講了那麼多,如今咱們來實戰一下。在上一篇文章裏,我介紹了webpack多頁打包的原理,同時搭建了一個簡單的webpack4-boilerplate。這個模板只是一個前端開發模板,其實它還對應着一個node後端模板koa2-multipage-boilerplate

這個node項目最重要的就是實現了前面說的:如何讀取manifest.json文件,動態渲染靜態文件的引用路徑,從而先後端分離開發和部署。

詳情見chunkmap.js這個koa2中間件的源碼。

const chunkmap = require('./chunkmap');
app.use(chunkmap({
  staticServer: '//0.0.0.0:9001',
  staticResourceMappingPath: './mainfest.json'
}));

這個中間件接受兩個參數

  • staticServer:靜態資源服務器地址,本地開發時,填寫的就是webpack4-boilerplate這個前端項目啓動的服務器。到了qa,產線時,則填寫真正的cdn地址
  • staticResourceMappingPath: 資源映射文件路徑,也就是manifest.json文件

本地開發時的manifest.json,不帶hash值

{
  "home.css": "/css/home/home-bundle.css",
  "home.js": "/js/home/home-bundle.js",
}

打包後的manifest.json,帶hash值

{
  "home.css": "/css/home/home-bundle.d2378378.css",
  "home.js": "/js/home/home-bundle.cb49dfaf.js",
}

使用了這個中間件後,koa的ctx.state全局變量上就帶有一個bundle屬性,裏面的內容是:

{
  "home.css": "//0.0.0.0:9001/css/home/home-bundle.d2378378.css",
  "home.js": "//0.0.0.0:9001/js/home/home-bundle.cb49dfaf.js",
}

而後經過模板引擎,動態渲染出實際頁面。固然你也能夠在頁面中動態生成展現內容,從而支持SEO。

<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <title><%= title %></title>
  <link href="<%= bundle['home.css']%>" rel="stylesheet">
</head>

<body>
  <div id="app"></div>
  <script src="<%= bundle['home.js']%>"></script>
</body>
</html>

總結

先後端分離帶來了工做效率上的提升,Node中間層則給前端打開了一條走進後端的道路。固然機遇老是與挑戰並存,在前端技術突飛猛進的今天,真想說一句:老子學不動啦!

參考

大公司裏怎樣開發和部署前端代碼?

相關文章
相關標籤/搜索