webpack的简单实现

webpack浅析

一、什么是webpack

webpack是一个用于现代javascript应用程序的静态模块打包工具,webpack会根据应用的入口(entry),遍历各个文件的依赖,构建依赖图谱(depsGraph),依赖图就相当于将各个不同的模块,通过loader处理打包成一个或者多个bundle。

二、浏览器端存在问题

以下代码为例:

// add.js
exports.default = function (a, b) {
	return a + b;
}
// index.js
var add = require('add.js').default
console.log(add(1,2))
复制代码

这时候浏览器会报错:require is not defined,因为在浏览器是无法识别这个commonjs的require的;
不采用webpack这种构建工具来解决这个问题的话,可以用type=”module”

三、模拟require和exports

在上面的栗子中,只要我们解决require is not defined这个报错,并且能实现require和exports的功能即可,require的根本作用其实就是读取模块中的代码字符串,这一点fs.readFileSync就能实现,同时,再利用new Functon()或者eval()就能实现代码字符串的执行

var exports = {}
var require = e => {
    eval("e.default = function (a, b) { return a + b }")
}
require(exports)
var add = exports.default
console.log(add(1,2))

// 控制台输出:3
复制代码

针对存在多个代码段需要执行的场景

var exports = {}
var require = e => {
    eval("e.default = function (a, b) { return a + b }")
}
require(exports)
var add = exports.default

var fileParse = fileList => {
    var require = file => {
        var exports = {}
        (function (e) {
            eval(fileList(file))
        })(exports)
        return exports
    }
    require('./index.js')
}
fileParse({
    "index.js": `
        var add = require('add.js').default
        console.log(add(1 , 2))
    `,
    "add.js": `exports.default = function(a,b){return a + b}`,
})
console.log(add(1,2))
复制代码

以上,其实我们基本解决了在浏览器端使用require和exports带来的问题,那么还有个问题,怎么解决各模块之前的依赖问题,如何建立依赖关系

四、模块之间的依赖关系

我们先不用关心如何提取各模块之间的依赖关系,假设有一段依赖关系(dependecies)及其模块代码(code),我们将如何处理

{
  './src/index.js': {
    dependecies: { './add.js': './src/add.js' },
    code: `
      var add = require('add.js').default
      console.log(add(1 , 2))
    `
  },
  './src/add.js': {
    dependecies: {},
    code: `
			exports.default = function(a,b){return a + b}
		`
  }
}
复制代码

上面这段数据结构,表示index.js依赖于add.js,而add.js就不依赖于其他模块了,那么我们开始着手改造上一part中的代码

const graph = {
    './src/index.js': {
        dependecies: { './add.js': './src/add.js' },
        code: `
            var add = require('./add.js').default
            console.log(add(1 , 2))
        `
    },
    './add.js': {
        dependecies: {},
        code: `
                exports.default = function(a,b){return a + b}
            `
    }
}
const parseGraph = function (graph) {
    // 重写require函数
    const require = moduleId => {
        var exports = {};
        (function (require, exports, code) {
            eval(code)
        })(require, exports, graph[moduleId].code)
        return exports
    }
    require('./src/index.js') // 相当于webpack中的entry
}
parseGraph(graph)
// 控制台:3 
复制代码

这么写虽然能正确输出,但是有个问题,就是dependecies没有利用起来,graph的key没有用绝对路径来建立映射,导致require('./add.js')这段代码中的引用路径和graph[‘./add.js’]进行了强关联,不够灵活;继续改造,根据dependecies,去建立各chunk的关联

const graph = {
    './src/index.js': {
        dependecies: { './add.js': './src/add.js' },
        code: `
            var add = require('./add.js').default
            console.log(add(1 , 2))
        `
    },
    './src/add.js': {
        dependecies: {},
        code: `
                exports.default = function(a,b){return a + b}
            `
    }
}
const parseGraph = function (graph) {
    // 重写require函数
    const require = moduleId => {
        var exports = {};
      	// 重新定义require,针对require('./add.js')这种相对路径的引入,需要通过dependecies,查找到绝对路径;再根据绝对路径					 去查找到code并执行
        function absPathRequire(relativePath) {
            return require(graph[moduleId].dependecies[relativePath])
        }
        (function (require, exports, code) {
            eval(code)
        })(absPathRequire, exports, graph[moduleId].code)
        return exports
    }
    require('./src/index.js') // 相当于webpack中的entry
}
// 解析依赖关系
parseGraph(graph)
复制代码

以上我们完成了针对依赖关系的代码解析过程,那么我们是如何收集依赖关系的呢?

依赖收集

const fs = require("fs");
const path = require("path");
const parser = require("@babel/parser");
const traverse = require("@babel/traverse").default;
const babel = require("@babel/core");
/**
 * 分析模块
 * @param {*} file
 * @returns
 */
function getModuleInfo(filePath) {
  // 读取文件
  const body = fs.readFileSync(file, "utf-8");
  // code转化成AST抽象语法树
  const ast = parser.parse(body, {
    sourceType: "module", //表示我们要解析的是ES模块
  });

  const deps = {};
  // 遍历ast,进行依赖收集
  traverse(ast, {
    // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
    ImportDeclaration({ node }) {
      const dirname = path.dirname(file);
      // 保存依赖模块的绝对路径,如'./src/add.js'
      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 = { filePath, deps, code };
  return moduleInfo;
}

/**
 * 获取依赖
 * @param {*} temp 
 * @param {*} param1 
 */
function getDeps(temp, { deps }) {
  Object.keys(deps).forEach((filePath) => {
    const child = getModuleInfo(deps[filePath]);
    temp.push(child);
    getDeps(temp, child); // 递归收集依赖,将依赖对应的path,code,deps,存入temp
  });
}

/**
 * 模块解析
 * @param {*} filePath 
 * @returns 
 */
function parseModules(filePath) {
  const entry = getModuleInfo(filePath);
  const temp = [entry]; // 从入口文件开始
  const depsGraph = {};

  getDeps(temp, entry);
	// 遍历temp,以path为key, { deps, code }为value, 放入depsGraph
  temp.forEach((moduleInfo) => {
    depsGraph[moduleInfo.filePath] = {
      deps: moduleInfo.deps,
      code: moduleInfo.code,
    };
  });
  return depsGraph;
}
/**
 * 生成bundle文件
 * @param {*} file 
 * @returns 
 */
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);
复制代码

初步实现了webpack的依赖分析、打包过程

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享