快捷搜索:  汽车  科技

webpack打包生成开发模式(如何实现Webpack的)

webpack打包生成开发模式(如何实现Webpack的)在平常的开发中,我们经常使用的就是 ES Module 的形式进行模块间的引用。那么,为了实现一个 Bundler 打包,我们准备这样一个例子:Bundler 打包背景,即它是什么? Bundler 打包指的是我们可以将模块化的代码通过 构建模块依赖图 、 解析代码 、 执行代码 等一系列手段来将模块化的代码聚合成 可执行的代码 。在下一篇文章,我将会介绍什么是「esbuild」,以及其带来的价值。但是,虽说后浪确实很强,不过起码近两年来看「Webpack」所处的地位是仍然 不可撼动 的。所以,更好地了解「Webpack」相关的原理,可以加强我们的个人竞争力。那么,回到今天的正题,我们就来从零实现一个「Webpack」的 Bundler 打包机制。

前言

webpack打包生成开发模式(如何实现Webpack的)(1)

原文地址:https://segmentfault.com/a/1190000027074192

原文作者:五柳

我想这两年,应该是「Webpack」受冲击最明显的时间段。前有「Snowpack」基于浏览器原生 ES Module 提出,后有「Vite」站在「Vue3」肩膀上的迅猛发展,真的是后浪推前浪,前浪....

并且,「Vite」主推的实现技术 不是一点点新 ,典型的一点使用「esbuild」来充当「TypeScript」的解释器,这一点是和目前社区内 绝大多数打包工具 是不同的。

在下一篇文章,我将会介绍什么是「esbuild」,以及其带来的价值。

但是,虽说后浪确实很强,不过起码近两年来看「Webpack」所处的地位是仍然 不可撼动 的。所以,更好地了解「Webpack」相关的原理,可以加强我们的个人竞争力。

那么,回到今天的正题,我们就来从零实现一个「Webpack」的 Bundler 打包机制。

1 Bundler 打包背景

Bundler 打包背景,即它是什么? Bundler 打包指的是我们可以将模块化的代码通过 构建模块依赖图解析代码执行代码 等一系列手段来将模块化的代码聚合成 可执行的代码

在平常的开发中,我们经常使用的就是 ES Module 的形式进行模块间的引用。那么,为了实现一个 Bundler 打包,我们准备这样一个例子:

目录

|—— src |-- person.js |-- introduce.js |-- index.js ## 入口 |—— bundler.js ## bundler 打包机制

代码

// person.js export const person = 'my name is wjc' // introduce.js import { person } from "./person.js"; const introduce = `Hi ${person}`; export default introduce; // index.js import introduce from "./introduce.js"; console.log(introduce);

除开 bundler.js 打包机制实现文件,另外我们创建了三个文件,它们分别进行了模块间的引用,最终它们会被 Bundler 打包机制解析生成可执行的代码。

接下来,我们就来一步步地实现 Bundler 打包机制。

2 单模块解析

Bundler 的打包实现第一步,我们需要知道每个模块中的代码,然后对模块中的代码进行依赖分析、代码转化,从而保证代码的正常执行。

首先,从入口文件 index.js 开始,获取其文件的内容(代码):

const fs = require("fs") const moduleParse = (file = "") => { const rawCode = fs.readFileSync(file 'utf-8') }

获取到模块的代码后,我们需要知道它依赖了哪些模块?这个时候,我们需要借助两个 babel 的工具: @babel/parser 和 @babel/traverse 。前者负责将代码转化为「抽象语法树 AST」,后者可以根据模块的引用构建依赖关系。

@babel/parser 将模块的代码解析成「抽象语法树 AST」:

const rawCode = fs.readFileSync(file 'utf-8') const ast = babelParser(rawCode { sourceType: "module" })

@babel/traverse 根据模块的引用标识 ImportDeclaration 来构建依赖:

const dependencies = {}; traverse(ast { ImportDeclaration({ node }) { const dirname = path.dirname(file); const absoulteFile = `./${path .join(dirname node.source.value) .replace("\\" "/")}`; dependencies[node.source.value] = absoulteFile; } });

这里,我们通过 @babel/traverse 来将入口 index.js 依赖的模块放到 dependencies 中:

// dependencies { './intro.js' : './src/intro.js' }

但是,此时 ast 中的代码还是初始 ES6 的代码,所以,我们需要借助 @babel/preset-env 来将其转为 ES5 的代码:

const { code } = babel.transformFromAst(ast null { presets: ["@babel/preset-env"] });

index.js 转化后的代码:

"use strict"; var _introduce = _interopRequireDefault(require("./introduce.js ")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } console.log(_introduce["default"]);

到此,我们就完成了对 单模块的解析 ,完整的代码如下:

const moduleParse = (file = "") => { const rawCode = fs.readFileSync(file "utf-8"); const ast = babelParser.parse(rawCode { sourceType: "module" }); const dependencies = {}; traverse(ast { ImportDeclaration({ node }) { const dirname = path.dirname(file); const absoulteFile = `./${path .join(dirname node.source.value) .replace("\\" "/")}`; dependencies[node.source.value] = absoulteFile; } }); const { code } = babel.transformFromAst(ast null { presets: ["@babel/preset-env"] }); return { file dependencies code }; };

接下来,我们就开始模块依赖图的构建。

2 构建模块依赖图

众所周知,「Webpack」的打包过程会构建一个模块依赖图,它的形成无非就是从入口文件出发,通过它的引用模块,进入该模块,继续单模块的解析,不断重复这个过程。大致的逻辑图如下:

webpack打包生成开发模式(如何实现Webpack的)(2)

所以,在代码层面,我们需要从入口文件出发,先调用 moduleParse() 解析它,然后再遍历获取其对应的依赖 dependencies ,以及调用 moduleParse() :

const buildDependenceGraph = (entry) => { const entryModule = moduleParse(entry); const rawDependenceGraph = [entryModule]; for (const module of rawDependenceGraph) { const { dependencies } = module; if (Object.keys(dependencies).length) { for (const file in dependencies) { rawDependenceGraph.push(moduleParse(dependencies[file])); } } } // 优化依赖图 const dependenceGraph = {}; rawDependenceGraph.forEach((module) => { dependenceGraph[module.file] = { dependencies: module.dependencies code: module.code }; }); return dependenceGraph; };

最终,我们构建好的模块依赖图会放到 dependenceGraph 。现在,对于我们这个例子,构建好的依赖图会是这样:

{ './src/index.js': { dependencies: { './introduce.js': './src/introduce.js' } code: '"use strict";\n\nvar...' } './src/introduce.js':{ dependencies: { './person.js': './src/person.js' } code: '"use strict";\n\nObject.defineProperty(exports ...' } './src/person.js': { dependencies: {} code: '"use strict";\n\nObject.defineProperty(exports ...' } }3 生成可执行代码

构建完模块依赖图后,我们需要根据依赖图将模块的代码转化成可以执行的代码。

由于 @babel/preset-env 处理后的代码用到了两个不存在的变量 require 和 exports 。所以,我们需要定义好这两个变量。

require 主要做这两件事:

  • 根据模块名,获取对应的代码并执行。

eval(dependenceGraph[module].code)

  • 处理模块名,由于引用的时候是相对路径,这里需要转成绝对路径,并且递归执行依赖模块代码

function _require(relativePath) { return require(dependenceGraph[module].dependencies[relativePath]); }

而 export 则用于存储定义的变量,所以我们定义一个对象来存储。完整的生成代码函数 generateCode 定义:

const generateCode = (entry) => { const dependenceGraph = JSON.stringify(buildDependenceGraph(entry)); return ` (function(dependenceGraph){ function require(module) { function localRequire(relativePath) { return require(dependenceGraph[module].dependencies[relativePath]); }; var exports = {}; (function(require exports code) { eval(code); })(localRequire exports dependenceGraph[module].code); return exports; } require('${entry}'); })(${dependenceGraph}); `; };4 完整的 bundler 打包机制实现代码

完整的 Bunlder 打包实现代码:

const fs = require("fs"); const path = require("path"); const babelParser = require("@babel/parser"); const traverse = require("@babel/traverse").default; const babel = require("@babel/core"); const moduleParse = (file = "") => { const rawCode = fs.readFileSync(file "utf-8"); const ast = babelParser.parse(rawCode { sourceType: "module" }); const dependencies = {}; traverse(ast { ImportDeclaration({ node }) { const dirname = path.dirname(file); const absoulteFile = `./${path .join(dirname node.source.value) .replace("\\" "/")}`; dependencies[node.source.value] = absoulteFile; } }); const { code } = babel.transformFromAst(ast null { presets: ["@babel/preset-env"] }); return { file dependencies code }; }; const buildDependenceGraph = (entry) => { const entryModule = moduleParse(entry); const rawDependenceGraph = [entryModule]; for (const module of rawDependenceGraph) { const { dependencies } = module; if (Object.keys(dependencies).length) { for (const file in dependencies) { rawDependenceGraph.push(moduleParse(dependencies[file])); } } } // 优化依赖图 const dependenceGraph = {}; rawDependenceGraph.forEach((module) => { dependenceGraph[module.file] = { dependencies: module.dependencies code: module.code }; }); return dependenceGraph; }; const generateCode = (entry) => { const dependenceGraph = JSON.stringify(buildDependenceGraph(entry)); return ` (function(dependenceGraph){ function require(module) { function localRequire(relativePath) { return require(dependenceGraph[module].dependencies[relativePath]); }; var exports = {}; (function(require exports code) { eval(code); })(localRequire exports dependenceGraph[module].code); return exports; } require('${entry}'); })(${dependenceGraph}); `; }; const code = generateCode("./src/index.js");

最终,我们拿到的 code 就是 Bundler 打包后生成的 可执行代码 。接下来,我们可以将它直接复制到浏览器的 devtool 中执行,查看结果。

写在最后

虽然,这个 Bundler 打包机制的实现,只是简易版的,它只是大致地实现了整个「Webpack」的 Bundler 打包流程,并不是适用于所有用例。但是,在我看来很多东西的学习都应该是从易到难,这样的吸收效率才是最高的。

原文地址:https://segmentfault.com/a/1190000027074192

原文作者:五柳

猜您喜欢: