手写一个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,学费了~

目录
相关文章
|
缓存 资源调度 编译器
原来是这样啊!浅谈webpack4和webpack5的区别
相对于webpack4,webpack5内置了很多plugin插件,比如、打包、压缩、缓存
772 1
|
1月前
|
缓存 前端开发 JavaScript
Webpack 4 和 Webpack 5 区别?
【10月更文挑战第23天】随着时间的推移,Webpack 可能会继续发展和演进,未来的版本可能会带来更多的新特性和改进。保持对技术发展的关注和学习,将有助于我们更好地应对不断变化的前端开发环境。
|
7月前
|
自然语言处理 JavaScript 前端开发
webpack实战——手写常用plugin
webpack实战——手写常用plugin
|
JavaScript 前端开发
webpack原理解析(一)实现一个简单的webpack
Webpack 是当下最热门的前端资源模块化管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分隔,等到实际需要的时候再异步加载。webpack如此强大,其内部机制到底是怎样的,今天我们来一探究竟。
|
移动开发 前端开发 JavaScript
Webpack入门
Webpack入门
115 0
Webpack入门
|
前端开发 JavaScript 开发者
webpack编写一个插件
webpack编写一个插件
102 0
|
前端开发 JavaScript
【WebPack】webpack详细操作
【WebPack】webpack详细操作
|
JavaScript 前端开发 编译器
webpack入门讲解(一)
webpack入门讲解(一)
webpack入门讲解(一)
|
JavaScript 前端开发 网络安全
webpack学习
webpack学习
138 0
webpack学习
|
JSON 资源调度 JavaScript
Webpack5 入门(1):webpack简单使用
自 2012年3月10日诞生,Webpack 到今年已经是一个有着悠久历史的老牌构建工具了。 Webpack 基于 Node.js 开发,默认采用了 CommonJS 模块化规范。每一个文件都是一个模块,默认支持的模块类型有 `.js` 和 `.json`。对于其他类型的模块,比如 `.vue`,`.jsx`,`.ts`、`.css` 以及图片类型的模块,都需要安装对应的 `loader` 进行编译处理。
164 0
下一篇
DataWorks