本文的初衷是來實現一個我們工作中最常用的構建工具,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實現了!
感謝各位的觀看了。此致,敬禮!