Webpack

功能包括

  • 模块打包器(Module bundler)
  • 模块加载器(Loader)
  • 代码拆分(Code Splitting)模块增量加载,渐进式加载
  • 资源模块(Asset Module)

打包工具解决的是前端整体的模块化,并不是单指JavaScript模块化

安装,因为是基于npm的包,所以

  • yarn init --yes
  • yarn add webpack webpack-cli --dev (^4.40.2, ^3.3.9)
  • yarn webpack --version 查看版本
  • yarn webpack 开始打包,webpack自动从src/index.js开始打包

4.0以后webpack执行支持零配置打包,将src/index.js作为打包入口 -> dist/main.js生成地址

添加配置文件 webpack.config.js,运行在node环境的js文件,按CommonJs方式编写代码

image.png

# webpack.config.js

const path = require('path')

module.exports = {
  entry:'./src/main.js', //项目打包入口文件路径
  output:{               //输出文件配置,是个对象
    filename:'bundle.js', //输出文件名称
    path:path.join(__dirname,'output')         //输出文件所在目录,必须是absolute path!
  },
  mode:'development' //webpack4以上 工作模式(不同环境的几组预设配置)
  //默认是production,自动启动优化,优化打包结果
  //development 优化打包速度,添加一些调试过程需要的辅助
  //none 原始打包不会进行额外处理
  // 使用yarn webpack --mode development执行
}

资源打包

非js代码通过loader加载,Webpack内部的loader默认只能处理js,json文件
image.png

yarn add css-loader --dev (^3.2.0)

yarn add style-loader --dev (^1.0.0)

const path = require('path')

module.exports = {
  entry:'./src/main.css', //项目打包入口文件路径
  output:{               //输出文件配置,是个对象
    filename:'bundle.js', //输出文件名称
    path:path.join(__dirname,'dist')         //输出文件所在目录,必须是absolute path!
  },
  mode:'none',//production,development
  module: {
    rules:[ //除js外其他资源模块加载规则配置
      {
        test:/.css$/,
        use:['style-loader','css-loader'] //匹配到的文件使用的loader
      }
    ]
  }
}

loader内的rules,use从后向前执行,通过style标签挂载到html上

image.png

导入资源模块

上面导入css通过修改entry入口文件,但是一般情况下入口文件还是js文件,打包的入口就相当于运行的入口,JS驱动整个前端应用业务,
通过入口文件引入 css的方式也能达到目的

# main.js

import createHeading from './heading.js'

import './main.css'

const heading = createHeading()

document.body.append(heading)

根据代码需要动态导入资源,例如js中引入css

为什么不是js,css分离的方式?

js可以比作驱动文件,而css和样式是辅助js进行美化的,因此在js中引入css逻辑更合理,而且js确实需要这些资源文件

确保上线资源不缺失,都是必要的

文件资源加载器

文件图片资源加载方式通过yarn add file-loader --dev (^4.2.0)

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    // publicPath: 'dist/' //默认值''网站的根目录
  },
  module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /.png$/,
        use: 'file-loader'
      }
    ]
  }
}

然而图片资源并没有被加载出来,查看下加载路径

image.png

默认加载网站根目录下的资源(应该加载dist下的资源),这是由于index.html没有生成到dist目录,而是放在项目的根目录下,所以把项目根目录作为网站根目录,
而webpack会默认认为所有打包结果都放在网站的根目录下面,解决方法配置publicPath: 'dist/'

webpack打包时遇到图片文件,根据配置文件配置,匹配到文件加载器,文件加载器开始工作,先将导入的文件copy到输出目录,然后将文件copy到输出目录过后的路径,作为当前模块的返回值返回,这样资源就被发布出来了。不理解可以看下bundle.js文件

URL加载器

data urls是特殊的url协议,可以用来直接表示一个文件,传统url要求服务器有一个对应文件,然后我们通过请求这个地址得到这个对应文件。 而data url是一种当前url就可以直接表示文件内容的方式,这种url中的文本包含文件内容

使用时不会发送任何http请求

data:text/html;charset=UTF-8,<h1>xxx</h1> //html内容,编码utf-8

对于图片,字体无法直接通过文本去表示的二进制类型文件,通过将文件内容进行base64编码,编号后的字符串表示文件内容

data:image/png;base64,iVBORw0KGgoAAAANSUhE... //png类型文件,编码base64,编码

webpack打包静态资源模块时,通过data urls我们可以用代码形式表示任何类型文件了

yarn add url-loader --dev (^2.2.0)

module: {
    rules: [
      {
        test: /.css$/,
        use: [
          'style-loader',
          'css-loader'
        ]
      },
      {
        test: /.png$/,
        use: 'url-loader'//将其转化为 dataurl 形式
      }
    ]
  }

适合体积比较小的资源,小文件使用data urls减少请求次数,大资源会导致生成的文件体积过大,影响加载效率,因此大文件用file-loader单独提取存放,提高加载速度 ,因此为loader添加options

module: {
  rules: [
    {
      test: /.css$/,
      use: [
        'style-loader',
        'css-loader'
      ]
    },
    {
      test: /.png$/,
      // use: 'url-loader'//将其转化为 dataurl 形式
      use:{
        loader:'url-loader',
        options:{ //loader配置选项
          limit:10*1024 //10kb
        }
      }
    }
  ]
}

需要注意的如果你使用url-loader的话,一定要同时安装file-loader,url-loader对于超出10kb的文件还是会调用file-loader

常用加载器分类

  1. 编译转换类
  2. 文件操作类
  3. 代码检查类

对写的代码进行校验的加载器,目的统一代码风格,提高代码质量,一般不会修改生产环境代码

webpack与es2015

webpack因为模块打包需要,所以处理了import和export,但是并不能转化代码中其他的es6特性

yarn add babel-loader @babel/core @babel/preset-env --dev (^8.2.2, ^7.14.3, ^7.14.2)

module: {
  rules: [
    {
      test:/.js$/,
      use:{
        loader:'babel-loader',
        options:{
          presets:['@babel/preset-env']
        }
      }
    },
    {
      test: /.css$/,
      use: [
        'style-loader',
        'css-loader'
      ]
    },
    {
      test: /.png$/,
      // use: 'url-loader'//将其转化为 dataurl 形式
      use:{
        loader:'url-loader',
        options:{ //loader配置选项
          limit:10*1024 //10kb
        }
      }
    }
  ]
}

webpack只是打包工具,loader加载器可以用来编译转换代码

模块加载方式

模块加载的方式有遵循ES Modules标准的import声明,遵循CommonJS 标准的require函数,遵循AMD标准的define函数和require函数,建议不要混着用,会降低项目可维护性

除了上面方式,触发模块加载方式还有css样式代码中的url函数和@import指令,html代码中图片标签的src属性

css样式文件中 url 触发模块加载

# main.css

body {
  min-height: 100vh;
  background: #f4f8fb;
  background-image: url(background.png);
  /* url触发了模块加载,注意下url-loader版本 2.2.0,现在4不显示 */
  background-size: cover;
}

@import url(reset.css)
# reset.css

* {
  margin:0;
  padding:0;
}
# main.js

import 'main.css'

html中的src属性触发模块加载

yarn add html-loader --dev (^0.5.5)

# footer.html

<footer>
  <img referrerpolicy="no-referrer" src="better.png" alt="">
</footer>
# main.js

//html文件默认会将html代码作为字符串导出,还需要为html模块配置loader
import footerHtml from './footer.html' 
document.write(footerHtml)
# webpack.config.js

{
  test:/.html$/,
  use:{
    loader:'html-loader'
  }
}

但是html-loader只能处理html 下 img:src 属性,其他额外属性通过attrs 配置

{
  test:/.html$/,
  use:{
    loader:'html-loader', //默认只能处理html img src属性,其他额外属性通过attrs配置
    options:{ 
      attrs:['img:src','a:href']
    }
  }
}

这个文件可以被处理了

# footer.html

<footer>
  <!-- <img src="better.png" alt=""> -->
  <a href="better.png">download png</a>
</footer>

核心工作原理

项目中散落各种各样代码及资源,webpack会根据我们的配置,找到其中一个文件作为打包的入口,一般情况下这个文件都是.js文件,然后他会顺着我们入口文件中的代码,根据代码中出现的import或require之类的语句,解析推断出这个文件所依赖的资源模块,然后分别去解析每一个资源模块对应的依赖,最后形成了依赖关系的依赖树,webpack会递归这个依赖树,找到每个节点对应的资源文件,最后根据配置文件中的rules属性,去找到模块对应的加载器,交给对应的加载器去加载这个模块,最后将加载到的结果放入到bundle.js中,从而实现整个项目打包。

整个过程中,loader机制事webpack的核心

开发一个loader

创建一个markdown-loader, 引入.md文件,输出转化后的html

先在根文件夹下创建配置和loader文件

image.png

每个webpack loader都需要导出一个函数,这个函数就是loader对加载到的资源的处理过程

输入就是加载到的资源文件的内容,输出就是此次加工过后的结果

# markdonw-loader.js

module.exports = source => {

  // console.log(source);

  //webpack加载资源过程类似于工作管道,可以在加载过程中依次使用多个loader,但是最终结果必须是js代码
  // return 'hello ~'   //x
  // return 'console.log("hello ~")'  //√


  //yarn add marked --dev (markdown解析模块)
  const html = marked(source) //返回值是html字符串

  
  // return html //面临上面同样问题,正确做法把他变成js代码

  // 方式1
  // html作为模块导出的字符串,通过module.exports = 这样一个字符串,
  // 但是简单拼接的话,html中存在的换行符或者引号拼接一起会造成语法错误

  // return `module.exports = ${html}` //x

  // return `module.exports = ${JSON.stringify(html)}` //webpack会解析模板字符串中的js代码

  // return `export default  ${JSON.stringify(html)}` //可以使用esm语法

  //方式2
  // 返回html 字符串,然后再向管道中添加一个loader处理字符串
  // 交给下一个loader处理,需要安装 html-loader (组合loader的形式)

  return html
}
# webpack.config.js

const path = require('path')

module.exports = {
  mode: 'none',
  entry: './src/main.js',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'dist'),
    publicPath: 'dist/'
  },
  module:{
    rules:[
      {
        test:/.md$/,
        use:[  
          'html-loader',
          './markdown-loader' //模块名称或文件路径,类似nodejs的require
        ]
      }
    ]
  }
}

当然也可以把自定义loader发布到npm包上

总结

loader负责资源文件从输入到输出的转换,实际上是一种管道概念,对同一资源可以依次使用多个loader

插件机制

增强webpack自动化能力

loader专注实现资源模块加载,plugin解决其他自动化工作

e.g. 清除dist目录

e.g. 拷贝静态文件至输出目录

e.g. 压缩输出代码
webpack+plugin 实现了绝大多数前端工程化工作

自动清除目录插件

yarn add clean-webpack-plugin --dev第三方插件

# webpack.config.js

const {CleanWebpackPlugin} = require('clean-webpack-plugin')

plugins:[
  new CleanWebpackPlugin()
]

自动生成使用bundle.js的HTML

yarn add html-webpack-plugin --dev第三方插件(^3.2.0)

dist下生成html,解决以前根下html中的引入路径发生改变需要硬编码的问题(通过 publicpath解决的),现在通过动态注入。

# webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins:[
    new HtmlWebpackPlugin()
]

会在dist/生成 index.html文件。注意要去掉publicpath,因为此时index在dist下,我们默认把根目录设置成了dist/ ,访问时浏览器会把index.html所在的路径设置为根路径,至此我们就可以删除项目中的index.html了,通过webpack自动生成

改进html-webpack-plugin生成结果

默认生成在index.html,如果想自定义修改通过配置。例如修改html的title,自定义html基础dom结构,html-webpack-plugin参数参考文章

# webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins:[
  new HtmlWebpackPlugin({
    title:'Webpack Plugin Sample',
    meta:{  //设置页面中元数据标签
      viewport:'width=device-width'
    },
    template:'./src/index.html'//对于大量的配置通过创建模板文件,根据模板生成页面
  })
]

模板文件

# ./src/index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Webpack</title>
</head>
<body>
  <div class="container">
    <!-- 访问插件配置数据 -->
    <h1><%= htmlWebpackPlugin.options.title %></h1>
  </div>
</body>
</html>

同时输出多个页面文件

除非是单页面应用,否则需要输出多个html,通过添加多个实例对象到plugins中

# webpack.config.js

  plugins:[
    //用于生成index.html
    new HtmlWebpackPlugin({
      title:'Webpack Plugin Sample',
      meta:{ 
        viewport:'width=device-width'
      },
      template:'./src/index.html'
    }),

    //用于生成about.html
    new HtmlWebpackPlugin({
      filename:'about.html' //默认的filename是index.html
    })
  ]

静态文件拷贝插件 copy-webpack-plugin

yarn add copy-webpack-plugin --dev (^5.0.4)

拷贝根目录下 public/xx.ico静态文件至dist

# webpack.config.js

const CopyWebpackPlugin = require('copy-webpack-plugin')

plugins:[
  //参数数组,指定拷贝的文件路径(通配符,目录,相对路径)
  new CopyWebpackPlugin([ 
    // 'public/**'
    'public'
  ])
]

总结

每个项目用到的,常见的plugin

  • clean-webpack-plugin
  • html-webpack-plugin
  • copy-webpack-plugin

通过github查看特性,做到心中有数。其他插件通过github特殊使用时特殊查找,例如imagemin-webpack-plugin 图片压缩

插件机制工作原理

相比于loader(加载模块时候),plugin(涉及webpack工作每个环节)拥有更宽的能力范围

webpack plugin机制就是软件开发中最常见的钩子机制,通过钩子机制实现。

钩子机制类似于web中的事件。在webpack工作过程中会有很多环节,为了便于插件的扩展,webpack几乎给每一个环节都埋下了钩子,这样我们在开发插件时,可以通过往这些不同的节点上去挂载不同任务,就可以轻松扩展webpack的能力。具体有哪些预先定义好的钩子参考文档

image.png

webpack开发一个插件

定义一个插件往钩子上挂载任务,这个插件用于清除bundle.js中无用的注释//

webpack要求插件必须是一个函数或者是一个包含apply方法的对象,
一般我们都会把插件定义为一个类型,然后再这个类型中定义一个apply方法
使用时通过类型构建实例去使用

# webpack.config.js

class MyPlugin{

  apply (compiler){ // 此方法在webpack启动时自动被调用,compile配置对象,配置信息

    console.log('MyPlugin 启动');

    // 通过hooks属性访问钩子emit 
    // 参考:https://webpack.docschina.org/api/compiler-hooks/
    // tap方法注册钩子函数(参数1:插件名称,参数2:挂载到钩子上的函数)

    compiler.hooks.emit.tap('MyPlugin',compilation=>{

      // compilation 可以理解为此次打包的上下文,所有打包过程产生的结果都会放到这个对象中
      // compilation.assets属性是个对象,用于获取即将写入目录当中的资源文件信息

      for(const name in compilation.assets) 
      {

        // console.log("文件名称:",name); 如图
        // console.log("文件内容:",compilation.assets[name].source());
        
        if(name.endsWith(".js")){

          //获取内容
          const contents = compilation.assets[name].source();

          //替换内容
          const withoutComments = contents.replace(/\/\*\*+\*\//g,'')

          //覆盖老内容
          compilation.assets[name] = {

            source:()=> withoutComments,
            size:()=> withoutComments.length //返回内容的大小,webpack要求必须加
          }
        }
      }
    }) 
  }
}

// 使用自定义插件
plugins:[
  new MyPlugin()
]

从上面的类我们了解了插件是通过在生命周期的钩子中挂载函数扩展


mcgee0731
60 声望4 粉丝

不会做饭的程序猿不是一个好厨子


« 上一篇
模块化
下一篇 »
Rollup个人笔记