webpack bundle实现 打包原理 webpack(二)

   日期:2020-08-24     浏览:105    评论:0    
核心提示:前言这篇博客将会从分析原理到实操写出一个小型的bundle我觉得,想去实现一个东西,不能立刻去敲代码,而是要观察,分析出它的特点,一步步分析,然后跟着这一步步分析,去实现分析出来的每一点,这是一个循序渐进的过程。要分析webpack,肯定要会最简单的打包了,如果不会的同学可以移步这里webpack是什么?以及安装和运行 webpack(一)此篇中,我会给每个分析得出的结论一个二级标题,以便你们迅速查阅(所以不能作为标题去看,而是要依次看下去)搭建分析环境首先我们从最简单的一个打印开始(以防万一,

前言

这篇博客将会从分析原理到实操写出一个小型的bundle

我觉得,想去实现一个东西,不能立刻去敲代码,而是要观察,分析出它的特点,一步步分析,然后跟着这一步步分析,去实现分析出来的每一点,这是一个循序渐进的过程。

要分析webpack,肯定要会最简单的打包了,如果不会的同学可以移步这里webpack是什么?以及安装和运行 webpack(一)

此篇中,我会给每个分析得出的结论一个二级标题,以便你们迅速查阅(所以不能作为标题去看,而是要依次看下去)

搭建分析环境

首先我们从最简单的一个打印开始(以防万一,还是从头开始创建)

// 在新创建的文件夹中
npm init -y // 生成package.json文件
npm install webpack webpack-cli -D // 安装webpack

执行完以上命令后,文件夹中会多出一个package.json文件和node_modules文件夹

此时,创建src文件夹,并在src文件夹下创建index.js

// src/index.js
console.log('hello webpack')

再创建一个webpack.config.js文件(这是webpack打包的配置文件)

// webpack.config.js
const path = require('path')

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'bundle.js'
  }
}

此时分析环境搭建完毕,文件目录如下

webpack分析

可以通过执行如下命令,对仅一行的打印进行打包

npx webpack // npx会在当局node_modules中搜索,也就是不会执行全局的webpack

此时,会在文件夹下生成一个dist文件夹,并在dist文件夹中会有一个bundle.js文件

好,在这停顿,现在是思考的时候了:为什么会在运行webpack打包指令后,生成一个dist文件夹和一个bundle.js文件呢?

相信很多人也知道这个问题的答案:在webpack执行打包时,会去寻找一个叫webpack.config.js的文件(这是它的打包配置文件),因此,这里会寻找到当前文件夹中的webpack.config.js.

const path = require('path')

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, './dist'),
    filename: 'bundle.js'
  }
}

而此时的webpack.config.js中,已经设置好了entry(入口文件),output(输出的路径和文件名)这两个options了,所以webpack会获取到这些配置信息,然后根据这些配置信息去启动webpack,然后生成相应文件夹和文件

当然别忘了,我们是为了干什么才来的,为了分析!

所以从这里我们能得出的结论是什么呢?

结论一

结论一:(前半部分)webpack首先会去获取配置信息, 根据配置信息启动Webpack, 执行构建。(后半部分)并根据配置信息生成对应文件夹和文件

接着开始分析打包出的bundle.js

 (function(modules) { // webpackBootstrap
 	// The module cache
 	var installedModules = {};

 	// The require function
 	function __webpack_require__(moduleId) {

 		// Check if module is in cache
 		if(installedModules[moduleId]) {
 			return installedModules[moduleId].exports;
 		}
 		// Create a new module (and put it into the cache)
 		var module = installedModules[moduleId] = {
 			i: moduleId,
 			l: false,
 			exports: {}
 		};

 		// Execute the module function
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

 		// Flag the module as loaded
 		module.l = true;

 		// Return the exports of the module
 		return module.exports;
 	}


 	// expose the modules object (__webpack_modules__)
 	__webpack_require__.m = modules;

 	// expose the module cache
 	__webpack_require__.c = installedModules;

 	// define getter function for harmony exports
 	__webpack_require__.d = function(exports, name, getter) {
 		if(!__webpack_require__.o(exports, name)) {
 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
 		}
 	};

 	// define __esModule on exports
 	__webpack_require__.r = function(exports) {
 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
 		}
 		Object.defineProperty(exports, '__esModule', { value: true });
 	};

 	// create a fake namespace object
 	// mode & 1: value is a module id, require it
 	// mode & 2: merge all properties of value into the ns
 	// mode & 4: return value when already ns object
 	// mode & 8|1: behave like require
 	__webpack_require__.t = function(value, mode) {
 		if(mode & 1) value = __webpack_require__(value);
 		if(mode & 8) return value;
 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
 		var ns = Object.create(null);
 		__webpack_require__.r(ns);
 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
 		return ns;
 	};

 	// getDefaultExport function for compatibility with non-harmony modules
 	__webpack_require__.n = function(module) {
 		var getter = module && module.__esModule ?
 			function getDefault() { return module['default']; } :
 			function getModuleExports() { return module; };
 		__webpack_require__.d(getter, 'a', getter);
 		return getter;
 	};

 	// Object.prototype.hasOwnProperty.call
 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

 	// __webpack_public_path__
 	__webpack_require__.p = "";


 	// Load entry module and return exports
 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })

 ({

 "./src/index.js":


 (function(module, exports) {

eval("console.log('hello webpack')\n\n//# sourceURL=webpack:///./src/index.js?");

 })

 });

一大串代码!当然得将其折叠起来分析


如图所示,bundle.js内部其实就是一个自执行函数,它的实参为一个对象,该对象的key值为一个路径,value值为一个函数体带eval函数的自执行函数,而eval函数包裹的内容则是./src/index.js文件中的内容,而这个文件的路径恰好就是这个实参对象的key值

这里可以得出第二点结论:

结论二

结论二:webpack打包出的其实是一个自执行函数,其接收一个对象参数,该对象key值为路径,value值为一个函数体内用eval包裹着key值代表的路径的文件内容的自执行函数

先别急着走,这可还没分析完呢,之前分析的是这个自执行函数的参数,那么现在来分析分析内部的函数体了


可以看出,其实函数体内部执行的也就是最后一句,return _webpack_require__,参数为入口文件路径,这里为什么会出现一个 _webpack_require__函数呢?

其实这是代替了require而已,因为这打包出来的bundle.js文件是要在浏览器中解析运行的,可require是不会被浏览器解析的,所以webpack内部使用_webpack_require__实现了一个自己的require,这使得浏览器可以解析

而正是因为这一句执行,也造就了webpack从入口文件开始分析的这一现象

结论三

结论三:(前半部分)webpack打包出的bundle.js中,自己实现了一个require函数,并返回以入口文件路径为参数的require函数的执行结果。(后半部分)并且webpack从入口文件开始分析

渐入佳境了,最简单一个打印已经分析完毕了,这时候就要引入import

在src文件夹下新建一个sayHi.js

// src/sayHi.js
export function sayHi (str) {
  return 'hi ' + str
}
// src/index.js
import { sayHi } from './sayhi'

console.log('hello webpack ' + sayHi('xiaolu'))

再运行一次打包

npx webpack

此时文件目录如下

现在在有了import的情况下,继续来分析bundle.js

 (function(modules) { // webpackBootstrap
 	// The module cache
 	var installedModules = {};

 	// The require function
 	function __webpack_require__(moduleId) {

 		// Check if module is in cache
 		if(installedModules[moduleId]) {
 			return installedModules[moduleId].exports;
 		}
 		// Create a new module (and put it into the cache)
 		var module = installedModules[moduleId] = {
 			i: moduleId,
 			l: false,
 			exports: {}
 		};

 		// Execute the module function
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

 		// Flag the module as loaded
 		module.l = true;

 		// Return the exports of the module
 		return module.exports;
 	}


 	// expose the modules object (__webpack_modules__)
 	__webpack_require__.m = modules;

 	// expose the module cache
 	__webpack_require__.c = installedModules;

 	// define getter function for harmony exports
 	__webpack_require__.d = function(exports, name, getter) {
 		if(!__webpack_require__.o(exports, name)) {
 			Object.defineProperty(exports, name, { enumerable: true, get: getter });
 		}
 	};

 	// define __esModule on exports
 	__webpack_require__.r = function(exports) {
 		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
 			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
 		}
 		Object.defineProperty(exports, '__esModule', { value: true });
 	};

 	// create a fake namespace object
 	// mode & 1: value is a module id, require it
 	// mode & 2: merge all properties of value into the ns
 	// mode & 4: return value when already ns object
 	// mode & 8|1: behave like require
 	__webpack_require__.t = function(value, mode) {
 		if(mode & 1) value = __webpack_require__(value);
 		if(mode & 8) return value;
 		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
 		var ns = Object.create(null);
 		__webpack_require__.r(ns);
 		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
 		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
 		return ns;
 	};

 	// getDefaultExport function for compatibility with non-harmony modules
 	__webpack_require__.n = function(module) {
 		var getter = module && module.__esModule ?
 			function getDefault() { return module['default']; } :
 			function getModuleExports() { return module; };
 		__webpack_require__.d(getter, 'a', getter);
 		return getter;
 	};

 	// Object.prototype.hasOwnProperty.call
 	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

 	// __webpack_public_path__
 	__webpack_require__.p = "";


 	// Load entry module and return exports
 	return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })

 ({

 "./src/index.js":


 (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n var _sayhi__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( \"./src/sayhi.js\");\n\r\n\r\nconsole.log('hello webpack ' + Object(_sayhi__WEBPACK_IMPORTED_MODULE_0__[\"sayHi\"])('xiaolu'))\n\n//# sourceURL=webpack:///./src/index.js?");

 }),

 "./src/sayhi.js":


 (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n __webpack_require__.d(__webpack_exports__, \"sayHi\", function() { return sayHi; });\nfunction sayHi (str) {\r\n return 'hi ' + str\r\n}\n\n//# sourceURL=webpack:///./src/sayhi.js?");

 })

 });

同样折叠起来看


可以很明显的看到,在import一个文件时,此时自执行函数接收的实参对象的key:value变成了两对,并且key值为一个根路径,value值为对应路径下的文件中的内容

那么是不是可以说,webpack对import和import的路径做了处理?猜测有可能是正则匹配出import和相应路径,也有可能是通过AST(抽象语法树)来完成的解析,这里先不管,但是有一点是明确的,webpack肯定要对import和路径进行解析

结论四

结论四:webpack会对import和import的文件的路径进行解析过滤,也就是获取到import路径的值(正则或AST)

但此时还有一点,也就是key值!可以看到每次key值都是以./src/index.js和./src/sayhi.js这种形式出现的,但是在import时,我给的则是相应路径,如图


我们这里是通过相对路径引入的,但是如果webpack解析了import和路径的话,key值应该是./sayhi这样的,但实际上却是./src/sayhi.js,所以webpack内部肯定也对这个相对路径进行了处理

结论五

结论五:webpack不仅会解析import的路径,还会将其相对路径处理为根路径(比如./sayhi处理为./src/sayhi.js)

然后再仔细看value中eval内部的代码

eval("__webpack_require__.r(__webpack_exports__);\n __webpack_require__.d(__webpack_exports__, \"sayHi\", function() { return sayHi; });\nfunction sayHi (str) {\r\n return 'hi ' + str\r\n}\n\n//# sourceURL=webpack:///./src/sayhi.js?");

可以看到内部有很多__webpack_require__这种形式的,代表这里已经是webpack转换后的代码了,也就代表着这是可以在浏览器执行的代码,因此可以知道要变成这样的代码,肯定得经过一次转换

结论六

结论六:webpack会将那些浏览器不执行的给转换成浏览器可执行的函数(这跟结论三的自己实现一个require函数其实关联了)

分析完一个import后,那么再加一个import

在src下创建一个howold.js

// src/howold.js
export function howOld (str) {
  return 'How old are you? ' + str
}
// src/sayhi.js
import { howOld } from './howold'

export function sayHi (str) {
  return 'hi ' + str + '\n' + howOld(str)
}
// src/index.js
import { sayHi } from './sayhi'

console.log('hello webpack ' + sayHi('xiaolu'))

执行打包命令后

npx webpack

此时目录如下

是可以正常输出的,可以自己创建个html引入bundle.js查看

这里我们在import的sayhi文件中又import了一个howold,这次能执行成功,代表着webpack是能深度查找import的

结论七

结论七:webpack会深度查找并解析import和其路径的(首先想到就是递归遍历查找)

分析了嵌套的import后,再来分析一个文件多个import

在src目录下创建howareyou.js

// src/howareyou.js
export function howAreYou (str) {
  return 'How are you?' + str
}
// src/sayhi.js
import { howOld } from './howold'
import { howAreYou } from './howareyou'

export function sayHi (str) {
  return 'hi ' + str + '\n' + howOld(str) + '\n' + howAreYou(str)
}

其他文件均没变化
此时文件目录如下

执行打包命令后

npx webpack

运行如下图

此时,是在sayhi中出现了两次import,这次能执行成功,代表着webpack会解析所有的import

结论八

结论八:webpack会对所有的import和其路径进行解析

分析总结

好!分析的差不多了,将前面分析出的八点在这里总结一下

结论一:(前半部分)webpack首先会去获取配置信息, 根据配置信息启动Webpack, 执行构建。(后半部分)并根据配置信息生成对应文件夹和文件
结论二:webpack打包出的其实是一个自执行函数,其接收一个对象参数,该对象key值为路径,value值为一个函数体内用eval包裹着key值代表的路径的文件内容的自执行函数
结论三:(前半部分)webpack打包出的bundle.js中,自己实现了一个require函数,并返回以入口文件路径为参数的require函数的执行结果。(后半部分)并且webpack从入口文件开始分析
结论四:webpack会对import和import的文件的路径进行解析过滤,也就是获取到import路径的值(正则或AST)
结论五:webpack不仅会解析import的路径,还会将其相对路径处理为根路径(比如./sayhi处理为./src/sayhi.js)
结论六:webpack会将那些浏览器不执行的给转换成浏览器可执行的函数(这跟结论三的自己实现一个require函数其实关联了)
结论七:webpack会深度查找并解析import和其路径的(首先想到就是递归遍历查找)
结论八:webpack会对所有的import和其路径进行解析

bundle实现

实现结论一的前半部分

结论一:(前半部分)webpack首先会去获取配置信息, 根据配置信息启动Webpack, 执行构建。

从结论一前半部分可知,webpack打包时会去读取webpack.config.js中的配置信息,那么想要实现bundle,我们也必须获取到webpack.config.js中的配置信息

在文件夹下创建一个luWebpack.js
此时文件目录如下

// luWebpack.js
const options = require('./webpack.config')

console.log(options)

通过以下命令运行

node luWebpack.js

打印结果如下


此时,已经成功获取到了配置信息了

当然结论一还没完成,之后还要根据这些配置信息去启动打包,执行构建

所以在这里我打算创建一个compiler来管理配置信息并执行构建,因此创建一个lib文件夹,并在其内部创建一个compiler.js文件

此时文件目录如下

// lib/compiler
module.exports = class Compiler {
  constructor (options) {
    // 保存配置信息中的entry
    this.entry = options.entry
    // 保存配置信息中的output
    this.output = options.output
  }
  // 启动函数
  run () {
    console.log(`我拿到了配置信息,入口文件为:${this.entry},输出配置为:${JSON.stringify(this.output)},并且我启动了,之后会执行构建`)
    this.build()
  }
  // 构建函数
  build () {
    console.log(`我开始构建了`)
  }
}
// luWebpack.js
// 获取配置信息
const options = require('./webpack.config')
// 获取Compiler类 通过其保存配置信息和执行构建
const Compiler = require('./lib/compiler')
// 通过options创建compiler,并执行run启动函数
new Compiler(options).run()

执行结果如下图

在这,我创建了一个class Compiler,通过构造函数保存配置信息,并创建了启动函数和构建函数,可以看到,这里已经成功的拿到了配置信息,并启动了构建

此时结论一实现完毕,接下来先把结论二放一放,先来实现结论三的后半部分

实现结论三的后半部分

结论三:(后半部分)并且webpack从入口文件开始分析

现在已经获取到了配置信息,并且执行了构建bulid函数(虽然只是个打印),那么现在完成结论三后半部分:webpack从入口文件开始分析

其实也就是在这时读取入口文件所有的内容

要读取文件内容,当然要引入fs模块了

// lib/compiler.js
// 引入fs模块,对入口文件的内容进行读取
const fs = require('fs')

module.exports = class Compiler {
  constructor (options) {
    // 保存配置信息中的entry
    this.entry = options.entry
    // 保存配置信息中的output
    this.output = options.output
  }
  // 启动函数
  run () {
    // 执行构建函数
    this.build()
  }
  // 构建函数
  build () {
    // 读取入口文件的内容
    const content = fs.readFileSync(this.entry, 'utf-8')
    console.log(content)
  }
}

这里我们引入了fs模块,并在build构建函数中,读取了入口文件的所有内容

打印内容如下:

此时已经成功的读取到了入口文件./src/index.js的内容

结论三后半部分完成

实现结论四

结论四:webpack会对import和import的文件的路径进行解析过滤,也就是获取到import路径的值(正则或AST)

这里正则太麻烦了,我们可以使用AST来进行解析过滤,也就是将之前读取到的入口文件的内容转换为AST,并过滤出文件的路径的值,而这一步是很复杂的(来自一个正在分析vue2.0x的parse函数的可怜人的哭诉),所以这次我选择调API:在babel中已经有很一系列强大的API可以完成这一操作

所以先安装如下两个模块

// 安装@babel/parser
npm install @babel/parser -D
// 安装@babel/traverse
npm install @babel/traverse -D

关于这些用法,可以去babel官网查看

这篇博客是为了实现bundle,而不是为了实现parse,parse都可以单独写一篇博客了,所以这里只要知道怎么使用这些API将读取到的入口文件内容转换为AST并进行过滤得到import路径的值就行了

// 引入fs模块,对入口文件的内容进行读取
const fs = require('fs')
// 引入babel/parser来进行AST转换
const parser = require('@babel/parser')
// 引入babel/traverse的默认导出 对AST进行过滤解析
const tarverse = require('@babel/traverse').default

module.exports = class Compiler {
  constructor(options) {
    // 保存配置信息中的entry
    this.entry = options.entry
    // 保存配置信息中的output
    this.output = options.output
  }
  // 启动函数
  run() {
    // 执行构建函数
    this.build(this.entry)
  }
  // 构建函数
  build(fileName) {
    // 读取入口文件的内容
    const content = fs.readFileSync(fileName, 'utf-8')
    // 接受字符串模板,也就是content
    const AST = parser.parse(content, {
      // ESModule形式导入的模块
      sourceType: 'module'
    })

    tarverse(AST, {
      ImportDeclaration({node}) {
        console.log(node)
      }
    })
  }
}

这里首先以ESmodule形式将读取到的入口文件的内容转换成了AST,然后通过tarverse进行过滤,过滤的是ImportDeclaration,也就是过滤import声明的,可以看看打印的node是什么


可以看到node里面有一个source,source内部有一个value属性,值为./sayhi,这个值不就是我们import的路径的值吗,因此可以通过node.source.value获取到这个路径的值,

// 构建函数
build(fileName) {
  // 读取入口文件的内容
  const content = fs.readFileSync(fileName, 'utf-8')
  // 接受字符串模板,也就是content
  const AST = parser.parse(content, {
    // ESModule形式导入的模块
    sourceType: 'module'
  })
  tarverse(AST, {
    ImportDeclaration({node}) {
      console.log(node.source.value)
    }
  })
}

打印一下node.source.value,看是否获取到了import的路径

好的,此时import的路径过滤获取完毕,结论四实现完毕

实现结论五

结论五:webpack不仅会解析import的路径,还会将其相对路径处理为根路径(比如./sayhi处理为./src/sayhi.js)

在前面已经获取到了import的路径,而现在要实现结论五,其实就是将之前获取到的import路径和根路径进行拼接

涉及到路径操作,因此要引入path模块

// lib/compiler.js
// 引入Path模块,对import的路径进行拼接
const path = require('path')

这里面我创建了一个dependencies对象,是用来存放路径的,因为要进行路径的拼接操作得到一个新路径,所以我想用对象的key:value来保存拼接前的路径和拼接后的路径

// lib/compiler.js
// 构建函数
build(fileName) {
  // 读取入口文件的内容
  const content = fs.readFileSync(fileName, 'utf-8')
  // 接受字符串模板,也就是content
  const AST = parser.parse(content, {
    // ESModule形式导入的模块
    sourceType: 'module'
  })
  // dependencies对象,可以保留相对路径和根路径
  const dependencies = {}
  tarverse(AST, {
    ImportDeclaration({node}) {
    	
      const dirname = path.dirname(fileName)
      console.log(dirname)
      const newPath = "./" + path.join(dirname, node.source.value)
      console.log(newPath)
      dependencies[node.source.value] = newPath
      console.log(dependencies)
    }
  })
}

看看打印结果


此时已经成功地将import路径转换成了根路径了 ,结论五实现完毕

转换代码

之前把内容转换为AST后解析过滤,那么现在当然还要把AST转换回代码

因此也要引入@babel/core和@babel/preset-env

npm install @babel/core @babel/preset-env -D

这也是一个单纯的API调用,此时的lib/compiler文件

// 引入fs模块,对入口文件的内容进行读取
const fs = require('fs')
// 引入Path模块,对import的路径进行拼接
const path = require('path')
// 引入babel/parser来进行AST转换
const parser = require('@babel/parser')
// 引入babel/traverse的默认导出 对AST进行过滤解析
const tarverse = require('@babel/traverse').default
// 引入@babel/core中的transformFromAst API 把AST做转换
const { transformFromAst }  = require('@babel/core')
module.exports = class Compiler {
  constructor(options) {
    // 保存配置信息中的entry
    this.entry = options.entry
    // 保存配置信息中的output
    this.output = options.output
  }
  // 启动函数
  run() {
    // 执行构建函数
    this.build(this.entry)
  }
  // 构建函数
  build(fileName) {
    // 读取入口文件的内容
    const content = fs.readFileSync(fileName, 'utf-8')
    // 接受字符串模板,也就是content
    const AST = parser.parse(content, {
      // ESModule形式导入的模块
      sourceType: 'module'
    })

    // dependencies对象,可以保留相对路径和根路径
    const dependencies = {}
	// 过滤AST中的import声明
    tarverse(AST, {
      ImportDeclaration({node}) {
        const dirname = path.dirname(fileName)
        const newPath = "./" + path.join(dirname, node.source.value)
        dependencies[node.source.value] = newPath
      }
    })
	// 将AST转换回code
    const { code } = transformFromAst(AST, null, {
      presets: ['@babel/preset-env']
    })
    console.log(code)
  }
}

打印结果

可以看到,转换后的代码中其实还是有require的,因此这又会回到了结论三

封装parse

这里是将之前的一些操作封装起来

创建parse.js

// 引入fs模块读取文件内容
const fs = require('fs')
// 引入path模块获取文件路径
const path = require('path')
// 引入babel/parser来进行AST转换
const parser = require('@babel/parser')
// 引入babel/traverse的默认导出 对AST进行过滤解析
const tarverse = require('@babel/traverse').default
// 引入@babel/core中的transformFromAst API 把AST做转换
const { transformFromAst }  = require('@babel/core')

module.exports = {
  // 分析模块 获得AST
  getAST:(fileName) => {
    //! 1.分析入口,读取入口模块的内容
    let content = fs.readFileSync(fileName, 'utf-8')
    // console.log(content)
    // 接受字符串模板,也就是content
    return parser.parse(content, {
      // ESModule形式导入的模块
      sourceType: 'module'
    })
  },

  // 拿到依赖 两个路径
  getDependencies:(AST, fileName) => {
     // 用来存放依赖路径的数组,
    // const denpendcies = []
    // 改成对象,可以保留相对路径和根路径
    const dependencies = {}

    tarverse(AST, {
      ImportDeclaration({node}) {
        const dirname = path.dirname(fileName)
        // node.source.value.replace('.', dirname)
        const newPath = "./" + path.join(dirname, node.source.value)
        dependencies[node.source.value] = newPath
      }
    })
    return dependencies
  },
  // AST转code
  getCode: (AST) => {
    const { code } = transformFromAst(AST, null, {
      presets: ['@babel/preset-env']
    })
    return code
  }
}
// lib/compiler.js
const {getAST, getDependencies, getCode} = require('./parse')

module.exports = class Compiler {
  constructor(options) {
    // 保存配置信息中的entry
    this.entry = options.entry
    // 保存配置信息中的output
    this.output = options.output
    // 保存所有模块info的数组
    this.modules = []
  }
  // 启动函数
  run() {
    // 执行构建函数
    const info = this.build(this.entry)
    this.modules.push(info)
    console.log(info)
  }
  // 构建函数
  build(fileName) {
    // 解析获取AST
    let AST = getAST(fileName)
    // 获取AST中的依赖路径和根路径,保存在对象的key, value中
    let dependencies = getDependencies(AST, fileName)
    // 将AST转为code
    let code = getCode(AST) 
    return {
      // 返回文件名
      fileName,
      // 返回依赖对象
      dependencies,
      // 返回代码
      code
    }
  }
}

这一波把前面的操作封装了一下,并在构造函数内部添加了一个modules数组,此数组用来存放所有模块的info信息

打印一下info,结果为

实现结论二和结论七和结论八

结论二:webpack打包出的其实是一个自执行函数,其接收一个对象参数,该对象key值为路径,value值为一个函数体内用eval包裹着key值代表的路径的文件内容的自执行函数
结论七:webpack会深度查找并解析import和其路径的(首先想到就是递归遍历查找)
结论八:webpack会对所有的import和其路径进行解析

webpack会深度查找并解析import和其路径的(首先想到就是递归遍历查找),并会对所有的Import进行解析

所以,当import出现嵌套时,怎么处理这种情况呢?

首先在分析完入口文件时,会得到dependencies对象,这里面是import的路径的对象,

如果对象为空,代表没有import,就不需要递归(没对象连递归都不行,太惨了ovo)

如果对象不为空就递归,然后递归时把dependencies[j]也就是根路径传给this.build,然后this.build会通过这个根路径又去解析这个路径的文件的内容,并将解析完的info返回值又push到modules数组中,这样就可以全部遍历完嵌套import了,

然后最后的modules数组中就包含了所有模块的info了

// 启动函数
run() {
  // 执行构建函数
  const info = this.build(this.entry)
  this.modules.push(info)
  
  for (let i = 0; i < this.modules.length; i++) {
    // 拿到info的信息
    const item = this.modules[i]
    // 解构出来
    const { dependencies } = item
    // 如果不为空,代表有依赖,就要递归去解析这些依赖的模块
    if (dependencies) {
      for (let j in dependencies) {
        // 把路径传进去就ok了,递归遍历了
        // 然后把返回的info又push进modules数组
        this.modules.push(this.build(dependencies[j]))
      }
    }
  }
  console.log(this.modules)
}

这里可能会报错,因为之前我们引入的那些路径都是没有加.js后缀的,在这里大家可以加上后缀再运行

[
  {
    fileName: './src/index.js',
    dependencies: { './sayhi.js': './src\\sayhi.js' },
    code: '"use strict";\n' +
      '\n' +
      'var _sayhi = require("./sayhi.js");\n' +
      '\n' +
      "console.log('hello webpack ' + (0, _sayhi.sayHi)('xiaolu'));"
  },
  {
    fileName: './src\\sayhi.js',
    dependencies: {
      './howold.js': './src\\howold.js',
      './howareyou.js': './src\\howareyou.js'
    },
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      ' value: true\n' +
      '});\n' +
      'exports.sayHi = sayHi;\n' +
      '\n' +
      'var _howold = require("./howold.js");\n' +
      '\n' +
      'var _howareyou = require("./howareyou.js");\n' +
      '\n' +
      'function sayHi(str) {\n' +
      " return 'hi ' + str + '\\n' + (0, _howold.howOld)(str) + '\\n' + (0, _howareyou.howAreYou)(str);\n" +
      '}'
  },
  {
    fileName: './src\\howold.js',
    dependencies: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      ' value: true\n' +
      '});\n' +
      'exports.howOld = howOld;\n' +
      '\n' +
      'function howOld(str) {\n' +
      " return 'How old are you? ' + str;\n" +
      '}'
  },
  {
    fileName: './src\\howareyou.js',
    dependencies: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      ' value: true\n' +
      '});\n' +
      'exports.howAreYou = howAreYou;\n' +
      '\n' +
      'function howAreYou(str) {\n' +
      " return 'How are you?' + str;\n" +
      '}'
  }
]

这是打印结果,可以看到,所有的import的文件的info都在数组中了

而此时有个不足,就是Modules是数组,而在打包出来的bundle.js里面接收的是一个对象参数

所以这里还要进行一波转换

// 启动函数
run() {
  // 执行构建函数
  const info = this.build(this.entry)
  this.modules.push(info)
  
  for (let i = 0; i < this.modules.length; i++) {
    // 拿到info的信息
    const item = this.modules[i]
    // 解构出来
    const { dependencies } = item
    // 如果不为空,代表有依赖,就要递归去解析这些依赖的模块
    if (dependencies) {
      for (let j in dependencies) {
        // 把路径传进去就ok了,递归遍历了
        // 然后把返回的info又push进modules数组
        this.modules.push(this.build(dependencies[j]))
      }
    }
  }
  // 转换数据结构 将数组对象转换成对象形式
  const obj = {}
  this.modules.forEach(item => {
    // 就是将fileName作为key dependencied和code作为value
    obj[item.fileName] = {
      dependencies: item.dependencies,
      code: item.code
    }
  })
  // 然后obj就是转换后的对象了
  console.log(obj) 
}

这里完成了数组对象转换对象形式,其实这种转换在源码中挺常见的,可以学习一下

打印结果

{
  './src/index.js': {
    dependencies: { './sayhi.js': './src\\sayhi.js' },
    code: '"use strict";\n' +
      '\n' +
      'var _sayhi = require("./sayhi.js");\n' +
      '\n' +
      "console.log('hello webpack ' + (0, _sayhi.sayHi)('xiaolu'));"
  },
  './src\\sayhi.js': {
    dependencies: {
      './howold.js': './src\\howold.js',
      './howareyou.js': './src\\howareyou.js'
    },
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      ' value: true\n' +
      '});\n' +
      'exports.sayHi = sayHi;\n' +
      '\n' +
      'var _howold = require("./howold.js");\n' +
      '\n' +
      'var _howareyou = require("./howareyou.js");\n' +
      '\n' +
      'function sayHi(str) {\n' +
      " return 'hi ' + str + '\\n' + (0, _howold.howOld)(str) + '\\n' + (0, _howareyou.howAreYou)(str);\n" +
      '}'
  },
  './src\\howold.js': {
    dependencies: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      ' value: true\n' +
      '});\n' +
      'exports.howOld = howOld;\n' +
      '\n' +
      'function howOld(str) {\n' +
      " return 'How old are you? ' + str;\n" +
      '}'
  },
  './src\\howareyou.js': {
    dependencies: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      ' value: true\n' +
      '});\n' +
      'exports.howAreYou = howAreYou;\n' +
      '\n' +
      'function howAreYou(str) {\n' +
      " return 'How are you?' + str;\n" +
      '}'
  }
}

此时对象转换完毕了!那么结论二和结论七和结论八也实现完毕

实现结论一后半部分

结论一后半部分:根据配置信息生成相应文件夹和文件

此时创建一个file函数,当然首先要获取输出路径了,这时候就是拼接this.output了,因为要进行路径操作,因此要引入path模块

那我们拿到路径之后,是不是要生成文件,因此也要引入fs模块

而要写入的内容其实就是类似于webpack打包后的bundle.js的内容,一个自执行函数,参数为一个对象,这个对象就是我们之前得出的obj,

// lib/compiler
const {
  getAST,
  getDependencies,
  getCode
} = require('./parse')
const path = require('path')
const fs = require('fs')

// Webpack启动函数
module.exports = class Compiler {
  // options为webpack配置文件的参数,因此可以获取到一系列配置,在这保存起来
  constructor(options) {
    // console.log(options)
    // 保存入口
    this.entry = options.entry
    // 保存出口
    this.output = options.output
    console.log(this.output)
    // 保存所有的模块的数组
    this.modules = []

  }

  run() {
    // info接收这些返回
    const info = this.build(this.entry)
    this.modules.push(info)


    for (let i = 0; i < this.modules.length; i++) {
      // 拿到info的信息
      const item = this.modules[i]
      // 解构出来
      const {
        dependencies
      } = item
      // 如果不为空,代表有依赖,就要递归去解析这些依赖的模块
      if (dependencies) {
        for (let j in dependencies) {
          // 把路径传进去就ok了,递归遍历了
          // 然后把返回的info又push进modules数组
          this.modules.push(this.build(dependencies[j]))
        }
      }
    }

    // 转换数据结构 将数组对象转换成对象形式
    const obj = {}
    this.modules.forEach(item => {
      // 就是将fileName作为key dependencied和code作为value
      obj[item.fileName] = {
        dependencies: item.dependencies,
        code: item.code
      }

    })
    // 然后obj就是转换后的对象了
    this.file(obj)
  }
  // 解析
  build(fileName) {
    // 解析获取AST
    let AST = getAST(fileName)
    // 获取AST中的依赖路径和根路径,保存在对象的key, value中
    let dependencies = getDependencies(AST, fileName)
    // 将AST转为code
    let code = getCode(AST)
    return {
      // 返回文件名
      fileName,
      // 返回依赖对象
      dependencies,
      // 返回代码
      code
    }
  }
  // 转换成浏览器可执行的文件
  file(code) {
    // 获取输出信息 拼接出输出的绝对路径 this.output是传入的options
    const filePath = path.join(this.output.path, this.output.filename)
    // console.log(filePath)
    const newCode = JSON.stringify(code)
    // 写一个类似于打包后的自执行函数的函数
    // code为传入的对象参数
    const bundle = `(function(graph){ } })(${newCode})`
    // console.log(bundle)
    // 创建dist目录
    let path1 = this.output.path
    fs.exists(this.output.path, function (exists) {
      if (exists) {
      	// 如果有相应文件夹,直接写入文件
        // 在对应路径下创建文件 bundle为文件内容
        fs.writeFileSync(filePath, bundle, 'utf-8')
        return
      } else {
      	// 如果没有相应文件夹,创建文件夹
        fs.mkdir(path1, (err) => {
          if (err) {
            console.log(err)
            return false
          }
        })
        // 在对应路径下创建文件 bundle为文件内容
        fs.writeFileSync(filePath, bundle, 'utf-8')
      }
    })
  }
}

此时可以删除dist文件夹,然后执行一次,看看是否会生成dist文件夹和bundle.js文件


这就是我们自己生成的bundle.js了,可以看到有点那种webpack的feel了~

这时结论一的后半部分实现完毕

实现结论三前半部分和结论六

结论三:(前半部分)webpack打包出的bundle.js中,自己实现了一个require函数,并返回以入口文件路径为参数的require函数的执行结果。
结论六:webpack会将那些浏览器不执行的给转换成浏览器可执行的函数(这跟结论三的自己实现一个require函数其实关联了)

这时,就是补全bundle函数体内部的东西了

const bundle = `(function(graph){ function require(module) { var exports = {}; (function(code){ eval(code) })(graph[module].code) return exports } require('${this.entry}') })(${newCode})`

首先给自执行函数传了newCode这个对象,内部可以通过graph访问到,然后在自执行函数内部自己写一个require,其接收的参数是入口文件的路径,this.entry,然后内部是一个自执行函数,接收一个code,也就是graph[module].code,其实是newCode[this.entry].code


这里大家要注意一下!特别是不喜欢写分号的同志们,可以看到我代码中var exports = {};这里加了一个分号,就是因为它!我没加这个分号,和自执行函数混起来了,报错找的我好辛苦!大家一定要注意

"code": "\"use strict\";\n\nvar _sayhi = require(\"./sayhi.js\");\n\nconsole.log('hello webpack ' + (0, _sayhi.sayHi)('xiaolu'));"},

然后code内部是会有exports和require的,所以我们需要在外部实现自己的require函数,当做参数传进去,当他碰到require和exports时,就会按照我们写的require和exports去执行了

而在code中的require的参数是相对路径,所以我们可以通过之前的dependenices对象通过相对路径的key去获取根路径的value,因此就是graph[module].dependenices[key]这样就可以了,然后同样执行require,也就是对其进行执行,

exports其实是个对象,执行exports会把那些东西都挂在exports这个对象上,所以直接给了空对象

所以现在补全

const bundle = `(function(graph){ function require(module) { function localRequire(relativePath) { return require(graph[module].dependencies[relativePath]) } var exports = {}; (function(require, exports, code){ eval(code) })(localRequire, exports, graph[module].code) return exports } require('${this.entry}') })(${newCode})`

通过localRequire和exports当做实参传入,而require和exports作为形参接收,所以当code内部遇见了require和exports时,会按照我们传入的实参的方式执行

当require时,会执行这一段代码

return require(graph[module].dependencies[relativePath])

但此时这里的require是外部定义的require了,而内部的参数,前面分析过,就是根据code内部require带的相对路径参数通过dependencies对象转换成根路径,所以require(根路径),相当于又去解析那个路径的模块了,形成了递归解析,直到code内部没有了require了,也就全部解析完了

此时,一个小小的bundle算是完成了
让我们再运行一下

node luWebpack.js

这是bundle.js如下

(function (graph) {
  function require(module) {

    function localRequire(relativePath) {
      return require(graph[module].dependencies[relativePath])
    }

    var exports = {};
    (function (require, exports, code) {
      eval(code)
    })(localRequire, exports, graph[module].code)

    return exports
  }
  require('./src/index.js')
})({
  "./src/index.js": {
    "dependencies": {
      "./sayhi.js": "./src\\sayhi.js"
    },
    "code": "\"use strict\";\n\nvar _sayhi = require(\"./sayhi.js\");\n\nconsole.log('hello webpack ' + (0, _sayhi.sayHi)('xiaolu'));"
  },
  "./src\\sayhi.js": {
    "dependencies": {
      "./howold.js": "./src\\howold.js",
      "./howareyou.js": "./src\\howareyou.js"
    },
    "code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.sayHi = sayHi;\n\nvar _howold = require(\"./howold.js\");\n\nvar _howareyou = require(\"./howareyou.js\");\n\nfunction sayHi(str) {\n return 'hi ' + str + '\\n' + (0, _howold.howOld)(str) + '\\n' + (0, _howareyou.howAreYou)(str);\n}"
  },
  "./src\\howold.js": {
    "dependencies": {},
    "code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.howOld = howOld;\n\nfunction howOld(str) {\n return 'How old are you? ' + str;\n}"
  },
  "./src\\howareyou.js": {
    "dependencies": {},
    "code": "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\nexports.howAreYou = howAreYou;\n\nfunction howAreYou(str) {\n return 'How are you?' + str;\n}"
  }
})

可以创建一个html或者在开发者工具内运行验证

可以看到能成功运行,那么这时候的bundle已经完成了!

bundle所有代码

此时所有的目录如下图


代码放在了github,可以来拿

想说的话

希望看到这里的同学们可以给俺点个赞!

这一篇算是一个非常小的bundle的实现,没有考虑loader和plugin,只是一些基础的打包实现,不过实现了这一个也会提升对webpack打包原理的理解了

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服