返回顶部
首页 > 资讯 > 前端开发 > JavaScript >mini webpack打包基础解决包缓存和环依赖
  • 736
分享到

mini webpack打包基础解决包缓存和环依赖

2024-04-02 19:04:59 736人浏览 八月长安
摘要

目录正文index.js 主入口文件读主入口文件对依赖文件进行读取操作解决依赖成环问题正文 本文带你实现 webpack 最基础的打包功能,同时解决包缓存和环依赖的问题 ~ 发车,先

正文

本文带你实现 webpack 最基础的打包功能,同时解决包缓存和环依赖的问题 ~

发车,先来看示例代码。

index.js 主入口文件

我们这里三个文件,index.js 是主入口文件:

// filename: index.js
import foo from './foo.js'
foo();
//filename: foo.js
import message from './message.js'
function foo() {
  console.log(message);
}
// filename: message.js
const message = 'hello world'
export default message;

接下来,我们会创建一个 bundle.js 打包这三个文件,打包得到的结果是一个 JS 文件,运行这个 JS 文件输出的结果会是 'hello world'。

bundle.js 就是 WEBpack 做的事情,我们示例中的 index.js 相当于 webpack 的入口文件,会在 webpack.config.js 的 entry 里面配置。

让我们来实现 bundle.js 的功能。

读主入口文件

最开始的,当然是读主入口文件了:

function createAssert(filename) {
  const content = fs.readFileSync(filename, {
    encoding: 'utf-8'
  });
  return content;
} 
const content = createAssert('./example/index.js');

接下来,需要做的事情就是把 import 语法引入的这个文件也找过来,在上图中,就是 foo.js,同时还得把 foo.js 依赖的也找过来,依次递推。

现在得把 foo.js 取出来,怎么解析 import foo from './foo.js' 这句,把值取出来呢?

把这行代码解析成 ast 会变成:

接下来的思路就是把上面的代码转化成 ast,接着去取上图框框里那个字段。

对依赖文件进行读取操作

const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
function createAssert(filename) {
  const dependencies = [];
  const content = fs.readFileSync(filename, {
    encoding: 'utf-8'
  });
  const ast = babylon.parse(content, {
    sourceType: 'module',
  }); 
  traverse(ast, {
    ImportDeclaration: ({node}) => {
      dependencies.push(node.source.value);
    }
  })
  console.log(dependencies); // [ './foo.js' ]
  return content;
}

上面我们做的事情就是把当前的文件读到,然后再把当前文件的依赖加到一个叫做 dependencies 的数组里面去。

然后,这里的 createAssert 只返回源代码还不够,再完善一下:

let id = 0;
function getId() { return id++; }
function createAssert(filename) {
  const dependencies = [];
  const content = fs.readFileSync(filename, {
    encoding: 'utf-8'
  });
  const ast = babylon.parse(content, {
    sourceType: 'module',
  }); 
  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value);
    }
  })
  return {
    id: getId(),
    code: content,
    filename,
    dependencies,
    mapping: {},
  };
}

假如对主入口文件 index.js 调用,得到的结果会是(先忽略 mapping):

我们不能只对主入口文件做这件事,得需要对所有在主入口这链上的文件做,上面 createAssert 针对一个文件做,我们基于这个函数,建一个叫做 crateGraph 的函数,里面进行递归调用。

不妨先直接看结果,来了解这个函数是做什么的。

运行这个函数,得到的结果如下图所示:

mapping 字段做了当前项 dependencies 里的文件和其他项的映射,这个,我们在后面会用到。

function createGraph(entry) {
  const modules = [];
  createGraphImpl(
    path.resolve(__dirname, entry),
  );
  function createGraphImpl(absoluteFilePath) {
    const assert = createAssert(absoluteFilePath);
    modules.push(assert);
    assert.dependencies.forEach(relativePath => {
      const absolutePath = path.resolve(
        path.dirname(assert.filename),
        relativePath
      );
      const id = createGraphImpl(absolutePath);
      assert.mapping[relativePath] = child.id;
    });
    return assert.id
  }
  return modules;
}

大家可以注意到,截图中,数组中每一项的 code 就是我们的源代码,但是这里面还留着 import 语句,我们先使用 babel 把它转成 commonJS 。

做的也比较简单,就是用 babel 修改 createAssert 中返回值的 code:

const code = transfORMFromAst(ast, null, {
  presets: ['env'],
}).code

截取其中一项,结果变成了:

接下来要做的一步刚上来会比较难以理解,最关键的是我们会重写 require 函数,非常的巧妙,不妨先看:

我们新建一个函数 bundle 来处理 createGraph 函数得到的结果。

function bundle(graph) {
  let moduleStr = '';
  graph.forEach(module => {
    moduleStr += `
    ${module.id}: [
      // require,module,exports 作为参数传进来
      // 在下面我们自己定义了,这里记作【位置 1】
      function(require, module, exports) {
        ${module.code}
      },
      ${JSON.stringify(module.mapping)}
    ],
    `
  })
  const result = `
    (function(modules){
      function require(id) {
        const [fn, mapping] = modules[id];
        // 这其实就是一个空对象,
        // 我们导出的那个东西会挂载到这个对象上
        const module = { exports: {} }
        // fn 就是上面【位置 1】 那个函数
        fn(localRequire, module, module.exports)
        // 我们使用 require 是 require(文件名)
        // 所有这里要做一层映射,转到 require(id)
        function localRequire(name) {
          return require(mapping[name])
        }       
        return module.exports;
      }
      require(0);
    })({${moduleStr}}) 
  `
  return result;
}

最终的使用就是:

const graph = createGraph('./example/index.js');
const res = bundle(graph);

res 就是最终打包的结果,复制整段到控制台运行,可见成功输出了 'hello world':

于是基本的功能就完成了,也就是 webpack 最基本的功能。

接下来解决包缓存的问题,目前来说,import 过的文件,会被转成 require 函数。每一次都会重新调用 require 函数,现在先办法把已经调用过的缓存起来:

function createGraph(entry) {
  const modules = [];
  const visitedAssert = {}; // 增加了这个对象
  createGraphImpl(
    path.resolve(__dirname, entry),
  );
  function createGraphImpl(absoluteFilePath) {
    // 如果已经访问过了,那就直接返回
    if (visitedAssert[absoluteFilePath]) {
      return visitedAssert[absoluteFilePath]
    }
    const assert = createAssert(absoluteFilePath);
    modules.push(assert);
    visitedAssert[absoluteFilePath] = assert.id;
    assert.dependencies.forEach(relativePath => {
      const absolutePath = path.resolve(
        path.dirname(assert.filename),
        relativePath
      );
      // 优化返回值,只返回 id 即可
      const childId = createGraphImpl(absolutePath);
      assert.mapping[relativePath] = childId;
    });
    return assert.id
  }
  return modules;
}
function bundle(graph) {
  let moduleStr = '';
  graph.forEach(module => {
    moduleStr += `
    ${module.id}: [
      function(require, module, exports) {
        ${module.code}
      },
      ${JSON.stringify(module.mapping)}
    ],
    `
  })
  const result = `
    (function(modules){
      // 增加对已访问模块的缓存
      let cache = {};
      console.log(cache);
      function require(id) {
        if (cache[id]) {
          console.log('直接从缓存中取')
          return cache[id].exports;
        }
        const [fn, mapping] = modules[id];
        const module = { exports: {} }
        fn(localRequire, module, module.exports)
        cache[id] = module;
        function localRequire(name) {
          return require(mapping[name])
        }       
        return module.exports;
      }
      require(0);
    })({${moduleStr}}) 
  `
  return result;
}

解决依赖成环问题

这个问题比较经典,如下所示,这个例子来自于 node.js 官网:

// filename: a.js
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
// filename: b.js
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
// filename: main.js
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);

目前我们只支持额外把 import 语句引用的文件加到依赖项里,还不够,再支持一下 require。做的也很简单,就是 解析 AST 的时候再加入 require 语法的解析就好:

 traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value);
    },
    CallExpression ({ node }) {
      if (node.callee.name === 'require') {
        dependencies.push(node.arguments[0].value)
      }
    }
  })

然后,如果这样,我们直接运行,按照现在的写法处理不了这种情况,会报错栈溢出:

但是我们需要改的也特别少。先看官网对这种情况的解释:

When main.js loads a.js, then a.js in turn loads b.js. At that point, b.js tries to load a.js. In order to prevent an infinite loop, an unfinished copy of the a.js exports object is returned to the b.js module. b.js then finishes loading, and its exports object is provided to the a.js module.

解决方法就是这句话:『an unfinished copy of the a.js exports object is returned to the b.js module』。也就是,提前返回一个未完成的结果出来。我们需要做到也很简单,只需要把缓存的结果提前就好了。

之前我们是这么写的:

fn(localRequire, module, module.exports)
cache[id] = module;

接着改为:

cache[id] = module;
fn(localRequire, module, module.exports)

这样就解决了这个问题:

到现在我们就基本了解了它的实现原理,实现了一个初版的 webpack,撒花~

明白了它的实现原理,我才知道为什么网上说 webpack 慢是因为要把所有的依赖都先收集一遍,且看我们的 createGraph 。它确实是做了这件事。

但是写完发现,这个题材不适合写文章,比较适合视频或者直接看代码,你觉得呢?ಥ_ಥ

所有的代码在这个仓库

以上就是mini webpack打包基础解决包缓存和环依赖的详细内容,更多关于mini webpack包缓存环依赖的资料请关注编程网其它相关文章!

--结束END--

本文标题: mini webpack打包基础解决包缓存和环依赖

本文链接: https://lsjlt.com/news/168514.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • mini webpack打包基础解决包缓存和环依赖
    目录正文index.js 主入口文件读主入口文件对依赖文件进行读取操作解决依赖成环问题正文 本文带你实现 webpack 最基础的打包功能,同时解决包缓存和环依赖的问题 ~ 发车,先...
    99+
    2024-04-02
  • Spring三级缓存解决循环依赖
    目录一级缓存为什么不能在实例化A之后就放入Map?二级缓存 二级缓存已然解决了循环依赖问题,为什么还需要三级缓存?三级缓存 源码 我们都知道Spring中的BeanFactory是一...
    99+
    2024-04-02
  • webpack如何独立打包和缓存处理
    这篇文章将为大家详细讲解有关webpack如何独立打包和缓存处理,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。webpack最基本的配置文件来打包压缩我们的代码:var&...
    99+
    2024-04-02
  • 使用Gradle打依赖包失败如何解决
    这篇文章主要介绍“使用Gradle打依赖包失败如何解决”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“使用Gradle打依赖包失败如何解决”文章能帮助大家解决问题。使用Gradle打依赖包失败以前在使...
    99+
    2023-07-06
  • Spring解决循环依赖的方法(三级缓存)
      说起Spring,作为流水线上装配工的小码农,可能是我们最熟悉不过的一种技术框架。但是对于Spring到底是个什么东西,我猜作为大多数的你可能跟我一样,只知道IOC、DI,却并不...
    99+
    2024-04-02
  • 使用Gradle打依赖包失败的问题及解决
    目录使用Gradle打依赖包失败经过测试后发现应用场景Gradle不能加载依赖包的处理总结使用Gradle打依赖包失败 以前在使用Maven开发java web的项目中,我们通常会用...
    99+
    2023-05-15
    使用Gradle Gradle打依赖包失败 Gradle依赖
  • Spring使用三级缓存解决循环依赖的问题
    Spring如何使用三级缓存解决循环依赖在没开始文章之前首先来了解一下什么是循环依赖 @Component public class A { @Autowired ...
    99+
    2024-04-02
  • Spring为什么需要三级缓存解决循环依赖
    今天小编给大家分享一下Spring为什么需要三级缓存解决循环依赖的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。bean生命周...
    99+
    2023-06-29
  • Spring三级缓存解决循环依赖的过程分析
    目录循环依赖解决思路解决流程二个缓存不行?总结循环依赖 什么是循环依赖?很简单,看下方的代码就知晓了 @Service public class A { @Autowired...
    99+
    2023-05-14
    Spring循环依赖 Spring三级缓存解决循环依赖
  • Spring为何需要三级缓存解决循环依赖详解
    目录前言bean生命周期三级缓存解决循环依赖总结前言 在使用spring框架的日常开发中,bean之间的循环依赖太频繁了,spring已经帮我们去解决循环依赖问题,对我们开发者来说是...
    99+
    2024-04-02
  • maven依赖包加载缓慢的原因以及解决方案
    目录maven依赖包加载缓慢的原因解决方案Maven下载jar包慢,pom报错其实就是把maven的镜像配置改下就好了maven依赖包加载缓慢的原因 首先看idea开发工具里配置的m...
    99+
    2024-04-02
  • 教你Spring如何使用三级缓存解决循环依赖
    一级缓存存放实例化对象 。二级缓存存放已经在内存空间创建好但是还没有给属性赋值的对象。三级缓存存放对象工厂,用来创建提前暴露到bean的对象。 @Service public cla...
    99+
    2024-04-02
  • Spring解决循环依赖问题及三级缓存的作用
    目录前言1什么是循环依赖2 如何解决循环依赖3无法解决的循环依赖前言 所谓的三级缓存只是三个可以当作是全局变量的Map,Spring的源码中大量使用了这种先将数据放入容器中等使用结束...
    99+
    2024-04-02
  • 怎么使用Spring三级缓存解决循环依赖问题
    这篇文章主要介绍了怎么使用Spring三级缓存解决循环依赖问题的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇怎么使用Spring三级缓存解决循环依赖问题文章都会有所收获,下面我们一起来看看吧。循环依赖什么是循环...
    99+
    2023-07-05
  • 解决idea中依赖报错找不到这个包,但是本地maven仓库中却存在此依赖包问题
    解决idea中依赖报错找不到这个包,但是本地maven仓库中却存在此依赖包问题 问题: intellj idea新打开一个项目,经过项目加载完毕后,但是发现maven的依赖存在一些无法下载到的问题。 ...
    99+
    2023-09-02
    intellij-idea maven java
  • 关于Java Spring三级缓存和循环依赖的深入理解
    目录一、什么是循环依赖?什么是三级缓存?二、三级缓存如何解决循环依赖?三、使用二级缓存能不能解决循环依赖?一、什么是循环依赖?什么是三级缓存? 【什么是循环依赖】什么是循环依赖很好理...
    99+
    2024-04-02
  • spring-boot-maven-plugin打包时排除provided依赖问题怎么解决
    这篇文章主要介绍了spring-boot-maven-plugin打包时排除provided依赖问题怎么解决的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇spring-boot-maven-plugin打包时排...
    99+
    2023-07-06
  • 你知道怎么用Spring的三级缓存解决循环依赖吗
    目录1. 前言2. Spring Bean的循环依赖3. Spring中三大循环依赖场景演示3.1 构造器注入循环依赖3.2 singleton模式field属性注入循环依...
    99+
    2024-04-02
  • 一篇文章带你理解Java Spring三级缓存和循环依赖
    目录一、什么是循环依赖?什么是三级缓存二、三级缓存如何解决循环依赖?三、使用二级缓存能不能解决循环依赖?总结一、什么是循环依赖?什么是三级缓存 【什么是循环依赖】什么是循环依赖很好理...
    99+
    2024-04-02
  • webpack打包、编译、热更新Node内存不足问题解决
    目录日常先上结论问题背景排查问题总结日常先上结论 上班的打工开发可以直接在这里看解决方案修复young object promotion failed Allocation fail...
    99+
    2023-03-19
    webpack打包超出内存 webpack node内存不足 webpack 打包 优化
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作