Webpack相关原理浅析

基本打包机制前端

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序须要的每一个模块,而后将全部这些模块打包成一个或多个 bundle。vue

打包过程能够拆分为四步:java

一、利用babel完成代码转换,并生成单个文件的依赖node

二、从入口开始递归分析,并生成依赖图谱webpack

三、将各个引用模块打包为一个当即执行函数git

四、将最终的bundle文件写入bundle.js中github

 

小解读:web

1.1 利用@babel/parser解析代码,识别modulenpm

1.2 利用@babel/traverse遍历AST,获取经过import引入的模块并保存所依赖的模块json

1.3 经过@babel/core和@babel/preset-env进行代码的转换,就是转化ES6/7/8代码等

1.4 输出单个文件的依赖

return{
        filename,//该文件名
        dependencies,//该文件所依赖的模块集合(键值对存储)
        code//转换后的代码
    }

2.1 从入口开始,广度遍历全部依赖,并输出整个项目的依赖图谱

graphArray.forEach(item => {
        graph[item.filename] = {
            dependencies: item.dependencies,
            code: item.code
        }
    })
    return graph

3.1 生成代码字符串

4.1 写入文件

 

完整代码见:https://github.com/LuckyWinty/blog/tree/master/code/bundleBuild

 

以上是打包的基本机制,而webpack的打包过程,会基于这些基本步骤进行扩展,主要有如下步骤:

1. 初始化参数 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数

2. 开始编译 用上一步获得的参数初始Compiler对象,加载全部配置的插件,通 过执行对象的run方法开始执行编译

3. 肯定入口 根据配置中的 Entry 找出全部入口文件

4. 编译模块 从入口文件出发,调用全部配置的 Loader 对模块进行编译,再找出该模块依赖的模块,再递归本步骤直到全部入口依赖的文件都通过了本步骤的处理

5. 完成模块编译 在通过第4步使用 Loader 翻译完全部模块后, 获得了每一个模块被编译后的最终内容及它们之间的依赖关系

6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再将每一个 Chunk 转换成一个单独的文件加入输出列表中,这是能够修改输出内容的最后机会

7. 输出完成:在肯定好输出内容后,根据配置肯定输出的路径和文件名,将文件的内容写入文件系统中。

整个流程归纳为3个阶段,初始化、编译、输出。而在每一个阶段中又会发生不少事件,Webpack会将这些事件广播出来供Plugin使用。具体钩子,能够看官方文档:https://webpack.js.org/api/compiler-hooks/#hooks

Webpack Loader

Loader 就像一个翻译员,能将源文件通过转化后输出新的结果,而且一个文件还能够链式地通过多个翻译员翻译。

概念:

  • 一个Loader 的职责是单一的,只须要完成一种转换
  • 一个Loader 其实就是一个Node.js 模块,这个模块须要导出一个函数

开发Loader形式

1.基本形式

module.exports = function (source ) { 
      return source; 
}

 

2.调用第三方模块

const sass= require('node-sass'); 
module.exports = function (source) { 
  return sass(source);
}

因为 Loader 运行在 Node.js 中,因此咱们能够调用任意 Node.js 自带的 API ,或者安装第三方模块进行调用

 

三、调用Webpack的Api

//获取用户为 Loader 传入的 options
const loaderUtils =require ('loader-utils'); 
module.exports = (source) => {
    const options= loaderUtils.getOptions(this); 
    return source; 
}
//返回sourceMap
module.exports = (source)=> { 
    this.callback(null, source, sourceMaps); 
    //当咱们使用 this.callback 返回内容时 ,该 Loader 必须返回 undefined,
    //以让 Webpack 知道该 Loader 返回的结果在 this.callback 中,而不是 return中
    return; 
}
// 异步
module.exports = (source) => {
    const callback = this.async()
    someAsyncOperation(source, (err, result, sourceMaps, ast) => {
        // 经过 callback 返回异步执行后的结果
        callback(err, result, sourceMaps, ast)
    })
}
//缓存加速
module.exports = (source) => { 
    //关闭该 Loader 的缓存功能
    this.cacheable(false)
    return source 
}

 

source参数是compiler 传递给 Loader 的一个文件的原内容,这个函数须要返回处理后的内容,这里为了简单起见,直接将原内容返回了,至关于该Loader 有作任何转换.这里结合了webpack的api和第三方模块以后,能够说loader能够作的事情真的很是很是多了...

更多的webpack Api能够看官方文档:https://webpack.js.org/api/loaders

Webpack Plugin

专一处理 webpack 在编译过程当中的某个特定的任务的功能模块,能够称为插件

 

概念:

  • 是一个独立的模块
  • 模块对外暴露一个 js 函数
  • 函数的原型 (prototype) 上定义了一个注入 compiler 对象的 apply 方法 apply 函数中须要有经过 compiler 对象挂载的 webpack 事件钩子,钩子的回调中能拿到当前编译的 compilation 对象,若是是异步编译插件的话能够拿到回调 callback
  • 完成自定义子编译流程并处理 complition 对象的内部数据
  • 若是异步编译插件的话,数据处理完成后执行 callback 回调。

 

开发基本形式

    // 一、BasicPlugin.js 文件(独立模块)
    // 二、模块对外暴露的 js 函数
    class BasicPlugin{ 
        //在构造函数中获取用户为该插件传入的配置
        constructor(pluginOptions) {
            this.options = pluginOptions;
        } 
        //三、原型定义一个 apply 函数,并注入了 compiler 对象
        apply(compiler) { 
            //四、挂载 webpack 事件钩子(这里挂载的是 emit 事件)
            compiler.plugin('emit', function (compilation, callback) {
                // ... 内部进行自定义的编译操做
                // 五、操做 compilation 对象的内部数据
                console.log(compilation);
                // 六、执行 callback 回调
                callback();
            });
        }
    } 
    // 七、暴露 js 函数
    module.exports = BasicPlugin;

 

Webpack 启动后,在读取配置的过程当中会先执行 new BasicPlugin(options )初始化一个 BasicPlugin 并得到其实例。在初始化 Compiler 对象后,再调用 basicPlugin.apply (compiler )为插件实例传入 compiler 对象。插件实例在获取到 compiler 对象后,就能够经过 compiler. plugin (事件名称 ,回调函数)监听到 Webpack 广播的事件,而且能够经过 compiler 对象去操做 Webpack。

Compiler对象

 

compiler 对象是 webpack 的编译器对象,compiler 对象会在启动 webpack 的时候被一次性地初始化,compiler 对象中包含了全部 webpack 可自定义操做的配置,例如 loader 的配置,plugin 的配置,entry 的配置等各类原始 webpack 配置等

webpack部分源码:https://github.com/webpack/webpack/blob/10282ea20648b465caec6448849f24fc34e1ba3e/lib/webpack.js#L30

 

Compilation 对象

 compilation 实例继承于 compiler,compilation 对象表明了一次单一的版本 webpack 构建和生成编译资源的过程。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,一次新的编译将被建立,从而生成一组新的编译资源以及新的 compilation 对象。一个 compilation 对象包含了 当前的模块资源、编译生成资源、变化的文件、以及 被跟踪依赖的状态信息。编译对象也提供了不少关键点回调供插件作自定义处理时选择使用。

Compiler 和 Compilation 的区别在于: Compiler 表明了整个 Webpack 从启动到关闭的生命周期,而 Compilation 只表明一次新的编译。

 

Tapable & Tapable 实例

webpack 的插件架构主要基于 Tapable 实现的,Tapable 是 webpack 项目组的一个内部库,主要是抽象了一套插件机制。它相似于 NodeJS 的 EventEmitter 类,专一于自定义事件的触发和操做。 除此以外, Tapable 容许你经过回调函数的参数访问事件的生产者。

 

webpack本质上是一种事件流的机制,它的工做流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,webpack中最核心的负责编译的Compiler和负责建立bundles的Compilation都是Tapable的实例,Tapable 可以让咱们为 javaScript 模块添加并应用插件。 它能够被其它模块继承或混合。

 

一些钩子的含义:

  • SyncBailHook:只要监听函数中有一个函数的返回值不为 null,则跳过剩下全部的逻辑。
  • SyncWaterfallHook:上一个监听函数的返回值能够传给下一个监听函数。
  • SyncLoopHook:当监听函数被触发的时候,若是该监听函数返回true时则这个监听函数会反复执行,若是返回 undefined 则表示退出循环。
  • AsyncParallelHook:异步并发,不关心监听函数的返回值
  • AsyncParallelBailHook:只要监听函数的返回值不为 null,就会忽略后面的监听函数执行,直接跳跃到callAsync等触发函数绑定的回调函数,而后执行这个被绑定的回调函数
  • AsyncSeriesHook:异步串行,不关心callback()的参数
  • AsyncSeriesBailHook:callback()的参数不为null,就会直接执行callAsync等触发函数绑定的回调函数
  • AsyncSeriesWaterfallHook:上一个监听函数的中的callback(err, data)的第二个参数,能够做为下一个监听函数的参数

同步钩子,用tap方式注册。异步钩子,有三种注册/发布的模式,tap、tapAsync、tapPromise。

 

Tapable 简化后的模型,就是咱们熟悉的发布订阅者模式

class SyncHook{
   constructor(){
      this.hooks = {}
   }
   
   tap(name,fn){
    if(!this.hooks[name])this.hooks[name] = []
     this.hooks[name].push(fn) 
   }      

   call(name){
     this.hooks[name].forEach(hook=>hook(...arguments))
   }
}

Loader & Plugin 开发调试

npm link

1. 确保正在开发的本地 Loader 模块的 package.json 已经配置好(最主要的main字段的入口文件指向要正确)

2. 在本地的 Npm 模块根目录下执行 npm link,将本地模块注册到全局

3. 在项目根目录下执行 npm link loader-name ,将第 2 步注册到全局的本地 Npm 模块连接到项目的 node moduels 下,其中的 loader-name 是指在第 1 步的package.json 文件中配置的模块名称

 

Npm link 专门用于开发和调试本地的 Npm 模块,能作到在不发布模块的状况下, 将本地的一个正在开发的模块的源码连接到项目的 node_modules 目录下,让项目能够直接使 用本地的 Npm 模块。因为是经过软连接的方式实现的,编辑了本地的 Npm 模块的代码,因此在项目中也能使用到编辑后的代码。

 

Resolveloader

ResolveLoader 用于配置 Webpack 如何寻找 Loader ,它在默认状况下只会去 node_modules 目录下寻找。为了让 Webpack 加载放在本地项目中的 Loader,须要修改 resolveLoader.modules。

构建工具选择

针对不一样的场景,选择最合适的工具

经过对比,不难看出,Webpack和Rollup在不一样场景下,都能发挥自身优点做用。webpack做为打包工具,可是在定义模块输出的时候,webpack确不支持ESM,webpack插件系统庞大,确实有支持模块级的Tree-Shacking的插件,如webpack-deep-scope-analysis-plugin。可是粒度更细化的,一个模块里面的某个方法,原本若是没有被引用的话也能够去掉的,就不行了....这个时候,就要上rollup了。rollup它支持程序流分析,能更加正确的判断项目自己的代码是否有反作用,其实就是rollup的tree-shaking更干净。因此咱们的结论是rollup 比较适合打包 js 的 sdk 或者封装的框架等,例如,vue 源码就是 rollup 打包的。而 webpack 比较适合打包一些应用,例如 SPA 或者同构项目等等。

结论:在开发应用时使用 Webpack,开发库时使用 Rollup

资料推荐

补充学习资料:https://github.com/LuckyWinty/blog/issues/1

更多学习资料推荐:

Loader: http://www.javashuo.com/article/p-yceuurxv-ho.html

Tapable: http://www.javashuo.com/article/p-qbjcitwl-de.html

webpack:

  • ebook:webpack深刻浅出
  • 极客时间:玩转webpack

 

 

更多:

想来深圳Shopee(外企,不加班,福利好,假期多)发展的。欢迎找我内推,前端、后台、测试、产品等各类岗~^_^

其余:若是方便的话,能够关注一下个人github,并给我刚开始的博客项目点个start~ ^_^

相关文章
相关标签/搜索