bunny笔记|手写webpack

2022-03-31  本文已影响0人  一只小小小bunny

学习目标

项目准备工作

  1. 新建一个项目,起一个名字(这里是hyh_webpack

  2. 新建bin目录,新建hyh_webpack.js文件,将打包工具主程序放入其中

    主程序的顶部应当有:#!/usr/bin/env node标识,指定程序执行环境为node

  3. package.json中配置bin脚本

    {
     "bin": "./bin/itheima-pack.js"
    }
    
  4. 通过npm link链接到全局包中,供本地测试使用

分析webpack打包的bundle文件

其内部就是自己实现了一个__webpack_require__函数,递归导入依赖关系

(function (modules) { // webpackBootstrap
  // The module cache
  var installedModules = {};

  // The require function
  function __webpack_require__(moduleId) {

    // Check if module is in cache
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;
  }

  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})
  ({
    "./src/index.js":
      (function (module, exports, __webpack_require__) {
        eval("let news = __webpack_require__(/*! ./news.js */ \"./src/news.js\")\r\nconsole.log(news.content)\n\n//# sourceURL=webpack:///./src/index.js?");
      }),
    "./src/message.js":
      (function (module, exports) {
        eval("module.exports = {\r\n  content: '今天要下雨了!!!'\r\n}\n\n//# sourceURL=webpack:///./src/message.js?");
      }),
    "./src/news.js":
      (function (module, exports, __webpack_require__) {
        eval("let message = __webpack_require__(/*! ./message.js */ \"./src/message.js\")\r\n\r\nmodule.exports = {\r\n  content: '今天有个大新闻,爆炸消息!!!内容是:' + message.content\r\n}\n\n//# sourceURL=webpack:///./src/news.js?");
      })
  });

自定义loader

在学习给自己写的itheima-pack工具添加loader功能之前,得先学习webpack中如何自定义loader,所以学习步骤分为两大步:

  1. 掌握自定义webpack的loader
  2. 学习给itheima-pack添加loader功能并写一个loader

webpack以及我们自己写的itheima-pack都只能处理JavaScript文件,如果需要处理其他文件,或者对JavaScript代码做一些操作,则需要用到loader。

loader是webpack中四大核心概念之一,主要功能是将一段匹配规则的代码进行加工处理,生成最终的代码后输出,是webpack打包环节中非常重要的一环。

loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

之前都使用过别人写好的loader,步骤大致分为:

  1. 装包
  2. 在webpack.config.js中配置module节点下的rules即可,例如babel-loader(省略其他配置,只论loader)
  3. (可选步骤)可能还需要其他的配置,例如babel需要配置presets和plugin
const path = require('path')

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'bundle.js'
  },
  module: {
    rules: [
      { test: /\.js$/, use: 'babel-loader' }
    ]
  },
  mode: 'development'
}

实现一个简单的loader

loader到底是什么东西?能不能自己写?

答案是肯定的,loader就是一个函数,同样也可以自己来写

  1. 在项目根目录中新建一个目录存放自己写的loader:

[图片上传失败...(image-d60744-1648713811934)]

  1. 编写myloader.js,其实loader就是对外暴露一个函数

    第一个参数就是loader要处理的代码

    module.exports = function(source) {
      console.log(source) // 只是简单打印并返回结果,不作任何处理
      return source
    }
    
  2. 同样在webpack.config.js中配置自己写的loader,为了方便演示,直接匹配所有的js文件使用自己的myloader进行处理

    const path = require('path')
    
    module.exports = {
      entry: './src/index.js',
      output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js'
      },
      module: {
        rules: [
          { test: /.js$/, use: './loaders/myloader.js' }
        ]
      },
      mode: 'development'
    }
    
  3. 如果需要实现一个简单的loader,例如将js中所有的“今天”替换成“明天”

    只需要修改myloader.js的内容如下即可

    module.exports = function(source) {
      return source.replace(/今天/g, '明天')
    }
    
  4. 同时也可以配置多个loader对代码进行处理

    const path = require('path')
    
    module.exports = {
      entry: './src/index.js',
      output: {
        path: path.join(__dirname, 'dist'),
        filename: 'bundle.js'
      },
      module: {
        rules: [
          { test: /.js$/, use: ['./loaders/myloader2.js', './loaders/myloader.js'] }
        ]
      },
      mode: 'development'
    }
    
  5. myloader2.js

    module.exports = function(source) {
      return source.replace(/爆炸/g, '小道')
    }
    

loader的分类

不同类型的loader加载时优先级不同,优先级顺序遵循:

前置 > 行内 > 普通 > 后置

pre: 前置loader

post: 后置loader

指定Rule.enforce的属性即可设置loader的种类,不设置默认为普通loader

在itheima-pack中添加loader的功能

通过配置loader和手写loader可以发现,其实webpack能支持loader,主要步骤如下:

  1. 读取webpack.config.js配置文件的module.rules配置项,进行倒序迭代(rules的每项匹配规则按倒序匹配)
  2. 根据正则匹配到对应的文件类型,同时再批量导入loader函数
  3. 倒序迭代调用所有loader函数(loader的加载顺序从右到左,也是倒叙)
  4. 最后返回处理后的代码

在实现itheima-pack的loader功能时,同样也可以在加载每个模块时,根据rules的正则来匹配是否满足条件,如果满足条件则加载对应的loader函数并迭代调用

depAnalyse()方法中获取到源码后,读取loader:

let rules = this.config.module.rules
for (let i = rules.length - 1; i >= 0; i--) {
    // console.log(rules[i])
    let {test, use} = rules[i]
    if (test.test(modulePath)) {
        for (let j = use.length - 1; j >= 0; j--) {
            let loaderPath = path.join(this.root, use[j])
            let loader = require(loaderPath)
            source = loader(source)
        }
    }
}

开发源码:

#!/usr/bin/env node

//如上,声明环境为node环境
//console.log('可以执行打包了');

const path = require('path')
// 1. 读取需要打包项目的配置文件
let config = require(path.resolve('webpack.config.js'))
 console.log(config)

// 2. 通过面向对象的方式来进行项目推进
const Compiler = require('../lib/Compiler')
new Compiler(config).start()
const path = require('path')
const fs = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generator = require('@babel/generator').default
const ejs = require('ejs')
const { SyncHook } = require('tapable')
class Compiler {
  constructor(config) {
    this.config = config
    this.entry = config.entry
    // 获取执行itheima-pack指令的目录
    this.root = process.cwd()
    // 初始化一个空对象, 存放所有的模块
    this.modules = {}
    // 将module.rules挂载到自身
    this.rules = config.module.rules
    //先有hooks才能调用apply
    this.hooks = {
      //生命周期钩子的定义 -->第一步
      compiler: new SyncHook(),
      afterCompiler: new SyncHook(),
      emit: new SyncHook(),
      afterEmit: new SyncHook(),
      done: new SyncHook()
    }
    //获取plugins数组中的所有插件对象,调用其apply方法
    if (Array.isArray(this.config.plugins)) {
      this.config.plugins.forEach(plugin => {
        plugin.apply()
      })
    }

  }
  getSource(path) {
    return fs.readFileSync(path, 'utf-8')
  }
  depAnalyse(modulePath) {
    // console.log(modulePath)
    // 读取模块内容
    let source = this.getSource(modulePath)
    // console.log(source)

    // 读取loader
    let readAndCallLoader = (use, obj) => {
      let loaderPath = path.join(this.root, use)
      let loader = require(loaderPath)
      source = loader.call(obj, source)
    }

    // 读取rules规则, 倒序遍历
    for (let i = this.rules.length - 1; i >= 0; i--) {
      // console.log(this.rules[i])
      let { test, use } = this.rules[i]
      // 获取每一条规则,与当前modulePath进行匹配
      // 匹配modulePath 是否符合规则,如果符合规则就要倒序遍历获取所有的loader
      if (test.test(modulePath)) {
        // 判断use是否为数组,如果是数组才需要倒序遍历
        if (Array.isArray(use)) {
          for (let j = use.length - 1; j >= 0; j--) {
            // 每一个loader的路径
            // console.log(path.join(this.root, use[j]))
            // let loaderPath = path.join(this.root, use[j])
            // let loader = require(loaderPath)
            // source = loader(source)
            readAndCallLoader(use[j])
          }
        } else if (typeof use === 'string') {
          // use为字符串时,直接加载loader即可
          // let loaderPath = path.join(this.root, use)
          // let loader = require(loaderPath)
          // source = loader(source)
          readAndCallLoader(use)
        } else if (use instanceof Object) {
          // console.log(use.options)
          // let loaderPath = path.join(this.root, use.loader)
          // let loader = require(loaderPath)
          // source = loader.call({ query: use.options }, source)
          readAndCallLoader(use.loader, { query: use.options })
        }
      }
    }

    // 准备一个依赖数组,用于存储当前模块的所有依赖
    let dependencies = []

    let ast = parser.parse(source)
    // console.log(ast.program.body)
    traverse(ast, {
      CallExpression(p) {
        if (p.node.callee.name === 'require') {
          // 修改require
          p.node.callee.name = '__webpack_require__'

          // 修改路径
          let oldValue = p.node.arguments[0].value
          oldValue = './' + path.join('src', oldValue)
          // 避免Windows出现反斜杠 : \ 
          p.node.arguments[0].value = oldValue.replace(/\\+/g, '/')

          // 每找到一个require调用, 就将其中的路径修改完毕后加入到依赖数组中
          dependencies.push(p.node.arguments[0].value)
        }
      }
    })
    let sourceCode = generator(ast).code

    // console.log(sourceCode)

    // 构建modules对象
    // { './src/index.js': 'xxxx', './src/news.js': 'yyyy' }
    // this.modules
    let modulePathRelative = './' + path.relative(this.root, modulePath)
    modulePathRelative = modulePathRelative.replace(/\\+/g, '/')
    this.modules[modulePathRelative] = sourceCode

    // 递归加载所有依赖
    // ./src/news.js   ./src/news2.js
    dependencies.forEach(dep => this.depAnalyse(path.resolve(this.root, dep)))
  }
  emitFile() {
    // 使用模板进行拼接字符串,生成最终的结果代码
    let template = this.getSource(path.join(__dirname, '../template/output.ejs'))
    let result = ejs.render(template, {
      entry: this.entry,
      modules: this.modules
    })
    // 获取输出目录
    let outputPath = path.join(this.config.output.path, this.config.output.filename)
    fs.writeFileSync(outputPath, result)
  }
  start() {
    //开始编译啦
    this.hooks.compiler.call()
    // 开始打包了!
    // 依赖的分析
    // __dirname表示的是 itheima-pack 项目中Compiler.js所在目录
    // 而非入口文件所在的目录
    // 如果需要获取执行itheima-pack指令的目录, 需要使用 process.cwd()
    this.depAnalyse(path.resolve(this.root, this.entry))
    //编译完成啦
    this.hooks.afterCompiler.call()
    //开始发射文件
    this.hooks.emit.call()
    this.emitFile()
    //文件发射完了
    this.hooks.afterEmit.call()
    // console.log(this.modules)
  }
}

module.exports = Compiler
(function (modules) { // webpackBootstrap
    // The module cache
    var installedModules = {};
  
    // The require function
    function __webpack_require__(moduleId) {
  
      // Check if module is in cache
      if (installedModules[moduleId]) {
        return installedModules[moduleId].exports;
      }
      // Create a new module (and put it into the cache)
      var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
      };
  
      // Execute the module function
      modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  
      // Flag the module as loaded
      module.l = true;
  
      // Return the exports of the module
      return module.exports;
    }
  
    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = "<%-entry%>");
  })
    ({
      <% for (let k in modules) { %>
        "<%-k%>":
        (function (module, exports, __webpack_require__) {
          eval(`<%-modules[k]%>`);
        }),
      <%}%>
    });
const {SyncHook} =require('tapable')

//学前端
//流程:1.开班 2.学html 3.学css 4.学js 5.学框架react
//安装tapable [npm i tapable]

//实现生命周期管理(具体需求看官方文档) 
class Frontend{
    constructor(){
        //定义好钩子(生命周期)
        this.hooks={
            //如果需要在call时传参,则需要在new SyncHook时定义需要的参数
            beforeStudy:new SyncHook(['name']),
            afterHtml:new SyncHook(),
            afterCss:new SyncHook(),
            afterJs:new SyncHook(),
            afterReact:SyncHook()
        }
    }
    study(){
        console.log('开班 ');
        this.hooks.beforeStudy.call()

        console.log('学html');
        this.hooks.afterHtml.call()
        //抽象化
        console.log('学css ');
        this.hooks.afterCss.call()

        console.log('学js');
        this.hooks.afterJs.call()

        console.log('学框架react');
        this.hooks.afterReact.call()
    }
}
let f= new Frontend()
f.hooks.afterHtml.tap('afterHtml',(name)=>{
    console.log('学完html后我想写更多页面');
})
//其它同-略
f.study()
上一篇下一篇

猜你喜欢

热点阅读