Webpack 达人的成长之路

课程简介

本课程是为有一定了解或想深入了解 Webpack 打包原理的读者定制的。

内容从 Webpack 的基本概念和使用逐步深入到核心,如 Loader 和 Plugin 的书写,以及 Compiler 和 Compilation 对象分析;同时也涵盖了 HMR 的实现原理及 Tree-shaking、按需加载等高级知识点。

通过本课程,你可以深入的解和使用 Webpack,并能够按照项目需求快速开发一个适合于自身项目的打包工具。

你可以学到什么?

  • Webpack 的核心概念
  • Webpack 基本使用
  • webpack-dev-server 核心概念
  • webpack-dev-server 基本使用
  • Webpack 的 HMR 原理分析
  • Webpack 中的 Compiler 和 Compilation 对象
  • Webpack 常见插件原理分析
  • 写一个 Webpack 插件
  • 写一个 Webpack 的 loader
  • Webpack 结合 react-router 实现按需加载
  • Webpack 2 的 Tree-shaking 深入分析
  • 以 Node 方式集成 Webpack 和 webpack-dev-server 打包

其实现在基于 Webpack 的打包工具非常成熟,读者可以在 Github 或者 npm 中轻松地找到需要的脚手架。但我见过很多同学虽能够正常地使用 Webpack,对 Webpack 的配置也十分了解,可当遇到问题时依然不知所措。

通过本系列课程,你可以深入地了解和使用 Webpack,并能够按照项目需求快速开发一个适合于自身项目的打包工具,在开发中做到得心应手。

作者介绍

覃亮,BAT 某厂前端开发工程师,社区代码贡献名:高山上的鱼,CSDN 博客专家。对前端技术领域有剖根问底、追求知其然知其所以然的精神,熟悉 Babel、Webpack、React 等技术。

课程内容

导读:课程内容简介

前端必不可少的脚手架

对于打包工具的熟悉程度渐渐地也已经成为衡量前端开发工程师水平的一个重要指标。记得在校招面试的时候就有问各种打包工具的问题,如对于 Gulp、Grunt、Webpack 的熟悉程度,各种打包工具的特点及优缺点等。而当我们逐渐融入到一个特定的团队中,一般都有现成的脚手架提供给我们使用,而对于脚手架本身的关注程度也会慢慢降低。那是否就意味着,不需要掌握脚手架的相关知识了呢?其实不然,有以下几个理由。

(1)任何脚手架都有一定的适用场景,但是同时也有边界,如果不小心跨域了这个边界,那么很可能遇到意想不到的问题,如 bug。此时,如果对脚手架的原理有一定的了解,那么也能够更快的定位问题。

(2)任何一个脚手架都不可能是完美的,都会存在一个优化的阶段,如果只是用它,而不去了解它、优化它,那么本身就是一个追求完美的工程师不应该具有的态度。况且,对于工程师来说,只是会用而不知道其原理本身就是一个笑话。

课程内容

本课程是基于对 Webpack 有一定的了解,或者是想深入了解 Webpack 打包原理的读者而写的。如果只是想了解如何使用 Webpack,那么网上的大部分资料已经足够了。现在对本课程做一个概括,主要内容包含以下部分。

(1)Webpack 的核心概念

在本章节,首先会通过一个依赖图谱的例子来展开,详细的论述 Webpack 的 loader、plugin、entry、output 等核心概念。结合 Webpack 2 官网的说明以及日常开发实践经验进行深入的分析。会使用完整的实例让读者对 Webpack 核心概念有深入的理解,什么是 chunk、common chunk、hotUpdated chunk、externals、libraryTarget、library 等疑问会在本章节给出答案。

(2)Webpack 基本使用

本章节从 Webpack 的基本使用出发,但是又不止于基本使用,会结合 7 个实例代码来深入的分析 Webpack 与 CommonChunkPlugin 结合后的打包实践与原理。同时对于 CommonChunkPlugin 的各种配置都会使用具体的实例来深入讲解。通过本章节的学习,不仅会使用 Webpack,而且还知道如何更好的使用 Webpack。

(3)webpack-dev-server 核心概念

本章节会深入分析 webpack-dev-server 相关的概念,如 Proxy 代理、HMR 原理、contentBase、publicPath、lazyload、filename 等诸多配置的详细讲解。通过深入的了解这部分内容,不仅可以了解优化的点,同时也能更好的解决真实项目开发中可能遇到的问题。

(4)webpack-dev-server 基本使用

本章节主要讲解如何在项目中使用 webpack-dev-server,并深入的分析了 webpack-dev-server 的 iframe 模式与 inline 模式的区别。网上关于这两者的区别大都来自于官网的翻译,在本章节中会结合具体的实例来进行分析。

(5)Webpack 的 HMR 原理分析

在本章节中,不仅会讲解 Webpack 实现 HMR 的原理,同时也会讲解如何写出支持 HMR 的代码,从而可以深入的了解 HMR。这其中会包含常见的函数:decline() 函数、accept() 函数、dispose() 函数、status() 函数、apply() 等函数进行分析,同时也会详细地讲解 Webpack 与 HMR 的相关配置信息,以便在以后使用 Webpack 的时候得心应手。

(6)Webpack 中的 Compiler 和 Compilation 对象

Compilation 和 Compiler 对象是写 Webpack 插件的核心内容,在本章节中不仅会详细讲述两者的作用以及如何在插件中使用它们,同时也会讲解在 Webpack 插件书写中经常使用到的方法或者属性。通过本章节,不仅能了解什么是模块、依赖模块、chunk、资源等,还能知道如何根据具体场景来使用这些资源。

(7)Webpack 常见插件原理分析

在本章节中会将关注点放在 Webpack 两个插件的原理上,包括 CommonChunkPlugin 和 PrepackWebpackPlugin,通过这两个插件来加深对上面知识的理解,从而为下文写一个 Webpack 插件做铺垫。

(8)写一个 Webpack 插件

Webpack 插件是扩展 Webpack 基础功能的主要渠道,在本章节中会讲解如何写一个 Webpack 插件。

(9)写一个 Webpack 的 loader

在本章节中会使用 Markdown 文件处理 loader 来讲解如何写 Webpack 的 loader。

(10)Webpack 结合 react-router 实现按需加载

在上面的章节中,讲到了如何使用 require.ensure 来动态产生独立的 chunk 的问题,在本章节会使用 react-router 的例子来讲解如何使用 Webpack 的这种特性。通过动态按需加载的特性能够减少页面首次加载的时长,配合单页面应用绝对是页面优化的首选。

(11)Webpack 2 的 Tree-shaking 深入分析

Tree-shaking 是 Webpack 2 引入的新特性,本章节会详细描述如何使用 Tree-shaking 及其原理和适用范围。本章节内容包含具体的实例,所以读者一定能够很好的了解这种新特性。

(12)以 Node 方式集成 Webpack 和 webpack-dev-server 打包

在本章节中将使用一个很好的例子来讲解如何基于 Webpack、webpack-dev-server 来写一个打包工具并适应具体的业务场景。通过本章节的内容,能很好的将上面章节的内容做一个串联,同时也能更好的理解 Webpack。

写给读者

其实现在基于 Webpack 的打包工具都已经非常成熟,所以读者可以随意的在 Github 或者 npm 中找到需要的脚手架。但是,就像文章开头所说,只有了解了 Webpack 的核心原理,才能在开发中做到得心应手。我见过很多同学,能够正常的使用 Webpack,对很多 Webpack 的配置也了解,但是当遇到问题的时候往往不知所措。通过本系列课程,会让读者摆脱现状,更好的理解 Webpack 原理,而不会知其然不知其所以然。

第01课:Webpack 核心概念

Webpack 的基础作用与打包实例

Webpack 是一个最新的 JS 模块管理器。当 Webpack 处理应用的时候,它会递归创建一个依赖图谱,这个图谱会详细的列出应用需要的每一个模块,最终将这些模块打包到几个小的输出文件中。当然一般只是一个文件(采用webpack-common-chunk插件除外),最终这个文件将会通过浏览器来加载。这里的核心概念是图谱,这里在开始介绍 Webpack 的核心概念之前,先给出一个图谱的例子,希望对理解 Webpack 有一定的帮助。

假如,有如下的 Webpack 配置:

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");module.exports = {    entry: {        main: process.cwd()+'/example3/main.js',        main1: process.cwd()+'/example3/main1.js',        common1:["jquery"],        common2:["vue"]    },    output: {        path: process.cwd()+'/dest/example3',        filename: '[name].js'    },    plugins: [        new CommonsChunkPlugin({            name: ["chunk",'common1','common2'],            minChunks:2        })    ]};

而且各个模块的依赖关系如下图:

Webpack 达人的成长之路_第1张图片

此时可以看一下上文说到的模块依赖图谱:

在这个图谱中可以清楚地看到各个模块的依赖关系,如 main.js 和 main1.js 的模块 id 分别为3、2,而且它们的父级模块 id 为1,即 chunk.js,而 chunk.js 的父级模块 id 为 0,即 common1.js,最后就是 common1.js 的父级模块 id 为 4,即 common2.js。也就是说,如果使用了 webpack-common-chunk 插件,那么会产生多个输出资源文件,而且输出资源的 name 是通过 output.filename 配置来指定的。不过有几个注意点需要了解下。

webpack-common-chunk 抽取公共模块的逻辑

上面的例子入口文件的配置如下:

entry: {        main: process.cwd()+'/example3/main.js',        main1: process.cwd()+'/example3/main1.js',        common1:["jquery"],        common2:["vue"]    }

而且 output 的配置为:

 output: {        path: process.cwd()+'/dest/example3',        filename: '[name].js'    },

此时,至少输出 4 个文件,分别为 main.js、main1.js、common1.js、common2.js,但是在 webpack 中又配置了 webpack-common-chunk 这个插件:

new CommonsChunkPlugin({            name: ["chunk",'common1','common2'],            minChunks:2            //这个配置表示,如果一个模块的依赖次数至少为 2 次才会被抽取到公共模块中        })

这个插件首先会将 main.js 和 main1.js 中出现两次以上模块依赖次数的模块单独提取出来,如上图中的 chunk1.js 和 chunk2.js(在 main.js 和 main1.js 中都被引用了),将代码抽取到 chunk.js 中;然后将 chunk.js 中被依赖两次以上的模块抽取到 common1.js 中;接着,继续将 common1.js 中被依赖两次以上的代码抽取到 common2.js 中。最后,将会发现:

main1.js 中只含有 jquery.jsmain2.js 中只含有 vue.jschunk.js 中含有 main1.js 和 main2.js 的公共模块,即 chunk1.js 和 chunk2.js 的内容common1.js 中只含有 jQuery 代码,因为 chunk.js 中不含有依赖两次以上的模块common2.js 中只含有 vue.js 代码,因为 common1.js 中不含有依赖两次以上的模块

模块加载顺序

经过 webpack-common-chunk 处理后的代码有一点要注意,即抽取层级越高的代码应该越先加载。具体的含义可以通过上面的例子来说明下。比如,上面的 common-chunk-plugin 的配置如下:

new CommonsChunkPlugin({            name: ["chunk",'common1','common2'],            minChunks:2            //这个配置表示,如果一个模块的依赖次数至少为 2 次才会被抽取到公共模块中        })

这样应该最先加载 common2.js,然后是 common1.js、chunk.js,最后才是 index.js。所以,我们经常可以看到下面的 html 模板:

        

之所以是这样,其实很好理解。原因之一在于:我们将下级模块公共的代码已经抽取到其他文件中了,这样,如果不预先加载公共模块而先加载其他模块,那么就会出现模块找不到的报错信息。原因之二:假如你去看过我们打包后的 common2.js,会看到如下代码:

/******/ (function(modules) { // webpackBootstrap/******/    // install a JSONP callback for chunk loading/******/    var parentJsonpFunction = window["webpackJsonp"];/******/    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {/******/        // add "moreModules" to the modules object,/******/        // then flag all "chunkIds" as loaded and fire callback/******/        var moduleId, chunkId, i = 0, callbacks = [];/******/        for(;i < chunkIds.length; i++) {/******/            chunkId = chunkIds[i];/******/            if(installedChunks[chunkId])/******/                callbacks.push.apply(callbacks, installedChunks[chunkId]);/******/            installedChunks[chunkId] = 0;/******/        }/******/        for(moduleId in moreModules) {/******/            modules[moduleId] = moreModules[moduleId];/******/        }/******/        if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);/******/        while(callbacks.length)/******/            callbacks.shift().call(null, __webpack_require__);/******/        if(moreModules[0]) {/******/            installedModules[0] = 0;/******/            return __webpack_require__(0);/******/        }/******/    };/******/    // The module cache/******/    var installedModules = {};/******/    // object to store loaded and loading chunks/******/    // "0" means "already loaded"/******/    // Array means "loading", array contains callbacks/******/    var installedChunks = {/******/        1:0/******/    };/******/    // 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] = {/******/            exports: {},/******/            id: moduleId,/******/            loaded: false/******/        };/******/        // Execute the module function/******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);/******/        // Flag the module as loaded/******/        module.loaded = 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, callback) {/******/        // "0" is the signal for "already loaded"/******/        if(installedChunks[chunkId] === 0)/******/            return callback.call(null, __webpack_require__);/******/        // an array means "currently loading"./******/        if(installedChunks[chunkId] !== undefined) {/******/            installedChunks[chunkId].push(callback);/******/        } else {/******/            // start chunk loading/******/            installedChunks[chunkId] = [callback];/******/            var head = document.getElementsByTagName('head')[0];/******/            var script = document.createElement('script');/******/            script.type = 'text/javascript';/******/            script.charset = 'utf-8';/******/            script.async = true;/******/            script.src = __webpack_require__.p + "" + chunkId + "." + ({"0":"common1","2":"main","3":"main1","4":"chunk"}[chunkId]||chunkId) + ".js";/******/            head.appendChild(script);/******/        }/******/    };/******/    // expose the modules object (__webpack_modules__)/******/    __webpack_require__.m = modules;/******/    // expose the module cache/******/    __webpack_require__.c = installedModules;/******/    // __webpack_public_path__/******/    __webpack_require__.p = "";/******/    // Load entry module and return exports/******/    return __webpack_require__(0);/******/ })

最顶级的输出文件中会包含 Webpack 加载其他模块的公共代码,可以理解为加载器,如果不先加载顶级的模块(上面的例子是 common2.js),那么就无法通过它来加载其他的模块,因此也会报错。这里提到的例子代码可以 点击这里 来仔细阅读。

好了,上面讲了一个复杂的例子,如果不懂也没关系,在 Webpack 常见插件部分中会更加详细的分析。下面看看一些 Webpack 的基础概念。

Webpack 的基础概念

Webpack 的入口文件

上面说过,Webpack 会构建一个模块的依赖图谱,而构建这个图谱的起点就是入口文件。Webpack 相当于从这个起点来绘制类似于上面的整个图谱。通过这个图谱,可以知道哪些模块会被打包到最终的文件中,而那些没有出现在图谱中的模块将会被忽略。在 Webpack 中使用 entry 这个配置参数来指定 Webpack 入口文件,比如上面的例子:

 entry: {        main: process.cwd()+'/example3/main.js',        main1: process.cwd()+'/example3/main1.js',        common1:["jquery"],        common2:["vue"]    }

可知,入口文件总共是 4 个,那么最终会构建出 4 个不同的文件依赖图谱。当然,这种配置常用于单页面应用,一般只有一个入口文件。这个配置可以允许我们使用 CommonChunkPlugin,然后将那些公共的模块抽取到 vendor.js(名字可以自定义)中。这种形式,通过上面的例子应该已经了解到了。

当然,上面这种配置依然可以应用于多页面应用,比如下面的例子:

const config = {  entry: {    pageOne: './src/pageOne/index.js',    pageTwo: './src/pageTwo/index.js',    pageThree: './src/pageThree/index.js'  }};

我们告诉 Webpack 需要三个不同的依赖图谱。因为在多页面应用中,当访问一个 URL 的时候,实际上需要的是一个 html 文档,只需要将打包后的资源插入到 html 文档中就可以了。比如上面的配置,需要分别读取 pageOne、pageTwo、pageThree 下的 index.js 作为入口文件,然后结合 html 模板打包一次,并将打包后的 html 以及输出资源结合起来输出到特定的文件路径下,这样当访问固定的 html 的时候就可以了。因此,Webpack 的单页面打包和多页面的打包其实原理是完全一致的。

Webpack 的 output 常见配置

output.path

当资源打包后,需要指定特定的输出路径,这就是 output 需要完成的事情。此时,可以通过 output 配置来完成,比如上面的例子:

entry: {        main: process.cwd()+'/example3/main.js',        main1: process.cwd()+'/example3/main1.js',        common1:["jquery"],        common2:["vue"]    },    output: {        path: process.cwd()+'/dest/example3',        filename: '[name].js'    },

指定了输出路径为 process.cwd()+'/dest/example3',相当于告诉 Webpack 所有的输出文件全部放到这个目录下。

output.filename

上面的文件名称指定为'[name].js',其中[name]和 entry 中的 key 保持一致。其中 filename 的配置还是比较多的,不仅可以使用 name,还可以使用 id、hash、chunkhash 等。

[name]:这个模块的名称,就是 entry 中指定的 key[id]:这个模块的 id,由 Webpack 来分配,通过上面的依赖图谱可以看到[hash]:每次 Webpack 完成一个打包都会生成这个 hash[chunkhash]:Webpack 每生成一个文件就叫一个 chunk,这个 chunk 本身的 hash

其中这里的 hash 如果不懂,可以查看 webpack-dev-server 的“深入源码分析”部分,这里有详细的论述。

publichPath 与 output.path

打包好的 bundle 在被请求的时候,其路径是相对于你配置的 publicPath 来说的。因为 publicPath 相当于虚拟路径,其映射于你指定的 output.path。假如你指定的 publicPath 为 "/assets/",而且 output.path 为"build",那么相当于虚拟路径 "/assets/" 对应于 "build"(前者和后者指向的是同一个位置),而如果 build 下有一个 "index.css",那么通过虚拟路径访问就是 /assets/index.css。比如有一个如下的配置:

module.exports = {  entry: {    app: ["./app/main.js"]  },  output: {    path: path.resolve(__dirname, "build"),    publicPath: "/assets/",    //此时相当于 /assets/ 路径对应于 build 目录,是一个映射的关系    filename: "bundle.js"  }}

那么我们要访问编译后的资源可以通过 localhost:8080/assets/bundle.js 来访问。如果在 build 目录下有一个 html 文件,那么可以使用下面的方式来访问 js 资源:

    Document  
hotUpdateChunkFilename vs hotUpdateMainFilename

这里提供了一个 例子,当修改了 test 目录下的文件的时候,比如修改了 scss 文件,此时会发现在页面中多出了一个 script 元素,内容如下:

当打开它会看到:

webpackHotUpdate(0,{/***/ 15:/***/ (function(module, exports, __webpack_require__) {exports = module.exports = __webpack_require__(46)();// imports// moduleexports.push([module.i, "html {\n  border: 1px solid yellow;\n  background-color: pink; }\n\nbody {\n  background-color: lightgray;\n  color: black; }\n  body div {\n    font-weight: bold; }\n    body div span {\n      font-weight: normal; }\n", ""]);// exports/***/ })})//# sourceMappingURL=0.188304c98f697ecd01b3.hot-update.js.map

从内容也可以看出,只是将修改的模块 push 到 exports 对象中!而 hotUpdateChunkFilename 就是为了让你能够执行 script 的 src 中的值!而同样的 hotUpdateMainFilename 是一个 json 文件用于指定哪些模块发生了变化,在 output 目录下。

externals vs libraryTarget vs library

假如需要完成下面的两个需求:

模块依赖于 jQuery,但是不希望 jQuery 打包到最后的文件中去;模块要存在于全局的变量 Foo 上面。

那么需要将 Webpack 配置如下:

module.exports = {  entry:   {    main:process.cwd()+'/example1/main.js',  },  output: {    path:process.cwd()+'/dest/example1',    filename: '[name].js',    // export itself to a global var    libraryTarget: "var",    // name of the global var: "Foo"    library: "Foo"  },    externals: {        // require("jquery") is external and available        //  on the global var jQuery        "jquery": "jQuery"    },  plugins: [   new CommonsChunkPlugin({       name:"chunk",       minChunks:2   }),    new  HtmlWebpackPlugin()  ]};

其中 external 配置表示模块 require('jquery') 中的 jquery 来自于 window.jQuery,也就是来自于全局对象 jQuery,而不要单独打包到入口文件的 bundle 中,在页面中通过 script 标签来引入!

 externals: {        // require("jquery") is external and available        //  on the global var jQuery        "jquery": "jQuery"    }

下面详细的分析下 libraryTarget 和 library 相关内容。

library:在 output 中配置,可以指定库的名称libraryTarget:指定模块输出类型,可以是 commonjs、AMD、script 形式、UMD 模式

例子1:其中 libraryTarget 设置为 var,而 library 设置为 'Foo'。也就是表示把入口文件打包的结果封装到变量 Foo 上面(以下例子的 external 全部是一样的,见上面的 webpack.config.js 文件)

output: {    path:process.cwd()+'/dest/example1',    filename: '[name].js',    // export itself to a global var    libraryTarget: "var",    // name of the global var: "Foo"    library: "Foo"  }

我们看看打包的结果:

var Foo =webpackJsonpFoo([0,1],[/* 0 *//***/ function(module, exports, __webpack_require__) {  var jQuery = __webpack_require__(1);  var math = __webpack_require__(2);  function Foo() {}  // ...  module.exports = Foo;/***/ },/* 1 *//***/ function(module, exports) {       module.exports = jQuery/***/ },/* 2 *//***/ function(module, exports) {       console.log('main1');/***/ }]);

从结果分析目的,入口文件的 bunle 被打包成为一个变量,变量名就是 library 指定的 Foo。而且 externals 中指定的 jQuery 也被打包成为一个模块,但是这个模块是没有 jQuery 源码的,他的模块内容很简单,就是引用 window.jQuery:

/* 1 *//***/ function(module, exports) {       module.exports = jQuery;/***/ },

关于 externals vs libraryTarget vs library 还有不懂的地方可以仔细阅读 webpack 中的 externals vs libraryTarget vs library 深入分析 文章。

Webpack 的 loader

因为浏览器并非能够识别所有的文件类型(如 scss/less/typescript),因此在浏览器加载特定类型的资源之前需要对资源本身进行处理。Webpack 将所有的文件类型都当做一个模块,如 css、html、scss、jpg 等,但是 Webpack 本身只能识别 JavaScript 文件。因此,需要特定的 loader 将文件转化为 JavaScript 模块,同时将这个模块添加到依赖图谱中。

Webpack 中 loader 具有如下的作用:

识别特定的文件类型应该被那个 loader 处理转化特定的文件以便将它添加到依赖图谱中,并最终添加到打包后的输出文件中

下面给出一个例子:

const path = require('path');const config = {  entry: './path/to/my/entry/file.js',  output: {    path: path.resolve(__dirname, 'dist'),    filename: 'my-first-webpack.bundle.js'  },  module: {    rules: [      { test: /\.txt$/, use: 'raw-loader' }    ]  }};module.exports = config;

上面这个例子相当于告诉 Webpack,当遇到 require()/import 一个后缀名为 txt 的文件的时候就要使用 raw-loader 来处理,但是必须保证在 node_modules 中已经安装了 raw-loader,否则报错。而且,在文件路径嵌套很深的情况下,建议使用如下方式:

 rules: [      { test: /\.txt$/, use: require.resolve('raw-loader') }    ]

否则可能出现即使安装了也找不到相应的 loader 的情况,特别是当 npm 3 出现以后。

Webpack 的 plugin

上面 loader 是在特定文件类型的基础上完成它的处理的,即它是针对于特定的文件类型,并把它转化为 JavaScript 中的模块。而 plugin 用于在编译和打包过程中对所有的模块执行特定的操作。而且 Webpack 插件系统是非常强大和可配置的。

为了使用插件,只需要 require 它并把它添加到配置文件的 plugins 数组中。比如下面的例子:

 plugins: [    //注意:这里使用了 webpack-common-chunk,所以会产生两个文件,即 common.js 和 index.js (名字自由设置)        new CommonsChunkPlugin({            name: ["chunk",'common1','common2'],            minChunks:2        })    ]

本章小结

该本章节中通过一个 CommonChunkPlugin 的例子引入了打包后 模块图谱 的概念,并对 CommonChunkPlugin 的打包原理进行了分析。同时,还介绍了 Webpack 中四大核心概念,即入口文件、输出文件、Loader、Plugin 等,并对输出文件中很多容易混淆的概念进行了深入的讲解。通过本章节,对 Webpack 本身应该会有一个初略的了解,后续会针对 Loader、Plugin 等核心内容进行进一步的剖析。

第02课:Webpack 基本使用

在本章节中将简要的对 Webpack 基本使用做一个演示。通过本章节的学习,能在项目中快速配置一个 Webpack 打包文件并完成项目的打包工作。在本章节,我们不牵涉到 Webpack 中那些深入的知识点,后续会针对这些知识点做更加细致的讲解。但是,正如在上一个章节中看到的那样,我很早就在 Webpack 配置中引入了 CommonChunkPlugin,因此在本章节,将在 Webpack 配置文件中继续使用这个插件。希望能对该插件引起足够的重视,并学会如何在自己的项目中使用它。

其中 CommonChunkPlugin 是 Webpack 用于创建一个独立的文件,即所谓的 common chunk。这个 chunk 会包含多个入口文件中共同的模块。通过将多个入口文件公共的模块抽取出来可以在特定的时间进行缓存,这对于提升页面加载速度是很好的优化手段。

Webpack 打包例子讲解

CommonChunkPlugin 参数详解

开始具体的例子之前先看下这个插件支持的配置和详细含义。同时,也给出官网描述的几个例子:

{  name: string, // or  names: string[],  // The chunk name of the commons chunk. An existing chunk can be selected by passing a name of an existing chunk.  // If an array of strings is passed this is equal to invoking the plugin multiple times for each chunk name.  // If omitted and `options.async` or `options.children` is set all chunks are used, otherwise `options.filename`  // is used as chunk name.  // When using `options.async` to create common chunks from other async chunks you must specify an entry-point  // chunk name here instead of omitting the `option.name`.  filename: string,  //指定该插件产生的文件名称,可以支持 output.filename 中那些支持的占位符,如 [hash]、[chunkhash]、[id] 等。如果忽略这个这个属性,那么原始的文件名称不会被修改(一般是 output.filename 或者 output.chunkFilename,可以查看 compiler 和 compilation 部分第一个例子)。但是这个配置不允许和 `options.async` 一起使用  minChunks: number|Infinity|function(module, count)  boolean,  //至少有 minChunks 的 chunk 都包含指定的模块,那么该模块就会被移出到 common chunk 中。这个数值必须大于等于2,并且小于等于没有使用这个插件应该产生的 chunk 数量。如果传入 `Infinity`,那么只会产生 common chunk,但是不会有任何模块被移到这个 chunk中 (没有一个模块会被依赖无限次)。通过提供一个函数,也可以添加自己的逻辑,这个函数会被传入一个参数表示产生的 chunk 数量  chunks: string[],  // Select the source chunks by chunk names. The chunk must be a child of the commons chunk.  // If omitted all entry chunks are selected.  children: boolean,  // If `true` all children of the commons chunk are selected  deepChildren: boolean,  // If `true` all descendants of the commons chunk are selected  async: boolean|string,  // If `true` a new async commons chunk is created as child of `options.name` and sibling of `options.chunks`.  // It is loaded in parallel with `options.chunks`.  // Instead of using `option.filename`, it is possible to change the name of the output file by providing  // the desired string here instead of `true`.  minSize: number,  //所有被移出到 common chunk 的文件的大小必须大于等于这个值}

上面的 filename 和 minChunks 已经在注释中说明了,下面重点说一下其他的属性。

  • children 属性

    其中在 Webpack 中很多 chunk 产生都是通过 require.ensure 来完成的。先看看下面的例子:

//main.js 为入口文件if (document.querySelectorAll('a').length) {    require.ensure([], () => {        const Button = require('./Components/Button').default;        const button = new Button('google.com');        button.render('a');    });}if (document.querySelectorAll('h1').length) {    require.ensure([], () => {        const Header = require('./Components/Header').default;        new Header().render('h1');    });}

此时会产生三个 chunk,分别为 main 和其他两个通过 require.ensure 产生的 chunk,比如 0.entry.chunk.js 和 1.entry.chunk.js。如果配置了多个入口文件(假如还有一个 main1.js),那么这些动态产生的 chunk 中可能也会存在相同的模块(此时 main1、main 会产生四个动态 chunk )。而这个 children 配置就是为了这种情况而产生的。通过配置 children,可以将动态产生的这些 chunk 的公共的模块也抽取出来。

很显然,以前是动态加载的文件现在都必须在页面初始的时候就加载完成,那么对于初始加载肯在时间上有一定的副作用。但是存在一种情况,比如进入主页面后,需要加载路由 A、路由 B……等一系列的文件(网站的核心模块都要提前一次性加载),那么把路由 A、路由 B……这些公共的模块提取到公有模块中,然后和入口文件一起加载,在性能上还是有优势的。下面是官网提供的一个例子:

new webpack.optimize.CommonsChunkPlugin({  // names: ["app", "subPageA"]  // (choose the chunks, or omit for all chunks)  children: true,  // (select all children of chosen chunks)  // minChunks: 3,  // (3 children must share the module before it's moved)})

对于 common-chunk-plugin 不太明白的,可以 查看这里。

  • async

    上面这种 children 的方案会增加初始加载的时间,这种 async 的方式相当于创建了一个异步加载的 common-chunk,其包含 require.ensure 动态产生的 chunk 中的公共模块。这样,当访问特定路由的时候,会动态的加载这个 common chunk,以及特定路由包含的业务代码。下面也是官网给出的一个实例:

new webpack.optimize.CommonsChunkPlugin({  name: "app",  // or  names: ["app", "subPageA"]  // the name or list of names must match the name or names  // of the entry points that create the async chunks  children: true,  // (use all children of the chunk)  async: true,  // (create an async commons chunk)  minChunks: 3,  // (3 children must share the module before it's separated)})
  • names

    该参数用于指定 common chunk 的名称。如果指定的 chunk 名称在 entry 中有配置,那么表示选择特定的 chunk。如果指定的是一个数组,那么相当于按照名称的顺序多次执行 common-chunk-plugin 插件。如果没有指定 name 属性,但是指定了 options.async 或者 options.children,那么表示抽取所有的 chunk 的公共模块,包括通过 require.ensure 动态产生的模块。其他情况下使用 options.filename 来作为 chunk 的名称。

    注意:如果指定了 options.async 来创建一个异步加载的 common chunk,那么必须指定一个入口 chunk 名称,而不能忽略 option.name 参数。可以 点击这个例子 查看。

  • chunks

    通过 chunks 参数来选择来源的 chunk。这些 chunk 必须是 common-chunk 的子级 chunk。如果没有指定,那么默认选中所有的入口 chunk。下面给出一个例子:

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");module.exports = {    entry: {        main: process.cwd()+'/example6/main.js',        main1: process.cwd()+'/example6/main1.js',        jquery:["jquery"]    },    output: {        path: process.cwd()  + '/dest/example6',        filename: '[name].js'    },    plugins: [        new CommonsChunkPlugin({            name: "jquery",            minChunks:2,            chunks:["main","main1"]        })    ]};

此时会发现在 jquery.js 的最后会打包进来 chunk1.js 和 chunk2.js。

/* 2 *//***/ function(module, exports, __webpack_require__) {  __webpack_require__(3);  var chunk1=1;  exports.chunk1=chunk1;/***/ },/* 3 *//***/ function(module, exports) {  var chunk2=1;  exports.chunk2=chunk2;/***/ }

关于 chunks 配置的使用可以 点击这里查看。所以,chunks 就是用于指定从那些 chunks 来抽取公共的模块,而 chunks 的名称一般都是通过 entry 来指定的,比如上面的 entry 为:

 entry: {        main: process.cwd()+'/example6/main.js',        main1: process.cwd()+'/example6/main1.js',        jquery:["jquery"]    },

而 chunks 指定的为:

 chunks:["main","main1"]

表明从 main、main1 两个入口 chunk 中来找公共的模块。

  • deepChildren

    如果将该参数设置为 true,那么 common-chunk 下的所有的 chunk 都会被选中,比如 require.ensure 产生的 chunk 的子级 chunk,从这些 chunks 中来抽取公共的模块。

  • minChunks 为函数

    可以给 minChunks 传入一个函数。CommonsChunkPlugin 将会调用这个函数并传入 module 和 count 参数。这个 module 参数用于指定某一个 chunks 中所有的模块,而这个 chunk 的名称就是上面配置的 name/names 参数。这个 module 是一个 NormalModule 实例,有如下的常用属性:

    module.context:表示存储文件的路径,比如 '/my_project/node_modules/example-dependency'

    module.resource:表示被处理的文件名称,比如 '/my_project/node_modules/example-dependency/index.js'

而 count 参数表示指定的模块出现在多少个 chunk 中。这个函数对于细粒度的操作 CommonsChunk 插件还是很有用的。可自己决定将那些模块放在指定的 common chunk 中,下面是官网给出的一个例子:

new webpack.optimize.CommonsChunkPlugin({  name: "my-single-lib-chunk",  filename: "my-single-lib-chunk.js",  minChunks: function(module, count) {    //如果一个模块的路径中存在 somelib 部分,而且这个模块出现在 3 个独立的 chunk 或者 entry 中,那么它就会被抽取到一个独立的 chunk 中,而且这个 chunk 的文件名称为 "my-single-lib-chunk.js",而这个 chunk 本身的名称为 "my-single-lib-chunk"    return module.resource && (/somelib/).test(module.resource) && count === 3;  }});

而官网下面的例子详细的展示了如何将 node_modules 下引用的模块抽取到一个独立的 chunk 中:

new webpack.optimize.CommonsChunkPlugin({  name: "vendor",  minChunks: function (module) {    // this assumes your vendor imports exist in the node_modules directory    return module.context && module.context.indexOf("node_modules") !== -1;  }})

因为 node_module 下的模块一般都是来源于第三方,所以在本地很少修改,通过这种方式可以将第三方的模块抽取到公共的 chunk 中。

还有一种情况就是,如果想把应用的 css/scss 和 vendor 的 css(第三方类库的 css)抽取到一个独立的文件中,那么可以使用下面的 minChunk() 函数,同时配合 ExtractTextPlugin 来完成。

new webpack.optimize.CommonsChunkPlugin({  name: "vendor",  minChunks: function (module) {    // This prevents stylesheet resources with the .css or .scss extension    // from being moved from their original chunk to the vendor chunk    if(module.resource && (/^.*\.(css|scss)$/).test(module.resource)) {      return false;    }    return module.context && module.context.indexOf("node_modules") !== -1;  }})

这个例子在抽取 node_modules 下的模块的时候做了一个限制,即明确指定 node_modules 下的 scss/css 文件不会被抽取,所以最后生成的 vendor.js 不会包含第三方类库的 css/scss 文件,而只包含其中的 js 部分。 同时通过配置 ExtractTextPlugin 就可以将应用的 css 和第三方应用的 css 抽取到一个独立的 css 文件中,从而达到 css 和 js 分离。

其中 CommonsChunkPlugin 插件还有一个更加有用的配置,即用于将 Webpack 打包逻辑相关的一些文件抽取到一个独立的 chunk 中。但是此时配置的 name 应该是 entry 中不存在的,这对于线上缓存很有作用。因为如果文件的内容不发生变化,那么 chunk 的名称不会发生变化,所以并不会影响到线上的缓存。比如下面的例子:

new webpack.optimize.CommonsChunkPlugin({  name: "manifest",  //这个 name 必须不在 entry 中  minChunks: Infinity})

但是你会发现抽取 manifest 文件和配置 vendor chunk 的逻辑不一样,所以这个插件需要配置两次:

[  new webpack.optimize.CommonsChunkPlugin({    name: "vendor",    minChunks: function(module){      return module.context && module.context.indexOf("node_modules") !== -1;    }  }),  new webpack.optimize.CommonsChunkPlugin({    name: "manifest",    minChunks: Infinity  }),]

你可能会好奇,假如有如下的配置:

  module.exports = {    entry: './src/index.js',    plugins: [      new CleanWebpackPlugin(['dist']),      new HtmlWebpackPlugin({       title: 'Caching'      })    ],    output: {    filename: '[name].[chunkhash].js',      path: path.resolve(__dirname, 'dist')    }  };

那么如果 entry 中文件的内容没有发生变化,运行 webpack 命令多次,那么最后生成的文件名称应该是一样的,为什么会重新通过 CommonsChunkPlugin 来生成一个 manifest 文件呢?这个官网也有明确的说明:

因为 Webpack 在入口文件中会包含特定的样板文件,特别是 runtime 文件和 manifest 文件。而最后生成的文件的名称到底是否一致还与 Webpack 的版本有关,在新版本中可能不存在这个问题,但是在老版本中可能会存在,所以为了安全起见一般都会使用它。那么问题又来了,样板文件和 runtime 文件指的是什么?可以查 我的这个例子,把关注点放在文中说的为什么要提前加载最后一个 chunk 的问题上。下面就这部分做一下深入的分析:

  • runtime

    当代码在浏览器中运行的时候,Webpack 使用 runtime 和 manifest 来处理应用中的模块化关系。其中包括在模块存在依赖关系的时候,加载和解析特定的逻辑,而解析的模块包括已经在浏览中加载完成的模块和那些需要懒加载的模块本身。

  • manifest

    一旦应用程序中,如 index.html 文件、一些 bundle 和各种静态资源被加载到浏览器中,会发生什么?精心安排的 /src 目录的文件结构现在已经不存在,所以 Webpack 如何管理所有模块之间的交互呢?这就是 manifest 数据用途的由来……

    当编译器(compiler)开始执行、解析和映射应用程序时,它会保留所有模块的详细要点。这个数据集合称为 "Manifest",当完成打包并发送到浏览器时,会在运行时通过 Manifest 来解析和加载模块。无论选择哪种模块语法,那些 import 或 require 语句现在都已经转换为 __webpack_require__方法,此方法指向模块标识符(module identifier)。通过使用 manifest 中的数据,runtime 将能够查询模块标识符,检索出背后对应的模块。比如提供的 这个例子,其中入口文件中有 main.js,入口文件中加载 chunk1.js 和 chunk2.js,而最后看到的就是下面转化为__webpack_require__ 后的内容:

  webpackJsonp([0,1],[/* 0 *//***/ function(module, exports, __webpack_require__) {    __webpack_require__(1);    __webpack_require__(2);/***/ },/* 1 *//***/ function(module, exports, __webpack_require__) {    __webpack_require__(2);    var chunk1=1;    exports.chunk1=chunk1;/***/ },/* 2 *//***/ function(module, exports) {    var chunk2=1;    exports.chunk2=chunk2;/***/ }]);

而 manifest 文件的作用就是在运行的时候通过__webpack_require__后的模块标识(Module identifier)来加载指定的模块内容。比如 manifest例子 生成的 manifest.json 文件内容是如下的格式:

{  "common.js": "common.js",  "main.js": "main.js",  "main1.js": "main1.js"}

这样就可以在源文件和目标文件之间有一个映射关系,而这个映射关系本身依然存在于打包后的输出目录,而不会因为 src 目录消失了而不知道具体的模块对应关系。而至于其中 moduleId 等的对应关系是由 Webpack 自己维护的,通过打包后的 可视化 可以了解。

CommonChunkPlugin 无法抽取单入口文件公共模块

上面讲了 Webpack 官网提供的例子以及原理分析,下面通过自己构造的几个例子来深入理解上面的概念。假如有如下的 Webpack 配置文件:

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");module.exports = {  entry:   {    main:process.cwd()+'/example1/main.js',  },  output: {    path:process.cwd()+'/dest/example1',    filename: '[name].js'  },  devtool:'cheap-source-map',  plugins: [   new CommonsChunkPlugin({       name:"chunk",       minChunks:2   })  ]};

下面是入口文件内容:

//main.jsrequire("./chunk1");require("./chunk2");console.log('main1.');

其中 chunk1.js 内容如下:

require("./chunk2");var chunk1=1;exports.chunk1=chunk1;

而 chunk2.js 内容如下:

var chunk2=1;exports.chunk2=chunk2;

我们引入了 CommonsChunkPlugin,并将那些引入了两次以上的模块输出到 chunk.js 中。那么你肯定会认为,chunk2.js 被引入了两次,那么它肯定会被插件抽取到 chunk.js 中,但是实际上并不是这样。可以查看 main.js,内容如下:

webpackJsonp([0,1],[/* 0 *//***/ function(module, exports, __webpack_require__) {    __webpack_require__(1);    __webpack_require__(2);/***/ },/* 1 *//***/ function(module, exports, __webpack_require__) {    __webpack_require__(2);    var chunk1=1;    exports.chunk1=chunk1;/***/ },/* 2 *//***/ function(module, exports) {    var chunk2=1;    exports.chunk2=chunk2;/***/ }]);

通过这个例子可知:“单入口文件时候不能把引用多次的模块打印到 CommonChunkPlugin 中”。

CommonChunkPlugin 抽取多入口文件公共模块

假如有如下的 Webpack 配置文件:

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");module.exports = {  entry:   {      main:process.cwd()+'/example2/main.js',      main1:process.cwd()+'/example2/main1.js',  },  output: {    path:process.cwd()+'/dest/example2',    filename: '[name].js'  },  plugins: [   new CommonsChunkPlugin({       name:"chunk",       minChunks:2   })  ]};

其中 main1.js 内容如下:

require("./chunk1");require("./chunk2");

而 main.js 内容如下:

require("./chunk1");require("./chunk2");

而 chunk1.js 内容如下:

require("./chunk2");var chunk1=1;exports.chunk1=chunk1;

而 chunk2.js 内容如下:

var chunk2=1;exports.chunk2=chunk2;

此时,很显然采用的是多入口文件模式,在相应的目录下会生成 main.js 和 main1.js,以及 chunk.js,而 chunk.js 中抽取的是 main.js 和 main1.js 中被引入了两次以上的模块,很显然 chunk1.js 和 chunk2.js 都会被引入到 chunk.js 中,下面是 chunk.js 中的部分代码:

/******/ ([/* 0 */,/* 1 *//***/ function(module, exports, __webpack_require__) {    __webpack_require__(2);    var chunk1=1;    exports.chunk1=chunk1;/***/ },/* 2 *//***/ function(module, exports) {    var chunk2=1;    exports.chunk2=chunk2;/***/ }/******/ ]);

CommonChunkPlugin 分离业务代码与框架代码

假如有如下的 Webpack 配置内容,同时 chunk1、chunk2、main1、main 的内容和上面保持一致。

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");module.exports = {    entry: {        main: process.cwd()+'/example3/main.js',        main1: process.cwd()+'/example3/main1.js',        common1:["jquery"],        //只含有 jquery.js        common2:["vue"]        //只含有 vue.js 和加载器代码    },    output: {        path: process.cwd()+'/dest/example3',        filename: '[name].js'    },    plugins: [        new CommonsChunkPlugin({            name: ["chunk",'common1','common2'],            minChunks:2            //引入两次以及以上的模块        })    ]};

按照 CommonsChunkPlugin 的抽取公共代码的逻辑,会有如下的结果:

chunk.js 中保存的是 main.js 和 main1.js 的公共代码,即 chunk1.js 和 chunk2.js common1.js 中只有 jquery.jscommon2.js 中只有 vue.js,但是必须含有 Webpack 的加载器代码 

其实道理很简单,chunk.js 中只有 chunk1.js 和 chunk2.js,而不存在被引入了两次的模块,最多引入次数的就是 chunk2.js,所以 common1.js 只含有 jquery.js。但是,正如前文所说,common2.js 必须最先加载。

minChunks 为 Infinity 配置

假如 Webpack 配置如下:

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");module.exports = {    entry: {        main: process.cwd()+'/example5/main.js',        main1: process.cwd()+'/example5/main1.js',        jquery:["jquery"]        //minChunks: Infinity 时候框架代码依然会被单独打包成一个文件    },    output: {        path: process.cwd() + '/dest/example5',        filename: '[name].js'    },    plugins: [        new CommonsChunkPlugin({            name: "jquery",            minChunks:2//被引用两次及以上        })    ]};

上面的文件输出将会是如下内容:

main.js 包含去掉的公共代码部分main1.js 包含去掉的公共代码部分main1.js 和 main2.js 的公共代码将会被打包到 jquery.js 中,即 jquery.js 包含 jquery+ 公共的业务代码

其实,这个配置稍微晦涩难懂一点,假如将上面的 minChunks 配置修改为"Infinity",那么结果将截然不同:

main.js 原样打包main1.js 原样打包jquery 包含 jquery.js 和 webpack 模块加载器    

因为将 minChunks 设置为 Infinity,也就是无穷大,那么 main.js 和 main1.js 中不存在任何模块被依赖的次数这么大,因此 chunk.js 和 chunk1.js 都不会被抽取出来。

chunks 指定那些入口文件中的公共模块会被抽取

继续修改 Webpack 配置如下:

var CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");module.exports = {    entry: {        main: process.cwd()+'/example6/main.js',        main1: process.cwd()+'/example6/main1.js',        jquery:["jquery"]    },    output: {        path: process.cwd()  + '/dest/example6',        filename: '[name].js'    },    plugins: [        new CommonsChunkPlugin({            name: "jquery",            minChunks:2,            chunks:["main","main1"]            //main.js 和 main1.js 中都引用的模块才会被打包的到公共模块        })    ]};

此时 chunks 设置为 ["main","main1"],表示只有 main.js 和 main1.js 中都引用的模块才会被打包的到公共模块,而且必须是依赖次数为 2 次以上的模块。因此结果将会如下:

jquery.js 中包含 main1.js 和 main.js 中公共的模块,即 chunk1.js 和 chunk2.js,以及 jquery.js 本身main1.js 表示是去掉公共模块后的文件内容main.js 表示是去掉公共模块后的文件内容    

我们也可以通过查看打包后的 jquery.js 看到结果验证,即 jquery.js 包含了 jquery.js 本身以及公共的业务代码:

/* 2 *//***/ function(module, exports, __webpack_require__) {  __webpack_require__(3);  var chunk1=1;  exports.chunk1=chunk1;/***/ },/* 3 *//***/ function(module, exports) {  var chunk2=1;  exports.chunk2=chunk2;/***/ }

本章小结

本章节主要通过 7 个例子展示了 Webpack 配合 CommonsChunkPlugin 的打包结果,但是为什么结果是这样,会在 Webpack 常见插件原理分析章节进行深入的剖析。本章节所有的例子代码你可以 点击这里 查看。文中的配置都是参考 Webpack 2 的,如果使用的是 Webpack 1,请升级。如果需要查看上面的例子的运行结果,请执行下面的命令:

npm install webpack -ggit clone https://github.com/liangklfangl/commonsChunkPlugin_Config.gitwebpack//修改 webpack.config.js 并运行 webpack 命令
第03课:webpack-dev-server 核心概念
第04课:webpack-dev-server 基本使用
第05课:Webpack 的 HMR 原理分析
第06课:Webpack 中的 Compiler 和 Compilation 对象
第07课:Webpack 常见插件原理分析
第08课:写一个 Webpack 插件
第09课:写一个 Webpack 的 Loader
第10课:Webpack 结合 React-Router 实现按需加载
第11课:Webpack 2 的 Tree-shaking 深入分析
第12课:以 Node 方式集成 Webpack 和 webpack-dev-server 打包

阅读全文: http://gitbook.cn/gitchat/column/59f57e2549cd43306135e255

你可能感兴趣的:(Webpack 达人的成长之路)