本系列将会从原理、开发、优化、对比四个方面给大家介绍webpack的工作流程。【默认是以webpack v3为例子】
储备知识
CommonJS 规范
// 模块引入
let moduleA = require('./a.js')
// 模块导出
module.exports = () => {}
复制代码
es6规范
// 模块引入
import {moduleA} from './a.js'
// 模块导出
export default () => {}
复制代码
黑盒体验
我们可以把webpack看做一个黑盒,只要会用就可以。先来体验一次很简单的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 就可看到一次打包过程
关于如何启动webpack
如果是全局安装了webpack,可以在命令行直接输入 webpack
如果只是项目文件夹安装,需要输入 node_modules/.bin/webpack
- npx
在 npmV5版本 会赠送一个npx
npx 会自动查找当前依赖包中的可执行文件,如果找不到,就会去 PATH 里找。如果依然找不到,就会帮你安装
所以也可以通过npx执行webpack
npx webpack
复制代码
require方法
实现一个require方法
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会在特定的时间点广播特定的事件 插件在监听到感兴趣的事件后会执行特定的逻辑 **
简化流程
其实以上流程可以简化为三个阶段
源码分析
核心库 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) {
})
复制代码
webpack 在初始化的时候 会将 compiler对象传入到plugin中 可以使用它来访问 webpack 的主环境
compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。
核心对象 compilation
compilation 继承自 tapable 可以进行事件的广播和监听
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对象
编写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页面
这样就完成了一个非常非常简单的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)
})
]);
复制代码
我们使用这个模板来重新编写一个简易的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
源码篇提问
- 在自己构建的plugin中 是否可以进行事件广播
可以。只要能拿到 compiler或者compilation对象 就可以广播事件,为其他插件监听使用
参考文章
- 《深入浅出webpack》 此书作者掘金地址
- 《细说 webpack 之流程篇》