2022年7月11日 星期一

每一句都可以讀得懂的最精簡 webpack 設定檔 + js toolchain 新手的行前說明

Javascript 語言本身已經 令程式設計師愛恨交織; 它的工具鏈 (toolchain) 更令人眼花撩亂。 去年 我很匆促地學了一點 webpack, 今年好像又不夠用了。 不同時期不同作者的設定檔都差好多, 其中很多也不適用於新版。 越爬文越迷惘, 覺得自己怎麼那麼弱, 都快要哭出來了... 偶然搜尋到這篇: A Crash Course in Modern JavaScript Tooling, 聽到作者安慰: "It's not your fault." 突然覺得獲得救贖, 原來很多 python 族都跟我有相同的感覺! 這次沒有時間壓力, 花了快一個月終於建立了一個最精簡的骨架程式 jstc-novice, package.json 跟 webpack.config.js 裡面的每一句話都看得懂, 可以作為 (已略熟 javascript, 包含會開啟 console 除錯的) webpack 初學者的出發點。 JS 的工具鏈有很多雷, 建議看程式碼之前先很快讀一下本文, 就像是旅遊之前的行前說明一樣, 對於避免踩雷很重要。

對初學者而言, javascript 的工具鏈裡面, 最需要知道的有三類工具:

  1. package manager (例如 npm 或 yarn; 類似 python 世界的 pip3) 用來安裝套件。
  2. transpiler (例如 babel 或 typescript) 用來把符合新規範的 ES2017 (也就是 ES8) javascript 程式碼甚或是改良過的 typescript 程式碼轉成 (苦追不上最新流行的) 眾瀏覽器們看得懂的較舊程式碼 (例如 ES2015, 也就是 ES6)。
  3. bundler (例如 webpack 或 parcel) 用來把所有的資源 (程式碼、 css、 圖片、 json 或 csv 資料檔、 ...) 整合打包起來。 我把它想成有點像是 c/c++ 世界的 ld (linker)。

至於給 npm 看的 package.json, 我把它想成 Makefile。 以上是救贖文簡短的摘要, 被我加了一點料。 還是大力推薦閱讀原文, 特別是文中的圖片。

關於實作, 經歷許多錯誤訊息繞了好幾條冤枉路之後, 我的心得是:

  1. 找教學文時, 一定要注意年份夠不夠新! 例如我很喜歡這份教學文: Setting Up a Babel Project 但是為什麼他推薦的 babel-core 最多只能升級到 2017 年的 6.26 版? 找好久才發現: 原來 命名方式有改變, 想安裝新版要用 npm i @babel/core。 Javascript 的世界, 變化太快、 又欠缺清楚的 roadmap 文件, 如果照著舊版的文件做, 很有可能會卡關。
  2. 盡量用最新版的 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
  3. 在 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 等等指令。
  4. 暫時離開 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, 甚或是改用別的語言寫程式, 我完全可以理解。

沒有留言:

張貼留言

因為垃圾留言太多,現在改為審核後才發佈,請耐心等候一兩天。