webpack早就已经在前端领域大放异彩,会使用和优化webpack也已经是中、高级工程师必备技能,在此基础之上再对webpack的原理进行理解和掌握,必定会在未来的开发中事半功倍。若是对于webpack不熟悉可以查看之前的文章进行学习和了解。

由于本人能力一般、水平有限,所以会在本篇文章编写过程中对一些内容进行又臭又长的赘述,就是为了能让一些基础比较薄弱的同学阅读起来可以更加省心点,接下来即将开始正题了,希望此文章能对你有些许帮助。

构建项目

  1. 新建一个文件夹 webpack-theory

    是之后插件的名字,可以理解为webpack的别名,可以直接 wepack-theory进行使用。

  2. 新建 bin 目录,在此目录下创建webpack-theory.js文件, 将打包工具主程序放入其中

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

    #!/usr/bin/env node
    // log的内容修改直接,可以直接生效
    console.log('当通过npm link链接之后,通过webpack-theory指令可以直接打出');
  3. 在package.json中配置 bin 脚本,与scripts平级

    {
    "bin": "./bin/webpack-theory.js"
    }
  4. 通过 npm link 将本地的项目webpack-theory 链接到全局包中,链接之后便可以直接在本地使用,供本地测试使用,具体参考 npm link

    1. 成功之后,可以 cd /usr/local/lib/node_modules 查看所有安装的包

进入目录后,可以看到webpack-theory,webpack-theory就是npm link时,在全局的node_modules中生成一个符号链接,指向模块(webpack-theory)的本地目录,当本地的文件(bin/webpack-theory)修改时会自动链接到全局,因为全局的node_modules只是本地的引用

  1. 在本地执行 webpack-theory, 会直接将 bin/webpack-theory.js 的console.log内容输出
>>> ~ » webpack-theory
>>> 当通过npm link链接之后,通过webpack-theory指令可以直接打出

分析bundle

在深入接触webpack 原理之前,需要知道其打包生成的文件结果是什么样,通过打包生成的文件可以从整体了解webpack在对文件处理过程中做了哪些事情,通过结果反推其原理。

  • 自行创建一个简单的weback项目,创建三个js文件,分别是index.js,parent.js 和 child.js,并将其通过webpack进行打包

    • index.js 内容
    const parent = require('./parent.js')
    
    console.log(parent)
    • parent.js 内容
    const child = require('./child.js')
    
    module.exports = {
    msg: '我是parent的信息',
    child: child.msg
    }
    • child.js 内容
    module.exports = {
    msg: '我是child的信息'
    }
  • 通过 npx webpack 进行打包,将打包文件进行简单的删除和整理之后

(function (modules) { // 将所有的模块组成一个modules对象传递进来, 键就是模块的路径,值就是模块内部的代码
// 模块缓存对象, 已经解析过的路径都会放进来,可以判断当前需要解析的模块是否已经解析过
var installedModules = {}; // 定义一个 webpack 自己的的 require polyfill
function __webpack_require__(moduleId) { // 检测 moduleId 是否已经存在缓存中了,若是已经存在则不需要在进行依赖解析
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
} // 创建一个新的 module, 并将其push至缓存中,方便在后续递归遍历解析依赖时,检测是否已经解析过
var module = installedModules[moduleId] = {
i: moduleId, // moduleId 是自执行函数的参数 modules 对象的键,根本是模块的路径
exports: {}
}; // 执行 modules[moduleId] 函数
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // 将 exports 返回
return module.exports;
} // 将 webpack.config.js 配置中的 entry 作为 moduleId 进行传递
return __webpack_require__("./src/index.js");
})
/*** 将项目中的几个模块作为自执行函数的参数传递 ***/
({
// webpack.config.js 配置中 entry 的值,会将其作为递归解析依赖的入口
"./src/index.js": (function (module, exports, __webpack_require__) {
eval("const parent = __webpack_require__(/*! ./parent.js */ \"./src/parent.js\")\n\nconsole.log(parent)\n\n//# sourceURL=webpack:///./src/index.js?");
}),
"./src/parent.js": (function (module, exports, __webpack_require__) {
eval("const child = __webpack_require__(/*! ./child.js */ \"./src/child.js\")\n\nmodule.exports = {\n msg: '我是parent的信息',\n child: child.msg\n}\n\n\n\n//# sourceURL=webpack:///./src/parent.js?");
}),
"./src/child.js": (function (module, exports) {
eval("\nmodule.exports = {\n msg: '我是child的信息'\n}\n\n//# sourceURL=webpack:///./src/child.js?");
})
});

根据生成的bundle.js可以梳理webpack的整体打包思路,就是利用一个自执行函数创建一个闭包,在这个独立的作用域中,将模块的路径作为modules的键、模块的内容放在一个函数中作为值作为自执行函数的形参传递进来,通过自定义的函数 __webpack_require__进行递归解析。

简单分析一下bundle的整体执行过程

  1. 第一步: 自执行函数第一次执行时,会直接运行内部的__webpack_require__函数,并将入口文件的路径./src/index.js作为形参moduleId传递
  2. 第二步: 在函数__webpack_require__执行过程中
    1. 会首先判断当前moduleId是否已经存在缓存installedModules中,若是存在则直接返回,不需要再继续解析其依赖。若是不存在,则会构造一个对象并将其同时存到installedModules中和module中。第一次执行时installedModules为空对象,moduleId为./src/index.js
    2. 执行modules[moduleId]函数,即执行modules['./src/index.js'],会通过call改变其作用域并传递module, module.exports, __webpack_require__三个形参,执行的内容就是入口文件模块./src/index.js中的js代码。
      1. call传递的作用域置为module.exports,由于module.exports此时为空对象,则index.js中的作用域就是指向它,这也是典型的使用闭包来解决作用域的问题。
      2. module, module.exports的作用就是用于模块内抛出对象使用的,作用是一个的,可以参考require.js进行这块的理解
      3. __webpack_require__的作用就很巧妙了,此时入口index.js中使用的require('./parent.js')已经被替换成__webpack_require__("./src/parent.js\"),执行modules[moduleId]函数时便会在此调用__webpack_require__函数进行递归调用,会再次回到第二步,直到child.js执行完毕,整个bundle才算执行结束。

分析完bundle之后,会发现对于webpack的打包结果,除了形参modules会跟着代码的业务逻辑修改而变化之外,自执行函数中的代码始终是固定不变的,因此想要编写一个属于自己的webpack时,重点关注和需要解决的就是modules这个对象是如何生成的。

创建bundle

分析完webpack打包完成之后的bundle文件,以结果为导向反推实现过程便会简单许多,若是让我们自己动手实现一个简单版的webpack,便会有了些思路。

首先需要一个简单的wbepack配置

const path = require('path')
module.exports = {
entry: './src/index.js',
output:{
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
}
}

简单版本的webpack实现思路

  1. 获取webpack配置文件
  2. 封装一个用于解析配置并将其简单打包的方法
    1. 利用抽象语法书解析模块内容
    2. 递归解析模块依赖
    3. 使用模版引擎输出结果

有了思路,接下来就是按部就班的实现

  • 获取webpack配置文件,而需要做的事情就是将这个配置文件进行解析,根据配置文件进行打包生成bundle。首先就是读取需要打包项目的配置文件
const config = require(path.resolve('webpack.config.js'))
  • 获取配置文件之后,便是如何解析并实现webpack的功能,这些功能全部封装在Compiler类中,用于解析配置文件的配置,并通过start进行启动解析
const Compiler = require('../lib/Compiler')
new Compiler(config).start()
  • 重点就是如何实现这个方法,定义一个Compiler类,提供一个start方法开始webpack打包,通过depAnalyse便可以获取到入口文件index的内容
const path = require('path')
const fs = require('fs') class Compiler {
constructor(config){
this.config = config
const { entry } = config // 配置文件
this.entry = entry // 入口文件
this.root = process.cwd() // 输入 webpack-theory 的路径
this.modules = {} // 初始化一个控对象,存放所有的模块
} /**
* 开始打包
* 打包最主要的就是依赖的分析
*/
start(){
this.depAnalyse(path.resolve(this.root, this.entry))
} /**
* 依赖分析
* 需要根据入口entry进行开始分析
*/
depAnalyse(modulePath){
// 获取 index.js 的内容
let source = this.getSource(modulePath)
} // 读取文件
getSource(path){
return fs.readFileSync(path, 'utf-8')
}
} module.exports = Compiler
  • 获取到index.js的文件内容之后,并不能直接使用,需要通过将其解析成抽象语法树进行处理,需要使用一个插件@babel/parser将模块代码解析成AST,然后插件@babel/traverse配合着使用,将AST的节点进行替换,替换完成之后,使用插件@babel/generator将AST转换成模块的原有代码,改变的只是将require变成__webpack_require__,需要注意的是需要将路径处理一下,因为此时的路径是相对于src下面的。处理完index之后需要递归调用处理全部的模块,并声称bundle中自执行函数的参数modules

此时index的模块代码经过处理之后,变成了需要的代码

const parent = __webpack_require__("./src/parent.js");
console.log(parent);

在函数depAnalyse中添加如下处理

// 获取 index.js 的内容
let source = this.getSource(modulePath)
// ------- // 准备一个依赖数组,用于存储当前模块
let dependenceArr = []
// 将js代码 解析成AST
let ast = parser.parse(source) // 将AST中的 require 替换为 __webpack_require__
traverse(ast, {
// p 是抽象语法树的节点
CallExpression(p) {
if (p.node.callee.name === 'require') {
// 将代码中的 require 替换为 __webpack_require__
p.node.callee.name = '__webpack_require__'
const oldValue = p.node.arguments[0].value
// 修改路径,避免windows出现反斜杠 \
p.node.arguments[0].value = ('./' + path.join('src', oldValue)).replace(/\\+/g, '/') // 每找到一个require调用,就将其中的路径修改完毕后加入到依赖数组中
dependenceArr.push(p.node.arguments[0].value)
}
}
}) // 构建modules对象
const sourceCode = generator(ast).code
const modulePathRelative = './' + (path.relative(this.root, modulePath)).replace(/\\+/g, '/')
this.modules[modulePathRelative] = sourceCode // 递归调用加载所有依赖
dependenceArr.forEach(dep => this.depAnalyse(path.resolve(this.root, dep)))

至此已经完成了modules的处理,接下来需要处理的就是怎么生成bundle.js,分析bundle时已经指出我们需要关注的地方只有modules的拼接,至于自执行函数中的内容都是基本都是固定的,不需要额外的处理

  • 如何使用模版引擎打包模块的代码呢?
  1. 使用模版引擎ejs创建模版,模版的内容就是webpack打包生成的内容,只需要根据Compilermodules进行遍历即可,还需要关注的是return __webpack_require__(__webpack_require__.s = "<%-entry%>"),这里传入的是配置文件的入口,也是自执行函数第一次执行时的参数

    1. 创建ejs的模板文件output.ejs,需要关注的只有两个地方,其它地方使用默认的代码
    // 第一次执行的参数就是配置的entry
    return __webpack_require__(__webpack_require__.s = "<%-entry%>"); // 拼接函数需要的形参 modules
    {
    <% for (let k in modules) {%>
    "<%-k%>": (function (module, exports, __webpack_require__) { eval(`<%-modules[k]%>`);
    }),
    <%}%>
    }
  2. Compiler增加一个emitFile方法,用于根据模板生成打包的bundle文件,在start函数中的depAnalyse之后进行调用

    /**
    * 根据写好的模板 创建文件
    */
    emitFile(){
    // 已经创建好的 ejs 模版
    const template = this.getSource(path.join(__dirname, '../template/output.ejs'))
    // 使用 ejs 进行编译
    const result = ejs.render(template, {
    entry: this.entry,
    modules: this.modules
    }) // 获取输出路径和文件名
    const {
    path: filePath,
    filename
    } = this.output
    const outputPath = path.join(filePath, filename) // 打包生成bundle 并放在指定的目录下
    fs.writeFile(outputPath, result, (err) => {
    console.log(err ? err : '打包生成bundle完成');
    })
    }

到目前为止,已经可以进行简单的模块打包,可以将index.js、parent.js和child.js进行简单的打包,这里仅仅是支持最简单的webpack用法打包

loader

loader是webpack的重要核心功能之一,也是使用频率非常高的,主要功能就是将代码按照预期的结果进行加工处理,生成最终的代码后输出,因此掌握loade的基本机制是很有必要的。loader的使用也是非常简单的,其基本配置和用法这里不再赘述,接下来一起看看如何在自己的webpack-theory中添加解析loader和如何编写一个自己的loader。

自制loader

在为webpack-theory添加处理loader的能力之前,先看看如何在webpack中实现一个自己的loader

  • webpack中loader,主要步骤如下

    • 读取webpack.config.js配置文件的module.rules配置项,进行倒序迭代(rules的每项匹配规则按倒序匹配)
    • 根据正则匹配到对应的文件类型,同时再批量导入loader函数
    • 默认是倒序迭代调用所有的loader函数(loader从右到左,从下到上),也可以自己来控制这个顺序
    • 最后返回处理后的代码
  • 当想要在webpack中增加处理cass文件能力的时候,会进行loader的配置

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

sass-loader其实就是一个函数,根据test的匹配规则,将以.scss结束的文件内容读取出来,然后将匹配到的文件内容作为参数传递给一个函数,这个函数将sass文件的内容按照规则进行加工处理成浏览器能够识别的css并输出,所以loader的本质就是一个函数,接受一个参数,这个参数就是匹配到的文件里面的代码。同理,css-loaderstyle-loader也是这样的处理流程,只是内部做的事情不同。

function handlerScss(sourceCode){
// 这里就是将scss文件的内容,按照规则进行加工、处理,结果就是浏览器能够识别解析的css,并将其返回
return newSourceCode
}
  • 接下来实现一个自己的简单loader,将之前的parent.jschild.js中的信息使用loader处理为msg
// 将js文件中的 信息 换成 msg
module.exports = function (source) {
return source.replace(/信息/g, 'msg')
}

在webpack中配置loader

{
test:/\.js/,
use:['./loader/handlerLoader1.js']
}

使用npx webpack打包之后,可以看到打包的代码中已经将原有代码中的信息更换为msg

  • 若是想讲handlerLoader1的loader中替换的内容通过配置自定义处理呢?就像是url-loader那样传递一个配置选项options,然后在loader中进行接受并处理。可以通过loader-utilsgetOptions提取loader中的options进行处理,老版本是通过thus.query来进行处理

修改loader文件handlerLoader1

const loaderUtils = require('loader-utils')
// 将js文件中的 信息 换成 通过options传递的name
module.exports = function (source) {
const optionsName = loaderUtils.getOptions(this).name || ''
return source.replace(/信息/g, optionsName)
}

修改webpack的loader

{
test:/\.js/,
use:{
loader: './loader/loader1.js',
options:{
name:'新的信息'
}
}
}

使用npx webpack打包之后,便可以通过options配置进行替换

  • 若是handlerLoader1处理完的东西还需要交给下一个loader进行处理之后,这样就会牵扯到多个同级loader的情况,将handlerLoader1拷贝两份,分别命名为handlerLoader11handlerLoader12,内容可保持原有的,只是在原有的函数中分别打印其对应的loader的文件名称,因为只是为了看看loader的加载。

handlerLoader1的内容为

// 将js文件中的 信息 换成 msg
module.exports = function (source) {
console.log('我是 handlerLoader1'); // 其余两loader 的log分别为 handlerLoader2 handlerLoader3
return source.replace(/信息/g, 'msg')
}

webpack配置loader

{
test:/\.js/,
use:[
'./loader/handlerLoader1.js',
'./loader/handlerLoader2.js',
'./loader/handlerLoader3.js'
]
}

执行webpack打包,输出结果,可以得出loader的默认顺序是由右到左

>>> 我是 handlerLoader3
>>> 我是 handlerLoader2
>>> 我是 handlerLoader1
  • 若修改webpack的loader为
{
test:/\.js/,
use:['./loader/loader1.js']
},{
test:/\.js/,
use:['./loader/loader2.js']
},{
test:/\.js/,
use:['./loader/loader3.js']
},

执行webpack打包,输出结果,可以得出loader的默认顺序是由下到上的

>>> 我是 handlerLoader3
>>> 我是 handlerLoader2
>>> 我是 handlerLoader1

添加loader功能

通过自制一个loader之后,可以总结得到webpack支持loader的功能,主要就是4步

  1. 读取配置文件webpack.config.jsmodule.rulesloader配置项,进行倒序迭代
  2. 根据正则匹配到对应的文件类型,同时再批量导入loader函数
  3. 倒序迭代调用所有loader函数
  4. 返回处理后的代码

webpack-theory中增加处理loader的能力,无非就是在加载每个模块的时候,根据配置的rules的正则进行匹配需要的资源,满足条件之后就会加载并使用对应的loader进行处理并迭代调用

需要注意的是,在什么时候去执行loader呢,在每次获取模块依赖的时候,都需要进行loadertest匹配,若是匹配到就加载对应的loader进行处理。例如本文的案例代码存在三个js文件,首先会加载index.js,在加载解析index的依赖之前就需要对其进行倒序便利全部的loader,若是匹配到对应的loader就会加载对应的loaderindex.js的内容进行处理,因为index引入了parent.js,接下来便会在递归调用depAnalyse方法解析parnet之前进行判断和处理,child.js同理。

depAnalyse方法中每次解析以来之前添加如下代码:

// 内部定义一个处理loader的函数
const _handleLoader = (usePath, _this) => {
const loaderPath = path.join(this.root, usePath)
const loader = require(loaderPath)
source = loader.call(_this, source)
} // 读取 rules 规则, 进行倒序遍历
const rules = this.rules
for (let i = rules.length - 1; i >= 0; i--) {
const {
test,
use
} = rules[i] // 匹配 modulePath 是否符合规则,若是符合规则就需要倒序遍历获取所有的loader
// 获取每一条规则,和当前的 modulePath 进行匹配
if (test.test(modulePath)) {
// use 可能是 数组、对象、字符串
console.log(use); if (Array.isArray(use)) {
// array
for (let j = use.length - 1; j >= 0; j--) {
// const loaderPath = path.join(this.root, use[j])
// const loader = require(loaderPath)
// source = loader(source)
_handleLoader(use[j])
}
} else if (typeof use === 'string') {
// string
_handleLoader(use)
} else if (use instanceof Object) {
// object
_handleLoader(use.loader, {
query: use.options
})
}
}
}

loader基础的相关编写到此为止,但是还是需要多加练习的思考,这里仅仅是演示了最简单的,大家可以参考官方文档进行loader的enforce异步loader等知识点的深入学习和查看babelsass-loader等社区优秀loader进行深入的理解和练习。

plugin

插件是 webpack 生态系统的重要组成部分,为社区用户提供了一种强大方式来直接触及 webpack 的编译过程(compilation process)。插件能够 钩入(hook) 到在每个编译(compilation)中触发的所有关键事件。在编译的每一步,插件都具备完全访问 compiler 对象的能力,如果情况合适,还可以访问当前 compilation 对象。

自定义插件本质就是在webpack的编译过程的提供的生命周期钩子中,进行编码开发实现一些功能,在适当的时间节点做该做的事情,例如clean-webpack-plugin插件,就是在编译之前执行插件,将打包目录清空。

自制plugin

  • 在实现自制插件之前,先了解一下webpack插件组成

    • 一个JavaScript命名函数

    • 在插件函数的prototype上定义一个apply方法

    • 指定一个绑定到webpack自身的事件钩子

    • 处理webpack内部实例的特定数据

    • 功能完成后调用webpack提供的回调

  • webpack的生命周期钩子 生命周期钩子

Compiler 模块是 webpack 的支柱引擎,它通过 CLINode API 传递的所有选项,创建出一个 compilation 实例。它扩展(extend)自 Tapable 类,以便注册和调用插件。大多数面向用户的插件首,会先在 Compiler上注册。

hello word

根据官方文档实现一个hello word插件,可以简单的了解到plugin

// 1. 一个JavaScript命名函数
// 2. 在插件函数的 prototype 上定义一个apply方法
class HelloWordPlugin {
// 3. apply 中有一个 compiler 形参
apply(compiler){
console.log('插件执行了');
// 4. 通过compiler对象可以注册对应的事件,全部的钩子都可以使用
// 注册一个编译完成的钩子, 一般需要将插件名作为事件名即可
compiler.hooks.done.tap('HelloWordPlugin', (stats) => {
console.log('整个webpack打包结束了');
}) compiler.hooks.emit.tap('HelloWordPlugin', (compilation) => {
console.log('触发emit方法');
})
}
} module.exports = HelloWordPlugin

webpack.config.js引入并使用

const HelloWordPlugin = require('./plugins/HelloWordPlugin')

{
// ...
plugins:[
new HelloWordPlugin()
]
}

npx webpack打包,可以查看插件的触发

>>> 插件执行了
>>> 触发emit方法
>>> 整个webpack打包结束了

HtmlWebpackPlugin

模仿实现HtmlWebpackPlugin插件的功能

html-webpack-plugin 可以将制定的html模板复制一份输出到dist目录下,自动引入bundle.js

  • 实现步骤

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

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

添加plugin功能

webpack-theory添加plugin功能,只需在Compiler构造时,创建对应的钩子即可,webpack-theory只是负责定义钩子,并在适当的时间节点去触发,至于钩子的事件注册都是各个plugin自己内部去实现。

// tapable 的构造函数内部定义的钩子
this.hooks = {
afterPlugins: new SyncHook(),
beforeRun: new SyncHook(),
run: new SyncHook(),
make: new SyncHook(),
afterCompiler: new SyncHook(),
shouldEmit: new SyncHook(),
emit: new SyncHook(),
afterEmit: new SyncHook(['compilation']),
done: new SyncHook(),
} // 触发plugins中所有插件的apply方法, 并传入Compiler对象
if(Array.isArray(this.config.plugins)){
this.config.plugins.forEach(plugin => {
plugin.apply(this)
});
}

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

start中调用定义的钩子

start() {
this.hooks.compiler.call() // 开始编译
this.depAnalyse(path.resolve(this.root, this.entry))
this.hooks.afterCompiler.call() //编译完成了
this.hooks.emit.call() // 开始发射文件
this.emitFile()
this.hooks.done.call() // 完成
}

补充

AST

就是将一行代码解析成对象的格式,可以使用在线工具生成ast语法树 astexplorer 进行查看

  1. 安装@babel/parser插件
npm i -S @babel/parser
  1. 使用
const parser = require('@babel/parser')
//source是需要生成ast语法树的代码片段
const ast = parser.parse(source)
  1. 生成效果

源码

const news = require('./news')
console.log(news.content)

生成的ast语法树

Node {
type: 'File',
start: 0,
end: 57,
loc:
SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 3, column: 25 } },
program:
Node {
type: 'Program',
start: 0,
end: 57,
loc: SourceLocation { start: [Position], end: [Position] },
sourceType: 'script',
interpreter: null,
body: [ [Node], [Node] ],
directives: [] },
comments: [] }

tabable

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

  • 基本用法

    • 定义钩子
    • 使用者注册事件
    • 在合适的阶段调用钩子,触发事件
    const { SyncHook } = require('tapable')
    /**
    * 学习前端
    * 学习过程 1.准备 2.学html 3.学css 4.学js 5.学框架
    * 学习的每个过程就类似于生命周期
    */
    class Frontend{ constructor(){
    // 1. 定义生命周期钩子
    this.hooks = {
    beforeStudy: new SyncHook(),
    afterHtml: new SyncHook(),
    afterCss: new SyncHook(),
    afterJs: new SyncHook(),
    // 需要传递的参数 需要在 new SyncHook() 的时候定义好
    afterFrame: new SyncHook(['name']),
    }
    } study(){
    // 3. 在合适的时候 调用
    console.log('准备');
    this.hooks.beforeStudy.call()
    console.log('准备学html');
    this.hooks.afterHtml.call()
    console.log('准备学css');
    this.hooks.afterCss.call()
    console.log('准备学js');
    this.hooks.afterJs.call()
    console.log('准备学框架');
    this.hooks.afterFrame.call('vue、react')
    }
    } const f = new Frontend() // 2. 注册事件
    f.hooks.afterHtml.tap('afterHtml', () => {
    console.log('学完html,完成了一部分前端知识');
    }) f.hooks.afterJs.tap('afterJs', () => {
    console.log('学完js,完成了一部分前端知识');
    }) f.hooks.afterFrame.tap('afterFrame', (name) => {
    console.log(`学完了${name}框架,天下无敌....`);
    }) f.study()

webpack原理的更多相关文章

  1. webpack原理与实战

    webpack是一个js打包工具,不一个完整的前端构建工具.它的流行得益于模块化和单页应用的流行.webpack提供扩展机制,在庞大的社区支持下各种场景基本它都可找到解决方案.本文的目的是教会你用we ...

  2. webpack原理探究 && 打包优化

    在做vue项目和react项目时,都用到了webpack.webpack帮助我们很好地提高了工作效率,但是一直以来没有对其原理进行探究,略有遗憾. 因为使用一个工具,能够深入了解其原理才能更好地使用. ...

  3. webpack原理类型问题

    1.webpack底层原理 (实现一个webpack) 步骤:1.拿到入口文件的代码并读出来转化为js对象(抽象语法术parser)2.拿到所有模块的依赖 ‘./message.js’,放进数组中 引 ...

  4. Webpack 原理浅析

    作者: 凹凸曼 - 风魔小次郎 背景 Webpack 迭代到4.x版本后,其源码已经十分庞大,对各种开发场景进行了高度抽象,阅读成本也愈发昂贵.但是为了了解其内部的工作原理,让我们尝试从一个最简单的 ...

  5. 手写webpack核心原理,再也不怕面试官问我webpack原理

    手写webpack核心原理 目录 手写webpack核心原理 一.核心打包原理 1.1 打包的主要流程如下 1.2 具体细节 二.基本准备工作 三.获取模块内容 四.分析模块 五.收集依赖 六.ES6 ...

  6. webpack开发与生产环境配置

    前言 作者去年就开始使用webpack, 最早的接触就来自于vue-cli.那个时候工作重点主要也是 vue 的使用,对webpack的配置是知之甚少,期间有问题也是询问大牛 @吕大豹.顺便说一句,对 ...

  7. 关于webpack官网的学习

    webpack,从名词上,"web pack",大概可以看出是一个网页打包工具,其实它具有打包.压缩.解析编译的功能. 使用(配置webpack.config.js) entry: ...

  8. webpack学习笔记--配置总结

    从前面的配置看来选项很多,Webpack 内置了很多功能. 你不必都记住它们,只需要大概明白 Webpack 原理和核心概念去判断选项大致属于哪个大模块下,再去查详细的使用文档. 通常你可用如下经验去 ...

  9. 手把手教你撸一个简易的 webpack

    背景 随着前端复杂度的不断提升,诞生出很多打包工具,比如最先的grunt,gulp.到后来的webpack和Parcel.但是目前很多脚手架工具,比如vue-cli已经帮我们集成了一些构建工具的使用. ...

随机推荐

  1. 【HDU5409】CRB and Graph 边双联通 子树最值

    HDU # 题意 有一个简单图,n个点,m条边.对于每条割边,求出删去这条边后,在两个联通块中各取一个u,v.使得u<v,并且u尽量大而v尽量小. # 思路 求出边双联通是肯定的. 答案的限制条 ...

  2. 主席树区间第K大

    主席树的实质其实还是一颗线段树, 然后每一次修改都通过上一次的线段树,来添加新边,使得每次改变就改变logn个节点,很多节点重复利用,达到节省空间的目的. 1.不带修改的区间第K大. HDU-2665 ...

  3. Harbinger vs Sciencepal

    Harbinger vs Sciencepal 题意:给你n对人, 每一对都有2个人,每个人分别有一个值, 现在将每队人拆开塞入2组,要求分完这n对人之后,2个组的差值最小. 题解:将每队人的差值算出 ...

  4. CF - 1110F Nearest Leaf

    题目传送门 题解: 先用题目给定的dfs方式得到dfs序,记录下出入的dfs序. 很明显可以得知的是,以u为根的子树的dfs序在 in[u] - out[u] 的范围之内. 将每个询问先全部存到对应的 ...

  5. 【Redis】缓存穿透与缓存雪崩

    一.缓存雪崩 1.1 缓存雪崩产生的原因 1.2 解决方案 1.3 锁的方式 1.4 消息中间件 1.5 一级和二级缓存 1.6 均摊分配redis key 失效时间 二.缓存穿透 一.缓存雪崩 1. ...

  6. FastJson格式化Request对象导致的一次异常思考

    一.问题描述: 近期,在环境中出现一个阻塞性的异常“nested exception is java.lang.IllegalStateException: It is illegal to call ...

  7. Python 为了提升性能,竟运用了共享经济

    大家或许知道,Python 为了提高内存的利用效率,采用了一套共用对象内存的分配策略. 例如,对于那些数值较小的数字对象([-5, 256]).布尔值对象.None 对象.较短的字符串对象(通常 是 ...

  8. java多线程之创建线程的4种方式及Future

    Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的实例.Java可以用四种方式来创建线程: 继承Thread创建线程 实现Runnable接口创建线程 实现callab ...

  9. Python(Head First)学习笔记:四

    4 持久存储:文件存储.读写 数据保存到文件:在学习的过程中出现了一个问题,老是报一个错:SyntaxError: invalid syntax: 这个是语法错误,后来搜了下才知道是python2.7 ...

  10. Android开发教程:开发框架基本原理

    1.提供应用程序框架(Framework) 开发者可以遵照这些框架搭建应用程序读者可以结合J2SE平台的Applet框架或J2ME平台的移动信息设备套件框架来理解Android平台的应用程序框架. 每 ...