likes
comments
collection
share

超通俗易懂的webpack核心原理Webpack 是一个模块打包工具,它可以分析项目的依赖关系,将这些依赖转换和打包为合

作者站长头像
站长
· 阅读数 16

Webpack 是一个模块打包工具,它可以分析项目的依赖关系,将这些依赖转换和打包为合适的格式以供浏览器使用。

基本原理

  • 入口:读取 entry入口文件的内容,分析项目的依赖关系
  • 模块解析:Webpack 通过其配置的加载器(loaders)解析模块。
  • 打包:Webpack 将这些模块打包为一个或多个bundle
  • 插件:Webpack 允许使用插件来扩展其功能,比如插件可以用来优化打包、压缩代码、生成静态资源等。
  • 出口:最后,Webpack 将打包后的文件输出到配置的出口路径。

简单介绍一下webpack,我们直接来看看webpack打包后的文件是什么样?

首先进行初始化和依赖的安装

npm init
yarn add webpack webpack-cli -D

然后在package.json增加打包命令

// package.json
"scripts": {
    "build": "webpack"
  }

然后新建一个 webpack.config.js 文件,进行输入文件和输出文件的配置


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

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

然后在src目录下创建index.js文件

// src/index.js
console.log('aaa')

执行 yarn bulid的命令,就可以看到在 dist 目录下生成了 bundle.js文件,其为打包后的产物,打包后的代码如下

(() => {
  var __webpack_modules__ = 
  ({
  "./src/index.js": (() => {
    eval("console.log('aaa')");
    })
  });
  var __webpack_exports__ = {};
  __webpack_modules__["./src/index.js"]();
})();

可以看到打包出来的代码被一个立即执行函数包裹着,里面的模块里是一个对象,key是入口文件的路径,value是对应的代码,假设index文件引入了其他文件会是怎样的?

// src/index.js
import add from './add'
console.log(add(1, 2))

// src/add.js
export default function add (a, b) {
  return a + b
}

打包看看bundle代码,放主要代码

"use strict";
var __webpack_modules__ = ({
    "./src/add.js":
        ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
        eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"default\": () => (/* binding */ add)\n/* harmony export */ });\nfunction add (a, b) {\r\n  return a + b\r\n}\n\n//# sourceURL=webpack://webpack/./src/add.js?");
    }),
    "./src/index.js":
        ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
        eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./add */ \"./src/add.js\");\n\r\nconsole.log((0,_add__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(1, 2))\n\n//# sourceURL=webpack://webpack/./src/index.js?");
    })
});

可以发现modules里的key是入口文件和import的路径,可以假设modules存储的是入口文件涉及的依赖,后面会验证。

我们一起手动实现一个简易的webpack,来学习一下基本原理

实现简易的webpack

webpack的核心原理

打包主要流程如下:

  1. 需要读到入口文件里面的内容。
  2. 分析入口文件,递归的去读取模块所依赖的文件内容,生成AST语法树。
  3. 根据AST语法树,生成浏览器能够运行的代码

获取依赖关系

假设入口文件依赖另外一个文件的,那么webpack是怎么获取这些依赖关系的?

Webpack 分析入口文件的依赖关系主要是通过解析文件的抽象语法树(AST,Abstract Syntax Tree)来实现的。Webpack 会先将源代码转换为 AST,然后通过遍历 AST 来查找 import 和 require 语句,从而确定代码的依赖关系。

代码转ast

什么工具可以将代码转换为ast?

babel插件 @babel/parser可以将源代码解析成 AST ,方便各个插件分析语法进行相应的处理。

我们来安装依赖并编写一个执行文件来试试

yarn add @babel/parser -D

// src/test.js
const fs = require('fs');
const path = require('path')
const parser  = require('@babel/parser')


const code = fs.readFileSync(path.resolve(__dirname, './index.js'), 'utf8');
const ast = parser.parse(code,{  
  sourceType: 'module', // 表明我们解析的是 ES6 模块  
  plugins: [],  
})

console.log(ast, '==============ast==============')

超通俗易懂的webpack核心原理Webpack 是一个模块打包工具,它可以分析项目的依赖关系,将这些依赖转换和打包为合

上面的截图就是将 index.js 文件转换为ast结构的样子,也可以通过该网站(在线将代码转换为ast)来查看ast的结构

遍历ast

接下来我们怎么来看看怎么获取到 import 和 require 语句?我们可以通过遍历入口文件然后识别对应的 import节点,就可以获取对应的路径,其中@babel/traverse插件可以实现遍历ast语法树的功能。

首先我们要知道 import的节点 是什么样?可以通过ast语法树中得知。

超通俗易懂的webpack核心原理Webpack 是一个模块打包工具,它可以分析项目的依赖关系,将这些依赖转换和打包为合

看看上图代码转换成的ast语法树,import语句对应的节点就是 ImportDeclaration,所以我们只需要在该节点内获取路径即可。

我们来给index文件增加import的例子来看看

// src/index.js
import add from './add.js'
const c = add(1, 2)

// src/test.js
const content = fs.readFileSync(path.resolve(__dirname, './index.js'), 'utf8');
const ast = parser.parse(content,{  
  sourceType: 'module', // 表明我们解析的是 ES6 模块  
  plugins: [],  
})
traverse(ast, {  
  ImportDeclaration(path) { // 访问每个 import 声明节点
    const source = path.node.source.value; // 获取模块的路径
    console.log(source, 'source')
  }  
})

超通俗易懂的webpack核心原理Webpack 是一个模块打包工具,它可以分析项目的依赖关系,将这些依赖转换和打包为合

确实获取到了 import 的路径,这样如果要获取所有的依赖只需要通过 递归,去遍历 import 的文件就可以获取到整一个依赖的关系了,接下来我们写一个完整的例子


// src/index.js
import add from './add.js'
import minus from './minus.js'
const c = add(1, 2)
const d = minus(3, 1)
console.log(c, d)

// src/add.js
import { defaultNum } from './const.js'
export default function add (a, b) {
  return a + b + defaultNum
}

// src/minus.js
export default function minus (a, b) {
  return a - b
}

// src/const.js
export const defaultNum = 5

开始实践

yarn add @babel/traverse -D


// src/test.js

const fs = require('fs');
const path = require('path')
const parser  = require('@babel/parser')
const traverse = require('@babel/traverse').default; 

// 获取对应模块结构
const getModule = (dPath) => {
  const content = fs.readFileSync(path.resolve(__dirname, dPath), 'utf8');
  const ast = parser.parse(content,{  
    sourceType: 'module', // 表明我们解析的是 ES6 模块  
    plugins: [],  
  })
  const dependencies = []
  traverse(ast, {  
    ImportDeclaration(path) { // 访问每个 import 声明节点
      const source = path.node.source.value; // 获取模块的路径
      dependencies.push(source)
    }  
  })
  return {
    path: dPath,
    dependencies
  }
}

// 获取依赖图谱
const getGraph = (entry) => {
  const entryModule = getModule(entry)
  const graphArray = [entryModule]
  const depsGraph = {}
  for(let module of graphArray) {
    if (module.dependencies) {
      for (let dep of module.dependencies) {
        graphArray.push(getModule(dep))
      }
    }
  }

  for (graph of graphArray) {
    depsGraph[graph.path] = graph
  }

  return depsGraph
}

getGraph('./index.js')

当我们遍历 entry 文件,获取到 import 对应的路径时,通过递归就可以获取到所有的依赖关系,依赖图谱结构如下

超通俗易懂的webpack核心原理Webpack 是一个模块打包工具,它可以分析项目的依赖关系,将这些依赖转换和打包为合

这样我们又完成了一大步!

转码

接下来我们就需要对代码进行转码,即将es6转换成es5,为了兼容不支持ES6语法的旧版浏览器,babel工具的@babel/core@babel/preset-env可以实现转码。

yarn add @babel/core @babel/preset-env -D

// test.js
const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
})

下图的code里就是转码后的代码

超通俗易懂的webpack核心原理Webpack 是一个模块打包工具,它可以分析项目的依赖关系,将这些依赖转换和打包为合

生成代码字符串

因为浏览器识别不了 requireexport,我们需要根据依赖图谱,通过重写require来获取到对应依赖所引用的东西,生成打包代码

function bundle(entry){
  //要先把对象转换为字符串,不然在下面的模板字符串中会默认调取对象的toString方法,参数变成[Object object]
  const graph = JSON.stringify(getGraph(entry))
  return `
      (function(graph) {
          //require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
          function require(module) {
              //localRequire的本质是拿到依赖包的exports变量
              function localRequire(relativePath) {
                  return require(relativePath);
              }
              var exports = {};
              (function(require, exports, code) {
                  eval(code);
              })(localRequire, exports, graph[module].code);
              return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
          }
          require('${entry}')
      })(${graph})`
}
console.log(bundle('./index.js'))

看代码肯定一脸懵逼不知道什么意思,我们一步步来分析,require('${entry}' 最开始的肯定是先执行index.js 入口文件的code,我们来看看

超通俗易懂的webpack核心原理Webpack 是一个模块打包工具,它可以分析项目的依赖关系,将这些依赖转换和打包为合

入口文件的code里有我们转码之后的 require 函数,会执行到我们重写的 require 函数里,然后就会执行到./add.js的code,如下

超通俗易懂的webpack核心原理Webpack 是一个模块打包工具,它可以分析项目的依赖关系,将这些依赖转换和打包为合

exports(外层定义的var exports = {})里的default值等于function add,然后会被return出去,即

var _add = _interopRequireDefault(require("./add.js"));
等价于======>
var _add = {'default': function add()...}

以此类推对通过依赖关系进行递归解析,最终就生成打包代码

超通俗易懂的webpack核心原理Webpack 是一个模块打包工具,它可以分析项目的依赖关系,将这些依赖转换和打包为合

我们将log出来的代码copy到浏览器的控制台中

超通俗易懂的webpack核心原理Webpack 是一个模块打包工具,它可以分析项目的依赖关系,将这些依赖转换和打包为合

哇!成功了,再次手写webpack就大功告成啦!

完整代码

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 getModule = (dPath) => {
  const content = fs.readFileSync(path.resolve(__dirname, dPath), 'utf8');
  const ast = parser.parse(content,{  
    sourceType: 'module', // 表明我们解析的是 ES6 模块  
    plugins: [],  
  })
  const dependencies = []
  traverse(ast, {  
    ImportDeclaration(path) { // 访问每个 import 声明节点
      const source = path.node.source.value; // 获取模块的路径
      dependencies.push(source)
    }  
  })
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"]
  })
  return {
    path: dPath,
    dependencies,
    code
  }
}

const getGraph = (entry) => {
  const entryModule = getModule(entry)
  const graphArray = [entryModule]
  const depsGraph = {}
  for(let module of graphArray) {
    if (module.dependencies) {
      for (let dep of module.dependencies) {
        graphArray.push(getModule(dep))
      }
    }
  }

  for (graph of graphArray) {
    depsGraph[graph.path] = graph
  }

  return depsGraph
}

function bundle(entry){
  //要先把对象转换为字符串,不然在下面的模板字符串中会默认调取对象的toString方法,参数变成[Object object],显然不行
  const graph = JSON.stringify(getGraph(entry))
  return `
      (function(graph) {
          //require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
          function require(module) {
              //localRequire的本质是拿到依赖包的exports变量
              function localRequire(relativePath) {
                  return require(relativePath);
              }
              var exports = {};
              (function(require, exports, code) {
                  eval(code);
              })(localRequire, exports, graph[module].code);
              return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
          }
          require('${entry}')
      })(${graph})`
}
console.log(bundle('./index.js'))
转载自:https://juejin.cn/post/7379157261426622505
评论
请登录