webpack多頁面入口生產專案開發配置
這不是一個純粹的學習帖子,最開始為了生產專案考慮的。公司有個新的、小的活動專案。以此為假想,所以我希望學習一些新的技術應用在上面;這個新的專案是作為舊專案的一個子系統存在的,所以又必須在一定程度上保持一致。
而這個舊專案的原有使用構建工具fis的版本比較老舊,不敢升級,怕出什麼么蛾子,所以又不能動他。
在網上學習了眾多攻略之後自己嘗試搭建了一下,解決了一些問題,也留下了一下疑惑。
專案資源路徑
github: ofollow,noindex">github.com/johnshere/a…
一、環境配置
node: v10.6.0(v6.12.3)、yarn: 1.7.0、webpack: 4.16.1
系統:windows10
yarn是類似npm但是效率更高的包管理工具,命令互換參考 yarnpkg.com/zh-Hans/doc…
可以使用npm安裝(這裡讓我想到IE存在的意義,-_-)
npm install -g yarn 複製程式碼
也可以去官網下載客戶端
二、目錄結構
如圖:

src:工程原始碼; release:工程釋出; webpack.config:webpack配置檔案;
釋出之後的HTML保持與src中的路徑一致;這樣程式碼中使用相對路徑訪問頁面就不會出現結構錯亂的問題。

src目錄下有三個文:entry.json/index.html/index.js;
這是目錄索引頁面,因為是多頁面入口,webpack-dev-server模式開啟的時候用於快速進入自己想要的頁面(下面會說)
三、兩個構建環境切換(與webpack無關)
公司原有的fis是最初版本的,一直沒有人做更新維護,現在已經落後現在的技術版本多年,但是又必須使用。
webpack4不可能在和他進行相容,所以我安裝了兩個不同版本的node,v10.6.0、v6.12.3;使用的時候切換

當然實際的環境變數配置了一個,然後我寫了一個指令碼,執行命令(changeNodeName)後切換資料夾名稱,把這個指令碼放在node個目錄下,如下圖:

指令碼很簡單,就是判斷資料夾名稱、改變名稱;改變後的名稱保持和環境變數裡面的名字一直就行。這樣做的問題也很大,就是沒有辦法同時編輯兩個工程。
set dir=D: set name1=node612 set name2=node106 set name=node if exist %dir%\%name1% ( echo "node612 ==> node" ren %dir%\%name% %name2% ren %dir%\%name1% %name% )else ( echo "node106 ==> node" ren %dir%\%name% %name1% ren %dir%\%name2% %name% ) pause 複製程式碼
這樣切換了node,實際上就是切換整個開發環境,畢竟這兩個構建工具都是依賴於node的。
切換時在cmd或者powershell裡執行:
changeNodeName
四、webpack配置
這個應該是重中之重了,在寫配置之前我首先確定了自己想解決的一些問題
- 釋出後保證目錄結構不變
- 分割公共檔案,如樣式、圖片;達到快取目的
- 分割的大檔案不能過大(未解決)、不能讓使用者頻繁載入
- 保證檔案之間快取良好互不干擾
- 轉義語法
1、webpack.entry.util.js
const path = require("path"); const Glob = require("glob"); const fs = require("fs"); let obj = { /** * 根據目錄獲取入口 * @param{[type]} globPath [description] * @return {[type]}[description] */ getEntryJs: function (globPath) { globPath = path.resolve(__dirname, globPath); let entries = {}; Glob.sync(globPath).forEach(function (entry) { let basename = path.basename(entry, path.extname(entry)), pathname = path.dirname(entry), paths = pathname.split('/'), fileDir = paths.splice(paths.indexOf("src") + 1).join('/'); //僅處理page路徑下的js if (pathname.indexOf("page") > -1) {// && fileDir && fileDir.indexOf(("page") === 0)) { entries[(fileDir ? fileDir + '/' : fileDir) + basename] = pathname + '/' + basename; } }); //目錄頁保留 entries["index"] = path.resolve(__dirname,"../src/index").split("\\").join("/"); console.log("---------------------------------------------\nentries:"); console.log(entries); console.log("----------------------------------------------"); return entries; }, /** * 根據目錄獲取 Html 入口 * @param{[type]} globPath [description] * @return {[type]}[description] */ getEntryHtml: function (globPath) { globPath = path.resolve(__dirname, globPath); let entries = []; Glob.sync(globPath).forEach(function (entry) { let basename = path.basename(entry, path.extname(entry)), pathname = path.dirname(entry), paths = pathname.split('/'), // @see https://github.com/kangax/html-minifier#options-quick-reference minifyConfig = process.env.NODE_ENV === "production" ? { removeComments: true, // collapseWhitespace: true, minifyCSS: true, minifyJS: true } : ""; //只處理page目錄下的HTML //保留目錄頁 if (entry.indexOf("page") > -1 ) { let chunkName = paths.splice(paths.indexOf("src") + 1).join('/') + "/" + basename; entries.push({ filename: chunkName + ".html", template: entry, chunks: ['public/vendor', chunkName], minify: minifyConfig }); } }); //保留目錄頁 entries.push({ filename: "index.html", template: path.resolve(__dirname,"../src/index.html").split("\\").join("/"), chunks: ['public/vendor',"index"] }); //儲存entry的json檔案 this.entry2JsonFile(entries); return entries; }, /** * 生成entry對應的json檔案 * @param entries */ entry2JsonFile: function (entries) { console.log(entries); let json = {}; if (entries) { entries.forEach(v => { json[v.filename] = v.filename; }); } console.log(json); //同步寫入檔案 let fd = fs.openSync(path.resolve(__dirname, "../src/entry.json"), "w"); fs.writeSync(fd, JSON.stringify(json), 0, "utf-8"); fs.closeSync(fd); } }; // obj.getEntry("../src/page/**/*.js"); // obj.getEntryHtml('../src/page/**/index.html'); module.exports = obj; 複製程式碼
這個地方的entry識別參考了:
github地址: github.com/givebest/we…
這個entry工具主要是為了識別js和HTML;我在原有的邏輯上進行了修改,符合了我的要求,即只識別page目錄下的entry。
同時,我添加了一個方法,即將所有的HTML路徑寫入到一個json檔案中儲存起來(後面dev-server模式用到)。前兩個方法裡也為入口目錄頁做了特殊處理 這個工具中對chunk的key值做了特殊處理,可以看出,切割出了從src之後的路徑作為key值,因為webpack的name是支援路徑的,這樣就達到問題1的效果。
2、webpack.base.conf.js
const path = require("path"); const HtmlWebpackPlugin = require('html-webpack-plugin'); // const ExtractTextPlugin = require('extract-text-webpack-plugin'); const CleanWebpackPlugin = require("clean-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const entryUtil = require("./webpack.entry.util"); let entryJs = entryUtil.getEntryJs('../src/page/**/index.js'); let conf = { entry: entryJs,//js打包入口識別 output: { path: path.resolve(__dirname, "../release"), filename: "[name].[chunkHash].js", // publicPath: "../../public" }, module: { rules: [ { test: /\.css$/, // loader: ExtractTextPlugin.extract({ //fallback: 'style-loader', //use: 'css-loader' // }) use:[MiniCssExtractPlugin.loader,'css-loader']//'style-loader', }, { test: /\.html$/, loader: 'html-withimg-loader' }, {test: require.resolve("jquery"), loader: "expose-loader?$!expose-loader?jQuery"} ] }, plugins: [ // new HtmlWebpackPlugin({ //filename: "index.html", //template: "src/page/index.html", //chunks: ["main", "vender"] // }), // new ExtractTextPlugin("./[name].[chunkHash].css") new CleanWebpackPlugin(["release"],{ root: path.resolve(__dirname, ".."), verbose: true, dry: false }), new MiniCssExtractPlugin({ filename: "[name].[contenthash:7].css", chunkFilename: "[name].[contenthash].css" }) ], optimization: { splitChunks: { cacheGroups: { commons: { name: "public/vendor", chunks: "all", minChunks: 2 } } } }, resolve: { extensions: [".js", ".jsx"], alias: { layer: path.resolve(__dirname, "../src/public/js/layer/mobile/layer.js"), "layer.css": path.resolve(__dirname, "../src/public/js/layer/mobile/need/layer.css") } } }; //HTML入口 let entryHtml = entryUtil.getEntryHtml('../src/page/**/index.html'); entryHtml.forEach(function (v) { conf.plugins.push(new HtmlWebpackPlugin(v)); }); module.exports = conf; 複製程式碼
這裡就需要給解釋了,開始學習webpack,然後網上不斷找各種帖子,學習、修改、測試最終成了這些配置檔案,有些改動時間長了我自己都忘記(-_-)!。
2.1 獲取entry、HtmlWebpackPlugin
使用工具獲取指定的HTML和js,這裡我做一個限制,只取index名稱的,這是因為公司很多模板檔案都是用html字尾。
webpack的入口是隻識別js的,這裡就需要用到HtmlWebpackPlugin,沒生成一個HTML與js的對應關係就要new一個HtmlWebpackPlugin。所以上面entryHtml是push進去的,還有就是entryHtml中做了生產環境的判斷。
2.2 分割css檔案
現在使用的是MiniCssExtractPlugin,但是從註釋開出來我最開始使用的是ExtractTextPlugin(我也是從註釋看到才想起來的,哈哈哈哈)。
先說ExtractTextPlugin,這個要在webpack4上面用正常安裝是不行的,現在必須指定版本@next,否則不能相容webpack4。如下:
yarn add ExtractTextPlugin@next 複製程式碼
配置好了之後,我用了一段時間,最後在思考上面第四個問題的時候,把這個替換掉了,ExtractTextPlugin好像不能使用contenthash。
我們公司是做bss系統的,業務複雜,而且更換業務邏輯的頻率很快,所以index.js修改比較多,但是樣式和圖片其實改動不多,不能因為改了一個if else,就需要使用者更新css和圖片吧。所以換成MiniCssExtractPlugin現在的樣子。
然後關於MiniCssExtractPlugin的配置
filename是配置每個chunk對應分割出css檔案的配置
chunkfilename是配置分離出的公共css檔案的配置

2.3 載入jquery
jquery沒有實現模組化,在loader裡面做了特殊處理;這樣之後在每個js裡面就可以使用require或者import引入jquery
但是實際上,這個只能達到引入效果,$還是全域性物件。
2.4 HTML中的圖片路徑
我在有些前輩的帖子中看到是需要在HTML標籤中加一下引用判斷、loader標識;這樣很不友好;這裡使用了一個loader:html-withimg-loader,用這個loader,就不用管了,他自己處理HTML中出現的圖片連結。
2.5 清理
清理已經存在的檔案,如果不清理每次釋出都會有殘餘檔案,雖然沒有什麼影響,但是不能忍。 CleanWebpackPlugin可以指定清理的正則配置,如:
new CleanWebpackPlugin(["release"],{ root: path.resolve(__dirname, ".."), verbose: true, dry: false }), 複製程式碼
new CleanWebpackPlugin(["release/*.js","release/**/*.*"],{ root: path.resolve(__dirname, ".."), verbose: true, dry: false }), 複製程式碼
3、webpack.devServer.conf.js
開發環境
'use strict'; const path = require("path"); const webpack = require("webpack"); const merge = require('webpack-merge'); const base = require('./webpack.base.conf'); // process.env.NODE_ENV = "development"; module.exports = merge(base, { mode: "development", devtool: "eval-source-map", output: { path: path.resolve(__dirname, "../release"),//"../release_dev"), filename: "[name].[hash].js", }, module: { rules: [ { test: /\.(png|jpg|gif)$/, // loader: 'url-loader?limit=8192&name=./public/images/[name].[hash].[ext]' loader: { loader: 'url-loader', options: { // 這裡的options選項引數可以定義多大的圖片轉換為base64 name: '[name].[hash].[ext]', // limit: 8192, // 表示小於50kb的圖片轉為base64,大於50kb的是路徑 // outputPath: '/public/images' //定義輸出的圖片資料夾 } } } ] }, plugins:[ new webpack.HotModuleReplacementPlugin() ], devServer: { port: 8080, contentBase: path.resolve(__dirname, "../release"), //本地伺服器所載入的頁面所在的目錄 historyApiFallback: true, //不跳轉 inline: true, //實時重新整理 hot: true, // 開啟熱更新, //伺服器代理配置項 proxy: { '/o2o/*':{ target: 'https://www.baidu.com', secure: true, changeOrigin: true } } } }); 複製程式碼
這個在base的基礎上做了些許調整,主要是為了使用webpack-dev-server;這個配置檔案是為它存在的。
3.1 output hash
這裡的hash有chunkhash改成hash,原因是使用HotModuleReplacementPlugin之後不能使用chunkhash和contenthash。
看到有些地方說把“hot:true”去掉就行了,但是我自己實際測試不行,只是去掉hot還是會報錯;所以我索性給改成hash了,反正是本機除錯,影響不大。
3.2 devServer
這個功能很強大,對開發人員來說是非常友好的。
安裝webpack-dev-server
yarn add webpack-dev-server 複製程式碼
這個代理proxy功能還是非常強大的,將後臺服務請求指向我們的測試環境或者本地。我們原有的fis是包裝了一層nginx,每次還要單開啟,單獨配置nginx。這裡整合這個功能,很好。本地開發減少依賴,也便於除錯。
3.4 入口(entry)目錄頁
前面在entry工具中將所有的entry寫入到一個json檔案中了。在這個地方就用到了,我們專案本質上根本不是spa,使用webpack還是比較牽強的。
當啟動了webpack-dev-server之後它會預設開啟根目錄下的index.html。其實我們專案的頁面很多,不論預設開啟哪個都不方便開發,我乾脆把這個index.html做成了一個目錄頁面。將entry.json中所有的路徑全顯示,點選之後進入各個頁面。
// const $ = require("jquery"); import $ from "jquery"; const entryJson = require("./entry.json"); console.log(1122333,entryJson); $(() => { $("html").css("font-size","16px"); for (let k in entryJson){ $("body").append("<a style='margin: 1rem;padding-left:3rem;font-size: 2rem;line-height: 2rem;display: block' href='"+entryJson[k]+"'>"+entryJson[k]+"</a></br>"); } }); 複製程式碼

4、webpack.pro.conf.js
生產環境
'use strict'; const path = require("path"); const merge = require('webpack-merge'); const base = require('./webpack.base.conf'); module.exports = merge(base, { mode: "production", optimization: { splitChunks: { cacheGroups: { commons: { name: "public/vendor", chunks: "all", minChunks: 2 } } } }, module: { rules: [ { test:/\.js$/, exclude: /node_modules/, loader: "babel-loader" }, { test: /\.(png|jpg|gif)$/, // loader: 'url-loader?limit=8192&name=./public/images/[name].[hash].[ext]' loader: { loader: 'url-loader', options: { // 這裡的options選項引數可以定義多大的圖片轉換為base64 name: '[name].[hash].[ext]', limit: 8192, // 表示小於的圖片轉為base64,大於的是路徑 outputPath: 'public/images' //定義輸出的圖片資料夾 } } } ] } }); 複製程式碼
這個生產的配置也是在前面的base基礎上調整的。
4.1 釋出目錄調整
這個小的工程是作為一個子工程存在於舊專案,所以url不是直接訪問的,需要加上“工程名”的一級路徑。url-loader的outputPath、所有chunkname都需要多加一段“activity”,具體需要自己除錯。
這個地方有個需要注意,最開始嘗試的時候,我想只要只要改output就行了;但是測試之後才發現不行。原因很簡單,這個圖片src是給瀏覽器用的,是統一資源定位符。僅僅調整output的path是不會在定位符上加“activity”的,那僅僅是改變了釋出後文件儲存的路徑。例如:
optimization: { splitChunks: { cacheGroups: { commons: { name: "activity/public/vendor", chunks: "all", minChunks: 2 } } } }, 複製程式碼
{ test: /\.(png|jpg|gif)$/, // loader: 'url-loader?limit=8192&name=./public/images/[name].[hash].[ext]' loader: { loader: 'url-loader', options: { // 這裡的options選項引數可以定義多大的圖片轉換為base64 name: '[name].[hash].[ext]', limit: 8192, // 表示小於的圖片轉為base64,大於的是路徑 outputPath: 'activity/public/images' //定義輸出的圖片資料夾 } } } 複製程式碼
4.2 圖片分割
如程式碼中展示這裡使用了url-loader,並且設定limit;當圖片超過limit限制會單獨生成檔案,否則就是base64儲存。
但是這裡我遇到一個棘手問題,當圖片單獨儲存時,options.name的hash值不能設定成contentHash或者chunkHash,並且也沒有找到合適的解決辦法,希望知道的朋友給我說一下。(雖然在一定程度上說不用hash值也行,但是我感覺這樣不好)
4.3 babel編譯
使用babel轉義ECMAScript6的語法,使之相容舊的瀏覽器。如程式碼中設定loader,然後在專案根目錄建立新檔案.babelrc,內容:
{ "presets": ["env"] } 複製程式碼
安裝babel
yarn add babel-core babel-loader babel-preset-env 複製程式碼
4.4 mode NODE_env
這裡在webpack配置檔案中設定了mode:production,並且在啟動指令碼中也設定node的環境為production。刪掉了devtool。 這裡設定的環境配合entry工具中對環境的識別,會配置壓縮設定。
package.json的scripts
如下:
{ "scripts": { "dev": "cross-env NODE_ENV=development webpack --config ./webpack.config/webpack.dev.conf.js", "pro": "cross-env NODE_ENV=production webpack --config ./webpack.config/webpack.pro.conf.js --progress", "devServer": "webpack-dev-server --config ./webpack.config/webpack.devServer.conf.js --open --mode development", "watch": "webpack --config ./webpack.config/webpack.dev.conf.js --watch" } } 複製程式碼
首先安裝cross-env,用於設定node環境;在上面的指令碼中可以看到cross-env的使用
yarn add cross-env 複製程式碼
上面設定兩個webpack的配置檔案,但是沒有實際使用,其實使用的命令就是scripts中的內容。只不過這裡可以是操作簡化,但我們使用時只需要啟動指令碼,如下: 開發環境:
yarn run devServer 複製程式碼
生產環境:
yarn run pro 複製程式碼
run也是可以省略的。 webpack-dev-server模式下不會將實際釋出的內容寫入在硬碟上,如果我們需要自行檢視內容,可以執行:
yarn run watch 複製程式碼
只不過這樣做意義不大,因為我發現,你每次修改都會產生一些列檔案,很快你就發現生成的是一堆垃圾,從中找東西費勁的很。
問題遺留
- 大圖片大單分割出來後無法使用contenthash,我如何能讓一個大圖長久快取吶
- 公共檔案過大,僅我寫的這個測試工程vender就已經一兆多,感覺不是很大,但是真實專案中就很可怕了。而且我們專案是移動端的,這樣大檔案下載的留白時間也很難受。