安装依赖

npm i @babel/parser @babel/traverse babel-types tapable -D

src/index.js

let title = require('./title.js');
console.log(title);

src/title.js

module.exports = 'title';

my-webpack.js

const fs = require("fs");//读文件
const path = require("path");//处理路径的

/*
@babel/types 是一款作用于 AST 的类 lodash 库,其封装了大量与 AST 有关的方法,大大降低了转换 AST 的成本。@babel/types 的功能主要有两种:一方面可以用它验证 AST 节点的类型,例如使用 isClassMethod 或 assertClassMethod 方法可以判断 AST 节点是否为 class 中的一个 method;另一方面可以用它构建 AST 节点,例如调用 classMethod 方法,可生成一个新的 classMethod 类型 AST 节点 。
*/
const types = require("babel-types");//处理类型 
const parser = require("@babel/parser");//把源代码转成抽象语法树
const traverse = require("@babel/traverse").default;//遍历语法树
const generate = require("@babel/generator").default;//把语法树生成源代码

// /Users/sun/Desktop/base/webpack-interview/my-webpack
const baseDir = process.cwd().replace(/\\/g,path.posix.sep);//根目录 将\换成/

// /Users/sun/Desktop/base/webpack-interview/my-webpack/src/index.js
const entry = path.posix.join(baseDir, "src/index.js");//打包的入口文件

let modules = [];//数组里面放着本次编译的所有模块

function buildModule(absolutePath){
  /*
  index.js 文件的内容
  let title = require('./title.js');
  console.log(title);

  title.js文件中的内容
  module.exports = 'title';
  */
  //  读取入口文件
  const body = fs.readFileSync(absolutePath, "utf-8");//读取文件内容
  
  // 将文件内容转为AST抽象语法树
  const ast = parser.parse(body, {
    sourceType: "module",//源代码转成抽象语法树
  });

  const moduleId = "./" + path.posix.relative(baseDir, absolutePath);// 第一调用值为./src/index.js;第二次调用值为./src/title.js 因为在webpack中moduleId都是相对于根目录的相对路径
  
  const module = { id: moduleId, deps:[] };//声明一个模块对象

  // @babel/traverse(遍历)方法维护这 AST 树的整体状态,我们这里使用它来帮我们找出依赖模块
  traverse(ast, {
    CallExpression({ node }) {
      if (node.callee.name === 'require') { // 是否存在依赖
        node.callee.name = "__webpack_require__"; // 因为打包后的require都会变成__webpack_require__
        
        let moduleName = node.arguments[0].value; // 获取模块名字 ./title.js

        //  /Users/sun/Desktop/base/webpack-interview/my-webpack/src
        const dirname = path.posix.dirname(absolutePath);
        
        //  /Users/sun/Desktop/base/webpack-interview/my-webpack/src/title.js
        const depPath = path.posix.join(dirname, moduleName);

        //  ./src/title.js
        const depModuleId = "./" + path.posix.relative(baseDir, depPath);

        // [ { type: 'StringLiteral', value: './src/title.js' } ]
        node.arguments = [types.stringLiteral(depModuleId)];
        
        // 递归处理所有模块
        module.deps.push(buildModule(depPath));
      }
    }
  });

  // 将 AST 语法树转换为浏览器可执行代码
  let { code } = generate(ast); // 重新生成新的代码

  module._source = code;//module._source存放着重新生成后的代码
  
  modules.push(module);
  
  return module;
}

// 所有模块编译完成后
let entryModule = buildModule(entry);

// 重写 require函数 (浏览器不能识别commonjs语法)
let content = `
(function (modules) {
    function __webpack_require__(moduleId) {
        var module = {
            i: moduleId,
            exports: {}
        };
        modules[moduleId].call(
            module.exports,
            module,
            module.exports,
            __webpack_require__
        );
        return module.exports;
    }

    return __webpack_require__("${entryModule.id}");
})(
    {
      ${modules
        .map(
          (module) =>
            `"${module.id}": function (module, exports,__webpack_require__) {${module._source}}`
        )
        .join(",")}
    }
);
`;

// 把文件内容写入到文件系统
fs.writeFileSync("./dist/bundle.js", content);

简单总结下

第一步,解析入口文件通过@babel/parser分析内部的语法,返回AST抽象语法树

第二步,通过@babel/traverse找出依赖模块

第三步,将 AST 语法树转换为浏览器可执行代码,

第四步,递归解析所有依赖项,生成依赖关系图

第五步,重写 require 函数,输出 bundle

参考:

Babel 插件有啥用 (opens new window)

webpack打包原理 ? 看完这篇你就懂了 ! (opens new window)