youngwind / blog

梁少峰的个人博客
4.66k stars 385 forks source link

webpack源码学习系列之一:如何实现一个简单的webpack #99

Open youngwind opened 7 years ago

youngwind commented 7 years ago

前言

在上一篇 #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](https://github.com/jquery/esprima)或者[acorn](https://github.com/ternjs/acorn)),将JS代码转换成抽象语法树(AST)**,再对AST进行遍历。这部分的核心代码是[parse.js](https://github.com/youngwind/fake-webpack/blob/1bfcd0edf1/lib/parse.js)。

在处理好了`require`的匹配之后,还有一个问题需要解决。那就是**匹配到`require`之后需要干什么呢?**
举个例子:
```js
// 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. https://github.com/DDFE/DDFE-blog/issues/12
  4. http://hao.jser.com/archive/13881/
  5. http://taobaofed.org/blog/2016/09/09/webpack-flow/

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

KevinHu-1024 commented 7 years ago

👍

chunpu commented 7 years ago

mark

zonghuan commented 7 years ago

66666

ww18 commented 7 years ago

我想知道怎么执行

F3n67u commented 7 years ago

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

youngwind commented 7 years ago

git log --reverse @F3n67u

tanxuewei commented 6 years ago

想请问下webstorm里怎么调试

youngwind commented 6 years ago

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

tanxuewei commented 6 years ago

@youngwind 用vscode调试了,谢谢

hanxu317317 commented 6 years ago

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

chenkang084 commented 3 years ago

我想知道怎么执行

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 commented 3 years ago

从头撸