实现简易版的webpack-flow

序言

今天早上小编写了一个简易版的webpack-flow,突发奇想想记录下来 && 也是想分享给大家,所以接下来让我们看看webpack-flow流程以及代码实现部分,伙计们,小板凳准备好了吗???????

1. webpack执行流程

webpackflow2020.jpg

  1. 上述的webpack执行流程大致就分为这几步,而我们的实现流程也是按照大致的方向来的,我们会从入口到方法一个一个讲起

2. 执行入口以及使用函数

  • webpack函数:我们会导出函数,这个函数用来收集参数以及实例化编译器的,同样也是执行入口
  • Compiler类:用来开始执行编译的,会暴露一个方法run,调用run方法正式启动编译流程
  • resolveLoader函数:依次执行loader来解析代码的,这里我们模拟了babel-loader来解析es6的语法
  • compose函数:实现loader自下而上执行

3. 具体的代码实现

3.1 首先我们从入口出发,来看看入口到底做了什么??????

// webpack 编译入口
function webpack(options) {
  // 1 -- 参数合并,将webpack调用传递的options 跟 shell中options进行合并
  const argv = process.argv.slice(2)
  const shellOptions = argv.reduce((shellOptions, curr) => {
    const {key, value} = curr.split('=')
    shellOptions[key.slice(2)] = value
    return shellOptions
  }, {})
  const mergeOptions = {...options, ...shellOptions}

  // 2 -- 开始编译 得到compiler对象
  const compiler = new Compiler(mergeOptions)
  // 2 -- 加载所有配置的插件
  if (mergeOptions.plugins && Array.isArray(mergeOptions.plugins)) {
    mergeOptions.plugins.forEach(plugin => plugin.apply(compiler))
  }
  return compiler
}
复制代码
  • 通过上述代码我们可以看到,webpack入口函数无非是以下两种功能??
  • 将webpack.config参数以及shell命令中的参数进行合并
  • 实例化编译类Compiler,得到实例
  • 加载plugins插件,这是预期说加载还不如说订阅,看下面的自定义plugin代码
class DonPlugin {
  apply(compiler) {
    // 函数tap 其实就是通过tapable库进行发布订阅
    compiler.hooks.done.tap('done', () => {
      console.log('执行结束了..............')
    })
  }
}

module.exports = DonPlugin
复制代码

3.2 Compiler 实现

class Compiler {
  constructor(options) {
    this.hooks = {
      run: new SyncHook(),
      done: new SyncHook()
    }
    this.options = options
  }

  // 开始编译的入口
  run() {
    const chunks = {}

    // 2 -- 开始确定编译的入口
    const {context = process.cwd(), entry} = this.options
    if (typeof entry === 'string') {
      this.options.entry = {main: entry}
    }

    // -- 此处开始调用钩子
    this.hooks.run.call('run')
    // 3 -- 开始遍历loader 开始解析文件
    resolveLoader(chunks, this)

    this.hooks.done.call('done')
    // 5 -- 写入文件
    const {path: dirPath, filename} = this.options.output
    if (!fs.existsSync(dirPath)) {
      fs.mkdirSync(dirPath)
    }
    Object.keys(chunks).forEach(fileName => {
      const newPath = path.join(dirPath, `${fileName}.js`)
      chunks[fileName].forEach(content => {
        fs.writeFileSync(newPath, content, {encoding: 'utf-8'})
      })
    })
  }
}
复制代码
  • 首先看代码构造函数的部分,其实就是通过tapable来实例化了两个发布订阅钩子,run以及done,而上面执行plugin的过程无非是先订阅,触发的事件会在下面讲述。
  • 开始确定编译入口,其实是通过编译入口依次进行依赖解析的
  • 此时我们会看到代码this.hooks.run.call('run') 这个就是触发的时机,其实我们这个插件就是为了在运行的时候调用
  • 编译类中会沿着入口通过loader挨个解析文件,就是这句代码resolveLoader('done'),这个代码我们会在下一步进行讲解
  • 此时运行到了代码this.hooks.done.call('done'). 到这一步说明通过loader解析结束了,马上开始要写入文件了。所以我们调用了done钩子。

此时我们联想到了上图截图中的一句话,"Webpack会在特定的时间广播出特定的事件,插件在监听到感兴趣的事件后执行特定的逻辑",这里所谓的特定时间就是不同阶段来触发订阅,而执行特定的事件就是插件中订阅的钩子函数

3.3 resolveLoader的实现

function resolveLoader(chunks, compiler) {
  // 入口文件 以及loader的module
  const {entry, module = {}} = compiler.options
  // 解析规则
  const {rules} = module
  // 是否配置loader
  if (rules && Array.isArray(rules)) {
    Object.keys(entry).forEach(keyName => {
      const modules = []
      // 文件路径
      const filePath = entry[keyName]
      // 读取文件内容
      const fileContent = fs.readFileSync(filePath, 'utf-8')
      rules.forEach(rule => {
        // 如果满足loader.test 匹配规则,调用对应的loader来解析
        if (rule.test.test(filePath)) {
          const methods = rule.use.map(methodPath => require(methodPath))
          modules.push(compose(methods)(fileContent))
        }
      })

      // 4 -- 完成编译 通过loader编译结束后 添加到对应的chunks
      chunks[keyName] = modules
    })
  }
}
复制代码
  • 上述的代码不做特殊的描述了,其实就是通过loadr解析文件,获得文件源码

3.4 compose函数实现

function compose(resolveLoaders) {
  if (resolveLoaders.length === 0) return x => x
  if (resolveLoaders.length === 1) return x => resolveLoaders[0](x)
  return resolveLoaders.reduce((a, b) => (...args) => a(b(...args)))
}
复制代码

上述代码不做过多的阐述,其实就是我们常写的compose的实现,这里参照了redux的源码实现

4. 结语

以上就是webpack-flow简单实现,如果不到之处欢迎指正

5. 源码地址 以及扩展

猜你喜欢

转载自juejin.im/post/7035125972739293221