大多數情況下,我們使用 webpack
來打包單頁應用程式,這個時候只需要配置一個入口,一個模板檔案,但也不盡是如此,有時候也會碰到多頁面的專案,而且以我的經驗來看,這種情況出現的頻率還不低,例如專案比較大,無法進行全域性的把握,或者專案需要多次的更新迭代等,都適合做成多頁面程式,這就涉及到了 webpack
的多頁面檔案的打包配置問題。
手動配置
單頁應用程式和多頁應用程式的 webpack
配置檔案其實絕大部分都還是相同的,只不過多頁的配置需要在單頁配置的基礎上顧及到多個頁面罷了,loader
、output
、plugins
這些基本都不需要改動,需要改動的一般都是入口檔案 entry
,如果你用到了 抽離css
樣式的外掛 extract-text-webpack-plugin
、自動模板外掛 html-webpack-plugin
的話,那麼還需要對這兩個外掛進行額外的改寫,大多數情況下,我們也都只需要改動這三個地方,所以本文就只簡單說下這三個位置,如果在實際的專案中還有其他的地方需要改動,參照這三個位置即可。
示例的檔案目錄如下:
entry
單頁應用程式的入口配置一般如下所示:
entry: resolve(__dirname, "src/home/index.js")
複製程式碼
這個配置就是指定 webpack
從 /src/home/index.js
這個檔案開始進入,進行一系列的打包編譯過程。
如果是多頁應用程式,則需要多個入口檔案,例如:
entry: {
home: resolve(__dirname, "src/home/index.js"),
about: resolve(__dirname, "src/about/index.js")
}
複製程式碼
這樣,整個專案就有了兩個入口 home
和 about
extract-text-webpack-plugin
extract-text-webpack-plugin
外掛主要是為了抽離css
樣式,防止將樣式打包在 js
中引起頁面樣式載入錯亂或者 js
指令碼體積過大等情況,單頁程式中,一般這樣使用此外掛:
plugins: [
new ExtractTextPlugin('style.[contenthash].css')
]
複製程式碼
而到了多頁程式,因為存在多個入口檔案以及對應的多個頁面,每個頁面都有自己的 css
樣式,所以需要為每個頁面各自配置一下:
plugins: [
new ExtractTextPlugin('home/[name].[contenthash].css'),
new ExtractTextPlugin('about/[name].[contenthash].css')
]
複製程式碼
除此之外還需要注意一點,每個頁面也只需要自己的 css
樣式,理論上把別的頁面 css
樣式檔案也打包到自己的頁面中當然也是可以的,但顯然是不合理的,這隻會增加冗餘程式碼,還可能會導致不可預測的樣式覆蓋等問題,所以需要對下面這種 loader
配置進行修改:
{
test: /\.css$/,
loader: 'style!css!autoprefixer'
},
{
test: /\.scss$/,
loaders: [
'style',
'css?modules&importLoaders=1&localIdentName=[path]___[name]__[local]___[hash:base64:5]',
'sass',
'autoprefixer'
]
},
複製程式碼
上面的配置會把所有編譯出來的 css
檔案打包到同一個檔案中,我們要做的就是把這些 css
分離開,每個頁面都有各自單獨的 css
樣式檔案:
// 為每個頁面定義一個 ExtractTextPlugin
const homeExtractCss = new ExtractTextPlugin('home/[name].[contenthash].css')
const aboutExtractCss = new ExtractTextPlugin('about/[name].[contenthash].css')
// ...
module: {
rules: [
// 每個頁面的 ExtractTextPlugin 只處理這個頁面的樣式檔案
{
test: /src(\\|\/)home(\\|\/)css(\\|\/).*\.(css|scss)$/,
use: homePageExtractCss.extract({
fallback: 'style-loader',
use: ['css-loader', 'postcss-loader', 'sass-loader']
})
},
{
test: /src(\\|\/)about(\\|\/)css(\\|\/).*\.(css|scss)$/,
use: salePersonalCenterExtractCss.extract({
fallback: 'style-loader',
use: ['css-loader', 'postcss-loader', 'sass-loader']
})
}
]
}
// ...
// 每個頁面都有各自的 ExtractTextPlugin,所以需要都宣告一遍
plugins: [
homeExtractCss,
aboutExtractCss
]
複製程式碼
html-webpack-plugin
html-webpack-plugin
外掛的使用,在單頁應用程式和多頁應用程式中的 webpack
配置沒什麼區別
new HtmlWebpackPlugin({
filename: 'home/home.html',
template: 'src/home/html/index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true
}
})
new HtmlWebpackPlugin({
filename: 'about/about.html',
template: 'src/about/html/index.html',
inject: true,
minify: {
removeComments: true,
collapseWhitespace: true
}
})
複製程式碼
有幾個頁面,就對每個頁面進行上述配置即可。
自動配置
上述的配置程式碼已經可以滿足多頁面開發需求了,但是有一點似乎有些遺憾,那就是每增加一個頁面,就需要更新一遍 entry
、extract-text-webpack-plugin
、HtmlWebpackPlugin
的配置,雖然只是幾行程式碼的問題,而且基本上都是複製貼上沒什麼難度,但畢竟程式碼再少也需要過問,並且需要改的地方比較多,倉促之下可能還會遺漏,要是能一勞永逸,寫一遍程式碼,無論以後增刪頁面都不需要過問就好了。
稍微觀察下這個目錄就可以發現,這個目錄結構其實是很有規律的:
每個頁面都是 src/
目錄下的一個資料夾,這個資料夾中有兩個子目錄,分別存放這個頁面的模板 html
,樣式檔案 css
,還有一個入口檔案 index.js
既然有規則,那麼肯定是可以進行程式編碼的,如果按照這種規則,每個頁面都是 ./src
下的一個目錄,目錄名即為頁面名,並且這個目錄中的結構也都是相同的,那麼可以通過一個通用方法來獲取所有的頁面名稱(例如 home
、about
),這個通用方法的一個示例如下:
function getEntry () {
let globPath = 'src/**/html/*.html'
// (\/|\\\\) 這種寫法是為了相容 windows和 mac系統目錄路徑的不同寫法
let pathDir = 'src(\/|\\\\)(.*?)(\/|\\\\)html'
let files = glob.sync(globPath)
let dirname, entries = []
for (let i = 0; i < files.length; i++) {
dirname = path.dirname(files[i])
entries.push(dirname.replace(new RegExp('^' + pathDir), '$2'))
}
return entries
}
複製程式碼
藉助 glob這個庫,遍歷 .src/
目錄下具有這種規律 src/**/html/*.html
的子目錄,通過正則匹配出這個子目錄的名稱
獲取到了所有的頁面名稱,下面就好辦了。
entry
// entry: resolve(__dirname, "src/home/index.js")
// 改為
entry: addEntry()
//...
function addEntry () {
let entryObj = {}
getEntry().forEach(item => {
entryObj[item] = resolve(__dirname, 'src', item, 'index.js')
})
return entryObj
}
複製程式碼
extract-text-webpack-plugin
// plugins: [
// new ExtractTextPlugin('home/[name].[contenthash].css'),
// new ExtractTextPlugin('about/[name].[contenthash].css')
//]
// 改為
const pageExtractCssArray = []
getEntry().forEach(item => {
pageExtractCssArray.push(new ExtractTextPlugin(item + '/[name].[contenthash].css'))
})
// ...
plugins: [...pageExtractCssArray]
複製程式碼
module.rules
樣式相關的兩個loaders
刪掉,改為動態新增:
getEntry().forEach((item, i) => {
webpackconfig.module.rules.push({
test: new RegExp('src' + '(\\\\|\/)' + item + '(\\\\|\/)' + 'css' + '(\\\\|\/)' + '.*\.(css|scss)$'),
use: pageExtractCssArray[i].extract({
fallback: 'style-loader',
use: ['css-loader', 'postcss-loader', 'sass-loader']
})
})
})
// ...
module.exports = webpackconfig
複製程式碼
html-webpack-plugin
plugins
中無需手動初始化 html-webpack-plugin
,改為動態新增:
getEntry().forEach(pathname => {
let conf = {
filename: path.join(pathname, pathname) + '.html',
template: path.join(__dirname, 'src', pathname, 'html', 'index.html')
}
webpackconfig.plugins.push(new HtmlWebpackPlugin(conf))
})
// ...
module.exports = webpackconfig
複製程式碼
完成了上述修改後,以後無論是在專案中新增頁面還是刪除頁面,都無需再對 webpack
配置進行手動修改了,雖然開始時開起來似乎這種動態的自動配置程式碼比較多,而且稍微複雜一點,但是從長期來看,絕對是一勞永逸的好做法。
另外,如果你的專案目錄結構和我示例的目錄結構不一樣,那麼就需要你根據自己的目錄結構對程式碼進行少許的修改,但整體解決問題的方法是不變的,一個易於維護的專案,目錄結構都該是有律可循的。