《深入浅出webpack--吴浩鳞》学习笔记

542 阅读2分钟

先附上地址:webpack.wuhaolin.cn/

第一章 入门

为什么需要模块化?

  • 命名空间冲突,两个库可能使用同一个名词,例如jQuery使用window.$,挂载在window下
  • 无法合理的控制项目的依赖和版本
  • 无法方便的控制依赖的加载顺便
  • 可以实现代码复用

项目大时难以维护,需要用模块化的思想来组织代码。

模块化规范CommonJS、AMD、ES6是什么?

CommonJS核心思想是通过require方法来同步的加载依赖的模块,通过module.exports导出需要暴露的接口。

AMD模块化规范采用异步的方式去加载依赖的模块。require导入,define定义一个模块

ES6模块化是Javascript模块化规范,将逐步取代CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。import导入,export default 导出。

第二章 配置

entry

context:webpack在寻找相对路径文件时会以context为根目录,所以context 必须是一个绝对路径的字符串

entry可以是string或array,就只会生成一个chunk。entry可以是object,就可能会出现多个chunk,这时chunk的名称是object键值对中的键的名称

output

变量名 含义
id chunk的唯一表示,从0开始
name chunk的名称
hash chunk的唯一标识的hash值
chunkhash chunk内容的hash值

其中 hash 和 chunkhash 的长度是可指定的,[hash:8] 代表取8位 Hash 值,默认是20位。

  • filename:可以设置'[name].js'
  • chunkFilename:配置无入口的 Chunk 在输出时的文件名称。应用场景使用 CommonChunkPlugin、 import('path/to/module') 动态加载等时
  • path:获取绝对路径。支持字符串模板,内置变量hash
  • publicPath:配置发布到线上资源的URL前缀。支持字符串模板,内置变量hash
  • crossOriginLoading:异步加载是,通过JSONP方式实现的。JSONP 的原理是动态地向 HTML 中插入一个 标签去加载异步资源。output.crossOriginLoading 则是用于配置这个异步插入的标签的 crossorigin 值。取值anonymous、或use-credentials
  • library导出库的名称,默认的输出格式是匿名的立即执行函数
  • libraryTarget导出库的类型,为枚举类型,默认var。可以是umd、umd2、commonjs2、commonjs、amd、this、var、assign、window、global、jsonp

module

module 配置处理模块的规则

  • loader配置:test、include、exclude三个配置来选中要应用规则的文件。use配置选中的文件应用的loader。enforce配置可将其中一个loader的执行顺序放到最前面(pre)或者最后(post)
  • noParse类型需要是RegExp 、[ RegExp )、 fun ction 中的一种。忽略对部分没有采用模块化的文件的递归解析和处理
  • parse属性可以更细颗粒度的配置哪些模块语法被解析,哪些不被解析

resolve

Webpack 在启动后会从配置的入口模块出发找出所有依赖的模块, Resolve 配置 Webpack 如何寻找模块所对应的文件。

  • alias:关键字替换,取别名
  • mainFields: 默认['browser', 'main']。Webpack 会按照数组里的顺序在 package .json 文件里寻找第三方模块的入口
  • extensions,导入语句没有后缀,webpack会自动带上后缀去尝试访问文件是否存在。默认['.js', '.json']
  • modules,配置webpack去哪些目录下寻找第三方模块,默认只会去node_modules目录下寻找
  • descriptionFiles配置描述第三方模块的文件名称,默认['package.json']
  • enforceExtension配置如果为true,则所有导入语句都必须带文件后缀
  • enforceModuleExtension配置,只针对node_modules下的模块生效

plugin

Plugin 用于扩展 Webpack 的功能 各种各样的 Plu gin 几乎可以让 Webpack 做任何与构建相关的事情。

devServer

注意,只有在通过devServer启动Webpack,配置文件里devServer才会生效,因为这些参数所对应的功能 DevServer 供的 Webpack 本身并不认 devServer 配置项

  • hot实现在不刷新整个页面的情况下通过用新模块替换老模块来实现实时预览
  • inline配置用于是否将这个代理客户端自动注入将运行在页面中的chunk里,默认自动注入
  • historyApiFallback用于方便的开发使用history 的单页用于,返回index.html
  • contentBase配置devserver HTTP服务器的文件根目录
  • header配置项科研在HTTP响应中注入一些响应头
  • host配置用于配置devserver服务监听的地址
  • port监听的端口
  • allowedHosts
  • disableHostCheck
  • clientLogLevel配置客户端的日志等级
  • compress 是否启用Gzip压缩
  • open第一次构建完,自动打开开发网页
  • openPage 配置打开指定的网页

contentBase 只能用来配置暴露本地文件的规则,可以通过 contentBase:false 关闭暴露本地文件。

其他配置

  • target配置 web、node、async-node、webworker、electron-main、electron-renderer
  • devtool配置source-map、eval-source-map等等
  • watch监听模式,支持监听文件更新。默认关闭,devServer时默认开启的。
  • watchOptions
  • externals外部扩展,用来告诉在Webpack要构建的代码中使用了哪些不用被打包的模块,也就是说这些模板是外部环境提供的, Webpack在打包时可以忽略它们。
  • resolveLoader用来告诉webpack如何去寻找loader
  • performance.hints:warning、error、false
module.exports = {
    resolveLoader:{
        modules: ['node_modules'],
        extensions: ['.js','.ts','.json'],
        mainFields: ['loader', 'main']//指明入口文件位置的字段
    },
}

导出多份配置

module.exports = [
    // 采用object 描述的一份配置
    {
        //...
    }
    //采用函数描述的一份配置
    function(){
        
    }
    //采用异步函数描述的一份配置
    function(){
        return Promise();
    }
]

总结

通常我们可用如下经验去判断如何配置 Webpack:

  • 若想让源文件加入构建流程中被 Webpack 控制,则配置 entry;
  • 若想自定义输出文件的位置和名称 ,则配置 output;
  • 若想自定义寻找依赖模块时 策略 ,则配置 resolve;
  • 若想自定义解析和转换文件的策略, 则配置 module ,通常是配置 module. rules 里的 Loader;
  • 若其他大部分需求可能通过Plugin去实现 ,则配置 plugin

第三章 实战

将新的ES6语法转成ES5、为新的API准入polyfill使其能在底端的浏览器中运行

babel

babel是一个javascript编译器,能将ES6代码转为ES5代码。 plugin属性告诉babel要使用哪些插件,这些插件可以控制如何转换代码。presets属性告诉babel要转换的源码使用了哪些新的语法特性

第四章 优化

缩小文件的搜索范围

  • loader: test、include、exclude
  • resolve.modules 第三方模块目录,默认['node_modules']
  • resolve.mainFields第三方模块描述入口文件的字段。默认值根据target不同不同
  • resolve.alias别名配置来将猿到人路径映射到一个新的路径
  • resolve.extensions后缀
  • module.noParse让webpack忽略对部分没有采用模块化的文件的递归解析处理。忽略的文件里不应该包含import、require、define等模块语句

使用DLLPlugin

.dll后缀文件,这些文件叫作动态链接库,在一个动态链接库中可以包含为其他模块调用的函数和数据。动态链接库的思想:

  • 1、将网页依赖的基础模块抽离出来,打包到一个单独的动态链接库中 。在一个动态链接库中可以包含多个模块。
  • 2、需要导入的模块存在于某个动态链接库中时,这个模块不能被再次打包,而是去动态链接库中获取。
  • 3、页面依赖的所有动态链接库都需要被加载。

manifest.json文件描述了与其对应的dll.js文件中包含哪些模块,以及每个模块的路径和ID

thread-loader

使用自动刷新

优化文件监听的性能:

watchOptions: {
    //不监听的目录
    ignored: /node_modules/,
    //第一个文件更改后,在重建之前添加一个延迟
    aggregateTimeout: 200,
    //轮询间隔
    poll: false
}

监听到文件更新后的下一步是刷新浏览器,webpack模块负责监听文件, webpack-dev-server模块则负责刷新浏览器。在使用webpack-dev-server 模块去启动webpack模块时webpack模块的监听模式默认会被开启。 webpack模块会在文件发生变化时通知webpack-dev-server模块。

devServer自动刷新:

  • 向要开发的网页中注入代理客户端代码,通过代理客户端去刷新整个页面,默认方式
  • 将要开发的网页装进iframe中,通过刷新iframe去看到最新效果

devServer.inline配置项,它用来控制是否向chunk中注入代理客户端,默认会注入。关闭后会以iframe 形式刷新

开启模块热替换

原理是在一个源码发生变化时,只需要新编译发生变化的模块,再用新输出的模块替换掉浏览器对应的老模块

webpack-dev-server --hot

plugins: {
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NameModulesPlugin(),
}


devServer: {
    hot: true
}

区分环境

压缩代码

js压缩,css压缩

CDN加速

CDN(内容分发网络)其实是通过优化物理链路层传输过程中的光速有限、丢包等问题来提升网速的

publicPath参数设置存放静态资源的CDN目录URL。为了让不同类型的资源输出到不同的CDN。

  • 针对html文件,不开启缓存,将html放到自己的服务器上,而不是CDN服务上,同事关闭自己服务器上的缓存
  • 针对静态的js、css、图片等文件,开启CDN和缓存,上传到CDN服务上,同时为每个文件名带上由文件内容算出的hash值。只要文件的内容发生变化,其对应的URL就会变化,它就会被重新下载,无论缓存时间有多长。

Tree Shaking

提取公共代码

代码分割

import() output.chunkFilename

babel-plugin-synta x-dynamic-import

使用prepack

开启scope hoisting

ModuleConcatenationPlugin

输出分析

webpack --profile --json > stats.json

优化总结

第五章 原理

webpack是什么?

webpack是一个打包模块化javascript的工具。通过loader转换文件,通过plugin注入钩子,最后输出由多个模块组合成的文件。webpack专注构建模块化项目。

一切文件如 JavaScript、css、scss 、图片、模板,对于 Webpack 来说都是一个个模块, 这样的好处是能清晰地描述各个模块之间的依赖关系,以方便 Webpack 对模块进行组合和打 包。经过 Webpack 的处理,最终会输出浏览器能使用的静态资源。

Webpack 可以为“模块化+新语言+新框架” 提供一站式的解决方案

webpack工作流程是怎么样的?

核心概念:

  • Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
  • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
  • Plugin:扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情。
  • Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果。

webpack工作流程是一个串行的过程,简述工作流程:

  • Webpack 启动后会从 Entry 里配置的 Module 开始递归解析 Entry 依赖的所有 Module。
  • 每找到一个 Module, 就会根据配置的 Loader 去找出对应的转换规则,对 Module 进行转换后,再解析出当前 Module 依赖的 Module。
  • 这些模块会以 Entry 为单位进行分组,一个 Entry 和其所有依赖的 Module 被分到一个组也就是一个 Chunk。
  • 最后 Webpack 会把所有 Chunk 转换成文件输出。 在整个流程中 Webpack 会在恰当的时机执行 Plugin 里定义的逻辑。

详细流程:

  • 1、初始化参数:从配置文件和shell语句中读取与合并参数,得到最终的参数
  • 2、开始编译:使用参数初始化compiler对象,加载所有配置的plugin,通过执行compiler.run方法开始执行编译
  • 3、确定入口:根据配置中的entry找到所有入口文件
  • 4、编译模块:从entry出发,调用所有配置的loader对module进行翻译,再找出module依赖的module。再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  • 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后, 得到了每个模块被翻译后的最终内容及它们之间的依赖关系。
  • 输出资源:根据entry和module之间的依赖关系,组装成个个包含多个module的Chunk,再将每个Chunk转换成个单独的文件加入输出列表中,这是可以修改输出内容的最后机会
  • 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,将文件的内容写入文件系统中

以上过程中, Webpack会在特定的时间点广播特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,井且插件可以调用Webpack提供的API改变Webpack 的运行结果。

为什么一个个的模块文件被合并成了单独的文件?为什么 输出bundle.js 能直接运行在浏览器中?

bundle.js 能直接运行在浏览器中的原因是,在输出的文件中通过__webpack require__函数,定义了一个可以在浏览器中执行的加载函数,来模拟 Node.js 中的 require语句。

原来一个个独立的模块文件被合并到了一个单独的bundle.js的原因是,浏览器不能Node.js那样快速地在本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。如果模块的数量很多,则加载时间会很长,因此将所有模块都存放在了数组中,执行一次网络加载。

异步加载

  • webpackJsonp 用于从异步加载的文件中安装模块。
  • 将 webpackJsonp 挂载到全局是为了方便在其他文件中调用。
  • param chunkids 异步加载的文件中存放的需要安装的模块对应的 Chunk ID
  • param moreModules 异步加载的文件中存放的需要安装的模块列表
  • param executeModules在异步加载的文件中存放的需要安装的模块都安装成功后,需要执行的模块对应的 index

编写loader

Loader本质就是接收字符串(或者buffer),再返回处理完的字符串(或者buffer)的过程。Loader的职责是单一的,只需要完成一种转换。如果一个源文件需要经历多步转换才能正常使用,就通过多个 Loader 去转换。

预处理文件csss:

  • sass-loader,将scss源码转成css
  • css-loader,找出css中依赖的资源、压缩css、autoprefixer、
  • style-loader,转换成通过脚本加载的 JavaScript代码

Webpack 是运行在 Node上的,一个Loader实就是一个node.js模块,这个模块需要导出一个函数。这个导出的函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。

官方地址:v4.webpack.docschina.org/contribute/…

const sass = require('node-sass');
const loaderUtils = require('loader-utils');

module.exports = function(source){
    // 获取配置参数
    const options = loaderUtils.getOptions(this);
    
    //通过 this.callback 告诉 Webpack 返回的结果
    this,callback(null, source, sourceMaps);
    return;
    
    // 告诉webpack本次转换是异步的,loader会在callback中返回调用结果
    var callback = this.async();
    someAsyncOperation(source, function(err, result , sourceMaps, ast) {
        //通过callback返回异步执行后的结果
        callback(err, result, sourceMaps, ast);
    })
    
    //通过raw的配置,传入loader数据为二进制数据
    source instanceof Buffer === true;
    return source;
    
    //关闭该loader的缓存功能
    this.cacheable(false);
    return source;
    
    //
    return sass(source);
}

//告诉webpack该loader是否需要二进制数据
module.exports.raw = true;

具体实例

module.exports = {
    module: {
        rules; [
             {
                test: /\.css$/i,
                use: [
                    {
                        loader: MiniCssExtractPlugin.loader,
                    },
                    'css-loader',
                    {
                        loader: path.resolve(__dirname, '../loaders/test.js'),
                        options: {
                            layout: 'eewfwefwefrewfe===='
                        }
                    }
                ]
            },
        ]
    }
}

//loaders/test.js
const loaderUtils = require('loader-utils');
module.exports = function test(source) {
   
    const options = loaderUtils.getOptions(this);
    console.log(options);
    return source.replace('red', '#333');
    
    //var callback = this.async();
    // fs.readFile(options.layout, 'utf-8', function (err, header) {
    //     if (err) {
    //         return callback(err);
    //     }
    //     callback(null, header + '\n' + source);
    // });
}

编写plugin

Webpack 通过 Plugin 机制让其更灵活,以适应各种应用场景。在 Webpack 行的生命 周期中会广播许多事件, Plugin 可以监昕这些事件,在合适的时机通过 Webpack 提供的 API改变输出结果。

compiler和compilation:

  • compiler对象包含webpack环境的所有配置信息,包含options、loaders、plugins等信息。这个对象在webpack启动时被实例化,它是全局唯一的,可以简单的将它理解为webpack的实例
  • compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。当webpack以开发模式运行时,每当检测到一个文件发送变化,便有一次新的compilation被创建。compilation对象也提供了很多事件回调供插件进行扩展。

二者区别:compiler代表整个webpack从启动到关闭的生命周期,而compilation只代表一次新的编译。

事件流: webpack通过Tapable来组织这条复杂的生产线。webpack在运行的过程中会广播事件,插件只需要监听它关心的事件,就能加入这条生产线中,去改变生产线的运作

class EndWebpackPlugin { 
    constructor (doneCallback, failCallback) { 
        //保存在构造函数中传入的回调函数
        this.doneCallback = doneCallback; 
        this.failCallback = failCallback; 
    }
    apply(compiler) { 
        compiler.hooks.done.tap('EndWebpackPlugin', (compilation, callback) =>{
            //在 done 事件中回调 doneCallback
            this.doneCallback(compilation) ;
        }
        compiler.hooks.failed.tap('EndWebpackPlugin', (err) => {
            //在 failed 件中回调 failCallback
            this.failCallback(err);
        }
        
         compiler.hooks.emit.tapAsync('AsyncPlugin', (compilation, callback) => {
             // 做一些异步的事情……
             setTimeout(function() {
                console.log('Done with async work...');
                callback();
            }, 1000);
        });
        
        compiler.hooks.emit.tapPromise('AsyncPlugin', compilation => {
            // 返回一个 Promise,在我们的异步任务完成时 resolve……
            return new Promise((resolve, reject) => {
                setTimeout(function() {
                    console.log('异步工作完成……');
                    resolve();
            }, 1000);
        });
    });
    }
}
    
module.exports = EndWebpackPlugin;

//配置
const EndWebpackPlugin = require('./plugins/BasicPlugin.js');
module.exports = {
    plugins: [
        new EndWebpackPlugin(()=>{}, ()=> {});
    ]
}