手写一个简易bundler打包工具带你了解Webpack原理

封面

用原生js手写一个简易的打包工具bundler

  • 🥝序言
  • 🍉一、模块分析(入口文件代码分析)
    • 1. 项目结构
    • 2. 安装第三方依赖
    • 3. 业务代码
    • 4. 开始打包
  • 🥑二、依赖图谱Dependencies Graph
    • 1. 结果分析
    • 2. 分析所有模块的依赖关系
  • 🍐三、生成代码
    • 1. 逻辑编写
    • 2. 结果分析
  • 🍓四、结束语
  • 🐣彩蛋 One More Thing
    • (:往期推荐
    • (:番外篇

🥝序言

我们都知道, webpack 是一个打包工具。在我们对它进行配置之前,它也是经过一系列的代码编写,才生成的打包工具。那这背后,又做了什么事情呢?

今天,我们就来原生 js ,来手写一个简易的打包工具 bundler ,实现对项目代码的打包。

下面开始进行本文的讲解~

🍉一、模块分析(入口文件代码分析)

1. 项目结构

下面先来看下我们的项目文件结构。请看下图👇

bundler项目结构

2. 安装第三方依赖

我们需要用到 4 个第三方依赖包,分别是:

  • @babel/parser —— 帮助我们分析源代码并生成抽象语法树 (AST)
  • @babel/traverse —— 帮助我们对抽象语法树进行遍历,并分析里语法树里面的语句;
  • @babel/core —— 将原始代码打包编译成浏览器能够运行的代码;
  • @babel/preset-env —— 用于在解析抽象语法树时进行配置。

下面依次给出安装这四个库的命令,分别是:

(1)@babel/parser

npm install @babel/parser --save

(2)@babel/traverse

npm install @babel/traverse --save

(3)@babel/core

npm install @balbel/core --save

(4)@babel/preset-env

npm install @babel/preset-env --save

3. 业务代码

当我们去做一个项目打包时,首先需要先对项目中的模块进行分析,现在我们先对入口文件进行分析。假设我们要实现一个业务,输出的是 hello monday 。那么我们先来编写我们的业务代码。

第一步: 编写 word.js 文件代码。具体代码如下:

export const word = 'monday';

第二步: 编写 message.js 文件代码。具体代码如下:

import { word } from './word.js';const message = `hello ${word}`;export default message;

第三步: 编写 index.js 文件代码。具体代码如下:

import message from "./message.js";console.log(message);

4. 开始打包

编写完业务代码之后,现在,我们要先来对入口文件 index.js 进行打包。注意,这里除了 babel 外,我们不使用任何工具,没有 webpack 、也没有 webpack-cli 等工具。

我们在根目录下先创建一个文件,命名为 bundler.js ,用这个文件来编写我们的打包逻辑。具体代码如下:

const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');const moduleAnalyser = (filename) => {//1. 首先拿到文件名,拿到文件名之后我们去读取文件里面的内容const content = fs.readFileSync(filename, 'utf-8');//2. 借助Babel-parser,将文件里js的字符串,转化成一个js的对象->这个js对象就是我们所说的抽象语法树const ast = parser.parse(content, {// 3. 如果你传入的ES6的语法,那么需要设置sourceType为modulesourceType: 'module'});//收集入口文件中的依赖文件const dependencies = {};traverse(ast, {/*4. 有了抽象语法树之后,我们需要去分析,它里面的声明都在哪些地方,去找到import这些语句对应的内容5. 需要借助@babel/traverse这个工具,这个工具表明当抽象语法树有ImportDeclaration这样的语句时,它就会继续下面的函数*/ImportDeclaration({ node }) {// console.log(node);const dirname = path.dirname(filename);const newFile = './' + path.join(dirname, node.source.value);// console.log(newFile);//6. 找到import语句之后,将这些语句拼装成一个对象,放在dependencies这个变量中(以键值对的方式来进行存储)dependencies[node.source.value] = newFile;}});/*7. 分析好了之后,对模块的源代码进行一次编译。通过使用transformFromAst,把它从一个ES module,转换成浏览器可以执行的语法,并将其存储在code里面,code生成的代码就是我们可以在浏览器上运行的代码*/const { code } = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"]})return {//返回入口文件的名字filename,//返回入口文件中的依赖文件dependencies,//返回浏览器上可以运行的代码code}// console.log(dependencies);
}const moduleInfo = moduleAnalyser('./src/index.js'); 
console.log(moduleInfo);

通过以上代码,相信大家对打包入口文件有一个基本的了解。之后呢,在控制台运行 node bundler.js 命令,可以对打包过程中的各种分析进行查看。

下面我们继续第二块的内容~

🥑二、依赖图谱Dependencies Graph

对于上述所讲的内容,我们谈到的,只是对一个入口文件进行分析。但是呢,这还远远不够。所以,现在我们要来对整个工程文件进行分析。

1. 结果分析

我们先来看下上述代码中,只分析入口文件时的打印情况。具体代码如下:

{filename: './src/index.js',dependencies: { './message.js': './src\\message.js' },code: '"use strict";\n' +'\n' +'var _message = _interopRequireDefault(require("./message.js"));\n' +'\n' +'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +'\n' +'console.log(_message["default"]);'
}

大家可以看到,入口文件分析完了以后,还有一层一层的依赖和code。现在,我们需要去顺着这些依赖,来把整个项目的内容分析出来。

2. 分析所有模块的依赖关系

我们现在来对 bundler.js 进行升级改造,把所有模块的依赖关系给描绘出来。具体代码如下:

const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');const moduleAnalyser = (filename) => {//1. 首先拿到文件名,拿到文件名之后我们去读取文件里面的内容const content = fs.readFileSync(filename, 'utf-8');//2. 借助Babel-parser,将文件里js的字符串,转化成一个js的对象->这个js对象就是我们所说的抽象语法树const ast = parser.parse(content, {// 3. 如果你传入的ES6的语法,那么需要设置sourceType为modulesourceType: 'module'});//收集入口文件中的依赖文件const dependencies = {};traverse(ast, {/*4. 有了抽象语法树之后,我们需要去分析,它里面的声明都在哪些地方,去找到import这些语句对应的内容5. 需要借助@babel/traverse这个工具,这个工具表明当抽象语法树有ImportDeclaration这样的语句时,它就会继续下面的函数*/ImportDeclaration({ node }) {// console.log(node);const dirname = path.dirname(filename);const newFile = './' + path.join(dirname, node.source.value);// console.log(newFile);//6. 找到import语句之后,将这些语句拼装成一个对象,放在dependencies这个变量中(以键值对的方式来进行存储)dependencies[node.source.value] = newFile;}});/*7. 分析好了之后,对模块的源代码进行一次编译。通过使用transformFromAst,把它从一个ES module,转换成浏览器可以执行的语法,并将其存储在code里面,code生成的代码就是我们可以在浏览器上运行的代码*/const { code } = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"]})return {//返回入口文件的名字filename,dependencies,code}// console.log(dependencies);
}const makeDependenciesGraph = (entry) => {//1. 对入口模块进行一次分析const entryModule = moduleAnalyser(entry);// console.log(entryModule);//2. 定义一个数组,存放入口文件和依赖const graphArray = [ entryModule ];//3. 对graphArray进行遍历for(i = 0; i < graphArray.length; i++){//4. 取出graphArray中的每一项const item = graphArray[i];//5. 取出每一项中的依赖dependenciesconst { dependencies } = item;//6. 如果入口文件有依赖时,就对依赖进行循环if(dependencies) {/*7. 通过不断的循环,最终,可以把它的入口文件,以及它的依赖,还有它的依赖的依赖,一层一层的遍历出来,并推到graphArray当中*/for(let j in dependencies) {/*8. 通过队列(先进先出)的方式实现递归的效果;为什么用递归?递归地进行分析,是因为每个依赖下面有可能还有依赖*/graphArray.push(moduleAnalyser(dependencies[j]))}}}//9. 处理后的graphArray是一个数组,现在需要对它进行格式上的转换const graph = {};graphArray.forEach(item => {graph[item.filename] = {dependencies: item.dependencies,code: item.code}});return graph;
}// './src/index.js' 为入口文件
const graphInfo = makeDependenciesGraph('./src/index.js'); 
console.log(graphInfo);

大家可以看到,我们制造了一个新的函数 makeDependenciesGraph ,来描述所有模块的依赖关系,并在最终对它进行格式上的转换,转换成我们理想中的 js 对象。现在,我们来看下依赖关系的打印结果。打印结果如下:

{'./src/index.js': {dependencies: { './message.js': './src\\message.js' },code: '"use strict";\n' +'\n' +'var _message = _interopRequireDefault(require("./message.js"));\n' +'\n' +'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +'\n' +'console.log(_message["default"]);'},'./src\\message.js': {dependencies: { './word.js': './src\\word.js' },code: '"use strict";\n' +'\n' +'Object.defineProperty(exports, "__esModule", {\n' +'  value: true\n' +'});\n' +'exports["default"] = void 0;\n' +'\n' +'var _word = require("./word.js");\n' +'\n' +'var message = "hello ".concat(_word.word);\n' +'var _default = message;\n' +'exports["default"] = _default;'},'./src\\word.js': {dependencies: {},code: '"use strict";\n' +'\n' +'Object.defineProperty(exports, "__esModule", {\n' +'  value: true\n' +'});\n' +'exports.word = void 0;\n' +"var word = 'monday';\n" +'exports.word = word;'}
}

大家可以看到,所有模块的依赖关系都给遍历出来了。这也就说明了,我们成功进行了这一步的分析。

🍐三、生成代码

1. 逻辑编写

上面我们已经成功生成了依赖图谱,那现在,我们就来把这个依赖图谱,生成能够真正在浏览器上运行的代码。我们继续在 bundle.js 上,编写一个生成代码的逻辑。具体代码如下:

const fs = require('fs');
const path = require('path');
const parser = require('@babel/parser');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');const moduleAnalyser = (filename) => {//1. 首先拿到文件名,拿到文件名之后我们去读取文件里面的内容const content = fs.readFileSync(filename, 'utf-8');//2. 借助Babel-parser,将文件里js的字符串,转化成一个js的对象->这个js对象就是我们所说的抽象语法树const ast = parser.parse(content, {// 3. 如果你传入的ES6的语法,那么需要设置sourceType为modulesourceType: 'module'});//收集入口文件中的依赖文件const dependencies = {};traverse(ast, {/*4. 有了抽象语法树之后,我们需要去分析,它里面的声明都在哪些地方,去找到import这些语句对应的内容5. 需要借助@babel/traverse这个工具,这个工具表明当抽象语法树有ImportDeclaration这样的语句时,它就会继续下面的函数*/ImportDeclaration({ node }) {// console.log(node);const dirname = path.dirname(filename);const newFile = './' + path.join(dirname, node.source.value);// console.log(newFile);//6. 找到import语句之后,将这些语句拼装成一个对象,放在dependencies这个变量中(以键值对的方式来进行存储)dependencies[node.source.value] = newFile;}});/*7. 分析好了之后,对模块的源代码进行一次编译。通过使用transformFromAst,把它从一个ES module,转换成浏览器可以执行的语法,并将其存储在code里面,code生成的代码就是我们可以在浏览器上运行的代码*/const { code } = babel.transformFromAst(ast, null, {presets: ["@babel/preset-env"]})return {//返回入口文件的名字filename,dependencies,code}// console.log(dependencies);
}const makeDependenciesGraph = (entry) => {//1. 对入口模块进行一次分析const entryModule = moduleAnalyser(entry);// console.log(entryModule);//2. 定义一个数组,存放入口文件和依赖const graphArray = [ entryModule ];//3. 对graphArray进行遍历for(i = 0; i < graphArray.length; i++){//4. 取出graphArray中的每一项const item = graphArray[i];//5. 取出每一项中的依赖dependenciesconst { dependencies } = item;//6. 如果入口文件有依赖时,就对依赖进行循环if(dependencies) {/*7. 通过不断的循环,最终,可以把它的入口文件,以及它的依赖,还有它的依赖的依赖,一层一层的遍历出来,并推到graphArray当中*/for(let j in dependencies) {/*8. 通过队列(先进先出)的方式实现递归的效果;为什么用递归?递归地进行分析,是因为每个依赖下面有可能还有依赖*/graphArray.push(moduleAnalyser(dependencies[j]))}}}//9. 处理后的graphArray是一个数组,现在需要对它进行格式上的转换const graph = {};graphArray.forEach(item => {graph[item.filename] = {dependencies: item.dependencies,code: item.code}});return graph;
}const generateCode = (entry) => {//1. 将生成的依赖图谱进行格式转换const graph = JSON.stringify(makeDependenciesGraph(entry));/** 2. 构造require函数和exports对象,转化成浏览器认识的字符串* return require(graph[module].dependencies[relative]) 目的是为了找到真实的路径*/return `(function(graph){function require(module){function localRequire(relativePath) {return require(graph[module].dependencies[relativePath])}var exports = {};(function(require, exports, code){eval(code)})(localRequire, exports, graph[module].code);return exports;};require('${entry}')})(${graph});`;
}// './src/index.js' 为入口文件
const code = generateCode('./src/index.js'); 
console.log(code);

通过上面的代码我们可以看到,我们先将生成的依赖图谱进行格式转换,之后呢,构造 require 函数和 exports 对象,最终转换成浏览器认识的字符串。

2. 结果分析

通过上面的业务编写,我们完成了对整个项目进行打包的过程。现在,我们来看一下打印结果:

(function(graph){function require(module){function localRequire(relativePath) {return require(graph[module].dependencies[relativePath])}var exports = {};(function(require, exports, code){eval(code)})(localRequire, exports, graph[module].code);return exports;};require('./src/index.js')})({"./src/index.js":{"dependencies":{"./message.js":"./src\\message.js"},"code":"\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nconsole.log(_message[\"default\"]);"},"./src\\message.js":{"dependencies":{"./word.js":"./src\\word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"hello \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"./src\\word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.word = void 0;\nvar word = 'monday';\nexports.word = word;"}});

接下来,我们把这个打印结果,放到浏览器上进行检验。检验结果如下:

检验结果

大家可以看到,打包后的结果,在浏览器上成功运行了,并显示除了 hello monday ,至此,说明我们的项目打包成功。

🍓四、结束语

在上面的这篇文章中,从模块的入口文件分析,再到依赖图谱的解析,最后到生成浏览器所认识的代码,我们了解了打包工具的整个操作流程。

到这里,关于本文的讲解就结束啦!希望对大家有帮助~

如文章有误或有不理解的地方,欢迎小伙伴们评论区留言撒~💬

本文代码已上传至公众号,后台回复关键词 webpack 即可获取~

🐣彩蛋 One More Thing

(:往期推荐

webpack入门核心知识👉不会webpack的前端可能是捡来的,万字总结webpack的超入门核心知识

webpack入门进阶知识👉webpack入门核心知识还看不过瘾?速来围观万字进阶知识

webpack实战案例配置👉[万字总结]webpack只会基础配置可不行!快来把实战案例配置一起打包带走

手写loader和plugin👉webpack实战之手写一个loader和plugin

(:番外篇

  • 关注公众号星期一研究室,第一时间关注优质文章,更多精选专栏待你解锁~

  • 如果这篇文章对你有用,记得留个脚印jio再走哦~

  • 以上就是本文的全部内容!我们下期见!👋👋👋

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/307913.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

不喜欢 merge 分叉,那就用 rebase 吧

阅读本文大概需要 3 分钟。有些人不喜欢 merge&#xff0c;因为在 merge 之后&#xff0c;commit 历史就会出现分叉&#xff0c;这种分叉再汇合的结构会让有些人觉得混乱而难以管理。如果你不希望 commit 历史出现分叉&#xff0c;可以用 rebase 来代替 merge。rebase &#xf…

你可能对position和z-index有一些误解

一文详解css中的position和z-index&#x1f9c3;序言&#x1f377;一、文章结构抢先知&#x1f378;二、position1. position的取值2. 标准文档流3. 各取值解析&#xff08;1&#xff09;static&#xff08;2&#xff09;relative&#xff08;3&#xff09;absolute&#xff08…

数据结构与算法专题——第九题 外排序

说到排序&#xff0c;大家第一反应基本上是内排序&#xff0c;是的&#xff0c;算法嘛&#xff0c;玩的就是内存&#xff0c;然而内存是有限制的&#xff0c;总有装不下的那一天&#xff0c;此时就可以来玩玩外排序&#xff0c;当然在我看来&#xff0c;外排序考验的是一个程序…

谁动了我的选择器?深入理解CSS选择器优先级

深入理解CSS选择器优先级&#x1f60f;序言&#x1f9d0;文章内容抢先看&#x1f910;一、基础知识1、为什么CSS选择器很强2、CSS选择器的一些基本概念&#xff08;1&#xff09;4种基本概念Ⅰ. 选择器Ⅱ. 选择符Ⅲ. 伪类Ⅳ. 伪元素&#xff08;2&#xff09;CSS选择器的命名空…

leetcode239. 滑动窗口最大值(思路+详解)

一&#xff1a;题目 二:思路 1.这个题不能用优先队列&#xff0c;虽然我们可以通过优先队列得到最大值&#xff0c;但是我们在移动 窗口的时候,便不可以正常的删除元素了 2.虽然不能用优先对列&#xff0c;但是我们依然希望可以得到队首的元素的时候是最大值&#xff0c;同时还…

《ASP.NET Core 与 RESTful API 开发实战》-- (第10章)-- 读书笔记

第 10 章 部署10.1 部署到 IISASP.NET Core 应用程序支持部署到 IIS 中&#xff0c;之后它将作为应用程序的反向代理服务器和负载均衡器&#xff0c;向应用程序中转传入的 HTTP 请求默认情况下&#xff0c;ASP.NET Core 项目的 Program 类使用如下方式创建 WebHostpublic stati…

翠香猕猴桃 和 薄皮核桃,快来下单

猴桃品种有很多&#xff0c;但不是所有的果子都叫翠香。椭圆形&#xff0c;果喙端较尖&#xff0c;黄褐色硬短茸毛&#xff1b;果肉翠绿色&#xff0c;质细多汁&#xff0c;香甜爽口&#xff0c;有芳香味&#xff0c;白色果心。这就是“翠香”&#xff0c;是集酸甜香于一身的猕…

你可能没有听说过 js中的 DOM操作还有这个: HTMLCollection 和 NodeList

一文了解DOM操作中的HTMLCollection和NodeList⛱️序言&#x1f388;一、基础知识1. 定义&#xff08;1&#xff09;HTMLCollection&#xff08;2&#xff09;NodeList2. 属性和方法&#xff08;1&#xff09;HTMLCollection&#xff08;2&#xff09;NodeList&#x1fa81;二、…

leetcode144. 二叉树的前序遍历(递归+迭代)

一:题目 二&#xff1a;上码 1&#xff1a;递归 class Solution { public:void preorder(TreeNode* root,vector<int>&v ) {if(root NULL) return;v.push_back(root->val);preorder(root->left,v);preorder(root->right,v);}vector<int> preorderT…

都说性能调优难?玩转这3款工具,让你秒变“老司机”!

鲁迅说过&#xff1a;菜鸟写业务&#xff0c;老鸟搭架构&#xff0c;高手玩调优。性能调优可谓是食物链顶端的技术&#xff0c;高薪面试必备良品。然而有不少的开发者&#xff0c;工作多年&#xff0c;却对性能调优几乎一无所知&#xff0c;今天就带大家掰扯掰扯&#xff0c;从…

一文梳理JavaScript中常见的七大继承方案

阐述JavaScript中常见的七大继承方案&#x1f4d6;序言&#x1f4d4;文章内容抢先看&#x1f4dd;一、基础知识预备1. 继承的定义2. 继承的方式&#x1f4da;二、6大常见继承方式1. 原型链继承 &#x1f4a1;&#xff08;1&#xff09;构造函数、原型和实例的关系&#xff08;2…

微软发布 Microsoft Edge 85 稳定版

喜欢就关注我们吧&#xff01;微软推出了 Microsoft Edge 85 稳定版&#xff08;85.0.564.41&#xff09;&#xff0c;现在正逐步向用户推送。此版本带来了以下新特性&#xff1a;收藏夹和设置的本地同步。现在可以在自己的环境中的 Active Directory 配置文件之间同步浏览器收…

leetcode94. 二叉树的中序遍历(左中右)

二:上码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* Tre…

浅谈前端路由原理hash和history

浅谈前端路由原理hash和history&#x1f3b9;序言&#x1f3b8;一、前端路由原理1、SPA2、什么时候需要路由&#x1f3b7;二、Hash模式1、定义2、网页url组成部分&#xff08;1&#xff09;了解几个url的属性&#xff08;2&#xff09;演示3、hash的特点&#x1f3ba;三、Histo…

leetcode145. 二叉树的后序遍历

一:题目 二:上码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}*…

.NET Core API文档管理组件 Swagger

Swagger这个优秀的开源项目相信大家都用过&#xff0c;不多介绍了&#xff0c;这里简单记录一下使用过程。开源地址&#xff1a;https://github.com/domaindrivendev/Swashbuckle.AspNetCore在项目中添加组件Install-Package Swashbuckle.AspNetCore下面用最少的代码完成接入&a…

「3.4w字」超保姆级教程带你实现Promise的核心功能

保姆级详解promise的核心功能&#x1f4da;序言&#x1f4cb;文章内容抢先看&#x1f4f0;一、js的同步模式和异步模式1. 单线程&#x1f4a1;2. 同步模式&#x1f4a1;&#xff08;1&#xff09;定义&#xff08;2&#xff09;图例3. 异步模式&#x1f4a1;&#xff08;1&…

leetcode199. 二叉树的右视图(层序遍历03)

一:题目 二&#xff1a;上码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(n…

如何做好一个开源项目之徽章(二)

在上一篇【如何做好一个开源项目&#xff08;一&#xff09;】&#xff0c;笔者已经介绍过开源项目运作和维护的一些理念了&#xff0c;本篇开始&#xff0c;笔者将着重于介绍一些开源项目维护过程中的一些细节&#xff0c;比如徽章、构建等等。由于最近经常出差&#xff0c;所…

值得关注的HTML基础

值得关注的HTML基础&#x1f973;序言&#x1f60b;一、网页三大元素&#x1f61c;二、HTML简介1. 定义2. 发展历史&#x1f61d;三、HTML结构1. 引例阐述2. 特点3. HTML页面结构&#xff08;1&#xff09;DOCTYPE&#xff08;2&#xff09;html&#xff08;3&#xff09;head&…