【新手向】如何写一个超超超简单的 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 的输出了呢

三. 结语

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


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

查看所有标签

猜你喜欢:

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

网页设计

网页设计

顾群业 / 山东美术 / 2007-1 / 42.00元

网页设计,是指网页设计者以既有的技术和艺术知识为基础,依照设计目的和要求,自觉地对网页的构成元素进行艺术构思,创造出艺术化、人性化的网站界面。如今,网页设计也发展成为一种新的艺术形式,是设计艺术的重要组成部分。优秀的网页设计,不仅要有鲜明的主题、统一的风格,还要求内容与形式的高度统一。一起来看看 《网页设计》 这本书的介绍吧!

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具