[ Webpack ] 实现一个 mini 版的 webpack

2021-05-30 10:20:42 浏览数 (1)

虽然随着 Vite 的发布和加上 Vue 团队的大力推广,使得 Vite 构建工具在社区内或者是业内慢慢地火了起来,甚至有一些开发者或者公众号已经大力吹嘘使用 Vite 进行了线上项目重构,就这样把 迭代了 5 个版本的 webpack 给扔了?而我就想起了那句话 “ 人有多大胆,地有多大产 ”。当然这是另一个话题了。

Vite 和 Webpack

Vite 的发布是不是意味着 webpack 的终结?当然不是, webpack 存在这么多年是解决了不少奇奇怪怪的问题而且也适合处理那些深度复杂的场景,这一点 Vite 肯定是还有些距离的,而且尤雨溪在前不久的直播中针对 Vite 做了解释,他说到 Vite 的设计初衷就是为了改善开发时的反馈速度,是改善体验而不是干掉 webpack 。

虽然以后会发生什么变化是不知道,但是,我觉得至少了解一下 webpack 大致做了什么工作也是有必要知道的,这一次就写一个简单的 webpack。

mini webpack 实现

创建基本目录结构

首先是和 webpack 一样,创建一个 mini.webpack.config.js 文件。

在 mini.webpack.config.js 文件中配置这个项目最基本的入口文件地址和输出文件地址,再使用 module.exports 导出。

代码语言:txt复制
const path = require('path');
module.exports = {
    entry: path.join(__dirname, './pages/index.js'),
    output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js'
    }
};

同时,创建 pages 目录和 lib 目录。pages 目录用于存放源码,lib 目录用于存放 mini-webpack 的构建文件。

lib 目录下,index.js 是 mini webpack 的入口文件,utils 文件主要负责编写模块功能解析ES6转ES5的功能,将代码解析成 AST 语法树和解析 AST 语法书转化为源代码的功能。compiler 文件主要是进行模块引用构建和输出逻辑。

dist 目录里面的 bundle 文件就是构建出来的文件。

pages 文件夹中 index.js 是模拟项目的入口文件,detail.js 和 search.js 是模拟在 index.js 中引用的各种模块。

初始化mini webpack

引用 mini webpack 的配置文件和初始化执行文件

代码语言:txt复制
const opts = require('../mini.webpack.config');
const Compiler = require('./compiler');
new Compiler(opts).init();

编写 utils 模块文件

utils 会导出多个模块函数,包括源码转化 AST 语法树模块,分析文件依赖模块,将 AST 转化为源码模块。这里我为了更加直观每个模块的引用,所以我就分开独立编写,具体使用的时候把重复的引用和 exports 去掉即可。

源码转化 AST 语法树模块:

引用 babylon ,使用 babylon的 parse 方法进行源码解析转化为 AST 语法树。

代码语言:txt复制
const babylon = require('babylon');
const fs = require('fs');
module.exports = {
  transformAST: (path) => {
    const source = fs.readFileSync(path, 'utf-8');
      return babylon.parse(source, {
        sourceType: 'module',
    });
  },
}
// babylon 文档地址: https://www.npmjs.com/package/babylon
对 AST 语法树分析依赖模块:

将所有的依赖 push 到一个数组里面,再一次性返回。

代码语言:txt复制
const traverse = require('babel-traverse').default;
module.exports = {
  analyseDependencies: (AST) => {
    const dependencies = [];
      traverse(AST, {
          ImportDeclaration: ({ node }) => {
            dependencies.push(node.source.value);
        }
      });
    return dependencies;
  },
}
// babel-traverse 文档地址: https://www.npmjs.com/package/babel-traverse
将 AST 语法树转化为源码模块:

这里需要引用 babel-core 模块,同时配置 preset 为 env。这样 es5,es6都可以解析。

代码语言:txt复制
const { transformFromAst } = require('babel-core');
const fs = require('fs');
module.exports = {
    transformFormatAST: (AST) => {
        const { code } = transformFromAst(AST, null, {
            presets: ["env"]
        });
        return code;
    },
}
// babel-core 文档地址: https://www.npmjs.com/package/babel-core

编写 compiler 模块文件

初始化模块:

写一个 compiler 的类,将刚刚 utils 的模块都引用进来,同时把基本的 fs 和 path 也进行引入。

代码语言:txt复制
const path = require('path');
const fs = require('fs');
const { transformAST, analyseDependencies, transformFormatAST } = require('./utils');
module.exports = class Compiler {
    constructor(options) {
        const { entry, output } = options;
        this.entry = entry;
        this.output = output;
        this.modules = [];
    }
    init() {
        const entryModule = this.buildModules(this.entry, true);
        this.modules.push(entryModule);
        this.modules.map((_module, index) => {
            _module.dependencies.map((dependency) => {
                this.modules.push(this.buildModules(dependency));
            });
        });
        this.emitFiles()
    }
    miniBuildModules() {}
    miniEmitFiles() {}
}

初始化时,传入入口和输出文件地址。在构建完毕之后,就是 miniEmitFiles 输出代码。

构建模块:

这里的就是分析文件中的所以依赖文件,分别进行打包构建。只是这个地方需要做个判断处理,因为,和入口文件不一样,入口文件就是绝对地址,而依赖文件都是相对地址,这里就需要判断如果是相对地址的话,就将它转化为绝对地址。

代码语言:txt复制
miniBuildModules(filename, isEntry) {
  let ast;
  if (isEntry) {
  	ast = transformAST(filename);
  };
  if (!isEntry) {
  	const absolutePath = path.join(process.cwd(), './pages', filename);
  	ast = transformAST(absolutePath);
  }
  return {
  	filename,
  	dependencies: analyseDependencies(ast),
  	source: transformFormatAST(ast),
  }
}

每一个依赖文件导出的就是三个参数,文件名·,依赖文件参数和源文件代码。

导出模块:

这个先用一个简单的例子说明,最后在写构建的代码

第一步需要导出成这样的一个形式,

代码语言:txt复制
(function (modules) {
  function require(filename) {
    var func = modules[filename];
    func(require)
  }
  require('1')
})({
  '1': function (require) {
    console.log('1')
  }, 
  '2': function (require) {
    console.log('2')
  }, 
  '3': function (require) {
    console.log('3')
  },
});
// console.log 1

modules 接受了一个对象带有 3 个属性,已经能获取到对象 1 了,但是,2 和 3 是没有获取到的,所以,需要在 1 中 执行 2 和 3 ,这就相当于 1 就是一个入口引用了 2和3,如下:

代码语言:txt复制
(function (modules) {
  function require(filename) {
    var func = modules[filename];
    func(require)
  }
  require('1')
})({
  '1': function (require) {
   var _detail = require("2");
   var _search = require("3");
    console.log('1')
  }, 
  '2': function (require) {
    console.log('2')
  }, 
  '3': function (require) {
    console.log('3')
  },
});
// console.log 1
// console.log 2
// console.log 3

这里仅仅只有引用还不够,需要 exports 一个模块,所以,改成

代码语言:txt复制
(function (modules) {
  function require(filename) {
    var func = modules[filename];
    var module = { exports: {} };
    func(require, modules, module.exports);
    return module.exports;
  }
  require('1')
})({
  '1': function (require) {
    var _detail = require("2");
    // var _search = require("3");
    console.log(_detail.detail('export:1'));
  }, 
  '2': function (require, modules,exports) {
    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports.detail = detail;
    function detail(name) {
      return "next:"   name;
    }
  }, 
   //'3': function (require) {
   //  console.log('3')
   //},
})

依赖函数一部分可以用拼接的形式,将已经分析好的所有依赖函数拼接。关键是前面的函数体,可以用字符串模版拼接完成,代码如下:

代码语言:txt复制
miniEmitFiles() {
	const outputPath = path.join(this.output.path, this.output.filename);

	let modules = '';
	this.modules.map((_module) => {
	modules  = `'${_module.filename}': function(require,modules,exports){${_module.source}},`
})
	const bundle = `(function(modules){
                  function require(filename){
                  var func = modules[filename];
                  var module = { exports : {} };
                  func(require ,module ,module.exports)
                  return module.exports;
                  }
                  require('${this.entry}')
                  })({${modules}})`;
	fs.writeFileSync(outputPath, bundle, 'utf-8');
}

modules 是将 this.modules 已经输出的所有依赖文件,以一键对值的形式拼接起来。

require 函数是将一键对值的执行函数封装,只要依赖文件 modules 里面有的他都会走这个返回进行 exports 函数。

构建出来的文件

代码语言:txt复制
(function (modules) {
  function require(filename) {
    var func = modules[filename];
    var module = { exports: {} };
    func(require, module, module.exports)
    return module.exports;
  }
  
  require('/${省略,这里为本机地址}/mini-webpack/pages/index.js')
})({
  '/${省略,这里为本机地址}/mini-webpack/pages/index.js': function (require, modules, exports) {
    "use strict";
    var _detail = require("./detail.js");

    var _search = require("./search.js");

    document.write((0, _detail.detail)('id=5'));
    (0, _search.search)('done');
  }, './detail.js': function (require, modules, exports) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports.detail = detail;
    function detail(name) {
      return "next:"   name;
    }
  }, './search.js': function (require, modules, exports) {
    "use strict";

    Object.defineProperty(exports, "__esModule", {
      value: true
    });
    exports.search = search;
    function search(name) {
      return alert(name);
    }
  },
});

到这里就完成了一个迷你版的 webpack 。

0 人点赞