webpack 打包原理

发布于 2025-01-08 09:23:07 字数 9079 浏览 5 评论 0

webpack.config.js 添加如下配置。

const path = require('path')

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
}

通过基础的 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__);
    return module.exports;
  }
  // 此处省略部分代码
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
  ({
    "./src/a.js":
      (function (module, exports) {
        eval("exports.a = 'a 模块'\r\n\n\n//# sourceURL=webpack:///./src/a.js?");
      }),
    "./src/index.js":
      (function (module, exports, __webpack_require__) {
        eval("const a = __webpack_require__(/*! ./a */ \"./src/a.js\")\r\nconsole.log(a)\r\n\n\n//# sourceURL=webpack:///./src/index.js?");
      })
  });

通过删减部分代码之后,我们很容易看出代码是通过一个自执行函数处理的

(function (modules) {
  // 此处省略部分代码
})({})

自执行函数的参数是一个对象,对象的属性名是路径,属性值是函数

{
  "./src/a.js":
    (function (module, exports) {
      eval("exports.a = 'a 模块'\r\n\n\n//# sourceURL=webpack:///./src/a.js?");
    }),
  "./src/index.js":
    (function (module, exports, __webpack_require__) {
      eval("const a = __webpack_require__(/*! ./a */ \"./src/a.js\")\r\nconsole.log(a)\r\n\n\n//# sourceURL=webpack:///./src/index.js?");
    })
}

我的原始打包文件是这样的依赖关系

src
	a.js -> exports.a = 'a 模块'
    index.js -> const a = require('./a');console.log(a);

通过原始文件和打包后的文件对比,可以知道 __webpack_require__ 是 webpack 实现的 require 方法,用来加载模块,原理和 commonJS 原理十分类似。因为浏览器是不支持 commonJS 原理,commonJS 原理只能在 node 环境中用,直接在浏览器却无法运行,所以要 webpack 自己去实现模块加载。

这里所说的是打包之后的代码是运行在浏览器,打包这个过程是 node 运行的。

值得注意的是模块的路径打包之后都是相对于当前工作目录的路径,就是相对于运行打包命令的路径。

命令

如果我们要自己实现一个 webpack,就必须写一个 webpack 包,发布的 npm 官方网站中,才能使用 npx webpack 命令,可是在本地开发必须得边开发边验证我们自己写的代码是否正确,我们可以通过 npm link 把包链接到全局,这样就可以不用发布到 npm 官方网站了,就直接使用 npx webpack 命令。开发完成再发布即可。

下面是我的 webpack,名字叫 z-pack

{
  "name": "z-pack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "bin": {
    "z-pack": "./bin/z-pack.js"
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

当在 CMD 中执行命令 z-pack 将会执行 ./bin/z-pack.js 的文件,bin 字段就是 npm 配置可执行文件的字段,是固定的字段名

#! /usr/bin/env node

console.log('hello')

在代码的第一行,必须指定代码的运行环境为 node,之后再写 JS 代码。

原理

首先是找到配置文件,然后开始编译执行

#! /usr/bin/env node

const path = require('path')
const configPath = path.resolve('webpack.config.js')
const config = require(configPath)
const compiler = new Compiler(config)
const Compiler = require('../lib/Compiler')
compiler.run()

从入口模块开始创建依赖关系

class Compiler {
  constructor (config) {
    this.config = config
    this.modules = {}
    this.entry = config.entry
    // 工作目录
    this.root = process.cwd()
  }
  buildModule(modulePath, inEntry) {

  }
  run () {
    // 从入口模块开始创建依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true)

    this.emitFile()
  }
  emitFile () {

  }
}

计算路径

let fs = require('fs')
let path = require('path')

class Compiler {
  constructor (config) {
    this.config = config
    this.modules = {}
    this.entry = config.entry
    // 工作目录
    this.root = process.cwd()
  }
  buildModule(modulePath, isEntry) {
    
    // 构建模块谱图
    let source = this.getSource(modulePath)
    // 计算相当路径 -> ./src/index
    let moduleName = './' + path.relative(this.root, modulePath).replace(path.sep, '/');
    // src/index 得到路径 ./src
    let parentName = path.dirname(moduleName)
    // 标注为主模块
    if (isEntry) {
      this.entryId = moduleName
    }
    console.log(moduleName, parentName)
  }
  getSource(modulePath) {
    // 获取模块内容
    let content = fs.readFileSync(modulePath, 'utf8')
    return content
  }
  run () {
    // 从入口模块开始创建依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true)

    this.emitFile()
  }
  emitFile () {

  }
}


module.exports = Compiler

通过语法树,将 require 替换 __webpack_require__

let fs = require('fs')
let path = require('path')
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const generate = require('babel-generator').default;

class Compiler {
  constructor (config) {
    this.config = config
    this.modules = {}
    this.entry = config.entry
    // 工作目录
    this.root = process.cwd()
  }
  buildModule(modulePath, isEntry) {
    
    // 构建模块谱图
    let source = this.getSource(modulePath)
    // 计算相当路径 -> ./src/index
    let moduleName = './' + path.relative(this.root, modulePath).replace(path.sep, '/');
    // src/index 得到路径 ./src
    let parentName = path.dirname(moduleName)
    // 标注为主模块
    if (isEntry) {
      this.entryId = moduleName
    }
    let {r, dependencies} = this.parse(source, parentName)
    this.modules[moduleName] = r
    dependencies.forEach(dep => {
      this.buildModule(path.join(this.root, dep), false)
    })
  }
  parse (source, parentDir) {
    const ast = babylon.parse(source);
    let dependencies = [];
    traverse(ast, {
      CallExpression(p) { // 匹配所有的调用表达式
        let node = p.node;
        if (node.callee.name === 'require') {
          node.callee.name = '__webpack_require__';
          let value = node.arguments[0].value;
          let ext = path.extname(value);
          value = ext ? value : `${value}.js`;
          value = path.join(parentDir, value);
          value = './' + value.replace(path.sep, '/');
          node.arguments[0].value = value;
          dependencies.push(value);
        }
      }
    });
    let r = generate(ast);
    // this.hooks.parser.call(this);
    return { r: r.code, dependencies }
  }
  getSource(modulePath) {
    // 获取模块内容
    let content = fs.readFileSync(modulePath, 'utf8')
    return content
  }
  run () {
    // 从入口模块开始创建依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true)
    console.log(this.modules, '模块')
    this.emitFile()
  }
  emitFile () {

  }
}


module.exports = Compiler

最后写入打包之后的文件

let fs = require('fs')
let path = require('path')
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const generate = require('babel-generator').default;

class Compiler {
  constructor (config) {
    this.config = config
    this.modules = {}
    this.entry = config.entry
    // 工作目录
    this.root = process.cwd()
  }
  buildModule(modulePath, isEntry) {
    
    // 构建模块谱图
    let source = this.getSource(modulePath)
    // 计算相当路径 -> ./src/index
    let moduleName = './' + path.relative(this.root, modulePath).replace(path.sep, '/');
    // src/index 得到路径 ./src
    let parentName = path.dirname(moduleName)
    // 标注为主模块
    if (isEntry) {
      this.entryId = moduleName
    }
    let {r, dependencies} = this.parse(source, parentName)
    this.modules[moduleName] = r
    dependencies.forEach(dep => {
      this.buildModule(path.join(this.root, dep), false)
    })
  }
  parse (source, parentDir) {
    const ast = babylon.parse(source);
    let dependencies = [];
    traverse(ast, {
      CallExpression(p) { // 匹配所有的调用表达式
        let node = p.node;
        if (node.callee.name === 'require') {
          node.callee.name = '__webpack_require__';
          let value = node.arguments[0].value;
          let ext = path.extname(value);
          value = ext ? value : `${value}.js`;
          value = path.join(parentDir, value);
          value = './' + value.replace(path.sep, '/');
          node.arguments[0].value = value;
          dependencies.push(value);
        }
      }
    });
    let r = generate(ast);
    // this.hooks.parser.call(this);
    return { r: r.code, dependencies }
  }
  getSource(modulePath) {
    // 获取模块内容
    let content = fs.readFileSync(modulePath, 'utf8')
    return content
  }
  run () {
    // 从入口模块开始创建依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true)
    console.log(this.modules, '模块')
    this.emitFile()
  }
  emitFile () {
    let ejs = require('ejs');
    let templateStr = this.getSource(path.resolve(__dirname, './template.ejs'));
    let str = ejs.render(templateStr, {
      entryId: this.entryId,
      modules: this.modules
    })
    let { filename, path: p } = this.config.output;
    // 将内容写到文件中
    // 资源文件库
    this.assets = {
      [filename]: str
    }
    Object.keys(this.assets).forEach(key => {
      fs.writeFileSync(path.join(p, key), this.assets[key]);
    })
    // this.hooks.emitFile.call(this);
  }
}


module.exports = Compiler

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

三生路

暂无简介

文章
评论
26 人气
更多

推荐作者

七七

文章 0 评论 0

囍笑

文章 0 评论 0

盛夏尉蓝

文章 0 评论 0

ゞ花落谁相伴

文章 0 评论 0

Sherlocked

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文