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

本文的初衷是來實現一個咱們工做中最經常使用的構建工具,webpack,固然咱們所實現的構建工具和真正的webpack差距甚遠。這裏只是簡單一個實現罷了,感興趣的同窗能夠繼續看下去。css

不過在說自動化構建以前仍是要訴說一下模塊化開發的發展史,這是前端er都經歷過的一段歷史,值得咱們和全部人再去回顧一番!!!前端

模塊化

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

命名空間

開發網頁要經過命名空間的方式來組織代碼jquery

<script src="jquery.js">
複製代碼
  • 命名空間衝突,兩個庫可能會使用同一個名稱
  • 沒法合理的管理項目的依賴和版本
  • 沒法方便的控制依賴的加載順序

CommonJS

CommonJS是一種使用普遍的JavaScript模範化管理,核心思想是經過require方法來同步地加載依賴的其餘模塊,經過module.exports導出須要暴露的接口webpack

用法

採用CommonJS導入及導出時的代碼以下:es6

// 導入
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規範主要是爲了解決針對瀏覽器環境的模塊化問題,最具表明性的實現是RequireJSweb

AMD的優勢npm

  • 可在不轉換代碼的狀況下直接在瀏覽器裏運行
  • 可加載多個依賴
  • 代碼可運行在瀏覽器環境和Node環境中

AMD的缺點json

  • 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實現了!

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

相關文章
相關標籤/搜索