手写一个Webpack吧~

简介: Webpack是前端热门打包工具,是前端工程化的根基,在Webpack眼中,万物皆模块,因此Webpack打包的核心目的其实就是把所有模块都打包到一个文件中(bundle.js)

您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

前言

Webpack是前端热门打包工具,是前端工程化的根基,在Webpack眼中,万物皆模块,因此Webpack打包的核心目的其实就是把所有模块都打包到一个文件中(bundle.js)

在这里插入图片描述

原理分析

如果现在有这些模块:

index.js

import add from './add.js';
console.log(add(1, 2));

add.js

export default function add(a, b) {
  return a + b;
}

我们配置一个最基本的webpack.config.js文件:

const path = require('path');

module.exports = {
   
   
  entry: './index.js',
  output: {
   
   
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  mode: 'none'
}

执行npx webpack,打包后结果:
在这里插入图片描述

可以看到,webpack将多个js文件打包到了一个文件中,这就是webpack的原理,要实现这个原理,需要这些步骤:

  • 分析entry文件;
  • 基于entry文件的导入依赖,递归查找出整个项目的依赖;
  • 将所有依赖转换成依赖图;
  • 写入bundle.js;

分析entry文件

由于需要es6转es5,以及ast语法树的生成,便于我们查找每个文件中的依赖关系,下载这些依赖包:

npm i --save-dev @babel/parser @babel/traverse @babel/core

并且在项目中新建webpack.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');

接下来开始写第一个方法getModuleInfo,获取单个文件的信息,代码如下:

//生成单文件依赖树
function getModuleInfo(file) {
   
   
  //读取入口文件内容
  const body = fs.readFileSync(file, 'utf-8');
  //转为ast语法树
  const ast = parser.parse(body, {
   
   
    sourceType: 'module'
  })
  //收集所有模块依赖
  const deps = {
   
   };
  traverse(ast, {
   
   
    ImportDeclaration({
   
    node }) {
   
   
      const dirname = path.dirname(file);
      //转换当前文件的绝对路径
      const absPath = path.join(dirname, node.source.value);
      deps[node.source.value] = absPath;
    }
  })
  //es6转es5代码
  const {
   
    code } = babel.transformFromAst(ast, null, {
   
   
    presets: ["@babel/preset-env"],
  });
  const moduleInfo = {
   
    file, deps, code };
  return moduleInfo;
}

从代码可以看到,读取到文件信息后,将文件转为ast语法树,并开始收集依赖,这里遍历器用的是babel-traverse自带的遍历器,让我们快速收集到所有import导入依赖的语句,最后将文件代码转为es5代码。

执行getModuleInfo('./index.js');出现结果:

在这里插入图片描述

这就是依赖树种一个文件的内容,接下来的思路很简单,基于entry文件的deps,递归查找所有deps文件中所用到的依赖,直到无依赖,并将收集到的信息全部保存在一个对象中。

收集依赖

这里我们创建两个函数,parseModules和getDeps,代码如下:

//收集entry文件依赖,并整合所有依赖
function parseModules(file) {
   
   
  const entry = getModuleInfo(file);
  let temps = [entry];
  const depsResult = {
   
   };          //整个项目最终所有文件 -> 代码块的映射表
  getDeps(entry, temps)
  temps.forEach(item => {
   
   
    depsResult[item.file] = {
   
   
      deps: item.deps,
      code: item.code
    }
  })
  return depsResult;
}

//收集entry文件所依赖的子文件其他依赖
function getDeps({
   
    deps }, temps) {
   
   
  //遍历入口文件的所有依赖,寻找更多依赖
  Object.keys(deps).forEach(d => {
   
   
    const child = getModuleInfo(deps[d]);
    temps.push(child);
    getDeps(child, temps)
  })
}

在parseModules函数中,我们首先获取到了入口文件的依赖树,基于他的deps,进行更多deps的获取,并记录他们的依赖树,最后保存在depsResult中,结果如下:

在这里插入图片描述
接下来,我们将所有文件的代码合并起来即可。

写入bundle

这里,我们写一个自执行函数,里面包括了自定义require函数和exports对象,让浏览器识别到我们自定义的导入导出:

function bundle(file) {
   
   
  const depsGraph = JSON.stringify(parseModules(file));
  return `(function (graph) {
        function require(file) {
            function absRequire(relPath) {
                return require(graph[file].deps[relPath])
            }
            var exports = {};
            (function (require,exports,code) {
                eval(code)
            })(absRequire,exports,graph[file].code)
            return exports
        }
        require('${
     
     file}')
    })(${
     
     depsGraph})`;
}

const content = bundle('./index.js');
!fs.existsSync("./dist") && fs.mkdirSync("./dist");
fs.writeFileSync("./dist/bundle.js", content);

最后把这块代码写入dist/bundle.js即可。

测试

我们在index.html中引入dist/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');

//生成单文件依赖树
function getModuleInfo(file) {
   
   
  //读取入口文件内容
  const body = fs.readFileSync(file, 'utf-8');
  //转为ast语法树
  const ast = parser.parse(body, {
   
   
    sourceType: 'module'
  })
  //收集所有模块依赖
  const deps = {
   
   };
  traverse(ast, {
   
   
    ImportDeclaration({
   
    node }) {
   
   
      const dirname = path.dirname(file);
      //转换当前文件的绝对路径
      const absPath = path.join(dirname, node.source.value);
      deps[node.source.value] = absPath;
    }
  })
  //es6转es5代码
  const {
   
    code } = babel.transformFromAst(ast, null, {
   
   
    presets: ["@babel/preset-env"],
  });
  const moduleInfo = {
   
    file, deps, code };
  return moduleInfo;
}

//收集entry文件依赖,并整合所有依赖
function parseModules(file) {
   
   
  const entry = getModuleInfo(file);
  let temps = [entry];
  const depsResult = {
   
   };          //整个项目最终所有文件 -> 代码块的映射表
  getDeps(entry, temps)
  temps.forEach(item => {
   
   
    depsResult[item.file] = {
   
   
      deps: item.deps,
      code: item.code
    }
  })
  return depsResult;
}

//收集entry文件所依赖的子文件其他依赖
function getDeps({
   
    deps }, temps) {
   
   
  //遍历入口文件的所有依赖,寻找更多依赖
  Object.keys(deps).forEach(d => {
   
   
    const child = getModuleInfo(deps[d]);
    temps.push(child);
    getDeps(child, temps)
  })
}

function bundle(file) {
   
   
  const depsGraph = JSON.stringify(parseModules(file));
  return `(function (graph) {
        function require(file) {
            function absRequire(relPath) {
                return require(graph[file].deps[relPath])
            }
            var exports = {};
            (function (require,exports,code) {
                eval(code)
            })(absRequire,exports,graph[file].code)
            return exports
        }
        require('${
     
     file}')
    })(${
     
     depsGraph})`;
}

const content = bundle('./index.js');
!fs.existsSync("./dist") && fs.mkdirSync("./dist");
fs.writeFileSync("./dist/bundle.js", content);

ok,学费了~

目录
相关文章
|
Web App开发 开发者
[爆]小程序内可直接打开网页了!附开发文档
昨天微信团队又深夜奉上大新闻:①小程序内可直接打开网页、②小程序可关联500个公众号。为便于开发者灵活配置小程序,小程序现开放内嵌网页能力。(网友评论称这个功能直接把小程序提高n个高度,秒掉手机浏览器。
1695 0
|
11月前
|
前端开发 JavaScript 开发者
前端 CSS 优化:提升页面美学与性能
前端CSS优化旨在提升页面美学与性能。通过简化选择器(如避免复杂后代选择器、减少通用选择器使用)、合并样式表、合理组织媒体查询,可减少浏览器计算成本和HTTP请求。利用硬件加速和优化动画帧率,确保动画流畅。定期清理冗余代码并使用缩写属性,进一步精简代码。这些策略不仅加快页面加载和渲染速度,还提升了视觉效果,为用户带来更优质的浏览体验。
|
机器学习/深度学习 人工智能 Cloud Native
大语言模型推理提速,TensorRT-LLM 高性能推理实践
大型语言模型(Large language models,LLM)是基于大量数据进行预训练的超大型深度学习模型,本文主要讲述TensorRT-LLM利用量化、In-Flight Batching、Attention、Graph Rewriting提升 LLM 模型推理效率。
102601 2
|
网络安全 Windows
备份SSH配置文件
备份SSH配置文件
374 1
|
缓存 前端开发 JavaScript
前端性能优化:Webpack与Babel的进阶配置与优化策略
【10月更文挑战第28天】在现代Web开发中,Webpack和Babel是不可或缺的工具,分别负责模块打包和ES6+代码转换。本文探讨了它们的进阶配置与优化策略,包括Webpack的代码压缩、缓存优化和代码分割,以及Babel的按需引入polyfill和目标浏览器设置。通过这些优化,可以显著提升应用的加载速度和运行效率,从而改善用户体验。
299 6
|
Linux 测试技术 iOS开发
Meson:现代的构建系统
Meson:现代的构建系统
930 0
|
弹性计算 缓存 并行计算
带你读《弹性计算技术指导及场景应用》——3. Ada Lovelace架构解读及RTX 4090性能测试分析(1)
带你读《弹性计算技术指导及场景应用》——3. Ada Lovelace架构解读及RTX 4090性能测试分析(1)
960 4
|
Oracle 关系型数据库 数据安全/隐私保护
在Oracle中,ORA-01017 invalid username password; logon denied原因有哪些
在Oracle中,ORA-01017 invalid username password; logon denied原因有哪些
2991 0
|
前端开发 JavaScript 搜索推荐
webpack----前端工程化与webpack的基本使用
webpack----前端工程化与webpack的基本使用