Webpack Loader源码导读之babel-loader

原文地址:http://hiihl.com/articles/2018/1/15/babelloader.md

webpack应该是当下最主流的前端构建工具之一,但是由于webpack本身糟糕的文档,使得使用者多是会用但不知其所以然,出现问题时难以入手;为此,我想根据自身的理解,详细讲解一些常用loader、plugin源码,深入各个loader及其配置所带来的影响;本系列将以babel-loader开篇,但只讲述babel-loader所做的事情,对于babel部份的内容,将在未来开篇详谈。

先看下源码src目录下的整体结构

|src
|——utils
|  |——exists.js  
|  |——read.js
|  |——relative.js
|——fs-cache.js
|——index.js
|——resolve-rc.js

配置说明

首先,我们进入index.js,找到module.exports = function(source, inputSourceMap),这里就是babel-loader的入口了。
每个webpack loader返回的都是一个function,这个function有两个参数,第一个是待处理的代码,第二个参数是上一loader处理后的sourcemap,如果有的话。
进入loader以后,首先是获取文件名,再通过loaderUtils.getOptions获取loader的配置,即query部份,其中babel-loader支持的配置有:

  • babelrc 配置文件.babelrc的位置,值false时不使用配置文件,配置为具体路径且文件存在时直接使用该文件,否则按默认处理(从当前文件位置开始向上查找
    .babelrc.babelrc.js或使用package.json中的babel配置,具体见resolve-rc.js)
  • cacheDirectory 是否缓存目录,默认false;设置为true时使用默认缓存目录node_modules/.cache/babel-loader或者系统默认临时文件目录os.tmpdir();
    也可以设置具体的文件夹路径
  • cacheIdentifier 缓存标识符;默认包括babel-core、babel-loader的版本号,.babelrc的内容以及BABEL_ENV(没有时会取NODE_ENV)的值
  • sourceMap 是否输出源码,默认值与webpack配置devtool一致;配置此值时,将无视devtool的配置。
  • forceEnv Babel编译的环境变量,默认不配置,环境变量先取BABEL_ENV再取NODE_ENV。当配置此值时,该值将覆盖BABEL_ENV和NODE_ENV。
  • metadataSubscribers 订阅元数据。这个配置在文档中并没有写出来,但是是允许配置的。主要作用是订阅一些编译过程中的一些元数据,订阅以后这些元数据将会被添加
    到webpack的上下文中。通常我们是用不上的,估计在某些babel-plugin中可能会使用到。数据大概是这样的,记录一些module导入导出的信息:
{
  "usedHelpers": [
    "createClass",
    "classCallCheck"
  ],
  "marked": [],
  "modules": {
    "imports": [],
    "exports": {
      "exported": [
        "Test"
      ],
      "specifiers": [
        {
          "kind": "local",
          "local": "Test",
          "exported": "default"
        }
      ]
    }
  }
}

cache详解

真正的编译过程都是在babel-core中执行的,babel-loader的主要作用时babel-core所需配置的一些初始化,以及编译结果的缓存,现在我们主要讲下缓存。
我们先修改下babel-loader的配置:

{
  test: /\.jsx?$/,
  loader: 'babel-loader',
  include: [path.resolve(process.cwd(), 'src')],
  exclude: [path.resolve(process.cwd(), 'node_modules')],
  query: {
    cacheDirectory: path.resolve(process.cwd(), 'tmp') // 配置缓存目录到当前项目tmp下,方便等下查看缓存文件
  }
}

cacheIdentifier的默认值如下,如果我们配置了此值,将覆盖默认值而非合并,所以暂时先不设置该值。

JSON.stringify({
  "babel-loader": pkg.version,
  "babel-core": babel.version,
  babelrc: babelrcPath ? read(fileSystem, babelrcPath) : null,
  env:
    loaderOptions.forceEnv ||
    process.env.BABEL_ENV ||
    process.env.NODE_ENV ||
    "development",
})

当我们配置了cacheDirectory时,loader会先查找缓存文件是否存在,文件名是通过下列方法计算得出

const filename = function(source, identifier, options) {
  const hash = crypto.createHash("SHA1");
  const contents = JSON.stringify({
    source: source,
    options: options,
    identifier: identifier,
  });

  hash.end(contents);

  return hash.read().toString("hex") + ".json.gz";
};

可以看到,文件名是以源码source、loader选项options以及loader标识符identifier三个值的json字符串经过SHA1编码得到的,所以当这三个值任意一个
发生变化时,都会导致文件名发生变化。
当缓存文件不存在,或者以上三个值发生变化导致缓存文件名变成一个不存在的文件时,会调用babel-coretransform方法进行编译,编译结果包含code
mapmetadata三个,其中map即与源码的一些映射关系,这三个内容将保存在缓存文件中;
缓存文件是一个经过压缩的JSON内容长这样:

|tmp
|  |-- 9d08ce6a6158ff5416a96e2290c7243607f9f5c8.json.gz
|  |-- cceb4d9049dfb84308e4cdd7eeedbdadc98c7c09.json.gz

缓存的内容示例:

{
  "code": "'use strict';\n\nvar _test = require('./test');\n\nvar _test2 = _interopRequireDefault(_test);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nvar test = new _test2.default();\n\nconsole.log(test.toString());",
  "map": {
    "version": 3,
    "sources": [
      "src/index.js"
    ],
    "names": [
      "test",
      "console",
      "log",
      "toString"
    ],
    "mappings": ";;AAAA;;;;;;AAEA,IAAMA,OAAO,oBAAb;;AAEAC,QAAQC,GAAR,CAAYF,KAAKG,QAAL,EAAZ",
    "file": "index.js",
    "sourceRoot": "/Users/yzf/webpack-tuition/loaders/babel",
    "sourcesContent": [
      "import Test from './test';\n\nconst test = new Test();\n\nconsole.log(test.toString());\n"
    ]
  },
  "metadata": {
    "usedHelpers": [
      "interopRequireDefault"
    ],
    "marked": [],
    "modules": {
      "imports": [
        {
          "source": "./test",
          "imported": [
            "default"
          ],
          "specifiers": [
            {
              "kind": "named",
              "imported": "default",
              "local": "Test"
            }
          ]
        }
      ],
      "exports": {
        "exported": [],
        "specifiers": []
      }
    }
  }
}

当缓存文件存在时,则将缓存中的编译结果read直接使用。

小结

babel-loader做的事情其实比较简单,本文只是作为一个引子,开启webpack常用loader的揭秘之路,对于babel整个编译过程,未来可能会单独开篇深入讲解,
如有兴趣欢迎关注我的博客hiihl.com

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,997评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,603评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,359评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,309评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,346评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,258评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,122评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,970评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,403评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,596评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,769评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,464评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,075评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,705评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,848评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,831评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,678评论 2 354

推荐阅读更多精彩内容