這不是一個純粹的學習帖子,最開始為了生產專案考慮的。公司有個新的、小的活動專案。以此為假想,所以我希望學習一些新的技術應用在上面;這個新的專案是作為舊專案的一個子系統存在的,所以又必須在一定程度上保持一致。
而這個舊專案的原有使用構建工具fis的版本比較老舊,不敢升級,怕出什麼么蛾子,所以又不能動他。
在網上學習了眾多攻略之後自己嘗試搭建了一下,解決了一些問題,也留下了一下疑惑。
專案資源路徑
github: 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("---------------------------------------------
entries:");
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”,具體需要自己除錯。
例如:
xxxx.com/index.html -> xxxx.com/activity/in…
xxxx.com/public/1.cs… -> xxxx.com/activity/pu…
這個地方有個需要注意,最開始嘗試的時候,我想只要只要改output就行了;但是測試之後才發現不行。原因很簡單,這個圖片src是給瀏覽器用的,是統一資源定位符。僅僅調整output的path是不會在定位符上加“activity”的,那僅僅是改變了釋出後檔案儲存的路徑。x現在需要在釋出的時候加深一個目錄級別,例如:
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就已經一兆多,感覺不是很大,但是真實專案中就很可怕了。而且我們專案是移動端的,這樣大檔案下載的留白時間也很難受。