手写webpack核心原理,再也不怕面试官问我webpack原理

成功。又是不出所料的成功。

不过,我们需要知道的是,当前我们解析出来的不单单是index.js文件里的内容,它也包括了文件的其他信息。

而它的内容其实是它的属性program里的body里。如图所示

我们可以改成打印ast.program.body看看

// 获取主入口文件

const fs = require(‘fs’)

const parser = require(‘@babel/parser’)

const getModuleInfo = (file)=>{

const body = fs.readFileSync(file,‘utf-8’)

const ast = parser.parse(body,{

sourceType:‘module’ //表示我们要解析的是ES模块

});

console.log(ast.program.body);

}

getModuleInfo(“./src/index.js”

执行

看,现在打印出来的就是 index.js文件里的内容(也就是我们再index.js里写的代码啦).

五、收集依赖


现在我们需要 遍历AST,将用到的依赖收集起来。什么意思呢?其实就是将用import语句引入的文件路径收集起来。我们将收集起来的路径放到deps里。

前面我们提到过,遍历AST要用到@babel/traverse依赖包

npm install @babel/traverse

现在,我们引入。

const fs = require(‘fs’)

const path = require(‘path’)

const parser = require(‘@babel/parser’)

const traverse = require(‘@babel/traverse’).default

const getModuleInfo = (file)=>{

const body = fs.readFileSync(file,‘utf-8’)

const ast = parser.parse(body,{

sourceType:‘module’ //表示我们要解析的是ES模块

});

// 新增代码

const deps = {}

traverse(ast,{

ImportDeclaration({node}){

const dirname = path.dirname(file)

const abspath = ‘./’ + path.join(dirname,node.source.value)

deps[node.source.value] = abspath

}

})

console.log(deps);

}

getModuleInfo(“./src/index.js”)

我们来看下官方文档对@babel/traverse的描述

好吧,如此简略

不过我们不难看出,第一个参数就是AST。第二个参数就是配置对象

我们看看我们写的代码

traverse(ast,{

ImportDeclaration({node}){

const dirname = path.dirname(file)

const abspath = ‘./’ + path.join(dirname,node.source.value)

deps[node.source.value] = abspath

}

})

配置对象里,我们配置了ImportDeclaration方法,这是什么意思呢?

我们看看之前打印出来的AST。

ImportDeclaration方法代表的是对type类型为ImportDeclaration的节点的处理。

这里我们获得了该节点中source的value,也就是node.source.value,

这里的value指的是什么意思呢?其实就是import的值,可以看我们的index.js的代码。

import add from “./add.js”

import {minus} from “./minus.js”;

const sum = add(1,2);

const division = minus(2,1);

console.log(sum);

console.log(division);

可见,value指的就是import后面的 ‘./add.js’ 和 ‘./minus.js’

然后我们将file目录路径跟获得的value值拼接起来保存到deps里,美其名曰:收集依赖。

ok,这个操作就结束了,执行看看收集成功了没?

oh my god。又成功了。

六、ES6转成ES5(AST)


现在我们需要把获得的ES6的AST转化成ES5,前面讲到过,执行这一步需要两个依赖包

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

我们现在将依赖引入并使用

const fs = require(‘fs’)

const path = require(‘path’)

const parser = require(‘@babel/parser’)

const traverse = require(‘@babel/traverse’).default

const babel = require(‘@babel/core’)

const getModuleInfo = (file)=>{

const body = fs.readFileSync(file,‘utf-8’)

const ast = parser.parse(body,{

sourceType:‘module’ //表示我们要解析的是ES模块

});

const deps = {}

traverse(ast,{

ImportDeclaration({node}){

const dirname = path.dirname(file)

const abspath = “./” + path.join(dirname,node.source.value)

deps[node.source.value] = abspath

}

})

新增代码

const {code} = babel.transformFromAst(ast,null,{

presets:[“@babel/preset-env”]

})

console.log(code);

}

getModuleInfo(“./src/index.js”)

我们看看官网文档对@babel/core 的transformFromAst的介绍

害,又是一如既往的简略。。。

简单说一下,其实就是将我们传入的AST转化成我们在第三个参数里配置的模块类型。

好了,现在我们来执行一下,看看结果

我的天,一如既往的成功。可见 它将我们写const 转化成var了。

好了,这一步到此结束,咦,你可能会有疑问,上一步的收集依赖在这里怎么没啥关系啊,确实如此。收集依赖是为了下面进行的递归操作。

七、递归获取所有依赖


经过上面的过程,现在我们知道getModuleInfo是用来获取一个模块的内容,不过我们还没把获取的内容return出来,因此,更改下getModuleInfo方法

const getModuleInfo = (file)=>{

const body = fs.readFileSync(file,‘utf-8’)

const ast = parser.parse(body,{

sourceType:‘module’ //表示我们要解析的是ES模块

});

const deps = {}

traverse(ast,{

ImportDeclaration({node}){

const dirname = path.dirname(file)

const abspath = “./” + path.join(dirname,node.source.value)

deps[node.source.value] = abspath

}

})

const {code} = babel.transformFromAst(ast,null,{

presets:[“@babel/preset-env”]

})

// 新增代码

const moduleInfo = {file,deps,code}

return moduleInfo

}

我们返回了一个对象 ,这个对象包括该模块的路径(file)该模块的依赖(deps)该模块转化成es5的代码

该方法只能获取一个模块的的信息,但是我们要怎么获取一个模块里面的依赖模块的信息呢?

没错,看标题,,你应该想到了就算递归。

现在我们来写一个递归方法,递归获取依赖

const parseModules = (file) =>{

const entry = getModuleInfo(file)

const temp = [entry]

for (let i = 0;i<temp.length;i++){

const deps = temp[i].deps

if (deps){

for (const key in deps){

if (deps.hasOwnProperty(key)){

temp.push(getModuleInfo(deps[key]))

}

}

}

}

console.log(temp)

}

讲解下parseModules方法:

  1. 我们首先传入主模块路径

  2. 将获得的模块信息放到temp数组里。

  3. 外面的循坏遍历temp数组,此时的temp数组只有主模块

  4. 循环里面再获得主模块的依赖deps

  5. 遍历deps,通过调用getModuleInfo将获得的依赖模块信息push到temp数组里。

目前bundle.js文件:

const fs = require(‘fs’)

const path = require(‘path’)

const parser = require(‘@babel/parser’)

const traverse = require(‘@babel/traverse’).default

const babel = require(‘@babel/core’)

const getModuleInfo = (file)=>{

const body = fs.readFileSync(file,‘utf-8’)

const ast = parser.parse(body,{

sourceType:‘module’ //表示我们要解析的是ES模块

});

const deps = {}

traverse(ast,{

ImportDeclaration({node}){

const dirname = path.dirname(file)

const abspath = “./” + path.join(dirname,node.source.value)

deps[node.source.value] = abspath

}

})

const {code} = babel.transformFromAst(ast,null,{

presets:[“@babel/preset-env”]

})

const moduleInfo = {file,deps,code}

return moduleInfo

}

// 新增代码

const parseModules = (file) =>{

const entry = getModuleInfo(file)

const temp = [entry]

for (let i = 0;i<temp.length;i++){

const deps = temp[i].deps

if (deps){

for (const key in deps){

if (deps.hasOwnProperty(key)){

temp.push(getModuleInfo(deps[key]))

}

}

}

}

console.log(temp)

}

parseModules(“./src/index.js”)

按照目前我们的项目来说执行完,应当是temp 应当是存放了index.js,add.js,minus.js三个模块。

,执行看看。

牛逼!!!确实如此。

不过现在的temp数组里的对象格式不利于后面的操作,我们希望是以文件的路径为key,{code,deps}为值的形式存储。因此,我们创建一个新的对象depsGraph。

const parseModules = (file) =>{

const entry = getModuleInfo(file)

const temp = [entry]

const depsGraph = {} //新增代码

for (let i = 0;i<temp.length;i++){

const deps = temp[i].deps

if (deps){

for (const key in deps){

if (deps.hasOwnProperty(key)){

temp.push(getModuleInfo(deps[key]))

}

}

}

}

// 新增代码

temp.forEach(moduleInfo=>{

depsGraph[moduleInfo.file] = {

deps:moduleInfo.deps,

code:moduleInfo.code

}

})

console.log(depsGraph)

return depsGraph

}

ok,现在存储的就是这种格式啦

八、处理两个关键字


我们现在的目的就是要生成一个bundle.js文件,也就是打包后的一个文件。其实思路很简单,就是把index.js的内容和它的依赖模块整合起来。然后把代码写到一个新建的js文件。

我们把这段代码格式化一下

// index.js

“use strict”

var _add = _interopRequireDefault(require(“./add.js”));

var _minus = require(“./minus.js”);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { “default”: obj }; }

var sum = (0, _add[“default”])(1, 2);

var division = (0, _minus.minus)(2, 1);

console.log(sum); console.log(division);

// add.js

“use strict”;

Object.defineProperty(exports, “__esModule”, { value: true});

exports[“default”] = void 0;

var _default = function _default(a, b) { return a + b;};

exports[“default”] = _default;

但是我们现在是不能执行index.js这段代码的,因为浏览器不会识别执行require和exports。

不能识别是为什么?不就是因为没有定义这require函数,和exports对象。那我们可以自己定义。

我们创建一个函数

const bundle = (file) =>{

const depsGraph = JSON.stringify(parseModules(file))

}

我们将上一步获得的depsGraph保存起来。

现在返回一个整合完整的字符串代码。

怎么返回呢?更改下bundle函数

const bundle = (file) =>{

const depsGraph = JSON.stringify(parseModules(file))

return `(function (graph) {

function require(file) {

(function (code) {

eval(code)

})(graph[file].code)

}

require(file)

})(depsGraph)`

}

我们看下返回的这段代码

(function (graph) {

function require(file) {

(function (code) {

eval(code)

})(graph[file].code)

}

require(file)

})(depsGraph)

其实就是

  1. 把保存下来的depsGraph,传入一个立即执行函数。

  2. 将主模块路径传入require函数执行

  3. 执行reuire函数的时候,又立即执行一个立即执行函数,这里是把code的值传进去了

  4. 执行eval(code)。也就是执行主模块的code这段代码

我们再来看下code的值

// index.js

“use strict”

var _add = _interopRequireDefault(require(“./add.js”));

var _minus = require(“./minus.js”);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { “default”: obj }; }

var sum = (0, _add[“default”])(1, 2);

var division = (0, _minus.minus)(2, 1);

console.log(sum); console.log(division);

没错执行这段代码的时候,又会用到require函数。此时require的参数为add.js的路径,哎,不是绝对路径,需要转化成绝对路径。因此写一个函数absRequire来转化。怎么实现呢?我们来看下代码

(function (graph) {

function require(file) {

function absRequire(relPath) {

return require(graph[file].deps[relPath])

}

(function (require,code) {

eval(code)

})(absRequire,graph[file].code)

}

require(file)

})(depsGraph)

实际上是实现了一层拦截。

  1. 执行require(’./src/index.js’)函数

  2. 执行了

(function (require,code) {

eval(code)

})(absRequire,graph[file].code)

  1. 执行eval,也就是执行了index.js的代码。

  2. 执行过程会执行到require函数。

  3. 这时会调用这个require,也就是我们传入的absRequire

  1. 而执行absRequire就执行了return require(graph[file].deps[relPath])这段代码,也就是执行了外面这个require

在这里return require(graph[file].deps[relPath]),我们已经对路径转化成绝对路径了。因此执行外面的require的时候就是传入绝对路径。

  1. 而执行require(“./src/add.js”)之后,又会执行eval,也就是执行add.js文件的代码。

是不是有点绕?其实是个递归。

这样就将代码整合起来了,但是有个问题,就是在执行add.js的code时候,会遇到exports这个还没定义的问题。如下所示

// add.js

“use strict”;

Object.defineProperty(exports, “__esModule”, { value: true});

exports[“default”] = void 0;

var _default = function _default(a, b) { return a + b;};

exports[“default”] = _default;

我们发现 这里它把exports当作一个对象来使用了,但是这个对象还没定义,因此我们可以自己定义一个exports对象。

JavaScript

  • js的基本类型有哪些?引用类型有哪些?null和undefined的区别。

  • 如何判断一个变量是Array类型?如何判断一个变量是Number类型?(都不止一种)

  • Object是引用类型嘛?引用类型和基本类型有什么区别?哪个是存在堆哪一个是存在栈上面的?

  • JS常见的dom操作api

  • 解释一下事件冒泡和事件捕获

  • 事件委托(手写例子),事件冒泡和捕获,如何阻止冒泡?如何组织默认事件?

  • 对闭包的理解?什么时候构成闭包?闭包的实现方法?闭包的优缺点?

  • this有哪些使用场景?跟C,Java中的this有什么区别?如何改变this的值?

  • call,apply,bind

  • 显示原型和隐式原型,手绘原型链,原型链是什么?为什么要有原型链

  • 创建对象的多种方式

  • 实现继承的多种方式和优缺点

  • new 一个对象具体做了什么

  • 手写Ajax,XMLHttpRequest

  • 变量提升

  • 举例说明一个匿名函数的典型用例

  • 指出JS的宿主对象和原生对象的区别,为什么扩展JS内置对象不是好的做法?有哪些内置对象和内置函数?

  • attribute和property的区别

  • document load和document DOMContentLoaded两个事件的区别

  • JS代码调试

  • 10
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值