Webpack学习笔记

579 阅读13分钟

1 理解

1.1 基本理解

1. webpack是一个静态模块打包工具

2. 它会在内部构建一个依赖图(dependency graph),此依赖图会映射项目中所需的每个模块,并生成一个或多个bundle文件;

3. 区别webpack和webpack-cli

webpack做js的打包工作,webpack-cli解析webpack命令,命令内部使用webpack的功能;

4. webpack不用全局下载,只需要局部下载即可;

原因:公司内部的多个项目可能使用了不同版本的webpack;npx webpack 使用局部的webpack打包;使用npm scripts运行的webpack命令默认先找的是局部的webpack;

5. webpack本身能解析打包各种模块规范的JS代码;

ES6,CommonJS, AMD/requiredjs, CMD/seajs等模块规范;

6. 核心概念

模式(mode),入口(entry),输出(output),加载起(loader),插件(plugin);

1.2 webpack常用配置

mode, entry, output, module, plugins, devtools, devServer, resolve, optimization, externals

1.3 系列问题

1,webpack的10大配置选项
mode, entry, output, module, plugins, devtools, devServer, resolve, optimization, externals

2,区别css-loader和style-loader?
css-loader: 加载.css文件,通过特定的语法规则进行转化内容最后并导出
style-loader: 通过js创建一个style标签,并将css-loader内部样式注入到HTML页面;

3,区别url-loader和file-loader?
url-loader可以将图片转为base64格式,能更快的加载图片,图片过大就需要file-loader加载本地图片;url-loader可以设置图片超过一定大小时使用file-loader加载图片;

4,说说热模块替换(HMR)的理解?

5,说说history路由404问题?

6,loader和Plugin的不同

  • 不同的作用
    loader作为加载器,webpack原生只能解析js文件,loader的作用是让webpack有了加载和解析非js文件的能力;
    plugin用来扩展webpack的功能;在webpack运行的生命周期中会广播出许多事件,Plugin监听这些事件,在合适的时机通过webpack提供的api改变输出结果;
  • 不同的用法
    loader在module.rules中配置,作为模块的解析规则而存在,类型为数组,每一项都是一个 Object,里面描述了对于什么类型的文件(test),使用什么加载(loader)和使用的参数(options);
    plugin在plugins中单独配置,类型为数组,每一项都是一个plugin实例,参数都通过构造函数传入;

7,webpack编译流程

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

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

8,tree-shaking的原理:

  • ES6 Module引入静态分析,编译的时候就能判断到底加载了哪些代码模块
  • 静态分析程序流,分析哪些变量和模块未被使用或者引用。进而删除对应代码;
  • tree-shaking不支持动态导入(如CommonJS的require()语法),只支持纯静态的导入(ES6的import/export);
  • webpack中可以在项目package.json文件中,添加一个"sideEffects“属性,手动指定有副作用的脚本;

2 处理

2.1 loader处理

1,打包js

相关的包

babel-loader

@babel/core

@babel/preset-env

  • Babel只转换不兼容的新语法(const/let, 箭头函数, 解构赋值,class语法)
  • Babel不转换新的 API (Promise,Map/Set, Object.assign(), Iterator, Generator, Proxy, Reflect, Symbol等)

@babel/polyfill

  • core-js2 (提供了es5, es6的polyfills, 不包含async函数)
  • regenerator-runtime(编译async函数)

@babel/plugin-transform-runtime & @babel/runtime(这个插件是用来复用辅助函数的)

配置

基础配置

webpack.config.js文件配置

     {
        test: /\.js$/,
        use: {
          loader: 'babel-loader',
          options: { // 用babel-loader需要把es6-es5
            presets: [
              '@babel/preset-env'
            ],
            plugins: [
              // '@babel/plugin-proposal-class-properties'
              ["@babel/plugin-proposal-decorators", { "legacy": true }],
              ["@babel/plugin-proposal-class-properties", { "loose" : true }],
              "@babel/plugin-transform-runtime",
            ]
          }
        },
        include: path.resolve(__dirname, 'src'),
        exclude: /node_modules/,
      },

babel.config.js文件配置

module.exports = function(api) {
  api.cache(true);
  const presets = [
    [
      '@babel/preset-env'
    ]
  ];
  const plugins = [];
  return {
    presets,
    plugins
  }
}

2,打包图片

相关包

  • file-loader
  • url-loader (依赖于file-loader,针对小图片可以进行base64处理,减少请求)
  • image-webpack-loader

打包图片和Base64编码

代码如下, 好处:减少图片请求的数量,缺点是打包文件变大

{
        test: /\.(png|jpg|gif)$/,
        // 做一个限制,当我们的图片小于多少K时,用base64来转化
        // 否则用file-loader产生真实的图片
        use: {
          loader: 'url-loader',
          options: {
            limit: 1024*5, // 把小于5Kb的文件转化成Base64的格式
            outputPath: '/img/',
            publicPath: 'http://www.zhufengpeixun.cn', // 图片单独添加cdn 地址
          }
        },
      },

图片压缩

{
  loader: 'image-webpack-loader',
  options: {
    // 压缩jpg/peg图片
    mozjpeg: {
      progressive: true,
      quality: 65 //压缩率
    },
    // 压缩png图片
    pngquanty: {
      quality: [0.65, 0.90],
      speed: 4
    }
  }
}

3,打包字体

{
  test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, //字体
  use: [
    {
      loader: 'url-loader',
      options: {
        limit: 10240,
        name: 'fonts/[name].[hash:8].[ext]',
    }
  ]
},

4,打包音视频

{
  test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)$/, //字体
  use: [
    {
      loader: 'url-loader',
      options: {
        limit: 10240,
        name: 'static/media/[name].[hash:8].[ext]'
    }
  ]
},

5,打包样式

相关包

css-loader
style-loader
postcss-loader
autoprefixer
postcss-px2rem
less & less-loader
stylus & stylus-loader
node-sass & sass-loader
mini-css-extract-plugin
optimize-css-assets-webpack-plugin

css

css-loader & style-loader

{
        test: /\.css$/,
        use: [{
          loader: 'style-loader',
          options: {
            insertAt: 'top'
          }
        },
          'css-loader'
        ]
      },

css预编译器

less

less & less-loader

{
        test: /\.less$/,
        use: [{
          loader: 'style-loader',
          options: {
            insertAt: 'top'
          }
        },
          'css-loader',
          'postcss-loader',
          'less-loader'
        ]
      }

stylus

stylus & stylus-loader

{
        test: /\.styl$/,
        use: [{
          loader: 'style-loader',
          options: {
            insertAt: 'top'
          }
        },
          'css-loader',
          'postcss-loader',
          'stylus-loader'
        ]
      }

sass

node-sass & saaa-loader(node-sacc下载可能失败,yarn < npm < cnpm)

{
        test: /\.scss$/,
        use: [{
          loader: 'style-loader',
          options: {
            insertAt: 'top'
          }
        },
          'css-loader',
          'postcss-loader',
          'sass-loader'
        ]
      }

PostCss

依赖包

  • postcss-loader
  • 内部依赖postcss

postCss的插件

//webpack.config.js
[  'style-loader',  'css-loader',  'postcss-loader']

// postcss.confif.js
module.exports = {
  plugins: [
    require('autoprefixer')(),
    require('postcss-px2rem')({
      unitRem: 37.5
    }),
  ]
}
  • autoprefixer
  • postcss-px2rem

移动端适配

  • postcss-px2rem
  • lib-flexible

抽取/单独打包css

mini-css-extract-plugin

{
        test: /\.css$/,
        use: [
          MiniCssExtractPlugin.loader,
          'css-loader',
          'postcss-loader',
        ]
      },

new MiniCssExtractPlugin({
      filename: 'main.css',
    }),

压缩css

optimize-css-assets-webpack-plugin

optimization: {
    minimizer: [
      new OptimizeCssAssetsWebpackPlugin(),
    ]
  },

2.2 plugin处理

  1. html-webpack-plugin
    自动帮你生成一个 html 文件,并且引用相关的 assets 文件(如 css, js)

      new HtmlWebpackPlugin({
          template: './src/index.html',
          filename: 'index.html',
          minify: {
            removeAttributeQuotes: true,
            collapseWhitespace: true,
          },
          hash: true,
        })
    
  2. uglifyjs-webpack-plugin
    此插件用来对js代码进行压缩

    new UglifyJsPlugin({
            test: /\.js(\?.*)?$/i,  //测试匹配文件,
            include: /\/includes/, //包含哪些文件
            excluce: /\/excludes/, //不包含哪些文件
    
            //允许过滤哪些块应该被uglified(默认情况下,所有块都是uglified)。 
            //返回true以uglify块,否则返回false。
            chunkFilter: (chunk) => {
                // `vendor` 模块不压缩
                if (chunk.name === 'vendor') {
                  return false;
                }
                return true;
              }
            }),
      
            cache: false,   //是否启用文件缓存,默认缓存在node_modules/.cache/uglifyjs-webpack-plugin.目录
            parallel: true,  //使用多进程并行运行来提高构建速度
    })
    
  3. clean-webpack-plugin
    用来清除打包之后dist目录下其他多余或者无用的代码

    new CleanWebpackPlugin('./dist'),
    
  4. copy-webpack-plugin
    在webpack中拷贝文件和文件夹

    new CopyWebpackPlugin([
          {from: './doc', to: './'}
        ])
    
  5. happypack
    用来实现多进程打包,减少总构建时间

    // loader配置
    {
            test: /\.css$/,
            use: 'Happypack/loader?id=css',
          },
          {
            test: /\.js$/,
            exclude: /node_modules/,
            include: path.resolve('src'),
            use: 'Happypack/loader?id=js',
          }
    
    // plugin配置
      new Happypack({
          id: 'css',
          use: ['style-loader', 'css-loader'],
        }),
        new Happypack({
          id: 'js',
          use: [
            {
              loader: 'babel-loader',
              options: { // 用babel-loader需要把es6-es5
                presets: [
                  '@babel/preset-env',
                  '@babel/preset-react'
                ]
              }
            }
          ]
        }),
    
  6. webpack-merge
    该插件主要对webpack.config.js文件抽离合并,分为prod(生产环境),dev(开发环境),base(基本),然后在webpack.json中的“bulid”“dev”脚本后面增加 "–config ./build/prod.config.js"指定对应的webpackconfig;配置分别如下代码所示;
    webpack.base.js

    let path = require('path');
    let HtmlWebpackPlugin = require('html-webpack-plugin');
    let Webpack = require('webpack');
    
    module.exports = {
      // mode: 'production',
      entry: {
        index: './src/index.js'
      },
      module: {
        rules: [
          {
            test: /\.css$/,
            use: ['style-loader', 'css-loader']
          },
          {
            test: /\.js$/,
            use: {
              loader: 'babel-loader',
              options: { // 用babel-loader需要把es6-es5
                presets: [
                  '@babel/preset-env'
                ]
              }
            },
          }
        ]
      },
      plugins: [
        new Webpack.DefinePlugin({
          DEV: JSON.stringify('dev'),
          FLAG: 'true',
          EXPRESSION: JSON.stringify('1+1'),
        }),
        new HtmlWebpackPlugin({
          template: './index.html',
          filename: 'index.html',
        }),
      ],
      output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
      }
    }
    

       webpack.dev.js

let {smart} = require('webpack-merge');
let base = require('./webpack.base.js');

module.exports = smart(base, {
  mode: 'development',
  devServer: {

  },
  devtool: 'source-map',
})

webpack.prod.js

let {smart} = require('webpack-merge');
let base = require('./webpack.base.js');

module.exports = smart(base, {
  mode: 'production',
  optimization: {
    minimizer: [

    ]
  },
  plugins: [
    
  ]
})

2.3 devtool

devtool用来控制,以及如何生成source map

  1. 源码映射,会单独生成一个sourcemap文件 出错了,会标识当前报错的列和行

    devtool: 'source-map', // 增加映射文件 可以帮我们调试源代码
    
  2. 不会产生单独的文件,但是可以显示行和列

    devtool: 'eval-source-map',
    
  3. 不会产生列,但是是一个单独的映射文件

    devtool: 'cheap-module-source-map', // 产生后你可以保留起来
    
  4. 不会产生文件,集成在打包后的文件中 不会产生列

    devtool: 'cheap-module-eval-source-map',
    

2.4 devServer

devServer是webpack开发服务器

  1. 请求代理

    proxy: { // 重写的方法,把请求代理到express 服务器上
          '/api': {
            target: 'http://localhost:3000',
            pathRewrite: {'/api': ''}
          } // 配置了一个代理
        }
    
  2. 前端只想单纯来模拟数据

    before(app) {  // 提供的方法 钩子
          app.get('/user', (req, res) => {
            res.json({name: 'Stoney!'});
          });
        }
    
  3. 有服务端 不用用代理,能不能在服务器中启动webpack 端口用服务器端口

    // express
    
    let express = require('express');
    
    let app = express();
    let webpack = require('webpack');
    
    let middle = require('webpack-dev-middleware');
    let config = require('./webpack.config.js');
    let compiler = webpack(config);
    
    app.use(middle(compiler));
    
    app.get('/user', (req, res) => {
      res.json({name: 'Stoney!'});
    });
    
    app.listen(3000);
    

2.5 resolve

resolve用来配置webpack如何寻找解析第三方模块

resolve: { //解析第三方模块
    modules: [path.resolve('node_modules')], //去哪些目录下寻找第三方模块
    extensions: ['.js', '.css', '.json'], //配置在尝试过程中用到的后缀列表
    mainFields: ['style', 'main'],
    mainFiles: [], //入口文件的名字 index.js
    alias: { //别名
      bootstrap: 'bootstrap/dist/css/bootstrap.css'
    }
  },

3 优化

3.1 压缩代码

压缩html

new HtmlWebpackPlugin({
      template: './src/index.html',
      filename: 'index.html',
      minify: {
        removeAttributeQuotes: true,
        collapseWhitespace: true,
      },
      hash: true,
    })

压缩css

通过mini-css-extract-plugin单独打包css文件,optimize-css-assets-webpack-plugin进行css压缩;

压缩js

通过uglifyjs-webpack-plugin压缩js代码

压缩图片

通过image-webpack-loader对图片进行压缩;

3.2 缩小打包作用域

  • exclude/include确定loader规则范围

  • 利用resolve设置解析第三方模块规则,详细如上有说明

  • noParse对完全不需要解析的依赖库进行忽略

  • IgnorePlugin(完全排除模块)

  • 合理使用alias;

3.3 抽取页面公共资源

splitChunks抽取页面公共模块

optimization: {
    splitChunks: { //分割代码块
      cacheGroups: { // 缓存组
        common: { // 公共的模块
          chunks: 'initial',
          minSize: 0,
          minChunks: 2,
        },
        verdor: {
          priority: 1,
          test: /node_modules/, //把你抽离出来
          chunks: 'initial',
          minSize: 0,
          minChunks: 2,
        }
      }
    }
  },

3.4 DLL

使用DllPlugin进行分包,使用DllReferencePlugin(索引链接)对manifest.json 引用,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间;

webpack.config.react.js

let path = require('path');
let Webpack = require('webpack');

module.exports = {
  mode: 'development',
  entry: {
    react: ['react', 'react-dom'],
  },
  output: {
    filename: '_dll_[name].js',
    path: path.resolve(__dirname, 'dist'),
    library: '_dll_[name]', // _dll_react
    // libraryTarget: 'commonjs' // umd this
    // libraryTarget: 'var'
  },
  plugins: [
    new Webpack.DllPlugin({ // name = library
      name: '_dll_[name]',
      path: path.resolve(__dirname, 'dist', 'manifest.json')
    })
  ]
}

html

<script src="/_dll_react.js"></script>

webpack.config.js

new Webpack.DllReferencePlugin({
      manifest: path.resolve(__dirname, 'dist', 'manifest.json')
    }),

3.5 多线程打包

4 Tapable介绍和实现

4.1 Tapable介绍

webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,通过Tapable实现。Tapable类似于nodejs的events库,核心原理依赖于发布订阅模式;

const {
  SyncHook,
  SyncBailHook,
  SyncWaterfallHook,
  SyncLoopHook,
  AsyncParallelHook,
  AsyncParallelBailHook,
  AsyncSeriesHook,
  AsyncSeriesBailHook,
  AsyncSeriesWaterfallHook
} = require("tapable");

4.2 使用和实现

SyncHook

通过SyncHook创建同步钩子;是多种钩子中比较简单的一种,通过tap注册回调,调用call触发

使用

let { SyncHook } = require('tapable');

class Lesson {
  constructor() {
    this.hooks = {
      arch: new SyncHook(['name']),
    }
  }
  tap() {
    this.hooks.arch.tap('node', function (name) {
      console.log('node', name)
    });
    this.hooks.arch.tap('react', function (name) {
      console.log('react', name)
    });
  }
  start() {
    this.hooks.arch.call('stoney');
  }
}

let l = new Lesson();
l.tap();
l.start();

// node stoney
// react stoney

实现

class SyncHook {
  constructor(args) {
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    this.tasks.forEach((task) => task(...args));
  }
}

let hook = new SyncHook(['name']);
hook.tap('react', function (name) {
  console.log('react', name);
});
hook.tap('node', function (name) {
  console.log('node', name);
});
hook.call('stoney!')

// react stoney!
// node stoney!

SyncBailHook

类似于SyncHook,执行过程中注册的回调返回非undefined时就停止不再执行

使用

let { SyncBailHook } = require('tapable');

class Lesson {
  constructor() {
    this.hooks = {
      arch: new SyncBailHook(['name']),
    }
  }
  tap() {
    this.hooks.arch.tap('node', function (name) {
      console.log('node', name);
      // return '想停止學習'
      return undefined;
    });
    this.hooks.arch.tap('react', function (name) {
      console.log('react', name)
    });
  }
  start() {
    this.hooks.arch.call('stoney');
  }
}

let l = new Lesson();
l.tap();
l.start();

// node stoney
// react stoney

实现

class SyncBailHook {
  constructor(args) {
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    // this.tasks.forEach((task) => task(...args));
    let ret;
    let index = 0;
    do{
      ret = this.tasks[index++](...args)
    } while(ret === undefined && index < this.tasks.length);
  }
}

let hook = new SyncBailHook(['name']);
hook.tap('react', function (name) {
  console.log('react', name);
  // return '停止向下執行'
});
hook.tap('node', function (name) {
  console.log('node', name);
});
hook.call('stoney!')

// react stoney!
// node stoney!

SyncWaterfallHook

接收至少一个参数,上一个注册的回调返回值会作为下一个注册的回调的参数

使用

let { SyncWaterfallHook } = require('tapable');

class Lesson {
  constructor() {
    this.hooks = {
      arch: new SyncWaterfallHook(['name']),
    }
  }
  tap() {
    this.hooks.arch.tap('node', function (name) {
      console.log('node', name);
      return 'node 學的還不錯'
    });
    this.hooks.arch.tap('react', function (data) {
      console.log('react', data)
    });
  }
  start() {
    this.hooks.arch.call('stoney');
  }
}

let l = new Lesson();
l.tap();
l.start();

// node stoney
// react node 學的還不錯

实现

class SyncWaterfallHook {
  constructor(args) {
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    let [first, ...others] = this.tasks;
    let ret = first(...args);
    others.reduce((a, b) => {
     return b(a);
    }, ret)
  }
}

let hook = new SyncWaterfallHook(['name']);
hook.tap('react', function (name) {
  console.log('react', name);
  return 'react ok'
});
hook.tap('node', function (data) {
  console.log('node', data);
  return 'node ok'
});
hook.tap('webpack', function (data) {
  console.log('webpack', data);
});
hook.call('stoney!')

// react stoney!
// node react ok// webpack node ok

SyncLoopHook

有点类似于SyncBailHook,但是在执行过程中回调返回非undefined时继续再次执行当前的回调

使用

let { SyncLoopHook } = require('tapable');

class Lesson {
  constructor() {
    this.index = 0;
    this.hooks = {
      arch: new SyncLoopHook(['name']),
    }
  }
  tap() {
    this.hooks.arch.tap('node', (name) => {
      console.log('node', name);
      // return 'node 學的還不錯'
      return ++this.index === 3 ? undefined : '繼續學'
    });
    this.hooks.arch.tap('react', (data) => {
      console.log('react', data)
    });
  }
  start() {
    this.hooks.arch.call('stoney');
  }
}

let l = new Lesson();
l.tap();
l.start();

// node stoney
// node stoney
// node stoney
// react stoney

实现

class SyncLoopHook {
  constructor(args) {
    this.tasks = [];
  }
  tap(name, task) {
    this.tasks.push(task);
  }
  call(...args) {
    this.tasks.forEach(task => {
      let ret;
      do{
        ret = task(...args)
      } while(ret !== undefined)
      ;
    })
  }
}

let hook = new SyncLoopHook(['name']);
let total = 0;
hook.tap('react', function (name) {
  console.log('react', name);
  return ++total === 3 ? undefined : '繼續學'
});
hook.tap('node', function (name) {
  console.log('node', name);
});
hook.tap('webpack', function (name) {
  console.log('webpack', name);
});
hook.call('stoney!')

// react stoney!
// react stoney!
// react stoney!
// node stoney!
// webpack stoney!

AsyncParallelHook

AsyncParallelHook是并行异步钩子,当注册的所有回调都并行执行完毕后再执行callAsync或者promise中的函数

使用一

let {AsyncParallelHook} = require('tapable');

class Lesson {
  constructor() {
    this.index = 0;
    this.hooks = {
      arch: new AsyncParallelHook(['name']),
    }
  }
  tap() {
    this.hooks.arch.tapAsync('node', (name, cb) => {
      setTimeout(() => {
        console.log('node', name);
        cb();
      }, 1000);
    });
    this.hooks.arch.tapAsync('react', (name, cb) => {
      setTimeout(() => {
        console.log('react', name);
        cb();
      }, 1000);
    });
  }
  start() {
    this.hooks.arch.callAsync('stoney', function () {
      console.log('end')
    });
  }
}

let l = new Lesson();
l.tap();
l.start();

// node stoney
// react stoney
// end

实现一

class AsyncParallelHook {
  constructor(args) {
    this.tasks = [];
  }
  tapAsync(name, task) {
    this.tasks.push(task);
  }
  callAsync(...args) {
    let finalCallback = args.pop();
    let index = 0;
    let done = () => {
      index++;
      if(index === this.tasks.length) {
        finalCallback();
      }
    }
    this.tasks.forEach(task => {
      task(...args, done);
    }) 
  }
}

let hook = new AsyncParallelHook(['name']);
let total = 0;
hook.tapAsync('react', function (name, cb) {
  setTimeout(() => {
    console.log('react', name);
    cb();
  }, 1000);
});
hook.tapAsync('node', function (name, cb) {
  setTimeout(() => {
    console.log('node', name);
    cb();
  }, 1000);
});
hook.callAsync('stoney!', function () {
  console.log('end')
})

// node stoney
// react stoney
// end

使用二

let {AsyncParallelHook} = require('tapable');

class Lesson {
  constructor() {
    this.index = 0;
    this.hooks = {
      arch: new AsyncParallelHook(['name']),
    }
  }
  tap() {
    this.hooks.arch.tapPromise('node', (name) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('node', name);
          resolve();
        }, 1000);
      })
    });
    this.hooks.arch.tapPromise('react', (name) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('react', name);
          resolve();
        }, 1000);
      })
    });
  }
  start() {
    this.hooks.arch.promise('stoney').then(function() {
      console.log('end')
    });
  }
}

let l = new Lesson();
l.tap();
l.start();

实现二

class AsyncParallelHook {
  constructor(args) {
    this.tasks = [];
  }
  tapPromise(name, task) {
    this.tasks.push(task);
  }
  promise(...args) {
    let tasks = this.tasks.map(task => task(...args));
    return Promise.all(tasks)
  }
}

let hook = new AsyncParallelHook(['name']);
hook.tapPromise('react', function (name, cb) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('react', name);
      resolve();
    }, 1000);
  })
});
hook.tapPromise('node', function (name, cb) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('node', name);
      resolve();
    }, 1000);
  })
});
hook.promise('stoney!').then(function () {
  console.log('end')
})

AsyncParallelBailHook

执行过程中注册的回调返回非 undefined 时就会直接执行 callAsync 或者 promise 中的函数(由于并行执行的原因,注册的其他回调依然会执行)

AsyncSeriesHook

异步串行执行钩子

使用一

let {AsyncSeriesHook} = require('tapable');

class Lesson {
  constructor() {
    this.index = 0;
    this.hooks = {
      arch: new AsyncSeriesHook(['name']),
    }
  }
  tap() {
    this.hooks.arch.tapAsync('node', (name, cb) => {
      setTimeout(() => {
        console.log('node', name);
        cb();
      }, 1000);
    });
    this.hooks.arch.tapAsync('react', (name, cb) => {
      setTimeout(() => {
        console.log('react', name);
        cb();
      }, 1000);
    });
  }
  start() {
    this.hooks.arch.callAsync('stoney', function() {
      console.log('end')
    });
  }
}

let l = new Lesson();
l.tap();
l.start();

// node stoney
// react stoney
// end

实现一

class AsyncSeriesHook {
  constructor(args) {
    this.tasks = [];
  }
  tapAsync(name, task) {
    this.tasks.push(task);
  }
  callAsync(...args) {
    let finalCallback = args.pop();
    let index = 0;
    let next = () => {
      if(this.tasks.length === index) {
        return finalCallback();
      }
      let task = this.tasks[index++];
      task(...args,  next);
    }
    next();
  }
}

let hook = new AsyncSeriesHook(['name']);
hook.tapAsync('react', function (name, cb) {
  setTimeout(() => {
    console.log('react', name);
    cb();
  }, 1000);
});
hook.tapAsync('node', function (name, cb) {
  setTimeout(() => {
    console.log('node', name);
    cb();
  }, 1000);
});
hook.callAsync('stoney!', function () {
  console.log('end')
})

使用二

let {AsyncSeriesHook} = require('tapable');

class Lesson {
  constructor() {
    this.index = 0;
    this.hooks = {
      arch: new AsyncSeriesHook(['name']),
    }
  }
  tap() {
    this.hooks.arch.tapPromise('node', (name) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('node', name);
          resolve();
        }, 1000);
      })
    });
    this.hooks.arch.tapPromise('react', (name) => {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('react', name);
          resolve();
        }, 1000);
      })
    });
  }
  start() {
    this.hooks.arch.promise('stoney').then(function() {
      console.log('end')
    });
  }
}

let l = new Lesson();
l.tap();
l.start();

实现二

class AsyncSeriesHook {
  constructor(args) {
    this.tasks = [];
  }
  tapPromise(name, task) {
    this.tasks.push(task);
  }
  promise(...args) {
    let [first, ...others] = this.tasks;
    return others.reduce((p, n) => {
      return p.then(() => n(...args))
    }, first(...args))
  }
}

let hook = new AsyncSeriesHook(['name']);
hook.tapPromise('react', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('react', name);
      resolve();
    }, 1000);
  })
});
hook.tapPromise('node', function (name) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log('node', name);
      resolve();
    }, 1000);
  })
});
hook.promise('stoney!').then(function () {
  console.log('end')
})

AsyncSeriesBailHook

执行过程中注册的回调返回非 undefined 时就会直接执行 callAsync 或者 promise 中的函数,并且注册的后续回调都不会执行

AsyncSeriesWaterfallHook

与 SyncWaterfallHook 类似,上一个注册的异步回调执行之后的返回值会传递给下一个注册的回调

使用

let {AsyncSeriesWaterfallHook} = require('tapable');

class Lesson {
  constructor() {
    this.index = 0;
    this.hooks = {
      arch: new AsyncSeriesWaterfallHook(['name']),
    }
  }
  tap() {
    this.hooks.arch.tapAsync('node', (name, cb) => {
      setTimeout(() => {
        console.log('node', name);
        cb('error', 'result');
      }, 1000);
    });
    this.hooks.arch.tapAsync('react', (data, cb) => {
      setTimeout(() => {
        console.log('react', data);
        cb();
      }, 1000);
    });
  }
  start() {
    this.hooks.arch.callAsync('stoney', function() {
      console.log('end')
    });
  }
}

let l = new Lesson();
l.tap();
l.start();

实现

class AsyncSeriesWaterfallHook {
  constructor(args) {
    this.tasks = [];
  }
  tapAsync(name, task) {
    this.tasks.push(task);
  }
  callAsync(...args) {
    let finalCallback = args.pop();
    let index = 0;
    let next = (err, data) => {
      let task = this.tasks[index];
      if(!task) return finalCallback();
      if(index === 0) {
        task(...args, next);
      } else {
        task(data, next);
      }
      index++;
    }
    next();
  }
}

let hook = new AsyncSeriesWaterfallHook(['name']);
hook.tapAsync('react', function (name, cb) {
  setTimeout(() => {
    console.log('react', name);
    cb(null, '结果');
  }, 1000);
});
hook.tapAsync('node', function (data, cb) {
  setTimeout(() => {
    console.log('node', data);
    cb();
  }, 1000);
});
hook.callAsync('stoney!', function () {
  console.log('end')
})