手写一个webpack(loader、plugin)

手写webpack

源码和更多案例放在github上面,欢迎star

实现一个简单weboack可以分为4点

  1. 编译一个入口文件(代码文件)
  2. 解析并改造代码如将 import、require转换成 webpack_require
  3. 收集依赖的模块并重复2
  4. 生成文件并导出上面的模版。

扩展:

  • 加上loader (文件转换)
  • 加上plugin (编译过程中 不同周期 做额外的事,比如编译过程中加注释,加图片等等。。。)
搭建骨架
 class KWebpack {
  constructor(){
    // 读取webpack.config.js 且初始化
  }

  // 构建模块
  buildModules(){

  }

  // 生成打包文件
  writeFile(){

  }

  // 读取本地文件
  readFile(){
    
  }

  // 解析模块 将源代码解析成 ast语法树并处理
  parse(){

  }

  // 运行模块
  start(){

  }

}
初始化(编译一个入口文件)
let path = require('path')
let _config = require(path.resolve('webpack.config.js'))

// 初始化配置
let defaultConfig = {
  entry:'./src/main.js',
  output:{
    fileName:'build.js'
  }
}

class KWebpack {
  constructor(config){
    // 读取webpack.config.js 且初始化
    this.config = {...defaultConfig,...config}
    this.root = process.pwd()
    // 所有模块依赖
    this.modules = {}
    this.entry = this.config.entry
  }
构建

1.读取代码文件

  // 读取本地文件
  readFile(path){
    return fs.readFileSync(path,'utf-8')
  }

2.构建模块

  // 构建模块
  buildModules(modulePath){
    let fileContent = this.readFile(modulePath)
    // 获取解析过后的内容和依赖的模块
    let {resrouce,deps}= this.parse(fileContent)
    this.modules['./'+path.relative(this.root,modulePath)] = resrouce
    deps.forEach(dep => {
      this.buildModules(path.join(path.dirname(modulePath),dep))
    })

  }

3.解析代码 依赖收集

这里由于要解析并且转成ast语法树,需要用到外部插件(babylon,traverse)

  // 解析模块 将源代码解析成 ast语法树并处理
  parse(data){
    let deps = []
    let ast = babylon.parse(data)
    let root = this.root
    // @babel/traverse遍历ast节点
    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 = moduleName + (path.extname(moduleName) ? '' : '.js') // ./a.js
              moduleName = path.join(moduleName) // ./src/a.js
              deps.push(moduleName)
          }
      }
    })
    let resrouce = generator(ast).code
    return { resrouce, deps} 

  }
导出模板

构建完之后 我们得到moudles对象,里面包括了所有引入文件,现在我们需要导出生成我们要的打包文件。
其中template.js是模板文件,

  // 生成打包文件
  writeFile(){
    let templateContent = this.readFile(__dirname + '/template.js')
    // 把this.moudles数组转成字符串,为了拼接到template里面。
    let modulesStr = ''
    Object.keys(this.modules).forEach((item)=>{
      modulesStr+= `"${item}":${this.modules[item]},`
    })
    templateContent = templateContent.replace('__entry__',this.entry).replace('__modules_content__',modulesStr)

    fs.writeFileSync(`./dist/${this.config.output.fileName}`,templateContent)
  }
// template.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__
    );
    
    return module.exports;
  }
  // return 入口
  return __webpack_require__("__entry__");
})({__modules_content__})
扩展loader

首先,loader所作的事情就是在加载文件时匹配hash,如果正则匹配则调用loader去处理它并返回处理后的结果。

既然我们知道loader是匹配正则去修改并返回结果,那我们可以修改咱们的readFile方法。

// 修改过后的readFile方法

  // 读取本地文件并且加入loader处理
  readFile(path){
    let _file = fs.readFileSync(path,'utf-8')
    return this.handleLoader(path,_file)
  }

handleLoader方法:
loader有几个规则要注意下:

  • loader是一个数组,从下往上执行
  • 每个子loader执行完,会返回结果给下一个子loader执行
  • loader是一个可执行函数
  handleLoader(path,content){
    let rules = this.config.module.rules
    rules.forEach((rule)=>{
      let length = rule.use.length - 1 
      let test = rule.test
      if(test.test(path)){
        do{
          let lo = rule.use[length].loader || rule.use[length]
          let loader = require(lo)  
          content = loader(content) // 这里如果引用正常的loader会报错,因为官方loader里面封装了很多方法。这里没写,所以这里代码封装了一个简单添加文字的loader。来演示
          length--
        }while(length > 0)
      }
      
    })
    return content
  }

ps:由于loader功能比较复杂。涉及到api比较多。理解主要功能就好了。loader就是在加载文件的时候,匹配正则,然后调用loader这个函数去执行返回新的文件内容。

这里我们写一个简单的loader,不涉及原生的loader api。功能是在每个匹配的js后面添加一句注释

// loaderText.js

function loader (source) {
  source += '// 我是添加的loader文案'
  return source
}

module.exports = loader

这样一个简单的loader就成功了。

扩展plugins

plugins只用记住一句话 :插件即钩子,插件的实现就很简单了。只要在constructor的时候添初始化钩子,然后在合适的地方调用即可。

一个webpack的插件由以下几方面组成:

  • 一个非匿名的js函数
  • 在它的原型对象上定义apply方法
  • 指明挂载自身的webpack钩子事件
  • 操作webpack内部情况的特定数据
  • 方法完成时唤起webpack提供的回调

划重点:webpack的钩子离不开tapable,去看webpack4.0的源码,会发现里面都是一些tapable的解构函数。tapable可以说是webpack的灵魂,webpack基本就是各种插件累积起来的,各种插件穿插在各种生命钩子里面就是靠的它。不理解的话,建议去看下tapable文档。不然去看webpack源码会一脸懵逼

这里简单说下tapable钩子,类似于node的Events, 也是注册类似于事件,然后通过不同的钩子调用,来触发一个个事件。(实现事件流机制的 “钩子” 大方向可以分为两个类别,“同步” 和 “异步”,“异步” 又分为两个类别,“并行” 和 “串行”,而 “同步” 的钩子都是串行的

我们先来扩展下webpack内容配置。前面说了插件就是钩子,所以我们先配置钩子** (这里只是演示,所以所有的都用同步钩子SyncHook了)**

  initHooks() {
    // 配置钩子
    this.hooks = {
        entryOption: new SyncHook(),
        compile: new SyncHook(),
        afterCompile: new SyncHook(),
        afterPlugins: new SyncHook(),
        run: new SyncHook(),
        emit: new SyncHook(),
        done: new SyncHook()
    }
  }

然后我们配置一个注册调用plugins的方法。

  hanldePlugins() {
    // 处理插件
    let { plugins } = this.config
    if (Array.isArray(plugins)) {

        plugins.forEach((plugin) => {
            plugin.apply(this) // 每个插件里面都会有一个apply方法来调用
        })
        this.hooks.afterPlugins.call(this)
    }
  }

继续修改启动项:

  // 运行模块
  start(){
    this.hooks.run.call(this)
    this.hooks.compile.call(this)

    this.buildModules(path.resolve(this.root, this.entry),this.entry)

    this.hooks.afterCompile.call(this)

    this.writeFile()
    
    this.hooks.emit.call(this)
    this.hooks.done.call(this)
  }

本质来说,插件就是生命钩子的调用,可以看到启动模块里面不同地方放了不同的钩子。然后通过tapable来调用设置的插件修改complie等等。

这里我们写一个简单插件(构建完之后emit钩子插件):

class PluginTest{
    apply(compiler) {
        compiler.hooks.emit.tap('emit', function() {
            console.log('现在是emit钩子触发')
        })
    }
}

module.exports = PluginTest

然后config文件里面plugins数组里面添加一个new PluginTest()

这样一个简单的plugin就成功了。

猜你喜欢

转载自blog.csdn.net/qq_24073885/article/details/107490610