前端模組化雜記

菜的黑人牙膏發表於2019-01-28
  • 前言
  • CMD\AMD簡介
  • Commonjs簡介
  • Module簡介
  • Common和Module的區別
  • Module與webpack
  • Module與Babel
  • 一些問題
  • 總結
  • 引用

前言

前端模組化在近幾年層出不窮,有Node的CommonJs,也有屬於client端的CMD/AMD模式,而ES6本身也出現了Modules,再加上Webpack以及babel的普及,雖然在程式碼中經常使用到這些用法,但是如果不去深入研究,總覺得是一個黑魔法,無法探測一些問題的根源。

AMD/CMD簡介

事實上,隨著打包工具和Babel在前端工程化的世界裡大放異彩,AMD/CMD也在逐步退出歷史的舞臺,這裡簡單的介紹下其用法及語義。

AMD及其用法

AMD 即Asynchronous Module Definition,中文名是非同步模組定義的意思。代表(require.js)

/** main.js 入口檔案/主模組 **/
// 首先用config()指定各模組路徑和引用名
require.config({
  baseUrl: "js/lib",
  paths: {
    "jquery": "jquery.min",  //實際路徑為js/lib/jquery.min.js
    "underscore": "underscore.min",
  }
});
// 執行基本操作
require(["jquery","underscore"],function($,_){
  // some code here
});

複製程式碼

CMD及其用法

CMD 即Common Module Definition, 中文名是通用模組定義的意思。代表(Sea.js)

/** sea.js **/
// 定義模組 math.js
define(function(require, exports, module) {
    var $ = require('jquery.js');
    var add = function(a,b){
        return a+b;
    }
    exports.add = add;
});
// 載入模組
seajs.use(['math.js'], function(math){
    var sum = math.add(1+2);
});

複製程式碼

兩者的區別

1、AMD推崇依賴前置,在定義模組的時候就要宣告其依賴的模組
2、CMD推崇就近依賴,只有在用到某個模組的時候再去require

Commonjs簡介

Commonjs的應用主要是在Node應用中。

通過require引入檔案, 檔案內部則通過module.export暴露,如下a 就是 module.export

// 引入某個檔案
const a = require('some.js')

// some.js

module.export = {
  ...
  // some code
}
複製程式碼

除去module.export,Commonjs還有一個exports屬性(不推薦使用), 事實上exports就是module.export

// 對外輸出介面可以新增變數
var exports = module.exports;
exports.area = function (r) {
  return Math.PI * r * r;
};

exports.circumference = function (r) {
  return 2 * Math.PI * r;
};

// 注意不要直接對exports賦值,這樣會切斷exports和module的關係
exports = a // 不要這麼做
複製程式碼

Module簡介

ES6的Module是官方正式推出的模組化寫法,雖然目前有挺多瀏覽器還不支援,不過我們可以利用babel將其轉換,話不多說,先介紹下Module的基本用法。

ES6的module主要是以import匯入想要的物件,export 和 export default匯出物件

import x from 'some.js'  // 引用some.js中的export default
import {a, b} from 'some.js'  // 引用some.js的 export a 和 export b
import x, {a, b} from 'some.js'  // 引用 some.js的 export default 和 export a 和 export b

// some.js
const x = () => {}

export const a = () => {}
export const b = () => {}

export default x
複製程式碼

因為import是編譯時載入,所以import命令具有提升效果,會提升到整個模組的頭部,首先執行。

// some code
...
...

import xxx from 'xxx' // 提升到最頂部

複製程式碼

Common和Module的區別

1. 載入的時機不同

Common是執行時載入的,可以使用變數或者表示式,如:

 const 'f' + 'oo' =  require('my_modules')
複製程式碼

Module是編譯時載入的,不可以使用變數或者表示式, 編譯時載入效率較高。

2.暴露出的介面不同

Common暴露出來的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。

// lib.js
var counter = 3;
function incCounter() {
  counter++;
}
module.exports = {
  counter: counter,
  incCounter: incCounter,
}; 
複製程式碼
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;

console.log(counter);  // 3
incCounter();
console.log(counter); // 3
複製程式碼

Module則相反, 輸出的是值的引用。

Module與webpack

webpack本身維護了一套模組系統,這套模組系統相容了所有前端歷史程式下的模組規範,包括 amd commonjs es6 等,為了看module在webpack中是怎麼執行的,我們可以看一下下面簡單的程式碼:

// webpack

const path = require('path');

module.exports = {
  entry: './a.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
  }
};

複製程式碼
// a.js
import a from './c';

export default 'a.js';
console.log(a);
複製程式碼
// c.js

export default 333;
複製程式碼

打包後的程式碼如下:

(function(modules) {

  
  function __webpack_require__(moduleId) {
    var module =  {
      i: moduleId,
      l: false,
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    return module.exports;
  }

  return __webpack_require__(0);
})([
  (function (module, __webpack_exports__, __webpack_require__) {

    // 引用 模組 1
    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__c__ = __webpack_require__(1);

/* harmony default export */ __webpack_exports__["default"] = ('a.js');
console.log(__WEBPACK_IMPORTED_MODULE_0__c__["a" /* default */]);

  }),
  (function (module, __webpack_exports__, __webpack_require__) {

    // 輸出本模組的資料
    "use strict";
    /* harmony default export */ __webpack_exports__["a"] = (333);
  })
]);


複製程式碼

簡化一波程式碼再看,可以看出打包後實際上是一個立即執行函式,並且入參為各個module檔案, 最後返回的是__webpack_require__(0)

(function(modules) {

  
  function __webpack_require__(moduleId) {
  }

  return __webpack_require__(0);
})([module1, module2]);
複製程式碼

ok, 我們繼續看__webpack_require__函式,可以看出它是呼叫了我們的入口模組,同時傳入了module相關的屬性,以及函式本身

function __webpack_require__(moduleId) {
    var module =  {
      i: moduleId,
      l: false,
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    return module.exports;
  }

複製程式碼

那麼繼續追溯到入口模組,也就是我們的第一個引數我們可以看到入口模組又呼叫了 webpack_require(1) 去引用入引數組裡的第2個函式。 然後會將入參的 webpack_exports 物件新增 default 屬性,並賦值。 這裡我們就能看到模組化的實現原理,這裡的 webpack_exports 就是這個模組的 module.exports 通過物件的引用傳參,間接的給 module.exports 新增屬性。 最後會將 module.exports return 出來。就完成了 webpack_require 函式的使命。

function (module, __webpack_exports__, __webpack_require__) {

/* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__c__ = __webpack_require__(1);

    /* harmony default export */ __webpack_exports__["default"] = ('a.js');
    console.log(__WEBPACK_IMPORTED_MODULE_0__c__["a" /* default */]);

  }

複製程式碼

至此,我們可以看出module其實在webpack中,最後的打包結果。

Module與Babel

雖然webpack可以打包轉換我們的module,但通常我們都會引入babel來對ES6轉成ES5的程式碼,而Moduel屬於ES6,也會被轉譯。

事實上,babel是將module轉換成commonjs,這樣 webpack 就無需再做處理,直接使用 webpack 執行時定義的 webpack_require 處理。

不過babel在轉換的時候,會有一些特殊的處理, 像下面

首先 export 的時候, 會新增一個__esModule屬性到exports,是為了表明這是經過轉換的module

export default a

// 轉換成  
Object.defineProperty(exports, "__esModule", {
  value: true
});

exports.default = a;

複製程式碼

再看 轉出的
轉出其實會多一個_interopRequireDefault函式,就是為了處理default這個屬性

import d from 'd'  
// 轉化後
var _d = require('d');

var _d2 = _interopRequireDefault(_d);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
複製程式碼

一些問題

1.為什麼有的地方使用 require 去引用一個模組時需要加上 default?

我們在上文 babel 對匯出模組的轉換提到,es6 的 export default 都會被轉換成 exports.default,即使這個模組只有這一個輸出。

2.經常在各大UI元件引用的文件上會看到說明 import { button } from 'xx-ui' 這樣會引入所有元件內容,需要新增額外的 babel 配置,比如 babel-plugin-component?

import { Button, Select } from 'element-ui'
// 轉換成
var a = require('element-ui');
var Button = a.Button;
var Select = a.Select;
複製程式碼

babel-plugin-component就做了一件事,將 import { Button, Select } from 'element-ui' 轉換成了

import Button from 'element-ui/lib/button'
import Select from 'element-ui/lib/select'  
複製程式碼

3.我們在瀏覽一些 npm 下載下來的 UI 元件模組時(比如說 element-ui 的 lib 檔案下),看到的都是 webpack 編譯好的 js 檔案,可以使用 import 或 require 再去引用。但是我們平時編譯好的 js 是無法再被其他模組 import 的,這是為什麼?

通過 webpack 模組化原理章節給出的 webpack 配置編譯後的 js 是無法被其他模組引用的,webpack 提供了 output.libraryTarget 配置指定構建完的 js 的用途。入口模組返回的 module.exports 賦值給 module.exports

總結

在剖析了整體的流程之後,可以看到相關的技術細節還是比較清晰的,學無止境~~~

引用

import、require、export、module.exports 混合使用詳解
Module的語法
前端模組化
Commonjs規範

相關文章