總所周知,現代前端,基本都離不開前端工程化,多多少少都要用上打包工具進行處理,例如webpack、rollup。不過寫得再多,也只是針對webpack/rollup的配置工程師,具體打包過程,對咱們來講倒是個黑盒。前端
不如,咱們來搞點事情?node
在構建以前咱們要梳理一下打包的流程。webpack
咱們的打包工具要有一個打包入口 [entry]
git
咱們的 [entry]
文件會引入依賴,所以須要一個 {graph}
來存儲模塊圖譜,這個模塊圖譜有3個核心內容:github
filepath
:模塊路徑,例如 ./src/message.js'
dependencies
:模塊裏用了哪些依賴code
:這個模塊中具體的代碼{graph}
會存儲全部的模塊,所以須要遞歸遍歷每個被依賴了的模塊web
根據 {graph}
的依賴路徑,用 {graph.code}
構建可運行的代碼shell
將代碼輸出到出口文件,打包完成npm
首先寫好咱們須要打包的代碼windows
// ./src/index.js
import message from './message.js';
import {word} from './word.js';
console.log(message);
console.log(word);
複製代碼
// ./src/message.js
import {word} from './word.js';
const message = `hello ${word}`;
export default message;
複製代碼
// ./src/word.js
export const word = 'paraslee';
複製代碼
而後在根目錄建立bundler.js,設計好具體要用上的功能,前端工程化
// ./bundler.js
// 模塊分析能力:分析單個模塊,返回分析結果
function moduleAnalyser(filepath) {
return {};
}
// 圖譜構建能力:遞歸模塊,調用moduleAnalyser獲取每個模塊的信息,綜合存儲成一個模塊圖譜
function moduleGraph(entry) {
const moduleMuster = moduleAnalyser(entry);
const graph = {};
return graph;
}
// 生成代碼能力:經過獲得的模塊圖譜生成可執行代碼
function generateCode(entry) {
const graph = moduleGraph(entry);
const code = '';
return code;
}
// 調用bundleCode執行打包操做,獲取到可執行代碼後,將代碼輸出到文件裏
function bundleCode() {
const code = generateCode('./src/index.js');
}
bundleCode();
複製代碼
首先是最底層的功能:moduleAnalyser(模塊分析)
由於第一個分析的模塊必定是入口文件,因此咱們在寫 moduleAnalyser 的具體內容時能夠把其餘代碼先註釋掉,單獨編寫這一塊。
function moduleAnalyser(filepath) {}
moduleAnalyser('./src/index.js');
複製代碼
首先,咱們須要讀取這個文件的信息,經過node提供的 fs
API,咱們能夠讀取到文件的具體內容
const fs = require('fs');
function moduleAnalyser(filepath) {
const content = fs.readFileSync(filePath, 'utf-8');
}
複製代碼
打印content能獲得下圖的結果
第二步,咱們須要獲取這個模塊的全部依賴,即 ./message.js
和 ./word.js
。有兩種方法能夠拿到依賴:1. 手寫規則進行字符串匹配;2. 使用babel工具操做
第一種方法實在吃力不討好,並且容錯性低,效率也不高,所以我採用第二種方法
babel有工具能幫咱們把JS代碼轉換成AST,經過對AST進行遍歷,能夠直接獲取到使用了 import 語句的內容
npm i @babel/parser @babel/traverse
複製代碼
...
const parser = require('@babel/parser');
const traverse = require("@babel/traverse").default;
const moduleAnalyser = (filePath) => {
const content = fs.readFileSync(filePath, 'utf-8');
// 經過 [@babel/parser的parse方法] 能將js代碼轉換成ast
const AST = parser.parse(content, {
sourceType: 'module' // 若是代碼使用了esModule,須要配置這一項
});
// [@babel/traverse] 能遍歷AST樹
traverse(AST, {
// 匹配 ImportDeclaration 類型節點 (import語法)
ImportDeclaration: function(nodePath) {
// 獲取模塊路徑
const relativePath = nodePath.node.source.value;
}
});
}
複製代碼
若是咱們在控制檯中打印 relativePath
就能輸出 ./message.js
和 ./word.js
AST實在是太長♂了,感興趣的小夥伴能夠本身輸出AST看看長啥樣,我就不放出來了
第三步,獲取到依賴信息後,連同代碼內容一塊兒存儲下來
下面是 moduleAnalyser的完整代碼,看完後可能會有幾個疑惑,我會針對標註處挨個進行解釋
npm i @babel/core
複製代碼
...
const babel = require('@babel/core');
const moduleAnalyser = (filePath) => {
const content = fs.readFileSync(filePath, 'utf-8');
const AST = parser.parse(content, {
sourceType: 'module'
});
// 存放文件路徑 #1
const dirname = path.dirname(filePath);
// 存放依賴信息
const dependencies = {};
traverse(AST, {
ImportDeclaration: function(nodePath) {
const relativePath = nodePath.node.source.value;
// 將相對模塊的路徑 改成 相對根目錄的路徑 #2
const absolutePath = path.join(dirname, relativePath);
// replace是爲了解決windows系統下的路徑問題 #3
dependencies[relativePath] = './' + absolutePath.replace(/\\/g, '/');
}
});
// 用babel將AST編譯成可運行的代碼 #4
const {code} = babel.transformFromAst(AST, null, {
presets: ["@babel/preset-env"]
})
return {
filePath,
dependencies,
code
}
}
複製代碼
#1 爲何要獲取dirname?
首先入口文件爲 ./src/index.js,默認 ./src 爲代碼的根目錄,全部依賴,全部模塊文件都在 ./src 下面 (暫時先不考慮node_modules),所以咱們要獲取這個根目錄信息, dirname === 'src'
#2 爲何要把相對模塊的路徑 改成 相對根目錄的路徑
在 ./src/index.js 中是這樣引入模塊的 import message from './message.js'
,relativePath 變量存儲的值爲 ./message.js
,這對於分析 message.js 文件很是不便,轉換成 ./src/message.js
後就能直接經過fs讀取這個文件,方便了不少
#3 爲何要這樣存儲依賴信息
經過鍵值對的存儲,既能夠保留 將相對模塊的路徑 ,又能夠存放 相對根目錄的路徑
#4 爲何要作代碼編譯
代碼編譯能夠將esModule轉換成commonjs,以後構建代碼時,咱們能夠編寫本身的 require()
方法進行模塊化處理.
OK,如今已經理解了 moduleAnalyser 方法,讓咱們看看輸出結果長啥樣
如今已經實現了 模塊分析能力,接下來咱們須要遞歸全部被導入了的模塊,將每一個模塊的分析結果存儲下來做爲 grapth
...
const moduleAnalyser = (filePath) => {...}
const moduleGraph = (entry) => {
// moduleMuster 存放已經分析過的模塊集合, 默認直接加入入口文件的分析結果
const moduleMuster = [moduleAnalyser(entry)];
// cache記錄已經被分析過的模塊,減小模塊的重複分析
const cache = {
[moduleMuster[0].filePath]: 1
};
// 存放真正的graph信息
const graph = {};
// 遞歸遍歷全部的模塊
for (let i = 0; i < moduleMuster.length; i++) {
const {filePath} = moduleMuster[i];
if (!graph[filePath]) {
const {dependencies, code} = moduleMuster[i];
graph[filePath] = {
dependencies,
code
};
for (let key in dependencies) {
if (!cache[dependencies[key]]) {
moduleMuster.push(moduleAnalyser(dependencies[key]));
cache[dependencies[key]] = 1;
}
}
}
}
return graph;
}
// 先直接傳入enrty信息,獲取圖譜信息
moduleGraph('./src/index.js');
複製代碼
moduleGraph 方法並不難理解,主要內容在遞歸層面。
輸出看看最終生成的圖譜 graph
接下來就是重點中的重點,核心中的核心:根據graph生成可執行代碼
...
const moduleAnalyser = (filePath) => {...}
const moduleGraph = (entry) => {...}
const generateCode = (entry) => {
// 代碼在文件裏實際上是一串字符串,瀏覽器是把字符串轉換成AST後再操做執行的,所以這裏須要把圖譜轉換成字符串來使用
const graph = JSON.stringify(moduleGraph(entry));
return ` (function(graph) { // 瀏覽器沒有require方法,須要自行建立 function require(module) { // 代碼中引入模塊時是使用的相對模塊的路徑 ex. var _word = require('./word.js') // 但咱們在引入依賴時須要轉換成相對根路徑的路徑 ex. require('./src/word.js') // 將requireInEval傳遞到閉包中供轉換使用 function requireInEval(relativePath) { return require(graph[module].dependencies[relativePath]); } // 子模塊的內容存放在exports中,須要建立空對象以便使用。 var exports = {}; // 使用閉包避免模塊之間的變量污染 (function(code, require, exports) { // 經過eval執行代碼 eval(code); })(graph[module].code, requireInEval, exports) // 將模塊所依賴的內容返回給模塊 return exports; } // 入口模塊需主動引入 require('${entry}'); })(${graph})`;
}
generateCode('./src/index.js');
複製代碼
此刻一萬匹草泥馬在心中狂奔:這破函數到底寫的是個啥玩意兒? 給爺看暈了
GGMM不着急,我來一步步說明
首先把字符串化的graph傳入閉包函數中以供使用。
而後須要手動導入入口文件模塊,即 require('${entry}')
,注意這裏要使用引號包裹,確保爲字符串
所以咱們的 require 函數此時爲
function require(module = './src/index.js') {}
複製代碼
根據 graph['./src/index.js']
能獲取到 入口文件的分析結果,
function require(module = './src/index.js') {
(function(code) {
// 經過eval執行代碼
eval(code);
})(graph[module].code)
}
複製代碼
而後咱們看一下此時eval會執行的代碼,即入口文件編譯後的代碼
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _word = require("./word.js");
var message = "hello ".concat(_word.word);
var _default = message;
exports["default"] = _default;
複製代碼
在第六行裏有一句 var _word = require("./word.js");
,由於做用域鏈的存在,這裏的require會調用最外層的require方法, 可是咱們本身寫的require方法接受的是相對根目錄的路徑,所以須要有一個方法進行轉換。
// 此時的require
function require(module = './src/index.js') {
function requireInEval(relativePath = './word.js') {
return require(graph[module].dependencies[relativePath]);
}
var exports = {};
(function(code, require, exports) {
eval(code);
})(graph[module].code, requireInEval, exports)
return exports;
}
複製代碼
經過 requireInEval 進行路徑轉換,並傳入到閉包當作,根據做用域的特性,eval中執行的 require 爲傳入的requireInEval 方法。
eval執行時,會把依賴裏的數據存放到exports對象中,所以在外面咱們也須要建立一個exports對象接受數據。
最後將exports返回出去
以後就是重複以上步驟的循環調用
如今,打包流程基本已經完成了,generateCode 方法返回的code代碼,能夠直接放到瀏覽器中運行。
不過好歹是個打包工具,確定要把打包結果輸出出來。
const os = require('os'); // 用來讀取系統信息
...
const moduleAnalyser = (filePath) => {...}
const moduleGraph = (entry) => {...}
const generateCode = (entry) => {...}
function bundleCode(entry, output) {
// 獲取輸出文件的絕對路徑
const outPath = path.resolve(__dirname, output);
const iswin = os.platform() === 'win32'; // 是不是windows
const isMac = os.platform() === 'darwin'; // 是不是mac
const code = generateCode(entry);
// 讀取輸出文件夾
fs.readdir(outPath, function(err, files) {
// 若是沒有文件夾就建立文件夾
let hasDir = true;
if (err) {
if (
(iswin && err.errno === -4058)
|| (isMac && err.errno === -2)
) {
fs.mkdir(outPath, {recursive: true}, err => {
if (err) {
throw err;
}
});
hasDir = false;
} else {
throw err;
}
}
// 清空文件夾裏的內容
if (hasDir) {
files = fs.readdirSync(outPath);
files.forEach((file) => {
let curPath = outPath + (iswin ? '\\' :"/") + file;
fs.unlinkSync(curPath);
});
}
// 將代碼寫入文件,並輸出
fs.writeFile(`${outPath}/main.js`, code, 'utf8', function(error){
if (error) {
throw error;
}
console.log('打包完成!');
})
})
}
bundleCode('./scr/index.js', 'dist');
複製代碼
執行 node bundler.js
看看最終結果吧!
到這裏,一個基礎的打包工具就完成了!
你能夠本身添加 bundler.config.js ,把配置信息添加進去,傳入bundler.js,這樣看上去更像個完整的打包工具了。
這個打包工具很是簡單,很是基礎,webpack/rollup由於涉及了海量的功能和優化,其內部實現遠比這個複雜N倍,但打包的核心思路大致相同。
這裏放上完整的代碼: github
若是文中有錯誤/不足/須要改進/能夠優化的地方,但願能在評論裏提出,做者看到後會在第一時間裏處理
既然你都看到這裏了,爲什麼不點個贊👍再走,github的星星⭐是對我持續創做最大的支持❤️️
![]()
拜託啦,這對我真的很重要