精华内容
下载资源
问答
  • 18.重学webpack——webpack动态加载原理/webpack异步加载原理/webpack动态解析import()原理
    千次阅读
    2021-08-03 15:00:12

    【重学webpack系列——webpack5.0】

    1-15节主要讲webpack的使用,当然,建议结合《webpack学完这些就够了》一起学习。
    从16节开始,专攻webpack原理,只有深入原理,才能学到webpack设计的精髓,从而将技术点运用到实际项目中。
    可以点击上方专栏订阅哦。

    以下是本节正文:


    import()对于webpack来说,是一个天然的分割点,也就是说,webpack碰到import()会对import()进行解析。怎么解析的,过程主要是:

    面试题:webpack解析import()的原理/webpack懒加载原理/webpack异步加载原理:

    1. 首先,webpack遇到import方法时,会将其当成一个代码分割点,也就是说碰到import方法了,那么就去解析import方法

    2. 然后,import引用的文件,webpack会将其编译成一个jsonp,也就是一个自执行函数,然后函数内部是引用的文件的内容,因为到时候是通过jsonp的方法去加载的。

    3. 具体就是,import引用文件,会先调用require.ensure方法(打包的结果来看叫require.e),这个方法主要是构造一个promise,会将resolverejectpromise放到一个数组中,将promise放到一个队列中。

    4. 然后,调用require.load(打包结果来看叫require.l)方法,这个方法主要是创建一个jsonp,也就是创建一个script标签,标签的url就是文件加载地址,然后塞到document.head中,一塞进去,就会加载该文件了。

    5. 加载完,就去执行这段jsonp,主要就是把moduleIdmodule内容存到modules数组中,然后再去走webpack内置的require

    6. webpack内置的require,主要是先判断缓存,这个moduleId是否缓存过了,如果缓存过了,就直接返回。如果没有缓存,再继续往下走,也就是加载module内容,然后最终内容会挂在都module,exports上,返回module.exports就返回了引用文件的最终执行结果。

    • 入口文件
    import(/* webpackChunkName: "title"*/'./title').then(result => {
      console.log(result);
    })
    
    • 依赖文件
    module.exports = "title"
    
    • webpack编译结果及分析(缩减过后)
    var modules = {}
    var cache = {}
    
    function require(moduleId) {
      var cachedModule = cache[moduleId];
      if (cachedModule) {
        return cachedModule.exports; // 最终返回的都是module.exports
      }
      var module= cache[moduleId] = {
        exports: {}
      }
      modules[moduleId](module, module.exports, require);
      return module.exports;
    }
    // 定义查找代码块的方法
    require.find = {};
    // 通过JSONP一部加载指定的代码块
    require.ensure = (chunkId) => {
      let promises = [];
      require.find.jsonp(chunkId, promises); // 在jsonp中会创建一个promise,并且添加到promises数组中
      return Promise.all(promises);
    }
    require.publicPath = ''; // 资源文件的访问路径,默认是空字符串,值从webpack.config.js的output的publicPath中去找
    require.unionFileName = (chunkId) => { // 统一文件名
      return "" + chunkId + ".js"; // title.js
    }
    require.load = (url) => {
      let script = document.createElement('script');
      script.src = url;
      document.head.appendChild(script); //一旦append之后,浏览器会立刻请求脚本
    }
    // 已经安装或者加载好的或者加载中的chunk
    var installedChunks = {
      "main": 0, // key为“main”表示主入口文件,0表示ok,也就是已经加载就绪
      // "title": [resolve, reject, promise]
    }
    require.find.jsonp = (chunkId, promises) => {
      var installedChunkData;
      let promise = new Promise((resolve, reject) => {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
        /*
          这个相当于installedChunks值变成了
          {
            "main": 0,
            "title": [resolve, reject, promise]
          }
        */
      })
      installedChunkData[2] = promise;
      promises.push(promise);
      var url = require.publicPath + require.unionFileName(chunkId); //title.js
      require.load(url); // 常见script,然后塞入document.head,然后会自动加载该文件
    }
    var webpackJsonpCallback = ([chunkIds, moreModules]) => {
      var resolves = [];
      for (let i = 0; i < chunkIds.length; i++) {
        let chunkId = chunkIds[i];
        resolves.push(installedChunks[chunkId][0]);//把chunk对应的promise的resolve方法添加到resolves数组里去
        installedChunks[chunkId][0] = 0; // 表示已经添加完成
      }
      for (let moduleId in moreModules) {
        modules[moduleId] = moreModules[moduleId];
      }
      while(resolves.length){
        resolves.shift()(); // 让promise的resolve执行,让promise成功
      }
    }
    // 由于打包好了依赖文件title.js是被
    var chunkLoadingGlobal = self["webpackChunk_1_webpack_bundle"] = self["webpackChunk_1_webpack_bundle"] || [];
    // 重写数组的push方法
    chunkLoadingGlobal.push = webpackJsonpCallback;
    require.ensure("title") // 先加载代码块,这里的参数"title"就是魔法字符串中的webpackChunkName的值title
      .then(require.bind(require, "./src/title.js")) // require模块
      .then(result => { // 获取结果
        console.log(result);
      })
    
    更多相关内容
  • 本篇文章主要介绍了webpack 样式加载的实现原理,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 因为Vue 是SPA,所以首页第一次加载时会把所有的组件以及组件相关的资源全都加载了。这样就会导致首页加载加载了许多首页用不上的资源,造成网站首页打开速度变慢,...本文将对懒加载的实现原理以及使用进行讲解。

    前言

    Vue 为什么需要懒加载(按需加载)?

    学习Vue的时候,各类教程都会告诉我们:Vue 的特点是SPA——Single Page Application(单页应用程序)。它有着诸如:“只有第一次会加载页面, 以后的每次页面切换,只需要进行组件替换;减少了请求体积,加快页面响应速度,降低了对服务器的压力” 等等优点。

    但是呢!因为Vue 是SPA,所以首页第一次加载时会把所有的组件以及组件相关的资源全都加载了。这样就会导致首页加载时加载了许多首页用不上的资源,造成网站首页打开速度变慢的问题,降低用户体验。

    为了解决上面问题,我们需要对Vue实现组件懒加载(按需加载)。

    阅前悉知:

    下面,我将简单讲解一下Javascript的懒加载(按需加载)原理以及在Vue上的应用。
    原理部分为我个人参考多篇文章总结而来,有可能存在错误。希望大家在阅读时,抱着质疑的态度去阅读
    ·········································································································································
    本文所讲的JS、webpack以及VueCLI版本分别为:

    • Javascript:ES2015(ES6)及以上
    • webpack:webpack v4及以上
    • VueCLI: VueCLI v4

    什么是懒加载(按需加载)?

    懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
    —— 摘自《webpack——懒加载》

    上面这段话,用一句俗语讲就是“占着茅坑不拉屎”。先占着茅坑,屎意来的时候再来!

    也就是,组件先在路由里注册但不进行组件的加载与执行,等到需要访问组件的时候才进行真正的加载。

    懒加载(按需加载)的前提

    懒加载前提
    进行懒加载的子模块(子组件)需要是一个单独的文件。

    为什么呢?因为懒加载是对子模块(子组件)进行延后加载。如果子模块(子组件)不单独打包,而是和别的模块掺和在一起,那其他模块加载时就会将整个文件加载出来了。这样子模块(子组件)就被提前加载出来了。

    所以,要实现懒加载,就得先将进行懒加载的子模块(子组件)分离出来

    懒加载前提的实现:ES6的动态地加载模块——import()

    调用 import() 之处,被作为分离的模块起点,意思是,被请求的模块和它引用的所有子模块,会分离到一个单独的 chunk 中。
    ——摘自《webpack——模块方法》的import()小节

    简单来讲就是,通过import()引用的子模块会被单独分离出来,打包成一个单独的文件(打包出来的文件被称为chunk )。

    这里有个知识的前提:
    项目通过webpack打包时会进行资源整合,也就是会把项目中的JS、CSS等文件按照一定的规则进行合并,已达到减少资源请求的目的。

    依照webpack原本的打包规则打包项目,我们就无法确定子模块在打包出来的哪个JS文件中,而且子模块的代码会和其他代码混合在同一个文件中。这样就无法进行懒加载操作。所以,要实现懒加载,就得保证懒加载的子模块代码单独打包在一个文件中。

    代码示例:

    构建一个简单的webpack项目:

    1. 首先,webpack.config.js 文件配置如下:
    /*webpack.config.js*/
    
    const path = require('path')
    
    module.exports = {
        entry:'./src/main.js', //入口文件
        output: {
            path: path.resolve(__dirname, 'dist'),
            chunkFilename: '[name].bundle.js',
            filename: 'bundle.js',
        }
    }
    
    1. 创建入口文件main,js
    /* main.js */
    
    // 这里引入con.js,注意没有使用import()
    
    require('./con.js')
    
    1. 创建被引入文件con.js
    /* con.js */
    function cons(){
      console.log("123")
    }
    
    module.exports = {cons}
    

    具体目录结构如下:
    目录结构
    接着运行npm run build进行打包。结果如下:
    在这里插入图片描述
    可以看到使用require()引入con.js,打包后的结果是con.js合并到的bundle.js

    打开bundle.js,可以看到con.js代码被嵌入bundle.js
    在这里插入图片描述
    接下来,使用import()引入con.js

    /* main.js */
    
    // 这里使用import()引入con.js
    import(/* webpackChunkName: "con" */ './con.js')
    

    打包结果如下:

    在这里插入图片描述
    展开bundle.js
    在这里插入图片描述
    可以看到,使用import()引入con.jscon.js打包成独立的js文件。

    注意:
    上面的例子,使用的都是同一套webpackconfig.js配置文件。所以,实现上面功能仅仅是因为import()的使用。

    小知识点:import()中的注释

    我们注意到,import()括号里面有一串注释。如:

    import(/* webpackChunkName: "con" */ './con.js')
    

    它并不是可有可无的东西。通过这个注释,再配合webpack.config.jsoutput.chunkFilename,我们可以设置打包生成的文件(chunk)的名字。
    例如,上面例子的webpack配置:

    module.exports = {
        entry:'./src/main.js', //入口文件
        output: {
            path: path.resolve(__dirname, 'dist'),
            chunkFilename: '[name].bundle.js',
            filename: 'bundle.js',
        }
    }
    

    这里设定了chunkFilename的命名规则为:[name]+.+bundle.js。这里的[name]就是/* webpackChunkName: "con" */设定的值。

    除了webpackChunkName,还有其他属性设定,详情请看webpack——Magic Comments

    再说明一点:
    webpack 打包生成的chunk有一下几种:

    • webpack当中配置的入口文件(entry)是chunk,可以理解为entry chunk;
    • 入口文件以及它的依赖文件通过code split (代码分割)出来的也是chunk(也就是我们这里一直讲到的),可以理解为children chunk;
    • 通过commonsChunkPlugin创建出来的文件也是chunk,可以理解为commons chunk;
      —— 摘自《 webpack配置中常用的CommonsChunkPlugin认识

    注意:CommonsChunkPlugin 已经从 webpack v4(代号 legato)中移除。想要了解最新版本是如何处理 chunk,请查看 SplitChunksPlugin

    借助import(),我们实现了子模块(子组件)的独立打包(children chunk)。现在,距离实现懒加载(按需加载) 还差关键的一步——如何正确使用独立打包的子模块文件(children chunk)实现懒加载。这也是懒加载的原理。

    借助函数实现懒加载(按需加载)

    首先,我们先来回顾一下JavaScript函数的特性。

    无论使用函数声明还是函数表达式创建函数,函数被创建后并不会立即执行函数内部的代码,只有等到函数被调用之后,才执行内部的代码。

    相信对于这个函数特性,大家都十分清楚的。看到这里,大家对于懒加载的实现可能已经有了思路。

    没错!

    只要将需要进行懒加载的子模块文件(children chunk)的引入语句(本文特指import())放到一个函数内部。然后在需要加载的时候再执行该函数。这样就可以实现懒加载(按需加载)

    这也是懒加载的原理了。

    将上面例子补充完整:

    新增页面文件index.html

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>懒加载演示</title>
    </head>
    <body>
        <button class="button1" style="cursor: pointer;">懒加载 con.js</button>
        <script src="bundle.js"></script>
    </body>
    </html>
    

    main.js改动如下:

    window.onload = () => {
        const btn = document.querySelector('.button1')
        
        // 给按钮添加click事件,按钮被点击后,加载 con.js
        btn.onclick = () => import(/* webpackChunkName: "con" */ './con.js')
    }
    

    目录结构如下:
    在这里插入图片描述
    打包后,运行项目:

    在这里插入图片描述
    可以看到,con.js实现了懒加载(按需加载)

    懒加载(按需加载)实现原理的概括

    回顾前两节的内容,懒加载(按需加载)原理分为两步:

    1. 将需要进行懒加载的子模块打包成独立的文件(children chunk);
    2. 借助函数来实现延迟执行子模块的加载代码;

    是不是很简单呀!这里的技术难点就是如何将懒加载的子模块打包成独立的文件。好在ES6提供了import()。然后这一切就变得十分简单了。

    在 Vue-router 实现路由懒加载(按需加载)

    讲了这么多,最后再来讲讲懒加载在vue-router的使用吧!
    有了前面的基础,在vue上使用懒加载就变得很简单了。

    以往,我们配置vue-router是这样的:

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    // 这里引入子模块
    import Home from '../views/Home.vue'
    
    Vue.use(VueRouter)
    
    const routes = [{
        path: '/',
        name: 'Home',
        component: Home
      }
    ]
    
    const router = new VueRouter({
      mode: 'history',
      routes
    })
    
    export default router
    

    上面的例子先加载子组件,然后将子组件命名为Home,最后再将Home赋给Vue的 component。这样就导致子组件的提前加载。

    接下来,实现子组件懒加载,则改动如下:

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    
    Vue.use(VueRouter)
    
    const routes = [{
        path: '/',
        name: 'Home',
        // 将子组件加载语句封装到一个function中,将function赋给component
        component: () => import( /* webpackChunkName: "home" */ '../views/Home.vue')
      }
    ]
    
    const router = new VueRouter({
      mode: 'history',
      routes
    })
    
    export default router
    

    将子组件加载语句封装到一个function中,将function赋给component。这样就可以实现Vue-router懒加载(按需加载)

    是不是非常简单!哈哈!

    可能这里有人会疑惑,component可以接收一个function吗?
    这确实可以的。不要被以往的观念束缚。component是对象的一个属性,在Javascript中属性的值是什么类型都可以。

    关于懒加载(按需加载) 在Vue中的应用,详细可参考:《Lazy Load in Vue using Webpack’s code splitting》

    简述另一种子模块打包方法:

    除了ES6 的import()这个方法,webpack本身还提供了另一个方法—— require.ensure()

    require.ensure() 是 webpack 特有的,已经被 import() 取代。
    ——摘自《webpack——module-methods:require.ensure》

    大概用法如下:

      {
        path: '/home',
        name: 'home',
        component: resolve  => require.ensure([], () => resolve (require('@/components/home')), 'demo')
      },
    

    关于require.ensure()更多内容可查看:《webpack——module-methods:require.ensure》《vue路由懒加载》

    结语

    至此,关于懒加载(按需加载) 的说明就结束了。刚开始时对这部分还是挺畏惧的。心想着懒加载,这么高大上的东西一定非常难。后来查阅许多资料,仔细研究后,逐渐了解了懒加载,并借着写这篇文章的机会,边写边学,进一步巩固知识点,算是对懒加载有了较深的理解吧。其实,仔细看来懒加载的原理并不是很复杂。希望本文对大家有所启发与帮助。

    其中可能存在错误,希望各位大佬踊跃指出。

    参考文档

    展开全文
  • 利用webpack对代码进行分割是懒加载的前提,懒加载就是异步调用组件,需要时候才下载(按需加载)。 为什么需要懒加载? 在单页应用中,如果没有应用懒加载,运用webpack打包后的文件将会异常的大,造成进入首页时,...

    vue异步组件懒加载(按需加载)

    利用webpack对代码进行分割是懒加载的前提,懒加载就是异步调用组件,需要时候才下载(按需加载)。

    为什么需要懒加载?
    在单页应用中,如果没有应用懒加载,运用webpack打包后的文件将会异常的大,造成进入首页时,需要加载的内容过多,延时过长,不利于用户体验,而运用懒加载则可以将页面进行划分,需要的时候加载页面,可以有效的分担首页所承担的加载压力,减少首页加载用时。

    vue开发过程中,我们会做出特别多特别多的组件,包括login,header,footer,main等等。
    这样使整个网站看起来就十分的庞大,当我们在打开网页的时候,突然一下子把这些所有的组件加载上来,这么多的请求全部同时开始请求,势必会造成网页打开很慢,使客户得到的是非常差劲的体验。

    因此,vue为我们专门设立了异步组件,通过异步组件,我们可以得到两点好处:
    1、 用不到的组件不会加载,因此网页打开速度会很快,当你用到这个组件的时候,才会通过异步请求进行加载;
    2、 缓存组件,通过异步加载的组件会缓存起来,当你下一次再用到这个组件时,丝毫不会有任何的疑迟,组件很快会从缓存中加载出来。

    异步组件=原理同webpack的按需加载
    好处:
    1)按需加载,可以节省首次加载时间,提高速度,性能优化
    2)第一次加载完成会缓存

    异步组件的描述:

    Vue允许将组件定义为一个异步解析(加载)组件定义的工厂函数,即Vue只在实际需要渲染组件时,才会触发调用工厂函数,并且将结果缓存起来,用于将来再次渲染。

    !!!ps:要使用异步组件,一个比较推荐的方式就是配合webpack代码分离功能。

    例子:(全局|局部)
    法一:结合webpack

    //1)全局:
     Vue.component('component-name',function(resolve){
     //require 语法告诉 webpack自动将编译后的代码分割成不同的块
     //这些块将通过 Ajax 请求自动下载
       require(['./my-async-componnet'],resolve)
     })
     //注册全局组件名,但只有一个名字,没有实体,相当于空的
    //当需要这个组件时,调用上面的工厂函数,触发webpack的异步加载模块方法
    //然后异步请求一个模块,请求成功后,这个模块内容即为组件实体部分,并对应地方渲染,加载内容也缓存下来。
    
    //2)局部
     new Vue({
       components: {
            'component-name':function(resolve) {
               require(['./my-component'], resolve)
            }
       }
     })
    
    
    //例子
    {
        path: '/mainPage',//测试
        name: 'mainPage',
        component: (resolve) => {
          return require(['../views/mainPage/mainPage.vue'], resolve)
        },
      },
    

    法二:通过webpack2+es2015返回一个promise

    //1)全局:
    Vue.component('component-name',
     ()=> import('./my-async-componnet')
     )
    //2) 局部:
     new Vue({
       components: {
           'component-name': () =>  import('./my-async-componnet')
       }
     })
    

    高级异步组件(即处理加载状态)
    工厂对象可以返回一个对象,对象里面的一些配置参数

        const asyncComponent = () => ({
        // 需要加载的组件 (应该是一个 `Promise` 对象)
        component: import('./my-async-componnet'),
        //异步加载时使用的组件(加载中的效果)
        loading: loadingComponent,
        //加载失败时使用的组件
        error: ErrorComponent,
        //展示加载中组件的延时时间,默认200毫秒
        delay: 200,
        //超时时间,超过该时间显示加载失败的组件
        timeout: 3000
        })
    

    单文件组件+vue-router案例:
    2个组件:first、second代表2个页面, 路由文件异步组件方式:

         //传统写法:import First from '@/components/First'
        方法一:
                const first = () => import("../components/first.vue")
                const second = () => import("../components/second.vue")
        方法二:
                const first = resolve => require.ensure([], () => resolve( require(../components/first.vue)),  'chunkname1')
                const second = resolve => require.ensure([], () => resolve( require(../components/second.vue)),  'chunkname2')
          //其余配置如以往一样
          const routes = [
              {
                  path: '/',
                  component: first
               },
                {
                  path: '/',
                  component: second
               }
          ]
    

    webpack中利用require.ensure()实现按需加载  js

    require.ensure()(webpack异步加载|代码分割)

    • 解释1:把js模块独立导出一个个js文件,然后使用这个模块时,webpack会构造这个script dom元素,加入到document.head中,由浏览器自动发起异步请求这个js文件,再写个回调,去定义得到这个js文件后的业务逻辑。
    • 解释2:webpack 在编译时,会静态地解析代码中的 require.ensure(),同时将模块添加到一个分开的 chunk 当中。这个新的 chunk 会被 webpack 通过 jsonp 来按需加载。

    require.ensure这个函数是一个代码分离的分割线,表示 回调里面的require
    是我们想要进行分割出去的,即require(’./baidumap.js’),把baidumap.js分割出去,形成一个webpack打包的单独js文件。当然ensure里面也是可以写一些同步的require的,如:

        var sync = require('syncdemo.js')   //下面ensure里面也用到
        btn.click(function() {
            require.ensure([], function() {
                  var map = require('./map.js') //map.js放在我们当前目录下
                  //这个不会独立出去,因为它已经加载到模块缓存中了
                   var sync = require('syncdemo.js')  
        })
      })
    

    ===》ensure会把没有使用过的require资源进行独立分成成一个js文件

    ensure语法及参数解释

    require.ensure(dependencies: string[], callback: function(require),chunkName:string)

    • 第一个参数:默认[],放置当前这个模块所依赖的其他模块。比如:假设A 和 B都是异步的,B中需要A,则B下载之前,先要下载A :require.ensure([‘A.js’], function...),!!!!需要注意:webpack会把参数里面的依赖异步模块和当前的需要分离出去的异步模块给一起打包成同一个js文件,这里可能会出现一个重复打包的问题, 假设A 和 B都是异步的, ensure A 中依赖B,ensure B中 依赖A,那么会生成两个文件,都包含A和B模块。
    • 第二个参数:当所有依赖都加载完成后,webpack会执行这个回调函数。require对象的一个实现会作为一个参数传递给这个回调函数。因此,我们可以进一步require()依赖和其他模块提供下一步的执行。
    • 第三个参数,chunkname打包后代码块的名字:chunk提供给这个特定的require.ensure()的chunk的名称。通过提供 require.ensure() 不同执行点相同的名称,我们可以保证所有的依赖都会一起放进相同的 文件束(bundle)。

    使用 vue-cli构建的项目,在 默认情况下 ,执行 npm run build 会将所有的js代码打包为一个整体。如果想要让build之后的代码更便于识别,可配置webpack代码,单独打包自己定义的名字(需要配置chunkFileName和publicPath)

      module.exports = {
            entry:  './src/main.js',
            output: {
                  path: path.resolve(__dirname, './dist'),
                  publicPath: '/dist/', //是按需加载单独打包出来的chunk是以publicPath为基准来存放的
                  filename: 'build.js',
                  chunkFilename: 'js/[name]-[chunkhash:8].js' //最终生成的路径和名字
              }
        }
    

    打包结果.png

    目录结果.png

    外部引入一些插件,不要在vue中引入

    列入,我在次项目中有用到moment.js这个插件,在vue内部引入后打包的项目大小要比在外部用src的方式引入打包的项目大个300k左右。

    首先,下载好moment.min.js包,然后在vue项目的webpack.base.conf.js中添加下图箭头所指向的那段代码

    加上上图的代码后是不会将moment.js给打包到js中的。

    最后一步,将刚才下载的moment.min.js包,手动引入到打包好的index.html 文件中。

    注:此方法真的会很有用,如果怕整个项目文件过大的话,还可以用cdn的方法引入:  https://cdn.bootcss.com/moment.js/2.22.1/moment.min.js

    很重要:冒号后面大写的名字是我们自己定义的名字和项目中要使用的名字  如:‘vue’: 'Vue', 后面的Vue 必须要和项目中的保持一致。

    参考地址:

    展开全文
  • 深入了解 webpack 模块加载原理

    千次阅读 2020-09-14 22:40:53
    webpack 模块加载原理

    webpack 是一个模块打包器,在它看来,每一个文件都是一个模块。

    无论你开发使用的是 CommonJS 规范还是 ES6 模块规范,打包后的文件都统一使用 webpack 自定义的模块规范来管理、加载模块。本文将从一个简单的示例开始,来讲解 webpack 模块加载原理。

    CommonJS 规范

    假设现在有如下两个文件:

    // index.js
    const test2 = require('./test2')
    
    function test() {}
    
    test()
    test2()
    
    // test2.js
    function test2() {}
    
    module.exports = test2
    

    以上两个文件使用 CommonJS 规范来导入导出文件,打包后的代码如下(已经删除了不必要的注释):

    (function(modules) { // webpackBootstrap
        // The module cache
        // 模块缓存对象
    	var installedModules = {};
    
        // The require function
        // webpack 实现的 require() 函数
    	function __webpack_require__(moduleId) {
            // Check if module is in cache
            // 如果模块已经加载过,直接返回缓存
    		if(installedModules[moduleId]) {
    			return installedModules[moduleId].exports;
    		}
            // Create a new module (and put it into the cache)
            // 创建一个新模块,并放入缓存
    		var module = installedModules[moduleId] = {
    			i: moduleId,
    			l: false,
    			exports: {}
    		};
    
            // Execute the module function
            // 执行模块函数
    		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    
            // Flag the module as loaded
            // 将模块标识为已加载
    		module.l = true;
    
    		// Return the exports of the module
    		return module.exports;
    	}
    
    
        // expose the modules object (__webpack_modules__)
        // 将所有的模块挂载到 require() 函数上
    	__webpack_require__.m = modules;
    
        // expose the module cache
        // 将缓存对象挂载到 require() 函数上
    	__webpack_require__.c = installedModules;
    
    	// define getter function for harmony exports
    	__webpack_require__.d = function(exports, name, getter) {
    		if(!__webpack_require__.o(exports, name)) {
    			Object.defineProperty(exports, name, { enumerable: true, get: getter });
    		}
    	};
    
    	// define __esModule on exports
    	__webpack_require__.r = function(exports) {
    		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
    			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    		}
    		Object.defineProperty(exports, '__esModule', { value: true });
    	};
    
    	// create a fake namespace object
    	// mode & 1: value is a module id, require it
    	// mode & 2: merge all properties of value into the ns
    	// mode & 4: return value when already ns object
    	// mode & 8|1: behave like require
    	__webpack_require__.t = function(value, mode) {
    		if(mode & 1) value = __webpack_require__(value);
    		if(mode & 8) return value;
    		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    		var ns = Object.create(null);
    		__webpack_require__.r(ns);
    		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
    		return ns;
    	};
    
    	// getDefaultExport function for compatibility with non-harmony modules
    	__webpack_require__.n = function(module) {
    		var getter = module && module.__esModule ?
    			function getDefault() { return module['default']; } :
    			function getModuleExports() { return module; };
    		__webpack_require__.d(getter, 'a', getter);
    		return getter;
    	};
    
    	// Object.prototype.hasOwnProperty.call
    	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
    
    	// __webpack_public_path__
    	__webpack_require__.p = "";
    
    
        // Load entry module and return exports
        // 加载入口模块,并返回模块对象
    	return __webpack_require__(__webpack_require__.s = "./src/index.js");
    })({
      "./src/index.js": (function(module, exports, __webpack_require__) {
        eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?");
      }),
      
      "./src/test2.js": (function(module, exports) {
        eval("function test2() {}\r\n\r\nmodule.exports = test2\n\n//# sourceURL=webpack:///./src/test2.js?");
      })
    });
    

    可以看到 webpack 实现的模块加载系统非常简单,仅仅只有一百行代码。

    打包后的代码其实是一个立即执行函数,传入的参数是一个对象。这个对象以文件路径为 key,以文件内容为 value,它包含了所有打包后的模块。

    {
      "./src/index.js": (function(module, exports, __webpack_require__) {
        eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?");
      }),
      
      "./src/test2.js": (function(module, exports) {
        eval("function test2() {}\r\n\r\nmodule.exports = test2\n\n//# sourceURL=webpack:///./src/test2.js?");
      })
    }
    

    将这个立即函数化简一下,相当于:

    (function(modules){
    	// ...
    })({
    	path1: function1,
    	path2: function2
    })
    

    再看一下这个立即函数做了什么:

    1. 定义了一个模块缓存对象 installedModules,作用是缓存已经加载过的模块。
    2. 定义了一个模块加载函数 __webpack_require__()
    3. … 省略一些其他代码。
    4. 使用 __webpack_require__() 加载入口模块。

    其中的核心就是 __webpack_require__() 函数,它接收的参数是 moduleId,其实就是文件路径。

    它的执行过程如下:

    1. 判断模块是否有缓存,如果有则返回缓存模块的 export 对象,即 module.exports
    2. 新建一个模块 module,并放入缓存。
    3. 执行文件路径对应的模块函数。
    4. 将这个新建的模块标识为已加载。
    5. 执行完模块后,返回该模块的 exports 对象。
       // The require function
       // webpack 实现的 require() 函数
    function __webpack_require__(moduleId) {
           // Check if module is in cache
           // 如果模块已经加载过,直接返回缓存
    	if(installedModules[moduleId]) {
    		return installedModules[moduleId].exports;
    	}
           // Create a new module (and put it into the cache)
           // 创建一个新模块,并放入缓存
    	var module = installedModules[moduleId] = {
    		i: moduleId,
    		l: false,
    		exports: {}
    	};
    
           // Execute the module function
           // 执行模块函数
    	modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    
           // Flag the module as loaded
           // 将模块标识为已加载
    	module.l = true;
    
    	// Return the exports of the module
    	return module.exports;
    }
    

    从上述代码可以看到,在执行模块函数时传入了三个参数,分别为 modulemodule.exports__webpack_require__

    其中 modulemodule.exports 的作用和 CommonJS 中的 modulemodule.exports 的作用是一样的,而 __webpack_require__ 相当于 CommonJS 中的 require

    在立即函数的最后,使用了 __webpack_require__() 加载入口模块。并传入了入口模块的路径 ./src/index.js

    __webpack_require__(__webpack_require__.s = "./src/index.js");
    

    我们再来分析一下入口模块的内容。

    (function(module, exports, __webpack_require__) {
        eval("const test2 = __webpack_require__(/*! ./test2 */ \"./src/test2.js\")\r\n\r\nfunction test() {}\r\n\r\ntest()\r\ntest2()\n\n//# sourceURL=webpack:///./src/index.js?");
      })
    

    入口模块函数的参数正好是刚才所说的那三个参数,而 eval 函数的内容美化一下后和下面内容一样:

    const test2 = __webpack_require__("./src/test2.js")
    function test() {}
    test()
    test2()
    //# sourceURL=webpack:///./src/index.js?
    

    将打包后的模块代码和原模块的代码进行对比,可以发现仅有一个地方发生了变化,那就是 require 变成了 __webpack_require__

    再看一下 test2.js 的代码:

    function test2() {}
    module.exports = test2
    //# sourceURL=webpack:///./src/test2.js?
    

    从刚才的分析可知,__webpack_require__() 加载模块后,会先执行模块对应的函数,然后返回该模块的 exports 对象。而 test2.js 的导出对象 module.exports 就是 test2() 函数。所以入口模块能通过 __webpack_require__() 引入 test2() 函数并执行。

    到目前为止可以发现 webpack 自定义的模块规范完美适配 CommonJS 规范。

    ES6 module

    将刚才用 CommonJS 规范编写的两个文件换成用 ES6 module 规范来写,再执行打包。

    // index.js
    import test2 from './test2'
    
    function test() {}
    
    test()
    test2()
    
    // test2.js
    export default function test2() {}
    

    使用 ES6 module 规范打包后的代码和使用 CommonJS 规范打包后的代码绝大部分都是一样的。

    一样的地方是指 webpack 自定义模块规范的代码一样,唯一不同的是上面两个文件打包后的代码不同。

    {
     	"./src/index.js":(function(module, __webpack_exports__, __webpack_require__) {
    		"use strict";
    		eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./test2 */ \"./src/test2.js\");\n\r\n\r\nfunction test() {}\r\n\r\ntest()\r\nObject(_test2__WEBPACK_IMPORTED_MODULE_0__[\"default\"])()\n\n//# sourceURL=webpack:///./src/index.js?");
    	}),
    	
    	"./src/test2.js": (function(module, __webpack_exports__, __webpack_require__) {
    		"use strict";
    		eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return test2; });\nfunction test2() {}\n\n//# sourceURL=webpack:///./src/test2.js?");
    	})
    }
    

    可以看到传入的第二个参数是 __webpack_exports__,而 CommonJS 规范对应的第二个参数是 exports。将这两个模块代码的内容美化一下:

    // index.js
    __webpack_require__.r(__webpack_exports__);
     var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test2.js");
     function test() {}
     test()
     Object(_test2__WEBPACK_IMPORTED_MODULE_0__["default"])()
     //# sourceURL=webpack:///./src/index.js?
    
    // test2.js
     __webpack_require__.r(__webpack_exports__);
     __webpack_require__.d(__webpack_exports__, "default", function() { return test2; });
     function test2() {}
     //# sourceURL=webpack:///./src/test2.js?
    

    可以发现,在每个模块的开头都执行了一个 __webpack_require__.r(__webpack_exports__) 语句。并且 test2.js 还多了一个 __webpack_require__.d() 函数。

    我们先来看看 __webpack_require__.r()__webpack_require__.d() 是什么。

    webpack_require.d()

    // define getter function for harmony exports
    __webpack_require__.d = function(exports, name, getter) {
    	if(!__webpack_require__.o(exports, name)) {
    		Object.defineProperty(exports, name, { enumerable: true, get: getter });
    	}
    };
    

    原来 __webpack_require__.d() 是给 __webpack_exports__ 定义导出变量用的。例如下面这行代码:

    __webpack_require__.d(__webpack_exports__, "default", function() { return test2; });
    

    它的作用相当于 __webpack_exports__["default"] = test2。这个 "default" 是因为你使用 export default 来导出函数,如果这样导出函数:

    export function test2() {}
    

    它就会变成 __webpack_require__.d(__webpack_exports__, "test2", function() { return test2; });

    webpack_require.r()

    // define __esModule on exports
    __webpack_require__.r = function(exports) {
    	if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
    		Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    	}
    	Object.defineProperty(exports, '__esModule', { value: true });
    };
    

    __webpack_require__.r() 函数的作用是给 __webpack_exports__ 添加一个 __esModuletrue 的属性,表示这是一个 ES6 module。

    添加这个属性有什么用呢?

    主要是为了处理混合使用 ES6 module 和 CommonJS 的情况。

    例如导出使用 CommonJS module.export = test2 导出函数,导入使用 ES6 module import test2 from './test2

    打包后的代码如下:

    // index.js
    __webpack_require__.r(__webpack_exports__);
    var _test2__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/test2.js");
    var _test2__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_test2__WEBPACK_IMPORTED_MODULE_0__);
    function test() {}
    test()
    _test2__WEBPACK_IMPORTED_MODULE_0___default()()
    //# sourceURL=webpack:///./src/index.js?
    
    // test2.js
     function test2() {}
     module.exports = test2
     //# sourceURL=webpack:///./src/test2.js?
    

    从上述代码可以发现,又多了一个 __webpack_require__.n() 函数:

    __webpack_require__.n = function(module) {
       var getter = module && module.__esModule ?
       	function getDefault() { return module['default']; } :
       	function getModuleExports() { return module; };
       __webpack_require__.d(getter, 'a', getter);
       return getter;
    };
    

    先来分析一下入口模块的处理逻辑:

    1. __webpack_exports__ 导出对象标识为 ES6 module。
    2. 加载 test2.js 模块,并将该模块的导出对象作为参数传入 __webpack_require__.n() 函数。
    3. __webpack_require__.n 分析该 export 对象是否是 ES6 module,如果是则返回 module['default']export default 对应的变量。如果不是 ES6 module 则直接返回 export

    按需加载

    按需加载,也叫异步加载、动态导入,即只在有需要的时候才去下载相应的资源文件。

    在 webpack 中可以使用 importrequire.ensure 来引入需要动态导入的代码,例如下面这个示例:

    // index.js
    function test() {}
    
    test()
    import('./test2')
    
    // test2.js
    export default function test2() {}
    

    其中使用 import 导入的 test2.js 文件在打包时会被单独打包成一个文件,而不是和 index.js 一起打包到 bundle.js

    在这里插入图片描述
    这个 0.bundle.js 对应的代码就是动态导入的 test2.js 的代码。

    接下来看看这两个打包文件的内容:

    // bundle.js
    (function(modules) { // webpackBootstrap
    	// install a JSONP callback for chunk loading
    	function webpackJsonpCallback(data) {
    		var chunkIds = data[0];
    		var moreModules = data[1];
    
    		// add "moreModules" to the modules object,
    		// then flag all "chunkIds" as loaded and fire callback
    		var moduleId, chunkId, i = 0, resolves = [];
    		for(;i < chunkIds.length; i++) {
    			chunkId = chunkIds[i];
    			if(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
    				resolves.push(installedChunks[chunkId][0]);
    			}
    			installedChunks[chunkId] = 0;
    		}
    		for(moduleId in moreModules) {
    			if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
    				modules[moduleId] = moreModules[moduleId];
    			}
    		}
    		if(parentJsonpFunction) parentJsonpFunction(data);
    
    		while(resolves.length) {
    			resolves.shift()();
    		}
    
    	};
    
    
    	// The module cache
    	var installedModules = {};
    
    	// object to store loaded and loading chunks
    	// undefined = chunk not loaded, null = chunk preloaded/prefetched
    	// Promise = chunk loading, 0 = chunk loaded
    	var installedChunks = {
    		"main": 0
    	};
    
    	// script path function
    	function jsonpScriptSrc(chunkId) {
    		return __webpack_require__.p + "" + chunkId + ".bundle.js"
    	}
    
    	// The require function
    	function __webpack_require__(moduleId) {
    
    		// Check if module is in cache
    		if(installedModules[moduleId]) {
    			return installedModules[moduleId].exports;
    		}
    		// Create a new module (and put it into the cache)
    		var module = installedModules[moduleId] = {
    			i: moduleId,
    			l: false,
    			exports: {}
    		};
    
    		// Execute the module function
    		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    
    		// Flag the module as loaded
    		module.l = true;
    
    		// Return the exports of the module
    		return module.exports;
    	}
    
    	// This file contains only the entry chunk.
    	// The chunk loading function for additional chunks
    	__webpack_require__.e = function requireEnsure(chunkId) {
    		var promises = [];
    
    		// JSONP chunk loading for javascript
    
    		var installedChunkData = installedChunks[chunkId];
    		if(installedChunkData !== 0) { // 0 means "already installed".
    
    			// a Promise means "currently loading".
    			if(installedChunkData) {
    				promises.push(installedChunkData[2]);
    			} else {
    				// setup Promise in chunk cache
    				var promise = new Promise(function(resolve, reject) {
    					installedChunkData = installedChunks[chunkId] = [resolve, reject];
    				});
    				promises.push(installedChunkData[2] = promise);
    
    				// start chunk loading
    				var script = document.createElement('script');
    				var onScriptComplete;
    
    				script.charset = 'utf-8';
    				script.timeout = 120;
    				if (__webpack_require__.nc) {
    					script.setAttribute("nonce", __webpack_require__.nc);
    				}
    				script.src = jsonpScriptSrc(chunkId);
    
    				// create error before stack unwound to get useful stacktrace later
    				var error = new Error();
    				onScriptComplete = function (event) {
    					// avoid mem leaks in IE.
    					script.onerror = script.onload = null;
    					clearTimeout(timeout);
    					var chunk = installedChunks[chunkId];
    					if(chunk !== 0) {
    						if(chunk) {
    							var errorType = event && (event.type === 'load' ? 'missing' : event.type);
    							var realSrc = event && event.target && event.target.src;
    							error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
    							error.name = 'ChunkLoadError';
    							error.type = errorType;
    							error.request = realSrc;
    							chunk[1](error);
    						}
    						installedChunks[chunkId] = undefined;
    					}
    				};
    				var timeout = setTimeout(function(){
    					onScriptComplete({ type: 'timeout', target: script });
    				}, 120000);
    				script.onerror = script.onload = onScriptComplete;
    				document.head.appendChild(script);
    			}
    		}
    		return Promise.all(promises);
    	};
    
    	// expose the modules object (__webpack_modules__)
    	__webpack_require__.m = modules;
    
    	// expose the module cache
    	__webpack_require__.c = installedModules;
    
    	// define getter function for harmony exports
    	__webpack_require__.d = function(exports, name, getter) {
    		if(!__webpack_require__.o(exports, name)) {
    			Object.defineProperty(exports, name, { enumerable: true, get: getter });
    		}
    	};
    
    	// define __esModule on exports
    	__webpack_require__.r = function(exports) {
    		if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
    			Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    		}
    		Object.defineProperty(exports, '__esModule', { value: true });
    	};
    
    	// create a fake namespace object
    	// mode & 1: value is a module id, require it
    	// mode & 2: merge all properties of value into the ns
    	// mode & 4: return value when already ns object
    	// mode & 8|1: behave like require
    	__webpack_require__.t = function(value, mode) {
    		if(mode & 1) value = __webpack_require__(value);
    		if(mode & 8) return value;
    		if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    		var ns = Object.create(null);
    		__webpack_require__.r(ns);
    		Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    		if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
    		return ns;
    	};
    
    	// getDefaultExport function for compatibility with non-harmony modules
    	__webpack_require__.n = function(module) {
    		var getter = module && module.__esModule ?
    			function getDefault() { return module['default']; } :
    			function getModuleExports() { return module; };
    		__webpack_require__.d(getter, 'a', getter);
    		return getter;
    	};
    
    	// Object.prototype.hasOwnProperty.call
    	__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
    
    	// __webpack_public_path__
    	__webpack_require__.p = "";
    
    	// on error function for async loading
    	__webpack_require__.oe = function(err) { console.error(err); throw err; };
    
    	var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
    	var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
    	jsonpArray.push = webpackJsonpCallback;
    	jsonpArray = jsonpArray.slice();
    	for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
    	var parentJsonpFunction = oldJsonpFunction;
    
    	// Load entry module and return exports
    	return __webpack_require__(__webpack_require__.s = "./src/index.js");
    })({
      "./src/index.js":(function(module, exports, __webpack_require__) {
        eval("function test() {}\r\n\r\ntest()\r\n__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./test2 */ \"./src/test2.js\"))\n\n//# sourceURL=webpack:///./src/index.js?");
      })
    });
    
    // 0.bundle.js
    (window["webpackJsonp"] = window["webpackJsonp"] || []).push(
      [
        [0],
        {
          "./src/test2.js":(function(module, __webpack_exports__, __webpack_require__) {
            "use strict";
            eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, \"default\", function() { return test2; });\nfunction test2() {}\n\n//# sourceURL=webpack:///./src/test2.js?");
          })
        }
      ]
    );
    

    这次打包的代码量有点膨胀,bundle.js 代码居然有 200 行。我们来看看相比于同步加载的 webpack 模块规范,它有哪些不同:

    1. 定义了一个对象 installedChunks,作用是缓存动态模块。
    2. 定义了一个辅助函数 jsonpScriptSrc(),作用是根据模块 ID 生成 URL。
    3. 定义了两个新的核心函数 __webpack_require__.e()webpackJsonpCallback()
    4. 定义了一个全局变量 window["webpackJsonp"] = [],它的作用是存储需要动态导入的模块。
    5. 重写 window["webpackJsonp"] 数组的 push() 方法为 webpackJsonpCallback()。也就是说 window["webpackJsonp"].push() 其实执行的是 webpackJsonpCallback()

    而从 0.bundle.js 文件可以发现,它正是使用 window["webpackJsonp"].push() 来放入动态模块的。动态模块数据项有两个值,第一个是 [0],它是模块的 ID;第二个值是模块的路径名和模块内容。

    然后我们再看一下打包后的入口模块的代码,经过美化后:

    function test() {}
    test()
    __webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/test2.js"))
    //# sourceURL=webpack:///./src/index.js?
    

    原来模块代码中的 import('./test2') 被翻译成了 __webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/test2.js"))

    __webpack_require__.e() 的作用是什么呢?

    webpack_require.e()

    __webpack_require__.e = function requireEnsure(chunkId) {
    	var promises = [];
    	// JSONP chunk loading for javascript
    	var installedChunkData = installedChunks[chunkId];
    	if(installedChunkData !== 0) { // 0 means "already installed".
    		// a Promise means "currently loading".
    		if(installedChunkData) {
    			promises.push(installedChunkData[2]);
    		} else {
    			// setup Promise in chunk cache
    			var promise = new Promise(function(resolve, reject) {
    				installedChunkData = installedChunks[chunkId] = [resolve, reject];
    			});
    			promises.push(installedChunkData[2] = promise);
    
    			// start chunk loading
    			var script = document.createElement('script');
    			var onScriptComplete;
    
    			script.charset = 'utf-8';
    			script.timeout = 120;
    			if (__webpack_require__.nc) {
    				script.setAttribute("nonce", __webpack_require__.nc);
    			}
    			script.src = jsonpScriptSrc(chunkId);
    
    			// create error before stack unwound to get useful stacktrace later
    			var error = new Error();
    			onScriptComplete = function (event) {
    				// avoid mem leaks in IE.
    				script.onerror = script.onload = null;
    				clearTimeout(timeout);
    				var chunk = installedChunks[chunkId];
    				if(chunk !== 0) {
    					if(chunk) {
    						var errorType = event && (event.type === 'load' ? 'missing' : event.type);
    						var realSrc = event && event.target && event.target.src;
    						error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
    						error.name = 'ChunkLoadError';
    						error.type = errorType;
    						error.request = realSrc;
    						chunk[1](error);
    					}
    					installedChunks[chunkId] = undefined;
    				}
    			};
    			var timeout = setTimeout(function(){
    				onScriptComplete({ type: 'timeout', target: script });
    			}, 120000);
    			script.onerror = script.onload = onScriptComplete;
    			document.head.appendChild(script);
    		}
    	}
    	return Promise.all(promises);
    };
    

    它的处理逻辑如下:

    1. 先查看该模块 ID 对应缓存的值是否为 0,0 代表已经加载成功了,第一次取值为 undefined
    2. 如果不为 0 并且不是 undefined 代表已经是加载中的状态。然后将这个加载中的 Promise 推入 promises 数组。
    3. 如果不为 0 并且是 undefined 就新建一个 Promise,用于加载需要动态导入的模块。
    4. 生成一个 script 标签,URL 使用 jsonpScriptSrc(chunkId) 生成,即需要动态导入模块的 URL。
    5. 为这个 script 标签设置一个 2 分钟的超时时间,并设置一个 onScriptComplete() 函数,用于处理超时错误。
    6. 然后添加到页面中 document.head.appendChild(script),开始加载模块。
    7. 返回 promises 数组。

    当 JS 文件下载完成后,会自动执行文件内容。也就是说下载完 0.bundle.js 后,会执行 window["webpackJsonp"].push()

    由于 window["webpackJsonp"].push() 已被重置为 webpackJsonpCallback() 函数。所以这一操作就是执行 webpackJsonpCallback() ,接下来我们看看 webpackJsonpCallback() 做了哪些事情。

    webpackJsonpCallback()

    对这个模块 ID 对应的 Promise 执行 resolve(),同时将缓存对象中的值置为 0,表示已经加载完成了。相比于 __webpack_require__.e(),这个函数还是挺好理解的。

    小结

    总的来说,动态导入的逻辑如下:

    1. 重写 window["webpackJsonp"].push() 方法。
    2. 入口模块使用 __webpack_require__.e() 下载动态资源。
    3. 资源下载完成后执行 window["webpackJsonp"].push(),即 webpackJsonpCallback()
    4. 将资源标识为 0,代表已经加载完成。由于加载模块使用的是 Promise,所以要执行 resolve()
    5. 再看一下入口模块的加载代码 __webpack_require__.e(0).then(__webpack_require__.bind(null, "./src/test2.js")),下载完成后执行 then() 方法,调用 __webpack_require__() 真正开始加载代码,__webpack_require__() 在上文已经讲解过,如果不了解,建议再阅读一遍。

    更多文章,敬请关注

    展开全文
  • 加载原理是利用了webpack的代码分割,它使用异步加载的方式去导入代码。 webpack解析依赖的时候,遇到异步加载的载入方式就会将这段代码分割成一个单独的js文件,等到要动态导入的时候,webpack会构造一个script...
  • 这个功能可以实现按需加载我们的代码,并且使用了promise式的回调,获取加载的包。 在代码中所有被import()的模块,都将打成一个单独的包,放在chunk存储的目录下。在浏览器运行到这一行代码时,就会自动请求这个...
  • "my-lib" 然后,就可以和普通npm安装的组件库一样使用了 在测试项目LibTest的入口js中,引入my-lib,执行Vue.use()注册组件 import MyLib from 'my-lib' Vue.use(MyLib) 在LibTest的App.vue中使用组件: 按需加载 ...
  • webpack异步加载原理

    2019-07-28 22:05:50
    ## webpack异步加载原理 ***不管任何语言,使用一个模块的步骤分为以下三步*** 第一步:找到该模块源码 第二步:加载该模块源码 第三步:使用该模块 对于在网页上的js而言,需要多一步,就是先加载js文件。就是在...
  • webpack 路由懒加载原理

    千次阅读 2020-07-23 09:07:47
    前言 路由懒加载也可以叫做路由组件懒加载,最常用的是通过import()来实现它。 function load(component) { ...在这里先不管Webpack是怎么按路由组件分割代码,只管在Webpack编译后,怎么实现按需加载对应的路由组件
  • 利用require.ensure这个API使得webpack单独将这个文件打包成一个可以异步加载的chunk. 具体的套路见我写的另一篇blog: webpack分包及异步加载套路 一句话总结就是: 在输出的runtime代码中,包含了异步chunk的id及ch
  • 本篇文章主要介绍了详解webpack分包及异步加载套路,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • webpack加载原理

    2021-12-02 09:25:44
    __webpack_require__.f = {} __webpack_require__.e = (chunkId) => { return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => { // promises = [], key = "j" // chunkId = ...
  • webpack异步加载原理

    千次阅读 2018-01-11 15:13:58
    有人称它为异步加载,也有人说做代码切割,那这个家伙到底是用来干嘛的?其实说白了,它就是把js模块给独立导出一个.js文件的,然后使用这个模块的时候,webpack会构造script dom元素,由浏览器发起异步请求这个js...
  • webpack加载原理探索

    千次阅读 2018-11-02 21:55:57
    这和纯前端 loader(比如 seajs、requirejs) 类似,但在 webpack 对模块设计上就区分了异步模块和同步模块,构建过程中自动构建成两个不同的 chunk 文件,异步模块按需加载。这一点突破是传统的 gulp 或者纯前端 ...
  • webpack4.0初探

    2021-08-04 06:59:18
    webpack4.0发布了一段时间了,本文稍微研究下1.webpack-cli必须默认安装了webpack,执行时,会报错,需要装上webpack-cli2.不再需要webpack.config.js默认情况下,已经不再需要这个配置文件,它已经有了最基本的配置...
  • webpack异步加载原理及分包策略
  • 利用webpack对代码进行分割是懒加载的前提,懒加载就是异步调用组件,需要时候才下载(按需加载)。为什么需要懒加载?在单页应用中,如果没有应用懒加载,运用webpack打包后的文件将会异常的大,造成进入首页时,需要...
  • 源自最近对业务项目进行 webpack 异步分包加载一点点的学习总结 提纲如下: 相关概念 webpack 分包配置 webpack 异步加载分包如何实现 相关概念 module、chunk、bundle 的概念 先来一波名词解释。先上网上一张...
  • 用于Webpack的TypeScript加载器 ts-loader 这是用于webpack的TypeScript加载器。 ·· 目录 入门 安装 yarn add ts-loader --dev 要么 npm install ts-loader --save-dev 如果尚未安装TypeScript,则还需要安装。 ...
  • vue+webpack实现页面的按需加载 通过vue写的单页应用时,可能会有很多的路由引入。当打包构建的时候,javascript包会变得非常大,影响加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的...
  • 作者:lzg9527原文链接:https://segmentfault.com/a/1190000038180453webpack 异步加载原理webpack ensure 有人称它为异...
  • 为了方便分析 webpack 加载模块的原理,我们准备了两个文件: hello.js const hello = { say: arg => { console.info('hello ' + arg || 'world'); } }; export default hello; index.js import Hello from '....
  • 授权转载自:Michael-lzghttps://github.com/Michael-lzg/my--articlewebpack 异步加载原理本文在github做了收录github...
  • ---恢复内容开始--- 一、前言 1、webpack异步加载原理’ ...
  • 为什么需要代码分割和按需加载代码分割就是我们根据实际业务需求将代码进行分割,然后在合适的时候在将其加载进入文档中。 举个简单的例子: 1.一个HTML中存在一个按钮 2.点击按钮出现一个包着图片的div 3.点击...
  • 作者:lzg9527 原文:https://segmentfault.com/a/1190000038180453webpack 异步加载原理webpack ensure有人称它为异...
  • 背景介绍: 我们在实际的开发过程中,vue-router的组件经常这样去写: { component: () => import('my/component/path/*.vue') } 这样写的目的是实现懒加载,也就是当需要...webpack.config.js const path = requ

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 16,865
精华内容 6,746
关键字:

webpack按需加载原理