iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 22
4
Modern Web

Half-Stack Developer 養成計畫系列 第 22

我也想要模組化開發:Webpack

我也想要模組化開發:Webpack

近期的前端開發變得越來越複雜,尤其是你看我們上次提到的 SPA,把整個網頁當作一個 App 在寫。當你的專案變得越來越大的時候,把東西切得越來越小就是很重要的一件事情。像是我們在用 Node.js 寫程式的時候,都會把東西切成一個個不同檔案,然後透過require來引入,讓整個專案更容易管理。

如果你想在寫前端的時候也這樣做,要怎麼做?大概是把檔案分成很多個不同的 script,然後用<script>一個一個引入,接著用全域變數去呼叫吧?因為不同檔案之間除了全域變數,我也想不出其他的溝通方式了。只是全域變數這種東西當然是能不用就盡量不用,不然名字相衝之類的十分麻煩。有沒有可能可以跟 Node.js 一樣,用require來引入其他檔案呢?這樣子就很方便了!

這個時候就要請出我們的主角登場了:webpack。其實 webpack 我也沒有說很熟,只是會用其中一些很基本的功能而已,想瞭解更多的可以參考:webpack 中文指南或是详解前端模块化工具-Webpack

對我來說,會用 webpack 純粹是因為我想要讓我的程式碼更好管理,並且可以切成很多不同的模組。這件事情用 webpack 可以很容易的辦到。接下來用一個超級簡單的小專案示範 webpack 的威力。

因為只是要示範前端也可以用 require,所以這個範例真的很簡單。先來看看我們的資料夾結構:

.
├── dist
├── index.html
├── package.json
├── node_modules
├── src
│   └── js
│       ├── constants.js
│       ├── i18n
│       │   ├── default.json
│       │   └── tw.json
│       ├── i18n.js
│       ├── index.js
│       └── utils.js
└── webpack.config.js

我們要寫一個非常簡單的小程式,可以去引用其他的檔案然後顯示出一個字串,就是這麼簡單而已。上面這個結構我大致上說明一下:

  1. dist,這個是最後儲存打包成的 js 的資料夾
  2. src,裡面就是你的所有程式碼
  3. webpack.config.js,你 config 的設定

直接先從 src/js/index.js 這個檔案來看會最快:

import {getLocaleString} from './utils';
import Constants from './constants';

$(document).ready(function() {
  $('#main').append(
    '<div>' + getLocaleString(Constants.LOCALE_ID.HELLO, 'tw') + '</div>'
  );
})

可以看到這邊我們用了 ES6 的 import 語法,把其他的檔案引入進來,getLocaleString(Constants.LOCALE_ID.HELLO, 'tw')會拿到這個 ID 對應到的字串,然後語系指定是 tw。這個是在做多國語言時的常見用法。就是你會有一張 ID 列表,可以想成是很多的 key,然後對應到不同的文字。所以每個語言都會有一個語言的檔案,是 key 跟真的文字的對照。直接讓你看一下 i18n/default.jsoni18n/tw.json


// default.json
{
  "hello": "hello"
}

// tw.json
{
  "hello": "你好"
}

你只要用hello這個 key 去相對應的語系檔拿資料,就可以拿到你想要的翻譯了。最簡單的多國語言原理就是這樣,應該還滿容易懂的。至於constants.js這個檔案也是很簡單,是再把上面的那些 key 用大寫的變數儲存起來:

module.exports = {
    
  LOCALE_ID: {
    HELLO: 'hello'
  }
}

這樣你就可以用Constants.LOCALE_ID.HELLO代表這個 key 了。

為什麼要這樣做,而不是直接用 hello 這個字串就好呢?因為有時候你可能會打錯字沒有發現,例如說不小心打成 helli,可是因為他是一個字串,所以你在編譯的時候完全不會發現這個錯誤,只有在執行期間才會知道你犯錯了。

但如果是用 Constants.LOCALE_ID.HELLO,你打錯成 Constants.LOCALE_ID.HELLI的時候,有些幫忙檢查語法的程式就會告訴你沒有這個變數,你就可以即時更改了。

再來看一下i18n.js,內容也很簡單:

module.exports = {
  default: require('./i18n/default.json'),
  tw: require('./i18n/tw.json')
}

其實就只是把兩個語系的檔案都引入進來而已。上面都理解以後,就不難寫出 utils.js 裡面的程式碼了:

import i18n from './i18n';

 // tw...
 // 傳入 id 跟地區,回傳字串
function getLocaleString(id, region) {
  if(!region || !i18n[region]) {
    region = 'default';
  }

  return i18n[region][id];
}

module.exports = {
  getLocaleString
}

把上面這些程式碼兜起來以後,你會發現只是輸出一個「你好」的字串而已。但重點是我們在這邊用了一堆 importmodule.exports 來達成模組化,這個是我們以前做不到的事。以前你頂多只能把 function 全部寫在一個檔案,或者是用很多個 <script> 標籤裡面宣告全域變數來互相呼叫。

index.html的內容也很簡單,就只是引入 jQuery 跟等等我們會 build 出來的 js 檔案而已:

<!DOCTYPE html>
<html>
  <head>
    <script src="https://code.jquery.com/jquery-1.12.4.min.js"></script>
    <script src="dist/bundle.js" charset="utf-8"></script>
  </head>
  <body>
    <div id="main">
    </div>
  </body>
 </html>

萬事俱備,只欠東風。剩下什麼?剩下的就是我們把 /src/js 裡面的程式碼跟檔案,全部合在一起,然後變成 dist/bundle.js,一切就大功告成了。這時候要靠誰?當然就是 webpack 了!

要用 webpack 的時候你必須提供一個 webpack.config.js 的檔案,裡面寫好他的配置。配置完成以後只要用webpack這個指令就好了。而 webpack 有一個很重要的概念叫做loader,只要有相對應的 loader,你的東西就可以模組化。舉例來說,你只要用 babel-loader,就可以把你的 ES6 檔案透過 babel compile 之後變成 ES5。而 json 檔案也可以透過 json-loader 載入,你在程式碼裡面就可以require('xx.json')

甚至你還可以把 CSS 也當作模組來載入,只要你有 css-loader 就好,你可以用 require 把 CSS 也引入進來。總之呢,只要有 loader,你想載入什麼就載入什麼。至於要載入哪些檔案,聰明的 webpack 會自己從 entry 設定的檔案去找。接著讓我們來看看設定檔吧:

var webpack = require('webpack');

module.exports = {

  // 程式的入口點
  entry: './src/js/index.js',

  // 你要輸出到哪裡
  output: {
    path: __dirname + '/dist',
    filename: 'bundle.js'
  },

  // 載入哪些類型的檔案
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader', // npm install babel-loader
      }, { 
        test: /\.json$/, 
        loader: 'json' // npm install json-loader
      }
    ]
  }
}

你只要給他 entry 那個檔案,其他需要打包的他就自己會找到。接著在 terminal 輸入 webpack,你應該會看到類似的訊息:

⚡[21:44:46] huli@LideMacBook-Pro ➜ half-stack/22/src» webpack
Hash: c989e2f07547ae5a6791
Version: webpack 1.13.3
Time: 1485ms
    Asset     Size  Chunks             Chunk Names
bundle.js  2.82 kB       0  [emitted]  main
    + 6 hidden modules

代表已經打包好了,你可以到 dist/bundle.js看一下成果。我把它一些註解刪除並且格式弄好之後會變成下面這樣,有興趣的可以稍微研究一下下它的原理(看不懂也沒關係,不是你的錯):

(function(modules) { // webpackBootstrap
    // The module cache
    var installedModules = {};

    // The require function
    function __webpack_require__(moduleId) {

        // Check if module is in cache
        if (installedModules[moduleId])
            return installedModules[moduleId].exports;

        // Create a new module (and put it into the cache)
        var module = installedModules[moduleId] = {
            exports: {},
            id: moduleId,
            loaded: false
        };

        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

        // Flag the module as loaded
        module.loaded = true;

        // Return the exports of the module
        return module.exports;
    }


    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules;

    // expose the module cache
    __webpack_require__.c = installedModules;

    // __webpack_public_path__
    __webpack_require__.p = "";

    // Load entry module and return exports
    return __webpack_require__(0);
})
([
    /* 0 */
    function(module, exports, __webpack_require__) {

        'use strict';

        var _utils = __webpack_require__(1);

        var _constants = __webpack_require__(5);

        var _constants2 = _interopRequireDefault(_constants);

        function _interopRequireDefault(obj) {
            return obj && obj.__esModule ? obj : {
                default: obj
            };
        }

        $(document).ready(function() {
            $('#main').append('<div>' + (0, _utils.getLocaleString)(_constants2.default.LOCALE_ID.HELLOA, 'tw') + '</div>');
        });

    },
    /* 1 */
    function(module, exports, __webpack_require__) {

        'use strict';

        var _i18n = __webpack_require__(2);

        var _i18n2 = _interopRequireDefault(_i18n);

        function _interopRequireDefault(obj) {
            return obj && obj.__esModule ? obj : {
                default: obj
            };
        }

        // tw, vn...
        function getLocaleString(id, region) {
            if (!region || !_i18n2.default[region]) {
                region = 'default';
            }

            return _i18n2.default[region][id];
        }

        module.exports = {
            getLocaleString: getLocaleString
        };

    },
    /* 2 */
    function(module, exports, __webpack_require__) {

        'use strict';

        module.exports = {
            default: __webpack_require__(3),
            tw: __webpack_require__(4)
        };

    },
    /* 3 */
    function(module, exports) {

        module.exports = {
            "hello": "hello"
        };

    },
    /* 4 */
    function(module, exports) {

        module.exports = {
            "hello": "你好"
        };

    },
    /* 5 */
    function(module, exports) {

        'use strict';

        module.exports = {

            LOCALE_ID: {
                HELLO: 'hello'
            }
        };

    }
]);

總之呢,用 webpack 以後,我們就可以像寫 Node.js 那樣,在寫前端的時候也利用這個優勢,切成很多不同的檔案跟 function,再用import或是require來引入。雖然你現在可能感覺不太出來好處,但是當你的專案變得越來越大的時候,你就會知道為什麼要這樣分了。因為如果不這樣分的話,你會很想死...

webpack 除了這個主要功能以外,其實還有更多也很好用的附加功能,這些就留給大家自己去研究囉。


上一篇
你走你的陽關道,我走我的獨木橋:前後端分離
下一篇
換一種思考方式:React
系列文
Half-Stack Developer 養成計畫30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言