美文网首页
bunny笔记|手写webpack

bunny笔记|手写webpack

作者: 一只小小小bunny | 来源:发表于2022-03-31 16:03 被阅读0次

    学习目标

    • 了解webpack打包原理
    • 了解webpack的loader原理
    • 了解webpack的插件原理
    • 了解ast抽象语法树的应用
    • 了解tapable的原理
    • 手写一个简单的webpack

    项目准备工作

    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)
            }
        }
    }
    

    开发源码:

    • bin目录下的hyh_webpack.js
    #!/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()
    
    • lib文件下的Compiler.js
    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
    
    • template目录下的output.ejs
      (简单学习tapabel-生命周期管理库)
    (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]%>`);
            }),
          <%}%>
        });
    
    • test目录下的tapabel_helloworld.js
    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()
    

    相关文章

      网友评论

          本文标题:bunny笔记|手写webpack

          本文链接:https://www.haomeiwen.com/subject/bydnjrtx.html