Webpack實戰-管理多個單頁應用

浩麟發表於2017-12-22

引入問題

上一節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個資料夾 indexlogin,把這兩個資料夾看成兩個單頁應用。 並且分別為每個單頁應用生成一個 Chunk 配置和 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》全書線上閱讀連結

閱讀原文

相關文章