webpack学习--自定义webpack、loader、plugin

在这里插入图片描述
webpack 是一个用于现代 JavaScript 应用程序的 静态模块打包工具

前端开发中遇到的问题:

  • 需要通过模块化的方式来开发
  • 使用高级特性,加快开发效率或者安全性,比如 es6+,ts开发脚本逻辑,sass、less编写样式代码
  • 监听文件的变化,并且反映到浏览器上,提高开发效率
  • 需要将代码进行压缩,合并以及其他相关的优化

打包工具需要具备的能力:(以实现模块化为目标)

  • 能后将散落的模块打包到一起
  • 能够编译代码中的新特性
  • 能够支持不同种类的前端资源模块

webpack 不同环境的预设配置

  • production
    启动内置优化插件,自动优化打包结果,打包速度偏慢
  • development
    自动优化打包速度,添加一些调试过程中的辅助插件,以便于更好的调试错误
  • none 模式
    运行最原始的打包,不做任何额外的处理,一般在分析模块的打包结果时用到

Loader

使webpack管理项目中任意类型的资源文件,所有资源的加载都是JS代码控制
在这里插入图片描述

比如css-loader使用方式
在这里插入图片描述
css-loader 只会把css模块加载到js代码中,而并不使用这个模块,所以一般配合style-loader使用

实现一个markdown-loader
实现逻辑:
1、安装一个能将markdown解析为html的模块–marked
2、在markdown-loader.js 中导入这个模块
3、用这个模块解析source

//markdown-loader.js

const {marked} = require('marked')
module.exports = source => {
 
  //将markdown 转为html字符串
  const html = marked(source)
  // 将html字符串,拼接为一段导出字符串的js代码
  const code = `module.exports= ${JSON.stringify(html)}`
  return code

}

//webpack.config.js

{
          test:/\.md$/, //正则表达式,匹配文件类型
          use:['html-loader','./markdown-loader'] //申明使用什么loader进行处理
      }

plugin

webpack 插件机制的目的,是为了增强webpack在项目自动化构建方面的能力

插件常见的场景:

  • 实现自动在打包之前清除dist目录
  • 自动生成应用所需要的Html文件
  • 根据不同环境为代码注入类似API地址这种可能变化的部分
  • 拷贝不需要参与打包的资源文件到目录
  • 压缩webpack打包完成后输出的文件
  • 自动发布打包结果到服务器实现自动部署

插件实现机制

plugins是可以用自身原型方法apply来实例化的对象。apply只在安装插件被Webpack compiler执行一次。apply方法传入一个webpck compiler的引用,来访问编译器回调。

手写插件 remove-comments-plugin 删除注释

class RemoveCommentsPlugin {
  apply (compiler) { // 这个方法会在启动的时候被调用
    // compiler 包含此次构建的所有配置信息
    // compiler 对象的hooks属性访问到emit钩子,这个钩子会在webpack即将向输出目录输出文件时执行
    
    // 再通过tap方法注册一个钩子函数,tap 方法接受两个参数。插件名称 和 要挂载到这个钩子上的函数
    compiler.hooks.emit.tap('RemoveCommentsPlugin', compilation => {
      // compilation可以理解为此次打包的上下文
      for(const name in compilation.assets) {
        // console.log(name) // 输出文件名称
        // console.log(compilation.assets[name].source()) // 输出文件名称
        if (name.endsWith('.js')) { // js文件
          const contents = compilation.assets[name].source()
          const noComments = contents.replace(/\/\*{2,}\/\s?/g, '')
          compilation.assets[name] = {
            source: () => noComments,
            size:() => noComments.length
          }

        }
      }
    })

    console.log('RemoveCommentsPlugin')
  }

}

module.exports=RemoveCommentsPlugin;

webpack Plugin 的工作原理

  • 读取配置的过程中会先执行 new RemoveCommentsPlugin(options) 初始化一个RemoveCommentsPlugin 获得其实例。
  • 初始化 compiler 对象后调用 RemoveCommentsPlugin.apply(compiler) 给插件实例传入compiler 对象。
  • 插件实例在获取到 compiler 对象后,就可以通过compiler.plugin(事件名称, 回调函数) 监听到 Webpack广播出来的事件。 并且可以通过 compiler 对象去操作 Webpack。

webpack 工作原理

根据配置,找到其中的一个文件作为指定的入口,一般为js文件;
根据代码中出现的import或者是require之类的语句解析推断出来这个文件所依赖的一些资源模块,然后再去分别解析每个资源模块的依赖,最终行成了整个项目中所有用到的文件之间的一个依赖关系树

在这里插入图片描述
在这里插入图片描述
当产生这个依赖关系树之后,webpack会递归这个依赖树,然后去找到每一个节点所对应的资源文件,然后根据配置选项中的loader配置,交给对应的loader去加载这个模块,最后将加载结果放入打包结果之中
在这里插入图片描述
webpack执行流程

  1. 初始化Compiler: webpack(config) 得到Compiler 对象
  2. 开始编译:调用Compiler 对象 run 方法开始执行编译
  3. 确定入口:根据配置中的entry 找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的loader 对模块进行编译,再找出该模块所依赖的模块,递归知道所有的模块被加载进来
  5. 完成模块编译:在使用loader 编译完所有的模块后,得到了每个模块被编译后的最终内容以及他们之间的依赖关系
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk。再把每个chunk准换成一个单独的文件加入到输出列表(这里是可以修改输出内容的最后机会)
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

实现webpack

babel: https://www.babeljs.cn/docs/

使用babel解析ast 结构:


const fs = require('fs')
const babelParser = require('@babel/parser')
const traverse = require('@babel/traverse').default
function webpack(config) {
  return new Compiler(config)
}

class Compiler {
  constructor (options ={}) {
    this.options = options
  }
  // 启动webpack打包
  run () {
    // 1.读取入口文件内容
    // 入口文件路劲
    const filePath = this.options.entry
    const file = fs.readFileSync(filePath, 'utf-8')
    // 2. 将其解析成ast 抽象语法树
    const ast = babelParser.parse(file, {
      sourceType: 'module' // 解析文件的模块化方案是 ES Module
    })
    console.log(ast)
   
  }
}

module.exports = webpack

在这里插入图片描述

收集依赖

 // 获取文件文件夹路径
    const dirname = path.dirname(filePath)
    // 存储依赖
    const deps = {}
    // 收集依赖
    traverse(ast, {
      // 内部会遍历ast 中的program.dody, 判断里面的语句类型
      // 如果type:"ImportDeclaration" 就会触发当前函数
      ImportDeclaration ({node}) {
        // 文件相对路径 './add.js'
        const relativePath = node.source.value
        // 生成基于入口文件的绝对路径
        const absoulatePath = path.resolve(dirname, relativePath)
        console.log(node)
        // 添加依赖
        deps[relativePath] = absoulatePath
      }
          console.log(deps)
    })

根据类型,获取引入文件地址:
在这里插入图片描述
这是打印的deps收集的依赖
在这里插入图片描述

编译代码

借助babel-core 中的transformFromAst
在这里插入图片描述

// 编译代码:将代码中浏览器不能识别的语法进行编译
    const { code } = transformFromAst (ast, null, {
      presets: ['@babel/preset-env']
    })
    console.log(code)

在这里插入图片描述

可以看到模块语法变成了commonJS

递归收集所有依赖

build函数是将前面的三个步骤进行了封装

 const filePath = this.options.entry
    // 第一次构建,得到入口文件的信息
    const fileInfo = this.build(filePath)
    this.modules.push(fileInfo)
    // 递归收集依赖
    // 遍历所有的依赖
    this.modules.forEach( fileInfo => {
      // deps: {
      //   './add': '/Users/qwr/Desktop/demo/webpack-demo/mywebpack/src/add',
      //   './count': '/Users/qwr/Desktop/demo/webpack-demo/mywebpack/src/count'
      // }
      // 取出当前文件的所有依赖
      const deps = fileInfo.deps
      // 遍历
      for(const relativePath in deps) {
        // 依赖文件的绝对路径
        const absoulatePath = deps[relativePath]
        // 对依赖文件进行处理
        const fileInfo = this.build(absoulatePath)
        // 将处理后的结果添加到modules中,后面遍历就回对fileInfo
        this.modules.push(fileInfo)
      }
      console.log(this.modules)
    })

看下结果
在这里插入图片描述
将依赖整理成关系图

// 整理成关系依赖图
    const depsGrsph = this.modules.reduce((graph, module) => {
      return {
        ...graph,
        [module.filePath]: {
          code:module.code,
          deps:module.deps
        }
      }
    }, {})

    console.log(depsGrsph)

在这里插入图片描述

打包生成bundle.js

js 知识点: 自执行函数一定要加分号!!!!

// 生成输出资源。--js文件
  generate(depsGraph) {
    const bundle = `
      (function (depsGraph) {
        // require函数: 为了加载入口文件
        function require(module) {
          // 定义模块内部的require函数--引入模块内容
          function localRequire (relativePath) {
            // 为了找到要引入模块的绝对路径,通过require加载
            return require(depsGraph[module].deps[relativePath])
          };
          // 定义暴露对象,(将来要暴露的内容)
          var exports = {};

          (function (require, exports, code){
            eval(code)
          })(localRequire, exports, depsGraph[module].code);

          // 作为require的返回值,返回出去
          // 为了后面的require函数能得到暴露的内容
          return exports;
        }
        // 加载入口文件
        require(${JSON.stringify(this.options.entry)});
      })(${JSON.stringify(depsGraph)});
    `
    // 生成文件的绝对路径
    const filePath = path.resolve(this.options.output.path, this.options.output.filename);
    // 写入文件
    fs.writeFileSync(filePath, bundle, 'utf-8');
  }

在这里插入图片描述

demo

demo地址:https://github.com/wkh1234/webpack-demo

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值