如何本身實現一個Webpack

前言

深刻理解 webpack 打包機制。node

模塊分析

依賴環境 node.jswebpack

  1. 建立文件,最簡單的字符串拼接
// index.js

import { message } from './message.js'
console.log(message)

// message.js

import { word } from './word.js'
export const message = `say ${ word }`

// word.js

export const word = 'hello world !'
複製代碼
  1. 編寫 bundler.js 文件

須要安裝以下 babelweb

  1. @babel/parser: 分析咱們經過 fs.readFileSync 讀取的文件內容,返回 AST (抽象語法樹)
  2. @babel/traverse: 能夠遍歷 AST, 拿到必要的數據
  3. @babel/core: babel 核心模塊,其有個transformFromAst方法,能夠將 AST 轉化爲瀏覽器能夠運行的代碼
  4. @babel/preset-env: 將代碼轉化成 ES5 代碼
const fs = require('fs');
const path = require('path')
const parser = require('@babel/parser'); // 幫助咱們分析字符串塊,即咱們經過 fs 來讀取文件時遇到的 import、export 語法
const traverse = require('@babel/traverse').default // traverse 採用的 ES Module 導出,咱們經過 requier 引入的話就加個 .default
const babel = require('@babel/core');

const moduleAnalyser = (filename) => {
	const content = fs.readFileSync(filename, 'utf-8');

	// parser 解析咱們的 content 以後會返回一個 AST (抽象語法樹)
	const AST = parser.parse(content, {
		sourceType: "module"
	})

	// 依賴收集
	const dependencies = {};

	// 使用 traverse 來遍歷 AST
	traverse(AST, {
		ImportDeclaration({ node }) { // 函數名是 AST 中包含的內容,參數是一些節點,node 表示這些節點下的子內容
			const dirname = path.dirname(filename); // 咱們從抽象語法樹裏面拿到的路徑是相對路徑,而後咱們要處理它,在 bundler.js 中才能正確使用
			const newDirname = './' + path.join(dirname, node.source.value).replace('\\', '/'); // 將dirname 和 獲取到的依賴聯合生成絕對路徑
			dependencies[node.source.value] = newDirname; // 將源路徑和新路徑以 key-value 的形式存儲起來
		}
	})

	// 將抽象語法樹轉換成瀏覽器能夠運行的代碼
	const { code } = babel.transformFromAst(AST, null, {
		presets: ['@babel/preset-env']
	})

	return {
		filename,
		dependencies,
		code
	}
}

// 建立依賴圖譜函數, 遞歸遍歷全部依賴模塊
const makeDependenciesGraph = (entry) => {
	const entryModule = moduleAnalyser(entry)
	const graghArray = [ entryModule ]; // 首先將咱們分析的入口文件結果放入圖譜數組中
	for (let i = 0; i < graghArray.length; i ++) {
		const item = graghArray[i];
		const { dependencies } = item; // 拿到當前模塊所依賴的模塊
		if (dependencies) {
			for ( let j in dependencies ) { // 經過 for-in 遍歷對象
				graghArray.push(moduleAnalyser(dependencies[j])); // 若是子模塊又依賴其它模塊,就分析子模塊的內容
			}
		}
	}

	const gragh = {}; // 將圖譜的數組形式轉換成對象形式
	graghArray.forEach( item => {
		gragh[item.filename] = {
			dependencies: item.dependencies,
			code: item.code
		}
	})
	return gragh;
}

const graghInfo = makeDependenciesGraph('./src/index.js')

console.log(graghInfo)

複製代碼

下圖graghArray的數據結構數組

  • 生成能夠運行的代碼

建立生成代碼的函數,傳入咱們的路口文件的路徑,整合以前的代碼 。瀏覽器

const generateCode = (entry) => {
	// 注意:咱們的 gragh 是一個對象,key是咱們全部模塊的絕對路徑,須要經過 JSON.stringify 來轉換
	const gragh = JSON.stringify(makeDependenciesGraph(entry));

	// 咱們知道,webpack 是將咱們的全部模塊放在閉包裏面執行的,因此咱們寫一個自執行的函數
	// 注意: 咱們生成的代碼裏面,都是使用的 require 和 exports 來引入導出模塊的,而咱們的瀏覽器是不認識的,因此須要構建這樣的函數
	return `
		(function( gragh ) {
			function require( module ) {
				// 相對路徑轉換成絕對路徑的方法
				function localRequire(relativePath) {
					return require(gragh[module].dependencies[relativePath])
				}
				const exports = {};
				(function( require, exports, code ) {
					eval(code)
				})( localRequire, exports, gragh[module].code )

				return exports;
			}
			require('${ entry }')
		})(${ gragh })
	`;
}

const code = generateCode('./src/index.js');

console.log(code)
複製代碼

總結

將代碼整合以後,在命令行運行,會打印出一段代碼,將其複製到瀏覽器中運行。bash

大功告成 !babel

相關文章
相關標籤/搜索