手写一个Bundle

277 阅读1分钟

代码会涉及到一些es6的转换,要安装一些相关的依赖

npm install @babel/parser @babel/traverse @babel/core @babel/preset-env -D

新建一个bundle文件夹,在文件夹下在创建一个src目录和一个bundle.js文件,src目录下分别创建一个index.js、message.js、word.js文件。代码如下:

// word.js
export const word = 'chenshun'

// message.js
import { word } from './word.js';const message = `hello ${word}`export default message;

// index.js
import message from './message.js'console.log(message)

以下是bundle的编写

首先要读取文件信息,导入相关的依赖包

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

文件信息必须包含文件名,该文件依赖的文件以及经bable转化后可以在浏览器执行的代码

function readFile(filename){  
  // 读入文件
  const content =  fs.readFileSync(filename, 'utf-8')
  const ast = parser.parse(content, {
      sourceType: 'module' // 为了识别ES Module
  })
  const dependencies = {}  
  // 遍历AST抽象语法树  traverse(ast, {
      // 获取通过import引入的模块
      ImportDeclaration({node}){
          const dirname = path.dirname(filename)
          const newFile = './' + path.join(dirname, node.source.value)
          // 保存所依赖的模块
          dependencies[node.source.value] = newFile
      }
  })
  // 通过@babel/core和@babel/preset-env进行代码的转换
  const {code} = babel.transformFromAst(ast, null, {
      presets: ["@babel/preset-env"]
  })
  return{
      filename,
      dependencies, // 该文件所依赖的模块集合(键值对存储)
      code // 转化后的代码
  }
}

生成依赖图谱

function generateDependentGraph(entry){
  const entryModule = readFile(entry)  
// 开始数组只有一个元素
  const graphArray = [entryModule]
  for(let i = 0; i < graphArray.length; i++){
      const item = graphArray[i];
      const { dependencies } = item;
       for(let j in dependencies){ // 把文件所有的依赖保存进数组
          graphArray.push(
              readFile(dependencies[j])
          )
      }
  }
  // 以健值对的形式存储依赖图谱
  const graph = {}
  graphArray.forEach(item => {
      graph[item.filename] = {
          dependencies: item.dependencies,
          code: item.code
      }
  })
  return graph
}



console.log(generateDependentGraph('./src/index.js'));
// 生成的依赖图谱如下:
{
  './src/index.js': {
    dependencies: { './message.js': './src/message.js' },
    code: '"use strict";\n' +
      '\n' +
      'var _message = _interopRequireDefault(require("./message.js"));\n' +
      '\n' +
      'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
      '\n' +
      '//index.js\n' +
      'console.log(_message["default"]);'
  },
  './src/message.js': {
    dependencies: { './word.js': './src/word.js' },
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports["default"] = void 0;\n' +
      '\n' +
      'var _word = require("./word.js");\n' +
      '\n' +
      '//message.js\n' +
      'var message = "say ".concat(_word.word);\n' +
      'var _default = message;\n' +
      'exports["default"] = _default;'
  },
  './src/word.js': {
    dependencies: {},
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports.word = void 0;\n' +
      '//word.js\n' +
      "var word = 'hello';\n" +
      'exports.word = word;'
  }
}

生成可以在浏览器执行的代码,这段代码会比较绕

function generateCode(entry){
  const graph = JSON.stringify(generateDependentGraph(entry))
  // 为了避免污染全局环境,必须以闭包的形式返回一段可以立即执行的代码,这里返回一个立即执行函数
  return `
    (function(graph) {
      //require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
      function require(module) {
        // 每次require一个文件都必须传入的是绝对路径,
        // 找到文件的绝对路径后以递归的形式再次调用require函数并传入改路径
        // 同时必须在新的函数作用域中重新定义require函数
        function localRequire(relativePath) {
          return require(graph[module].dependencies[relativePath]);
        }
        var exports = {}; // 不同模块之间实际上是以exports来进行通信的
        // 转化后的代码会调用require函数,浏览器不能识别,必须自己定义
        (function(require, exports, code) {
          eval(code);
        })(localRequire, exports, graph[module].code);
        // 下一个函数会引用exports变量,通过改变exports变量值的形式继续往下传递值,
        // 达到不同模块进行通信的目的
        return exports;
      }
      require('${entry}')
    })(${graph})`
}

测试

const code = generateCode('./src/index.js')console.log(code)
// 生成的代码如下
(function(graph) {
      //require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
      function require(module) {
        // 每次require一个文件都必须传入的是绝对路径,
        // 找到文件的绝对路径后以递归的形式再次调用require函数并传入改路径
        // 同时必须在新的函数作用域中重新定义require函数,目的是
        //拿到当前执行文件所依赖的文件的绝对路径
        function localRequire(relativePath) {
          return require(graph[module].dependencies[relativePath]);
        }

        var exports = {}; // 不同模块之间实际上是以exports来进行通信的

        // 转化后的代码会调用require函数,浏览器不能识别,必须自己定义
        (function(require, exports, code) {
          eval(code);
        })(localRequire, exports, graph[module].code);

        // 下一个函数会引用exports变量,通过改变exports变量值的形式继续往下传递值,
        // 达到不同模块进行通信的目的
        return exports;
      }
      require('./src/index.js')
    })({"./src/index.js":{"dependencies":{"./message.js":"./src/message.js"},"code":"\"use strict\";\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\n//index.js\nconsole.log(_message[\"default\"]);"},"./src/message.js":{"dependencies":{"./word.js":"./src/word.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nvar _word = require(\"./word.js\");\n\nvar message = \"hello \".concat(_word.word);\nvar _default = message;\nexports[\"default\"] = _default;"},"./src/word.js":{"dependencies":{},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports.word = void 0;\n// word.js\nvar word = 'chenshun';\nexports.word = word;"}})

放在浏览器执行

大功告成

参考教程:手把手带你掌握webpack4.0