Webpack-Flow

一 编译流程

  1. 初始化参数:从配置文件和Shell语句中读取并合并参数,得出最终的配置对象
  2. 用上一步得到的参数初始化Compiler对象
  3. 加载所有配置的插件
  4. 执行对象的run方法开始执行编译
  5. 根据配置中的entry找出入口文件
  6. 从入口文件出发,调用所有配置的Loader对模块进行编译
  7. 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
  8. 根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
  9. 再把每个Chunk转换成一个单独的文件加入到输出列表
  10. 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

flow

TIP

在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果

二 加载插件

flow

1 运行入口

// debug.js

//1.引入核心模块
const webpack = require('./webpack');
//2.加载配置文件
const options = require('./webpack.config');
// 3. 执行webpack得到编译对象Compiler,就是一个大管理,是核心编译对象
const compiler = webpack(options);
// 4. 调用它的run方法开始启动编译
compiler.run();

2 配置文件

// webpack.config.js

const path = require('path');
const Run1Plugin = require('./plugins/run1-plugin')
const DonePlugin = require('./plugins/done-plugin')

module.exports = {
    mode:'development',
	entry: './src/index.js',
    output:{
        path:path.resolve(__dirname,'dist'),
        filename: 'main.js' //'[name].js'
    },
    resolve:{
        extensions:['.js','.jsx','.json']
    },
    module:{
        rules:[
        ]
    },
    plugins:[
        new Run1Plugin(),
        new DonePlugin(),
    ]
}

3 webpack函数

// webpack/index.js

let Compiler = require('./Compiler');

function webpack(options) {
	// 1. 初始化参数:从配置文件和Shell语句中读取并合并参数,得出最终的配置对象
	// 1.1 获取shell命令中的参数
	let shellConfig = process.argv.slice(2).reduce((shellConfig, item) => {
		let [key, value] = item.split('=');
		shellConfig[key.slice(2)] = value;
		return shellConfig
	}, {});
	// 1.2 合并参数,得到最终配置对象
	let finalOptions = {...options, ...shellConfig};

	// 2. 用上一步得到的参数初始化Compiler对象
	let compiler = new Compiler(finalOptions);

	// 3. 加载所有配置的插件
	if (finalOptions.plugins && Array.isArray(finalOptions.plugins)) {
		// 有插件,就依次挂载所有的插件
		for (const plugin of finalOptions.plugins) {
			// 插件在webpack开始编译之前就全部挂载,也就是调用钩子的tap方法全部进行注册
			// 但是插件需要等到插件关注的钩子触发的时候才会执行
			plugin.apply(compiler);
		}
	}
	return compiler;
}
module.exports = webpack;

4 compiler类

// webpack/Compiler.js

let {SyncHook} = require('tapable');

// 编译类
class Compiler {
	constructor(options) {
		this.options = options;
		// 预设钩子集合
		this.hooks = {
			run: new SyncHook(), // 会在开始编译的时候触发
			done: new SyncHook() // 会在完成编译的时候触发
		}
	}

	// 4.执行对象的run方法开始执行编译
	run() {
		// 当调用run方法的时候会触发run这个钩子, 进而执行它的回调函调
		this.hooks.run.call();
		// 编译逻辑。。。
		this.hooks.done.call();
	}
}

module.exports = Compiler;

5 两个插件编写

// 1. plugins/run1-plugin.js

class RunPlugin {
	constructor(options) {
		this.options = options;
	}
	// 每个插件定死了有一个apply方法
	// 每个插件的核心方法,用于webpack函数初始化挂载插件钩子
	apply(compiler) {
		 // 监听感兴趣的钩子
		compiler.hooks.run.tap('RunPlugin', () => {
			console.log('RUN~~~~~~~~~~~~~~~~~~~');
		})
	}
}
module.exports = RunPlugin;
// 2. plugins/done-plugin.js

class DonePlugin {
	constructor(options) {
		this.options = options;
	}
	// 每个插件定死了有一个apply方法
	apply(compiler) {
		// 监听感兴趣的钩子
		compiler.hooks.done.tap('DonePlugin', () => {
			console.log('DONE~~~~~~~~~~~~~~~~~~~');
		})
	}
}
module.exports = DonePlugin;

6 执行命令输出结果

node debug.js

RUN~~~~~~~~~~~~~~~~~~~
DONE~~~~~~~~~~~~~~~~~~~

三 加载loader

1 编写四个loader

  • loaders\logger1-loader.js
function loader(source){
    console.log('logger1-loader');
    return source+'//1';
}
module.exports = loader;
  • loaders\logger2-loader.js
function loader(source){
    console.log('logger2-loader');
    return source+'//2';
}
module.exports = loader;
  • loaders\logger3-loader.js
function loader(source){
    console.log('logger3-loader');
    return source+'//3';
}
module.exports = loader;
  • loaders\logger4-loader.js
function loader(source){
    console.log('logger4-loader');
    return source+'//4';
}
module.exports = loader;

2 编译loader

  • 核心思想:遍历入口文件,然后读取文件内容,根据后缀匹配文件名称,最后循环执行loader对源代码进行编译
// webpack\Compiler.js

let {SyncHook} = require('tapable');
const path = require('path');
const fs = require('fs');

// 编译类
class Compiler {
	constructor(options) {
		this.options = options;
		// 预设钩子集合
		this.hooks = {
			run: new SyncHook(), // 会在开始编译的时候触发
			done: new SyncHook() // 会在完成编译的时候触发
		}
	}

	// 4.执行对象的run方法开始执行编译
	run() {
		// 当调用run方法的时候会触发run这个钩子, 进而执行它的回调函调
		this.hooks.run.call();
		// 5.根据配置中的entry找出入口文件,得到entry的绝对路径
		// let entry = path.join(this.options.context, this.options.entry);
		// console.log(entry);
		// D:\web_project\webpack_demo\learnwebpackdemo\5.flow\src\index.js
		
		// 找到入口文件,可能是字符,可能是对象》统一转化为对象
		let entry = {};
		if (typeof this.options.entry === 'string') {
			entry.main = this.options.entry;
		} else {
			entry = this.options.entry;
		}
		for (const entryName in entry) {
			// 统一不同操作系统的斜巷为 /
			let entryFilePath = toUnixPath(path.join(this.options.context, entry[entryName]));
			 // 6.从入口文件出发,调用所有配置的Loader对模块进行编译
			let entryModule = this.buildModule(entryFilePath);
		}
		this.hooks.done.call();
	}

	/**
	 * 编译模块
	 * 1. 读取模块内容
	 * @param {} modulePath 
	 */
	buildModule(modulePath) {
		// 读取原始源代码
		let originalSourceCode = fs.readFileSync(modulePath, 'utf-8');
		let targetSourceCode = originalSourceCode;

		// 查找此模块对应的loader对代码进行转换
		let loaders = [];
		let rules = this.options.module.rules;
		for (let i = 0; i < rules.length; i++) {
			// 正则匹配上了模块的路径
			if (rules[i].test.test(modulePath)) {
				loaders = [...loaders, ...rules[i].use];
			}
		}
		// loader需要倒着执行
		// loaders=['logger1-loader.js','logger2-loader.js','logger3-loader.js','logger4-loader.js']
		for (let i = loaders.length - 1; i >= 0; i--) {
			let loader = loaders[i];
			// 利用require加载loader方法,然后执行loader
			targetSourceCode = require(loader)(targetSourceCode);
		}
		console.log('originalSourceCode:', originalSourceCode);
		console.log('targetSourceCode:', targetSourceCode);
	}
}

// path.posix.sep => /
// path.sep不同操作系统的路径分隔符 windows: \ mac和unix: /
function toUnixPath(filePath){
	return filePath.replace(/\\/g, '/');
}

module.exports = Compiler;
  • 打印结果
// node debug.js
RUN~~~~~~~~~~~~~~~~~~~

logger4-loader
logger3-loader
logger2-loader
logger1-loader

originalSourceCode:
let title = require('./title');
console.log(title);

targetSourceCode:
let title = require('./title');
console.log(title);//4//3//2//1

DONE~~~~~~~~~~~~~~~~~~~

四 依赖分析

  1. 利用@babel/parser把源代码转化为AST
  2. 利用@babel/traverse遍历AST,找到依赖模块节点进行替换
  3. 利用@babel/generator把AST重新转化为源代码
  4. 在线转AST-astexploreropen in new window
npm i babel-types @babel/generator @babel/parser @babel/traverse -D
const types = require('babel-types'); // 创建节点的库
const parser = require('@babel/parser'); // 源代码转成AST抽象语法树
const traverse = require('@babel/traverse').default; // 遍历语法树
const generator = require('@babel/generator').default; // 把语法树重新生成代码

/**
* 编译模块
* 1. 读取模块内容
* @param {} modulePath 
*/
buildModule = (name, modulePath) => {
	// 读取原始源代码
	let originalSourceCode = fs.readFileSync(modulePath, 'utf-8');
	let targetSourceCode = originalSourceCode;

	// 查找此模块对应的loader对代码进行转换
	let loaders = [];
	let rules = this.options.module.rules;
	for (let i = 0; i < rules.length; i++) {
		// 正则匹配上了模块的路径
		if (rules[i].test.test(modulePath)) {
			loaders = [...loaders, ...rules[i].use];
		}
	}
	// loader需要倒着执行
	// loaders=['logger1-loader.js','logger2-loader.js','logger3-loader.js','logger4-loader.js']
	for (let i = loaders.length - 1; i >= 0; i--) {
		let loader = loaders[i];
		// 利用require加载loader方法,然后执行loader
		targetSourceCode = require(loader)(targetSourceCode);
	}
	// console.log('originalSourceCode:', originalSourceCode);
	// console.log('targetSourceCode:', targetSourceCode);

	// 7. 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理

	// 当前目录
	let baseDir = toUnixPath(this.options.context);
	let module = {
		id: './' + path.posix.relative(baseDir, modulePath), // 模块ID
		dependencies: [], // 依赖数组
		name // 模块名称,为了方便chunk代码块做区分
	};
	// 原代码转化为ast,第一个参数源代码  第二个参数是表示一个模块
	let astTree = parser.parse(targetSourceCode, {sourceType: 'module'});
	// 遍历语法树,并找出require节点
	traverse(astTree, {
		CallExpression: ({node}) => {
			if (node.callee.name === 'require') {
				// 获取节点名称
				let moduleName = node.arguments[0].value;
				// 获取相对路径
				// D:\web_project\webpack_demo\learnwebpackdemo\5.flow\src
				let dirname = path.posix.dirname(modulePath);
				// 要判断一个moduleName绝对路径还是相对路径,相对路径才需要下面的处理
				let depModulePath;
				if (path.isAbsolute(moduleName)) {
					// 如果是绝对路径直接使用
					depModulePath = moduleName;
				}else{
					// 如果是相对路径需要转化为绝对路径
					// D:\web_project\webpack_demo\learnwebpackdemo\5.flow\src\title.js
					depModulePath = path.posix.join(dirname, moduleName);
				}
				// 获取扩展名,进行查找依赖模块文件
				let extensions = this.options.resolve.extensions;
				depModulePath = tryExtensions(depModulePath, extensions, moduleName, dirname);
				// 模块ID的问题 每个打包后的模块都会有一个moduleId,例如:"./src/title.js"
				let depModuleId = './' + path.posix.relative(baseDir, depModulePath);
				// 修改抽象语法树
				node.arguments = [types.stringLiteral(depModuleId)];
				module.dependencies.push(depModulePath);
			}
		}
	});

	// 根据新的语法树生成新代码
	let {code} = generator(astTree);
	module._source = code;// 转换后的代码 module moduleId dependencies _source

	//再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
	module.dependencies.forEach(dependency => {
		let dependencyModule = this.buildModule(name, dependency);
		this.modules.add(dependencyModule);
	});
	return module;
}

五 多入口编译

  • 配置多入口
// webpack.config.js

const path = require('path');
const Run1Plugin = require('./plugins/run1-plugin')
const DonePlugin = require('./plugins/done-plugin')
module.exports = {
    mode:'development',
    devtool:'inline-source-map',
	context:process.cwd(),//根目录 current working directory
    entry:{
        page1:'./src/page1.js',
        page2:'./src/page2.js'
    },
    output:{
        path:path.resolve(__dirname,'dist'),
        filename: '[name].js'
    },
    resolve:{
        extensions:['.js','.jsx','.json']
    },
    module:{
        rules:[
            {
                test:/\.js$/,
                use:[
                    path.resolve(__dirname,'loaders','logger1-loader.js'),
                    path.resolve(__dirname,'loaders','logger2-loader.js'),
                ]
			},
			{
                test:/\.js$/,
                use:[
                    path.resolve(__dirname,'loaders','logger3-loader.js'),
                    path.resolve(__dirname,'loaders','logger4-loader.js'),
                ]
            }
        ]
    },
    plugins:[
        new Run1Plugin(),
        new DonePlugin(),
    ]
}
  • 新建入口文件
// src\page1.js

let title = require('./title');
console.log(title);
// src\page2.js

let title = require('./title');
console.log(title);
  • 写入文件
// webpack\Compiler.js

let {SyncHook} = require('tapable');
const path = require('path');
const fs = require('fs');

const types = require('babel-types'); // 创建节点的库
const parser = require('@babel/parser'); // 源代码转成AST抽象语法树
const traverse = require('@babel/traverse').default; // 遍历语法树
const generator = require('@babel/generator').default; // 把语法树重新生成代码

// 编译类
class Compiler {
	constructor(options) {
		this.options = options;
		// 预设钩子集合
		this.hooks = {
			run: new SyncHook(), // 会在开始编译的时候触发
			done: new SyncHook() // 会在完成编译的时候触发
		}

		this.modules = new Set(); // 这里存放着所有的模块
        this.chunks = new Set(); // 存放所有的代码块
        this.assets = {}; // 输出列表 存放着将要产出的资源文件
		this.files = new Set(); // 表示本次编译的所有产出的文件名
		this.entries = new Set();// 这个数组存放着所有的入口
	}

	// 4.执行对象的run方法开始执行编译
	run() {
		// 当调用run方法的时候会触发run这个钩子, 进而执行它的回调函调
		this.hooks.run.call();
		// 5.根据配置中的entry找出入口文件,得到entry的绝对路径
		// let entry = path.join(this.options.context, this.options.entry);
		// console.log(entry);
		// D:\web_project\webpack_demo\learnwebpackdemo\5.flow\src\index.js
		
		// 找到入口文件,可能是字符,可能是对象》统一转化为对象
		let entry = {};
		if (typeof this.options.entry === 'string') {
			// 如果字符串,默认为main
			entry.main = this.options.entry;
		} else {
			entry = this.options.entry;
		}
		for (const entryName in entry) {
			// 统一不同操作系统的斜巷为 /
			let entryFilePath = toUnixPath(path.join(this.options.context, entry[entryName]));
			 // 6.从入口文件出发,调用所有配置的Loader对模块进行编译
			let entryModule = this.buildModule(entryName, entryFilePath);

			// 8.根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
			let chunk = {
				name: entryName,
				entryModule,
				modules: new Set([...this.modules].filter(module => module.name === entryName))
				// 过滤入口名称相同的代码块切分到一起
			};
			this.chunks.add(chunk);
			this.entries.add(chunk); // 也是入口代码块
		}

		// 9.再把每个Chunk转换成一个单独的文件加入到输出列表
       //一个 chunk会成为this.assets对象的一个key value
       //一个chunk对应this.assets的一个属性,而每个assets属性会对应一个文件file
      /*  this.chunks.forEach(chunk=>{
        //key文件名 值是打包后的内容
        let filename = this.options.output.filename.replace('[name]',chunk.name);   
        let targetPath = path.join(this.options.output.path,filename);//page1.js page2.js
        fs.writeFileSync(targetPath,getSource(chunk));
      }); */
		// console.log(this.chunks);

		this.chunks.forEach(chunk => {
			let filename = this.options.output.filename.replace('[name]', chunk.name);
			// key文件名 值是打包后的内容
			this.assets[filename] = getSource(chunk);
		});
		// this.hooks.emit.call();
		// 10.在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统
		this.files = Object.keys(this.assets);//['main.js']
		// 存放本次编译输出的目标文件路径
		for(let file in this.assets){
			let targetPath = path.join(this.options.output.path, file);//page1.js page2.js
			fs.writeFileSync(targetPath, this.assets[file]);
		}

		this.hooks.done.call();
	}

	/**
	 * 编译模块
	 * 1. 读取模块内容
	 * @param {} modulePath 
	 */
	buildModule = (name, modulePath) => {
		// 读取原始源代码
		let originalSourceCode = fs.readFileSync(modulePath, 'utf-8');
		let targetSourceCode = originalSourceCode;

		// 查找此模块对应的loader对代码进行转换
		let loaders = [];
		let rules = this.options.module.rules;
		for (let i = 0; i < rules.length; i++) {
			// 正则匹配上了模块的路径
			if (rules[i].test.test(modulePath)) {
				loaders = [...loaders, ...rules[i].use];
			}
		}
		// loader需要倒着执行
		// loaders=['logger1-loader.js','logger2-loader.js','logger3-loader.js','logger4-loader.js']
		for (let i = loaders.length - 1; i >= 0; i--) {
			let loader = loaders[i];
			// 利用require加载loader方法,然后执行loader
			targetSourceCode = require(loader)(targetSourceCode);
		}
		// console.log('originalSourceCode:', originalSourceCode);
		// console.log('targetSourceCode:', targetSourceCode);

		// 7. 再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理

		// 当前目录
		let baseDir = toUnixPath(this.options.context);
        let module = {
			id: './' + path.posix.relative(baseDir, modulePath), // 模块ID
			dependencies: [], // 依赖数组
			name // 模块名称,为了方便chunk代码块做区分
		};
		// 原代码转化为ast,第一个参数源代码  第二个参数是表示一个模块
		let astTree = parser.parse(targetSourceCode, {sourceType: 'module'});
		// 遍历语法树,并找出require节点
		traverse(astTree, {
			CallExpression: ({node}) => {
				if (node.callee.name === 'require') {
					// 获取节点名称
					let moduleName = node.arguments[0].value;
					// 获取相对路径
					// D:\web_project\webpack_demo\learnwebpackdemo\5.flow\src
					let dirname = path.posix.dirname(modulePath);
					// 要判断一个moduleName绝对路径还是相对路径,相对路径才需要下面的处理
					let depModulePath;
					if (path.isAbsolute(moduleName)) {
						// 如果是绝对路径直接使用
						depModulePath = moduleName;
					}else{
						// 如果是相对路径需要转化为绝对路径
						// D:\web_project\webpack_demo\learnwebpackdemo\5.flow\src\title.js
						depModulePath = path.posix.join(dirname, moduleName);
					}
					// 获取扩展名,进行查找依赖模块文件
					let extensions = this.options.resolve.extensions;
					depModulePath = tryExtensions(depModulePath, extensions, moduleName, dirname);
					// 模块ID的问题 每个打包后的模块都会有一个moduleId,例如:"./src/title.js"
                    let depModuleId = './' + path.posix.relative(baseDir, depModulePath);
                    // 修改抽象语法树
                    node.arguments = [types.stringLiteral(depModuleId)];
                    module.dependencies.push(depModulePath);
				}
			}
		});

		// 根据新的语法树生成新代码
		let {code} = generator(astTree);
		module._source = code;// 转换后的代码 module moduleId dependencies _source
		//再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理
		module.dependencies.forEach(dependency => {
			let dependencyModule = this.buildModule(name, dependency);
			this.modules.add(dependencyModule);
		});
		return module;
	}
}

// path.posix.sep => /
// path.sep不同操作系统的路径分隔符 windows: \ mac和unix: /
function toUnixPath(filePath){
	return filePath.replace(/\\/g, '/');
}

/**
 * 匹配后缀查找文件
 * @param {*} modulePath 依赖模块
 * @param {*} extensions 后缀数组
 * @param {*} originalModulePath 模块名称
 * @param {*} moduleContext 相对路径
 */
function tryExtensions(modulePath, extensions, originalModulePath, moduleContext){
	for (let i = 0; i < extensions.length; i++) {
		if (fs.existsSync(modulePath + extensions[i])){
			return modulePath+extensions[i];
		}
    }
    throw new Error(`Module not found: Error: Can't resolve '${originalModulePath}' in '${moduleContext}'`);
}

//let chunk = {name:'main',entryModule,modules:this.modules}; 
function getSource(chunk){
	return `
	(() => {
	 var modules = {
		${
		   [...chunk.modules].map(module=>`
		   "${module.id}": (module,exports,require) => {
			 ${module._source}
		   }`).join(',')
	   }
	 };
	 var cache = {};
	 function require(moduleId) {
	   if (cache[moduleId]) {
		 return cache[moduleId].exports;
	   }
	   var module = (cache[moduleId] = {
		 exports: {},
	   });
	   modules[moduleId](module, module.exports, require);
	   return module.exports;
	 }
	 (() => {
	  ${chunk.entryModule._source}
	 })();
   })();
	`;
}

module.exports = Compiler;