手把手教你实现一个简易版Webpack

1,265 阅读3分钟

作为一名前端开发工程师,想必大家在工作和学习中少不了跟Webpack打交道,本文从手写Webpack的角度,一步一步带你实现一个简易版的Webpack,顺便让大家了解一下Webpack的原理。 源码地址

基本实现流程

  • 读取入口文件的内容
  • 分析入口文件,读取模块并递归读取模块所依赖的文件内容,生成AST语法树。
  • 根据AST语法树,生成浏览器能够运行的代码

工具模块包

  • path:路径操作
  • fs :读写文件
  • @babel/parser :将内容转成AST语法树
  • @babel/traverse: 遍历AST收集依赖
  • @babel/core和@babel/preset-env:将ES6语法转成ES5

功能实现

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

const fs = require("fs");
const main = filename => {
 const content = fs.readFileSync(filename, "utf-8");
 console.log(content);
};
main("./index.js");

将内容转成AST语法树

使⽤@babel/parser,这是babel7的⼯具,来帮助我们分析内部的语法,包括es6,返回⼀个ast抽象语法树。我们解析出来的不单单是index.js文件里的内容,它也包括了文件的其他信息。而它的内容其实是它的属性program里的body里。

  • 安装@babel/parser
$ npm install @babel/parser --save
  • 具体代码
const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const parse = filename => {
    const content = fs.readFileSync(filename, "utf-8");
    const Ast = parser.parse(content, {
        sourceType: "module"
    });
    console.log(Ast.program.body);
};
parse("./index.js");

遍历AST收集依赖

接下来我们就可以根据body⾥⾯的分析结果,遍历出所有的引⼊模块。其实就是将用import语句引入的文件路径收集起来。这⾥还是推荐babel的⼀个模块@babel/traverse,来帮我们处理。

  • 安装 @babel/traverse
$ npm install @babel/traverse --save
  • 具体代码
const parse = filename => {
    const content = fs.readFileSync(filename, "utf-8");
    const Ast = parser.parse(content, {
        sourceType: "module"
    });
    const dependencies = [];
    //分析ast抽象语法树,根据需要返回对应数据,
    //根据结果返回对应的模块,定义⼀个数组,接受⼀下node.source.value的值
    traverse(Ast, {
        ImportDeclaration({ node }) {
            console.log(node);
            dependencies.push(node.source.value);
        }
    });
    console.log(dependencies);
};
parse("./index.js");

ES6转成ES5(AST)

现在我们需要把获得的ES6的AST转化成ES5,执行这一步需要@babel/core和@babel/preset-env

  • 安装@babel/core和@babel/preset-env
npm install @babel/core @babel/preset-env
  • 具体代码
const parse = filename => {
    const content = fs.readFileSync(filename, "utf-8");
    const Ast = parser.parse(content, {
        sourceType: "module"
    });
    const dependencies = [];
    //分析ast抽象语法树,根据需要返回对应数据,
    //根据结果返回对应的模块,定义⼀个数组,接受⼀下node.source.value的值
    traverse(Ast, {
        ImportDeclaration({ node }) {
            console.log(node);
            dependencies.push(node.source.value);
        }
    });
    const {code} = babel.transformFromAst(ast,null,{
        presets:["@babel/preset-env"]
    })
    console.log(code);
};
parse("./index.js");

递归获取所有依赖

将上面的parse方法返回一个对象,新建一个run函数,在里面写一个递归方法,递归获取依赖。

const run = filename => {
  const entry =  parse(filename)
  const temp = [entry]
  //!处理其他依赖模块,做一个信息汇总
  for (let i = 0; i < temp.length; i++) {
    const item = temp[i];
    const { dependencies } = item;
    if (dependencies) {
      for (let j in dependencies) {
        temp.push(parse(dependencies[j]));
      }
    }
  }
  console.log(temp)
}
const parse = filename => {
    const content = fs.readFileSync(filename, "utf-8");
    const Ast = parser.parse(content, {
        sourceType: "module"
    });
    const dependencies = [];
    //分析ast抽象语法树,根据需要返回对应数据,
    //根据结果返回对应的模块,定义⼀个数组,接受⼀下node.source.value的值
    traverse(Ast, {
        ImportDeclaration({ node }) {
            console.log(node);
            dependencies.push(node.source.value);
        }
    });
    const {code} = babel.transformFromAst(ast,null,{
        presets:["@babel/preset-env"]
    })
    return {
      entryFile,
      dependencies,
      code
    };
};
run("./index.js");

自定义requireexports

浏览器不会识别执行require和exports,我们可以自己定义requireexports

file(code) {
  //! 生成bundle.js => ./dist/main.js
  const filePath = path.join(this.output.path, this.output.filename);
  console.log(filePath);
  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
  })(${newCode})`;
  fs.writeFileSync(filePath, bundle, "utf-8");
}

完整代码

以上这些功能的完整代码可参考:webpack.js

功能测试

  • src里面新建三个文件expo.jsindex.jstest.js
///test.js
export const test = function() {
  console.log("test webpack");
};

//expo.js
import { test } from "./test.js";
test()
export const add = function(a, b) {
  return a + b;
};

export const minus = function(a, b) {
  return a - b;
};

//index.js
import { add,minus} from "./expo.js";
console.log('add(1,2)',add(1, 2));
console.log('minus(10,3)',minus(10,3));
  • 运行命令生成代码如下:
(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
    })({"./src/index.js":{"dependencies":{"./expo.js":"./src\\expo.js"},"code":"\"use strict\";\n\nvar _expo = require(\"./expo.js\");\n\nconsole.log('add(1,2)', (0, _expo.add)(1, 2));\nconsole.log('minus(10,3)', (0, _expo.minus)(10, 3));"},"./src\\expo.js":{"dependencies":{"./test.js":"./src\\test.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.minus = exports.add = void 0;\n\nvar _test = require(\"./test.js\");\n\n(0, _test.test)();\n\nvar add = function add(a, b) {\n  return a + b;\n};\n\nexports.add = add;\n\nvar minus = function minus(a, b) {\n  return a - b;\n};\n\nexports.minus = minus;"},"./src\\test.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.test = void 0;\n\nvar test = function test() {\n  console.log(\"test webpack\");\n};\n\nexports.test = test;"}})
  • 将以上代码放到Chrome控制台执行,输入结果如下:测试成功!
VM2516:9 test webpack
VM2514:5 add(1,2) 3
VM2514:6 minus(10,3) 7

更多

源码地址

Web开发知识点总结

参考文章

手写webpack核心原理,再也不怕面试官问我webpack原理