從 laravel-mix 0.8 升級到 1.4 版的記錄與坑

August 7, 2017

相信現在許多在使用 Laravel 的朋友,應該都是使用 laravel-mix處理前端的相關資源的打包, 不知道大家覺得 laravel-mix 用起來的感覺是如何?我自己覺得用起來真的沒有想像的好用, 雖然 Laravel 官方有提供文件說明操作的方法,但是我自己覺得它的擴展性很差,而且又把它再抽象化了 :skull:。

webpack

webpack 雖然設定起來比較繁瑣,但其實你會遇到的問題,可能很多人都有遇過了, 也或許有人提出相關 issue,所以要尋找一些解決方式我自己覺得相對容易, webpack2 (現在是 webpack 3 囉!)文件相較於 webpack1 也真的友善許多 :smirk:, 老實說,我第一次看到 webpack 的文件真的覺得是天書,對新手開發者來說真的是一個痛點! 但是現在官方文件真的寫得清楚容易多了,像是 Guides 的部分我覺得就蠻不錯的, 對岸也有人將它翻譯成簡體文件,閱讀英文較吃力的朋友不妨可以參考。

升級的一些地雷

最近把公司專案的 laravel-mix 從 0.8.1 升級至目前最新的 1.4.2 版本,遇到的一些問題, 稍微做個整理和記錄,如果有打算升級 laravel-mix 的朋友,若有遇到相關問題可以參考看看 :metal:。

:bomb: 地雷一 - cross-env

比較早之前 Laravel 內的 package.json 版本的 scripts 大概是這樣:

{
  "scripts": {
    "dev": "node node_modules/cross-env/dist/bin/cross-env.js NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
  }
}

透過 cross-env 來解決跨平台設定 NODE_ENV 的問題,但是較早的版本是引用 node_modules 內的 js, 在新版的部分都會另外安裝 cross-env,所以設定要改成如下:

{
  "scripts": {
    "dev": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
  }
}

:bomb: 地雷二 - mix.extract() & mix.autoload()

在前端,我們可能會用了不少的 library 來幫助我們做到介面上的美化,或是與使用者的互動, 但這些套件全部一次打包到一個 xxx.min.js 其實不是非常的好,當網站在載入的時候,會浪費非常多時間, 而且當你只是修改一小部分的 JavaScript 就必須要整個重新打包(包含那些 library),使用者載入網站就必須要再重新下載整個 bundle 檔,原先 cache 的檔案就沒用了!

laravel-mix 提供了一個 extract 的方法,讓你可以把一些 libray 給抽取出來,把它打包成 vendor.js, 就是一個 Code Splitting 的概念,但是請記得按照以下順序把你的 JavaScript 給 include 到你的網頁中:

<script src="/js/manifest.js"></script>
<script src="/js/vendor.js"></script>
<script src="/js/app.js"></script>

一般在這裡會把 Vueaxioslodash 等等 library 給提取出來,我自己在這裡也把 jQuery 也給提出出來,就遇到了一些問題,你必須還要再寫一個 mix.autoload 的方法,讓你所需要的 library 自動被 include(因為所用到的 Bootstrap 依賴 jQuery),目前在 Laravel 官方文件還看不到 autoload 方法的說明,請直接到 laravel-mix - Autoloading 查看文件!

mix
  .js('resources/assets/js/app.js', 'public/js/app.js')
  .extract([
    'jquery',
    'bootstrap-sass',
    'axios',
    'vue',
    'lodash'
  ])
  .autoload({
    jquery: ['$', 'window.jQuery', 'jQuery', 'jquery'],
    vue: ['Vue', 'window.Vue'],
    axios: ['axios', 'window.axios'],
    lodash: ['lodash', 'window._']
  })
  .sass('resources/assets/sass/app.scss', 'public/css/app.css');

autoload 內是一個 object 的形式,每個 key 是你 library 名稱,value 的地方就是代表這些形式的變數會直接載入對應(key)的 libray;意思是,當我某個地方用到了 $ 或是 jQuery,webpack 會自動幫我把 library 給準備好,讓我可以使用,同理像是 Vueaxios 也是一樣的道理。

但是!我這麼做了之後發現一載入到網站還是找不到 jQuery,在看 error log 得到了以下的訊息:

// other error messages...

} else {
  // Browser globals
  factory(jQuery);
}

解決方式是需要把 window 修改為 global

- window.$ = window.jQuery = require('jquery');
+ global.$ = global.jQuery = require('jquery');

如果有遇到這樣問題的朋友,或許可以試試看這個方法。

:bomb: 地雷三:如果用到了 async/await

我在撰寫 Vue Component 的時候,用到了 async/await 的語法,在 development 模式下沒什麼問題,但是跑 CI 卻炸掉了, 在本機沒有先測試過 npm run production 的結果,在 CI 是 run production 的 script,後來 debug 找到的錯誤訊息是:

ReferenceError: regeneratorRuntime is not defined

大概知道是因為 async/await 的關係,造成在 runtime 的時候錯誤,我的解決方式再去額外安裝 babel 的 transform-runtime

$ yarn add babel-plugin-transform-runtime --dev
$ yarn add babel-runtime

為了確保在 development 和 production 都可以運作正常,所以兩個都安裝,接著建立一個 .babelrc

{
  "plugins": ["transform-runtime"]
}

plugins 內把 transform-runtime 加入進去就沒問題了。

一些使用的心得

在 Laravel 官方 mix 文件有個部分是 Copying Files & Directories,這裡主要是 mix 提供了方法讓你可以去 copy 檔案到你要的目錄下,如果是 copy node_modules 下的檔案,我覺得這有點多此一舉,其實可以用更容易的方式來做,應該可以節省一些 compile 的時間 :stuck_out_tongue:!

善用 path.join

Nodejs 內建的 path 的模組非常好用,這裡我們使用 join 的方法直接連結到 node_modules 目錄下的檔案:

const path = require('path');

mix.styles(
  [
    path.join(__dirname, 'node_modules/sweetalert/dist/sweetalert.css'),
    path.join(__dirname, 'node_modules/select2/dist/css/select2.min.css'),
  ],
  'public/css/all.css'
);

mix.combine(
  [
    path.join(__dirname, 'node_modules/sweetalert/dist/sweetalert.min.js'),
    path.join(__dirname, 'node_modules/select2/dist/js/select2.min.js'),
  ],
  'public/js/all.js'
);

這樣就可以不需要再多寫 mix.copy() 啦!順帶一提,mix.combine() 也是新的方法,類似 scripts(),但是使用 combine 方法可以幫你將必要的檔案給 bundle 並 minify,詳細可以參考文件:Concatenation and Minification

其他更多相關的方法我就沒使用過了,未來有機會用到或是遇到相關的問題再補上來。

如果文章內有任何說明上的錯誤,歡迎提出告訴我,希望大家可以一起多交流!:v:

補充

感謝 Recca 大大關於設定 processCssUrls 的建議,今天嘗試修改了一下,又減少了 bundle 的時間了!使用 processCssUrls 需要特別注意的是,像是 Bootstrap 內的 fonts 需要自己從 node_modules copy 到 public 資料夾下,如果有用到 fontawesome 的話也要記得從 node_modules 複製到 public 資料夾,或者是其他相關你有用到圖片等等,這邊我踩的地雷是,要注意你複製到目的路徑,因為我在 extract() 有將 bootstrap-sass 給提取出來,所以在 copy 時必須對應到相對的資料夾下:

mix.copyDirectory(
  'node_modules/bootstrap-sass/assets/fonts/bootstrap',
  'public/fonts/vendor/bootstrap-sass/bootstarp'
);

mix.copyDirectory(
  'node_modules/font-awesome/fonts',
  'public/fonts/vendor/font-awesome'
);

app.scss 內我們有 import font-awesome,這裡有用到一個 $fa-font-path 變數,原先的路徑是:

~font-awesome/fonts/

但是在 bundle 完後,發現網頁上並沒有正確顯示,對應了上面的 mix.copyDirectory 的路徑必須改成:

../fonts/vendor/font-awesome

路徑會修改成是因為 bootstrap 現在是被 bundle 在 vendor.js 內,所以當他需要這些用到 font-awesome 就必須去調整在 $fa-font-path 的路徑,當然這不是絕對,你必須要根據你自己的專案目錄去調整相對路徑,我自己另外也調整了好幾個其他相關套件文件路徑。

Reference