【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

不寫bug的米公子發表於2019-04-18

前言

最近接了一個公司官網的專案,需要 SEO 友好,所以不能使用前端框架,前端框架自帶的腳手架工具自然也幫不上啥忙。只好自己使用 webpack4 + ejs + express ,從頭搭建一個多頁應用的專案架構。搭建過程中,遇到許多坑,然而網上的相關參考也是非常少,所以寫個部落格記錄一下搭建過程以及注意事項。

以下我會將重要的細節標紅,給需要的朋友參考。

明確需求

在動手開發之前,我們需要先明確這個專案的定位——公司官網,一般來說,官網不會涉及大量的資料互動,比較偏向於資料展示。所以不用前端框架,jquery 即可滿足需求。但是考慮到 SEO 所以需要用到服務端渲染,就要使用模板語言(ejs),配合 node 來完成。

根據以上資訊,我們就可以確定打包指令碼的基本功能,先來簡單列個清單:

  1. 需要 webpack 來打包多頁應用,且不需要每次新增一個檢視檔案都新增一個 HTMLWebpackPlugin 和重啟 server ,能做到 webpack 配置和檔名解耦,儘量的自動化。
  2. 需要使用 ejs 模板語言編寫,能夠插入變數和外部 includes 檔案,最後執行 build 命令的時候能將通用模板檔案(<meta>/<title>/<header>/<footer> 等)自動插入每個檢視檔案對應位置。
  3. 需要用到服務端渲染,所以開發環境要脫離 webpack 整合的 webpack-dev-server,能使用自己編寫的 node 程式碼啟動服務。
  4. 擁有完善的 overlay 功能,可以像 webpack-dev-server 那樣整合漂亮的 overlay 螢幕報錯。
  5. 能監聽檔案變化,自動打包和重啟服務,最好能做到熱更新

開始構建

先建立一個空專案,由於需要自己編寫服務端程式碼,所以我們需要多建一個 /server 資料夾,用來存放 express 的程式碼,搭建完成後,我們的專案結構看起來是這樣。

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

除此以外,我們需要初始化一些通用配置檔案,包括:

  • .gitignore git 忽略檔案
  • .editorConfig 編輯器配置檔案
  • .eslintrc.js eslint 配置檔案
  • README.md 檔案
  • package.json 檔案

大的框架出來以後,開始編寫工程程式碼。

打包指令碼

首先是編寫打包指令碼,在/build資料夾裡新建幾個檔案

  1. webpack.base.config.js,用來存放生產環境和開發環境通用的 webpack 配置
  2. webpack.dev.config.js用來存放開發環境的打包配置
  3. webpack.prod.config.js用來存放生產環境的打包配置
  4. config.json 用來存放一些配置常量,例如埠名,路徑名之類。

一般來說,webpack.base.config 檔案裡,放一些開發生產環境通用的配置,例如 outputentry 以及一些 loader 例如編譯ES6語法的 babel-loader、打包檔案的 file-loader 等。常用的 loader 的使用方式我們可以檢視文件 webpack loaders

需要注意的是,這邊有個非常重要的 loader ———— ejs-html-loader

一般來說,我們使用 html-loader 來對.html結尾的檢視檔案做處理,然後扔給 html-webpack-plugin生成對應的檔案,但是 html-loader 無法處理 ejs 模板語法中的 <% include ... %> 語法,會報錯。在多頁應用裡,這個 include 的功能是必須的,不然每個檢視檔案裡都要手動去寫一份 header/footer 是什麼感覺。。。所以我們需要再多配置一份 ejs-html-loader:

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

第一個坑繞過之後,第二個:

entry 入口要怎麼寫?

記得之前公司的一個老專案,五十幾個頁面,五十幾個 entrynew HTMLwebpackPlugin() 一個檔案展開來可以繞地球一圈。。。這邊為了避免這種慘狀,寫一個方法,返回一個 entry 陣列。

可以使用 glob 來處理這些檔案,獲取檔名,當然同樣也可以使用原生 node 來實現。只要保證 JavaScript 檔名和檢視檔名相同即可,比如,首頁的檢視檔名是 home.ejs,那麼對應的指令碼檔名就要用同樣的名字 home.js 來命名,webpack 打包的時候會找到指令碼檔案入口,通過對映關係生成對應檢視檔案:

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

HTMLWebpackPlugin 也同理:

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

編寫好 webpack.base.config.js 檔案,根據自己專案需求編寫好 webpack.dev.config.jswebpack.prod.config.js,使用 webpack-merge 將基礎配置和對應環境下的配置合併。

webpack 其他的一些細節配置大家可以參考 webpack 中文網址

服務端

打包指令碼編寫完成,我們開始編寫服務,我們使用 express 來搭建服務。(由於是工程架構演示,所以這個服務暫不涉及任何的資料庫的增刪改查,只是包含基本的路由跳轉)

server 簡單的結構如下:

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

服務端啟動檔案

bin/server.js 啟動檔案,作為服務的入口,需要同時啟動本地服務和 webpack 的開發時編譯。一般專案 webpack-dev-server 是寫在 package.json 裡的,當你執行 npm run dev 的時候,就在使用 webpack-dev-server 啟動開發服務,這個 webpack-dev-server 功能十分強大,不僅能一鍵啟動本地服務,還可以監聽模組,實時編譯。這邊我們使用 express + webpack-dev-middleware 也可以達到同樣的功能。

webpack-dev-middleware 可以理解為一個抽離出來的 webpack-dev-server,只是沒有啟動本地服務的功能,以及使用方式上略有改變。它相比於 webpack-dev-server 的靈活性在於,它以一箇中介軟體的形式存在,允許開發者編寫自己的服務來使用它。

其實 webpack-dev-server 的內部實現機制也是藉助於 webpack-dev-middleware 和 express 有興趣的朋友可以去看一下。

以下是服務入口檔案的部分程式碼

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

服務端路由

路由的跳轉方式,屬於整個工程中非常重要的一步。不知道閱讀文章的朋友有沒有疑問,本地的檢視檔案是 .ejs 字尾結尾的檔案,瀏覽器只能識別 .html 字尾檔案,這塊檢視資料的渲染是怎麼做的? webpack-dev-middleware 打包出來的資源都是存在記憶體中的,儲存在記憶體中的資原始檔,服務端要怎麼獲取?

先來看具體的路由程式碼,此處以首頁路由作為演示

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

再來看看這個 getTemplate 做了咩

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

從上面程式碼可以看到,路由中的做的非常重要的事情,就是直接用對應檢視的 ejs 檔名,去請求自身服務,從而獲取到存在 webpack 快取中的資源和資料。
通過這種方式拿到模板字串後,ejs 引擎會用資料渲染對應變數,最終以 html 字串的形式返回到瀏覽器進行渲染。
本地服務會以一個 publicPath 路徑字首來標記靜態資源請求,如果服務接受到的請求是帶有 publicPath 字首,就會被 `/bin/server.js` 中的靜態資源中介軟體攔截到,對映到對應資源目錄,返回靜態資源,而這個 publicPath 就是 webpack 配置中的 output.publicPath

關於 webpack 的打包時快取,我之前翻了很多地方都沒有找到很好的文件和操作工具,這邊給大家推薦兩個連結

  1. Webpack Custom File Systems (webpack 自定義檔案系統官方說明)
  2. memory-fs(獲取 webpack 編譯到記憶體中的資料)

客戶端

完成了服務端渲染、webpack 構建配置後,算是搞定了 80% 的工作量,還有一些小細節需要注意,不然服務啟動起來還是會報錯。

webpack 編譯時的坑

這個坑就埋在客戶端的檢視檔案裡,先來看看坑是什麼:當我們使用 ejs 語法(<%= title %>)這種語法的時候,webpack 編譯就會報錯,說是 title is undefined

要解決這個問題,需要首先明白 webpack 編譯時的執行機制,它做了什麼。我們知道,webpack 內部模板機制就是基於的 ejs,所以在我們服務端渲染之前,也就是 webpack 的編譯階段,已經執行過了一次 ejs.render 了,這個時候,在 webpack 的配置檔案裡,我們是沒有傳遞過 title 這個變數的,所以編譯會報錯。那麼要怎麼寫才能識別呢?答案就在 ejs 的官方文件

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

從官網的介紹上可以看出,當我們使用 <%% 打頭的時候,會被轉義成 <% 字串,類似於 html 標籤的轉義,這樣才能避免 webpack 中自帶的 ejs 的錯誤識別,生成正確的 ejs 檔案。所以以變數為例,在程式碼中我們需要這樣寫: <%%= title %>
這樣,webpack 才能順利編譯完成,將 compiler 繼續傳遞到 ejs-html-loader 這裡

使用 html-loader 識別圖片資源

如果瞭解 html-loader 的朋友就知道,在專案中,我們之所以能夠在 html 中方便的寫 <img src="../static/imgs/XXX.png"> 這種圖片格式,還能被 webpack 正確識別,離不開 html-loader 裡的配置

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

但是在 ejs-html-loader 裡,沒有提供這種方便的功能,所以我們依舊要使用 html-loader 來對 html 中的圖片引用做處理,這邊需要注意 loader 的配置順序

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

配置熱更新

接下來是配置熱更新,使用 webpack-dev-middleware 時的熱更新配置方式和 webpack-dev-server 略有不同,但是 webpack-dev-middleware 稍微簡單一點。webpack 打包多頁應用配置熱更新,一共四步:

  1. entry 入口裡多寫一個 webpack-hot-middleware/client?reload=true 的入口檔案

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

  1. 在 webpack 的 plugins 裡多寫三個 plugin:
    plugins: [
    ...
    
    // OccurrenceOrderPlugin is needed for webpack 1.x only
    new Webpack.optimize.OccurrenceOrderPlugin(),
    new Webpack.HotModuleReplacementPlugin(),
    // Use NoErrorsPlugin for webpack 1.x
    new Webpack.NoEmitOnErrorsPlugin()
    
    ...
    ]
    複製程式碼
  2. bin/server.js 服務入口中引入 webpack-hot-middleware, 並將 webpack-dev-server 打包完成的 compilerwebpack-hot-middleware 包裝起來:
    let compiler = webpack(webpackConfig)
    
    // 用 webpack-dev-middleware 啟動 webpack 編譯
    app.use(webpackDevMiddleware(compiler, {
        publicPath: webpackConfig.output.publicPath,
        overlay: true,
        hot: true
    }))
    
    // 使用 webpack-hot-middleware 支援熱更新
    app.use(webpackHotMiddleware(compiler, {
        publicPath: webpackConfig.output.publicPath,
        reload: true,
        noInfo: true
    }))
    複製程式碼
  3. 在檢視對應的 js 檔案里加一段程式碼:
    if (module.hot) {
        module.hot.accept()
    }
    複製程式碼

關於 webpack-hot-middleware 的更多配置細節,請看文件

這邊需要注意的是:光是這麼寫的話,webpack hot module 只能支援 JS 部分的修改,如果需要支援樣式檔案( css / less / sass ... )的 hot reload ,就不能使用 extract-text-webpack-plugin 將樣式檔案剝離出去,否則無法監聽修改、實時重新整理。而且,webpack hot module 預設是不支援 html 的熱替換的
這是我翻到的相關 issues 截圖,如果是懶癌患者,實在是怕按個 f5 重新整理,那麼可以自己找下解決的辦法。

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

webpack-hot-middleware 預設繼承了 overlay,所以當熱更新配置完成以後,overlay 報錯功能也能正常使用了

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

package.json 啟動指令碼

最後來看一下 package.json 裡的啟動指令碼,這邊沒啥難度,就直接上程式碼了

【實戰】webpack4 + ejs + express 帶你擼一個多頁應用專案架構

當客戶端程式碼變動時 webpack 會自動幫我們編譯重啟,但是服務端的程式碼變動卻不會實時重新整理,這時需要用到 nodemon,設定好監聽目錄以後,服務端的任何程式碼修改就能被 nodemon 監聽,服務自動重啟,非常方便。

這邊也有一個小細節需要注意,nodemon --watch 最好指定監聽服務端資料夾,因為畢竟只有服務端的程式碼修改才需要重啟服務,不然預設監聽整個根目錄,寫個樣式都能重啟服務,簡直要把人煩死。

總結

專案整體搭完後再回頭看,還是有不少需要注意和值得學習的地方。雖然踩了不少坑,但也對其中的一些原理有了更深入的瞭解。

得益於前端腳手架工具,讓我們能在大部分專案中一鍵生成專案的基礎配置,免去了很多工程搭建的煩惱,但這種方便在造福了開發者的同時,卻也弱化了前端工程師的工程架構能力。現實中總有一些腳手架工具沒辦法的觸及到的業務場景,這時就需要開發者主動尋求解決方案,甚至自己動手構建工程,以獲得開發的最佳靈活性。

完整專案地址可以檢視我的 GitHub ,喜歡的話給個 Star⭐️ ,多謝多謝~??

相關文章