手写webpack打包工具

156 阅读2分钟

随着前端的快速发展,各种打包工具增出不穷。目前使用最为广泛的无疑是webpack。那么问题就来了,你了解 webpack 吗?

接下来教你如何手写一个webpack打包工具。demo

webpack 原理

webpack底层原理甚为复杂,在此只做简单描述,满足下面demo需要。日后会对webpack做一个详细的揭秘。

从webpack工程里面可以看出,首先一般会有一个入口模块路径。webpack会先读取这个入口模块的内容,然后对内容进行解析,找出依赖模块。接下来对依赖模块进行解析,找出依赖的依赖,不断递归,直至找出所有的依赖的模块。最后将所有模块的代码进行封装,输出到bundle文件,在html中引入。

手写webpack

目录结构

首先看一下demo的目录结构。

  • webpack.js是webpack实例。
  • build/index.js运行webpack实例。
  • src目录为待编译代码。
├── README.md
├── build
│   ├── index.js // webpack 配置
│   └── webpack.js // webpack 实例
├── index.html
├── package-lock.json
├── package.json
└── src // 待编译代码
    ├── component.js
    ├── helloworld.js
    └── index.js

webpack 实例

1. 读取入口文件

在执行 run 方法之后,首先调用 readFile 方法传入入口模块路径,然后调用 parseFile 读取入口模块内容。

import helloworld from "./helloworld.js";
import component from "./component.js";

var setup = () => {
  const wrap = document.createElement('div');
  const helloworldElement = document.createElement('div');
  helloworldElement.innerHTML = helloworld;
  wrap.appendChild(helloworldElement);
  wrap.appendChild(component());
  document.body.appendChild(wrap);
}

setup();

然后通过babel遍历出依赖模块。

// 将code转化成ast树
const ast = babel.parse(text);
const dependences = [];

// 遍历 ast 所有 import 节点
babel.traverse(ast, {
  ImportDeclaration(p) {
    dependences.push(p.node.source.value);
  },
});

// 将ast转化为code
const { code } = babel.transformFromAstSync(ast, null,{
  presets:["@babel/preset-env"]
});

2. 遍历所有依赖

在处理完入口模块的解析之后,会得到它的依赖列表。接着对每个依赖模块执行解析,找出剩下的依赖模块。

result.dependences.forEach((value) => {
  const re = this.parseFile(value);
  modules.push(re);
});

将所有模块封装,并输出

最后调用assets方法,将所有模块封装成一个对象.

const modules = {
  [module.filePath]: function (module, exports, require) {
    // module.code
  }
}

并输出到output文件中,在html中引入。

完整代码:

const fs = require('fs');
const path = require('path');
const babel = require('@babel/core');

class Webpack {
  constructor(config) {
    this.config = config;
  }

  run () {
    this.readFile(this.config.entry);
  }

  assets(modules = []) {

    const stats = [];
    modules.map((module) => {
      stats.push(`
        '${module.filePath}': function (module, exports, require) {
          ${module.code}
        }
      `);
    });

    const code = `
      const modules = {
        ${stats.join()}
      }

      var __webpack_module_cache__ = {};

      function require(moduleId) {
        console.log(modules, moduleId)
      
        if(__webpack_module_cache__[moduleId]) {
          return __webpack_module_cache__[moduleId].exports;
        }
      
        var module = __webpack_module_cache__[moduleId] = {
          exports: {}
        };
      
        modules[moduleId](module, module.exports, require);
      
        return module.exports;
      }

      require('${this.config.entry}');
    `

    const dir = path.dirname(path.resolve(this.config.output));
    // 将代码写入bundle
    fs.mkdir(dir, { recursive: true }, (err) => {
      fs.writeFile(
        path.resolve(this.config.output),
        code,
        'utf8',
        () => {}
      );

      // 处理index.html
      let html = fs.readFileSync(path.resolve('./index.html'), "utf-8");
      html = html.replace('</head>', `  <script defer src="./index.js"></script>\n</head>`);

      fs.writeFile(
        `${dir}/index.html`,
        html,
        'utf8',
        () => {}
      )
    });
  }

  readFile (filePath) {
    const result = this.parseFile(filePath);
    const modules = [result];

    result.dependences.forEach((value) => {
      const re = this.parseFile(value);
      modules.push(re);
    });
    this.assets(modules);
  }

  parseFile (filePath) {
    const text = fs.readFileSync(path.resolve('src', filePath.replace('/src', '')), "utf-8");
    const ast = babel.parse(text);
    const dependences = [];

    babel.traverse(ast, {
      ImportDeclaration(p) {
        dependences.push(p.node.source.value);
      },
    });

    const { code } = babel.transformFromAstSync(ast, null,{
      presets:["@babel/preset-env"]
    });

    return {
      code,
      dependences,
      filePath
    }
  }
}

module.exports = Webpack;