超通俗易懂的webpack核心原理

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 ,方便各个插件分析语法进行相应的处理。

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

image.png

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

遍历ast

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

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

image.png

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

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

作者:前端笨鸟
链接:https://juejin.cn/post/7379157261426622505
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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==============')

image.png

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

遍历ast

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

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

image.png

看看上图代码转换成的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')
  }  
})

image.png

确实获取到了 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 对应的路径时,通过递归就可以获取到所有的依赖关系,依赖图谱结构如下

image.png

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

转码

接下来我们就需要对代码进行转码,即将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里就是转码后的代码

image.png

生成代码字符串

因为浏览器识别不了 require 和 export,我们需要根据依赖图谱,通过重写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,我们来看看

image.png

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

image.png

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

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

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

image.png

哇!成功了,再次手写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 

Webpack是一个强大的静态模块打包工具,其核心原理基于模块化的思想,它将项目中的各种资源(如JavaScript、CSS、图片等)通过依赖分析和打包优化,构建出一个适合浏览器运行的文件集合。 Webpack的工作流程主要包括以下几个步骤: 1. **模块解析**: Webpack读取配置文件,遍历整个项目的模块树,识别出哪些是需要处理的模块(如JavaScript文件)。 2. **模块加载**: 对每个模块,Webpack会根据模块的导入路径寻找对应的源文件,并将其转换成内部的虚拟模块系统。 3. **模块打包**: 根据模块间的依赖关系,Webpack会生成一个包含所有模块及其依赖的依赖图。这个过程可能会涉及代码分割、压缩和混淆等优化操作。 4. **输出生成**: 最终,Webpack会将处理后的模块合并成一个或多个单独的打包文件,通常是一个包含所有JavaScript的bundle.js文件,以及CSS和其他资源的文件。 下面是一个简单的Webpack配置示例(假设使用的是webpack v4): ```javascript const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); module.exports = { // 输出目录 output: { filename: 'main.js', // 打包后的主入口文件名 path: path.resolve(__dirname, 'dist') // 静态资源输出目录 }, // 模块解析规则 module: { rules: [ { test: /\.js$/, use: ['babel-loader'], exclude: /node_modules/ } // 使用Babel处理.js文件 ] }, // 插件设置 plugins: [ new HtmlWebpackPlugin({ template: './src/index.html', // HTML模板文件 inject: true // 将生成的JS插入到HTML中 }) ], resolve: { // 解析模块规则 extensions: ['.js', '.json'] // 后缀自动补全 } }; ``` 在这个配置中,Webpack会查找所有的`.js`文件并使用Babel转换它们,然后生成一个`main.js`文件,并在`index.html`中引入。这只是最基础的配置,实际项目中可能还会包括热更新、代码分割等高级特性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值