Webpack原理解析?

471 阅读5分钟

webpack是什么?

webpack是静态资源模块化打包工具,通过分析模块间依赖关系,打包出一个或者多个bundle文件

问题思考

  1. 什么是模块化,js中模块化包含哪些东西?
  2. webpack是如何分析模块间的依赖关系,最终打包出来?
  3. 打包中bundle和chunk的区别是什么?
  4. 打包过程中的loader和plugin分别有什么作用?

前置知识

  1. ES6,CommonJS,AMD,CMD等一一些模块化设计规范,有什么异同点
  2. AST 语法树

设计步骤

  1. 找到入口文件
  2. 解析入口文件,提取依赖
  3. 递归的创建一个文件间的依赖图,描述所有文件间的依赖关系
  4. 把所有的文件打包成一个文件

场景假设(手动实现一个webpack)

场景1

  • 入口文件index.js
  • 依赖文件name.js,person.js
  • 目录结构如下

image.png

场景关系依赖分析

index.js(入口) -> person.js(依赖) -> name.js(依赖)

按照设计步骤,手动实现myWebpack,文件名为myWebpack.js 和src同目录

  1. 步骤1:找到入口文件
const fs = require("fs");
// 封装读取文件内容方法
function readContent(filename) {
	const fileContent = fs.readFileSync(filename, "utf-8");
	return fileContent;
}
// 1. 找到入口文件
const content = readContent("./src/index.js");
  1. 步骤2:这里需要有一些ast语法树的知识,通俗一点讲就是源码的另一种表现形式,通过对象的形式去描述内容,能够清晰的表现文件内容,依赖关系 在线ast转换生成器

image.png 观察发现其实就是各种属性表示各种js代码(此过程有词法分析,语法分析啥的)

// 2. 解析入口文件内容,生成AST语法树(这里通过插件@babel/parser先转义成ast语法树)
const astTree = require("@babel/parser").parse(content, {
	// parse in strict mode and allow module declarations
	sourceType: "module",
	plugins: [
		// enable jsx and flow syntax
		"jsx",
		"flow",
	],
});
console.log("astTree", astTree);

image.png 3. 步骤3:深度遍历AST语法树,获取entry.js 依赖

// 3. babel-traverse 作用是像遍历对象一样 对 AST 进行遍历转译,得到新的 AST(通过astTree中的astTree->program Node -> body Node -> ImportDeclaration Node -> source -> value )
const dependencies = [];
const deepTraverseAstTree = require("babel-traverse").default(astTree, {
	// 需要遍历语法树中的属性
	ImportDeclaration: ({ node }) => {
            dependencies.push(node.source.value);
		console.log("node", node);
	},
});

image.png 4. 步骤4:封装获取所有文件依赖方法,解析入口文件提取依赖

let id = 0;
function createAsset(filename) {
	const content = readContent(filename);
	const astTree = babylon.parse(content, {
		sourceType: "module",
	});
	const dependencies = [];
	babelTraverse(astTree, {
		// 需要遍历语法树中的属性
		ImportDeclaration: ({ node }) => {
			dependencies.push(node.source.value);
		},
	});
	return {
		id: id++,
		filename,
		dependencies,
	};
}
const mainAsset = createAsset("./src/index.js");
console.log("mainAsset", mainAsset);
  1. 步骤5:递归的创建一个文件间的依赖图,需要一个map表示路径和资源的依赖关系
//递归的创建一个文件间的依赖图,需要一个map表示路径和资源的依赖关系
function createGraph(entry) {
	const mainAsset = createAsset(entry);
	// 遍历所有的资源文件
	const allAssets = [mainAsset];
	for (const asset of allAssets) {
		const dirname = path.dirname(asset.filename);
		asset.map = {};
		asset.dependencies.forEach(relativePath => {
			// 转换成绝对路径
			const absolutePath = path.join(dirname, relativePath);
			const childAsset = createAsset(absolutePath);
			asset.map[relativePath] = childAsset.id;
			allAssets.push(childAsset);
		});
	}
	return allAssets;
}
const graph = createGraph("./src/index.js");
console.log("graph", graph);

image.png 6. 步骤6:创建整体的结果代码块,需要接收参数且立即执行,所以定义一个自执行函数包裹,遍历graph拿到所有的module,以上createAsset 方法只获取到了id,filename,dependencies这三个属性只能表示模块间的依赖关系,并未拿到真正的code,所以需要安装插件,通过babel将ast转换成code

//编译所有代码,获取模块内容,并返回code
const { code } = babel.transformFromAst(astTree, null, {
        presets: ["@babel/preset-env"],
});

image.png 7. 步骤7:经babel编译打包后数据可以分析得出模块需要

function(require,module,exports){
  ${module.code}
}
  1. 步骤8:封装require函数
function require(id){
      const [fn,map] = modules[id];
      function localRequire(relativePath) {
        return require(map[relativePath]);
      }
      const module = {exports: {}}
      fn(localRequire,module,module.exports)
      return module.exports;
 }
  1. 步骤9:编译源代码,把编译后的代码加入result中
function bundle(graph) {
	let modules = "";
        // 遍历依赖关系图,拿到将每个模块代码,依赖关系进行组装
	graph.forEach(module => {
            modules += `${module.id}: [
                function(require,module,exports){
                  ${module.code}
                },
                ${JSON.stringify(module.map)},
            ],`;
	});
	// 实现require方法,自执行函数,从入口开始引入执行,然后fn(localRequire,module,module.exports)加载执行每个模块diamante
        const result = `
            (function(modules){
             function require(id){
              const [fn,map] = modules[id];
              function localRequire(relativePath) {
                return require(map[relativePath]);
              }
              const module = {exports: {}}
              fn(localRequire,module,module.exports)
              return module.exports;
             }
             require(0)
            })({${modules}})
          `;
        return result;
}

总结

什么是模块化,js中模块化包含哪些东西? 模块化,就像积木一样,相互独立,有自己的作用范围,通过向外界定义一些接口和方法和外界交互,这样就遵循了开闭原则,单一职责原则可解耦,提高开发效率,提高复用率 前端模块化,现在主要是ES6,和CommonJs规范 webpack如何分析模块间的依赖关系?

  • 通过插件babel/parser将代码转成ast语法树
  • 通过插件babel/traverse遍历ast语法树拿到每个模块所有依赖
  • 递归并遍历依赖模块,建立文件间的依赖图 打包中bundle和chunk的区别是什么?
  1. chunk 是 webpack 打包过程中 modules 的集合,是打包过程中的概念
  2. bundle 是我们最终打包好的一个或者多个文件

大多数情况下,chunk 和 bundle 是一一对应的,但是也有例外,如果加了 source-map,一个 entry,一个 chunk 也会产生两个 bundle

打包过程中的loader和plugin分别有什么作用?

  1. loader 是文件处理器,将各种非 js 文件,处理成 webpack 可识别的 js 和 json 文件 本质上是将所有类型的文件,转换成引用程序的依赖图,可以直接引用的模块
  2. 拓展插件,是运行在 webpack 打包的各个阶段,都会广播自己的事件,然后插件去监听对应的事件,然后去处理一些事情 项目地址