被Webpack弄的頭大
這幾天在準備開發一個基於 React/Redux 的應用, NodeJS 和 React 一天之內就學完了, 但是學習 Webpack 的時候卻花了兩天的時間折騰, 主要原因有兩個:
- Webpack非常強大, 配置文件和插件生態都非常豐富, 但是當可以選擇的配置和插件太多以後, 一開始不知道怎麼下手
- 怎麼用 Webpack 把兩種不同的框架粘合在一起? 比如 React 和 Express, 一個是前端框架, 一個是後端框架, 怎麼粘合在一起, 官網的示例文檔看完只能瞭解 Webpack 配置文件的基本結構, 但是怎麼粘合複雜還需要自己折騰
學習方法
好在經過兩天Google, 看了一百多個國外的技術博客, 算是把Webpack入門了, 知道怎麼用Webpack搭建 React + Express 開發環境了.
在分享搭建環境步驟之前, 我想分享一下我是怎麼快速學習的方法, 特別是針對這種多維度、高複雜度和相互交叉的綜合知識領域, 因爲很多同學除了喜歡我的技術文章外, 更加關心我快速學習的方法.
1 要對自己有足夠的信心, 即使一開始你還是一臉懵逼的狀態, 不知道如何下手, 也要對自己有絕對的自信, 因爲有時候我們沒法精通一門技術, 不是因爲我們笨, 而是一開始我們的信心不足, 被困難嚇到了而不敢嘗試. 所以在一開始看到 Webpack 一團亂麻的情況下, 我的信心來源於: "Emacs這個世界上最複雜、最難折騰的軟件我都可以玩的很溜, 還有什麼東西是我沒法搞定的? " 所以, 保持了這種強大的信心, 經過兩天的纏鬥最終還是徹底理解了 Webpack
2 自己想要解決的問題是什麼, 只有非常清晰的知道自己想要解決的問題, 這樣在搜索和學習的時候, 纔不會被很多不關心的信息所幹擾, 比如這兩天, 我一直在Google上搜索 "React Express Webpack" 相關的文章, 這過程中, 看到了很多關於 Vue.js, Gulp 等等不相關的文章, 雖然也寫的很好, 但是通通被我無情 pass 了, 我一直盯着我的目標在看材料. 所以, 清晰的目標能夠幫助你避免過度無用信息的干擾.
3 通過看大量知識縮小思考範圍, 在目標明確的前提下, 先看大量的技術博客和文章, 每個技術文章都會提供解決某一個小問題的知識, 比如, 有些文章對搭建環境的目錄結構寫的很好, 有些文章講解 webpack 配置很詳細, 有些文章講熱替換講的很好. 你需要看足夠多的文章, 通過對比, 才能知道哪篇文章是最新的, 而且是寫的最清晰的, 哪篇文章寫的東西是已經陳舊沒用的. 我這兩天看了100多篇技術文章, 最後通過對比和實驗, 發現最後只有幾篇文章中的內容纔是最新的, 並符合我要求的素材. 大量看技術文章才能學到足夠的知識去解決問題, 如果你看的文章和知識量不夠, 即使最後你東平西湊碰巧成功了, 將來遇到問題後還是不清楚解決問題的根源, 最後會花費更多時間去交學費.
4 依葫蘆畫瓢纔會融匯貫通, 一般複雜的知識都是沒有現成解決方案的, 你只能通過多種知識組合才能解決問題, 而怎麼組合的關鍵就是要先依葫蘆畫瓢把代碼和配置步驟都 手工 敲一遍, 手工敲的目的是爲了讓手產生足夠強度和手指記憶, 這樣你在手工敲打的同時會在腦袋裏反覆的思考每一個步驟後面的意義, 這種看似笨的方法, 會最大強度的增強你對所學知識的印象, 這些知識會在潛意識裏面沉澱着, 當你遇到兩種不同的知識需要融合的時候, 這些潛意識的知識就會蹦出來幫你產生非常重要的聯繫和線索, 當所有知識點最後都連成一張清晰的邏輯網以後, 問題自然就解決了.
5 把解決方案寫下來, 原來在深度的時候, 每個月都會給團隊培訓, 其實自己學的知識其實已經非常熟練和牢固了, 但當我把這些知識通過 PPT 或者技術文章的形式寫出來的時候, 我會非常注意文章的簡潔性和清晰度, 以方便別人能夠最小負擔的看懂. 最後, 我發現, 當你教別人的時候, 自己的知識體系也會變得更深更廣, 思路也會更加清晰.
配置 React + Express 開發環境
帶着目的性去學習
在折騰之前, 先把上面的這張邏輯流程想清楚, 這就是我們開發環境需要達到的最後目標, 記住這個開發流程, 你在閱讀那些配置文件細節的時候, 就會理解的更深入:
- 我們修改前端資源文件 (js css image) 時, Webpack 會實時的重新編譯打包, 打包完成後通過熱替換 (HMR) 實時替換瀏覽器中的資源文件, 在無刷新頁面的前提下, 實時的在瀏覽器看JS/CSS變動後的UI效果, 熱替換的好處就是代碼邏輯和樣式效果更換的前提下, 當前頁面的狀態是不變的, 你不用像刷新頁面那樣, 要重新點一遍前端頁面才能看到某些效果(比如菜單).
- 我們修改後端代碼、配置或後端頁面時, Nodemon 會立即重啓服務器, 並在重啓完成後, 通知瀏覽器刷新頁面, 達到自動刷新前端頁面的效果.
- 不論是前端還是後端代碼更新後, 都會觸發瀏覽器頁面更新, 我們在瀏覽器中檢查效果後, 繼續修改代碼, 以完成一個工作流週期.
其實 Webpack 本質上就是通過各種插件來完成這個工作流, 只不過隨着你經驗越來越豐富, 你會用更多的插件去粘合前後端框架和各種工具模塊, 以完成更加智能的自動化流程.
沒看懂? 沒關係, 跟着我配置一遍吧, 配置完以後, 你就懂了.
準備工作
安裝 homebrew
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
安裝 nodejs
brew install node
安裝cnpm
npm 每次去國外服務器下載東西都很慢, 浪費了很多時間, 所以我們需要先安裝一下 cnpm 來加速 npm 包的下載:
npm install -g cnpm --registry=http://r.cnpmjs.org
新建項目
mkdir project && cd project && npm init -y
這個命令會在當前文件中創建一個 package.json 文件, package.json 的作用主要是記錄開發過程中都安裝了哪些包及包的版本, 方便用 git 管理和以後生產環境部署用.
安裝依賴包
下面是 npm/cnpm 命令常用的三個安裝命令的區別:
cnpm 命令 | 參數說明 |
---|---|
cnpm install package -g | 安裝到系統中, 任何項目都可以使用 |
cnpm install package --save | 安裝到項目本地目錄中, 並保存包名信息到依賴字段 dependencies 下, 下次部署服務器就可以通過 package.json 文件自動安裝依賴包 |
cnpm install package --save-dev | 和 --save 參數類似, 只不過保存包信息到依賴字段 devDependencies 下, 表示只在開發環境纔會安裝, 生產環境不會安裝的包 |
執行下面的命令可以自動安裝所有依賴包:
cnpm install --save react react-dom react-hot-loader
cnpm install --save express body-parser cookie-parser multer
cnpm install --save-dev webpack webpack-dev-server webpack-cli webpack-dev-middleware webpack-hot-middleware
cnpm install --save-dev @babel/core @babel/preset-env @babel/preset-react babel-loader
cnpm install --save-dev css-loader style-loader
cnpm install --save-dev reload nodemon
下面是每條命令和安裝包的作用說明:
依賴包 | 作用說明 |
---|---|
react react-dom react-hot-loader | React 前端框架相關的庫和熱替換插件 |
express body-parser cookie-parser multer | 基於 NodeJS 的後端框架 Express 和它的依賴庫 |
webpack webpack-dev-server webpack-cli webpack-dev-middleware webpack-hot-middleware | Webpack 和熱替換中間件 |
@babel/core @babel/preset-env @babel/preset-react babel-loader | Babel 插件, 主要用於轉換 React 的 ES6 語法到瀏覽器可以識別的正常 JS 語法 |
css-loader style-loader | React JSX 文件可以直接調用 CSS 組件 |
reload nodemon | 頁面主動刷新插件和服務器重啓工具 |
目錄結構
在折騰配置文件之前, 我們看看一下最終的目錄結構:
project
├── app.js
├── client
│ ├── index.css
│ └── index.js
├── server
│ └── index.html
├── dist
│ └── bundle.js
├── .babelrc
├── nodemon.json
├── package.json
└── webpack.config.js
文件 | 文件說明 |
---|---|
app.js | 啓動入口, 後端 express 代碼, 控制路由和HTTP請求響應 |
client | React 前端組件代碼, 包括 JS 和 CSS 文件 |
server | 後端視圖文件, 主要是 html 文件 |
dist | Webpack 編譯打包文件存放的目錄 |
.babelrc | Babel 的配置文件 |
nodemon.json | nodemon 的配置文件 |
package.json | 項目配置文件, 主要存放包信息和服務端啓動命令 |
webpack.config.js | Webpack 配置文件 |
折騰配置文件
我們按照上面的目錄結構去填充每個配置文件, 我會在文件下面講解每行代碼的意思:
app.js
var express = require('express'),
app = express(),
reload = require('reload'),
rootPath = __dirname;
var webpack = require("webpack"),
webpackConfig = require("./webpack.config"),
webpackDevMiddleware = require("webpack-dev-middleware"),
webpackHotMiddleware = require("webpack-hot-middleware"),
compiler = webpack(webpackConfig);
app.use(
webpackDevMiddleware(compiler, {
publicPath: webpackConfig.output.publicPath,
noInfo: true,
stats: {
colors: true
}
})
);
app.use(webpackHotMiddleware(compiler));
app.use(express.static(__dirname + '/dist'));
app.get('/', function (req, res) {
res.sendFile(rootPath + '/server/index.html');
});
reload(app);
app.listen(3000, () => {
console.log('* Server starting...');
});
- 頂部是依賴 import 相關的代碼
- 中間 app.use 的代碼的意思是加入 Webpack 熱替換插件, 一旦 Webpack 根據 webpack.config.js 的 entry 配置發現你修改了相關的 JS/CSS 文件, webpack 會自動重新編譯、打包和熱替換瀏覽器的 dist/bunlde.js 代碼, 而無需重啓服務器和手動刷新, 注意這段代碼需要放在路由代碼 app.get 之前才能生效
- app.get 的代碼就是 express 路由代碼, 訪問 http://localhost:3000 的時候, 把 ./server/index.html 的文件返回給瀏覽器去渲染
- reload(app) 代碼的作用是, 一旦你修改後端代碼或者 HTML 文件, 會引發 nodejs/express 服務器重啓, 重啓後, reload 插件會自動通過WebSocket去重新加載瀏覽器頁面, 不需要手動刷新
- app.listen 代碼的作用就是監聽本地 3000 端口, 打開 http://localhost:3000 即可本地開發了
client/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import "./index.css";
const title = 'React + Express rocks!';
ReactDOM.render(
<div className="title">{title}</div>,
document.getElementById('app')
);
if (module.hot) {
module.hot.accept();
}
- 頂部是 import 代碼, 導入 React 庫和 CSS 組件
- 中間 ReactDOM.render 就是 React 組件的代碼, 替換 server/index.html 中 id 爲 app 的 DIV 區域, React 組件的樣式主要通過
import "./index.css"
來引入, 這樣每個組件的樣式都單獨放在組件相關的 css 文件中, 不會因爲組件越來越多, CSS的作用域相互影響 -
module.hot.accpet()
代碼的意思就是當JS修改後, 如果當前模塊是熱的即進行熱替換, 和 app.js 中的app.use(webpackHotMiddleware(compiler))
代碼對應, 只有這兩個代碼都存在, JS/CSS文件修改後纔會進行熱替換操作
client/index.js 這個文件主要是開發用的文件, 修改後會自動被 webpack 添加JS依賴後彙總編譯成 dist/bundle.js 文件, 所以 server/index.html 的JS腳本不是 index.js , 而應該是 bundle.js
client/index.css
.title {
color: red;
margin: 20px;
}
最簡單的CSS文件, 主要通過 CSS Modules 來實現 React 樣式的組件化組織, 需要安裝 style-loader 和 css-loader 這兩個插件, 才能通過 webpack 保證 JS/JSX 文件中可以直接執行 import "./index.css"
來使用 CSS 文件的樣式內容.
server/index.html
<!DOCTYPE html>
<html>
<head>
<title>Cool</title>
</head>
<body>
<h1>Hello World</h1>
<div id="app"></div>
<script src="./bundle.js"></script>
<script src="/reload/reload.js"></script>
</body>
</html>
非常簡單的 HTML 文件
-
<div id="app"><div>
一個DIV佔位符, 用於 React 的JS組件進行DOM替換操作 -
<script src="./bundle.js"></script>
注意, 這裏加載的是 Webpack 編譯後的 bundle.js 文件, 而不是 client/index.js 文件 -
<script src="/reload/reload.js"></script>
這裏的 reload.js 就對應 app.js 中的reload(app)
代碼, reload 插件在服務器重啓後, 會自動重新加載所有包括<script src="/reload/reload.js"></script>
的頁面
dist/bundle.js
Webpack 爲了加快編譯速度, bundle.js 文件其實都存在於內存中, 所以 dist 目錄下什麼都沒有, 只是方便 HTML/JS 文件之間能夠通過 dist/bundle.js 進行關聯, 我們這裏需要 dist 這個目錄佔坑, 以使得 Webpack 可以粘連 HTML 和 JS 文件.
.babelrc
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}
Babel 相關的設置, 可以讓你使用最新的 JavaScript 語法, Babel 會自動把最新的語法轉換成所有瀏覽器可以執行的陳舊語法, 讓你開發的時候用JavaScript最新語法, 同時又保證編譯後的JS代碼對瀏覽器的兼容性.
注意在 .babelrc 配置後, 就不需要在 package.json 文件中配置 babel 選項了.
nodemon.json
{
"ignore": ["dist/", "client/*.js"],
"ext": "json js html"
}
- ignore 規則的意思是, 忽略 dist 目錄和 client 目錄下的 JS 文件變動, 避免這些JS文件變動引起服務器重啓, JS文件的變動由 Webpack 的熱替換機制來控制, 來獲得更方便的 JS 自動刷新體驗
- ext 規則的意思是, 監聽項目目錄下所有 js、json、html結尾的文件, 這些文件變動後立即重啓服務器, ext 這個規則需要排除 ignore 規則的例外情況
package.json
{
"name": "project",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon ./app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"body-parser": "^1.18.3",
"cookie-parser": "^1.4.3",
"express": "^4.16.4",
"multer": "^1.4.1",
"react": "^16.6.3",
"react-dom": "^16.6.3",
"react-hot-loader": "^4.3.12"
},
"devDependencies": {
"@babel/core": "^7.2.0",
"@babel/preset-env": "^7.2.0",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.4",
"css-loader": "^2.0.0",
"nodemon": "^1.18.7",
"reload": "^2.4.0",
"style-loader": "^0.23.1",
"webpack": "^4.27.1",
"webpack-cli": "^3.1.2",
"webpack-dev-middleware": "^3.4.0",
"webpack-dev-server": "^3.1.10",
"webpack-hot-middleware": "^2.24.3"
}
}
大部分代碼都是被 npm/cnpm 命令自動生成的, 我們只用看 scripts 這一段:
-
"start": "nodemon ./app.js"
這句代碼的意思是, 由 nodemon 來啓動 app.js , nodemon 會監聽項目的所有文件, 在文件變動後重啓服務器, 重啓服務器的規則受 nodemon.json 配置文件中的規則影響
webpack.config.js
如果你看到這裏了, 你會發現上面的這些配置一點都不復雜, 其實最複雜的就是 webpack.config.js , 但是如果你理解了我上面說的工作流程, 你會發現理解 webpack.config.js 的內容也是非常自然的:
const webpack = require('webpack');
var hotMiddlewareScript = "webpack-hot-middleware/client?reload=true";
module.exports = {
entry: {
client: ["./client", hotMiddlewareScript]
},
mode: "development",
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: ['babel-loader']
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
]
},
resolve: {
extensions: ['*', '.js', '.jsx']
},
output: {
path: __dirname + '/dist',
publicPath: "/",
filename: 'bundle.js'
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin(),
],
devServer: {
hot: true
}
};
-
const webpack = require('webpack');
導入 webpack 這個庫 -
var hotMiddlewareScript = "webpack-hot-middleware/client?reload=true"
這個代碼非常重要, app.js 和 client/index.js 都加入了對 webpack-hot-middleware 這個中間件的支持, 以實現 JS 文件改動後的熱替換支持, 這句的意思是, JS熱替換失敗以後重新加載頁面, 如果這句不寫, 很多情況下, 你改 JS 文件的內容會導致熱替換機制不一定成功, 最終導致即使修改了JS文件, 瀏覽器也不會刷新頁面內容 -
client: ["./client", hotMiddlewareScript]
用上面的熱替換規則, 監聽 client 下所有文件, 如果 client 下的文件變動後, webpack會自動重新編譯、打包和熱替換, 記住 JS/CSS 文件的變動和替換是由 Webpack 來監聽和執行的, nodemon 主要用於監聽後端代碼、配置文件和HTML頁面文件 - rules 相關代碼主要控制 js/jsx 文件的需要通過 babel-loader 來轉換 React JSX文件的 JavaScript 語法, css 文件需要通過 style-loader 和 css-loader 來使得 React JS/JSX 文件可以直接 import CSS 組件的代碼
- resolve 相關的代碼主要控制 JS import 模塊的時候, 指導從哪裏找這些 JS 模塊, 更專業的講解可以查看 webpack resolve
- output 相關的代碼控制 JS/CSS 編譯後的文件名和存放的目錄, 這裏就是 dist/bundle.js
- plugins 相關的代碼, 表示加載 HotModuleReplacementPlugin 這個插件, 配合 app.js、client/index.js 進行 JS/CSS 熱替換操作, NoEmitOnErrorsPlugin 插件的目的是, 當文件有語法錯誤時不要刷新頁面, 只是在終端裏打印錯誤
- devServer 最主要的配置就是
hot: true
, 還是關於熱替換操作的, 你看, 爲了讓 JS/CSS 熱替換成功, 首先需要 app.js 使用app.use
代碼使用熱替換中間件, 其次 client/index.js 中要通過module.hot.accept()
來手動控制JS模塊是否需要熱替換, 最後還需要在 webpack.config.js 中通過 hotMiddlewareScript、 plugins 和 devServer 三個字段來控制, 最終需要這5個地方共同設置來達成熱替換的操作, 其實 Webpack 的難點就在這, 配置太分散, 一處沒有設置對就沒法操作.
啓動服務
好了, 到目前爲止, 所有配置文件都折騰完了, 執行下面命令就可以啓動Web服務了:
npm start
然後打開 http://localhost:3000 就可以看到效果, 嘗試修改一下 JS、CSS、HTML甚至服務器代碼, 看看是不是全棧都自動刷新了呢?
Happy hacking!
相關技術博客參考鏈接
折騰過程中遇到的技術博客也非常有參考價值, 下面是我折騰過程中, 主要參考的技術博客和我覺得每個技術博客主要的參考價值
技術博客 | 參考價值 |
---|---|
React + Webpack 4 + Babel 7 Setup Tutorial | 主要介紹在 Webpack4 下怎麼成功安裝 Babel , 網上大部分的文章都是基於 Webpack3 安裝的, 各種 Babel 相關的報錯, 而且從項目整體配置上講的也很清晰易懂 |
Express結合Webpack的全棧自動刷新 | 怎麼做到 JS/CSS 熱替換講的非常好, 比如 reload=true 的技巧, 其他的技術博客各種折騰都不成功 |
CSS Modules 用法教程 | 怎麼在 React 使用 CSS 組件的方案 |
最後
前端這個技術圈是我看到的最爲混亂的技術圈, 各種框架和各種插件層出不窮, 寫個Hello World之前就要折騰一大堆配置環境.
當然, 我也從前端技術圈中看到了像Emacs這樣的折騰氛圍, 技術圈和人一樣, 折騰纔有活力, 折騰才能學到更多知識, 因爲當你折騰一遍以後, 不管折騰的多麼複雜, 一旦你記住, 你就不會那麼難了. 哈哈哈哈.