前面八天,我們一直在追這行xd
npm run dev
從 npm、package.json、node_modules/.bin,一路追到 Vite。
到 Day 8,我們已經走到:
npm run dev
↓
Vite 啟動開發伺服器
↓
localhost:5173
↓
瀏覽器看到網頁
我們平常改的是:
src/App.jsx
存檔之後,瀏覽器就跟著變。
問題是:
瀏覽器到底怎麼知道要去找 App.jsx?
今天就沿著真的檔案走一次。
vite-why-labDay 1 建立的還是同一個專案:
cd vite-why-lab
npm run dev
如果前面的 vite-why-lab 還在,就直接打開它。
沒有的話,手邊任何一個 Vite + React 專案也可以,我們今天只是想看看:這些檔案到底是怎麼串起來的。
專案裡大概會看到:
vite-why-lab/
│
├─ index.html
├─ package.json
├─ vite.config.js
├─ node_modules/
│
└─ src/
├─ main.jsx
├─ App.jsx
├─ App.css
├─ index.css
└─ assets/
src 是 source 的縮寫,可以先把它理解成:
專案主要原始碼放的地方。
今天先追三個檔案:
index.html
src/main.jsx
src/App.jsx
App.jsx打開:
src/App.jsx
會看到比想像中多很多的東西。
以我現在這個 Vite React 專案為例,開頭大概是:
import { useState } from 'react'
import heroImg from './assets/hero.png'
import reactLogo from './assets/react.svg'
import viteLogo from './assets/vite.svg'
import './App.css'
function App() {
const [count, setCount] = useState(0)
return (
<>
{/* 中間還有一大串預設畫面 */}
</>
)
}
export default App
下面還有圖片、按鈕、計數器、Documentation 連結等等。
但今天先抓住最後這句:
export default App
先把它放在心裡,等等會看到它的另一半。
順便留一個小問號:
import heroImg from './assets/hero.png'
import './App.css'
咦,怎麼連圖片和 CSS 都可以 import?
先知道一件事就好:Vite 不只是幫我們開 localhost,它也在處理這些模組。
index.html回到專案根目錄,我現在這個 vite-why-lab/index.html 實際長這樣:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vite-why-lab</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
以前看到這一整份,大概只知道「這是 HTML」。
現在可以把它拆成兩大塊:
<head>
→ 告訴瀏覽器這個網頁的一些基本設定
<body>
→ 真正放頁面內容,以及 React 的入口
先看 head。
<meta charset="UTF-8" />
告訴瀏覽器文字編碼使用 UTF-8,中文、英文、符號才有一致的解讀方式。
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
是瀏覽器分頁旁邊的小圖示。
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
主要是在告訴手機瀏覽器:頁面寬度照裝置螢幕寬度來算。
<title>vite-why-lab</title>
就是瀏覽器分頁上看到的標題。
真正跟 React 接起來的,是在 body 裡面的這兩行:
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
<div id="root"></div> 一開始空空的瀏覽器解析 HTML 時,會把 HTML 轉成一棵可以被 JavaScript 操作的 DOM。
莫慌,請繼續往下看:
例如我們的 body:
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
可以先想成瀏覽器建立出:
document
└─ html
├─ head
└─ body
└─ div#root
所以這行:
<div id="root"></div>
本來就存在於 index.html 裡,一開始裡面什麼都沒有。
可以想像成:
HTML 先留一塊空地給 React。
等等的 main.jsx (你可以現在看一下這個專案的結構,旁邊有一個main.jsx) 裡會出現:
document.getElementById('root')
它就是在這棵 DOM 裡問:
幫我找到 id 叫做
root的元素。
答案就是剛才這個:
<div id="root"></div>
main.jsxroot 後面緊接著:
<script type="module" src="/src/main.jsx"></script>
這行可以先拆成三塊。
第一個是:
<script>
意思是這裡要載入、執行 JavaScript。
第二個是:
src="/src/main.jsx"
意思是:
JavaScript 的入口在
/src/main.jsx。
所以 index.html 完全沒有直接寫:
App.jsx
第一段路線其實是:
index.html
↓
main.jsx
不是:
index.html
↓
App.jsx
/src/main.jsx 前面的 / 是什麼?這個 / 不是 Mac 或 Windows 硬碟的根目錄。
在這裡可以先理解成:
從目前網站的根路徑開始找。
我們現在開的是:
http://localhost:5173
所以:
/src/main.jsx
可以想成瀏覽器去請求:
http://localhost:5173/src/main.jsx
而現在站在中間接這個請求的人,就是 Vite 的開發伺服器。
所以不是瀏覽器直接跑進你的 Mac 硬碟找:
/Users/.../src/main.jsx
而是:
瀏覽器
↓
GET /src/main.jsx
↓
Vite dev server
↓
找到專案裡的 src/main.jsx
↓
處理後回給瀏覽器
type="module" 到底是在說什麼?完整這行是:
<script type="module" src="/src/main.jsx"></script>
type="module" 是在告訴瀏覽器:
這支 JavaScript 是一個 ES Module,請用模組的方式載入它。
這件事很快就會有意義。
因為 main.jsx 裡馬上會看到:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
這些 import,以及剛才 App.jsx 裡的:
export default App
就是 JavaScript 模組系統的一部分。
所以這行
<script type="module" src="/src/main.jsx"></script>
現在可以白話讀成:
載入 /src/main.jsx,並把它當成 JavaScript module 來執行。
另外,module script 預設有類似 defer 的行為:可以先下載,但會等 HTML 文件解析完成後再執行。
而我們這份 index.html 本身也是先寫:
<div id="root"></div>
才寫:
<script type="module" src="/src/main.jsx"></script>
所以等 main.jsx 真正執行到:
document.getElementById('root')
瀏覽器通常早就已經把:
<div id="root"></div>
建立成 DOM 元素了。
main.jsx?不能直接 App.jsx 嗎?不知道有沒有人想過這個問題XD
既然最後真正想看到的是 App.jsx 的畫面,那 index.html 為什麼不乾脆寫:
<script type="module" src="/src/App.jsx"></script>
還要多一個 main.jsx?
答案其實不是「React 規定一定要有 main.jsx」。
main.jsx 也不是瀏覽器認得的特殊檔名。
真正讓它成為入口的是:
<script type="module" src="/src/main.jsx"></script>
是 index.html 指定:
JavaScript 從
main.jsx開始。
假設今天真的把 main.jsx 改名成:
banana.jsx
然後 index.html 也跟著改:
<script type="module" src="/src/banana.jsx"></script>
那 banana.jsx 一樣可以成為入口。
所以重點不是 main 這個名字。
重點是入口檔案負責把 React 啟動起來。
現在的 main.jsx 做的是:
找到 index.html 裡的 root
↓
建立 React root
↓
把 App 拿進來
↓
把 <App /> render 進去
而 App.jsx 比較專心做另一件事:
描述 App 本身的畫面和元件。
所以可以先把兩個角色分成:
main.jsx
= React 怎麼啟動、要掛在哪裡
App.jsx
= App 本身長什麼樣子
如果真的讓 index.html 直接指向 App.jsx,也不是完全不行。
只是那時候 App.jsx 就得連原本 main.jsx 的工作一起做,例如:
import { createRoot } from 'react-dom/client'
function App() {
return <h1>Hello</h1>
}
createRoot(document.getElementById('root')).render(
<App />
)
這樣也能跑,但「怎麼啟動 React」和「App 畫什麼」就混在同一個檔案裡了。
Vite 的 React 範本幫我們拆成兩層:
index.html
↓
main.jsx ← 啟動 React
↓
App.jsx ← 主要 App 元件
main.jsx,終於看到 App 的另一半打開:
src/main.jsx
內容大概是:
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
先只看這句:
import App from './App.jsx'
這時候就可以把剛才 App.jsx 最下面那句接起來:
App.jsx main.jsx
export default App ───────→ import App from './App.jsx'
原來 main.jsx 不是自己「知道」有一個 App.jsx。
是我們明明白白寫了:
import App from './App.jsx'
它才去把 App 拿進來。
./App.jsx 的 ./ 又是什麼?剛才 index.html 裡是:
/src/main.jsx
現在變成:
./App.jsx
./ 可以先讀成:
從目前這個檔案所在的資料夾開始找。
現在 main.jsx 在:
src/main.jsx
而 App.jsx 也在同一層:
src/
├─ main.jsx
└─ App.jsx
所以:
import App from './App.jsx'
就是:
去旁邊的
App.jsx,把它 export 出來的 App 拿進來。
現在就可以把前面的 index.html 接回來了。
index.html 先準備:
<div id="root"></div>
main.jsx 再找:
document.getElementById('root')
兩邊正好對上:
index.html
<div id="root"></div>
↑
│
main.jsx
document.getElementById('root')
再往外看:
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
現在可以把它翻成人話:
main.jsx 找到 HTML 裡的 root
↓
把這塊地方交給 React
↓
把 <App /> render 進去
原本:
<div id="root"></div>
一開始是空的。
React 啟動後,可以概念性地想成:
<div id="root">
React 畫出來的內容
</div>
所以整條路現在變成:
index.html
│
├─ 先建立 <div id="root"></div>
│
└─ 載入 /src/main.jsx
↓
main.jsx
↓
找到 #root
↓
import App
↓
App.jsx
↓
render <App />
↓
畫面
.jsx 嗎?看到這裡又冒出一個問題。
我們寫的是:
<App />
甚至 App.jsx 裡面直接有:
<section>
<h1>Get started</h1>
</section>
這長得很像 HTML,但它其實寫在 JavaScript 檔案裡。
瀏覽器不是直接拿 .jsx 原始碼來執行的。
開發時,中間還站著 Vite:
瀏覽器
↓
向 Vite 要 /src/main.jsx
↓
Vite 讀取、處理 main.jsx / App.jsx
↓
把瀏覽器能執行的 JavaScript 模組送回去
↓
瀏覽器執行
所以 Day 8 的 Vite 又接回來了。
原來:
npm run dev
不是單純「幫我開一個 localhost」。
我們開發時寫的 JSX、CSS import、圖片 import,以及模組之間的關係,Vite 都在中間幫忙處理。
這裡我又想到另一件事。
如果最後網站不是跑在:
localhost:5173
而是部署到 Nginx,或放到像 Cloudflare Pages 這類靜態託管環境,還是一樣嗎?
概念很像,但正式上線前通常會先做:
npm run build
Vite 會產生可以部署的檔案,例如:
dist/
├─ index.html
└─ assets/
├─ index-xxxxx.js
└─ index-xxxxx.css
這時候正式網站的入口仍然可以是:
dist/index.html
但它已經不是再去載入:
/src/main.jsx
而是載入 Vite build 出來的 JavaScript、CSS 等檔案。
所以可以先分成兩張圖。
開發時:
index.html
↓
/src/main.jsx
↓
App.jsx
↓
Vite 即時處理
↓
瀏覽器
正式 build 後:
App.jsx / main.jsx / CSS / 圖片
↓
Vite build
↓
dist/index.html + dist/assets/*
↓
Nginx / 靜態託管
↓
瀏覽器
Nginx 不需要看懂 React。
它主要做的是把 build 好的 HTML、JavaScript、CSS 等檔案送給瀏覽器;真正執行 React 的還是瀏覽器。
前幾天我們追的是:
npm run dev
↓
npm
↓
package.json
↓
node_modules
↓
Vite
↓
localhost
今天接的是:
localhost
↓
index.html
↓
建立 <div id="root"></div>
↓
<script type="module" src="/src/main.jsx">
↓
main.jsx
↓
document.getElementById('root')
↓
import App from './App.jsx'
↓
App.jsx
↓
render(<App />)
↓
畫面
好累Zzzzz
希望對大家有幫助@@