前情提要:
我之前配過兩次webpack多頁面,gulp+webpack多頁面,後來經過項目實踐,我發現gulp+webpack那一版還能湊合用,但是之前配置的那一版純webpack版本的簡直難用的要死,進羣的小夥伴大部分也是問這個問題的,所以我痛定思痛,重新溫習了下webpack基礎,通過一個星期的努力,終於配置出了我自己認爲很完美的純webpack4的多頁面的開發環境,以前的配置多頁面博客我會刪除,免得誤導大家!!!!
結尾我會附上我這個模板的碼雲地址
下面說下配置思路
- 配置共分4個文件,一個入口文件,一個基礎配置文件,一個開發環境文件,一個生產環境文件,足矣
- 可以支持ES6語法,還要支持async await,Class等高級語法,還要支持includes這種語法會被轉譯
- 可以支持scss語法,生成css文件後,自動加瀏覽器css後綴
- html可以處理公共的頭部和尾部,html去空格,去掉屬性的雙引號
- 可以自動處理圖片,小的圖片自動處理成base64,大點的通過url-loader處理,打包到目標文件夾下
- 我們是根據項目需求,把url全部抽離出來了,當然你也可以不抽離
- 開發,測試,部署請求地址分開,基於第6項
- 公共的js抽離出來,scss文件引入js後,可以單獨拉出來形成css文件,js生產環境下壓縮混淆,css壓縮
- 清除dos窗口的一些打包出來的信息,開發時類似vue-cli腳手架,打包完了提示項目在哪裏運行
- 把首頁index.html單獨抽離出來放在服務器的最外邊,這樣就可以通過域名直接訪問首頁
- 默認端口號一旦被佔用,自動尋找下一個能用的端口號
- js我是全部下載的min.js通過公共的html引入的,當然你在實際項目中也可以import引入,這都不影響
- assets文件夾下只放webpack能處理的文件,static放一些靜態文件,不會被webpack處理的文件
項目結構
先說下我的項目結構
經過我們的一致討論,決定還是把每個頁面都單獨弄成一個文件夾,然後html,js,scss文件全部放到自己的文件夾下,在src同級目錄下會有個static文件夾,把一些靜態文件,不需要webpack處理的文件可以放在裏面,這樣這個文件夾會原封不動的複製到目標文件夾下
就像這樣,下面先說下多頁面處理多入口的處理辦法
多入口處理
一般人會選擇glob這個npm包來遍歷我們的文件夾,但是我喜歡用nodejs的原生的fs文件系統,整體思路就是先把所以頁面的js文件讀出來,形成一個入口對象,然後再把所有頁面的html文件讀出來,形成一個可以被html-webpack-plugin這個插件給處理的一個html數組,然後再通過這個插件形成對應的html文件
如果你的項目和我的項目結構一樣,那麼入口文件應該是這樣的
const fs = require('fs');
const path = require('path');
function readDir(url){
return fs.readdirSync(path.resolve(__dirname,url));
}
const files = readDir('../src/pages').filter(item => item != 'include');
let htmlArray = [];
let entries = {};
files.forEach(item => {
let result = readDir(`../src/pages/${item}`);
result.forEach(val => {
let extname = path.extname(val).slice(1);
switch(extname){
case 'html':
if(val == 'index.html'){
htmlArray.push({
template:path.resolve(__dirname,`../src/pages/${item}/${val}`),
filename:`${val}`,
chunks:result.length == 3?[val.split('.')[0]]:[],
minify:{
removeAttributeQuotes:true,//刪除html屬性的雙引號
collapseWhitespace:true,//摺疊空行,把所有的html摺疊成一行
}
});
}else{
htmlArray.push({
template:path.resolve(__dirname,`../src/pages/${item}/${val}`),
filename:`pages/${item}/${val}`,
chunks:result.length == 3?[val.split('.')[0]]:[],
minify:{
removeAttributeQuotes:true,//刪除html屬性的雙引號
collapseWhitespace:true,//摺疊空行,把所有的html摺疊成一行
}
});
}
break;
case 'js':
entries[val.split('.')[0]] = path.resolve(__dirname,`../src/pages/${item}/${val}`);
break;
}
})
});
module.exports = {
entries,
htmlArray
}
如果你的項目結構是這樣的,那麼入口這樣的
把文件都拆成三個文件夾了,那麼你的入口應該是這個樣子的
const fs = require('fs');
const path = require('path');
let files = fs.readdirSync(path.resolve(__dirname,'../src/js'));
let htmlFiles = fs.readdirSync(path.resolve(__dirname,'../src/pages'))
htmlFiles = htmlFiles.filter(item => item.indexOf('.html') != -1);
let entries = {};
let htmlArray = [];
files.forEach(item => {
entries[item.split('.')[0]] = [path.resolve(__dirname,`../src/js/${item}`)];
});
htmlFiles.forEach(val => {
let arr = files.filter(item => val.split('.')[0] == item.split('.')[0]);
if(arr.length > 0){
htmlArray.push({
template:path.resolve(__dirname,`../src/pages/${arr[0].split('.')[0]}.html`),
filename:`pages/${arr[0].split('.')[0]}.html`,
chunks:['vendors','common',arr[0].split('.')[0]],
minify:{
removeAttributeQuotes:true,//刪除html屬性的雙引號
collapseWhitespace:true,//摺疊空行,把所有的html摺疊成一行
}
})
}else{
htmlArray.push({
template:path.resolve(__dirname,`../src/pages/${val.split('.')[0]}.html`),
filename:`pages/${val.split('.')[0]}.html`,
chunks:[],
minify:{
removeAttributeQuotes:true,//刪除html屬性的雙引號
collapseWhitespace:true,//摺疊空行,把所有的html摺疊成一行
}
})
}
})
module.exports = {
entries,
htmlArray
}
具體情況,看你自己的項目,自己做調整就好
下面我貼下我的配置文件
webpack.base.js
const path = require('path');
const entry = require('./entries');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const autoprefixer = require('autoprefixer');
const copyWebpackPlugin = require('copy-webpack-plugin');
const targetPath = 'dist';
function getProjectAbsolutePath(dir) {
return path.resolve(__dirname, '../', dir)
}
module.exports = {
entry:entry.entries,
output:{
filename:'pages/[name]/[name].js',
path:path.resolve(__dirname,`../${targetPath}`),
publicPath:'/'
},
resolve:{
extensions:['.js','.scss','css','.json','.vue'],
alias:{//別名
'vue$':'vue/dist/vue.esm.js',
'@': path.join(__dirname, '../src')
},
},
plugins:[
new copyWebpackPlugin({
patterns: [
{
from:'static',
to:'static'
}
]
}),
new MiniCssExtractPlugin({
filename:'pages/[name]/[name].css'
})
],
optimization:{
splitChunks:{//分割代碼塊
cacheGroups:{//緩存組
common:{//公共的模塊
name:'common',
chunks:'initial',
reuseExistingChunk: true,
minSize:0,
minChunks:2
},
vendors:{
name:"vendors",
priority:1,
test:/node_modules/,
chunks:'initial',
reuseExistingChunk: true,
minSize:0,
minChunks:2
}
}
}
},
module:{
rules:[
{
test:/\.js$/,
exclude:/node_modules/,//排除哪個文件夾
include:getProjectAbsolutePath('src'),//包括哪個文件夾
use:[
{
loader:'babel-loader',
options:{//用babel-loader需要把es6轉換成es5
"presets":[
['@babel/preset-env',
{
targets: {
ie: "9",
edge: "17", // 程序支持支持 Edge 17
firefox: "50", // 程序支持支持 firefox 60
chrome: "60", // 程序支持支持 chrome 67
safari: "10" // 程序支持safari 11.1
// 支持的配置有以下: chrome, opera, edge, firefox, safari, ie, ios, android, node, electron.
},
useBuiltIns: "usage", //按需注⼊
corejs:2
}
]
],
}
}
]
},
{
//可以處理scss文件, node-sass,sass-loader
test:/\.css$/,
use:[
{
loader:MiniCssExtractPlugin.loader
},
'css-loader',
{
loader: "postcss-loader",
options: {
plugins: [
autoprefixer({
overrideBrowserslist: ["defaults","> 1%","last 10 versions","Firefox < 20","not ie <= 8","ios > 7","cover 99.5%"]
})
]
}
}
]
},
{
test:/\.scss$/,
use:[
{
loader:MiniCssExtractPlugin.loader
},
'css-loader',//@import語法解析路徑
{
loader: "postcss-loader",
options: {
plugins: [
autoprefixer({
overrideBrowserslist: ['ie >= 8','Firefox >= 20', 'Safari >= 5', 'Android >= 4','Ios >= 6', 'last 4 version']
})
]
}
},
'sass-loader'//把scss -> css
]
},
{
test:/\.html$/,
use:'html-withimg-loader'
},
{
test:/\.(jpg|png|gif|svg)$/,
use:[
{
loader:"url-loader",
//當圖片小於多少k的時候用base64轉化
options:{
limit:10*1024,
esModule: false,
outputPath: 'assets/imgs'
}
}
]
}
]
}
}
entry.htmlArray.forEach(element => {
module.exports.plugins.push(new HtmlWebpackPlugin(element));
});
webpack.dev.js
const {smart} = require('webpack-merge');
const webpack = require('webpack');
const base = require('./webpack.base.js');
const FriendlyErrorsWebpackPlugin = require('friendly-errors-webpack-plugin');
const portfinder = require('portfinder');
const os = require('os');
///////////////////獲取本機ip///////////////////////
function getIPAdress() {
var interfaces = os.networkInterfaces();
for (var devName in interfaces) {
var iface = interfaces[devName];
for (var i = 0; i < iface.length; i++) {
var alias = iface[i];
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
return alias.address;
}
}
}
}
const Host = getIPAdress();
const path = require('path')
const fs = require('fs');
const data = fs.readFileSync(path.resolve(__dirname,'./測試.js'),'utf8');
let lines = data.split('\n');
let indexs = [];
lines.forEach((item,index) => {
item = item.replace('\r','');
if(item.indexOf('`') != -1){
indexs.push(index)
}
})
let urls = ''
for(let i = indexs[0]; i <= indexs[1];i++){
urls += (lines[i].trim());
}
urls = JSON.parse(urls.replace(/`/gi,''));
let url = urls.data.filter(item => item.name == 'URL')[0].value;
const devWebpackConfig = smart(base,{
mode:'development',
devtool:'eval-source-map',
devServer:{
contentBase:false,//啓動目錄
compress:true,//gzip壓縮
overlay: {
warnings: false,
errors: true
},
host:'0.0.0.0',
historyApiFallback: true,
disableHostCheck:true,
quiet:true,
proxy: {
'/api': {
target: url,
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
},
},
plugins:[
new webpack.HotModuleReplacementPlugin(),//熱更新插件
new webpack.NamedModulesPlugin()
],
});
module.exports = new Promise((resolve, reject) => {
portfinder.basePort = 8080;
portfinder.getPort((err, port) => {
if (err) {
reject(err)
} else {
devWebpackConfig.devServer.port = port;
devWebpackConfig.plugins.push(new FriendlyErrorsWebpackPlugin({
compilationSuccessInfo: {
messages: [`項目模板運行在這裏 : http://${Host}:${port}`],
},
onErrors: function(severity,errors){
const notifier = require('node-notifier');
if (severity !== 'error') return;
notifier.notify({
title: '小笨蛋',
message: '運行出錯啦,快去看看錯誤提示吧'
})
}
}))
resolve(devWebpackConfig);
}
})
})
webpack.prod.js
const {smart} = require('webpack-merge');
const base = require('./webpack.base.js');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin')
module.exports = smart(base,{
mode:'production',
devtool:false,
optimization:{
minimizer:[
new TerserPlugin(),
new OptimizeCSSAssetsPlugin({})
]
},
plugins:[
new CleanWebpackPlugin()
],
})
值得一提的是,配置了這裏了之後,如下圖所示,就可以不用在每個入口引入@babel/polyfill文件了,但是這個@babel/polyfill必須得npm i一下
而且必須下載到這裏才能行,如上圖所示,對了,還有個事,以下圈起來的兩個
這兩個是我們使用arcgis做地圖用的,如果你沒有arcgis地圖業務,可以把這兩個刪除,最後再貼一個抽取公共方法的配置
optimization: {
splitChunks: {
chunks: 'all',//拆分代碼類型(同步/異步/同步+異步)
minSize: 30000,//引入的庫大小大於30000個字節(30kb)纔回去做代碼分割
maxSize: 0,////對超出的大小進行二次拆分(一般不配置)
minChunks: 1,//當一個模塊被用了至少多少次才進行代碼分割
maxAsyncRequests: 6,//同時加載的模塊數最多是六個,也就是說,如果我引入了很多庫,最多拆分出六個,多了的就不進行拆分了
maxInitialRequests: 4,//入口文件進行代碼分割最多引入四個
automaticNameDelimiter: '~',//文件生成的時候的連接符
name:true,//使cacheGroups中配置的文件名稱有效
cacheGroups: {//緩存組:符合條件的先緩存着,當所有的文件都分析完成之後,在開啓拆分
vendors: {//會將node_modules中所有的依賴拆分到filename的文件中
test: /[\\/]node_modules[\\/]/,
priority: -10,//值越大優先級越高(優先匹配)
filename:'js/[name].js'//自定義打包出來的 文件路徑和名稱
},
default: {//會將自己定義的一些方法拆分到這個緩存組中(所有的模塊都符合default的要求)
minChunks: 2,//當一個模塊被用了至少多少次才進行代碼分割
priority: -20,
reuseExistingChunk: true,//如果一個模塊已經被打包過了,就不會再次被打包了,而是回去找打包的文件
filename:'js/[name].js'
}
}
}
}
項目如何啓動在README.md文件中都又詳細說明,如果有問題可以加入我們的交流羣來提問!!!
下面我貼上一張運行起來的圖
是不是很熟悉,對,我就是模仿的vue-cli的腳手架,哈哈!
此模板碼雲地址:webpack4多頁面配置開發模板https://gitee.com/zhaoxiang688/webpack4-vue-multipage
點擊上述超鏈接即可訪問