webpack打包原理分析

·  阅读 105

概览

获取配置 根据配置信息启动webpack,执行构建

  1. 从入口模块开始分析,有哪些依赖,转换代码;
  2. 递归的分析其他依赖模块,有哪些依赖,转换代码;
  3. 生成可以在浏览器端执行的bundle文件。

自己实现一个bundle.js

模块分析

读取入口文件,分析代码:

let fs = require("fs");
let build = entryFile => {
    let content = fs.readFileSync(entryFile, "utf-8");
};
build("./index.js");

生成AST抽象语法树

这里我们推荐使用@babel/parser,这是babel7 的工具,来帮助我们分析内部的语法,包括es6,返回一个ast抽象语法树

//安装@babel/parser
yarn add @babel/parser -D

let fs = require("fs");
const parser = require("@babel/parser");
let build = entryFile => {
    let content = fs.readFileSync(entryFile, "utf-8");
    const Ast = parser.parse(content, {
        sourceType: "module"
    });
};
build("./index.js");

通过parser.parse方法,可以将我们的index.js文件内容转换成AST抽象语法树

//AST抽象语法树
{
  type: 'File',
  start: 0,
  end: 155,
  loc: SourceLocation {
    start: Position { line: 1, column: 0 },
    end: Position { line: 6, column: 0 }
  },
  errors: [],
  program: Node {
    type: 'Program',
    start: 0,
    end: 155,
    loc: SourceLocation { start: [Position], end: [Position] },
    sourceType: 'module',
    interpreter: null,
    body: [ [Node], [Node], [Node], [Node] ],
    directives: []
  },
  comments: []
}

拿到文件中的依赖

接下来我们就可以根据AST里面的分析结果,遍历出所有的引入模块,但是比较麻烦,这里还是推荐babel推荐的一个模块,@babel/traverse,来帮我们处理。

//安装@babel/parser
yarn add @babel/traverse -D

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

let build = entryFile => {
    let content = fs.readFileSync(entryFile, "utf-8");
    const Ast = parser.parse(content, {
        sourceType: "module"
    });
    const dependencies = {}; //可以保留相对路径和根路径两种信息
    traverse(ast, {
      ImportDeclaration({ node }) {
        // dependencies.push(node.source.value); 相对路径
        //分析ast抽象语法树,返回依赖模块的绝对路径。
        const dirname = path.dirname(entryFile);
        const newPath = "./" + path.join(dirname, node.source.value);
        dependencies[node.source.value] = newPath;
      },
    });
};
build("./index.js");

在index.js中依赖模块是同级目录下的hello.js,

import { say } from "./hello.js";
let str = "hello" + say("webpack");
document.body.innerHTML = `<h1>${str}</h1>`;
console.log("hello" + say("webpack"));

//dependencies值:
{ './hello.js': './src\\hello.js' }

转换成浏览器可以识别的代码

把代码处理成浏览器可运行的代码,需要借助@babel/core,和@babel/preset-env,把ast语法树转换成合适的代码。

//安装@babel/preset-env和@babel/core。 yarn add @babel/preset-env -D yarn add @babel/core -D

const { transformFromAst } = require("@babel/core");
const { code } = transformFromAst(Ast, null, {presets: ["@babel/preset-env"]});

将以上三个方法放置在一个解析文件里面:

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const { transformFromAst } = require("@babel/core");

module.exports = {
  //分析模块 获得AST
  getAst: (fileName) => {
    let content = fs.readFileSync(fileName, "utf-8");
    let ast = parser.parse(content, {
      sourceType: "module",
    });
    return ast;
  },

  //获取依赖
  getDependencies: (ast, fileName) => {
    const dependencies = {}; //可以保留相对路径和根路径两种信息
    traverse(ast, {
      ImportDeclaration({ node }) {
        // dependencies.push(node.source.value); 相对路径
        const dirname = path.dirname(fileName);
        const newPath = "./" + path.join(dirname, node.source.value);
        dependencies[node.source.value] = newPath;
      },
    });
    console.log(dependencies);
    return dependencies;
  },

  //转换代码
  getCode: (ast) => {
    const { code } = transformFromAst(ast, null, {
      presets: ["@babel/preset-env"],
    });
    return code;
  },
};

返回的信息

{
  fileName: './src/index.js',
  dependencies: { './hello.js': './src\\hello.js' },
  code: '"use strict";\n' +
    '\n' +
    'var _hello = require("./hello.js");\n' +
    '\n' +
    'var str = "hello" + (0, _hello.say)("webpack");\n' +
    'document.body.innerHTML = "<h1>".concat(str, "</h1>");\n' +
    'console.log("hello" + (0, _hello.say)("webpack"));'
}

在Complier编译文件里面引入这三个方法

const fs = require("fs");
const path = require("path");
const { getAst, getDependencies, getCode } = require("./parser");

module.exports = class Complier {
  constructor(options) {
    this.entry = options.entry;
    this.output = options.output;
    this.modules = [];
  }

  run() {
    const info = this.build(this.entry);
    this.modules.push(info);

    for (let i = 0; i < this.modules.length; i++) {
      const item = this.modules[i];
      const { dependencies } = item;
      if (dependencies) {
      // 通过循环遍历所有的模块,生成所有模块的信息放入modules里面
        for (let j in dependencies) {
          this.modules.push(this.build(dependencies[j]));
        }
      }
    }

    //转换数据结构
    const obj = {};
    this.modules.forEach((item) => {
      obj[item.fileName] = {
        dependencies: item.dependencies,
        code: item.code,
      };
    });
    //生成代码文件
    this.file(obj);
  }
    
  //在build方法里面引入这三个方法,发挥一个对象,包含了文件路径,依赖模块,可执行代码
  build(fileName) {
    let ast = getAst(fileName);
    let dependencies = getDependencies(ast, fileName);
    let code = getCode(ast);

    return {
      fileName,
      dependencies,
      code,
    };
  }

  file(code) {
    //获取输出信息 .../dist/main.js
    const filePath = path.join(this.output.path, this.output.filename);
    const newCode = JSON.stringify(code);
    const bundle = `(function(graph){
        function require(module){
            function localRequire(relativePath){
               return require(graph[module].dependencies[relativePath])
            }
            
            var exports = {};
            (function(require,exports,code){
                eval(code)
            })(localRequire,exports,graph[module].code)
            return exports;
        }
        
        require('${this.entry}') //./src/index.js
    })(${newCode})`;

    fs.writeFileSync(filePath, bundle, "utf-8");
  }
};

生成webpack.js文件模仿webpack命令,生成可以在浏览器端执行的bundle文件

const Complier = require("./lib/complier");
const options = require("./webpack.config.js");

new Complier(options).run();

以下为webpack.config.js文件

const path = require("path");

module.exports = {
  entry: "./src/index.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "./dist")
  }
};

执行node webpack.js命令,生成的main.js文件

(function (graph) {
  function require(module) {
    function localRequire(relativePath) {
      return require(graph[module].dependencies[relativePath]);
    }

    var exports = {};
    (function (require, exports, code) {
      eval(code);
    })(localRequire, exports, graph[module].code);
    return exports;
  }

  require("./src/index.js"); //./src/index.js
})({
  "./src/index.js": {
    dependencies: { "./hello.js": "./src\\hello.js" },
    code:
      '"use strict";\n\nvar _hello = require("./hello.js");\n\nvar str = "hello" + (0, _hello.say)("webpack");\ndocument.body.innerHTML = "<h1>".concat(str, "</h1>");\nconsole.log("hello" + (0, _hello.say)("webpack"));',
  },
  "./src\\hello.js": {
    dependencies: {},
    code:
      '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nexports.say = say;\n\nfunction say(str) {\n  return str;\n}',
  },
});

到这里,webpack整体流程执行完毕,webpack的核心是通过AST抽象语法树将各个模块紧密关联起来,生成一个紧密结合的文件,供浏览器执行。

分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改