webpack系列之-原理篇

本系列将会从原理、开发、优化、对比四个方面给你们介绍webpack的工做流程。【默认是以webpack v3为例子】javascript

储备知识

CommonJS 规范

// 模块引入
let moduleA = require('./a.js')

// 模块导出
module.exports = () => {}

复制代码

es6规范

// 模块引入
import {moduleA} from './a.js'

// 模块导出
export default () => {}
复制代码

黑盒体验

咱们能够把webpack看作一个黑盒,只要会用就能够。先来体验一次很简单的webpack打包过程html

webpack

const webpack = require('webpack')
const path = require('path')

module.exports = {
  entry: './index.js',
  output: {
    filename: 'index.js',
    path: path.resolve(__dirname, 'public')
  }
}
复制代码

启动编译,在命令行输入 node_modules/.bin/webpack 就可看到一次打包过程java

查看打包结果node

关于如何启动webpack

若是是全局安装了webpack,能够在命令行直接输入 webpackwebpack

若是只是项目文件夹安装,须要输入 node_modules/.bin/webpackgit

  • npx

在 npmV5版本 会赠送一个npxes6

npx 会自动查找当前依赖包中的可执行文件,若是找不到,就会去 PATH 里找。若是依然找不到,就会帮你安装github

因此也能够经过npx执行webpackweb

npx webpack
复制代码

require方法

实现一个require方法shell

common.js的规范中 引入一个模块须要

let getA = require('./a')
复制代码

本身写一个require方法

let fs = require('fs')
// 查找module
function myReq (myModule) {
  // 读取文件信息
  let cont = fs.readFileSync(myModule, 'utf-8')
  /* function (exports, require, module, __filename, __dirname) { moduel.exports = {a: 'apple'} return moduel.exports } */
  let nodeFn = new Function('exports', 'require', 'module', '__filename', '__dirname', cont + 'return module.exports')
  let module = {
    exports: {}
  }
  return nodeFn(module.exports, myReq, module, __filename, __dirname)
}
// let getA = require('./a')
let getA = myReq('./a.js')
console.log(getA, 'getA')
复制代码

思路:读取文件内容,根据node的封装规范,传入几个必须的参数便可。

  • 删减 webpack 编译后的文件

把刚刚打包以后的 dist/index.js 删减掉一些不用的代码

(function(modules) {
	function myRequire(moduleId) {
		var module = {
			exports: {}
		};
    modules[moduleId].call(module.exports, module, module.exports, myRequire);
        // call 用于让 modules[moduleId] 函数执行 执行的是传入后面的参数
		return module.exports;
	}
  return myRequire(/* 下面的第一个函数参数 */);
})
([
  (function(module, exports) {
    console.log('123')
  })
]);

复制代码

在线查看

能够看出来, webpack打包生成以后的文件内容就和编译的require方法相似。这就是为何打包以后的js文件可直接在浏览器中运行的缘由

编译流程

常见名词解释

参数 说明
entry 项目入口
module 开发中每个文件均可以看作module
chunk 代码块
loader 模块转化器
plugin 扩展插件 自定义webpack打包过程
bundle 最终打包完成的文件

打包流程

webpack的运行流程是一个串行的过程,从启动到结束,会依次执行如下流程

  • 参数初始化

从配置文件 【webpack.config.js】和 shell 语句中读取与合并参数

  • 开始编译

初始化一个compiler对象 加载全部插件 执行对象的run方法开始编译

  • 肯定入口文件

根据配置文件找到项目全部的入口文件

  • 编译模块

从入口开始 调用配置的loader对模块进行编译 【有一个递归寻找依赖模块的流程】

模块编译完成后 获得模块被转化后的最后内容以及他们之间的依赖关系

  • 资源输出

根据入口文件和模块之间的依赖关系 组成chunk文件 【一个chunk可能包含多个模块】每个chunk将会被转化成一个单独的文件加入输出列表中

  • 输出

根据配置的输出参数 【路径和文件名】将输出内容写入文件系统

** 在以上的过程 WP会在特定的时间点广播特定的事件 插件在监听到感兴趣的事件后会执行特定的逻辑 **

简化流程

其实以上流程能够简化为三个阶段

webpack

源码分析

核心库 tapable

在node中有一个事件发射器 EventEmitter ,能够进行事件监听与发射。

var EventEmitter = require('events').EventEmitter;
var event = new EventEmitter();
event.on('some_event', function () {
    console.log('some_event 事件触发');
});
setTimeout(function () {
    event.emit('some_event');
}, 1000);
复制代码

webpack核心库 tapable 的原理和 EventEmitter 相似,经过事件的注册和监听,触发各个编译周期中的函数方法. Tapable 还容许你经过回调函数的参数,访问事件的“触发者(emittee)”或“提供者(producer)”

查看在线代码

核心对象 compiler

compiler 继承自 tapable 能够进行事件的广播和监听

compiler 进行事件的广播和监听的方式为

// 广播事件 params 为附带参数
compiler.apply('event-name', params)

// 监听 名为 event-name 的事件
compiler.plugin('event-name', function (params) {

})

复制代码

查看177行代码

webpack 在初始化的时候 会将 compiler对象传入到plugin中 可使用它来访问 webpack 的主环境

查看45行代码

compiler 对象表明了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性创建,并配置好全部可操做的设置,包括 options,loader 和 plugin。

核心对象 compilation

compilation 继承自 tapable 能够进行事件的广播和监听

查看57行代码

compilation 对象表明了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会建立一个新的 compilation,从而生成一组新的编译资源。

一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息

plugin 实现机制

做用原理

在webpack的编译流程,每个阶段都会广播不一样的事件,好比 run, done 等事件。plugin会监听到这些事件,一旦事件发生,就会执行注册好的函数方法

plugin分析

每个plugin都是 一个具备 apply 属性的 JavaScript 对象

class MPlugin {
  // 这里获取用户为插件传入的配置参数
  constructor (options) {

  }
  // webpack 会调用 MPlugin 实例的apply方法 为插件实例传入 compiler 对象
  apply (compiler) {
    compiler.plugin('compilation', function (compilation) {
      // 回调函数中 传入了 compilation 对象

    })
  }
}

复制代码

在webpack初始化的阶段 会往plugin中传递compiler对象

查看45行代码

编写plugin

class StartWp {
    constructor(options) {
        this.options = options
    }
    apply(compiler) {
        let {name} = this.options
        // 监听事件 这是异步的 因此要执行cb 否则会卡到这里不动了
        compiler.plugin('run', function (compilation, cb) {
            console.log('run', name)
            // 每一次从新编译的时候又会触发
            // compilation.plugin('')
            cb();
        })
        compiler.plugin('done', function (compilation) {
            console.log('done', name)
        })
    }
}
module.exports = StartWp
复制代码
  • 传递给插件的compiler和compilation是相同的 也就是某一个插件有修改对象的话会影响后面的插件的使用

  • 有的事件是异步的,因此在使用的时候,要执行 cb() 去通知webpack 本次事件监听结束了 要往下继续执行不然会卡到这里

如何使用此插件

plugins: [
  new StartWp({
    name: 'v3 - plugin '
  })
]
复制代码

本身来写一个简易版本的webpack打包器

实现原理: 根据打包的模板格式 读取文件信息并输入到指定的位置

  • 借助ejs

  • 将简化的webpack打包结果拿出来做为 字符串模板

最简易的webpack

const fs = require('fs')

// 入口文件
let input = './index.js'
// 输出地址
let output = './dist/index.js'

const ejs = require('ejs')

const getIntry = fs.readFileSync(input, 'utf-8')

let template = `(function(modules) { function __webpack_require__(moduleId) { var module = { exports: {} }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); return module.exports; } return __webpack_require__(0); }) ([ (function(module, exports) { <%- getIntry %> }) ])`

let result = ejs.render(template, {
  getIntry
})

// 将结果输出到 dist 
fs.writeFileSync(output, result)
复制代码

在命令行执行一次 node webpack.0.1.0.js

执行以后的编译结果

能够看到在dist目录有index.js生成 将其引入 html页面

myWebpack

这样就完成了一个很是很是简单的webpack

在线查看简单的webpack

加入 require 处理

若是入口文件中 有使用到 require 则须要将其替换为webpack提供的 webpack_require

先看一下若是有使用 require 以后的打包以后的结果 [简化版本]

bundle.js

(function(modules) {
 	function __webpack_require__(moduleId) {
 		var module = {
 			exports: {}
 		};
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
 		return module.exports;
 	}
 	return __webpack_require__(0);
 })
 ([
  (function(module, exports, __webpack_require__) {
  __webpack_require__(1)
  console.log('index.js')
  }),
  (function(module, exports) {
    console.log(123)
  })
]);
复制代码

查看bundle.js在线代码

咱们使用这个模板来从新编写一个简易的webpack

const fs = require('fs')
const path = require('path')

// 入口文件
let input = './index.js'
// 输出地址
let output = './dist/index.js'

const ejs = require('ejs')

const getIntry = fs.readFileSync(input, 'utf-8')

// 将getIntry 中的 require 进行处理
const contAry = []
let dealIntry = getIntry.replace(/(require)\(['"](.+?)['"]\)/g, ($1, $2, $3, $4) => {
	let cont = fs.readFileSync($3, 'utf-8')
	contAry.push(cont)
	return $2 = `__webpack_require__(${contAry.length})`
})

let template = `(function(modules) { function __webpack_require__(moduleId) { var module = { exports: {} }; modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); return module.exports; } return __webpack_require__(0); }) ([ (function(module, exports, __webpack_require__) { <%- dealIntry %> }), <% for(var i=0;i < contAry.length; i++){ %> (function(module, exports) { <%- contAry[i] %> }), <%}%> ])`

let result = ejs.render(template, {
  dealIntry,
  contAry
})

// 将结果输出到 dist
fs.writeFileSync(output, result)

复制代码

在命令行执行一次 node webpack.1.0.0.js

执行以后的编译结果

myWebpack

在线查看简单的webpack

源码篇提问

  • 在本身构建的plugin中 是否能够进行事件广播

能够。只要能拿到 compiler或者compilation对象 就能够广播事件,为其余插件监听使用

参考文章

相关文章
相关标签/搜索