likes
comments
collection
share

webpack从入门到进阶(四)---webpack原理

作者站长头像
站长
· 阅读数 2

项目准备工作

  1. 新建一个项目,起一个炫酷的名字

  2. 新建bin目录,将打包工具主程序放入其中

    主程序的顶部应当有:#!/usr/bin/env node标识,指定程序执行环境为node

  3. package.json中配置bin脚本

    {
    	"bin": "./bin/mywebpack-pack.js"
    }
    
  4. 通过npm link链接到全局包中,供本地测试使用

分析webpack打包的bundle文件

其内部就是自己实现了一个__webpack_require__函数,递归导入依赖关系

(function (modules) { // webpackBootstrap
  // The module cache
  var installedModules = {};

  // 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] = {
      i: moduleId,
      l: false,
      exports: {}
    };

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;
  }

  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
  ({
    "./src/index.js":
      (function (module, exports, __webpack_require__) {
        eval("let news = __webpack_require__(/*! ./news.js */ \"./src/news.js\")\r\nconsole.log(news.content)\n\n//# sourceURL=webpack:///./src/index.js?");
      }),
    "./src/message.js":
      (function (module, exports) {
        eval("module.exports = {\r\n  content: '今天要下雨了!!!'\r\n}\n\n//# sourceURL=webpack:///./src/message.js?");
      }),
    "./src/news.js":
      (function (module, exports, __webpack_require__) {
        eval("let message = __webpack_require__(/*! ./message.js */ \"./src/message.js\")\r\n\r\nmodule.exports = {\r\n  content: '今天有个大新闻,爆炸消息!!!内容是:' + message.content\r\n}\n\n//# sourceURL=webpack:///./src/news.js?");
      })
  });

自定义loader

学习目标

在学习给自己写的mywebpack-pack工具添加loader功能之前,得先学习webpack中如何自定义loader,所以学习步骤分为两大步:

  1. 掌握自定义webpack的loader
  2. 学习给mywebpack-pack添加loader功能并写一个loader

webpack以及我们自己写的mywebpack-pack都只能处理JavaScript文件,如果需要处理其他文件,或者对JavaScript代码做一些操作,则需要用到loader。

loader是webpack中四大核心概念之一,主要功能是将一段匹配规则的代码进行加工处理,生成最终的代码后输出,是webpack打包环节中非常重要的一环。

loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

之前都使用过别人写好的loader,步骤大致分为:

  1. 装包
  2. 在webpack.config.js中配置module节点下的rules即可,例如babel-loader(省略其他配置,只论loader)
  3. (可选步骤)可能还需要其他的配置,例如babel需要配置presets和plugin
const path = require('path')

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      { test: /\.js$/, use: 'babel-loader' }
    ]
  },
  mode: 'development'
}

实现一个简单的loader

loader到底是什么东西?能不能自己写?

答案是肯定的,loader就是一个函数,同样也可以自己来写

  1. 在项目根目录中新建一个目录存放自己写的loader:

webpack从入门到进阶(四)---webpack原理

  1. 编写myloader.js,其实loader就是对外暴露一个函数

    第一个参数就是loader要处理的代码

    module.exports = function(source) {
      console.log(source) // 只是简单打印并返回结果,不作任何处理
      return source
    }
    
  2. 同样在webpack.config.js中配置自己写的loader,为了方便演示,直接匹配所有的js文件使用自己的myloader进行处理

    const path = require('path')
    
    module.exports = {
      entry: './src/index.js',
      output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js'
      },
      module: {
        rules: [
          { test: /.js$/, use: './loaders/myloader.js' }
        ]
      },
      mode: 'development'
    }
    
  3. 如果需要实现一个简单的loader,例如将js中所有的“今天”替换成“明天”

    只需要修改myloader.js的内容如下即可

    module.exports = function(source) {
      return source.replace(/今天/g, '明天')
    }
    
  4. 同时也可以配置多个loader对代码进行处理

    const path = require('path')
    
    module.exports = {
      entry: './src/index.js',
      output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js'
      },
      module: {
        rules: [
          { test: /.js$/, use: ['./loaders/myloader2.js', './loaders/myloader.js'] }
        ]
      },
      mode: 'development'
    }
    
  5. myloader2.js

    module.exports = function(source) {
      return source.replace(/爆炸/g, '小道')
    }
    

loader的分类

不同类型的loader加载时优先级不同,优先级顺序遵循:

前置 > 行内 > 普通 > 后置

pre: 前置loader

post: 后置loader

指定Rule.enforce的属性即可设置loader的种类,不设置默认为普通loader

在mywebpack-pack中添加loader的功能

通过配置loader和手写loader可以发现,其实webpack能支持loader,主要步骤如下:

  1. 读取webpack.config.js配置文件的module.rules配置项,进行倒序迭代(rules的每项匹配规则按倒序匹配)
  2. 根据正则匹配到对应的文件类型,同时再批量导入loader函数
  3. 倒序迭代调用所有loader函数(loader的加载顺序从右到左,也是倒叙)
  4. 最后返回处理后的代码

在实现mywebpack-pack的loader功能时,同样也可以在加载每个模块时,根据rules的正则来匹配是否满足条件,如果满足条件则加载对应的loader函数并迭代调用

depAnalyse()方法中获取到源码后,读取loader:

let rules = this.config.module.rules
for (let i = rules.length - 1; i >= 0; i--) {
    // console.log(rules[i])
    let {test, use} = rules[i]
    if (test.test(modulePath)) {
        for (let j = use.length - 1; j >= 0; j--) {
            let loaderPath = path.join(this.root, use[j])
            let loader = require(loaderPath)
            source = loader(source)
        }
    }
}

自定义插件

学习目标

在学习给自己写的mywebpack-pack工具添加plugin功能之前,得先学习webpack中如何自定义plugin,所以学习步骤分为两大步:

  1. 掌握自定义webpack的plugin
  2. 学习给mywebpack-pack添加plugin功能并写一个plugin

插件接口可以帮助用户直接触及到编译过程(compilation process)。 插件可以将处理函数(handler)注册到编译过程中的不同事件点上运行的生命周期钩子函数上。 当执行每个钩子时, 插件能够完全访问到编译(compilation)的当前状态。

简单理解,自定义插件就是在webpack编译过程的生命周期钩子中,进行编码开发,实现一些功能。

webpack插件的组成

  • 一个 JavaScript 命名函数。
  • 在插件函数的 prototype 上定义一个 apply 方法。
  • 指定一个绑定到 webpack 自身的事件钩子。
  • 处理 webpack 内部实例的特定数据。
  • 功能完成后调用 webpack 提供的回调。

webpack的生命周期钩子

钩子作用参数类型
entryOption在处理了webpack选项的entry配置后调用context, entrySyncBailHook
afterPlugins在初始化内部插件列表后调用。compilerSyncHook
afterResolversCompiler初始化完毕后调用。compilerSyncHook
environment在准备编译器环境时调用,在对配置文件中的插件进行初始化之后立即调用。SyncHook
afterEnvironment在environment钩子之后立即调用,当编译器环境设置完成时。SyncHook
beforeRun在运行Compiler之前调用。compilerAsyncSeriesHook
runCompiler开始工作时调用。compilerAsyncSeriesHook
watchRun在新的编译被触发但在实际开始编译之前,在监视模式期间执行插件。compilerAsyncSeriesHook
normalModuleFactoryNormalModuleFactory创建后调用。normalModuleFactorySyncHook
contextModuleFactoryContextModuleFactory创建后运行插件。contextModuleFactorySyncHook
beforeCompile创建compilation参数后执行插件。compilationParamsAsyncSeriesHook
compilebeforeCompile在创建新编辑之前立即调用。compilationParamsSyncHook
thisCompilation在触发compilation事件之前,在初始化编译时调用。compilation,compilationParamsSyncHook
compilation创建compilation后运行插件。compilation,compilationParamsSyncHook
make在完成编译前调用。compilationAsyncParallelHook
afterCompile在完成编译后调用。compilationAsyncSeriesHook
shouldEmit在发射assets之前调用。应该返回一个告诉是否发射出去的布尔值。compilationSyncBailHook
emit向assets目录发射assets时调用compilationAsyncSeriesHook
afterEmit在将assets发送到输出目录后调用。compilationAsyncSeriesHook
done编译完成后调用。statsAsyncSeriesHook
failed如果编译失败,则调用。errorSyncHook
invalid在watching compilation失效时调用。fileName,changeTimeSyncHook
watchClose在watching compilation停止时调用。SyncHook

实现一个简单的plugin

compiler.hooks.done表示编译完成后调用的钩子,所以只需要在这个阶段注册时间,当打包完成会自动回调这个函数

class HelloWorldPlugin {
  apply(compiler) {
    compiler.hooks.done.tap('Hello World Plugin', (stats) => {
      console.log('Hello World!');
    });
  }
}

module.exports = HelloWorldPlugin;

实现一个html-webpack-plugin

使用html-webpack-plugin非常简单,而且功能非常好用,可以将指定的html模板复制一份输出到dist目录下,同时会自动引入bundle.js

如何自己实现?

  1. 编写一个自定义插件,注册afterEmit钩子
  2. 根据创建对象时传入的template属性来读取html模板
  3. 使用工具分析HTML,推荐使用cheerio,可以直接使用jQuery api
  4. 循环遍历webpack打包的资源文件列表,如果有多个bundle就都打包进去(可以根据需求自己修改,因为可能有chunk,一般只引入第一个即可)
  5. 输出新生成的HTML字符串到dist目录中
const path = require('path')
const fs = require('fs')
const cheerio = require('cheerio')
module.exports = class HTMLPlugin {
  constructor(options) {
    // 传入filename和template
    this.options = options
  }
  apply(compiler) {
    compiler.hooks.afterEmit.tap('HTMLPlugin', compilation => {
      // 根据模板读取html文件内容
      let result = fs.readFileSync(this.options.template, 'utf-8')
      // 使用cheerio来分析HTML
      let $ = cheerio.load(result)
      // 创建script标签后插入HTML中
      Object.keys(compilation.assets).forEach(item => $(`<script src="${item}"></script>`).appendTo('body'))
      // 转换成新的HTML并写入到dist目录中
      fs.writeFileSync(path.join(process.cwd(), 'dist', this.options.filename), $.html())
    })
  }
}

Compiler和Compilation的区别

  • compiler 对象表示不变的webpack环境,是针对webpack的
  • compilation 对象针对的是随时可变的项目文件,只要文件有改动,compilation就会被重新创建。

在mywebpack-pack中添加plugin的功能

tapable简介

在webpack内部实现事件流机制的核心就在于tapable,有了它就可以通过事件流的形式,将各个插件串联起来,tapable类似于node中的events库,核心原理也是发布订阅模式

基本用法如下

  1. 定义钩子
  2. 使用者注册事件
  3. 在合适的阶段调用钩子,触发事件
let { SyncHook } = require('tapable')
class Lesson {
  constructor() {
    this.hooks = {
      html: new SyncHook(['name']),
      css: new SyncHook(['name']),
      js: new SyncHook(['name']),
      react: new SyncHook(['name']),
    }
  }
  study() {
    console.log('开班啦,同学们好!')
    console.log('开始学html啦,同学们好!')
    this.hooks.html.call('小明')
    console.log('开始学css啦,同学们好!')
    this.hooks.css.call('小花')
    console.log('开始学js啦,同学们好!')
    this.hooks.js.call('小黑')
    console.log('开始学react啦,同学们好!')
    this.hooks.react.call('紫阳')
  }
}

let l = new Lesson()
l.hooks.html.tap('html', () => {
  console.log('我要写个淘宝!!!挣他一个亿!')
})

l.hooks.react.tap('react', (name) => {
  console.log('我要用react构建一个属于自己的王国!' + name + '老师讲的真好!!!')
})
l.study()

通过该案例可以看出,如果需要在学习的不同阶段,做出不同的事情,可以通过发布订阅模式来完成。而tapable可以帮我们很方便的实现发布订阅模式,同时还可以在调用时传入参数。

以上只是最基础的同步钩子演示,如果感兴趣,可以查阅官方文档,并练习对应的其他钩子,以下是tapable对外暴露的所有钩子:

exports.Tapable = require("./Tapable");
exports.SyncHook = require("./SyncHook");
exports.SyncBailHook = require("./SyncBailHook");
exports.SyncWaterfallHook = require("./SyncWaterfallHook");
exports.SyncLoopHook = require("./SyncLoopHook");
exports.AsyncParallelHook = require("./AsyncParallelHook");
exports.AsyncParallelBailHook = require("./AsyncParallelBailHook");
exports.AsyncSeriesHook = require("./AsyncSeriesHook");
exports.AsyncSeriesBailHook = require("./AsyncSeriesBailHook");
exports.AsyncSeriesWaterfallHook = require("./AsyncSeriesWaterfallHook");
exports.HookMap = require("./HookMap");
exports.MultiHook = require("./MultiHook");

利用tapable实现mywebpack-pack的plugin功能

在Compiler构造时,创建对应的钩子即可

	// Compiler的构造函数内部定义钩子
	this.hooks = {
      afterPlugins: new SyncHook(),
      beforeRun: new SyncHook(),
      run: new SyncHook(),
      make: new SyncHook(),
      afterCompile: new SyncHook(),
      shouldEmit: new SyncHook(),
      emit: new SyncHook(),
      afterEmit: new SyncHook(['compilation']),
      done: new SyncHook(),
    }

    // 触发所有插件的apply方法,并传入Compiler对象
    if (Array.isArray(this.config.plugins)) {
      this.config.plugins.forEach(plugin => {
        plugin.apply(this)
      })
    }

在合适的时机调用对应钩子的call方法即可,如需传入参数,可以在对应的钩子中定义好需要传入的参数,call时直接传入

webpack从入门到进阶(四)---webpack原理