前言
这篇博客将会从分析原理到实操写出一个小型的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":
/*!**********************!*\
!*** ./src/index.js ***!
\**********************/
/*! no static exports found */
/***/ (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":
/*!**********************!*\
!*** ./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?");
/***/ })
/******/ });
同样折叠起来看
可以很明显的看到,在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/* 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
此时目录如下
是可以正常输出的,可以自己创建个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打包原理的理解了