Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

解析 webpack 源码 #7

Open
BYChoo opened this issue Mar 4, 2021 · 0 comments
Open

解析 webpack 源码 #7

BYChoo opened this issue Mar 4, 2021 · 0 comments

Comments

@BYChoo
Copy link
Owner

BYChoo commented Mar 4, 2021

虽然 Webpack 看上去无所不能,但从其本质上来说,Webpack 实质就是一个“前端模块打包器”。前端模块打包器做的事情很简单:它帮助开发者将 JavaScript 模块(各种类型的模块化规范)打包为一个或多个 JavaScript 脚本文件。

回到最初起源:前端为什么需要一个模块打包器呢?其实理由很简单:

  • 不是所有浏览器都直接支持javascript
  • 前端需要管理依赖脚本,把控不同脚本加载的顺序
  • 前端需要按需加载不同类型的静态资源

想想一下,我们的 Web应用有这样一段内容:

<html>
  <script src="/src/1.js"></script>
  <script src="/src/2.js"></script>
  <script src="/src/3.js"></script>
  <script src="/src/4.js"></script>
  <script src="/src/5.js"></script>
  <script src="/src/6.js"></script>
</html>

每个javascript文件都需要额外的HTTP请求获取,并且因为依赖关系,1.js到6.js需要按顺序加载。因此,打包需求应运而生:

<html>
  <script src="/dist/bundle.js"></script>
</html>

这里需要注意几点:

  • 随着HTTP/2技术的推广,未来长远上看,浏览器像上述代码一样发送多个请求不再是性能瓶颈,但目前来看还过于乐观
  • 并不是将所有脚本都打包在一起就是性能最优,/dist/bundle.js 的size一般较大,但这属于另外的 性能优化话题了

总之,打包器的需求就是前端“刚需”,实现上述打包需要也并不简单,需要考虑:

  • 如何维护不同脚本的打包顺序,保证bundle.js的可用性
  • 如何避免不同脚本、不同模块的命名冲突
  • 在打包过程中,如何确定真正需要的脚本,而不将没有用到的脚本排除在 bundle.js 之外

事实上,虽然当前 webpack 依靠loader机制实现了对于不同类型资源的解析和打包,依靠插件机制实现了第三方介入编译构建的过程,但究其本质,webpack只是一个“无所不能”的打包器,实现了:

a.js + b.js + c.js. => bundle.js

为了简化,以ESM模块化规范举例。假设我们有:

  • circle.js模块求圆形面积
  • square.js模块求正方形面积
  • app.js模块作为主模块

对应内容分别如下代码:

const PI = 3.141;
export default function area(radius) {
  return PI * radius * radius;
}
// filename: square.js
export default function area(side) {
  return side * side;
}
// filename: app.js
import squareArea from './square';
import circleArea from './circle';
console.log('Area of square: ', squareArea(5));
console.log('Area of circle', circleArea(5));

经过 Webpack 打包之后,我们用 bundle.js 来表示 Webpack 处理结果

// filename: bundle.js
const modules = {
  'circle.js': function(exports, require) {
    const PI = 3.141;
    exports.default = function area(radius) {
      return PI * radius * radius;
    }
  },
  'square.js': function(exports, require) {
    exports.default = function area(side) {
      return side * side;
    }
  },
  'app.js': function(exports, require) {
    const squareArea = require('square.js').default;
    const circleArea = require('circle.js').default;
    console.log('Area of square: ', squareArea(5))
    console.log('Area of circle', circleArea(5))
  }
}
webpackBundle({
  modules,
  entry: 'app.js'
});

如上代码,我们维护了 module 变量,存储了不同模块信息,这个map中,key为模块路径名,value为一个被 wrapped 过的模块函数,我们先称之为 module factory function , 该函数形如:

function(exports, require) {
	// 模块内容
}

这样做是为每个模块提供exports和require能力,同时保证了每个模块都处于一个隔离的函数作用域范围。

有了modules变量还不够,我们依赖webpackBundle方法,将所有内容整合在一起。webpackBundle方法接收modules模块信息以及一个入口脚本。代码如下:

function webpackBundle({ modules, entry }) {
  const moduleCache = {};
  
  const require = moduleName => {
    // 如果已经解析并缓存过,直接返回缓存内容
    if (moduleCache[moduleName]) {
      return moduleCache[moduleName];
    }
    
    const exports = {};
    // 这里是为了防止循环引用
    moduleCache[moduleName] = exports;
    // 执行模块内容,如果遇见了 require 方法,则继续递归执行 require 方法 
    modules[moduleName](exports, require);
    
    return moduleCache[moduleName];
  };
  require(entry);
}

手动实现打包器

核心思路如下:

  1. 读取入口文件 (比如entry.js)
  2. 基于AST分析入口文件,并产出依赖列表
  3. 使用Babel将相关模块编译到ES5
  4. 对每个依赖模块产出一个唯一的ID,方便后续读取模块相关内容
  5. 将每个依赖以及经过的Babel编译过后的内容,存储在一个对象中维护
  6. 遍历上一步中的对象,构建出一个依赖图
  7. 将各模块内容bundle产出

首先创建项目:

mkdir bundler-playground && cd $_

并启动npm

npm init -y

安装以下依赖:

  • @babel/parser用于分析源代码,产出 AST;
  • @babel/traverse用于遍历 AST,找到 import 声明;
  • @babel/core用于编译,将源代码编译为 ES5;
  • @babel/preset-env搭配@babel/core使用;
  • resolve用于获取依赖的绝对路径。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant