為什么要搭建本地服務器患朱?
目前我們開發(fā)的代碼鲁僚,為了運行需要有兩個操作:
- 操作一:npm run build,編譯相關的代碼裁厅;
- 操作二:通過live server或者直接通過瀏覽器冰沙,打開index.html代碼,查看效果执虹;
這個過程經(jīng)常操作會影響我們的開發(fā)效率拓挥,我們希望可以做到,當文件發(fā)生變化時袋励,可以自動的完成 編譯 和 展示侥啤。
為了完成自動編譯,webpack提供了幾種可選的方式:
- webpack watch mode茬故;
- webpack-dev-server盖灸;
- webpack-dev-middleware
接下來,我們一個個來學習一下它們磺芭。
1. Webpack watch
webpack給我們提供了watch模式:
- 在該模式下赁炎,webpack依賴圖中的所有文件,只要有一個發(fā)生了更新钾腺,那么代碼將被重新編譯徙垫;
- 我們不需要手動去運行 npm run build指令了;
如何開啟watch呢放棒?兩種方式:
- 方式一:在
webpack.config.js
姻报,添加 watch: true; - 方式二:在
package.json
啟動webpack的命令中哨查,添加 --watch的標識逗抑;
這里我們選擇方式二,在package.json的 scripts 中添加一個 watch 的腳本:
{
"scripts": {
"build": "webpack --watch"
}
}
2. webpack-dev-server
上面的方式可以監(jiān)聽到文件的變化寒亥,但是事實上它本身是沒有自動刷新瀏覽器的功能的邮府, 當然,目前我們可以在VSCode中使用live-server來完成這樣的功能溉奕,但是褂傀,我們希望在不適用live-server的情況下,可以具備live reloading(實時重新加載)的功能加勤。
webpack-dev-server 在編譯之后不會寫入到任何輸出文件仙辟。而是將 bundle 文件保留在內存中,
事實上webpack-dev-server使用了一個庫叫memfs鳄梅。
安裝
yarn add webpack-dev-server -S
使用
package.json
{
"scripts": {
"serve": "webpack serve"
}
}
webpack serve
是webpack5的寫法
3. webpack-dev-middleware
webpack-dev-middleware 是一個封裝器(wrapper)叠国,它可以把 webpack 處理過的文件發(fā)送到一個 server。webpack-dev-server 在內部使用了它戴尸,然而它也可以作為一個單獨的 package 來使用粟焊,以便根據(jù)需求進行更多自定義設置。
我們可以搭配一個服務器來使用它孙蒙,比如express项棠。
安裝
yarn add express webpack-dev-middleware
新建server.js
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const app = express();
const config = require("./webpack.config");
// 傳入配置信息, webpack根據(jù)配置信息進行編譯
const compiler = webpack(config);
const middleware = webpackDevMiddleware(compiler);
app.use(middleware);
app.listen(3000, () => {
console.log("服務已經(jīng)開啟在3000端口上~");
});
運行命令node server.js
,然后就能在http://loacalhost:3000看到效果啦挎峦。
4. 模塊熱替換(HMR)
什么是HMR呢香追?
- HMR的全稱是Hot Module Replacement,翻譯為模塊熱替換坦胶;
- 模塊熱替換是指在 應用程序運行過程中透典,替換、添加顿苇、刪除模塊掷匠,而無需重新刷新整個頁面;
HMR通過如下幾種方式岖圈,來提高開發(fā)的速度: - 不重新加載整個頁面讹语,這樣可以保留某些應用程序的狀態(tài)不丟失;
- 只更新需要變化的內容蜂科,節(jié)省開發(fā)的時間顽决;
- 修改了css、js源代碼导匣,會立即在瀏覽器更新才菠,相當于直接在瀏覽器的devtools中直接修改樣式;
如何使用HMR呢贡定?
- 默認情況下赋访,webpack-dev-server已經(jīng)支持HMR,我們只需要開啟即可;
- 在不開啟HMR的情況下蚓耽,當我們修改了源代碼之后渠牲,整個頁面會自動刷新,使用的是live reloading步悠;
4.1 開啟HMR
webpack.config.js
module.exports = {
devServer: {
hot: true
},
}
目前devserver和brwserlist工具有沖突签杈,在webpack.config.js
加入target: "web"
module.exports = {
target: 'web'
}
然后執(zhí)行yarn serve
就能看到熱更新提示
但是你會發(fā)現(xiàn),當我們修改了某一個模塊的代碼時鼎兽,依然是刷新的整個頁面答姥,這是因為我們需要去指定哪些模塊發(fā)生更新時,進行HMR谚咬。
入口文件:
if (module.hot) {
module.hot.accept("./math.js", () => {
console.log("math模塊發(fā)生了更新~");
});
}
4.2 框架的HMR
有一個問題:在開發(fā)其他項目時鹦付,我們是否需要經(jīng)常手動去寫入 module.hot.accpet相關的API呢?
- 比如開發(fā)Vue择卦、React項目敲长,我們修改了組件,希望進行熱更新互捌,這個時候應該如何去操作呢潘明?
- 事實上社區(qū)已經(jīng)針對這些有很成熟的解決方案了;
- 比如vue開發(fā)中秕噪,我們使用vue-loader钳降,此loader支持vue組件的HMR,提供開箱即用的體驗腌巾;
- 比如react開發(fā)中遂填,有React Hot Loader,實時調整react組件(目前React官方已經(jīng)棄用了澈蝙,改成使用reactrefresh)吓坚;
接下來我們分別對React、Vue實現(xiàn)一下HMR功能灯荧。
4.2.1 React的HMR
在之前礁击,React是借助于React Hot Loader來實現(xiàn)的HMR,目前已經(jīng)改成使用react-refresh來實現(xiàn)了逗载。
安裝實現(xiàn)HMR相關的依賴
yarn add react-refresh-webpack-plugin react-refresh -D
webpack.config.js
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
plugins: [
new ReactRefreshWebpackPlugin(),
]
};
babel.config.js
module.exports = {
presets: [
["@babel/preset-env"],
["@babel/preset-react"],
],
plugins: [
["react-refresh/babel"]
]
}
4.2.2 Vue的HMR
Vue的加載我們需要使用vue-loader哆窿,而vue-loader加載的組件默認會幫助我們進行HMR的處理。
第六節(jié)已經(jīng)介紹過vue-loader厉斟。
4.3 HMR的原理
那么HMR的原理是什么呢挚躯?如何可以做到只更新一個模塊中的內容呢?
- webpack-dev-server會創(chuàng)建兩個服務:提供靜態(tài)資源的服務(express)和Socket服務(net.Socket)擦秽;
- express server負責直接提供靜態(tài)資源的服務(打包后的資源直接被瀏覽器請求和解析)码荔;
HMR Socket Server漩勤,是一個socket的長連接:
- 長連接有一個最好的好處是建立連接后雙方可以通信(服務器可以直接發(fā)送文件到客戶端);
- 當服務器監(jiān)聽到對應的模塊發(fā)生變化時缩搅,會生成兩個文件.json(manifest文件)和.js文件(update chunk)越败;
- 通過長連接,可以直接將這兩個文件主動發(fā)送給客戶端(瀏覽器)誉己;
- 瀏覽器拿到兩個新的文件后眉尸,通過HMR runtime機制域蜗,加載這兩個文件巨双,并且針對修改的模塊進行更新;
HMR的原理圖:
5. publicPath
5.1 output的publicPath
output中的path的作用是告知webpack之后的輸出目錄:
- 比如靜態(tài)資源的js霉祸、css等輸出到哪里筑累,常見的會設置為dist、build文件夾等丝蹭;
output中還有一個publicPath屬性慢宗,該屬性是指定index.html文件打包引用的一個基本路徑:
- 它的默認值是一個空字符串,所以我們打包后引入js文件時奔穿,路徑是 bundle.js镜沽;
- 在開發(fā)中,我們也將其設置為 / 贱田,路徑是 /bundle.js缅茉,那么瀏覽器會根據(jù)所在的域名+路徑去請求對應的資源;
- 如果我們希望在本地直接打開html文件來運行(file://xxx)男摧,會將其設置為 ./蔬墩,路徑時 ./bundle.js,可以根據(jù)相對路徑去查找資源耗拓。
webpacl.config.js
module.exports = {
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './build'), // 出口文件必須要用相對路徑
publicPath: './'
}
}
5.2 devServer的publicPath
devServer中也有一個publicPath的屬性拇颅,該屬性是指定本地服務所在的文件夾:
- 它的默認值是 /冈涧,也就是我們直接訪問端口即可訪問其中的資源 http://localhost:8080惹盼;
- 如果我們將其設置為了 /abc,那么我們需要通過 http://localhost:8080/abc才能訪問到對應的打包后的資源捧颅;
- 并且這個時候竿刁,我們其中的bundle.js通過 http://localhost:8080/bundle.js也是無法訪問的:
- 所以必須將output.publicPath也設置為 /abc黄锤;
- 官方其實有提到,建議 devServer.publicPath 與 output.publicPath相同们妥;
6. devServer的其他屬性
6.1 contentBase
devServer中contentBase對于我們直接訪問打包后的資源其實并沒有太大的作用猜扮,它的主要作用是如果我們打包后的資源,又依賴于其他的一些資源监婶,那么就需要指定從哪里來查找這個內容:
- 比如在index.html中旅赢,我們需要依賴一個 abc.js 文件齿桃,這個文件我們存放在 public文件 中;
- 在index.html中煮盼,我們應該如何去引入這個文件呢短纵?
- 比如代碼是這樣的:<script src="./public/abc.js"></script>;
- 但是這樣打包后瀏覽器是無法通過相對路徑去找到這個文件夾的僵控;
- 所以代碼是這樣的:<script src="/abc.js"></script>;
- 但是我們如何讓它去查找到這個文件的存在呢香到? 設置contentBase即可;
當然在devServer中還有一個可以監(jiān)聽contentBase發(fā)生變化后重新編譯的一個屬性:watchContentBase报破。
目錄結構如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<div id="root"></div>
<script src="./abc.js"></script>
</body>
</html>
webpack.config.js
module.exports = {
devServer: {
contentBase: path.resolve(__dirname, "./why"),
watchContentBase: true
}
6.2 hotOnly
hotOnly是當代碼編譯失敗時悠就,是否刷新整個頁面:
- 默認情況下當代碼編譯失敗修復后,我們會重新刷新整個頁面充易;
- 如果不希望重新刷新整個頁面梗脾,可以設置hotOnly為true;
6.3 host
host設置主機地址:
- 默認值是localhost盹靴;
- 如果希望其他地方也可以訪問炸茧,可以設置為 0.0.0.0;
localhost 和 0.0.0.0 的區(qū)別:
- localhost:本質上是一個域名稿静,通常情況下會被解析成127.0.0.1;
- 127.0.0.1:回環(huán)地址(Loop Back Address)梭冠,表達的意思其實是我們主機自己發(fā)出去的包,直接被自己接收;
- 正常的數(shù)據(jù)庫包經(jīng)過 應用層 - 傳輸層 - 網(wǎng)絡層 - 數(shù)據(jù)鏈路層 - 物理層 ;
- 而回環(huán)地址改备,是在網(wǎng)絡層直接就被獲取到了控漠,是不會經(jīng)常數(shù)據(jù)鏈路層和物理層的;
- 比如我們監(jiān)聽 127.0.0.1時,在同一個網(wǎng)段下的主機中绍妨,通過ip地址是不能訪問的;
- 0.0.0.0:監(jiān)聽IPV4上所有的地址润脸,再根據(jù)端口找到不同的應用程序;
- 比如我們監(jiān)聽 0.0.0.0時,在同一個網(wǎng)段下的主機中他去,通過ip地址是可以訪問的;
6.4 port
port設置監(jiān)聽的端口毙驯,默認情況下是8080
6.5 open
open是否打開瀏覽器:
- 默認值是false,設置為true會打開瀏覽器灾测;
- 也可以設置為類似于 Google Chrome等值爆价;
6.6 compress
compress是否為靜態(tài)文件開啟gzip compression,默認值是false媳搪,可以設置為true铭段。
6.7 Proxy代理
proxy是我們開發(fā)中非常常用的一個配置選項,它的目的設置代理來解決跨域訪問的問題:
- 比如我們的一個api請求是 http://localhost:8888秦爆,但是本地啟動服務器的域名是 http://localhost:8000序愚,這
個時候發(fā)送網(wǎng)絡請求就會出現(xiàn)跨域的問題; - 那么我們可以將請求先發(fā)送到一個代理服務器等限,代理服務器和API服務器沒有跨域的問題爸吮,就可以解決我們的
跨域問題了芬膝;
我們可以進行如下的設置:
- target:表示的是代理到的目標地址,比如 /api-hy/moment會被代理到 http://localhost:8888/api-hy/moment形娇;
- pathRewrite:默認情況下锰霜,我們的 /api-hy 也會被寫入到URL中,如果希望刪除桐早,可以使用pathRewrite癣缅;
- secure:默認情況下不接收轉發(fā)到https的服務器上,如果希望支持哄酝,可以設置為false友存;
- changeOrigin:它表示是否更新代理后請求的headers中host地址;
changeOrigin的解析
changeOrigin其實是要修改代理請求中的headers中的host屬性:
- 因為我們真實的請求炫七,其實是需要通過 http://localhost:8888來請求的爬立;
- 但是因為使用了代碼钾唬,默認情況下它的值時 http://localhost:8000万哪;
- 如果我們需要修改,那么可以將changeOrigin設置為true即可抡秆;
webpack.config.js
devServer: {
proxy: {
"/why": {
target: "http://localhost:8888",
pathRewrite: {
"^/why": ""
},
secure: false,
changeOrigin: true
}
}
}
6.8 historyApiFallback
historyApiFallback是開發(fā)中一個非常常見的屬性奕巍,它主要的作用是解決SPA頁面在路由跳轉之后,進行頁面刷新
時儒士,返回404的錯誤的止。
- boolean值:默認是false,如果設置為true着撩,那么在刷新時诅福,返回404錯誤時,會自動返回 index.html 的內容拖叙;
- object類型的值氓润,可以配置rewrites屬性,可以配置from來匹配路徑薯鳍,決定要跳轉到哪一個頁面咖气。
7. resolve模塊解析
resolve用于設置模塊如何被解析:
- 在開發(fā)中我們會有各種各樣的模塊依賴,這些模塊可能來自于自己編寫的代碼挖滤,也可能來自第三方庫崩溪;
- resolve可以幫助webpack從每個 require/import 語句中,找到需要引入到合適的模塊代碼斩松;
- webpack 使用 enhanced-resolve 來解析文件路徑伶唯;
webpack能解析三種文件路徑:
- 絕對路徑,由于已經(jīng)獲得文件的絕對路徑惧盹,因此不需要再做進一步解析乳幸。
- 相對路徑
- 在這種情況下奋救,使用 import 或 require 的資源文件所處的目錄,被認為是上下文目錄反惕;
- 在 import/require 中給定的相對路徑尝艘,會拼接此上下文路徑,來生成模塊的絕對路徑姿染;
- 模塊路徑
- 在 resolve.modules中指定的所有目錄檢索模塊背亥;默認值是 ['node_modules'],所以默認會從node_modules中查找文件悬赏;
- 我們可以通過設置別名的方式來替換初識模塊路徑狡汉,具體后面講解alias的配置;
7.1 文件還是文件夾
如果是一個文件:
- 如果文件具有擴展名闽颇,則直接打包文件盾戴;
- 否則,將使用 resolve.extensions選項作為文件擴展名解析兵多;
如果是一個文件夾:
- 會在文件夾中根據(jù) resolve.mainFiles配置選項中指定的文件順序查找尖啡;
- resolve.mainFiles的默認值是 ['index'];
- 再根據(jù) resolve.extensions來解析擴展名剩膘;
7.2 extensions和alias配置
extensions是解析到文件時自動添加擴展名:
- 默認值是 ['.wasm', '.mjs', '.js', '.json']衅斩;
- 所以如果我們代碼中想要添加加載 .vue 或者 jsx 或者 ts 等文件時,我們必須自己寫上擴展名怠褐;
另一個非常好用的功能是配置別名alias:
- 特別是當我們項目的目錄結構比較深的時候畏梆,或者一個文件的路徑可能需要 ../../../這種路徑片段;
- 我們可以給某些常見的路徑起一個別名奈懒;
webpack.config.js
resolve: {
extensions: ['.wasm', '.mjs', '.js', '.json', '.jsx', '.ts', '.vue'],
alias: {
"@": path.resolve(__dirname, "./src"),
"pages": path.resolve(__dirname, "./src/pages")
}
}