近期的前端開發變得越來越複雜,尤其是你看我們上次提到的 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
我們要寫一個非常簡單的小程式,可以去引用其他的檔案然後顯示出一個字串,就是這麼簡單而已。上面這個結構我大致上說明一下:
直接先從 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.json
跟 i18n/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
}
把上面這些程式碼兜起來以後,你會發現只是輸出一個「你好」的字串而已。但重點是我們在這邊用了一堆 import
跟 module.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 除了這個主要功能以外,其實還有更多也很好用的附加功能,這些就留給大家自己去研究囉。