webpack bundle实现 打包原理 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 bundle实现 打包原理 webpack(二)
webpack bundle实现 打包原理 webpack(二)

webpack分析

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

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

此时,会在文件夹下生成一个dist文件夹,并在dist文件夹中会有一个bundle.js文件
webpack bundle实现 打包原理 webpack(二)
好,在这停顿,现在是思考的时候了:为什么会在运行webpack打包指令后,生成一个dist文件夹和一个bundle.js文件呢?
webpack bundle实现 打包原理 webpack(二)

相信很多人也知道这个问题的答案:在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 bundle实现 打包原理 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":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! no static exports found */
/***/ (function(module, exports) {

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

/***/ })

/******/ });

一大串代码!当然得将其折叠起来分析
webpack bundle实现 打包原理 webpack(二)

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

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

结论二

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

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

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

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

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

结论三

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

渐入佳境了,最简单一个打印已经分析完毕了,这时候就要引入import了
webpack bundle实现 打包原理 webpack(二)

在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

此时文件目录如下
webpack bundle实现 打包原理 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":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _sayhi__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./sayhi */ \"./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":
/*!**********************!*\
!*** ./src/sayhi.js ***!
\**********************/
/*! exports provided: sayHi */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __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 bundle实现 打包原理 webpack(二)
可以很明显的看到,在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 bundle实现 打包原理 webpack(二)
我们这里是通过相对路径引入的,但是如果webpack解析了import和路径的话,key值应该是./sayhi这样的,但实际上却是./src/sayhi.js,所以webpack内部肯定也对这个相对路径进行了处理

结论五

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

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

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __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

此时目录如下

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

是可以正常输出的,可以自己创建个html引入bundle.js查看
webpack bundle实现 打包原理 webpack(二)
这里我们在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)
}

其他文件均没变化
此时文件目录如下
webpack bundle实现 打包原理 webpack(二)
执行打包命令后

npx webpack

运行如下图
webpack bundle实现 打包原理 webpack(二)
此时,是在sayhi中出现了两次import,这次能执行成功,代表着webpack会解析所有的import

结论八

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

分析总结

好!分析的差不多了,将前面分析出的八点在这里总结一下
webpack bundle实现 打包原理 webpack(二)

结论一:(前半部分)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
此时文件目录如下
webpack bundle实现 打包原理 webpack(二)

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

console.log(options)

通过以下命令运行

node luWebpack.js

打印结果如下

webpack bundle实现 打包原理 webpack(二)
此时,已经成功获取到了配置信息了webpack bundle实现 打包原理 webpack(二)

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

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

此时文件目录如下
webpack bundle实现 打包原理 webpack(二)

// 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()

执行结果如下图
webpack bundle实现 打包原理 webpack(二)
在这,我创建了一个class Compiler,通过构造函数保存配置信息,并创建了启动函数和构建函数,可以看到,这里已经成功的拿到了配置信息,并启动了构建

此时结论一实现完毕,接下来先把结论二放一放,先来实现结论三的后半部分
webpack bundle实现 打包原理 webpack(二)

实现结论三的后半部分

结论三:(后半部分)并且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构建函数中,读取了入口文件的所有内容

打印内容如下:
webpack bundle实现 打包原理 webpack(二)
此时已经成功的读取到了入口文件./src/index.js的内容

结论三后半部分完成

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

实现结论四

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

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

所以先安装如下两个模块

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

关于这些用法,可以去babel官网查看webpack bundle实现 打包原理 webpack(二)

这篇博客是为了实现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是什么

webpack bundle实现 打包原理 webpack(二)
可以看到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的路径
webpack bundle实现 打包原理 webpack(二)
webpack bundle实现 打包原理 webpack(二)

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

实现结论五

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

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

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

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

这里面我创建了一个dependencies对象,是用来存放路径的,因为要进行路径的拼接操作得到一个新路径,所以我想用对象的key:value来保存拼接前的路径和拼接后的路径webpack bundle实现 打包原理 webpack(二)

// 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)
}
})
}

看看打印结果

webpack bundle实现 打包原理 webpack(二)
此时已经成功地将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)
}
}

打印结果
webpack bundle实现 打包原理 webpack(二)
可以看到,转换后的代码中其实还是有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 bundle实现 打包原理 webpack(二)

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

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

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

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

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

如果对象为空,代表没有import,就不需要递归(没对象连递归都不行,太惨了ovo)
webpack bundle实现 打包原理 webpack(二)

如果对象不为空就递归,然后递归时把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里面接收的是一个对象参数
webpack bundle实现 打包原理 webpack(二)

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

// 启动函数
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文件

webpack bundle实现 打包原理 webpack(二)
这就是我们自己生成的bundle.js了,可以看到有点那种webpack的feel了~webpack bundle实现 打包原理 webpack(二)

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

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

结论三:(前半部分)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

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

"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或者在开发者工具内运行验证
webpack bundle实现 打包原理 webpack(二)
webpack bundle实现 打包原理 webpack(二)

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

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

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

bundle所有代码

此时所有的目录如下图

webpack bundle实现 打包原理 webpack(二)
代码放在了github,可以来拿

想说的话

希望看到这里的同学们可以给俺点个赞!
webpack bundle实现 打包原理 webpack(二)

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

原创:https://www.panoramacn.com
源码网提供WordPress源码,帝国CMS源码discuz源码,微信小程序,小说源码,杰奇源码,thinkphp源码,ecshop模板源码,微擎模板源码,dede源码,织梦源码等。

专业搭建小说网站,小说程序,杰奇系列,微信小说系列,app系列小说

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

免责声明,若由于商用引起版权纠纷,一切责任均由使用者承担。

您必须遵守我们的协议,如果您下载了该资源行为将被视为对《免责声明》全部内容的认可-> 联系客服 投诉资源
www.panoramacn.com资源全部来自互联网收集,仅供用于学习和交流,请勿用于商业用途。如有侵权、不妥之处,请联系站长并出示版权证明以便删除。 敬请谅解! 侵权删帖/违法举报/投稿等事物联系邮箱:2640602276@qq.com
未经允许不得转载:书荒源码源码网每日更新网站源码模板! » webpack bundle实现 打包原理 webpack(二)
关注我们小说电影免费看
关注我们,获取更多的全网素材资源,有趣有料!
120000+人已关注
分享到:
赞(0) 打赏

评论抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

您的打赏就是我分享的动力!

支付宝扫一扫打赏

微信扫一扫打赏