本章主要包含以下几部分内容:
大家在学习本章之前可以先学习Webpack 从入门到精通-基础篇,基础篇主要讲述如下内容:
Webpack 从入门到精通-进阶篇源码
本部分只讲述通过脚手架创建的项目的分析路线及步骤,具体每个文件夹里面讲述了什么内容分别在源码中进行注释讲解。
通过 npm run eject将配置文件暴露出来
这里只讲述通过脚手架创建的项目的分析路线及步骤,具体每个文件夹里面讲述了什么内容分别在源码中进行注释。
vue inspect --mode=development > webpack.dev.js
将vue开发环境配置打包一起放在webpack.dev.js文件下面,开发环境代码只需要研究webpack.dev.js文件即可vue inspect --mode=production > webpack.prod.js
将vue生产环境配置打包一起放在webpack.prod.js文件下面,生产环境代码只需要研究webpack.prod.js文件即可开发环境文件webpack.dev.js
生产环境文件webpack.prod.js(除了在css上面以及多线程打包上面进行了一些修改,其余和开发环境是一样的)
loader本质上是一个函数
// 方式一
module.exports = function (content, map, meta) {
console.log(111);
return content;
}
// 方式二
module.exports = function (content, map, meta) {
console.log(111);
this.callback(null, content, map, meta);
}
module.exports.pitch = function () {
console.log('pitch 111');
}
// 异步loader(推荐使用,loader在异步加载的过程中可以执行其余的步骤)
module.exports = function (content, map, meta) {
console.log(222);
const callback = this.async();
setTimeout(() => {
callback(null, content);
}, 1000)
}
module.exports.pitch = function () {
console.log('pitch 222');
}
cnpm install loader-utils
loader3.js中代码
// 1.1 获取options 引入
const {
getOptions
} = require('loader-utils');
// 2.1 获取validate(校验options是否合法)引入
const {
validate
} = require('schema-utils');
// 2.3创建schema.json文件校验规则并引入使用
const schema = require('./schema');
module.exports = function(content, map, meta) {
// 1.2 获取options 使用
const options = getOptions(this);
console.log(333, options);
// 2.2校验options是否合法 使用
validate(schema, options, {
name: 'loader3'
})
return content;
}
module.exports.pitch = function() {
console.log('pitch 333');
}
schema.json中代码
{
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "名称~"
}
},
"additionalProperties": false // 如果设置为true表示除了校验前面写的string类型还可以 接着 校验其余类型,如果为false表示校验了string类型之后不可以再校验其余类型
}
webpack.config.js中代码
const path = require('path');
module.exports = {
module: {
rules: [{
test: /\.js$/,
use: [
{
loader: 'loader3',
// options部分
options: {
name: 'jack',
age: 18
}
}
]
}]
},
// 配置loader解析规则:我们的loader去哪个文件夹下面寻找(这里表示的是同级目录的loaders文件夹下面寻找)
resolveLoader: {
modules: [
'node_modules',
path.resolve(__dirname, 'loaders')
]
}
}
{
"type": "object",
"properties": {
"presets": {
"type": "array"
}
},
"addtionalProperties": true
}
const { getOptions } = require('loader-utils');
const { validate } = require('schema-utils');
const babel = require('@babel/core');
const util = require('util');
const babelSchema = require('./babelSchema.json');
// babel.transform用来编译代码的方法
// 是一个普通异步方法
// util.promisify将普通异步方法转化成基于promise的异步方法
const transform = util.promisify(babel.transform);
module.exports = function (content, map, meta) {
// 获取loader的options配置
const options = getOptions(this) || {};
// 校验babel的options的配置
validate(babelSchema, options, {
name: 'Babel Loader'
});
// 创建异步
const callback = this.async();
// 使用babel编译代码
transform(content, options)
.then(({code, map}) => callback(null, code, map, meta))
.catch((e) => callback(e))
}
const path = require('path');
module.exports = {
module: {
rules: [{
test: /\.js$/,
loader: 'babelLoader',
options: {
presets: [
'@babel/preset-env'
]
}
}]
},
// 配置loader解析规则:我们的loader去哪个文件夹下面寻找(这里表示的是同级目录的loaders文件夹下面寻找)
resolveLoader: {
modules: [
'node_modules',
path.resolve(__dirname, 'loaders')
]
}
}
hooks
tapable
文件tapable.test.js
const { SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook } = require('tapable');
class Lesson {
constructor() {
// 初始化hooks容器
this.hooks = {
// 同步hooks,任务会依次执行
// go: new SyncHook(['address'])
// SyncBailHook:一旦有返回值就会退出~
go: new SyncBailHook(['address']),
// 异步hooks
// AsyncParallelHook:异步并行
// leave: new AsyncParallelHook(['name', 'age']),
// AsyncSeriesHook: 异步串行
leave: new AsyncSeriesHook(['name', 'age'])
}
}
tap() {
// 往hooks容器中注册事件/添加回调函数
this.hooks.go.tap('class0318', (address) => {
console.log('class0318', address);
return 111;
})
this.hooks.go.tap('class0410', (address) => {
console.log('class0410', address);
})
// tapAsync常用,有回调函数
this.hooks.leave.tapAsync('class0510', (name, age, cb) => {
setTimeout(() => {
console.log('class0510', name, age);
cb();
}, 2000)
})
// 需要返回promise
this.hooks.leave.tapPromise('class0610', (name, age) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('class0610', name, age);
resolve();
}, 1000)
})
})
}
start() {
// 触发hooks
this.hooks.go.call('c318');
this.hooks.leave.callAsync('jack', 18, function () {
// 代表所有leave容器中的函数触发完了,才触发
console.log('end~~~');
});
}
}
const l = new Lesson();
l.tap();
l.start();
class Plugin1 {
apply(complier) {
complier.hooks.emit.tap('Plugin1', (compilation) => {
console.log('emit.tap 111');
})
complier.hooks.emit.tapAsync('Plugin1', (compilation, cb) => {
setTimeout(() => {
console.log('emit.tapAsync 111');
cb();
}, 1000)
})
complier.hooks.emit.tapPromise('Plugin1', (compilation) => {
return new Promise((resolve) => {
setTimeout(() => {
console.log('emit.tapPromise 111');
resolve();
}, 1000)
})
})
complier.hooks.afterEmit.tap('Plugin1', (compilation) => {
console.log('afterEmit.tap 111');
})
complier.hooks.done.tap('Plugin1', (stats) => {
console.log('done.tap 111');
})
}
}
module.exports = Plugin1;
"scripts": {
"start": "node --inspect-brk ./node_modules/webpack/bin/webpack.js"
}
const fs = require('fs');
const util = require('util');
const path = require('path');
const webpack = require('webpack');
const { RawSource } = webpack.sources;
// 将fs.readFile方法变成基于promise风格的异步方法
const readFile = util.promisify(fs.readFile);
/*
1. 初始化compilation钩子
2. 往要输出资源中,添加一个a.txt文件
3. 读取b.txt中的内容,将b.txt中的内容添加到输出资源中的b.txt文件中
3.1 读取b.txt中的内容需要使用node的readFile模块
3.2 将b.txt中的内容添加到输出资源中的b.txt文件中除了使用 2 中的方法外,还有两种形式可以使用
3.2.1 借助RawSource
3.2.2 借助RawSource和emitAsset
*/
class Plugin2 {
apply(compiler) {
// 1.初始化compilation钩子
compiler.hooks.thisCompilation.tap('Plugin2', (compilation) => {
// debugger
// console.log(compilation);
// 添加资源
compilation.hooks.additionalAssets.tapAsync('Plugin2', async (cb) => {
// debugger
// console.log(compilation);
const content = 'hello plugin2';
// 2.往要输出资源中,添加一个a.txt
compilation.assets['a.txt'] = {
// 文件大小
size() {
return content.length;
},
// 文件内容
source() {
return content;
}
}
const data = await readFile(path.resolve(__dirname, 'b.txt'));
// 3.2.1 compilation.assets['b.txt'] = new RawSource(data);
// 3.2.1
compilation.emitAsset('b.txt', new RawSource(data));
cb();
})
})
}
}
module.exports = Plugin2;
CopyWebpackPlugin的功能:将public文件夹中的文件复制到dist文件夹下面(忽略index.html文件)
{
"type": "object",
"properties": {
"from": {
"type": "string"
},
"to": {
"type": "string"
},
"ignore": {
"type": "array"
}
},
"additionalProperties": false
}
const path = require('path');
const fs = require('fs');
const {promisify} = require('util')
const { validate } = require('schema-utils');
const globby = require('globby');// globby用来匹配文件目标
const webpack = require('webpack');
const schema = require('./schema.json');
const { Compilation } = require('webpack');
const readFile = promisify(fs.readFile);
const {RawSource} = webpack.sources
class CopyWebpackPlugin {
constructor(options = {}) {
// 验证options是否符合规范
validate(schema, options, {
name: 'CopyWebpackPlugin'
})
this.options = options;
}
apply(compiler) {
// 初始化compilation
compiler.hooks.thisCompilation.tap('CopyWebpackPlugin', (compilation) => {
// 添加资源的hooks
compilation.hooks.additionalAssets.tapAsync('CopyWebpackPlugin', async (cb) => {
// 将from中的资源复制到to中,输出出去
const { from, ignore } = this.options;
const to = this.options.to ? this.options.to : '.';
// context就是webpack配置
// 运行指令的目录
const context = compiler.options.context; // process.cwd()
// 将输入路径变成绝对路径
const absoluteFrom = path.isAbsolute(from) ? from : path.resolve(context, from);
// 1. 过滤掉ignore的文件
// globby(要处理的文件夹,options)
const paths = await globby(absoluteFrom, { ignore });
console.log(paths); // 所有要加载的文件路径数组
// 2. 读取paths中所有资源
const files = await Promise.all(
paths.map(async (absolutePath) => {
// 读取文件
const data = await readFile(absolutePath);
// basename得到最后的文件名称
const relativePath = path.basename(absolutePath);
// 和to属性结合
// 没有to --> reset.css
// 有to --> css/reset.css(对应webpack.config.js中CopyWebpackPlugin插件的to的名称css)
const filename = path.join(to, relativePath);
return {
// 文件数据
data,
// 文件名称
filename
}
})
)
// 3. 生成webpack格式的资源
const assets = files.map((file) => {
const source = new RawSource(file.data);
return {
source,
filename: file.filename
}
})
// 4. 添加compilation中,输出出去
assets.forEach((asset) => {
compilation.emitAsset(asset.filename, asset.source);
})
cb();
})
})
}
}
module.exports = CopyWebpackPlugin;
npm init -y
拉取出package.json文件,修改文件中scripts部分为"build": "node ./script/build.js"
表示通过在终端输入命令npm run build
时会运行/script/build.js文件,在scripts中添加"debug": "node --inspect-brk ./script/build.js"
表示通过在终端输入命令npm run debug
时会调试/script/build.js文件中的代码,调试代码的步骤第四章已经介绍npm install @babel/parser -D
用来将代码解析成ast抽象语法树npm install @babel/traverse -D
用来遍历ast抽象语法树代码npm install @babel/core-D
用来将代码中浏览器不能识别的语法进行编译index.js
const fs = require('fs');
const path = require('path');
// babel的库
const babelParser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const { transformFromAst } = require('@babel/core');
function myWebpack(config) {
return new Compiler(config);
}
class Compiler {
constructor(options = {}) {
this.options = options;
}
// 启动webpack打包
run() {
// 1. 读取入口文件内容
// 入口文件路径
const filePath = this.options.entry;
const file = fs.readFileSync(filePath, 'utf-8');
// 2. 将其解析成ast抽象语法树
const ast = babelParser.parse(file, {
sourceType: 'module' // 解析文件的模块化方案是 ES Module
})
// debugger;
console.log(ast);
// 获取到文件文件夹路径
const dirname = path.dirname(filePath);
// 定义存储依赖的容器
const deps = {}
// 3. 收集依赖
traverse(ast, {
// 内部会遍历ast中program.body,判断里面语句类型
// 如果 type:ImportDeclaration 就会触发当前函数
ImportDeclaration({node}) {
// 文件相对路径:'./add.js'
const relativePath = node.source.value;
// 生成基于入口文件的绝对路径
const absolutePath = path.resolve(dirname, relativePath);
// 添加依赖
deps[relativePath] = absolutePath;
}
})
console.log(deps);
// 4. 编译代码:将代码中浏览器不能识别的语法进行编译
const { code } = transformFromAst(ast, null, {
presets: ['@babel/preset-env']
})
console.log(code);
}
}
module.exports = myWebpack;
我们开发代码过程中讲究的是模块化开发,不同功能的代码放在不同的文件中
创建myWebpack2–>parser.js(放入解析代码)/Compiler.js(放入编译代码)/index.js(主文件)
所有代码位于myWebpack文件夹中
Compiler.js文件中build函数用于构建代码,run函数中modules通过递归遍历收集所有的依赖,depsGraph用于将依赖整理更好依赖关系图(具体的代码功能都在代码中进行了注释)
代码位于myWebpack–>Compiler.js中的bundle部分
整个myWebpack–>Compiler.js代码
const path = require('path');
const fs = require('fs');
const {
getAst,
getDeps,
getCode
} = require('./parser')
class Compiler {
constructor(options = {}) {
// webpack配置对象
this.options = options;
// 所有依赖的容器
this.modules = [];
}
// 启动webpack打包
run() {
// 入口文件路径
const filePath = this.options.entry;
// 第一次构建,得到入口文件的信息
const fileInfo = this.build(filePath);
this.modules.push(fileInfo);
// 遍历所有的依赖
this.modules.forEach((fileInfo) => {
/**
{
'./add.js': '/Users/xiongjian/Desktop/atguigu/code/05.myWebpack/src/add.js',
'./count.js': '/Users/xiongjian/Desktop/atguigu/code/05.myWebpack/src/count.js'
}
*/
// 取出当前文件的所有依赖
const deps = fileInfo.deps;
// 遍历
for (const relativePath in deps) {
// 依赖文件的绝对路径
const absolutePath = deps[relativePath];
// 对依赖文件进行处理
const fileInfo = this.build(absolutePath);
// 将处理后的结果添加modules中,后面遍历就会遍历它了~(递归遍历)
this.modules.push(fileInfo);
}
})
console.log(this.modules);
// 将依赖整理更好依赖关系图
/*
{
'index.js': {
code: 'xxx',
deps: { 'add.js': "xxx" }
},
'add.js': {
code: 'xxx',
deps: {}
}
}
*/
const depsGraph = this.modules.reduce((graph, module) => {
return {
...graph,
[module.filePath]: {
code: module.code,
deps: module.deps
}
}
}, {})
console.log(depsGraph);
this.generate(depsGraph)
}
// 开始构建
build(filePath) {
// 1. 将文件解析成ast
const ast = getAst(filePath);
// 2. 获取ast中所有的依赖
const deps = getDeps(ast, filePath);
// 3. 将ast解析成code
const code = getCode(ast);
return {
// 文件路径
filePath,
// 当前文件的所有依赖
deps,
// 当前文件解析后的代码
code
}
}
// 生成输出资源
generate(depsGraph) {
/* index.js的代码
"use strict";\n' +
'\n' +
'var _add = _interopRequireDefault(require("./add.js"));\n' +
'\n' +
'var _count = _interopRequireDefault(require("./count.js"));\n' +
'\n' +
'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
'\n' +
'console.log((0, _add["default"])(1, 2));\n' +
'console.log((0, _count["default"])(3, 1));
*/
const bundle = `
(function (depsGraph) {
// require目的:为了加载入口文件
function require(module) {
// 定义模块内部的require函数
function localRequire(relativePath) {
// 为了找到要引入模块的绝对路径,通过require加载
return require(depsGraph[module].deps[relativePath]);
}
// 定义暴露对象(将来我们模块要暴露的内容)
var exports = {};
(function (require, exports, code) {
eval(code);
})(localRequire, exports, depsGraph[module].code);
// 作为require函数的返回值返回出去
// 后面的require函数能得到暴露的内容
return exports;
}
// 加载入口文件
require('${this.options.entry}');
})(${JSON.stringify(depsGraph)})
`
// 生成输出文件的绝对路径
const filePath = path.resolve(this.options.output.path, this.options.output.filename)
// 写入文件
fs.writeFileSync(filePath, bundle, 'utf-8');
}
}
module.exports = Compiler;
参考资料:https://www.bilibili.com/video/BV1cv411C74F
感谢尚硅谷的视频讲解!!!