Javascript 語言本身已經 令程式設計師愛恨交織; 它的工具鏈 (toolchain) 更令人眼花撩亂。 去年 我很匆促地學了一點 webpack, 今年好像又不夠用了。 不同時期不同作者的設定檔都差好多, 其中很多也不適用於新版。 越爬文越迷惘, 覺得自己怎麼那麼弱, 都快要哭出來了... 偶然搜尋到這篇: A Crash Course in Modern JavaScript Tooling, 聽到作者安慰: "It's not your fault." 突然覺得獲得救贖, 原來很多 python 族都跟我有相同的感覺! [2023/2 很多時候其實根本可以略過工具鏈啊!] 這次沒有時間壓力, 花了快一個月終於建立了一個最精簡的骨架程式 jstc-novice, package.json 跟 webpack.config.js 裡面的每一句話都看得懂, 可以作為 (已略熟 javascript, 包含會開啟 console 除錯的) webpack 初學者的出發點。 JS 的工具鏈有很多雷, 建議看程式碼之前先很快讀一下本文, 就像是旅遊之前的行前說明一樣, 對於避免踩雷很重要。
對初學者而言, javascript 的工具鏈裡面, 最需要知道的有三類工具:
- package manager (例如 npm 或 yarn; 類似 python 世界的 pip3) 用來安裝套件。
- transpiler (例如 babel 或 typescript) 用來把符合新規範的 ES2017 (也就是 ES8) javascript 程式碼甚或是改良過的 typescript 程式碼轉成 (苦追不上最新流行的) 眾瀏覽器們看得懂的較舊程式碼 (例如 ES2015, 也就是 ES6)。
- bundler (例如 webpack 或 parcel) 用來把所有的資源 (程式碼、 css、 圖片、 json 或 csv 資料檔、 ...) 整合打包起來。 我把它想成有點像是 c/c++ 世界的 ld (linker)。
至於給 npm 看的 package.json, 我把它想成 Makefile。 以上是救贖文簡短的摘要, 被我加了一點料。 還是大力推薦閱讀原文, 特別是文中的圖片。
關於實作, 經歷許多錯誤訊息繞了好幾條冤枉路之後, 我的心得是:
- 找教學文時, 一定要注意年份夠不夠新!
例如我很喜歡這份教學文:
Setting Up a Babel Project
但是為什麼他推薦的 babel-core 最多只能升級到 2017 年的 6.26 版?
找好久才發現: 原來
命名方式有改變, 想安裝新版要用
npm i @babel/core
。 Javascript 的世界, 變化太快、 又欠缺清楚的 roadmap 文件, 如果照著舊版的文件做, 很有可能會卡關。 - 盡量用最新版的 nodejs。 舊版的 nodejs
和舊版的工具經常會遇到相容性的問題。 不值得在這裡浪費時間。
所以我用 docker:
docker run --name nodejs01 -it -v $HOME/nodejs:/worksp -p 8080:8080 node /bin/bash
啟動 nodejs docker。 其中 $HOME/nodejs 是我在 host 上的家目錄底下的自選工作目錄; 而 port 8080 則是為了讓 webpack-dev-server 這個網頁伺服器在前端呈現你的程式。 關於 -v 跟 -p 請見 交換 ethercalc 容器學 docker。 - 在 docker 裡面, 如果採用 root 身份做事,
會遇到權限不足的問題。 不是 root 的權限不足,
而是某些工具會採用別的身份, 所以無法寫入 host 的 /home/ckhung/...
奮鬥很久之後放棄研究, 改採最簡單的解決方式:
在 docker 裡面
useradd -g users -u 9876 -m -s /bin/bash ckhung
其中 9876 應改成正確的數字: 在 host 裡面id -u $USER
得到的數字。 最後回 guest 裡面, 用sudo ckhung ; cd /worksp/...
進到程式碼的工作目錄之後, 才開始下 npm 等等指令。 - 暫時離開 docker, 下次要再進入時, 要下:
docker start -ai nodejs01
。 詳見 圖解 docker 狀態轉換。
[我是 nodejs 外行人,現在才知道...]
每安裝一個 nodejs 套件, 可能也會在專案下的 node_modules/.bin/
底下多出一些執行檔。 例如 npm i webpack
會帶來 node_modules/.bin/webpack 。 想要使用這些指令,
就必須設定 $PATH 環境變數, 或打出完整的路徑。
第三個更簡單的選擇則是用 npx webpack
的方式執行。
詳見 npx 與 npm 的差別、
differences between npm and npx。
想要認真學 webpack 時, 不停地搜尋求救,
就會發現每篇教學文的範例設定檔都 不大相同 大不相同。
convention over configuration 這篇文章很棒:
如果盡量都採用預設值, js 程式碼和 webpack 的設定檔可以簡化到什麼地步?
我不喜歡迷迷糊糊抄一個很複雜的設定檔, 裡面很多看不懂的東西。
所以打算學這篇, 從最精簡的設定檔出發, 有必要時才改設定。
寫 python 時一定免不了要 import 一些函式庫來用, 寫 js 也是。
但 js 的模組有兩種格式。
預設採用較舊的 CommonJS 格式, 相容性比較好;
如果在 package.json 裡面加上 "type": "module"
,
則採用較新的 ES modules 格式, 是未來趨勢。
詳見
CommonJS vs. ES modules in Node.js。
順便一提, 如果看到 "Field 'browser' doesn't contain
a valid alias configuration", 有可能是
import 語法錯誤 或是
完全不相關的原因。
至於 index.html 應該放在哪裡呢?
這一篇 由簡到繁, 提供三個選擇。
第一, 直接在 dist/ 底下手工打造 index.html,
並且手工寫入 <script src="app.bundle.js"> </script>
。
但是有些文章建議每次 bundle 時都要把整個 dist/ 砍掉,
所以手工打造的檔案放這裡並不是個好主意。
第二, 採用 webpack-html-plugin 並且在 webpack.config.js
裡面設定 filename: './index.html'
(這個路徑相是對於 dist/ )
叫它自動產生 dist/index.html。
第三, 手工建立一個模版 src/index.html 並且在 webpack.config.js
裡面加上 template: './src/index.html'
(對, 這個路徑不是相對於 dist 也不是相對於 src, 而是相對於專案的根目錄)
叫 webpack-html-plugin 根據這個模版幫你產生 dist/index.html 。
「使用 html-webpack-plugin 生成 HTML 文件」 有更完整的介紹。
有些程式碼或 html 片段只在開發過程 (development) 當中會想要納入;
真正要釋出時 (production) 則會略過。
(對於較熟悉 C 語言的人來說, 就是類似
#ifdef ...
之類 conditional compilation 的情境,
或是 assert()
之類的程式碼。) 這時會想要用
DefinePlugin 搭配 process.env.NODE_ENV 來達成。
不過更常用到的應該是 html 裡面的圖片檔:
<img src="xyz.jpg" ... >
要怎麼做才可以正確地叫 webpack 把 src/*.jpg 複製到 dist/ 去呢?
這個問答 有很多解答,
也很適合警告大家為何 js 不適合作為初學者入門程式語言。
我不喜歡採用 file loader 或新式的 asset module,
因為採用這兩者的話, 都必須在你的 *.js 裡面 import 或 require 圖片檔,
結果會佔用兩倍的空間。 (圖檔 + js 裡面的 object)
難怪多數人的最佳選擇都是 採用 copy-webpack-plugin。
不過 指定來源與目的地路徑並不是非常直覺。
「讀取資料檔」 是程式碼很常需要做的事。
若是 perl 語言, 直接在命令列就可以解決;
若是 python 語言, 兩句話就解決。
至於 js 嘛... 一般的靜態資料檔可以比照圖片檔的處理方式。
如果是 json 檔, 假設你的
node js 版本至少 17.5,
那麼甚至不需要採用 copy-webpack-plugin,
只要在程式碼裡面 import 變數 from '檔名.json'
就可以了。
有些較舊的文章說要安裝 json-loader。
不要! 那是舊版的 webpack 才需要。
我想玩的很多程式都是用 typescript 語言寫的。 如果自己也想學 typescript 語言, typescript 跟 babel 這兩個 transpiler 應該採用哪一個呢? 這篇比較文 說 babel 欠缺較嚴格的檢查、 比較符合 ECMAScript 的標準、 decorator @#$% (看不懂)、 有豐富的 plugins 可以對程式碼作最佳化。 另一方面, 你也可以並用兩者、 讓它們互相截長補短。 作者認為原先採用哪個, 就繼續用就好, 不必費力移民; 如果從零開始, 他可能稍微傾向推薦 typescript。 考量 js 有那麼多變種方言, 我則可能會選擇 babel。
好, 那麼現在可以把我的 jstc-novice 下載回去, 研究一下幾乎是從零開始手工打造的 webpack.config.js 設定檔, 是不是每一句都有意義了呢? 另外開一個終端機分頁進入 docker、 用正確的身份 (與 host 相用的用戶 id) 在專案的根目錄裡面執行:
npm i -D webpack webpack-cli html-webpack-plugin # npm i -D webpack-dev-server # 最終其實並沒有用到這個,可省略 npm i -D copy-webpack-plugin copy-webpack-plugin npm i 自己的程式碼要用到的模組 npm run build
上面 npm 的 -D 選項等同於 --save-dev , 它指示 npm
把欲安裝的這個套件的名稱記錄到 package.json 的
devDependencies 欄位去, 意謂著打包過程會需要用到它。
如果是安裝 js 程式碼執行時 才需要用到的套件,
就不需要 -D 選項, npm 會自動把它記錄到 package.json 的
dependencies 欄位。 事實上當你的工作目錄底下已有先前備好的 package.json 時,
直接一句 npm i
它就會自動把上述兩欄裡面的所有套件都自動安裝好,
不需要逐一手動安裝。 最後一句執行之後, 就可以從 host 裡用瀏覽器開啟
.../jstc-novice/dist/index.html 並打開瀏覽器的 console 查看執行結果。
蠻多教學文或範例都會用到 webpack-dev-server 這個套件。 它提供 Live Reload 與 HMR 等等功能, 不過省略它也並不影響專案的打包與執行。
Javascript toolchain 像是一座永遠到處在施工的城市, 每次逛到同一個地點, 地形地貌可能都跟上次來的時候不一樣。 我挑了最常用的功能, 也已經從每個功能的眾多解決方案當中盡力挑選, 只推薦較簡易、 較接近預設、 較多人使用的解法。 但是這篇到了明年還適用嗎? 我也不知道。 看到這裡, 如果你想要放棄 js toolchain、 改用傳統簡單的 js, 甚或是改用別的語言寫程式, 我完全可以理解。
其實這樣比較本身就不合哩,比較的前提就是錯的。
回覆刪除如果以 python (或其他語言)來開發 web ,最後的成品通常是 MPA (multiple page application)的形式
這點單純用 node.js 搭配 express.js 或其他 framework 也都可以做到 MPA
(Flask 對照的是 express.js 或是 fastify , django 對照的是 FoalTS 或是其他類似的)
這類 MPA 的架構不需要 webpack 也不需要任何 bundler
甚至很多後端 framework 都整合得很好,只需要下他們提供的 CLI 指令就好
譬如 nest.js 或是 FoalTS
這些都完全不需要開發懂 webpack 或 bundler
SPA (single page application)的部分會很複雜,這點是確實
因為要部署前端程式碼跟整合各種資源,需要搞定 bundler
但是比較的前提要對,要比就要公平。
我自己認為 JS 真的非常適合初學者學習
語法簡單,接近任何 C Like 的語言(python 那種語法才奇怪)
執行環境哪裡都有,一開始學習只需要有瀏覽器就好
因為 BREAK THE WEB 的關係, JS 語法也幾乎沒有在 deprecated 的(極少)
不用像 python/PHP 這樣被搞來搞去
目前比較嚴重的當屬 commonjs 跟 ES 的模組過渡
(不過這是歷史因素也沒辦法,當初 node 出來時還沒有標準的模組)
喜歡圖形設計的,整合 HTML CSS 更是簡單不過
JS 這個語言真的承受太多的誤解