实现一个简单的webpack

1,069 阅读1分钟

什么是webpack?

我们先来看看webpack官方的定义:

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。

相信这个定义已经说的非常清楚了。首先,它的本质是一个模块打包器,其工作是将每个模块打包成相应的bundle。那么在这中间究竟做了什么事情呢?

场景引入

现在有以下文件

// word.js
export const word = 'hello'
// message.js

import {word} from './word.js'
const message = `say ${word}`

export default message
// index.js
import message from './message.js'

console.log(message)

请编写一个bundler.js,将其中的ES6代码转换为ES5代码,并将这些文件打包,生成一段能在浏览器正确运行起来的代码。(最后输出say hello)

我们将需求进行拆解成下面的三个步骤

  1. 利用bebel完成代码转换,并生成单个文件的依赖。
  2. 生成依赖图谱
  3. 生成最后的打包代码

第一步:转换代码、生成依赖

//先安装好相应的包
npm install @babel/parser @babel/traverse @babel/core @babel/preset-env -D

转换代码需要利用@bebel/parser生成AST抽象语法树,然后利用@babel/traverse进行AST遍历,记录依赖关系,最后通过@babel/core和@babel/preset-env进行代码的转换。

function stepOne(filename) {
  // 读取文件
  const content = fs.readFileSync(path.resolve(__dirname, filename), "utf8");

  // 解析文件成ast
  const ast = parser.parse(content, {
    sourceType: "module",
  });

  const dependencies = {};

  // 遍历ast 语法树
  traverse(ast, {
    // 获取通过 import 导入的模块
    ImportDeclaration({ node }) {
      // 返回类似于unix目录的命令
      const dirname = path.dirname(filename);
      // 返回一个带有完成标准化路径的字符串,其中包含多有段
      const newFile = './' + path.join(dirname, node.source.value);

      // 保存所有依赖的模块
      dependencies[node.source.value] = newFile;
    },
  });

  // 通过 babel.core 和 @babel/preset-env进行代码的转换
  const { code } = babel.transformFromAst(ast, null, {
    presets: ["@babel/preset-env"],
  })

  return {
    filename,  // 当前文件名
    dependencies,  // 该文件所依赖的模块的集合
    code    // 转换后的代码
  }
}

image.png

第二步:生成依赖图谱

// entry 为入口文件
function stepTwo(entry) {
  const entryModule = stepOne(entry);

  // 

  const graphArray = [entryModule]

  for (let i = 0; i < graphArray.length; i++) {
    const item = graphArray[i]
    const { dependencies } = item   // 拿到文件依赖的模块集合

    for (let j in dependencies) {
      const dep = stepOne(dependencies[j])
      graphArray.push(dep)  // 目的是让入口模块及其所有相关的模块放入数组
    }
  }

  // 接下来生成依赖图

  const graph = {}

  graphArray.forEach(item => {
    graph[item.filename] = {
      dependencies: item.dependencies,
      code: item.code
    }
  })
  return graph
}

image.png

一边遍历依赖数组,一边根据最开始入口的依赖信息,分析出更深层的依赖文件,并调用第一步的函数生成依赖,并把其依次加入到依赖数组中。

最终遍历生成的这个数组,为了展示他们之间的关系,我们使用map来存储, 生成最终的依赖关系图。

{
  fileName: 'index.js': {
    dependencies: {'message.js': 'message.js'},
    code: '转换后的代码' // 使用 babel.code 和 babel/preset-env转换之后的代码。
  }
}
//测试一下 console.log(stepTwo('./src/index.js'))

// 结果如下
{
  "index.js": {
    dependencies: {
      "./message.js": "./message.js",
    },
    code: "\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);",
  },
  "./message.js": {
    dependencies: {
      "./word.js": "./word.js",
    },
    code: "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"say \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;",
  },
  "./word.js": {
    dependencies: {
    },
    code: "\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.word = void 0;\nvar word = \"word\";\nexports.word = word;",
  },
}

image.png

第三步:生成代码字符串

// 第三步  生成代码字符串
// 到第三步,我们就需要了解一下 require 函数的实现了
function step3(entry) {
  //要先把对象转换为字符串,不然在下面的模板字符串中会默认调取对象的toString方法,参数变成[Object object],显然不行
  const graph = JSON.stringify(stepTwo(entry))

  return `
  (function(graph) {
    // require 函数的本质是执行一个模块的代码,然后将相应的变量挂载到exports对象上

    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;  // 返回模块的exports对象
    }
    require('${entry}')
  })(${graph})
  `
}

let code = step3("index.js");

console.log(code);

我们得到最终的代码字符串在控制台中执行, 便可以输出 say hello 的文本,

image.png

这篇文章中理解的不太深的地方就是 eval(code) 这里就是执行一段代码,这段代码里面会把 相应的变量挂载到 exports 对象上。