webpack基本原理实现

63 阅读2分钟

1.解析模块

主要用到fs模块以及babel相关库,主要依赖如下:

const parser = require('@babel/parser');
const path = require('path');
const fs = require('fs');
const traverse = require('@babel/traverse').default;
const babel = require('@babel/core');

const generate = require('@babel/generator').default;

先利用node提供的fs模块读取文件,再利用babel解析文件:生成AST -> 便利ast收集依赖 -> es6转es5,最后将模块信息返回

const getModuleInfo = (file) => {
  const body = fs.readFileSync(file, 'utf-8');
  // console.log('@@@@@@@@@@body====\n', body);

  // 解析成ast
  const ast = parser.parse(body, {
    sourceType: 'module', //表示我们要解析的是ES模块
  });
  // console.log('@@@@ast===\n', ast);
  // console.log('@@@@ast.program.body===\n', ast.program.body);

  // 收集依赖
  const deps = {};
  traverse(ast, {
    /* ImportDeclaration方法代表的是对type类型为ImportDeclaration的节点的处理 */
    ImportDeclaration({ node }) {
      const dirname = path.dirname(file);
      console.log('@@@dirname==============', dirname); // ./src
      const abspath = './' + path.join(dirname, node.source.value);
      deps[node.source.value] = abspath;
    },

    /* 处理ast里的函数声明 */
    // FunctionDeclaration: function (path) {
    //   /* 改变函数名 */
    //   path.node.id.name = 'x';
    //   console.log('@@@FunctionDeclaration===', path.node);
    // },
  });
  console.log('@@@deps==============\n', deps);

  const result = generate(ast);
  console.log('@@@result==============\n', result);
  /* 转换代码, es6转es5 */
  const { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env'],
  });
  // console.log('@@@@@@@@@@transformed code=====\n', code);

  /*我们返回了一个对象 ,这个对象包括该模块的路径(file),该模块的依赖(deps),该模块转化成es5的代码 */
  const moduleInfo = { file, deps, code };
  return moduleInfo;
};

2.递归解析模块信息

根据入口文件递归获取依赖并解析,最后得到依赖图depsGraph(这里未考虑循环引用,是用键值对对象表示)


getModuleInfo('./src/index.js');
// getModuleInfo('./src/indexTest.js');
/* 递归获取依赖 */
const parseModules = (file) => {
  const entry = getModuleInfo(file);
  const temp = [entry];
  for (let i = 0; i < temp.length; i++) {
    const deps = temp[i].deps;
    if (deps) {
      /* 遍历所有依赖,递归获取到依赖模块数据 */
      for (const key in deps) {
        if (deps.hasOwnProperty(key)) {
          temp.push(getModuleInfo(deps[key]));
        }
      }
    }
  }
  // console.log('@@@temp====\n', temp);

  /* 路径为key,{code,deps}为值的形式存储。因此,我们创建一个新的对象depsGraph。 */
  const depsGraph = {}; //新增代码
  temp.forEach((moduleInfo) => {
    depsGraph[moduleInfo.file] = {
      deps: moduleInfo.deps,
      code: moduleInfo.code,
    };
  });
  // console.log('@@@@@@@@depsGraph==========\n', depsGraph);
  return depsGraph;
};

3.实现require和export

实现require 和 export关键字,以file为入口文件,根据依赖图递归执行代码

const 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})`;
};

4.写入文件

将打包转换后的代码写入文件(一般是dist目录下)


//写入到我们的dist目录下
!!fs.existsSync('./dist') && fs.rmdirSync('./dist');
// fs.writeFileSync('./dist/bundle.js', content);