手写webpack简易版

249 阅读5分钟

简易版webpack

实现思路:创建webpack.config.js,设置基础配置,webpack读取配置,从配置当中分析入口模块,以及其依赖模块,并且分析内容(@babel/parser),返回一个ast抽象语法树,最后编译内容(@babel/traverse),最后递归分析编译依赖的依赖,生成bundle.js(可以直接在浏览器执行的js)

ast 抽象语法树

d1ee86dd7a99cb7f36f82cb608b8e23.jpg

ast 节点分析器:astexplorer.net/

准备插件

@babel/parser:帮助我们分析内部的语法,包括es6,返回⼀个ast抽象语法。

@babel/traverse:可以用来遍历更新@babel/parser生成的AST 进入节点(enter) 退出节点 (exit)

@babel/core:是把 js 代码分析成 ast ,方便各个插件分析语法进行相应的处理。 //有些新语法在低版本 js 中是不存在的,如箭头函数,rest 参数,函数默认值等,这种语言层面的不兼容只能通过将代码转为 ast,分析其语法后再转为低版本 js。

@babel/preset-env:(1) 将尚未被大部分浏览器支援的JavaScript 语法转换成能被浏览器支援的语法。(2) 让较旧的浏览器也能支援大部分浏览器能支援的语法,例如Promise、Map、Set等。

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

流程解析

image.png

开始

webpack生成的编译入口:

(function (modules) {
  var installedModules = {};
//   闭包
  function __webpack_require__(moduleId) {
    //   存在缓存 直接返回
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 没有缓存 则做一个缓存
    var module = (installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {},
    });
    // 调用依赖时 给依赖传参
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    module.l = true;
    return module.exports;
  }
  //默认moduleId = 0 后面自己传
  return __webpack_require__((__webpack_require__.s = 0));
})([
  /* 0 */
  function (module, exports, __webpack_require__) {
    var b = __webpack_require__(1);
    b();
  },
  /* 1 */
  function (module, exports) {
    module.exports = function () {
      console.log(11);
    };
  },
]);
//自执行函数 传入依赖参数


项目结构

npm init初始化项目

webpack.config.js

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

bundle.js

const options = require('./webpack.config.js')

const Webpack =  require('./lib/webpack.js')

new Webpack(options).run()

./lib/webpack.js (重点)

module.exports = class Webpack {
  constructor(params) {
    const { entry, output } = params;
    this.output = output;
    this.entry = entry;
    this.modules = [];
  }
 }

run 方法

首先先解析入口文件,将所有依赖的子模块缓存起来。然后遍历这个数组再去收集子模块的依赖。通过扁平化收集到所有的依赖模块和对应的可执行的代码。

    const info = this.parse(this.entry);
    // console.log("Webpack -> run -> info", info)
    this.modules.push(info);
        //入口依赖的依赖 开始扁平化
    for (let i = 0; i < this.modules.length; i++) {
      const item = this.modules[i];
      const { dependencies } = item;
      if (dependencies) {
        for (let j in dependencies) {
          this.modules.push(this.parse(dependencies[j])); //依赖扁平化
        }
      }
    }
    
   const obj = {} 
   this.modules.forEach( item => {
     obj[item.entryFile] = {
       dependencies: item.dependencies,
       code: item.code
     }
   })
   console.log(obj)
   //生成bundle.js  bundle.js里面的代码可以直接在浏览器中运行
   this.file(obj)

parse 方法

  parse(entryFile) {
    console.log("Webpack -> parse -> entryFile", entryFile)
    //分析入口模块内容
    //读取到入口文件中的编码内容     content 为./src/index.js的编码内容
    const content = fs.readFileSync(entryFile, "utf-8");
    // console.log(content);
    //分析入口文件有哪些依赖 自己依赖路径
    //把内容通过parse抽象成语法树便于分析 提取
    const ast = parser.parse(content, {
      sourceType: "module", //es module语法
    });
    //  console.log(ast)
    //每一行代码都会有一个node节点解析
    //如果是import 语法 就是可以获取到 value 然后借助@babel/traverse处理 得到value
   // console.log(ast.program.body);
   
   //提取依赖
   //依赖模块的缓存数组 
    const dependencies = {};
    traverse(ast, {
      //提取类型为ast.program.body打印出来的导入 ImportDeclaration 提取ImportDeclaration中的node
      ImportDeclaration({ node }) {
        //得到模块路径 这个路径不是最终 要./expo.js => ./src/expo.js
        console.log(node.source.value);
        // console.log(path.dirname(entryFile));
        const sourceValue = node.source.value + '.js';
        const newPathName =
          "./" + path.join(path.dirname(entryFile), sourceValue);
        //当前文件所有依赖的模块
        dependencies[node.source.value] = newPathName.replace("\\", "/");
      },
    });
    //  处理内容  转换ast
    // 把代码处理成浏览器可运⾏的代码,需要借助@babel/core,和 @babel/preset-env,把ast语法树转换成合适的代码
    //返回一段可执行的代码 但是现在不能直接在浏览器执行
    const { code } = transformFromAst(ast, null, {
      presets: ["@babel/preset-env"],
    });
    // console.log(code);
    /**
           * "use strict";
      //浏览器不认识require
      var _a = _interopRequireDefault(require("./a"));

      function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }

      console.log("hhhhh");
      console.log(_a["default"].add(12, 1));
          */
    return {
      entryFile,
      dependencies,
      code,
    };
  }

ast 抽象语法树:content里面的每一行代码都会有一个node节点解析,如果是import 语法 就是可以获取到 value 然后借助@babel/traverse处理 得到value

./src/index.js

ast.program.body:./src/index.js有4行代码,第一行是表达式所以type是ExpressionStatement,二三两行是import所以type是ImportDeclaration

node.source.value

dependencies:

obj:

file 方法

 //执行代码 自执行函数 实现require 
 // 生成bundle.js => ./dist/main.js (手动创建dist文件夹)
   file(code){
    // 生成bundle.js => ./dist/main.js
    const filePath = path.join(this.output.path,this.output.filename )
    //手动创建dist 目录
    const newCode = JSON.stringify(code)
    //graph 就是本模块的依赖和可执行代码对象 obj 
    const bundle = `(function(graph){
    //module 模块名 如 ./src/index.js
      function require(module){
     //localRequire 引入模块的依赖
          function localRequire(relativePath){
             //obj['./src/index.js']
             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})`;
  // 执行node bundle.js 会在dist目录下生成main.js,里面的代码内容为newCode  也就上面的obj内容
    fs.writeFileSync(filePath,bundle,"utf-8")
  }

执行node bunlde.js

生成的./dist/main.js

(function(graph){
      function require(module){
          function localRequire(relativePath){
              //这边又调用了require, 比如code中的 require(\"./a\")
              //就是调用localRequire('./a'),就是又开始了a的子模块的代码执               //行。
             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":{"./a":"./src/a.js","./b":"./src/b.js"},"code":"\"use strict\";\n\nvar _a = require(\"./a\");\n\nvar _b = _interopRequireDefault(require(\"./b\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(\"hhhhh\");\nconsole.log((0, _a.add)(12, 1));"},"./src/a.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.minus = exports.add = void 0;\n\nvar add = function add(a, b) {\n  console.log(a + b);\n};\n\nexports.add = add;\n\nvar minus = function minus(a, b) {\n  console.log(a - b);\n};\n\nexports.minus = minus;"},"./src/b.js":{"dependencies":{"./c":"./src/c.js"},"code":"\"use strict\";\n\nvar _c = _interopRequireDefault(require(\"./c\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(\"xixixiixbb\");"},"./src/c.js":{"dependencies":{},"code":"\"use strict\";\n\nconsole.log(\"hhhhhhhhh\");"}})

拿入口文件生成的code解析一下执行:

这个require就是localRequire方法,参数就是localRequire的relativePath,这样就引入子模块了。

"\"use strict\";

//这个require就是localRequire方法,参数就是localRequire的relativePath,这样就引入子模块了。

var _a = require(\"./a\");

var _b = _interopRequireDefault(require(\"./b\"));

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }

console.log(\"hhhhh\");
console.log((0, _a.add)(12, 1));"

复制到浏览器执行

完整项目:github.com/lppwlwzj/lp…