webpack进阶: 简易的实现打包js文件功能

步骤

1.创建一个文件夹,初始化项目和安装依赖
npm init -y
npm i @babel/parser @babel/traverse @babel/core @babel/preset-env -D
2.在项目根目录下创建src目录,并在src中创建index.js、message.js、test.js这三个文件

index.js文件:

import message from './message.js';

console.log('hello bundler');

message.js文件:

import test from './test.js';

console.log('message page');

test.js文件:

console.log('test page');
3.在项目根目录下创建bundle.js

bundle.js先留着空,下面教你们如何编写。

4.整个项目初始化文件图

在这里插入图片描述

下面开始编写bundle.js文件,思路如下:

1.编写moduleAnalyzer函数

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 moduleAnalyzer(entry) {
	// 读取目标文件
  const content = fs.readFileSync(entry, 'utf8');
  const dependencies = {};
  // 使用@babel/parser来帮我们生成抽象语法树
  const ast = parser.parse(content, {
    sourceType: 'module',
  });
  // 使用traverse来帮我们过滤type为ImportDeclaration的项
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(entry);
      const newFile = path.join(dirname, node.source.value);
      dependencies[node.source.value] = newFile;
    },
  });
  const { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env'],
  });
  return {
    file: entry,
    dependencies,
    code,
  };
}
const info = moduleAnalyzer('./src/index.js');
console.log(info);

思路:我们这里先对单一的文件进行分析。我们这里先对index.js进行分析。

  1. 想要进行分析,我们需要读取目标文件的内容。
  2. 我们将读取的结果交给@babel/parser插件来帮我们进行解析,解析最后返回一个抽象语法树ast。
    我们打印看一下抽象语法树的主体:console.log(ast.program.body)
[ Node {
    type: 'ImportDeclaration',
    start: 0,
    end: 35,
    loc:
     SourceLocation {
       start: [Position],
       end: [Position],
       filename: undefined,
       identifierName: undefined },
    range: undefined,
    leadingComments: undefined,
    trailingComments: undefined,
    innerComments: undefined,
    extra: undefined,
    specifiers: [ [Node] ],
    source:
     Node {
       type: 'StringLiteral',
       start: 20,
       end: 34,
       loc: [SourceLocation],
       range: undefined,
       leadingComments: undefined,
       trailingComments: undefined,
       innerComments: undefined,
       extra: [Object],
       value: './message.js' } },
  Node {
    type: 'ExpressionStatement',
    start: 39,
    end: 68,
    loc:
     SourceLocation {
       start: [Position],
       end: [Position],
       filename: undefined,
       identifierName: undefined },
    range: undefined,
    leadingComments: undefined,
    trailingComments: undefined,
    innerComments: undefined,
    extra: undefined,
    expression:
     Node {
       type: 'CallExpression',
       start: 39,
       end: 67,
       loc: [SourceLocation],
       range: undefined,
       leadingComments: undefined,
       trailingComments: undefined,
       innerComments: undefined,
       extra: undefined,
       callee: [Node],
       arguments: [Array] } } ]

注:我们通过观察得知,我们的代码import message from './message.js'对应的就是type为:ImportDeclaration那个部分,我们需要我们import文件的路径
3. 我们使用@babel/traverse来帮我们更加简便的获取文件路径。
4. 将获取的路径以{相对路径:绝对路径}的形式存储到dependencies这个对象中
5. 然后使用@babel/core和@babel/preset-env这两个插件来帮助我们将es6的代码转变成es5的代码
6. 然后就返回文件路径、依赖、转变的代码出去。

2.编写makeDependenciesGraph函数:

const makeDependenciesGraph = function (entry) {
  // 用来存储每一个模块的分析结果
  const graphArray = [];
  // 将所有的模块的分析结果转成键值对的形式进行存储
  const graph = {};
  // 存储第一个(入口)模块的分析结果
  graphArray.push(moduleAnalyzer(entry));
  // 使用队列的数据结构来递归分析每一个模块
  for (let i = 0; i < graphArray.length; i++) {
    const dependencies = graphArray[i].dependencies;
    if (dependencies) {
      for (let key in dependencies) {
        graphArray.push(moduleAnalyzer(dependencies[key]));
      }
    }
  }
  // 将所有的分析结果以对象的形式存储
  graphArray.forEach((item) => {
    graph[item.file] = {
      dependencies: item.dependencies,
      code: item.code,
    };
  });
  return graph;
};
const graph = makeDependenciesGraph('./src/index.js');
console.log(graph);
  1. 我们moduleAnalyzer函数只能对一个模块进行分析,我们得使用队列来递归每一个模块
  2. 最后将graph的每一项转成对象的形式:{文件名:{dependencies: 依赖,code: 代码}}

3.generatorCode函数:

const generatorCode = function (entry) {
  const graph = JSON.stringify(makeDependenciesGraph(entry));
  return `
    (function (graph) {
      function require(path) {
        const exports = {}
        function localRequire(relativePath) {
          return require(graph[path].dependencies[relativePath])
        }
        ((code,exports,require)=>{
          eval(code)
        })(graph[path].code,exports,localRequire)
        return exports
      }
      require('${entry}')
    })(${graph})
  `;
};

注释:generatorCode这个函数,对于js基础要求比较高。

  1. 代码其实就是字符串,所以我们generatorCode返回值就是字符串
  2. 为了防止全局变量的污染,我们会经常使用闭包。
  3. 由于我们需要将graph传给代码里面,所以我们需要将graph转成JSON字符串,不然在字符串中,graph就是[object Object]这样的字符串
  4. require方法,就是根据文件的路径,来执行文件里的代码,然后返回exports变量。require就是我们在代码写的import。
  5. require('${entry}'):我们想要入口文件执行,我们需要手动的调用require方法,至于为什么要在${entry}这里加单引号,是因为entry是一个字符串变量。而${graph}不用加,是因为graph是一个对象。
  6. 我们需要在require方法中申明exports变量,它是一个空对象来的,最后需要将exports中进行return出去。
  7. 由于用babel生成的代码中,他有调用require方法,而浏览器没有require方法,所以我们需要自己手写。
  8. 由于babel生成的代码中,它有调用require方法,但是他传进来的文件路径是相对于引用这个文件的相对路径,所以我们需要重写require方法,编写一个localRequire方法,然后我们将localRequire和exports等等传进去自调用函数,用require形参来接收localRequire方法,从而在里面改写require方法。

babel生成的代码,里面有require、exports,浏览器没有,所以我们需要自己手写:
在这里插入图片描述

完整的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 moduleAnalyzer(entry) {
  const content = fs.readFileSync(entry, 'utf8');
  const dependencies = {};
  // 抽象语法树
  const ast = parser.parse(content, {
    sourceType: 'module',
  });
  traverse(ast, {
    ImportDeclaration({ node }) {
      const dirname = path.dirname(entry);
      const newFile = path.join(dirname, node.source.value);
      dependencies[node.source.value] = newFile;
    },
  });
  const { code } = babel.transformFromAst(ast, null, {
    presets: ['@babel/preset-env'],
  });
  return {
    file: entry,
    dependencies,
    code,
  };
}
const makeDependenciesGraph = function (entry) {
  const graphArray = [];
  const graph = {};
  graphArray.push(moduleAnalyzer(entry));
  for (let i = 0; i < graphArray.length; i++) {
    const dependencies = graphArray[i].dependencies;
    if (dependencies) {
      for (let key in dependencies) {
        graphArray.push(moduleAnalyzer(dependencies[key]));
      }
    }
  }
  graphArray.forEach((item) => {
    graph[item.file] = {
      dependencies: item.dependencies,
      code: item.code,
    };
  });
  return graph;
};
const generatorCode = function (entry) {
  const graph = JSON.stringify(makeDependenciesGraph(entry));
  return `
    (function (graph) {
      function require(path) {
        const exports = {}
        function localRequire(relativePath) {
          return require(graph[path].dependencies[relativePath])
        }
        ((code,exports,require)=>{
          eval(code)
        })(graph[path].code,exports,localRequire)
        return exports
      }
      require('${entry}')
    })(${graph})
  `;
};
const code = generatorCode('./src/index.js');
console.log(code);

通过命令来进行打包:

node bundle.js

将控制台的代码复制到浏览器去控制台运行:
在这里插入图片描述
输出正确,那就打包成功了。

总结思路:
我们想要分析一个文件,就得读取文件的内容,读取到之后,用对应的babel来帮我们处理和获取我们想要的信息。
由于一个模块可能会包含另一个模块,我们就得对这些模块递归的分析,我们这里使用了队列的方式来递归调用,然后把分析的结构弄成图的数据结构。
最后生成代码,由于不想污染全局变量,我们就得编写自调用闭包函数,由于生成的代码会调用require方法和使用exports这个变量,所以需要我们手工的创建。由于在一个模块中,它会require(相对路径),我们就得改写require方法,通过编写localRequire方法,传进去,用require形参接收,这样就不会调用外层得require方法了。然后根据相对路径弄成绝对路径,再调用最外层得require方法。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值