進擊的模組化+webpack的簡單實現

chenhongdong發表於2018-04-08

本文的初衷是來實現一個我們工作中最常用的構建工具,webpack,當然我們所實現的構建工具和真正的webpack差距甚遠。這裡只是簡單一個實現罷了,感興趣的同學可以繼續看下去。

不過在說自動化構建之前還是要訴說一下模組化開發的發展史,這是前端er都經歷過的一段歷史,值得我們和所有人再去回顧一番!!!

模組化

模組化是指把一個複雜的系統分解到多個模組以方便編寫

名稱空間

開發網頁要通過名稱空間的方式來組織程式碼

<script src="jquery.js">
複製程式碼
  • 名稱空間衝突,兩個庫可能會使用同一個名稱
  • 無法合理的管理專案的依賴和版本
  • 無法方便的控制依賴的載入順序

CommonJS

CommonJS是一種使用廣泛的JavaScript模範化管理,核心思想是通過require方法來同步地載入依賴的其他模組,通過module.exports匯出需要暴露的介面

用法

採用CommonJS匯入及匯出時的程式碼如下:

// 匯入
const A = require('./a.js');
fn();
// 匯出
module.exports = A.fn;
複製程式碼
原理實現
// a.js
module.exports = '剛好遇見你';

//b.js
const fs = require('fs');
// CommonJS簡單實現
function req(pathName) {
    // content代表的是檔案內容
    let content = fs.readFileSync(pathName, 'utf8');
    // 最後一個引數是函式的內容體
    let fn = new Function('exports', 'require', 'module', '__filename', '__dirname', content+'\n return module.exports');
    let module = {
        exports: {}
    };
    // 函式執行就可以取到module.exports的值了
    return fn(module.exports, req, module, __filename, __dirname);
}
const str = req('./a.js');  // 匯入a模組
console.log(str);   // '剛好遇見你'
複製程式碼

AMD

AMD也是一種JavaScript模組化規範,與CommonJS最大的不同在於它採用非同步的方式去載入依賴的模組。 AMD規範主要是為了解決針對瀏覽器環境的模組化問題,最具代表性的實現是RequireJS

AMD的優點

  • 可在不轉換程式碼的情況下直接在瀏覽器裡執行
  • 可載入多個依賴
  • 程式碼可執行在瀏覽器環境和Node環境中

AMD的缺點

  • Js執行環境沒有原生支援AMD,需要先匯入實現了AMD的庫才能正常使用(這裡指的是RequireJS)
用法
// define定義模組
define('song', [], () => {
    return '告白氣球';
});
define('singer', ['song', 'album'], (song, album) => {  // 依賴了song和album模組
    let singer = '周杰倫';
    return `${singer}${song}屬於專輯《${album}》`;
});
define('album', [], () => {
    return '床邊故事';
});
// require使用模組
require(['singer'], singer => {
    console.log(singer);   // 周杰倫的告白氣球屬於專輯床邊故事
});
複製程式碼
原理實現

RequireJS有兩個方法,一個是define,另一個是require,所以首先我們先定義兩個函式,看如下程式碼

let factories = {};     // 管理一個關聯物件,將模組名和函式關聯起來
// 定義模組define  三個引數:1.模組名 2.依賴 3.工廠函式
function define(name, depend, factory) {
    factories[name] = factory;
    factory.depend = depend;    // 將依賴記到factory上
}
// 通過require使用模組
function require(modules, callback) {
    let result = modules.map(mod => {   // 返回一個結果陣列
        let factory = factories[mod];   // 拿到模組對應的函式
        let exports;
        let depend = factory.depend;    // 取到函式上的依賴 ['a']
        
        // require(['song','album'], function(song,album) {})  可能會有很多依賴
        require(depend, () => {         // 遞迴require
            exports = factory.apply(null, arguments);
        });
        return exports;     // exports得到的是函式返回的值  如:'告白氣球' , ' 床邊故事'
    });
    callback.apply(null, result);   //  result為一個結果陣列,所以用apply
}
複製程式碼

★ define作用就是把定義模組的函式保留下來

★ require需要哪個模組的時候就把該函式執行,然後將執行後的結果傳到回撥裡

ES6 模組化

  • ES6模組化是ECMA提出的JS模組化規範,它在語言的層面上實現了模組化
  • 最主要的是它將取代CommonJS和AMD規範,成為瀏覽器和伺服器通用的模組解決方案
// 匯入
import {each, ...} from 'underscore.js';	// es6 按需引入
var _ = require('underscore.js');			// amd 全域性引入

// 匯出
export {each, map, ...};	// es6 多點暴露
module.exports = _;		// amd 全域性暴露
複製程式碼

小遺憾:ES6模組雖然是終極模組化方案,但它的缺點在於目前無法直接執行在大部分 JavaScript 執行環境下,必須通過工具轉換成標準的 ES5 後才能正常執行

好了,以上內容就是模組化大致的發展歷程

如今我們在工作中開始大量使用ES6等先進的語法來開發專案了,但是正如ES6模組化的“小遺憾”一樣,還有一些環境下並不能支援,所以本著為國為民的態度,我們還需將其轉化為能夠識別的程式碼

正因如此慢慢的出現了自動化構建,簡單來說,就是把原始碼轉換成釋出到線上的可執行JS、CSS、HTML 程式碼,當然這是最主要的目的,除此之外還有很多用途(檔案優化、自動重新整理、模組合併、程式碼校驗等),這裡就不一一細說了。我們直接進入主題,來說說webpack這個目前構建工具中的爆款吧

webpack

webpack是一個打包模組化JS的工具,在webpack裡一切檔案皆模組,通過loader轉換檔案,通過plugin注入鉤子,最後輸出由多個模組組合成的檔案。webpack 專注於構建模組化專案

安裝webpack

安裝到專案中

  • 需要先在專案中npm init初始化一下
  • 建議node版本安裝到8.2以上
// 安裝最新版
npm i webpack -D
// 安裝指定版本
npm i webpack@<version> -D
// 目前webpack4已經發布 這裡的安裝需要多一個webpack-cli
npm i webpack webpack-cli -D
複製程式碼

★ npm i -D 是 npm install --save-dev 的簡寫,是指安裝模組並儲存到 package.json 的 devDependencies

安裝到全域性

npm i webpack -g
複製程式碼

注意:推薦安裝到當前專案,原因是可防止不同專案依賴不同版本的webpack而導致衝突

使用webpack

預設情況下我們會將src下的入口檔案進行打包

// node v8.2版本以後都會有一個npx
// npx會執行bin裡的檔案
npx webpack     // 不設定mode的情況下 打包出來的檔案自動壓縮

// 設定mode為開發模式,打包後的檔案不被壓縮
npx webpack --mode development
複製程式碼

這裡使用webpack打包編譯,是針對src目錄下的預設檔案index.js來做的處理

原始檔目錄結構
src -
    - index.js
    - a.js
    
// a.js
module.exports = '剛好遇見你';
// index.js
let str = require('./a.js');
console.log(str);
複製程式碼

程式碼打包後會生成一個dist目錄,並建立一個main.js,將打包後的程式碼放在其中,那麼我們就來看看打包後的程式碼,到底是何方神聖

編譯後的樣子

打包後目錄結構
dist -
     - main.js
     
// 下面來看下內部,取其精華
(function (modules) {
    function require(moduleId) {    // moduleId代表的是檔名
        var module = {
            exports: {}
        };
        modules[moduleId].call(module.exports, module, module.exports, require);
        return module.exports;
    }
    return require("./src/index.js");
})
({
    "./src/index.js": (function (module, exports, require) {
        eval("let str =  require(/*! ./a.js */ \"./src/a.js\");\n\nconsole.log(str);\n\n//# sourceURL=webpack:///./src/index.js?");
    }),
    "./src/a.js": (function (module, exports) {
        eval("module.exports = '剛好遇見你';\n\n//# sourceURL=webpack:///./src/a.js?");
    })
});
複製程式碼
  • 整體來說還是包在了一個自執行函式內,函式中的引數modules,其實就是下面()內的物件{}。
  • modules[moduleId].call()這段程式碼其實就是將下面()內對應的key執行,modules['./src/index.js'] ()得到eval解析的程式碼

寫一個試試

試試就試試,根據打包後的核心程式碼我們也來實現一個看看,來弄一個類似webpack腳手架,廢話不多說,搞起來

// pack目錄
pack -
     - bin
        - pack.js
複製程式碼
  • 首先我們先建立一個資料夾叫pack,裡面有對應的檔案
  • 然後我們希望在命令列裡直接執行pack命令就可以進行打包
    • 必須是個模組才可以執行
    • 在pack目錄下npm init -y
    • 初始化後的package.json檔案中將bin下的pack路徑改成"bin/pack.js"
    • 將pack.js的命令引用到npm全域性下,再執行pack命令的時候就可以直接使用
    • 在pack目錄下執行npm link就可將pack的包放到了npm全域性下(mac下需要加sudo)
    • 每次修改pack.js後,都需要重新npm link一下
  • 命令列中再次執行pack便可以打包了

上面幾項說的是一個整體流程,接下來我們開始實現pack.js裡的主要邏輯

// pack.js
#! /usr/bin/env node    
// 寫上面這句話是告訴檔案是在node下執行,不然會報錯無法編譯
let entry = './src/index.js';   // 入口檔案
let output = './dist/main.js'   // 出口檔案
let fs = require('fs');
let path = require('path');
let script = fs.readFileSync(entry, 'utf8');
let results = [];
// 如果有require引用的依賴,那就需要替換處理依賴
script = script.replace(/require\(['"](.+?)['"]\)/g, function() {
    let name = path.join('./src/',  arguments[1]);     // ./src/a.js
    let content = fs.readFileSync(name, 'utf8');
    results.push({
        name,
        content
    });
    return `require('${name}')`;    // require('./src/a.js')
});
// 用ejs可以實現內容的替換
let ejs = require('ejs');

// 這裡的模板其實就是dist/main.js裡的核心程式碼
let template = `
    (function (modules) {
    function require(moduleId) {
        var module = {
            exports: {}
        };
        modules[moduleId].call(module.exports, module, module.exports, require);
        return module.exports;
    }
    return require("<%-entry%>");
})
    ({
        "<%-entry%>": (function (module, exports, require) {
            eval(\`<%-script%>\`);
        })
        <%for(let i=0;i<results.length;i++){
            let mod = results[i];%>,
            "<%-mod.name%>": (function (module, exports, require) {
                eval(\`<%-mod.content%>\`);
            })
        <%}%>
    });
`;

// result為替換後的結果,最終要寫到output中
let result = ejs.render(template, {
    entry,
    script,
    results
});

try {
    fs.writeFileSync(output, result);
} catch(e) {
    console.log('編譯失敗', e);
}
console.log('編譯成功');
複製程式碼

上面用到了ejs模板引擎,下面給大家寫一下簡單的用法

let name = '周杰倫';
console.log(<a><%-name%></a>);  // <a>周杰倫</a>
複製程式碼

實現一個loader

接下來再寫一個loader吧,loader其實就是函式,我們載入個css樣式進行編譯看看。在src目錄下新增一個style.css檔案

// style.css
* {
    margin: 0;
    padding: 0;
}
body {
    background: #0cc;
}

// index.js引入css檔案
let str = require('./a.js');
require('./style.css');
console.log(str);
複製程式碼

根據程式碼新增一個style-loader去編譯css檔案

// pack.js
// 省略...
let results = [];
// loader其實就是函式
// 這裡寫一個style-loader
+ let styleLoader = function(src) {
    // src就是樣式中的內容
    return `
        let style = document.createElement('style');
        style.innerHTML = ${JSON.stringify(src).replace(/(\\r)?\\n/g, '')};
        document.head.appendChild(style);
    `;
+ };
// 如果有require引用的依賴,那就需要替換處理依賴
script = script.replace(/require\(['"](.+?)['"]\)/g, function() {
    let name = path.join('src',  arguments[1]);     // ./src/a.js
    let content = fs.readFileSync(name, 'utf8');
    // 如果有css檔案,就進行編譯
+   if (/\.css$/.test(name)) {
+       content = styleLoader(content);
+   }

    results.push({
        name,
        content
    });
    return `require('${name}')`;    // require('./src/a.js')
});
複製程式碼

這裡用JSON.stringify處理字串不能換行的問題,如下程式碼

body {
    background: #0cc;
}
複製程式碼

但是有個小瑕疵就是會帶上換行符,所以replace這裡來處理stringify後的\r\n換行符(mac下只有\n)

寫到這裡,一個簡單的編譯工具就完成了,這個功能雖然很不完善,但是也是開拓一下視野去實現一下我們常用的webpack是如何從0到1的過程。

當然我們實現的這個不能和成熟的webpack去比較,而且webpack的實現不僅於此。我也在嘗試著繼續研究webpack如何實現,像涉及到的ast這些內容,也在慢慢學習中。

希望下一次再寫的時候會給各位觀眾一個更高大上的webpack實現了!

感謝各位的觀看了。此致,敬禮!

相關文章