# webpack ## Tree Shaking 他是一個打包的一種方式,幫你把沒有使用的程式碼,或是相關的引入去排除,通常會搭配 siedeEffect 去使用。 備註: 他依賴ES module ### 範例 ```javascript //Math.js const add = (a, b) => a + b; export default { add }; // string.js const composeString = (a, b) => `${a} ${b}`; const addString = (a) => `label: ${a}`; export default { addString }; // index.js import { add } from './math'; import { addString } from './string'; console.log(add(1, 2)) ``` 你可以看到 index.js 引入 Math.js string.js 這兩隻檔案,但卻只使用 add ,這時你打包後發現 ![](https://hackmd.io/_uploads/B1xCDZxP2.png) composeString 這個函數也被打包進去了,這就是 Tree Shaking 在 es module import 時的sideEffect。 解決方式 在package.json 中設置 sideEffects:false,告訴 webpack 所有要打包的模塊不要執行任何的 side effect ```javascript // package.json { "name": "tree-shaking", "sideEffects": false, "version": "1.0.0", ... } ``` sideEffects: false 的運作原理就是把 import {a} from xx 转换为 import {a} from 'xx/a' 從而改善不必要的自動 import ,如同[babel-plugin-import](https://github.com/umijs/babel-plugin-import) 功能。 使用後你就會發現 composeString 不會被打包進去了 ![](https://hackmd.io/_uploads/BJ7sObePh.png) siedeEffect 的原因是因為 module import 會把所有的檔案都打包進去儘管 composeString 他沒有被export **補充:** 有時候我們使用 UI 庫,例如MUI 他們會要求我們更改 import 方式 ```typescript //not use import {Button} from '@mui/material'; // use import Button from '@mui/material/Button'; ``` 這是一種手動 Tree Shaking 的一種方式 ### 但為什麼不直接用 sideEffect: fasle? 但這時候你會想那為什麼不全部用sideEffect: fasle就好了? 相信大家一定用過 polyfill ,這時我們import 進去 ```javascript //index.js import './polyfill'; import { add } from './math'; import { addString } from './string'; console.log([].customMethod()); // polyfill.js Array.prototype.customMethod = () => { console.log('customMethods'); }; ``` 因為你開啟 sideEffect false 只要你引入的代碼沒有被使用就不會被打包,這樣就會導致你的build就無法包含 polyfill 的內容了,所以你可以修改一下 sideEffect 設置 ```javascript { "name": "tree-shaking", "sideEffects": ["./src/polyfill.js"], "version": "1.0.0", ... } ``` 這樣 polyfill.js 就會被打包了 ![](https://hackmd.io/_uploads/BJ5HnWlP3.png) 值得注意的是有時候專案會引入 css 檔案,這時也要記得放到sideEffect 中,不然就會造成你打包後的內容沒有樣式XD,例如 sideEffects: ["*.css"] ### 進階 有時候我們會需要測試函數, 你可以透過/*#__PURE__*/ 將你調用的函數不被打包進去,這時對於你專案的調適或是打包後的體積都有幫助。 ``` /*#__PURE__*/ double(55); ``` ## hash 這邊我想補充一下為什麼會有舊資料問題,原因是瀏覽器的 cache 策略: 1. cache control 也就是狐狸大大說的 cache control header設定,在 cache control 的一段時間內,如果靜態資源的檔名不變,瀏覽器將不會重新下載同一個黨名的資源。 優點: 避免重新下載重複資源,並加速整個 page 的 loading 時間與不必要的 request 2. 問題闡述 所以很多時候假如你的照片有修改但你檔名不變,就會發生 prod 的 image 內容需要等 cache control 下一次重新 update 才會重新抓取資源,這樣的更新是不及時的 。 3. 解決方式 在 builder 可以針對靜態資源的檔名做 hash 增加唯一性。 `[ext]` : 副黨名假如你是 Image.png 那這邊就是 png `[name]`: 原本的黨名 Image.png 那這邊就是 Image `[hash]`: 每次 build 後會有特定的 hash 值 `Timestamp` : 自己定義變數 ```typescript build: { rollupOptions: { output: { chunkFileNames: `static/js/[name].[hash]${Timestamp}.js`, entryFileNames: `static/js/[name].[hash]${Timestamp}.js`, assetFileNames: `static/[ext]/[name].[hash]${Timestamp}.[ext]`, }, } } ``` 但這邊你一定會有疑慮為什麼要加 `Timestamp` 原因是每個檔案 build 最後的結果他的 `hash` 都會是一樣的,假設我 build image.png 這個檔案那他每次的解果都會是 `static/png/image.abcdef1234567890_1671234567890.png` , 所以才會需要加 Timestamp 增加唯一性,這樣才能讓瀏覽器每次重新抓取新資料,但筆者這邊補充一個小知識除了 `Timestamp` 方式外 `hash` 還有其他用法: `hash` : 使資源內容的 hash 有唯一性。 `chunkhash` : 根據 chunk 資源內容有唯一性,可以用於代碼分割。 `contenthash` : 根據檔案內容產生唯一性,同常用於 `css` 的編碼。 ## 在 react 中為什麼不能使用 relative path 放 img source 原因是你需要將 `img` 的 `source` `import` 近來給 `webpack` 打包。 ### 所以你不能這樣寫 這樣會導致 `webpack` 編譯時找不到 `img` 的 `source` ```typescript <img src="./somePath"> ``` 在 `webpack` 中讀取 `assert` 的設定用 `webpack` 解釋的話看一下 `demo` ```typescript module.exports = { entry: "./src/main.js", output: { // 打包後的路徑 path: isProduction ? path.resolve(__dirname, "../dist") : undefined, // js module 的打包位置,沒做 code spilt 的 js filename: isProduction ? "static/js/[name].[contenthash:10].js" : "static/js/[name].js", // 有做 code spilt 的 js 的打包路徑 chunkFilename: isProduction ? "static/js/[name].[contenthash:10].chunk.js" : "static/js/[name].chunk.js", // 所有 assert 打包後的路徑 assetModuleFilename: "static/media/[hash:10][ext][query]", // 每次打包都重新刪除 dist 資料夾確保每次的打包都是最新的內容 clean: true, // 哪個資料夾底下的內容需要給 webpack 打包 publicPath: '/src', }, } ``` 那 `webpack` 是怎麼知道讀取 `assert` 的則是透過 `rules` 設定,這邊不廢話直接簡單總結。 `test` : 哪些 `assert` 需要被打包。 `type` : `asset/resource` 代表是圖片資源。 * 補充一下在 `webpack 4 `打包圖片資源是透過 `file-loader` 跟 `url-loader` ,但在 `webpack 5` 把這兩個 `loader`整合再一起,所以只需要設定 ` type: 'asset/resource` `webpack` 就會自動幫你打包圖片資源了。 這代表當圖片大小大於 `4kb` 就把圖片資源轉乘 `base64` 到 `img` 的 `src` 上,所以在打包後的 `dist` 資料夾中會看不到大於 `4kb` 的圖片喔~ ```typescript parser: { dataUrlCondition: { maxSize: 4 * 1024 // 4kb } } ``` ```typescript module.exports = { // .. module: { rules: [ { test: /\.(png|jpg)/, type: 'asset/resource', parser: { dataUrlCondition: { maxSize: 4 * 1024 // 4kb } } }, ] }, // .. } ``` 所以你在 `react` 中需要先 `import img` 的`source` 才能放到 `src` 中 , `webpack` 才能幫你打包圖片~ ```typescript import path from './someImg.png' <img src={path}> ``` 那問題來了為什麼我們可以透過 `absoult path` 方式引入 `public` 的 `path` ```typescript some-project-root ├── public │ └── image.png └── src └── App.css <img src='/image.png'> ``` 原因是 `public` 是 `react` 自己處理的, `webpack` 並不會去打包,因為有指定 `publicPath` 是 `src`所以 `public` 的內容 `webpack` 才可以不用去理會。 ```typescript module.exports = { entry: "./src/main.js", output: { // .. // 哪個資料夾底下的內容需要給 webpack 打包 publicPath: '/src', }, } ``` `react` 會透過 `process.env.PUBLIC_URL` 這個環境變數原封不動地把 `public` 裡面的內容 `copy` 到 `dist`資料夾中。 ```typescript return <img src={process.env.PUBLIC_URL + '/img/logo.png'} />; ``` 最後提醒一下,建議放在 `public` 的內容是靜態資源為主,如果是長期變更的資源可能會跟`browser` 的`cache`週期撞到導致內容無法立即更新,那放在 `src/` 裡面的 `image` 因為有將打包後的 `source` 做 `hash`,所以不會因為 `cache` 的關係內容是舊的。 ```typescript assetModuleFilename: "static/media/[hash:10][ext][query]", ``` ## 結論 `public` : 建議放靜態資源。 `src/*` : 適合放需要長期變更的資料或是 `image` ,但要急得加上 `hash` 設定。