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

目录
  • 正文
    • index.js 主入口文件
    • 读主入口文件
    • 对依赖文件进行读取操作
  • 解决依赖成环问题

正文

本文带你实现 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包缓存环依赖的资料请关注我们其它相关文章!

(0)

相关推荐

  • JavaScript webpack模块打包器如何优化前端性能

    目录 一.webpack的使用背景 二.webpack如何优化 1. JS代码压缩 2.CSS代码压缩 3. HTML文件压缩 4. 文件大小压缩 5. 图片压缩 6. Tree Shaking 7. 代码分离 8. 内联chunk 9. 利用CDN加速以及提取公共第三方库 三.总结 一.webpack的使用背景 随着前端的项目逐渐扩大,必然会导致性能问题.尤其大大型复杂的项目中,前端业务可能因为一个小小的数据依赖,导致整个页面的卡顿甚至崩溃. 一般项目在完成后,会通过webpack进行打包,利

  • JS逆向之 webpack 打包站点实战原理分享

    目录 webpack 原理说明 扣 JS 代码 webpack 原理说明 webpack 是前端程序员用来进行打包 JS 的技术,打包之后的代码特征非常明显,例如下述代码. (window.webpackJsonp = window.webpackJsonp || []).push([[0], []]); 有经验之后,当看到出现 app.版本号.js,chunk-libs.版本号.js> 就能大概猜到 JS 是使用了 webpack 加密. 学习过程中,我们顺手解决一个 webpack 的加密站

  • webpack-cli在webpack打包中的作用小结

    目录 webpack & webpack-cli webpack Introduction webpack-cli 详解 Reference webpack & webpack-cli webpack Introduction webpack 是一个静态的模块化打包工具,为现代的JavaScript应用程序服务 打包 bundler:webpack可以帮助我们进行打包,所以它是一个打包工具 静态的static:将代码打包成最终的静态资源(部署到静态服务器) 模块化module:webpac

  • 使用webpack打包ts代码的实现

    目录 使用 webpack 打包 生成 package.json 安装 cnpm 安装 webpack 相关 编写 webpack 配置文件 创建 tsconfig.json 修改 package.json 使用webpack打包 安装插件 html-webpack-plugin webpack-dev-server clear-webpack-plugin babel 模块问题 使用 webpack 打包 生成 package.json 本质上,webpack 是一个用于现代 JavaScri

  • webpack打包的3种hash值详解

    目录 前言 当年的校招 哪三种? 实践讲解 事先准备 打包环境搭建 hash chunkhash contenthash 前言 大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心 当年的校招 依稀记得,当年我参加了大厂的校招,面试的是网易雷火工作室,当时有一道题,我记得很清楚,就是:说说webpack中三种hash配置的区别 哈哈,我当时连webpack都不太会配置,所以也答不出来,然后也...没有然后了.. 哪三种? webpack中的三种hash分别是

  • vue-cli5.0 webpack 采用 copy-webpack-plugin 打包复制文件的方法

    目前 vue-cli已经发布5.0了,webpack 配置上也与之前老版本的有所不同,调整 webpack 配置最简单的方式就是在 vue.config.js 中的对象中进行配置,该对象将会被 webpack-merge 合并入最终的 webpack 配置. 有些 webpack 选项是基于 vue.config.js 中的值设置的,所以不能直接修改.例如你应该修改 vue.config.js 中的 outputDir 选项而不是修改 output.path:你应该修改 vue.config.j

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

    目录 正文 index.js 主入口文件 读主入口文件 对依赖文件进行读取操作 解决依赖成环问题 正文 本文带你实现 webpack 最基础的打包功能,同时解决包缓存和环依赖的问题 ~ 发车,先来看示例代码. index.js 主入口文件 我们这里三个文件,index.js 是主入口文件: // filename: index.js import foo from './foo.js' foo(); //filename: foo.js import message from './messag

  • Webpack打包慢问题的完美解决方法

    前言 这几天写腾讯实习生 Mini 项目的时候用上了 React 全家桶,当然同时引入了 Webpack 作为打包工具.但是开发过程中遇到一个很棘手的问题就是,React 加上 React-Router.superagent.eventproxy 这些第三方轮子一共有好几百个 module,Webpack 的打包速度极慢.这对于开发是非常不好的体验,同时效率也极低. 问题分析 我们先来看一下完全没有任何优化的时候,Webpack 的打包速度(使用了jsx和babel的loader). 下面是我们

  • webpack之基础打包优化的实现

    目录 前言 优化loader 压缩代码 gzip压缩 抽离公共依赖包 最后 前言 webpack至今已经发展了5个版本,如果你还不会webpack,那么赶紧学习起来吧.webpack是web前端开发人员必学的一个知识点.本篇文章带大家一起看看webpack基础的打包优化.话不多说,上酸菜~~~~ 优化loader 在脚手架项目中,想必各位开发人员都会用到预处理的css.比如sass.stylus.less这些预处理语言.我们知道使用这些预处理css必须要在webpack中配置相应的loader.

  • 浅谈webpack打包之后的文件过大的解决方法

    以前一直使用 create-react-app 这个脚手架进行 react 开发,后面因为一些自定义的配置,转而使用 webpack 搭建一套自己的脚手架.但是在使用 webpack 打包之后发现,纳尼?怎么文件这么大??? 于是研究了一下如何处理 webpack 打包之后文件太大的情况,简单记录下来. 首先配置全局变量 首先,通过指定环境,告诉 webpack 我们当前处于 production 环境中,要按照 production 的方式去打包. //指定环境,将process.env.NO

  • vue解决使用webpack打包后keep-alive不生效的方法

    问题是这样的,我使用webpack的npm run dev运行的时候,keep-alive路由缓存是有效的,但是我npm run build,把文件放到实际的项目中去的时候,会有如下的问题: 路由如下: var menus = [ { path: '/user', name: '用户', component: '/user', redirect: '/user/index1', icon: 'fa-bandcamp', meta: { keepAlive: false }, children:

  • 解决vue-cli webpack打包开启Gzip 报错问题

    前两天项目上线,用vue-cli npm run build命令打包,打包完成后我擦吓了一跳,15M.本来暂时不打算优化的,但是每次看着部署包这么大,想想还是先优化一下,让包好看点,免得以后出现心理阴影! 在把 map文件干掉后,发现webpack这打包的速度,也忒感人了.在进行不自动生成 map文件设置时,有看到webpack自带的productionGzip功能,索性就一次性一起鼓捣鼓捣. 下面是瞎鼓捣历程,差点就鼓捣不出来了. 1.在项目 根目录config/index.js中build内

  • vue多次打包后出现浏览器缓存的问题及解决

    目录 vue多次打包后出现浏览器缓存 解决方案 使用hash解决vue浏览器的缓存 缓存问题 怎么解决缓存问题 解决办法 vue多次打包后出现浏览器缓存 每次打包更新版本上传到服务器上,会偶尔出现代码没有更新还是旧代码的逻辑,这就代表浏览器存在缓存的问题了. 解决方案 vue-cli2 webpack .prod.conf.js 下修改output const  Timestamp = new Date().getTime(); output: {     path: config.build.

  • Webpack打包css后z-index被重新计算的解决方法

    发现问题 最近在使用 Webpack 打包 css 文件时,发现了一个问题,发现打包后的 z-index 值跟源文件 z-index 不一致. 如下图,左侧是源文件,右侧是打包后的文件: 即使加上 !important,经过 OptimizeCssAssetsPlugin 调用 cssProcessor cssnano处理之后也是 z-index: 2. 因此,很可能是 cssnano 进行了重新计算(cssnano 称为 rebase),而且这种计算是不够准确的. 因为打包后的文件有两处 z-

  • 解决webpack打包速度慢的解决办法汇总

    刚开始用webpack,谈一谈解决webpack打包慢的问题的方法 技巧1 webpack在打包的时候第一次总是会做很长的准备工作,包括加载插件之类的.在刚接触webpack的时候总是webpack一下-测一下-改一下-再webpack一下,这种方式最后让很多人崩溃了觉得webpack一点都不好用.其实这是错误的使用方式. 正确的方式应直接执行webpack --watch 这样webpack会自动编译,第一回的时候确实很慢,但之后的自动编译就要快了好多,打包时间相差几倍. 技巧2 webpac

  • 解决vue+webpack打包路径的问题

    最近写了一个vue小项目,不想单独作为一个web项目发布,所以就准备放到资源项目的public文件夹下,遇到一些小问题,在此总结一下. 资源路径如下: public目录配置的访问路径为"/",在这样的情况下,我们的访问路径就变成了"域名/vue-demo".访问的时候发下程序未报错,但是页面一片空白.此前也这样发布的项目都没有问题,但这次是怎么回事呢? 仔细探索后发现是vue-router搞得鬼.因项目需要,所以使用了滚动行为,滚动行为必须开启history模式,在

随机推荐