webpack机制

bbddl 2019-07-01

简介

以下仅为个人粗略总结和代码,看不懂的稍加理解,本文主要用做个人记录。

先大致总结一下

  • 1.从哪里开始:webpack根据入口模块开始。
  • 2.如何进行:递归读取每个文件,会形成一个依赖列表,依赖列表的,依赖列表是一个以文件相对路径为key,文件内容为value的对象。
  • 3.如何处理:对于每个文件会通过AST解析语法树,返回源码。
  • 4.loader在哪:loader是何时何地进行处理?它是在读取文件的时候开始起作用,通过正则匹配文件是否需要处理。然后读取配置文件的loader配置,然后通过递归的方式处理文件。
  • 5. Plugins呢:plugins是在合适的时机开始工作,那么这个合适时机如何控制呢,是通过tapable事件流机制,实现发布订阅模式。
  • 6.最后:最后返回的是一个匿名自执行函数,定义了一个webpack__require方法,解析传入的依赖列表,递归执行。

然后看下代码

核心代码如下:

//入口文件webpack.js
#! /usr/bin/env node

//第一步:找到当前执行命令的路径,拿到webpack.config.js
let path = require("path")
let config = require(path.resolve('webpack.config.js'))
console.log(path.resolve(),'resolve--------------->')
let Compiler = require('../lib/Compiler')
let compiler = new Compiler(config)

//标识运行编译
compiler.run()
//Compiler.js
const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const travere = require('@babel/traverse').default
const t = require('@babel/types')
const generator = require('@babel/generator').default
const ejs = require('ejs')
const {SyncHook} = require('tapable')
//babylon把源码转为AST
//@babel/traverse
//@babel/generator
//@babel/types

class Compiler {
  constructor(config) {
    this.config = config //保存入口文件路径
    this.entryID = '' //主模块入口路径
    this.modules = {} //存放模块依赖关系
    this.entry = config.entry //入口路径
    this.root = process.cwd() //当前工作目录
    this.hooks = {
      entryOption: new SyncHook(),
      compile: new SyncHook(),
      afterCompile: new SyncHook(),
      afterPlugins: new SyncHook(),
      run: new SyncHook(),
      emit: new SyncHook(),
      done: new SyncHook(),
    }
    //如果传递了plugins参数
    let plugins = this.config.plugins
    if(Array.isArray(plugins)) {
      plugins.forEach(plugin => {
        plugin.apply(this)
      })
    }
  }
  
  run() {
    //执行,并创建模块的依赖关系
    this.buildModule(path.resolve(this.root, this.entry), true)
    //发射一个文件,就是打包后的文件
    this.emitFile()
  }

  //构建模块
  buildModule(modulePath, isEntry) {
    //首先读取入口文件
    let source = this.getSource(modulePath)
    //模块的ID = this.root - modulePath
    let moduleName = './' + path.relative(this.root, modulePath)
    if(isEntry) {
      this.entryID = moduleName //保存入口名字
    }
    //解析,需要把source源码进行改造,返回一个依赖列表
    let {sourceCde, dependencies } = this.parse(source, path.dirname(moduleName))
    this.modules[moduleName] = sourceCde

    dependencies.forEach(dep => { //附模块的递归加载
      this.buildModule(path.join(this.root, dep), false)
    })
  }

  //解析源码, AST解析语法树
  parse(source, parentPath) {
    // console.log(source, parentPath)
    let ast = babylon(source)
    let dependencies = [] //依赖数组
    travere(ast, {
      CallExpression() {
        let node = p.node
        if(node.callee.name === 'require') {
          node.callee.name = '_webpack_require_'
          let moduleName = node.arguments[0].value //这里就是引用模块的名字
          moduleName = moduleName + (path.extname(moduleName) ? '' : '.js')
          moduleName = './' + path.join(parentPath, moduleName) //'src/a.js'
          dependencies.push(moduleName)
          node.arguments = [t.stringLiteral(moduleName)]
        }
      }
    })
    let sourceCode = generator(ast).code
    return {sourceCode, dependencies}
  }

  //公用读文件的方法
  getSource(modulePath) {
    let content = fs.readFileSync(modulePath, 'utf8')
    let rules = this.config.module.rules //拿到规则
    //拿到每个规则来处理
    for(let i=0; i<rules.length; i++) {
      let rule = rules[i]
      let { test, use } = rule
      let len = use.length - 1
      if(test.test(modulePath)) { //这个模块需要通过loader转换
        function normalLoader() {
          let loader = require(use[len]) //获取对应loader函数
          content = loader(content)
          //递归调用loader
          if(len >= 0) {
            normalLoader()
          }
        }
        normalLoader()
      }
    }
    
    return content
  }

  //发射文件
  emitFile() {
    //用数据 渲染我们的
    //拿到输出到哪个目录下
    let main = path.join(this.config.output.path, this.config.output.filename)
    //模板路径
    let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))
    let code = ejs.render(templateStr, {entryId: this.entryID, modules: this.modules})
    this.assets = {
      //资源中路径对应的代码
     }
    this.assets[main] = code
    fs.writeFileSync(main, this.assets[main])
  }
}

module.exports = Compiler

相关推荐