Webpack實戰-管理多個單頁應用
引入問題
上一節3-9為單頁應用生成HTML中只生成了一個 HTML 文件,但在實際應用中一個完整的系統不會把所有的功能都做到一個網頁中,因為這會導致這個網頁性能不佳。
實際的做法是按照功能模塊劃分成多個單頁應用,每個單頁應用生成一個 HTML 文件。並且隨著業務的發展更多的單頁應用可能會逐漸被加入到項目中去。雖然上一節已經解決了自動化生成 HTML 的痛點,但是手動去管理多個單頁應用的生成也是一件麻煩的事情。
來繼續改造上一節的例子,要求如下:
- 項目目前共有2個單頁應用組成,一個是主頁
index.html
,一個是用戶登入頁login.html
; - 多個單頁應用之間會有公共的代碼部分,需要把這些公共的部分抽離出來,放到單獨的文件中去以防止重複載入。例如多個頁面都使用一套 CSS 樣式,都採用了 React 框架,這些公共的部分需要抽離到單獨的文件中;
- 隨著業務的發展後面可能會不斷的加入新的單頁應用,但是每次新加入單頁應用不能去改動構建相關的代碼。
在開始前先來看看該應用最終發布到線上的代碼。
login.html
文件內容:
<html><head><meta charset="UTF-8"><!--從多個頁面中抽離出的公共 CSS 代碼--><link rel="stylesheet" href="common_7cc98ad0.css"><!--只有這個頁面需要的 CSS 代碼--><link rel="stylesheet" href="login_e31e214b.css"><!--注入 google_analytics 中的 JS 代碼--><script>(function(i,s,o,g,r,a,m){i["GoogleAnalyticsObject"]=r;i[r]=i[r]||function(){ (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)})(window,document,"script","https://www.google-analytics.com/analytics.js","ga");ga("create", "UA-XXXXX-Y", "auto");ga("send", "pageview");</script><!--非同步載入 Disqus 評論--><script async="" src="https://dive-into-webpack.disqus.com/embed.js"></script></head><body><div id="app"></div><!--從多個頁面中抽離出的公共 JavaScript 代碼--><script src="common_a1d9142f.js"></script><!--只有這個頁面需要的 JavaScript 代碼--><script src="login_f926c4e6.js"></script><!--Disqus 評論容器--><div id="disqus_thread"></div></body></html>
構建出的目錄結構為:
dist├── common_029086ff.js├── common_7cc98ad0.css├── index.html├── index_04c08fbf.css├── index_b3d3761c.js├── login.html├── login_0a3feca9.js└── login_e31e214b.css
如果按照上節的思路,可能需要為每個單頁應用配置一段如下代碼:
new WebPlugin({ template: "./template.html", // HTML 模版文件所在的文件路徑 filename: "login.html" // 輸出的 HTML 的文件名稱})
並且把頁面對應的入口加入到 enrty
配置項中,就像這樣:
entry: { index: "./pages/index/index.js",// 頁面 index.html 的入口文件 login: "./pages/login/index.js",// 頁面 login.html 的入口文件}
當有新頁面加入時就需要修改 Webpack 配置文件,新插入一段以上代碼,這會導致構建代碼難以維護而且易錯。
解決方案
上一節中的 web-webpack-plugin 插件也內置了解決這個問題的方法,上一節中只使用了它的 WebPlugin
,
AutoWebPlugin
來解決以上問題,使用方法非常簡單,下面來教你具體如何使用。
項目源碼目錄結構如下:
├── pages│ ├── index│ │ ├── index.css // 該頁面單獨需要的 CSS 樣式│ │ └── index.js // 該頁面的入口文件│ └── login│ ├── index.css│ └── index.js├── common.css // 所有頁面都需要的公共 CSS 樣式├── google_analytics.js├── template.html└── webpack.config.js
從目錄結構中可以看成出下幾點要求:
- 所有單頁應用的代碼都需要放到一個目錄下,例如都放在 pages 目錄下;
- 一個單頁應用一個單獨的文件夾,例如最後生成的
index.html
相關的代碼都在index
目錄下,login.html
同理; - 每個單頁應用的目錄下都有一個
index.js
文件作為入口執行文件。
雖然
AutoWebPlugin
強制性的規定了項目部分的目錄結構,但從實戰經驗來看這是一種優雅的目錄規範,合理的拆分了代碼,又能讓新人快速的看懂項目結構,也方便日後的維護。
Webpack 配置文件修改如下:
const { AutoWebPlugin } = require("web-webpack-plugin");// 使用本文的主角 AutoWebPlugin,自動尋找 pages 目錄下的所有目錄,把每一個目錄看成一個單頁應用const autoWebPlugin = new AutoWebPlugin("pages", { template: "./template.html", // HTML 模版文件所在的文件路徑 postEntrys: ["./common.css"],// 所有頁面都依賴這份通用的 CSS 樣式文件 // 提取出所有頁面公共的代碼 commonsChunk: { name: "common",// 提取出公共代碼 Chunk 的名稱 },});module.exports = { // AutoWebPlugin 會為尋找到的所有單頁應用,生成對應的入口配置, // autoWebPlugin.entry 方法可以獲取到所有由 autoWebPlugin 生成的入口配置 entry: autoWebPlugin.entry({ // 這裡可以加入你額外需要的 Chunk 入口 }), plugins: [ autoWebPlugin, ],};
以上配置文件為了重點展示出本文側重修改的部分,省略了部分和上一節一致的代碼,完整代碼可以參照上一節或者下載本項目完整代碼。
AutoWebPlugin
會找出 pages
目錄下的2個文件夾 index
和 login
,把這兩個文件夾看成兩個單頁應用。
WebPlugin
配置。
每個單頁應用的 Chunk 名稱就等於文件夾的名稱,也就是說 autoWebPlugin.entry()
方法返回的內容其實是:
{ "index":["./pages/index/index.js","./common.css"], "login":["./pages/login/index.js","./common.css"]}
但這些事情 AutoWebPlugin
都會自動為你完成,你不用操心,明白大致原理即可。
template.html
模版文件如下:
<html><head> <meta charset="UTF-8"> <!--在這注入該頁面所依賴但沒有手動導入的 CSS--> <!--STYLE--> <!--注入 google_analytics 中的 JS 代碼--> <script src="./google_analytics.js?_inline"></script> <!--非同步載入 Disqus 評論--> <script src="https://dive-into-webpack.disqus.com/embed.js" async></script></head><body><div id="app"></div><!--在這注入該頁面所依賴但沒有手動導入的 JavaScript--><!--SCRIPT--><!--Disqus 評論容器--><div id="disqus_thread"></div></body></html>
注意到模版文件中出現了2個重要的新關鍵字 <!--STYLE-->
和 <!--SCRIPT-->
,它們是什麼意思呢?
由於這個模版文件被當作項目中所有單頁應用的模版,就不能再像上一節中直接寫 Chunk 的名稱去引入資源,因為需要被注入到當前頁面的 Chunk 名稱是不定的,每個單頁應用都會有自己的名稱。
<!--STYLE-->
和 <!--SCRIPT-->
的作用在於保證該頁面所依賴的資源都會被注入到生成的 HTML 模版里去。web-webpack-plugin 能分析出每個頁面依賴哪些資源,例如對於 login.html
來說,插件可以確定該頁面依賴以下資源:
- 所有頁面都依賴的公共 CSS 代碼
common.css
; - 所有頁面都依賴的公共 JavaScrip 代碼
common.js
; - 只有這個頁面依賴的 CSS 代碼
login.css
; - 只有這個頁面依賴的 JavaScrip 代碼
login.css
。
由於模版文件 template.html
里沒有指出引入這些依賴資源的 HTML 語句,插件會自動將沒有手動導入但頁面依賴的資源按照不同類型注入到 <!--STYLE-->
和 <!--SCRIPT-->
所在的位置。
- CSS 類型的文件注入到
<!--STYLE-->
所在的位置,如果<!--STYLE-->
不存在就注入到 HTML HEAD 標籤的最後; - JavaScrip 類型的文件注入到
<!--SCRIPT-->
所在的位置,如果<!--SCRIPT-->
不存在就注入到 HTML BODY 標籤的最後。
如果後續有新的頁面需要開發,只需要在 pages
目錄下新建一個目錄,目錄名稱取為輸出 HTML 文件的名稱,目錄下放這個頁面相關的代碼即可,無需改動構建代碼。
由於 AutoWebPlugin
是間接的通過上一節提到的 WebPlugin
實現的,WebPlugin
支持的功能 AutoWebPlugin
都支持。
AutoWebPlugin
插件還支持一些其它更高級的用法,詳情可以訪問該項目主頁閱讀文檔。
本實例提供項目完整代碼
《深入淺出Webpack》全書在線閱讀鏈接
閱讀原文
推薦閱讀:
※webpack真的適合SPA么?
※webpack如何全局引入jquery和插件?
※徹底解決Webpack打包性能問題
※webpack技術講解及入門
※webpack源碼學習系列之一:如何實現一個簡單的webpack