写一个简单的webpack打包工具

1.搭建项目基础骨架

我们在项目中新建一个bin文件夹,在bin目录下创建start.js文件,这个文件的主要作用就是将webpack的基础配置传入编译模块(compiler),然后开启编译过程。

// bin/start.js
const path = require('path') 
//  1. 读取需要打包项目的配置文件
let config = require(path.resolve('webpack.config.js'))
// 核心文件 编译器
const Compiler = require('../lib/compiler')
// 将配置传入编译器,实例化编译器
let compiler = new Compiler(config)
// 开启编译器
compiler.start()

2.编译器文件

编译器文件是核心文件,主要包含的内容为三部分
1)读取文件、转换代码

depAnalyse(filename) {
    // 读取模块的内容
    let content = fs.readFileSync(filename, "utf-8");
    // 用于存取当前模块的所有依赖。便于后面遍历
    let dependencies = {};
    // 对文件内容进行解析并生成初始的抽象语法树
    const ast = parser.parse(content, {
      sourceType: "module", //babel官方规定必须加这个参数,不然无法识别ES Module
    });
    // 遍历ast 通过import找到依赖,存储依赖
    traverse(ast, {
      ImportDeclaration({ node }) {
        // 去掉文件名 返回目录
        const dirname = path.dirname(filename);
        const newFile = path.join(dirname, node.source.value);
        //保存所依赖的模块
        dependencies[node.source.value] = newFile;
      },
    });
    // transformFromAst 相当于是traverse和generate的结合
    const { code } = babel.transformFromAst(ast, null, {
      presets: ["@babel/preset-env"],
    });
    // generate 将ast转换为代码
    // 把当前的依赖,和文件内容推到对象里面去
    return {
      filename,
      dependencies, //该文件所依赖的模块集合(键值对存储)
      code, //转换后的代码
    };
  }

上面这个函数做了几件事:

  • 读取模块内容
  • 用@babel/parser将ES6代码转换为ES6的抽象语法树(AST)
  • 利用@babel/traverse遍历AST,找到本模块的其他依赖文件并保存
  • 用babel.transformFromAst将ES6的AST转换成ES5的AST,并将AST转换成代码。

2)得到图谱

getAtlas(entry) {
    const entryModule = this.depAnalyse(entry);
    this.analyseObj = [entryModule];
    for (let i = 0; i < this.analyseObj.length; i++) {
      const item = this.analyseObj[i];
      const { dependencies } = item; //拿到文件所依赖的模块集合(键值对存储)
      for (let j in dependencies) {
        this.analyseObj.push(this.depAnalyse(dependencies[j])); //敲黑板!关键代码,目的是将入口模块及其所有相关的模块放入数组
      }
    }
    //接下来生成图谱
    const graph = {};
    this.analyseObj.forEach((item) => {
      graph[item.filename] = {
        dependencies: item.dependencies,
        code: item.code,
      };
    });
    return graph;
  }

上述函数做了几件事:

  • 递归调用depAnalyse函数得到所有模块的依赖数组
  • 依次对数组中的每一个元素都进行转换
  • 得到图谱
    最后的图谱结果为:
{
  './src/index.js': {
    dependencies: { './message.js': 'src/message.js' },
    code: '"use strict";\n' +
      '\n' +
      'Object.defineProperty(exports, "__esModule", {\n' +
      '  value: true\n' +
      '});\n' +
      'exports["default"] = void 0;\n' +
      '\n' +
      'var _message = _interopRequireDefault(require("./message.js"));\n' +
      '\n' +
      'function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }\n' +
      '\n' +
      'var _default = _message["default"];\n' +
      'exports["default"] = _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' +
      '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' +
      "var word = 'hello';\n" +
      'exports.word = word;'
  }
}

3)生成webpack模版文件
上面的函数中,我们已经生成了各个文件的依赖文件及其代码,那我们要如何将这几个文件结合起来,使其输出我们想要的东西呢?
我们先来研究一下webpack打包输出的代码(精简):

(function(modules) {

  function __webpack_require__(moduleId) {
    var module =  {
      i: moduleId,
      l: false,
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    return module.exports;
  }

  return __webpack_require__(0);
})([
  (function (module, __webpack_exports__, __webpack_require__) {

    // 引用 模块 1
    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__c__ = __webpack_require__(1);

/* harmony default export */ __webpack_exports__["default"] = ('a.js');
console.log(__WEBPACK_IMPORTED_MODULE_0__c__["a" /* default */]);

  }),
  (function (module, __webpack_exports__, __webpack_require__) {

    // 输出本模块的数据
    "use strict";
    /* harmony default export */ __webpack_exports__["a"] = (333);
  })
]);

这个文件从整体来看,其实是一个自执行函数。

(function(modules) {

})([]);

自执行函数的入参是经过babel转换后的每一个模块的代码组成的数组,而函数的主体是webpack用来处理模块的逻辑的,作用就是执行每一个模块(module)中的代码,并将代码中导出的方法存放在module.exports中,并在模块的最后将module.exports返回,供其他模块调用。
本质上是webpack实现了自己的require函数。

按照上述webpack打包输出的代码规范,我们写了自己的代码输出模版如下:

toEmitCode(entry, graph) {
    //要先把对象转换为字符串,不然在下面的模板字符串中会默认调取对象的toString方法,参数变成[Object object],显然不行
    graph = JSON.stringify(graph);
    
    return `
        (function(graph) {
            //require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
            function require(module) {
                //localRequire的本质是拿到依赖包的exports变量
                function localRequire(relativePath) {
                    return require(graph[module].dependencies[relativePath]);
                }
                var exports = {};
                (function(require, exports, code) {
                    eval(code);
                })(localRequire, exports, graph[module].code);
                return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
            }
            return require('${entry}')
        })(${graph})`;
  }

上述函数做了几件事:

  • 执行每一个模块的代码,并将其导出的内容放在exports,便于其他模块通过require引用
  • 将入口文件的导出exports返回作为整个函数的返回值
  • 由于转换输出的代码是commonjs规范的,不能在浏览器上运行,webpack实现的自执行require函数可以帮助输出代码在浏览器上运行

3.进行打包输出
在src文件夹下新建以下3个文件:

// index.js
import message from './message.js';

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

webpack.config.js配置文件如下:

// webpack.config.js
const path = require("path");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
  }
};

在命令行执行node bin/start.js,发现在dist文件夹下生成了一个main.js文件:

 (function(graph) {
        //require函数的本质是执行一个模块的代码,然后将相应变量挂载到exports对象上
        function require(module) {
            //localRequire的本质是拿到依赖包的exports变量
            function localRequire(relativePath) {
                return require(graph[module].dependencies[relativePath]);
            }
            var exports = {};
            (function(require, exports, code) {
                eval(code);
            })(localRequire, exports, graph[module].code);
            return exports;//函数返回指向局部变量,形成闭包,exports变量在函数执行后不会被摧毁
        }
        return require('./src/index.js')
    })({"./src/index.js":{"dependencies":{"./message.js":"src/message.js"},"code":"\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n  value: true\n});\nexports[\"default\"] = void 0;\n\nvar _message = _interopRequireDefault(require(\"./message.js\"));\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { \"default\": obj }; }\n\nvar _default = _message[\"default\"];\nexports[\"default\"] = _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 = \"say \".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;\nvar word = 'hello';\nexports.word = word;"}})

将以上代码在浏览器控制台执行之后,得到结果:
在这里插入图片描述

三.打包代码被引用

上面输出的代码当中虽然打包完成,但是这个js文件不能被其他模块通过import或者require的方式引用,因为它只是在当前作用域有效,但是如果想要它能被其他模块引用,这就要用到webpack中的output.libraryTarget。
output.libraryTarget :commonjs2;
会将打包过的代码赋值给module.exports。

可以在上述代码前面加上module.exports = 打包输出代码块
这样,就可以在别的模块利用require引用这个模块。

关于webpack模块化的文章点这里

源码地址点这里

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值