【新手向】如何写一个超超超简单的 webpack?

栏目: JavaScript · 发布时间: 5年前

内容简介:webpack 的定义和重要程度在此我不再赘述,直接进入正题,一个最最简单的 webpack 是如何实现的?我们从 webpack 的打包过程来进行分析首先创建两个项目,一个叫每个文件对应内容如下:

webpack 的定义和重要程度在此我不再赘述,直接进入正题,一个最最简单的 webpack 是如何实现的?我们从 webpack 的打包过程来进行分析

一. 准备活动

首先创建两个项目,一个叫 webpack-project ,另一个叫 mn-webpack 。前者用于观察打包结果,后者用于简单实现 webpack。 项目建立好之后,首先进入到 webpack-project 中,新建一个 src 文件夹和 webpack.config.js 文件, src 下具有 index.jsa.js 两个文件以及一个 base 文件夹, base 文件夹下有 b.js 一个文件。结构如下:

|--src
     |--index.js
     |--a.js
     |--base
           |--b.js
|--webpack.config.js
复制代码

每个文件对应内容如下:

// b.js
module.exports = 'b'

// a.js
const b = require('./base/b.js')
module.exports = 'a' + b

// index.js
const str = require('./a.js')
console.log(str)
复制代码
// webpack.config.js
const path = require('path')
module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  mode: 'development'
}
复制代码

到此,准备活动就结束了

二. webpack 编写

设想一下,当我们在 webpack-project 完成上述配置并执行 npx webpack 之后会发生什么?答案是:会在目录下生成一个 dist 目录,其中包含打包结果 bundle.js ,运行 bundle.js 会在控制台中打印出 ab 字符串。如果你已经知道这其中的机制,那么恭喜你,这篇文章对你来说已经没什么用处了,再往下看也是浪费时间而已。如果你还不知道的话,接下来可要注意咯~

执行 npx wepack ,结果和上述一样。打开 bundle.js ,删掉一部分代码,剩下最关键的,在控制台中执行 bundle.js ,同样可以输出 ab 。删后的 bundle.js 如下所示:

(function(modules) { 
 	var installedModules = {};
 	function __webpack_require__(moduleId) {
 		if(installedModules[moduleId]) {
 			return installedModules[moduleId].exports;
 		}
 		var module = installedModules[moduleId] = {
 			i: moduleId,
 			l: false,
 			exports: {}
 		};
 		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
 		module.l = true;
 		return module.exports;
 	}
return __webpack_require__(__webpack_require__.s = "./src/index.js");
 })
 ({
 "./src/a.js":
 (function(module, exports, __webpack_require__) {
  eval("const b = __webpack_require__(/*! ./base/b.js */ \"./src/base/b.js\")\r\nmodule.exports = 'a' + b\n\n//# sourceURL=webpack:///./src/a.js?");
  }),
  "./src/base/b.js":
  (function(module, exports) {
  eval("module.exports = 'b'\n\n//# sourceURL=webpack:///./src/base/b.js?");
  }),
  "./src/index.js":
  (function(module, exports, __webpack_require__) {
  eval("const str = __webpack_require__(/*! ./a.js */ \"./src/a.js\")\r\nconsole.log(str)\n\n//# sourceURL=webpack:///./src/index.js?");
  })
});
复制代码

可以看到,我们整个项目中用到的文件地址和内容在 bundle.js 中都体现了出来。如何拿到这些文件呢?待会儿会讲。

切换至 mn-webpack 文件夹,在其下建立一个 bin 文件夹和 lib 文件夹, bin 文件夹下有 mn-webpack.js 文件, lib 文件夹下有 Compiler.jsmain.ejs 文件。结构如下:

|--bin
     |--mn-webpack.js
|--lib
     |--Compiler.js
     |--main.ejs
复制代码

mn-webpack.js

接着介绍一下每个文件的作用, mn-webpack.js 为整个项目的入口文件,我们需要在 package.json 文件中增加一个 bin 选项对该文件进行配置,如下所示:

"bin": {
  "mn-pack": "./bin/mn-pack.js"
}
复制代码

bin 用于放置用户的自定义命令。再在控制台中运行 npm link ,可将命令链接到全局中。这一步是为了可以在 webpack-project 中使用 mn-webpack 。使用方式也很简单,首先在 webpack-project 控制台下运行 npm link mn-webpack 建立连接,然后直接运行 npx mn-webpack 即可。对于该命令不太了解的朋友可以查看这篇文章。

mn-webpack.js 文件内容如下:

const path = require('path')
const Compiler = require('../lib/Compiler.js')

// 拿到配置文件
const config = require(path.resolve('webpack.config.js'))

// Compiler 为一个类,实例化后执行 run 方法开始编译
const compiler = new Compiler(config)
compiler.run()
复制代码

main.ejs

该文件为一个模板文件,我们将之前修改过的 bundle.js 内容复制进来。对其中的依赖部分进行修改:

(function(modules) { 
  var installedModules = {};
  function __webpack_require__(moduleId) {
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    module.l = true;
    return module.exports;
  }
  return __webpack_require__(__webpack_require__.s = "<%-entryId%>");
})
({
<%for(let key in modules){%>
  "<%-key%>":
  (function(module, exports, __webpack_require__) {
  eval(`<%-modules[key]%>`);
  }),
<%}%>
});
复制代码

entryIdmodules 通过其他文件进行传入,下面会介绍到。

Compiler.js

该文件最开始内容很简单,导出一个 Compiler 类:

class Compiler {
  constructor (config) {
    this.config = config
    this.entry = config.entry
    this.modules = {}
    this.root = process.cwd()
  }
  run () {
    // 从 entry 开始分析依赖
    this.buildModule()
    // 发射输出文件
    this.emitFile()
  }
  buildModule () {}
  emitFile () {}
}
复制代码

buildModule 方法需要接收 modulePath 作为一个参数,拿到对应的文件后对其中的依赖进行解析(这里的依赖就是我们 require 进来的东西)。这个步骤需要用到 babylon 生成 AST树 ,然后利用 @babel/traverse 遍历 AST树 并修改成我们想要的样子,最终使用 @babel/generator 生成代码返回。 AST树 的信息可以在 这里 进行查看

const types = require('@babel/types')
const babylon = require('babylon')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default

parse (source, parentPath) { 
    const ast = babylon.parse(source)
    const dependencies = []
    traverse(ast, {
      CallExpression(p) {
        const node = p.node
        if (node.callee.name === 'require') {
          // 将 require 改为 webpack 自定义的 __webpack_require__
          node.callee.name = '__webpack_require__'
          let moduleName = node.arguments[0].value
          moduleName = moduleName + (path.extname(moduleName) ? '' : '.js')
          moduleName = ('./' + path.join(parentPath, moduleName)).replace(/\\/g, '/')
          dependencies.push(moduleName)
          node.arguments = [types.stringLiteral(moduleName)]
        }
      }
    })
    const sourceCode = generator(ast).code
    return { sourceCode, dependencies }
  }

  getSource(modulePath) {
    return fs.readFileSync(modulePath, 'utf8')
  }

  buildModule (modulePath, isEntry) {
    // 首先拿到 modulePath 里的文件内容
    const source = this.getSource(modulePath)
    // 拿到 moduleName,为相对路径
    const moduleName = ('./' + path.relative(this.root, modulePath)).replace(/\\/g, '/')
    // 拿到父目录
    const parentDir = path.dirname(moduleName)
    if(isEntry) {
      this.entryId = moduleName
    }
    // dependicies 表示当前文件依赖了哪些文件,如果是用 require 进行了调用则表示依赖
    const { sourceCode, dependencies } = this.parse(source, parentDir)
    this.modules[moduleName] = sourceCode
    dependencies.forEach(dep => { // 遍历附属模块,递归加载
      this.buildModule(path.join(this.root, dep), false)
    })
  }
  run () {
    this.buildModule(path.resolve(this.root, this.entry), true)
    this.emitFile()
  }
复制代码

emitFile 方法的功能是生成一个输出文件。

const ejs = require('ejs')
  emitFile () { // 用数据渲染模板
    // 输出路径
    const main = path.join(this.config.output.path, this.config.output.filename)
    const templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
    const code = ejs.render(templateStr, {entryId: this.entryId, modules: this.modules})
    this.assets = {}
    // 资源中 路径对应的代码
    this.assets[main] = code
    fs.writeFileSync(main, this.assets[main])
  }
复制代码

到这里基本就结束啦!!

见证奇迹的时刻

切换到 webpack-project 文件夹下,执行 npx mn-pack 命令,等待一会儿,你会看见文件夹下多了一个 dist 目录,里面的 bundle.js 就是我们打包出来的文件啦~执行一下,是不是看见 ab 的输出了呢

三. 结语

这篇文章确确实实很简单了,希望看到这里的朋友们没有觉得是在浪费时间。第一次写博客肯定有不足的地方,希望大家多多担待。有问题的话可以在评论区提问哦。文中有不正确的地方也请大家指出来,共同进步。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

如何构建敏捷项目管理团队

如何构建敏捷项目管理团队

丽萨·阿金斯 / 徐蓓蓓、白云峰、刘江华 / 电子工业出版社 / 2012-6 / 49.00元

《敏捷项目管理系列丛书•PMI-ACPSM考试指定教材•如何构建敏捷项目管理团队:ScrumMaster、敏捷教练与项目经理的实用指南》结合作者的亲身经历告诉读者如何建立一个高性能的敏捷项目管理团队,以及最终成为一名优秀的敏捷教练。作者将敏捷教练定义为导师、协助者、老师、问题解决者、冲突领航员、协作指挥者,正是这种不同角色之间的细微区别才使敏捷教练的工作富有深度。《敏捷项目管理系列丛书•PMI-A......一起来看看 《如何构建敏捷项目管理团队》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具