打包工具webpack(二)

132 阅读6分钟

「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。

一.webpack核心工作原理

  • 一般项目中会散落着各种资源文件
  • 然后webpack会找到其中的一个文件,作为文件打包的入口。一般这个文件会是JavaScript文件
  • 然后会顺着这个文件中的代码,比如import或者require等语句,去解析并推断这个资源所依赖的模块。然后去解析每个模块的依赖,最后就形成了项目中所有依赖关系的依赖树。
  • 然后webpack会递归或者遍历这个资源树,找到对于节点的配置文件。最后根据配置文件中的rules属性,去找到这个模块的加载器,然后交给对应的加载器,去加载这个模块。
  • 最后会把加载后的结果,放到bundle.js,打包结果之中。从而去实现整个项目的打包。

1.webpack中的loader工作原理

  • 这里的需求是一个markdown文件的加载器,markdown-loader
  • md一般是转换成html后,再呈现到页面上,所以,希望得到的结果,转换后的html
  1. 创建markdown-loader.js文件
// 所有的loader都需要导出一个函数
// 这个函数就是对所加载的资源一个处理过程
// 输入就是加载的资源文件内容
// 输出就是此次加工后的结果
// 通过source参数去接受输入,通过返回值去输出
module.exports = source => {
  // 这里先打印source试试
  console.log(source)
  return 'hello~'
}
  • 这里去配置下webpack.config.js
{
  test:/.md$/,
  // use参数不仅可以匹配模块名,也可以匹配路径
  use:'./markdown-loader.js'
}
  • 运行输出,确实是md文件的内容,但同时也出现一个解析错误,需要再加载一个loader

  • 其实webpack的loader像一个管道,可以依次处理,染最终的输出必须是Javascript代码。而这里返回的是一个字符串(return 'hello~'),所以报错。

  • 所以修改上述代码,返回标准的js代码

module.exports = source => {
  console.log(source)
  return 'console.log("hello~")'
}
  • 运行webpack打包,没有报错,打开bundle.js文件,查看最后一个loader。发现console直接拼接到最后的代码上面了。
  • 先安装一个markdown解析模块,yarn add marked --dev
const marked = require('marked')

module.exports = source => {
  const html = marked(source)
  // 因为需要直接导出html,但loader的结果又必须是js,所以可以用module.exports
  // 这里通过stringfy进行转换,把可能存在的字符串问题转换成js理解的字符串
  return `module.exports = ${JSON.stringfy(html)}`
}
  • 进行编译,查看bundle.js。此时的编译结果就是我们所需要的了。
  • 也可以用export default的方式导出
const marked = require('marked')
module.exports = source => {
  const html = marked(source)
  return `export default ${JSON.stringfy(html)}`
}
  • 编译输出的结果也是可以的。webpack会自动转换导出过程中的ESmodule代码
  • 试一下其他的情况,比如返回一个html,在用html-loader去转换成js
const marked = require('marked')
module.exports = source => {
  const html = marked(source)
  // 返回html,字符串交给下一个loader去处理
  return html
  • yarn add html-loader --dev
  • 修改配置文件
{
  test:/.md$/,
  // use修改成数组,这样就会依次加载loader,执行顺序是从后往前。
  use:[
    'html-loader',
    './markdown-loader.js'
  ]
}
  • 总结:loader的作用就是把资源文件进行输入到输出的一个转换。loader类似一种管道的概念,对同一个资源可以依次使用loader。

二.webpack插件机制介绍

  • loader专注实现资源模块的加载

  • plugin是解决除了资源加载以外,其他的自动化的工作。

    • 比如打包前自动清除dist目录

    • 拷贝静态文件至输出目录

    • 压缩输出代码等

1. webpack常用插件的介绍--clean-webpack-plugin

  • 自动清除输出目录的插件,clean-webpack-plugin

  • yarn add clean-webpack-plugin --dev

  • 修改配置文件

const { CleanWebpakcPlugin } = require('clean-webpack-plugin')
const path = require('path')
module.exports = {
  entry:'./src/main.js',
  output:{
    filename:'bundle.js',
    path:path.join(__dirname,'dist')
  },
  publicPath:'dist/'
  module:{
    rules:[
      test:/.css$/,
      use:[
        'style-loader',
        'css-loader'
      ]
    ]
  },
  // 需要专门配置一个使用插件的属性,是一个数组。
  // 添加一个插件,就是添加一个元素。
  // 绝大多数插件导出的都是一个类型
  // 所以使用它就是通过一个类型去创建一个实例
  // 然后把这个实例放到数组之中
  plugins:[
    new CleanWebpakcPlugin()
  ]
}
  • 进行编译,查看结果

2. webpack常用插件的介绍--html-webpack-plugin

  • 自动去生成使用打包结果的html

  • 之前都是通过硬编码的方式,写在项目的根目录之下。这种方式会有两个问题

    • 1.如果需要发布,则既需要发布dist目录下的文件,也需要同时发布这个html。而且还需要确保上线过后,html里面的路径引用是正确的。
    • 2.如果webpack打包的配置发生了变化,那么html中的script标签的引用路径,也同时需要去修改。
  • 解决这两个问题的最好办法就是通过webpack自动去生成html文件。这样上线的时候,就只需要把dist发布出去就好了。

  • yarn add html-webpack-plugin

  • 修改配置文件

const { CleanWebpakcPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
module.exports = {
  entry:'./src/main.js',
  output:{
    filename:'bundle.js',
    path:path.join(__dirname,'dist')
    注释这个配置,否则生成的html文件引入的路径不对
    // publicPath:'dist/'
  },
  module:{
    rules:[
      test:/.css$/,
      use:[
        'style-loader',
        'css-loader'
      ]
    ]
  },
  plugins:[
    new CleanWebpakcPlugin(),
    new HtmlWebpackPlugin()
  ]
}
  • 运行打包命令,会自动生成一段html文件放在dist目录下。
  • 更详细的配置
plugins:[
    new CleanWebpakcPlugin(),
    new HtmlWebpackPlugin({
      // 设置html的标题
      title:'Webpack Plugin Sample',
      // meta属性可以用对象的形式去设置标签
      meta:{
        viewport:'width=device-width'
      }
    })
  ]
  • 最后编译的结果

  • 更好的方式是添加一个html的模板,然后根据模版,插件在进行输出

    • 新建src/index.html

    • 对于需要动态生成的部分,用loadsh语法进行插入<%= %>

    • 通过htmlWebpackPlugin.options去获取配置属性,比如htmlWebpackPlugin.options.title

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div class="container">
      <h1><%= htmlWebpackPlugin.options.title %></h1>
    </div>
</body>
</html>
  • 指定模板
plugins:[
    new CleanWebpakcPlugin(),
    new HtmlWebpackPlugin({
      title:'Webpack Plugin Sample',
      meta:{
        viewport:'width=device-width'
      },
      // 指定模板
      template:'./src/index.html'
    })
  ]
  • 除了自定义内容,同时输出多个页面文件,也是一个常见的需求
plugins:[
    new CleanWebpakcPlugin(),
    // 用于生成index.html
    new HtmlWebpackPlugin({
      title:'Webpack Plugin Sample',
      meta:{
        viewport:'width=device-width'
      },
      template:'./src/index.html'
    }),
    // 那么就可以添加多个实例来添加多个页面
    // 用于生成about.html
    new HtmlWebpackPlugin({
      // 指定输出的文件名
      // 这个属性默认是index.html
      filename:'about.html',

    })
  ]

3. webpack常用插件总结

  • 在项目中可能有一些不需要要打包编译的静态文件,它们最终也需要发布到线上

  • 所以需要将这些静态文件复制到输出的目录

  • 对于这种需求,可以通过copy-webpack-plugin来实现

  • yarn add copy-webpack-plugin --dev

  • 修改配置文件

const { CleanWebpakcPlugin } = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
// 导出插件
const CopyWebpackPlugin = require('copy-webpack-plugin')
const path = require('path')
module.exports = {
  entry:'./src/main.js',
  output:{
    filename:'bundle.js',
    path:path.join(__dirname,'dist')
    注释这个配置,否则生成的html文件引入的路径不对
    // publicPath:'dist/'
  },
  module:{
    rules:[
      test:/.css$/,
      use:[
        'style-loader',
        'css-loader'
      ]
    ]
  },
  plugins:[
    new CleanWebpakcPlugin(),
    new HtmlWebpackPlugin(),
    // 同时构造一个实例
    // 参数需要传入一个数组
    // 用来指定需要拷贝的文件路径
    // 可以是通配符'public/**'
    // 也可以是目录或者文件的相对路径
    new CopyWebpackPlugin([
      // 这里是一个目录,就是把整个public下的文件复制到输出目录
      // 不会带着public这个目录
      'public'
    ])
  ]
}

4. webpack开发一个插件

  • 相比于loader,plugin的能力会更加宽泛

  • Plugin通过钩子机制来实现

  • 在webpack工作的过程中会有很多的环节,为了便于插件的扩展,webpack几乎给每个环节都埋下了一个钩子。

  • 这样开发任务的时候,在不同的节点上挂载不同的任务,就可以轻松的扩展webpack的能力。\

  • 具体有哪些预定好的钩子,可以去参考官方的[api文档]

  • webpack要求我们插件必须是一个函数,或者一个包含apply的对象。

  • 一般都会把这个插件定义为一个类型,然后在这个类型中定义一个apply的方法。使用的时候就是通过这个类型构建一个实例,然后去使用

// 定义一个MyPlugin类型
class MyPlugin {
  // 类型中定义一个apply方法
  // 这个方法会在webpack启动后,自动被调用
  apply( compiler ){
    // compiler参数是webpack构建中的核心对象,里面有所有构建的配置信息
    // 同时也是通过这个对象去注册钩子函数
    // 这个插件的需求是,清除构建打包后的js文件中,哪些没必要的注释
    // 了解好需求后,就需要明白注册的时机。
    // 从官网找到一个emit钩子,是即将要往文件输出内容的时候执行
    console.log(compiler)
    // 从compiler的hooks属性访问钩子
    // 通过tap方法去注册一个钩子函数
    // tap方法接受两个参数,一个是插件的名称,另一个挂载到钩子上的函数
    compiler.hooks.emit.tap('MyPlugin', compilation => {
      // compilation是指此次打包过程中的上下文
      // 所有打包过程中的结果,都会放到这个对象当中
      // 通过assets属性,获取即将写入文件的资源信息
      // 因为是一个对象,所以可以通过for in进行遍历
      for( const name in compilation.assets){
        // 这个对象的键就是每个文件的名称
        // 可以通过console.log(name)来查看,并将MyPlugin new到配置中
        // 文件的内容需要通过source方法来获取
        // 进行打印尝试console.log( compilation.assets[name].source() )
        if(name.endsWith('.js'){
          // 判断文件名是否由js结尾
          // 获取文件的内容
          const contents = compilation.assets[name].source()
          // 进行正则替换
          const withoutComments = contents.replace(//**+*//g,'')
          // 将替换完的结果,覆盖到原有的结果当中
          compilation.assets[name] = {
            // 对于新的对象,同样暴露一个source方法,去返回新的内容。
            source: () => withoutComments,
            // 同时还需要一个size方法,去返回内容的大小,webpack要求的必须方法
            size: () => withoutComments.length
          }
        })
      }
    })
  }
}
  • 总结:插件是通过往webpack生命周期的钩子挂载函数实现扩展