webpack打包原理

151 阅读6分钟

打包原理

⼿写webpack原理 juejin.cn/post/685457…

webpack打包原理 blog.csdn.net/weixin_4131…

主要流程

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

具体细节

  1. 获取主模块内容

  2. 分析模块

    • 安装@babel/parser包(生成AST)
  3. 对模块内容进行处理

    • 安装@babel/traverse包(遍历AST收集依赖)
    • 安装@babel/core和@babel/preset-env包 (es6转ES5)
  4. 递归所有模块

  5. 执行require和exports。生成最终代码

基本准备工作

先建一个项目

我们创建了add.js文件和minus.js文件,然后 在index.js中引入,再将index.js文件引入index.html。

代码如下:

add.js

 export default (a,b)=>{
   return a+b;
 }
 复制代码

minus.js

 export const minus = (a,b)=>{
     return a-b
 }
 复制代码

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);

index.html

 <!DOCTYPE html>
 <html lang="en">
 <head>
     <meta charset="UTF-8">
     <title>Title</title>
 </head>
 <body>
 <script src="./src/index.js"></script>
 </body>
 </html>
 复制代码

现在我们打开index.html。你猜会发生什么???显然会报错,因为浏览器还不能识别import等ES6语法

img

获取主模块内容

bundle.js文件

 // 获取主入口文件
 const fs = require('fs')
 const getModuleInfo = (file)=>{
     const body = fs.readFileSync(file,'utf-8')
     console.log(body);
 }
 getModuleInfo("./src/index.js")

执行一下bundle.js:node bundle.js

img

分析模块babel/parser

分析模块的主要任务是 将获取到的主模块内容 解析成AST语法树,这个需要用到一个依赖包@babel/parser

 npm install @babel/parser

ok,安装完成我们将@babel/parser引入bundle.js,

 // 获取主入口文件
 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);
 //当前我们解析出来的不单单是index.js文件里的内容,它也包括了文件的其他信息。 而它的内容其实是它的属性program里的body里
     console.log(ast.program.body);
 }
 getModuleInfo("./src/index.js")

收集依赖babel/traverse

现在我们需要 遍历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方法代表的是对ast中type类型为ImportDeclaration的节点的处理。
         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")

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")

递归获取所有依赖

经过上面的过程,现在我们知道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数组里。

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

img

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

 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]))
                 }
             }
         }
     }
     // 新增代码
     const depsGraph = {}
     temp.forEach(moduleInfo=>{
         depsGraph[moduleInfo.file] = {
             deps:moduleInfo.deps,
             code:moduleInfo.code
         }
     })
     console.log(depsGraph)
     return depsGraph
 }

img

导入导出

我们现在的目的就是要生成一个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)); 
   console.log(depsGraph);
   /**
    * 把保存下来的depsGraph,传入一个立即执行函数。
     将主模块路径传入require函数执行
     执行reuire函数的时候,又立即执行一个立即执行函数,这里是把code的值传进去了
     执行eval(code)。也就是执行主模块的code这段代码
    */
   return `(function (graph) {
     function require(file) {
       //转化成绝对路径
       function absRequire(relPath) {
         return require(graph[file].deps[relPath])
       }
       //执行add.js的code时候,会遇到exports这个还没定义的问题.因此我们可以自己定义一个exports对象。
       var exports = {};
       (function (require, exports, code) {
         console.log(1, exports);
         //code代码执行过程中会执行到require函数。
         //这时会调用这个require,也就是我们传入的absRequire
         eval(code);
       })(absRequire, exports, graph[file].code)
       return exports;
     }
     require('${file}')
   })(${depsGraph})`;
 };
 const content = bundle("./src/index.js");
  • 把保存下来的depsGraph,传入一个立即执行函数。

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

      • reuire函数中立即执行函数

        • require:absRequire,因为code代码中require路径不是绝对路径,需要转化成绝对路径,因此写一个函数absRequire来转化

        • exports:exports

          • 增添了一个空对象 exports,执行add.js代码的时候,会往这个空对象上增加一些属性

             // 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 = {
               __esModule:{  value: true},
               defaultfunction _default(a, b) {  return a + b;}
             }
            
          • 然后我们把exports对象return出去。

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

            return出去的值,被interopRequireDefault接收, interopRequireDefault再返回default这个属性给_add,因此_add = function _default(a, b) { return a + b;}

        • code:graph[file].code

          • 执行eval(code),也就是执行模块的code这段代码

            • 执行eval(code)过程会执行到require函数,这时会调用这个require,也就是我们传入的absRequire,而执行absRequire就执行了return require(graph[file].deps[relPath])这段代码,也就是执行了外面这个require。而执行require("./src/add.js")之后,又会执行eval,也就是执行add.js文件的代码。