动态加载 Webpack Entry

首发于 https://blog.leodots.me/post/9-dynamic-add-webpack-entry.html

在多页面的项目中,一般我们会把每个页面作为一个 webpack 的 entry,比如像下面这样的配置文件

module.exports = {
    entry: {
        page1: "./page1",
        page2: "./page2"
    }
}

当一个项目很庞大的时候,比如有上百个 entry,如果我们只是要改其中一个页面,但是却要把全部 entry 都打包,势必速度就会大大降低。
所以会先让用户选择某一个,然后让 webpack 只打包这个 entry。

然而这也带来另外一个问题,如果我们要改另外的页面,就得不得不结束当前的程序,重新运行命令选择 entry。

next.js

前几天把博客用 next.js 生成,发现 next.js 是根据路由去动态加载 entry。也就是当你访问某个页面,如果这个 entry 之前已经编译过,就直接返回。没有的话,就把这个页面加进去让 webpack 重新编译。这种方式就很好地解决了上面说的问题。 那 next.js 里面是怎样实现的呢?如果你也跑过 demo,应该能在终端看到类似下面的提示

> Building page: xxx

在源码里面搜下,可以很快的定位到 on-demand-entry-handler.js 里面的 ensurePage

{
    async ensurePage (page) {
      await this.waitUntilReloaded()
      page = normalizePage(page)

      const pagePath = join(dir, 'pages', page)
      const pathname = await resolvePath(pagePath)
      const name = join('bundles', pathname.substring(dir.length))

      const entry = [`${pathname}?entry`]

      await new Promise((resolve, reject) => {
        const entryInfo = entries[page]

        if (entryInfo) {
          if (entryInfo.status === BUILT) {
            resolve()
            return
          }

          if (entryInfo.status === BUILDING) {
            doneCallbacks.on(page, processCallback)
            return
          }
        }

        console.log(`> Building page: ${page}`)

        entries[page] = { name, entry, pathname, status: ADDED }
        doneCallbacks.on(page, processCallback)

        invalidator.invalidate()

        function processCallback (err) {
          if (err) return reject(err)
          resolve()
        }
      })
    }
}

参数 page 是当前访问的路径,先从 entries 取对应的 entryInfo

if (entryInfo) {
    if (entryInfo.status === BUILT) {
        resolve()
        return
    }

    if (entryInfo.status === BUILDING) {
        doneCallbacks.on(page, processCallback)
        return
    }
}

如果存在的话,判断下这个 entryInfo 的状态。如果是已经编译过的,直接返回。如果是正在编译,则监听以这个页面为名字的事件。这里猜想当编译完之后,就会触发对应的事件。这里的 doneCallbacks 是一个 EventEmitter 实例。

import { EventEmitter } from 'events'

let doneCallbacks = new EventEmitter()

如果不存在这个 entry 的话,打印提示,初始化 entry,加到 entries 当中。同样的也要监听这个页面事件。

console.log(`> Building page: ${page}`)

entries[page] = { name, entry, pathname, status: ADDED }
doneCallbacks.on(page, processCallback)

invalidator.invalidate()

最后面一句的作用是让 webpack 重新编译,invalidatorInvalidator 实例,用于维护 webpack 打包时候的状态。

class Invalidator {
  constructor (devMiddleware) {
    this.devMiddleware = devMiddleware
    this.building = false
    this.rebuildAgain = false
  }

  invalidate () {
    if (this.building) {
      this.rebuildAgain = true
      return
    }

    this.building = true
    this.devMiddleware.invalidate()
  }

  startBuilding () {
    this.building = true
  }

  doneBuilding () {
    this.building = false
    if (this.rebuildAgain) {
      this.rebuildAgain = false
      this.invalidate()
    }
  }
}

这个类中初始化传入的 devMiddleware 就是我们平时用的 webpack-dev-middleware,项目的 README 也有对 invalidate 进行说明。上面我们只是把 entry 加到了 entries 里面,而 entries 只是一个很普通的全局对象,那 webpack 的配置是怎样被更改的呢?

在这个文件往上翻的话,可以看到初始化了下面这个插件

compiler.plugin('make', function (compilation, done) {
    invalidator.startBuilding()

    const allEntries = Object.keys(entries).map((page) => {
        const { name, entry } = entries[page]
        entries[page].status = BUILDING
        return addEntry(compilation, this.context, name, entry)
    })

    Promise.all(allEntries)
        .then(() => done())
        .catch(done)
})

写过 webpack 插件的应该对上面的代码很熟悉了,这里调用了 invalidator.startBuilding,保证 webpack 只有在编译完成后才能再次编译。然后遍历 entries 这个全局对象,给每个 entry 调用 addEntry 这个方法。

import DynamicEntryPlugin from 'webpack/lib/DynamicEntryPlugin'

function addEntry (compilation, context, name, entry) {
  return new Promise((resolve, reject) => {
    const dep = DynamicEntryPlugin.createDependency(entry, name)
    compilation.addEntry(context, dep, name, (err) => {
      if (err) return reject(err)
      resolve()
    })
  })
}

addEntry 返回一个 Promise,调用 webpack 内置的 DynamicEntryPlugin 插件创建真正的 entry,并加入到 compilation 中。到此,你新加入的 entry 就会被编译了。那编译成功之后呢,我们还监听了对应页面的事件。

这里面其实还初始化了另外一个插件

compiler.plugin('done', function (stats) {
    // Call all the doneCallbacks
    Object.keys(entries).forEach((page) => {
      const entryInfo = entries[page]
      if (entryInfo.status !== BUILDING) return

      entryInfo.status = BUILT
      entries[page].lastActiveTime = Date.now()
      doneCallbacks.emit(page)
    })

    invalidator.doneBuilding()
})

源码里面还做了一些错误处理,这里只看我们关心的部分。在 webpack 编译完成后,先遍历 entries,找到其中状态为 BUILDING 的 entry,把状态改为 BUILT,然后触发对应的事件,doneCallbacks.emit(page),这样之前监听在这个页面的回调函数就被会被触发。

完成上面的事情之后,还调用了 doneBuilding。这个其实是检查一下在刚刚 webpack 的编译过程中,有没有新的 entry 要求被重新编译,有的话,再次调用 invalidate,让 webpack 再编译一次。

后记

在实际的项目当中,可以把上面的处理过程封装成一个 express 的中间件,这样子我们平时开发只要运行一下 make dev,启动一下服务器,然后根据访问的路由去编译对应的 entry 就可以。

最后,还剩下一个问题。一开始的 entry 要初始化成什么呢,如果 webpack 接受到的是一个空的 entry,会直接抛出错误。我的做法是随机选一个 entry 初始化,如果第一次访问到这个随机 entry,还可以加多一个彩蛋效果。比如在终端输出

Boom!!! You have hit the random entry.

(完)

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

推荐阅读更多精彩内容

  • 在现在的前端开发中,前后端分离、模块化开发、版本控制、文件合并与压缩、mock数据等等一些原本后端的思想开始...
    Charlot阅读 5,419评论 1 32
  • 写在开头 先说说为什么要写这篇文章, 最初的原因是组里的小朋友们看了webpack文档后, 表情都是这样的: (摘...
    Lefter阅读 5,266评论 4 31
  • 无意中看到zhangwnag大佬分享的webpack教程感觉受益匪浅,特此分享以备自己日后查看,也希望更多的人看到...
    小小字符阅读 8,125评论 7 35
  • GitChat技术杂谈 前言 本文较长,为了节省你的阅读时间,在文前列写作思路如下: 什么是 webpack,它要...
    萧玄辞阅读 12,662评论 7 110
  • 这两天一直在下雨,感觉突然要到冬天了。今一天都窝在沙发里刷剧,看小说,了解别人的故事。刚才起身收拾了中午的外...
    余言之阅读 272评论 0 0