webpack简单版实现

355 阅读4分钟

将完成以下功能点:

  • webpack打包文件:分析webpack打包之后的文件内容,看webpack是如何加载文件的。
  • 实现一个简单的webpack :实现webpack的基本功能,包括语法转换,AST生成等。
  • 增加对loader和plugin的处理 :增加loader和piugin的处理,完成整体流程。

webpack打包之后的文件

webpack这里就不过多介绍,还不会的朋友们请移步webpack官方文档 下面我们来看看具体的打包出来的文件

// index.js
let title = require('./title.js')
console.log(title)

// title.js
var title = '我是标题'
module.exports = title

// 打包后的代码
;(function (modules) {
  // 模块的缓存
  var installedModules = {}

  // 自己定义的require方法
  function __webpack_require__(moduleId) {
    // 检查是否有缓存,有就直接返回
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports
    }
    // 创建一个缓存,并将加载的模块放入
    var module = (installedModules[moduleId] = {
      i: moduleId, // 模块id
      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/index.js': function (module, exports, __webpack_require__) {
    const title = __webpack_require__(/*! ./title.js */ './src/title.js')

    console.log(title)
  },

  './src/title.js': function (module, exports) {
    var title = '我是标题' has 
    module.exports = title
  },
})

我们可以看到加载的顺序:我删除了很多要的代码,让代码看起来更容易懂些。

  1. 自执行函数传入初始化的分析结果。
  2. __webpack_require__执行传入入口模块。
  3. 执行入口模块的加载方法,然后再次调用__webpack_require__加载依赖模块。

读取config 建立依赖关系

#! /user/bin/env node

let path = require('path')

let config = require(path.resolve('webpack.config.js'))

let Compiler = require('../bin/compiler')

let compiler = new Compiler(config)

compiler.run()

// #! /user/bin/env node 指出运行在弄的环境。 5行代码核心就是找到webpack.config.js传到Compiler类中,然后调用run运行。

Compiler类

class Compiler {
  run() {
    this.hooks.run.call()
    // 根据传入的构建模块依赖关系
    const entryPath = path.resolve(this.root, this.entry)
    // true 标志是入口
    this.buildModule(entryPath, true)
    // 发射打包好的文件
    this.emitFile()
  }
}

module.exports = Compiler

我一步一步的给大家讲解。

run方法

1.触发hooks的run,调用所有监听run的plugin。 2.根据你的工作路径和你的传入的entry,找到你的入口文件。 3.看开始编译你的文件,isEntry标志是否是入口文件。 4.发射已经打包好文件。

buildModule方法

buildModule(modulePath, isEntry) {
    let source = this.getSource(modulePath)
    // 拿到文件的模块id src/index = user/webpack/src/index - user/webpack/
    // moduleName = modulePath - this.root
    let moduleName = './' + path.relative(this.root, modulePath)

    if(isEntry) this.entryId = moduleName

    // 我们要将源码的里面的require -> __webpack_require__ 将应用路径加上./src
    // 返回一个依赖列表
    let {sourceCode, dependencies} = this.parse(source, path.dirname(moduleName))
    this.modules[moduleName] = sourceCode
    // 递归调用buildModule 来生成依赖关系
    // a.js 中引用b.js
    dependencies.forEach(dep => {
      this.buildModule(path.join(this.root, dep), false)
    })
  }

1.拿到入口的源码,getSource根据路径返回源码。 2.moduleName = ./src/index.js => this.entryId (moduleName = ./ + user/webpack/src/index - user/webpack/) 就是相对路径+(文件的绝对路径 减去 文件的执行路径) 3.将源码给到parse解析,返回源码和依赖关系。 4.递归点用buildModule解析模块的依赖模块。

parse方法

/**
   * 解析模块
   * 路径拼接
   * AST解析语法树
   */
  parse(source, parentPath) {
    let ast = babylon.parse(source)
    // 依赖数组
    let dependencies = []
    traverse(ast, {
      CallExpression(p) {
        let node = p.node
        if(node.callee.name === 'require') {
          node.callee.name = '__webpack_require__'
          let moduleName = node.arguments[0].value
          moduleName += path.extname(moduleName) ? '' : '.js'
          moduleName = './' + path.join(parentPath, moduleName)
          dependencies.push(moduleName)
          node.arguments = [types.stringLiteral(moduleName)]
        }
      }
    })
    // 转换后的源码
    let sourceCode = generator(ast).code
    return {sourceCode, dependencies}
  }

1.babylon将我们的源码解析成ast。 2.traverse将我们的源码进行修改,require => _webpack_require_,相对路径也要变成用户执行的文件夹的相对路径。(./src.js => ./src/src.js) 3.dependencies简历依赖关系。 4.types将ast的节点替换掉。 5.generator将ast转换成sourceCode源码。

emitFile方法

// 发射数据,用数据渲染我们的模版
  emitFile() {
    // 1.拿到输出目录。
    const { path, filename} = this.config.output
    const { entryId, modules } = this
    const main = path.join(path, filename)
    const template = this.getSource(path.join(__dirname, 'main.ejs'))
    const code = ejs.render(template, {
      entryId, modules
    })
    this.assets = {}
    this.assets[main] = code
    fs.writeFileSync(main, this.assets[main])
    this.hooks.run.done()
  }

1.拿到输出目录。 2.拿到ejs的模版(这里说明一下,我们将用户的源码转换的结果,内容格式大致是一定的,就是入口只执行函数,不一样的是内容模块,内容模块我们用this.modules[moduleName]建立了依赖关系) 3.写入文件。 4.执行done的hooks

loader的处理

// 根据路径来获取源码
  // 所以这里要根据获取源码的文件类型。用规则来处理源码。
  getSource(modulePath) {
    const rules = this.config.modules.rules
    // 这里默认。rules是【】
    rules.forEach(rule => {
      // 拿到每个规则和loader
      const {test, use} = rule
      let len = use.length - 1

      if(test.test(modulePath)) {
        // 递归调用,用loader处理文件
        function normalLoader() {
          let loader = require(use[len--])
          content = loader(content)
          if(len >= 0) {
            normalLoader()
          }
        }
        normalLoader()
      }

    })
    const content = fs.readFileSync(modulePath, 'utf8')
    return content
  }

1.拿到rules循环拿到每个rule 2.判断当前的路径是否匹配rule 3.因为一个路径有多个loader处理所以normalLoader递归调用并处理文件。 4.返回处理以后的文件内容。

plugins的处理

const tapable = require('tapable')
runPlugins() {
    const plugins = this.config.plugins
    plugins.forEach(plugin => {
      // 注意这里并不是改变this指向,而是调用plugin的apply方法,将当前Compiler实力传入
      plugin.apply(this)
    })
  }
  // plugins的编写
  p.js
  class P{
  apply(compiler) {
    compiler.hooks.run.tap(()=>{
      console.log('run')
    })
  }
}

plugins监听webpack暴露出来的钩子,webpack会在相关的路径上调用,并将当前的compiler返回给你。

基本上我们的webpack差不多就能完成。完整代码