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源码学习系列之一:如何实现一个简单的webpack #99

Open
youngwind opened this issue Feb 4, 2017 · 12 comments
Open

Comments

@youngwind
Copy link
Owner

youngwind commented Feb 4, 2017

前言

在上一篇 #98 中,我们通过实现requireJS,对模块化有了一些认识。今天我们更进一步,看看如何实现一个简单的webpack,实现的源码参考这里

目标

现在的webpack是一个庞然大物,我们不可能实现其所有功能。
那么,应该将目光聚焦在哪儿呢?
webpack的第一个commit可以看出,其当初最主要的目的是在浏览器端复用符合CommonJS规范的代码模块。这个目标不是很难,我们努力一把还是可以实现的。

注意:在此我们不考虑插件、loaders、多文件打包等等复杂的问题,仅仅考虑最基本的问题:如何将多个符合CommonJS规范的模块打包成一个JS文件,以供浏览器执行。

bundle.js

显然,浏览器没法直接执行CommonJS规范的模块,怎么办呢?
答案:将其转换成一个自执行表达式
注意:此处涉及到webpack构建出来的bundle.js的内部结构问题,如果不了解bundle.js具体是如何执行的,请务必搞清楚再往下阅读。可以参考 #64 或者这里

例子

我们实际要处理的例子是这个:example依赖于a、b和c,而且c位于node_modules文件夹中,我们要将所有模块构建成一个JS文件,就是这里的output.js

思路

仔细观察output.js,我们能够发现:

  1. 不管有多少个模块,头部那一块都是一样的,所以可以写成一个模板,也就是templateSingle.js
  2. 需要分析出各个模块间的依赖关系。也就是说,需要知道example依赖于a、b和c
  3. c模块位于node_modules文件夹当中,但是我们调用的时候却可以直接require('c'),这里肯定是存在某种自动查找的功能。
  4. 在生成的output.js中,每个模块的唯一标识是模块的ID,所以在拼接output.js的时候,需要将每个模块的名字替换成模块的ID。也就是说,
// 转换前
let a = require('a');
let b = require('b');
let c = require('c');

// 转换后
let a = require(/* a */1);
let b = require(/* b */2);
let c = require(/* c */3);

ok,下面我们来逐一看看这些问题。

分析模块依赖关系

CommonJS不同于AMD,是不会在一开始声明所有依赖的。CommonJS最显著的特征就是用到的时候再require,所以我们得在整个文件的范围内查找到底有多少个require
怎么办呢?
最先蹦入脑海的思路是正则。然而,用正则来匹配require,有以下两个缺点:

  1. 如果require是写在注释中,也会匹配到。
  2. 如果后期要支持require的参数是表达式的情况,如require('a'+'b'),正则很难处理。

因此,正则行不通。
一种正确的思路是:使用JS代码解析工具(如esprima或者acorn),将JS代码转换成抽象语法树(AST),再对AST进行遍历。这部分的核心代码是parse.js

在处理好了require的匹配之后,还有一个问题需要解决。那就是匹配到require之后需要干什么呢?
举个例子:

// example.js
let a = require('a');
let b = require('b');
let c = require('c');

这里有三个require,按照CommonJS的规范,在检测到第一个require的时候,根据require即执行的原则,程序应该立马去读取解析模块a。如果模块a中又require了其他模块,那么继续解析。也就是说,总体上遵循深度优先遍历算法。这部分的控制逻辑写在buildDeps.js中。

找到模块

在完成依赖分析的同时,我们需要解决另外一个问题,那就是如何找到模块?也就是模块的寻址问题。
举个例子:

// example.js
let a = require('a');
let b = require('b');
let c = require('c');

在模块example.js中,调用模块a、b、c的方式都是一样的。
但是,实际上他们所在的绝对路径层级并不一致:a和bexample同级,而c位于与example同级的node_modules。所以,程序需要有一个查找模块的算法,这部分的逻辑在resolve.js中。

目前实现的查找逻辑是:

  1. 如果给出的是绝对路径/相对路径,只查找一次。找到?返回绝对路径。找不到?返回false。
  2. 如果给出的是模块的名字,先在入口js(example.js)文件所在目录下寻找同名JS文件(可省略扩展名)。找到?返回绝对路径。找不到?走第3步。
  3. 在入口js(example.js)同级的node_modules文件夹(如果存在的话)查找。找到?返回绝对路径。找不到?返回false。

当然,此处实现的算法还比较简陋,之后有时间可以再考虑实现逐层往上的查找,就像nodejs默认的模块查找算法那样。

拼接output.js

这是最后一步了。
在解决了模块依赖模块查找的问题之后,我们将会得到一个依赖关系对象depTree,此对象完整地描述了以下信息:都有哪些模块,各个模块的内容是什么,他们之间的依赖关系又是如何等等。具体的结构如下:

{
    "modules": {
        "/Users/youngwind/www/fake-webpack/examples/simple/example.js": {
            "id": 0,
            "filename": "/Users/youngwind/www/fake-webpack/examples/simple/example.js",
            "name": "/Users/youngwind/www/fake-webpack/examples/simple/example.js",
            "requires": [
                {
                    "name": "a",
                    "nameRange": [
                        16,
                        19
                    ],
                    "id": 1
                },
                {
                    "name": "b",
                    "nameRange": [
                        38,
                        41
                    ],
                    "id": 2
                },
                {
                    "name": "c",
                    "nameRange": [
                        60,
                        63
                    ],
                    "id": 3
                }
            ],
            "source": "let a = require('a');\nlet b = require('b');\nlet c = require('c');\na();\nb();\nc();\n"
        },
        "/Users/youngwind/www/fake-webpack/examples/simple/a.js": {
            "id": 1,
            "filename": "/Users/youngwind/www/fake-webpack/examples/simple/a.js",
            "name": "a",
            "requires": [],
            "source": "// module a\n\nmodule.exports = function () {\n    console.log('a')\n};"
        },
        "/Users/youngwind/www/fake-webpack/examples/simple/b.js": {
            "id": 2,
            "filename": "/Users/youngwind/www/fake-webpack/examples/simple/b.js",
            "name": "b",
            "requires": [],
            "source": "// module b\n\nmodule.exports = function () {\n    console.log('b')\n};"
        },
        "/Users/youngwind/www/fake-webpack/examples/simple/node_modules/c.js": {
            "id": 3,
            "filename": "/Users/youngwind/www/fake-webpack/examples/simple/node_modules/c.js",
            "name": "c",
            "requires": [],
            "source": "module.exports = function () {\n    console.log('c')\n}"
        }
    },
    "mapModuleNameToId": {
        "/Users/youngwind/www/fake-webpack/examples/simple/example.js": 0,
        "a": 1,
        "b": 2,
        "c": 3
    }
}

根据这个depTree对象,我们便能完成这最后的一步:**output.js文件的拼接。**其控制逻辑无非是一层循环,写在writeChunk.js中。
但是这里有一个需要注意的地方,那就是本文思路章节提到的第4点:要把模块名转换成模块ID,这是writeSource.js所要完成的功能。

至此,我们就实现了一个非常简单的webpack了。

遗留问题

  1. 尚未支持require('a' + 'b')这种情况。
  2. 如何实现自动 watch 的功能?
  3. 其loader或者插件机制又是怎样的?
  4. ……

参考资料

  1. webpack 源码解析
  2. http://www.jianshu.com/p/01a606c97d76
  3. [webpack]源码解读:命令行输入webpack的时候都发生了什么? DDFE/DDFE-blog#12
  4. http://hao.jser.com/archive/13881/
  5. http://taobaofed.org/blog/2016/09/09/webpack-flow/

========EOF===========

@KevinHu-1024
Copy link

👍

@chunpu
Copy link

chunpu commented Feb 7, 2017

mark

@youngwind youngwind changed the title webpack源码学习:如何实现一个简单的webpack webpack源码学习系列之一:如何实现一个简单的webpack Feb 10, 2017
@zonghuan
Copy link

66666

@ww18
Copy link

ww18 commented Mar 3, 2017

我想知道怎么执行

@F3n67u
Copy link

F3n67u commented Mar 29, 2017

请教一下:你怎么能找到webpack的第一个commit时的镜像的?

@youngwind
Copy link
Owner Author

git log --reverse @F3n67u

@tanxuewei
Copy link

想请问下webstorm里怎么调试

@youngwind
Copy link
Owner Author

webstorm 有 debug 功能,你可以参考一下这里:http://www.cnblogs.com/jinguangguo/p/4809886.html @tanxuewei

@tanxuewei
Copy link

@youngwind 用vscode调试了,谢谢

@hanxu317317
Copy link

好多人.明明的官方的文档,死活就是不看,非得百度

@chenkang084
Copy link

chenkang084 commented Aug 10, 2020

我想知道怎么执行

node bin/webpack.js ./examples/simple/example.js;

vscode里面,打开launch.json,配置如下:

{
      "type": "node",
      "request": "launch",
      "name": "debug js file",
      "program": "${workspaceFolder}/bin/webpack.js",
      "args": ["./examples/simple/example.js"]
    }

@Mica-Ma
Copy link

Mica-Ma commented Oct 23, 2020

从头撸

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

10 participants