实现一个简易的 webpack

先来看下用webpack打包后的文件,

webpack.config.js

const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

在src文件夹中有greeting.js  和 index.js 两个文件:

greeting.js

export function greeting (name) {
  return "hello " + name;
}

 index.js

import { greeting } from './greeting.js';

document.write(greeting('world!'))

 执行webpack打包后生成的main.js


 (() => { // webpackBootstrap
 	"use strict";
 	var __webpack_modules__ = ({

 "./src/greeting.js":
 ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */   \"greeting\": () => (/* binding */ greeting)\n/* harmony export */ });\nfunction greeting (name) {\r\n  return \"hello \" + name;\r\n}\n\n//# sourceURL=webpack://demo01/./src/greeting.js?");

 }),

 "./src/index.js":
 ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {

eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _greeting_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./greeting.js */ \"./src/greeting.js\");\n\r\n\r\ndocument.write((0,_greeting_js__WEBPACK_IMPORTED_MODULE_0__.greeting)('world!'))\n\n//# sourceURL=webpack://demo01/./src/index.js?");

 })

 	});
 	// The module cache
 	var __webpack_module_cache__ = {};
 	
 	// The require function
 	function __webpack_require__(moduleId) {
 		// Check if module is in cache
 		var cachedModule = __webpack_module_cache__[moduleId];
 		if (cachedModule !== undefined) {
 			return cachedModule.exports;
 		}
 		// Create a new module (and put it into the cache)
 		var module = __webpack_module_cache__[moduleId] = {
 			// no module.id needed
 			// no module.loaded needed
 			exports: {}
 		};
 	
 		// Execute the module function
 		__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
 	
 		// Return the exports of the module
 		return module.exports;
 	}
 	
 	/* webpack/runtime/define property getters */
 	(() => {
 		// define getter functions for harmony exports
 		__webpack_require__.d = (exports, definition) => {
 			for(var key in definition) {
 				if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
 					Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
 				}
 			}
 		};
 	})();
 	
 	/* webpack/runtime/hasOwnProperty shorthand */
 	(() => {
 		__webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
 	})();
 	
 	/* webpack/runtime/make namespace object */
 	(() => {
 		// define __esModule on exports
 		__webpack_require__.r = (exports) => {
 			if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
 				Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
 			}
 			Object.defineProperty(exports, '__esModule', { value: true });
 		};
 	})();
 	
/************************************************************************/
 	
 	// startup
 	// Load entry module and return exports
 	// This entry module can't be inlined because the eval devtool is used.
 	var __webpack_exports__ = __webpack_require__("./src/index.js");
 	
 })();

可以看出经过webpack打包出来的文件是一个匿名闭包函数, 该函数里边只有三个变量和一个函数方法:

__webpack_modules__存放了编译后的各个文件模块的JS内容;

__webpack_module_cache__ 用来做模块缓存;

__webpack_require__Webpack内部实现的一套依赖引入函数(我们在模块化开发的时候,通常会使用ES Module或者CommonJS规范导出/引入依赖模块,webpack打包编译的时候,会统一替换成自己的__webpack_require__来实现模块的引入和导出,从而实现模块缓存机制,以及抹平不同模块规范之间的一些差异性。)。最后一句则是代码运行的起点,从入口文件开始,启动整个项目。

__webpack_require__ 函数内部会优先从缓存对象中获取 moduleId 对应的模块,若该模块已存在,就会返回该模块对象上 exports 属性的值。如果缓存对象中不存在 moduleId 对应的模块,则会创建一个包含 exports 属性的 module 对象,然后会根据 moduleId__webpack_modules__ 对象中,获取对应的函数并使用相应的参数进行调用,最终返回 module.exports 的值。

准备工作

执行npm init -y初始化一个项目,并新建以下文件:

|-- simplepack
    |-- dist
    |   |-- bundle.js
    |   |-- index.html
    |-- lib
    |   |-- compiler.js
    |   |-- index.js
    |   |-- parser.js
    |   |-- test.js
    |-- src
    |   |-- greeting.js
    |   |-- index.js
    |-- .babelrc
    |-- simplepack.config.js
    |-- package.json

文件及文件夹说明:

  • dist : 打包目录
  • lib : 核心文件, 其中compiler.js , parse.js
  • src: 源代码;
  • .babelrc:babel的配置
  • simplepack.config.js : 配置文件,类似于webpack.config.js;
  • package.json

src中两个文件内容还是使用上面的greeting.js 和 index.js

simplepack.config.js配置文件内容:

const path = require('path');
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'main.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

接着编写compiler.js,

const path = require("path");
const fs = require("fs");

module.exports = class Compiler {
  constructor(options) {
    const { entry, output } = options;
    this.entry = entry;
    this.output = output;
    this.modules = [];
  }
  // 开启编译
  run () {
  }
  /**
   * 构建模块相关
  */
  buildModule (filename, isEntry) {
  }
  // 输出文件
  emitFiles () {
  }
};

compile.js主要做了几个事情:

1. 读取config.js 配置参数,并初始化entry、output

2. 开启编译run方法,处理构建模块、收集依赖、输出文件等;

3. buildModule方法,用于构建模块;

4. emitFiles方法,输出文件

parse.js主要做了几个事情:

1. 需要将源文件解析生成AST(利用babylon模块);

2. 对AST节点进行递归遍历得到依赖(利用babel-traverse模块);

3. 将AST重新生成源码(利用babel-core模块)

所以我们需要先执行安装这几个依赖npm i babylon babel-traverse babel-core babel-preset-env -S

babylon

babel中使用的 JavaScript 解析器

babel-traverse

对ast进行遍历的工具

babel-core

可以看做 babel 的编译器。babel 的核心 api 都在这里面,比如 transform,主要都是处理转码的。它会把我们的 js 代码,转化成抽象语法树AST(源代码的抽象语法结构的树状表现形式)。最后转化成 es5

babel-preset-env

babel预设

const fs = require('fs');
const babylon = require('babylon');
const traverse = require('babel-traverse').default;
const { transformFromAst } = require('babel-core');

module.exports = {
  // 生成AST
  getAST: (path) => {
    // 读取文件内容
    const source = fs.readFileSync(path, 'utf-8');

    return babylon.parse(source, {
      sourceType: 'module'
    })
  },
  // 对AST节点进行递归遍历, 获取模块的依赖属性
  getDependencies: (ast) => {
    const dependencies = [];
    traverse(ast, {
      // 类型为 ImportDeclaration 的 AST 节点 (即为import 语句)
      ImportDeclaration: ({ node }) => {
        dependencies.push(node.source.value)
      }
    });
    return dependencies;
  },
  // 将获得的ES6的AST转化成ES5
  transform: (ast) => {
    const { code } = transformFromAst(ast, null, {
      presets: ['env']
    });
    return code;
  }
}

还得配置.babelrc文件

{
    "presets": [
        "@babel/preset-env"
    ]
}

接着我们写个test.js来测试下:

const path = require('path');
const { getAST, getDependencies, transform } = require('./parse');

const ast = getAST(path.join(__dirname, '../src/index.js'))
console.log(ast);

const dependencies = getDependencies(ast);
console.log(dependencies);

const source = transform(ast);
console.log(source)

执行node lib/test.js 得到以下结果(截图不全):

 完善compiler.js:  

const { getAST, getDependencies, transform } = require("./parse");
const path = require("path");
const fs = require("fs");

module.exports = class Compiler {
  constructor(options) {
    const { entry, output } = options;
    this.entry = entry;
    this.output = output;
    this.modules = [];
  }
  // 开启编译
  run () {
    const entryModule = this.buildModule(this.entry, true);
    this.modules.push(entryModule);
    this.modules.map((_module) => {
      _module.dependencies.forEach((dependency) => {
        this.modules.push(this.buildModule(dependency));
      });
    });
    // console.log(this.modules);
    this.emitFiles();
  }
  /**
   * 构建模块相关
   * @param filename 文件名称
   * @param isEntry 是否是入口文件
  */
  buildModule (filename, isEntry) {
    let ast;
    if (isEntry) {
      ast = getAST(filename);
    } else {
      const absolutePath = path.join(process.cwd(), "./src", filename);
      ast = getAST(absolutePath);
    }

    return {
      filename, // 文件名称
      dependencies: getDependencies(ast), // 依赖列表
      transformCode: transform(ast), // 转化后的代码
    };
  }
  // 输出文件
  emitFiles () {
    const outputPath = path.join(this.output.path, this.output.filename);
    let modules = [];
    this.modules.forEach((_module) => {
      modules.push(`'${_module.filename}' : function(require, module, exports) {${_module.transformCode}}`);
    });
    modules = modules.join(',')

    const bundle = `
        (function(modules) {
          function require(fileName) {
            const fn = modules[fileName];
            const module = { exports:{}};
            fn(require, module, module.exports)
            return module.exports
          }
          require('${this.entry}')
        })({${modules}})
    `;

    fs.writeFileSync(outputPath, bundle, "utf-8");
  }
};

lib/index.js: 

const Compiler = require('./compiler');
const options = require('../simplepack.config');

new Compiler(options).run();

 执行node lib/index.js


(function (modules) {
  function require (fileName) {
    const fn = modules[fileName];
    const module = { exports: {} };
    fn(require, module, module.exports)
    return module.exports
  }
  require('./src/index.js')
})({
  './src/index.js': function (require, module, exports) {
    "use strict";

    var _greeting = require("./greeting.js");

    document.write((0, _greeting.greeting)('world!'));
  }, './greeting.js': function (require, module, exports) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports.greeting = greeting;
    function greeting (name) {
      return 'hello ' + name;
    }
  }
})

完整代码地址simplepack     

webpack系列:

webapck5基础

webpack5入门

参考资料:

玩转webpack

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值