一文搞懂 Webpack 多入口配置

  • 2019 年 10 月 4 日
  • 筆記

最近在做項目的時候遇到了一個場景:一個項目有多個入口,不同的入口,路由、組件、資源等有重疊部分,也有各自不同的部分。由於不同入口下的路由頁面有一些是重複的,因此我考慮使用 Webpack 多入口配置來解決這個需求。

再一次,在網上找的不少文章都不合我的需求,很多文章都是只簡單介紹了生產環境下配置,沒有介紹開發環境下的配置,有的也沒有將多入口結合 vue-routervuexElementUI 等進行配置,因此在下通過不斷探坑,然後將思路和配置過程記錄下來,留給自己作為筆記,同時也分享給大家,希望可以幫助到有同樣需求的同學們~

1. 目標分析

  1. 一個項目中保存了多個 HTML 模版,不同的模版有不同的入口,並且有各自的 router、store 等;
  2. 不僅可以打包出不同 HTML,而且開發的時候也可以順利進行調試;
  3. 不同入口的文件可以引用同一份組件、圖片等資源,也可以引用不同的資源;

程式碼倉庫:multi-entry-vue

示意圖如下:

2. 準備工作

首先我們 vue init webpack multi-entry-vue 使用 vue-cli 創建一個 webpack 模版的項。文件結構如下:

.  ├── build  ├── config  ├── src  │   ├── assets  │   │   └── logo.png  │   ├── components  │   │   └── HelloWorld.vue  │   ├── router  │   │   └── index.js  │   ├── App.vue  │   └── main.js  ├── static  ├── README.md  ├── index.html  ├── package-lock.json  └── package.json

這裡順便介紹在不同系統下生成目錄樹的方法:

  1. mac 系統命令行生成目錄樹的方法 tree-I node_modules--dirsfirst ,這個命令的意思是,不顯示 node_modules 路徑的文件,並且以文件夾在前的排序方式生成目錄樹。如果報沒有找到 tree 命令的錯,安裝 tree 命令行 brew install tree 即可。
  2. windows 系統在目標目錄下使用 tree/f1.txt 即可把當前目錄樹生成到一個新文件 1.txt 中。

首先我們簡單介紹一下 Webpack 的相關配置項,這些配置項根據使用的 Webpack 模版不同,一般存放在 webpack.config.jswebpack.base.conf.js 中:

const path = require('path')  module.exports = {    context: path.resolve(__dirname, '../'),    entry: {      app: './src/main.js'    },    output: {      path: path.resolve(__dirname, '../dist'),      filename: 'output-file.js',      publicPath: '/'    },    module: {},        // 文件的解析 loader 配置    plugins: [],       // 插件,根據需要配置各種插件    devServer: {}      // 配置 dev 服務功能  }

這個配置的意思是,進行 Webpack 後,會在命令的執行目錄下新建 dist 目錄(如果需要的話),並將打包 src 目錄下的 main.js 和它的依賴,生成 output-file.js 放在 dist 目錄中。

下面稍微解釋一下相關配置項:

  1. entry: 入口文件配置項,可以為字元串、對象、數組。以上面的對象形式為例, app 是入口名稱,如果 output.filename 中有 [name] 的話,就會被替換成 app
  2. context: 是 webpack 編譯時的基礎目錄,用於解析 entry 選項的基礎目錄(絕對路徑), entry 入口起點會相對於此目錄查找,相當於公共目錄,下面所有的目錄都在這個公共目錄下面。
  3. output: 出口文件的配置項。
  4. output/path: 打包文件輸出的目錄,比如上面的 dist,那麼就會將輸出的文件放在當前目錄同級目錄的 dist 文件夾下,沒有這個文件夾就新建一個。可以配置為 path.resolve(__dirname,'./dist/${Date.now()}/') (md 語法不方便改成模板字元串,請自行修改)方便做持續集成。
  5. output.filename: 輸出的文件名稱, [name] 的意為根據入口文件的名稱,打包成相同的名稱,有幾個入口,就可以打包出幾個文件。比如入口的 keyapp,打包出來就是 app.js,入口是 my-entry,打包出來就是 my-entry.js
  6. output.publicPath: 靜態資源的公共路徑,可以記住這個公式: 靜態資源最終訪問路徑=output.publicPath+資源loader或插件等配置路徑。舉個例子, publicPath 配置為 /dist/,圖片的 url-loader 配置項為 name:'img/[name].[ext]' ,那麼最終打包出來文件中圖片的引用路徑為 output.publicPath+'img/[name].[ext]'='/dist/img/[name].[ext]'

本文由於是入口和出口相關的配置,所以內容主要圍繞著 entryoutput 和一個重要的 webpack 插件 html-webpack-plugin,這個插件是跟打包出來的 HTML 文件密切相關,主要有下面幾個作用:

  1. 根據模版生成 HTML 文件;
  2. 給生成的 HTML 文件引入外部資源比如 linkscript 等;
  3. 改變每次引入的外部文件的 Hash,防止 HTML 引用快取中的過時資源;

下面我們從頭一步步配置一個多入口項目。

3. 開始配置

3.1 文件結構改動

src 目錄下將 main.jsApp.vue 兩個文件各複製一下,作為不同入口,文件結構變為:

.  ├── build  │   ├── build.js  │   ├── check-versions.js  │   ├── logo.png  │   ├── utils.js  │   ├── vue-loader.conf.js  │   ├── webpack.base.conf.js  │   ├── webpack.dev.conf.js    # 主要配置目標  │   └── webpack.prod.conf.js   # 主要配置目標  ├── config  │   ├── dev.env.js  │   ├── index.js  │   └── prod.env.js  ├── src  │   ├── assets  │   │   └── logo.png  │   ├── components  │   │   └── HelloWorld.vue  │   ├── router  │   │   └── index.js  │   ├── App.vue  │   ├── App2.vue       # 新增的入口  │   ├── main.js  │   └── main2.js       # 新增的入口  ├── static  ├── README.md  ├── index.html  └── package.json

3.2 簡單配置

要想從不同入口,打包出不同 HTML,我們可以改變一下 entryoutput 兩個配置,

// build/webpack.prod.conf.js    module.exports = {    entry: {      entry1: './src/main.js',      entry2: './src/main2.js'    },    output: {      filename: '[name].js',      publicPath: '/'    },      plugins: [          new HtmlWebpackPlugin({              template: "index.html",  // 要打包輸出哪個文件,可以使用相對路徑              filename: "index.html"   // 打包輸出後該html文件的名稱          })      ]  }

根據上面一小節我們知道,webpack 配置里的 output.filename 如果有 [name] 意為根據入口文件的名稱,打包成對應名稱的 JS 文件,那麼現在我們是可以根據兩個入口打包出 entry.jsentry2.js

打包的結果如下:

當前程式碼:Github – multi-entry-vue1

如上圖,此時我們 npm run build 打包出一個引用了這兩個文件的 index.html,那麼如何打包出不同 HTML 文件,分別應用不同入口 JS 文件呢,此時我們需要藉助於 HtmlWebpackPlugin 這個插件。

HtmlWebpackPlugin 這個插件, new 一個,就打包一個 HTML 頁面,所以我們在 plugins 配置里 new 兩個,就能打包出兩個頁面來。

3.3 打包出不同的 HTML 頁面

我們把配置文件改成下面這樣:

// build/webpack.prod.conf.js    module.exports = {    entry: {      entry: './src/main.js',   // 打包輸出的chunk名為entry      entry2: './src/main2.js'  // 打包輸出的chunk名為entry2    },    output: {      filename: '[name].js',      publicPath: '/'    },    plugins: [      new HtmlWebpackPlugin({        filename: 'entry.html',  // 要打包輸出的文件名        template: 'index.html',  // 打包輸出後該html文件的名稱        chunks: ['manifest', 'vendor', 'entry']  // 輸出的html文件引入的入口chunk        // 還有一些其他配置比如minify、chunksSortMode和本文無關就省略,詳見github      }),      new HtmlWebpackPlugin({        filename: 'entry2.html',        template: 'index.html',        chunks: ['manifest', 'vendor', 'entry2']      })    ]  }

上面一個配置要注意的是 chunks,如果沒有配置,那麼生成的 HTML 會引入所有入口 JS 文件,在上面的例子就是,生成的兩個 HTML 文件都會引入 entry.jsentry2.js,所以要使用 chunks 配置來指定生成的 HTML 文件應該引入哪個 JS 文件。配置了 chunks 之後,才能達到不同的 HTML 只引入對應 chunks 的 JS 文件的目的。

大家可以看到除了我們打包生成的 chunk 文件 entry.jsentry2.js 之外,還有 manifestvendor 這兩個,這裡稍微解釋一下這兩個 chunk

  1. vendor 是指提取涉及 node_modules 中的公共模組;
  2. manifest 是對 vendor 模組做的快取;

打包完的結果如下:

文件結構:

現在打包出來的樣式正是我們所需要的,此時我們在 dist 目錄下啟動 live-server(如果你沒安裝的話可以先安裝 npm i-g live-server),就可以看到效果出來了:

當前程式碼:Github – multi-entry-vue2

至此就實現了一個簡單的多入口項目的配置。

4. 配置改進

4.1 文件結構改動

我們在前文進行了多入口的配置,要想新建一個新的入口,就複製多個文件,再手動改一下對應配置。

但是如果不同的 HTML 文件下不同的 vue-routervuex 都放到 src 目錄下,多個入口的內容平鋪在一起,項目目錄會變得凌亂不清晰,因此在下將多入口相關的文件放到一個單獨的文件夾中,以後如果有多入口的內容,就到這個文件夾中處理。

下面我們進行文件結構的改造:

  1. 首先我們在根目錄創建一個 entries 文件夾,把不同入口的 routerstoremain.js 都放這裡,每個入口相關單獨放在一個文件夾;
  2. src 目錄下建立一個 common 文件夾,用來存放多入口共用的組件等;

現在的目錄結構:

.  ├── build    # 沒有改動  ├── config   # 沒有改動  ├── entries  # 存放不同入口的文件  │   ├── entry1  │   │   ├── router       # entry1 的 router  │   │   │   └── index.js  │   │   ├── store        # entry1 的 store  │   │   │   └── index.js  │   │   ├── App.vue      # entry1 的根組件  │   │   ├── index.html   # entry1 的頁面模版  │   │   └── main.js      # entry1 的入口  │   └── entry2  │       ├── router  │       │   └── index.js  │       ├── store  │       │   └── index.js  │       ├── App.vue  │       ├── index.html  │       └── main.js  ├── src  │   ├── assets  │   │   └── logo.png  │   ├── common          # 多入口通用組件  │   │   └── CommonTemplate.vue  │   └── components  │       ├── HelloWorld.vue  │       ├── test1.vue  │       └── test2.vue  ├── static  ├── README.md  ├── index.html  ├── package-lock.json  └── package.json

4.2 webpack 配置

然後我們在 build/utils 文件中加兩個函數,分別用來生成 webpack 的 entry 配置和 HtmlWebpackPlugin 插件配置,由於要使用 node.js 來讀取文件夾結構,因此需要引入 fsglob 等模組:

// build/utils  const fs = require('fs')  const glob = require('glob')  const merge = require('webpack-merge')  const HtmlWebpackPlugin = require('html-webpack-plugin')  const ENTRY_PATH = path.resolve(__dirname, '../entries')    // 多入口配置,這個函數從 entries 文件夾中讀取入口文件,裝配成webpack.entry配置  exports.entries = function() {    const entryFiles = glob.sync(ENTRY_PATH + '/*/*.js')    const map = {}    entryFiles.forEach(filePath => {      const filename = filePath.replace(/.*/(w+)/w+(.html|.js)$/, (rs, $1) => $1)      map[filename] = filePath    })    return map  }    // 多頁面輸出模版配置 HtmlWebpackPlugin,根據環境裝配html模版配置  exports.htmlPlugin = function() {    let entryHtml = glob.sync(ENTRY_PATH + '/*/*.html')    let arr = []    entryHtml.forEach(filePath => {      let filename = filePath.replace(/.*/(w+)/w+(.html|.js)$/, (rs, $1) => $1)      let conf = {        template: filePath,        filename: filename + '.html',        chunks: [filename],        inject: true      }        // production 生產模式下配置      if (process.env.NODE_ENV === 'production') {        conf = merge(conf, {          chunks: ['manifest', 'vendor'],          minify: {            removeComments: true,            collapseWhitespace: true,            removeAttributeQuotes: true          },          chunksSortMode: 'dependency'        })      }      arr.push(new HtmlWebpackPlugin(conf))    })    return arr  }

稍微解釋一下這兩個函數:

  1. exports.entries 函數從 entries 文件夾中找到二級目錄下的 JS 文件作為入口文件,並且將二級目錄的文件夾名作為 key,生成這樣一個對象: {"entry1":"/multi-entry-vue/entries/entry1/main.js"},多個入口情況下會有更多鍵值對;
  2. exports.htmlPlugin 函數和之前函數的原理類似,不過組裝的是 HtmlWebpackPlugin 插件的配置,生成這樣一個數組,可以看到和我們手動設置的配置基本一樣,只不過現在是根據文件夾結構來生成的:
// production 下  [    {      template: "/multi-entry-vue/entries/entry1/index.html",      chunks: ['manifest', 'vendor', 'entry1'],      filename: "entry1.html",      chunksSortMode: 'dependency'    },    { ... }   // 下一個入口的配置  ]

有了這兩個根據 entries 文件夾的結構來自動生成 webpack 配置的函數,下面來改一下 webpack 相關的幾個配置文件:

// build/webpack.base.conf.js    module.exports = {    entry: utils.entries(),   // 使用函數生成 entry 配置    output: {      path: config.build.assetsRoot,      filename: '[name].js',      publicPath: process.env.NODE_ENV === 'production'        ? config.build.assetsPublicPath        : config.dev.assetsPublicPath    }  }
// build/webpack.dev.conf.js    // const HtmlWebpackPlugin = require('html-webpack-plugin')  // 不需要了    const devWebpackConfig = merge(baseWebpackConfig, {    devServer: {      historyApiFallback: {        rewrites: [        // 別忘了把 devserver 的默認路由改一下          { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'entry1.html') },        ],      }    },    plugins: [      // https://github.com/ampedandwired/html-webpack-plugin      // new HtmlWebpackPlugin({      //   filename: 'index.html',      //   template: 'index.html',      //   inject: true      // }),                   // 注釋掉原來的 HtmlWebpackPlugin 配置,使用生成的配置    ].concat(utils.htmlPlugin())  })
// build/webpack.prod.conf.js    // const HtmlWebpackPlugin = require('html-webpack-plugin')    const webpackConfig = merge(baseWebpackConfig, {    plugins: [      // new HtmlWebpackPlugin({      //   ... 注釋掉,不需要了      // }),    ].concat(utils.htmlPlugin())  })

現在我們再 npm run build,看看生成的目錄是什麼樣的:

此時我們在 dist 目錄下啟動 live-server 看看是什麼效果:

當前程式碼:Github – multi-entry-vue3


網上的帖子大多深淺不一,甚至有些前後矛盾,在下的文章都是學習過程中的總結,如果發現錯誤,歡迎留言指出~

參考:

  1. webpack解惑:多入口文件打包策略
  2. webpack配置文件:入口和出口,多入口、多出口配置
  3. 一看就懂之webpack高級配置與優化

文章轉載自公眾號 前端下午茶 , 作者 SHERlocked93