本文的初衷是來實現一個咱們工做中最經常使用的構建工具,webpack,固然咱們所實現的構建工具和真正的webpack差距甚遠。這裏只是簡單一個實現罷了,感興趣的同窗能夠繼續看下去。css
不過在說自動化構建以前仍是要訴說一下模塊化開發的發展史,這是前端er都經歷過的一段歷史,值得咱們和全部人再去回顧一番!!!前端
模塊化是指把一個複雜的系統分解到多個模塊以方便編寫node
開發網頁要經過命名空間的方式來組織代碼jquery
<script src="jquery.js">
複製代碼
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也是一種JavaScript模塊化規範,與CommonJS最大的不一樣在於它採用異步的方式去加載依賴的模塊。 AMD規範主要是爲了解決針對瀏覽器環境的模塊化問題,最具表明性的實現是RequireJSweb
AMD的優勢npm
AMD的缺點json
// 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須要哪一個模塊的時候就把該函數執行,而後將執行後的結果傳到回調裏
// 導入
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是一個打包模塊化JS的工具,在webpack裏一切文件皆模塊,經過loader轉換文件,經過plugin注入鉤子,最後輸出由多個模塊組合成的文件。webpack 專一於構建模塊化項目
安裝到項目中
// 安裝最新版
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而致使衝突
默認狀況下咱們會將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?");
})
});
複製代碼
試試就試試,根據打包後的核心代碼咱們也來實現一個看看,來弄一個相似webpack腳手架,廢話很少說,搞起來
// pack目錄
pack -
- bin
- pack.js
複製代碼
上面幾項說的是一個總體流程,接下來咱們開始實現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其實就是函數,咱們加載個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實現了!
感謝各位的觀看了。此致,敬禮!