webpack是一个款非常流行的构建打包工具,它具有两个核心的功能loader、plugin。
webpack的loader有什么作用?
webpack默认只能理解JavaScript和JSON对于其他的文件类型,比如.vue、.ts、图片、.css等文件是不能直接处理的。需通过特定的loader转换后才能处理。
webpack的plugin有什么用?
主要目的就是解决 loader 无法实现的事情,loader 只是用作于将特定的模块进行转换,而 pulgin 可以用于执行更加广泛的任务,比如打包优化、资源管理、环境变量注入等。
webpack执行流程
模拟webpack执行流程如下
文件目录
// add.js
export default (a, b) => {
return a + b
}
// minus.js
export default (a, b) => {
return a - b
}
// index.js
import add from "./add.js"
import minus from './minus.js'
const sum = add(1, 2)
const division = minus(2, 1)
console.log(sum)
console.log(division)
//webpack.js
const fs = require('fs')
const path = require('path')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const babel = require('@babel/core')
const {
SyncHook,
AsyncSeriesWaterfallHook,
} = require("tapable");
let depsGraph = {}, depsG = {}
// 构建依赖图
const getModuleInfo = (file) => {
const body = fs.readFileSync(file, 'utf-8')
const ast = parser.parse(body, {
sourceType: 'module' //表示我们要解析的是ES模块
});
const deps = {}
// 查找某一模块的依赖
traverse(ast, {
ImportDeclaration ({
node
}) {
const dirname = path.dirname(file)
const abspath = "./" + path.join(dirname,node.source.value)
deps[node.source.value] = abspath
}
})
// ast转es5
const {
code,
map
} = babel.transformFromAst(ast, null, {
presets: ["@babel/preset-env"],
sourceMaps:true
})
depsGraph[file] = {
deps,
code
}
depsG[file] = {
deps,
// map
}
for (const key in deps) {
if (Object.hasOwnProperty.call(deps, key)) {
const el = deps[key];
getModuleInfo(el)
}
}
}
// 新增代码
const bundle = (entry, compiler) => {
getModuleInfo(entry)
fs.writeFileSync('./code.js',JSON.stringify(depsGraph,null,2))
compiler.hooks.deps.call(depsG)
return `(function (graph) {
function require(file) {
function absRequire(relPath) {
return require(graph[file].deps[relPath])
}
var exports = {}
;(function (require,exports,code) {
eval(code)
})(absRequire,exports,graph[file].code)
return exports
}
require('${entry}')
})(${JSON.stringify(depsGraph)})`
}
// 模拟webpack的Compiler
class Compiler {
constructor(context, options = {}) {
this.hooks = {
start: new SyncHook([]),
deps: new SyncHook(['deps']),
emit: new AsyncSeriesWaterfallHook(['compilation']),
afterEmit: new AsyncSeriesWaterfallHook(['compilation']),
done: new SyncHook([])
}
}
}
const PluginName = 'HelloPlugin'
// 一个简单的webpack插件
class HelloPlugin {
constructor() {
}
apply (compiler) {
compiler.hooks.start.tap(PluginName, () => {
console.log('build start...')
})
compiler.hooks.deps.tap(PluginName, deps => {
console.log('deps:', deps)
})
compiler.hooks.emit.tapPromise(PluginName, (compilation) => {
return new Promise((resolve, reject) => {
// 在编译阶段可以对资源进行一下处理
const prefix = `
/*
* @LastEditTime: 2022-10-20 10:53:23
* @LastEditors: shangYin
*/
`
resolve(prefix + compilation)
// resolve(compilation)
})
})
compiler.hooks.afterEmit.tapAsync(PluginName, (compilation, cb) => {
// console.log(compilation)
cb()
})
compiler.hooks.done.tap(PluginName, () => {
console.log('build done...')
})
}
}
// 模拟webpack编译打包
const webpack = (options = {}) => {
const { entry, output, plugins = [] } = options
const compiler = new Compiler()
for (const plugin of plugins) {
plugin.apply(compiler)
}
runBuild(compiler, entry, output)
}
// 模拟编译打包的流程
const runBuild = (compiler, entry, output) => {
compiler.hooks.start.call()
compiler.hooks.emit.promise(bundle(entry, compiler)).then(compilation => {
fs.writeFileSync(output, compilation)
compiler.hooks.afterEmit.callAsync(compilation, () => {
compiler.hooks.done.call()
})
}).catch(e => e)
}
webpack({
entry: './src/index.js',
output: './bundle.js',
plugins: [new HelloPlugin()]
})
执行node webpack即可进行打包,更新关于webpack插件的知识见下面
Tapable
在 Webpack 的编译过程中,本质上通过 Tapable 实现了在编译过程中的一种发布订阅者模式的插件 Plugin 机制。
在 Tapable 中所有注册的事件可以分为同步、异步两种执行方式:
- 同步表示注册的事件函数会同步进行执行。
- 异步表示注册的事件函数会异步进行执行。
- 针对同步钩子来
tap方法是唯一的注册事件的方法,通过call方法触发同步钩子的执行。 - 异步钩子可以通过
tap、tapAsync、tapPromise三种方式来注册,同时可以通过对应的call、callAsync、promise三种方式来触发注册的函数。
plugin大致代码
class MyPlugin {
constructor(options){
this.options = options
}
apply(compiler){
// doSomething...
}
}
Compiler与Compilation
- Compiler 在 Webpack 启动打包时创建,保存着本次打包的所有初始化配置信息包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地把它理解为 Webpack 实例;
- Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做扩展。通过 Compilation 也能读取到 Compiler 对象。
compiler-hooks
compiler钩子,当webpack编译器处理到特定时间时就会触发处于这个时间段钩子函数。
- emit 【AsyncSeriesHook】输出 asset 到 output 目录之前执行。这个钩子 不会 被复制到子编译器。
- afterEmit 【AsyncSeriesHook】输出 asset 到 output 目录之后执行。这个钩子 不会 被复制到子编译器。
- entryOption 【SyncBailHook】 在 webpack 选项中的 entry 被处理过之后调用。
- done 【AsyncSeriesHook】 在 compilation 完成时执行。这个钩子 不会 被复制到子编译器。
更多hooks见:webpack.docschina.org/api/compile…
compilation-hooks
Compilation 模块会被 Compiler 用来创建新的 compilation 对象(或新的 build 对象)。 compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。 它会对应用程序的依赖图中所有模块, 进行字面上的编译(literal compilation)。 在编译阶段,模块会被加载(load)、封存(seal)、优化(optimize)、 分块(chunk)、哈希(hash)和重新创建(restore)。
- buildModule ****【SyncHook】在模块构建开始之前触发,可以用来修改模块。
- succeedModule【SyncHook】模块构建成功时执行。
更多hooks见:webpack.docschina.org/api/compila…
清除无用文件插件实现
作用:清除项目当中无用的文件,支持文件目录、文件后缀筛选扫描,支持对无用文化的删除/移动/恢复操作。
思路:
- 获取所有用到的资源,如何获取?
通过以上的知识我们已经知道,webpack在打包构建有一个compilation对象。这个对象包含了此次编译资源的相关信息。获取已使用资源的文件的路径我们就可以之后那些执行被使用了。
- 获取扫描目录下所有文件,然后与第一步中的资源信息做对比,筛选出无用的文件。
- 对筛选出的无用文件进行删除/移动/恢复操作。
插件发布
-
注册一个npm账号。(注意registry地址 npm config set registry registry.npmjs.org/)
-
执行
npm login进行登录。(OTP二次认证或Token方式验证) -
如插件需要打包压缩操作,进行相关操作。
-
填写关于插件的package.json信息,如description、keywords、version等等。
-
npm publish将你的插件发布到npm官网。