webpack之loader和plugin简介

webpack是一个模块打包器(module bundler),提供了一个核心,核心提供了很多开箱即用的功能,同时它可以用loader和plugin来扩展。webpack本身结构精巧,基于tapable的插件架构,扩展性强,众多的loader或者plugin让webpack稍显复杂。

webpack常用配置包括:devtool、entry、 output、module、resolve、plugins、externals等,本文主要介绍下webpack常用的loader和plugin

webpack允许我们使用loader来处理文件,loader是一个导出为function的node模块。可以将匹配到的文件进行一次转换,同时loader可以链式传递。

loader的使用方式

一般loader的使用方式分为三种:

1:在配置文件webpack.config.js中配置

module.exports = {
  module: {
    rules: [
      {
        test: /\.txt$/,
        use: 'raw-loader'
      }
    ]
  }
}

2:通过命令行参数方式

webpack --module-bind 'txt=raw-loader'

3:通过内联使用

import txt from 'raw-loader!./file.txt';

webpack常用的loader

样式:style-loader、css-loader、less-loader、sass-loader等

文件:raw-loader、file-loader 、url-loader等

编译:babel-loader、coffee-loader 、ts-loader等

校验测试:mocha-loader、jshint-loader 、eslint-loader等

比如下面配置,可以匹配.scss的文件,分别经过sass-loader、css-loader、style-loader的处理。

sass-loader转化sass为css文件,并且包一层module.exports成为一个js module。style-loader将创建一个style标签将css文件嵌入到html中。css-loader则处理其中的@import和url()。

module.exports = {
  module: {
    rules: [
        {
          test: /\.scss$/,
          use:[
              {loader:'style-loader'},
              {loader:'css-loader',options:{sourceMap:true,modules:true}},
              {loader:'sass-loader',options:{sourceMap:true}}
          ],
          exclude:/node_modules/
      }
    ]
  }
}

vue-loader、coffee-loader、babel-loader等可以将特定文件格式转成js模块、将其他语言转化为js语言和编译下一代js语言

file-loader、url-loader等可以处理资源,file-loader可以复制和放置资源位置,并可以指定文件名模板,用hash命名更好利用缓存。

url-loader可以将小于配置limit大小的文件转换成内敛Data Url的方式,减少请求。

raw-loader可以将文件已字符串的形式返回

imports-loader、exports-loader等可以向模块注入变量或者提供导出模块功能,常见场景是:

1:jQuery插件注入$,imports-loader?$=jquery

2:禁用AMD,imports-loader?define=false

等同于:var $ = require("jquery") 和 var define = false;

expose-loader:暴露对象为全局变量

如何写一个loader:官网介绍How to write a loader?

下面是一个简单的raw-loader,它可以将文本类文件转成字符串到js文件中。其中this.cacheable、this.value等是loader的api,分别是将结果标记为可缓存和把值传递给下一个loader。

module.exports = function(content) {
	this.cacheable && this.cacheable();
	this.value = content;
	return "module.exports = " + JSON.stringify(content);
}
webpack的plugin比loader强大,通过钩子可以涉及整个构建流程,可以做一些在构建范围内的事情。

webpack常用的plugin

官网介绍Plugins

第三方插件webpack-contrib/awesome-webpack

首先webpack内置UglifyJsPlugin,压缩和混淆代码。

webpack内置CommonsChunkPlugin,提高打包效率,将第三方库和业务代码分开打包。

ProvidePlugin:自动加载模块,代替require和import

new webpack.ProvidePlugin({
      $: 'jquery',
      jQuery: 'jquery'
    })

html-webpack-plugin可以根据模板自动生成html代码,并自动引用css和js文件

extract-text-webpack-plugin 将js文件中引用的样式单独抽离成css文件

DefinePlugin 编译时配置全局变量,这对开发模式和发布模式的构建允许不同的行为非常有用。

new webpack.DefinePlugin({
    PRODUCTION: JSON.stringify(true),
    VERSION: JSON.stringify("5fa3b9"),
    BROWSER_SUPPORTS_HTML5: true,
    TWO: "1+1",
    "typeof window": JSON.stringify("object")
  })

HotModuleReplacementPlugin 热更新

  1. 添加HotModuleReplacementPlugin
  2. entry中添加 "webpack-dev-server/client?localhost:8080/",
  3. entry中添加 "webpack/hot/dev-server"

(热更新还可以直接用webpack_dev_server --hot --inline,原理也是在entry中添加了上述代码)

webpack 内置的DllPluginDllReferencePlugin相互配合,前置第三方包的构建,只构建业务代码,同时能解决Externals多次引用问题。DllReferencePlugin引用DllPlugin配置生成的manifest.json文件,manifest.json包含了依赖模块和module id的映射关系

babili-webpack-plugin、transform-runtime 、transform-object-rest-spread

  1. babili-webpack-plugin:构建在babel之上,它的用处可以看BabiliWebpackPlugin
  2. transform-runtime :解决了babel在每个文件都插入了辅助代码,代码体积过大的问题。
  3. transform-object-rest-spread:Transform rest properties for object destructuring assignment and spread properties for object literals,为对象字面量添加解构赋值和spread属性

optimize-css-assets-webpack-plugin 不同组件中重复的css可以快速去重

webpack-bundle-analyzer 一个webpack的bundle文件分析工具,将bundle文件以可交互缩放的treemap的形式展示。

compression-webpack-plugin 生产环境可采用gzip压缩JS和CSS

happypack:通过多进程模型,来加速代码构建

const os = require('os');
      let HappyPack = require('happypack');
      let happyThreadPool = HappyPack.ThreadPool({size: os.cpus().length});
      exports.plugins = [
        new HappyPack({
          id: 'jsx',
          threadPool: happyThreadPool,
          loaders: [ 'babel-loader' ]
        }),

        new HappyPack({
          id: 'coffeescripts',
          threadPool: happyThreadPool,
          loaders: [ 'coffee-loader' ]
        })
      ];

      exports.module.loaders = [
        {
          test: /\.js$/,
          loaders: [ 'happypack/loader?id=jsx' ]
        },
        {
          test: /\.coffee$/,
          loaders: [ 'happypack/loader?id=coffeescripts' ]
        },
      ]


写一个webpack插件:

官网介绍:[how to write a plugin](How to write a plugin?)

主要的步骤如下:

  1. 编写一个JavaScript命名函数。
  2. 在它的原型上定义一个apply方法。
  3. 指定挂载的webpack事件钩子。
  4. 处理webpack内部实例的特定数据。
  5. 功能完成后调用webpack提供的回调。

编写插件之前要理解compiler和compilation两个对象,以及webpack生命周期的各个阶段和钩子,plugin比loader强大,通过plugin你可以访问compliler和compilation过程,通过钩子拦截webpack的执行。

比如我们可以在构建生成文件时,将所有生成的文件名生成到filelist.md的文件中

webpack会将compilation.assets的内容生成文件,所以可以在构建中利用它生成我们想要的文件。
function FileListPlugin(options) {}
FileListPlugin.prototype.apply = function(compiler) {
  compiler.plugin('emit', function(compilation, callback) {
    var filelist = 'In this build:\n\n';
    for (var filename in compilation.assets) {
      filelist += ('- '+ filename +'\n');
    }
    compilation.assets['filelist.md'] = {
      source: function() {
        return filelist;
      },
      size: function() {
        return filelist.length;
      }
    };
    callback();
  });
};

module.exports = FileListPlugin;

比如我们可以在html-webpack-plugin生成文件后刷新页面,完成热更新效果。

var webpack = require('webpack')
var webpackConfig = require('./webpack.config')
var compiler = webpack(webpackConfig)
var hotMiddleware = require('webpack-hot-middleware')(compiler, {
  log: () => {}
})
compiler.plugin('compilation', function (compilation) {
  compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) {
    hotMiddleware.publish({ action: 'reload' })
    cb()
  })
})

比如我们可以在构建完成后,打开一个提示窗口。

class Notifier {
  apply(compiler) {
    compiler.plugin("done", (stats) => {
      const pkg = require("./package.json");
      const notifier = require("node-notifier");
      const time = ((stats.endTime - stats.startTime) / 1000).toFixed(2);

      notifier.notify({
        title: pkg.name,
        message: `WebPack is done!\n${stats.compilation.errors.length} errors in ${time}s`,
        contentImage: "https://path/to/your/logo.png",
      });
    });
  }
}

module.exports = Notifier;

webpack 插件分析

1. 首先介绍webpack源码分析方法

  • node --inspect-brk ./node_modules/webpack/bin/webpack.js --config ./webpack.config.js
  • chrome输入 chrome://inspect/

2. 主要的流程是:



3. webpack构建的主要钩子:



主要包括编译,分析模块及依赖关系,构建模块,封装结果,生成文件等

4. compiler和compilation都继承于Tapable

webpack的插件是基于Tapable的,Tapable允许你添加和应用插件到javascript模块中,类似于 NodeJS的EventEmitter,可以被继承和mixin到其他模块中,详情见官网Tapable

其中关键的方法是

  • plugin(name:string, handler:function)
  • apply(...pluginInstances: (AnyPlugin|function)[])
  • applyPlugins*(name:string, ...)
  • mixin(pt: Object)

tapable主要负责处理事件,采用的是发布订阅模式,apply相当于trigger,plugin相当于addEventListener

Tapable.prototype.plugin = function plugin(name, fn) {
	if(Array.isArray(name)) {
		name.forEach(function(name) {
			this.plugin(name, fn);
		}, this);
		return;
	}
	if(!this._plugins[name]) this._plugins[name] = [fn];
	else this._plugins[name].push(fn);
};

plugin方法将插件对应的方法加入一个数组中、注册到事件(name)上,等待apply的时候串行调用/触发

Compilation中做了很多事情,处理编译过程。所对应的方法,如addEntry ,buildModule,processModuleDependencies,createChunkAssets,seal等

后记:

webpack的设计思想、插件机制值得我们深入学习和交流,还有一些特性,比如tree shaking,scope hoisting……


参考文章:

编辑于 2017-08-01

文章被以下专栏收录

    我们是美团点评点餐事业部终端团队,支持点餐业务各终端应用的开发工作。 团队目标 我们的目标是打造业界一流的终端技术团队。在业务场景的丰富度、技术栈的成熟度、团队管理的专业度方面追求业界一流水平。 我们始终以持续的节奏来引进新技术,保持技术栈的先进性,并结合业务需要做出创新。 我们关注每个人的个人成长,通过日常的培训分享,多层次的1v1指导体系,以及IDP沟通来帮助大家提升自身能力。 团队文化 我们有高效而活泼的工作氛围,不会强调工作时长,而是关注有效产出。

    前端干货,欢迎投稿