学习目标
- 了解webpack打包原理
- 了解webpack的loader原理
- 了解webpack的插件原理
- 了解ast抽象语法树的应用
- 了解tapable的原理
- 手写一个简单的webpack
项目准备工作
-
新建一个项目,起一个名字(这里是
hyh_webpack
) -
新建
bin
目录,新建hyh_webpack.js
文件,将打包工具主程序放入其中主程序的顶部应当有:
#!/usr/bin/env node
标识,指定程序执行环境为node -
在
package.json
中配置bin
脚本{ "bin": "./bin/itheima-pack.js" }
-
通过
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
在学习给自己写的itheima-pack工具添加loader功能之前,得先学习webpack中如何自定义loader,所以学习步骤分为两大步:
- 掌握自定义webpack的loader
- 学习给itheima-pack添加loader功能并写一个loader
webpack以及我们自己写的itheima-pack都只能处理JavaScript文件,如果需要处理其他文件,或者对JavaScript代码做一些操作,则需要用到loader。
loader是webpack中四大核心概念之一,主要功能是将一段匹配规则的代码进行加工处理,生成最终的代码后输出,是webpack打包环节中非常重要的一环。
loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。
之前都使用过别人写好的loader,步骤大致分为:
- 装包
- 在webpack.config.js中配置module节点下的rules即可,例如babel-loader(省略其他配置,只论loader)
- (可选步骤)可能还需要其他的配置,例如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就是一个函数,同样也可以自己来写
- 在项目根目录中新建一个目录存放自己写的loader:
[图片上传失败...(image-d60744-1648713811934)]
-
编写myloader.js,其实loader就是对外暴露一个函数
第一个参数就是loader要处理的代码
module.exports = function(source) { console.log(source) // 只是简单打印并返回结果,不作任何处理 return source }
-
同样在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' }
-
如果需要实现一个简单的loader,例如将js中所有的“今天”替换成“明天”
只需要修改myloader.js的内容如下即可
module.exports = function(source) { return source.replace(/今天/g, '明天') }
-
同时也可以配置多个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' }
-
myloader2.js
module.exports = function(source) { return source.replace(/爆炸/g, '小道') }
loader的分类
不同类型的loader加载时优先级不同,优先级顺序遵循:
前置 > 行内 > 普通 > 后置
pre: 前置loader
post: 后置loader
指定Rule.enforce的属性即可设置loader的种类,不设置默认为普通loader
在itheima-pack中添加loader的功能
通过配置loader和手写loader可以发现,其实webpack能支持loader,主要步骤如下:
- 读取webpack.config.js配置文件的module.rules配置项,进行倒序迭代(rules的每项匹配规则按倒序匹配)
- 根据正则匹配到对应的文件类型,同时再批量导入loader函数
- 倒序迭代调用所有loader函数(loader的加载顺序从右到左,也是倒叙)
- 最后返回处理后的代码
在实现itheima-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)
}
}
}
开发源码:
- bin目录下的
hyh_webpack.js
#!/usr/bin/env node
//如上,声明环境为node环境
//console.log('可以执行打包了');
const path = require('path')
// 1. 读取需要打包项目的配置文件
let config = require(path.resolve('webpack.config.js'))
console.log(config)
// 2. 通过面向对象的方式来进行项目推进
const Compiler = require('../lib/Compiler')
new Compiler(config).start()
- lib文件下的
Compiler.js
const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const ejs = require('ejs')
const { SyncHook } = require('tapable')
class Compiler {
constructor(config) {
this.config = config
this.entry = config.entry
// 获取执行itheima-pack指令的目录
this.root = process.cwd()
// 初始化一个空对象, 存放所有的模块
this.modules = {}
// 将module.rules挂载到自身
this.rules = config.module.rules
//先有hooks才能调用apply
this.hooks = {
//生命周期钩子的定义 -->第一步
compiler: new SyncHook(),
afterCompiler: new SyncHook(),
emit: new SyncHook(),
afterEmit: new SyncHook(),
done: new SyncHook()
}
//获取plugins数组中的所有插件对象,调用其apply方法
if (Array.isArray(this.config.plugins)) {
this.config.plugins.forEach(plugin => {
plugin.apply()
})
}
}
getSource(path) {
return fs.readFileSync(path, 'utf-8')
}
depAnalyse(modulePath) {
// console.log(modulePath)
// 读取模块内容
let source = this.getSource(modulePath)
// console.log(source)
// 读取loader
let readAndCallLoader = (use, obj) => {
let loaderPath = path.join(this.root, use)
let loader = require(loaderPath)
source = loader.call(obj, source)
}
// 读取rules规则, 倒序遍历
for (let i = this.rules.length - 1; i >= 0; i--) {
// console.log(this.rules[i])
let { test, use } = this.rules[i]
// 获取每一条规则,与当前modulePath进行匹配
// 匹配modulePath 是否符合规则,如果符合规则就要倒序遍历获取所有的loader
if (test.test(modulePath)) {
// 判断use是否为数组,如果是数组才需要倒序遍历
if (Array.isArray(use)) {
for (let j = use.length - 1; j >= 0; j--) {
// 每一个loader的路径
// console.log(path.join(this.root, use[j]))
// let loaderPath = path.join(this.root, use[j])
// let loader = require(loaderPath)
// source = loader(source)
readAndCallLoader(use[j])
}
} else if (typeof use === 'string') {
// use为字符串时,直接加载loader即可
// let loaderPath = path.join(this.root, use)
// let loader = require(loaderPath)
// source = loader(source)
readAndCallLoader(use)
} else if (use instanceof Object) {
// console.log(use.options)
// let loaderPath = path.join(this.root, use.loader)
// let loader = require(loaderPath)
// source = loader.call({ query: use.options }, source)
readAndCallLoader(use.loader, { query: use.options })
}
}
}
// 准备一个依赖数组,用于存储当前模块的所有依赖
let dependencies = []
let ast = parser.parse(source)
// console.log(ast.program.body)
traverse(ast, {
CallExpression(p) {
if (p.node.callee.name === 'require') {
// 修改require
p.node.callee.name = '__webpack_require__'
// 修改路径
let oldValue = p.node.arguments[0].value
oldValue = './' + path.join('src', oldValue)
// 避免Windows出现反斜杠 : \
p.node.arguments[0].value = oldValue.replace(/\\+/g, '/')
// 每找到一个require调用, 就将其中的路径修改完毕后加入到依赖数组中
dependencies.push(p.node.arguments[0].value)
}
}
})
let sourceCode = generator(ast).code
// console.log(sourceCode)
// 构建modules对象
// { './src/index.js': 'xxxx', './src/news.js': 'yyyy' }
// this.modules
let modulePathRelative = './' + path.relative(this.root, modulePath)
modulePathRelative = modulePathRelative.replace(/\\+/g, '/')
this.modules[modulePathRelative] = sourceCode
// 递归加载所有依赖
// ./src/news.js ./src/news2.js
dependencies.forEach(dep => this.depAnalyse(path.resolve(this.root, dep)))
}
emitFile() {
// 使用模板进行拼接字符串,生成最终的结果代码
let template = this.getSource(path.join(__dirname, '../template/output.ejs'))
let result = ejs.render(template, {
entry: this.entry,
modules: this.modules
})
// 获取输出目录
let outputPath = path.join(this.config.output.path, this.config.output.filename)
fs.writeFileSync(outputPath, result)
}
start() {
//开始编译啦
this.hooks.compiler.call()
// 开始打包了!
// 依赖的分析
// __dirname表示的是 itheima-pack 项目中Compiler.js所在目录
// 而非入口文件所在的目录
// 如果需要获取执行itheima-pack指令的目录, 需要使用 process.cwd()
this.depAnalyse(path.resolve(this.root, this.entry))
//编译完成啦
this.hooks.afterCompiler.call()
//开始发射文件
this.hooks.emit.call()
this.emitFile()
//文件发射完了
this.hooks.afterEmit.call()
// console.log(this.modules)
}
}
module.exports = Compiler
- template目录下的output.ejs
(简单学习tapabel-生命周期管理库)
(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 = "<%-entry%>");
})
({
<% for (let k in modules) { %>
"<%-k%>":
(function (module, exports, __webpack_require__) {
eval(`<%-modules[k]%>`);
}),
<%}%>
});
- test目录下的
tapabel_helloworld.js
const {SyncHook} =require('tapable')
//学前端
//流程:1.开班 2.学html 3.学css 4.学js 5.学框架react
//安装tapable [npm i tapable]
//实现生命周期管理(具体需求看官方文档)
class Frontend{
constructor(){
//定义好钩子(生命周期)
this.hooks={
//如果需要在call时传参,则需要在new SyncHook时定义需要的参数
beforeStudy:new SyncHook(['name']),
afterHtml:new SyncHook(),
afterCss:new SyncHook(),
afterJs:new SyncHook(),
afterReact:SyncHook()
}
}
study(){
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('学框架react');
this.hooks.afterReact.call()
}
}
let f= new Frontend()
f.hooks.afterHtml.tap('afterHtml',(name)=>{
console.log('学完html后我想写更多页面');
})
//其它同-略
f.study()
网友评论