webpack打包原理

252 阅读5分钟

一、什么是webpack

image.png

当我们在使用webpack打包一个文件的时候,只要配置好文件的entry、output、plugins等就可以打包出一个dist文件夹出来。我们打包压缩的文件就在dist文件夹下。那么webpack是如何将那么多的文件内容压缩打包到dist文件夹下呢?我们通过一个例子来说明。

webpack 核心原理

在此之前我们要先了解webpack的核心原理是什么?

  1. 打包的主要流程
    • 需要读取到入口文件
    • 递归去获取模块依赖的文件内容,生成AST语法树
    • 根据AST语法树,生成浏览器能够运行的代码
  2. 获取模板内容
  3. 模块分析
  4. 收集依赖
  5. ES6转ES5

二、 实践一个简单的例子

1. 创建一个小demo

  1. 创建一个文件,并初始化
  1. mkdir my-webpack && cd my-webpack
  2. npm init -y
  1. 在根目录下创建src文件夹,在src文件夹下创建index.js、add.js、minus.js。 代码也很简单:创建加和减两个函数,在index.js中引入。
//index.js
import add from './add.js'
import {minus} from './minus.js'   //ES模块---

const sum = add(1,2)
const division =minus(2,1)

console.log('sum:',sum);
console.log('division:',division);

// add.js   ES6规范
export default (a,b) =>{
  return a + b
}

// minus.js 
export const  minus = (a,b) =>{
  return a - b 
}export const  minus = (a,b) =>{
  return a - b 
}
  1. 在根目录下创建一个index.html,并引入src下面的index.js文件。我们打开这个index.html的控制台。会看到一个经典报错:

Screenshot_20220505_103941_com.alibaba.android.ri.jpg 因为浏览器还无法识别import等ES6的语法。

  1. 接下来在根目录下创建一个bundle.js,现在的目录结构为:

image.png

2. 在bundle.js里面开始打包的流程

第一步:获取模板内容

  const fs = require('fs')   //用来读取文件

  const getModuleInfo = (file) => {
  
  const body = fs.readFileSync(file, 'utf-8')
  console.log(body);
  })
  getModuleInfo('./src/index.js')

我们可以运行输出一下,可以看到,我们index.js 的代码就被获取到了。

image.png

第二步:模块分析 这里我们要借助 babel 来解析模块。

npm i @babel/parser -D

@babel/parser用来做模块分析,并生成抽象语法树。

  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);
  })
  
 getModuleInfo('./src/index.js')

我们运行打印一下,可以看到AST语法树就生成了,其中progarm.body我们所收集到的模块的node结点。

image.png

第三步:收集依赖 这里我们借用 babel 的 @babel/traverse 来进行收集依赖。

npm i @babel/traverse -D @babel/traverse 我们可以将它与 babel 解析器一起使用来遍历和更新节点,用来遍历结点收集依赖 代码更新为:

  const fs = require('fs')   //用来读取文件
  const parser = require('@babel/parser') 
  const tarverse = require('@babel/traverse').default

  const getModuleInfo = (file) => {
  
  const body = fs.readFileSync(file, 'utf-8')
   //解析模块依赖
  const ast = parser.parse(body, {
    sourceType: 'module',  //要解析的是代码中的ES模块
  })
 
   //遍历收集依赖
  const deps = {}  //用来保存依赖
  tarverse(ast, {
    //指明收集啥依赖
    ImportDeclaration({ node }) {
      const dirname = path.dirname(file)  //读取当前文件所处文件夹的文件名
      const abspath = './' + path.join(dirname, node.source.value) 
      let abs = abspath.replace(/\\/g, '/') // window 会有\\ ,需要转成 /
      deps[node.source.value] = abs  // abspath = ./src/sum.js 
    }
  })
 console.log(deps)
  })
  getModuleInfo('./src/index.js')

我们打印输出deps:可以看到输出一个对象

image.png

第四步:将ES6代码转成ES5 借助 babel 的 @babel/core @babel/preset-env,将ES6 降级为ES5

npm i @babel/core @babel/preset-env -D

  //ES6 转ES5
  const { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env']  // 这个插件会生成我们想要的代码
  })
  console.log(code);  //降级后的代码

我们打印输出code

image.png 最后在getModuleInfo 函数中,将路径、收集依赖、降级的代码整和成对象返回出去:

image.png getModuleInfo完整代码:

  const fs = require('fs')   //用来读取文件
  const parser = require('@babel/parser') 
  const tarverse = require('@babel/traverse').default

  const getModuleInfo = (file) => {
  
  const body = fs.readFileSync(file, 'utf-8')
   //解析模块依赖
  const ast = parser.parse(body, {
    sourceType: 'module',  //要解析的是代码中的ES模块
  })
 
   //遍历收集依赖
  const deps = {}  //用来保存依赖
  tarverse(ast, {
    //指明收集啥依赖
    ImportDeclaration({ node }) {
      const dirname = path.dirname(file)  //读取当前文件所处文件夹的文件名
      const abspath = './' + path.join(dirname, node.source.value) 
      let abs = abspath.replace(/\\/g, '/') // window 会有\\ ,需要转成 /
      deps[node.source.value] = abs  // abspath = ./src/sum.js 
    }
  })
    //ES6 转ES5
    const { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env']
  })
  
  const moduleInfo = { file, deps, code }   // 路径 、依赖 、降级代码
  return moduleInfo
  })
  getModuleInfo('./src/index.js')

第五步: 递归获取文件依赖

创建一个parseModules 用来递归收集依赖,

//递归获取文件依赖
const parseModules = (file) => {
const entry = getModuleInfo(file)  //调用getModuleInfo得到一个包含路径 、依赖 、降级代码的对象
  const temp = [entry]             // [{url,deps,es5code}]
  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]))  // getModuleInfo(./src/add.js) ...
        }
      }
    }
  }
  console.log(temp);
}
parseModules('./src/index.js')

我们打印一下temp,可以看到 idnex add minus 的依赖都被存入。

image.png 我们修改一下目录结构,让它看起来更简洁,方便之后的操作:

  //修改数据结构,使 file为key, code 为value
  const depsCraph = {}
  temp.forEach(moduleInfo => {
    depsCraph[moduleInfo.file] = {
      deps: moduleInfo.deps,
      code: moduleInfo.code
    }
  })
  

就变成:

image.png parseModules 的完整代码:

//递归获取文件依赖
const parseModules = (file) => {
const entry = getModuleInfo(file)  //调用getModuleInfo得到一个包含路径 、依赖 、降级代码的对象
  const temp = [entry]             // [{url,deps,es5code}]
  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]))  // getModuleInfo(./src/add.js) ...
        }
      }
    }
  }
  const depsCraph = {}
  temp.forEach(moduleInfo => {
    depsCraph[moduleInfo.file] = {
      deps: moduleInfo.deps,
      code: moduleInfo.code
    }
  })
   return depsCraph
}
parseModules('./src/index.js')

这样我们就拿到了文件里面的所有依赖,并将其转换成了ES5的语法

第六步:解决关键字冲突

code的打印我们可以看到,虽然被降级成了ES5的语法,但是依然存在requireexports两个关键字,浏览默也识别不了,所以我们就需要自己定义一个require函数和exports对象。 image.png

  • 创建一个bundle函数用来创建关键字
//解决关键字require和exports
const bundle = (file) => {
const depsCraph = JSON.stringify(parseModules(file)) // 拿到所有的依赖

//自执行函数,返回出去的函数不能让它执行,所以用云括号包装成一个字符串返回
 return  `(function (graph) {
    function require(file) {  // 自定义require函数
    
  //返回处一个自执行函数
   var exports = {}; // 定义exports关键字对象
  (function (require,exports,code) {   //这里的形参不能是别的名,要和code的打印的名字一致
       eval(code)  
  })(absRequire,exports,graph[file].code) 
  
  //拿到文件的绝对路径,将函数传给自执行函数
  function absRequire(relPath){
  return require(graph[file].deps[relPath])//将code里面require接收的路径转化为绝对路径
 }
}
 require('${file}')
 })(${depsCraph})`
}
 const content = bundle('./src/index.js')

我们打印一下content,拿到了所有打包好的数据。

image.png

最后,我们只需将我们打包好的数据写入文件即可:

fs.mkdirSync('./dist')  // 生成文件夹
fs.writeFileSync('./dist/bundle.js',content)  //写入文件

我们就可以看到目录结构下生成的dist文件夹了,里面的bundle.js就是我们打包好的数据了。

image.png

bundle.js

返回了一个自执行函数,只要我们引入这份js文件,就会自动执行。

image.png

测试

我们重新在index.html引入这份我们打包好的bundle.js,打开控制台我们可以看到:

image.png

输出成功了,我们把代码解析成浏览器能够识别的样子了。

这就是webpack的打包的核心原理,希望对你有所帮助。