Loader工作原理

前端工程化模块化开发Webpack

Loader是webpack的核心特性,借助于Loader就可以加载任何类型的资源。

webpack内部的loader只能处理js文件,如果去处理css等其他类型的文件会报错,我们需要使用其他的Loader加载器会处理其他类型的文件。

# Loader原理

  • Loader负责资源文件从输入到输出的转换
  • Loader有管道的概念,对于同一个资源可以依次使用多个Loader处理

# webpack常用加载器分类

webpack加载器有点像工厂车间,用来处理加工打包过程中遇到的资源文件,常用的加载器有:

加载器分类 描述
编译转换类 将加载到的资源模块转换成JavaScript代码,例如:css-loader(将css代码 -> bundle.js文件中以JS形式工作的CSS模块)
文件操作类 文件加载的资源模块拷贝到输出目录,又将访问路径向外导出,例如:file-loader
代码检查类 对加载到的资源文件进行代码校验,目的是为了统一代码风格,提高代码质量,这种加载器不会去修改生产环境的代码,例如:eslint-loader

# 常用Loader

# css-loader

css-loader的作用是将css文件转化成一个js模块

  1. 安装css-loader,npm i css-loader --save-dev
  2. 在webpack.config.js里面添加配置
const path = require('path')
module.exports = {
  entry: './src/main.css',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output')
  },
  mode: 'none',
  module: {
    // 其他模块加载规则的配置,数组
    rules: [{
      // 匹配文件打包路径
      test: /\.css$/,
      // 匹配到的文件所使用的loader,如果是单个的直接写字符
      use: 'css-loader' 
    }]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  1. 命令行执行npm run build

这个时候虽然编译不报错,但是使用的时候样式并没有生效,是因为css-loader只是转化了代码,但是并没有追加到style标签里,这里还需要用到style-loader

# style-loader

style-loader的作用是将已经成为js模块的css代码通过<style>标签的形式加载到页面上

  1. 安装style-loader,npm i style-loader --save-dev
  2. 在webpack.config.js里面这样写
const path = require('path')
module.exports = {
  entry: './src/main.css',
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output')
  },
  mode: 'none',
  module: {
    rules: [{
      test: /\.css$/,
      // 匹配到的文件所使用的loader,如果是单个的直接写字符串,如果是多个的,需要使用到数组,而且使用顺序从后往前,css-loader要写在style-loader的后面
      use: [
        'style-loader',
        'css-loader' 
      ]
    }]
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  1. 命令行执行npm run build,打开页面可以看到样式生效

# PS: webpack导入资源模块

上面我们可以加载css模块资源,入口文件entry写的是main.css,但是目前而言,前端的业务是由JavaScript整体驱动,但是正确 的做法都是webpack的打包入口设为js文件(打包入口就是运行入口),然后在入口文件中import引用css文件。

  1. 在webpack.config.js的entry改为./main.js
  2. 在main.js里面引入mian.css
import createHeading from './heading.js'
// 直接执行
import './main.css'
const heading = createHeading()

document.body.append(heading)
1
2
3
4
5
6
  1. 在同名文件中添加heading.css文件,编写样式
.heading {
  padding: 20px;
  background-color: #fff;
  color: #333;
}
1
2
3
4
5
  1. 在heading.js中引入文件并给标签添加类名
// 引入heading的css文件
import './heading.css'
export default () => {
  const element = document.createElement('h2')

  element.textContent = 'hello world'
  // 给标签添加类名
  element.classList.add('heading')
  element.addEventListener('click', () => {
    alert('hello webpack')
  })

  return element
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 命令行运行npm run build,可以看到样式还可以正常运行。

传统我们要把css和js文件分离开,单独维护单独引入,但是webpack要求我们将css文件引入到js文件中。 webpack不仅仅要求我们引入css文件,它要求我们在js文件中引入所有我们需要的动态资源。

真正需要资源的不是应用,而是我们目前正在编写的代码 代码要正常工作就必须要加载对应的资源,这是webpack的思维,这个对于js和其他资源文件建立依赖关系是有很明显的优势的。

javascript代码本身负责整个业务的功能,放大就是驱动了整个前端应用。在实现功能的时候可能会用到样式图片之类的资源文件,如果建立这种依赖关系,就会觉得逻辑比较合理。

  • 逻辑合理,js完成功能本身需要这些资源
  • 确保线上资源不缺失,且都是必要的

# file-loader

文件资源加载器

  1. 安装npm i file-loader --save-dev
  2. 在文件中使用
//main.js
import createHeading from './heading.js'
import './main.css'
// 这里需要接收一下这个资源的默认导出,这个导出是打包过后这个图片的资源路径
import icon from './icon.png'
const heading = createHeading()
document.body.append(heading)

// 创建一个image对象,然后将路径赋值给这个图片的src
const img = new Image()
img.src = icon
document.body.append(img)
1
2
3
4
5
6
7
8
9
10
11
12
  1. 在webpack中配置loader
  module: {
    // 其他模块加载规则的配置,数组
    rules: [{
      test: /\.css$/,
      use: [
        'style-loader',
        'css-loader' 
      ]
    },{
      // 匹配图片文件使用file-loader文件加载器
      test:/\.png$/,
      use: 'file-loader'
    }]
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  1. 运行如果加载不出来可以指定文件资源的根目录
// webpack.config.js
output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'output'),
    // 默认空字符串表示当前根目录,这里可以指定,output后面的 / 不能省略
    publicPath: 'output/'
  }
1
2
3
4
5
6
7
  1. 再次尝试运行成功就可以看到图片被加载出来了

# 总结工作过程

  • webpack在打包的时候遇到了文件资源,然后在配置文件中的配置匹配到对应的文件加载器
  • 文件加载器先把导入的文件拷贝到输出目录,然后将文件拷贝到输出目录的路径作为当前模块的返回值返回
  • js所需要的文件资源就发布出来了,我们还可以通过返回的模块成员也拿到了返回值路径,引用之后图片正常显示

image

# URL Loader

# Data URLs

Data URLs是一种特殊URL协议,可以直接用来表示一个文件。

  • 传统的URL:要求服务器上有一个对应的文件,我们请求服务器地址得到这个对应的文件
  • Data URLs:当前URL就可以直接表示这个文件内容,我们在使用的时候就不会发送任何的HTTP请求

image

举个例子:

这是一个html的内容,编码是UTF-8,内容是一个包含h1标签的代码

data:text/html;charset=UTF-8,<h1>html content</h1>

如果是图片或者文字这种无法用文本表示的二进制类型的文件,我们可以将文件的内容进行base64编码,以base64编码之后的字符串表示这个文件的内容。

举个例子: 这是一个png类型的文件,编码是base64 data:img/png;base64,iVBPR ... SuQmCC

# url-loader

  1. 安装npm i url-loader --save-dev
  2. 在webpack.config.js中修改
module: {
    rules: [{
        test: /\.css$/,
        use: [
            'style-loader',
            'css-loader' 
        ]
    },{
        // 匹配图片文件使用url-loader加载器
        test:/\.png$/,
        use: 'url-loader'
    }]
}
1
2
3
4
5
6
7
8
9
10
11
12
13

这个时候webpack在打包的时候,就会将png图片以url-loader加载成为Data URLs的形式。

image

这种加载方式很适合体积比较小的资源,因为体积比较大的资源会造成打的包大小过大,从未影响运行次数

# 最佳实践

  • 小文件使用Data URLs,减少请求次数
  • 大文件单独提取存放,提高加载速度

在webpack里面配置大于10kb的文件仍然使用file-loader加载单独存放,小于10kb的使用url-loader加载转换为Data URLs嵌入代码中

module: {
    rules: [{
      test: /\.css$/,
      use: [
        'style-loader',
        'css-loader' 
      ]
    },{
      test:/\.png$/,
      // 这里要传一个对象,options里面要配置最大限定大小,超过的仍然使用file-loader处理
      use: {
        loader:'url-loader',
        options: {
          limit: 10 * 1024 // 这里显示字节,10KB
        }
      }
    }]
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

ps: 对于使用url-loader,如果这样使用必须安装file-loader

# babel-loader

因为webpack中可以用export和import,所以很多人认为webpack可以使用es6代码,其实不是。

webpack可以使用export和import是因为模块打包需要,所以会对这两个进行转换,除此之外,并不能转换其他的ES6特性。

如果要对ES6进行转换,需要添加一个编译型loader,最常见的就是babel-loader

  1. 安装npm i babel-loader @babel/core @babel/preset-env --save-dev
  2. 在配置文件webpack.config.js中指定加载器
module: {
    rules: [{
      test: /\.js/,
      // babel-loader只是一个平台,单独写是不会进行转化的,需要使用插件
      // preset-env是一个插件集合,用来处理新特性的转化
      use: {
        loader: 'babel-loader',
        options: {
          presets: ['@babel/preset-env']
        }
      }
    },{
      test: /\.css$/,
      use: [
        'style-loader',
        'css-loader' 
      ]
    },{
      test:/\.png$/,
      use: {
        loader:'url-loader',
        options: {
          limit: 10 * 1024 // 10KB
        }
      }
    }]
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  1. 在命令行运行npm run build可以看到生成的bundle.js文件已经成功转化

# 总结

  • webpack只是打包工具,不对代码进行编译转化
  • 加载器可以用来编译转换代码

# html-loader

  1. 安装npm i html-loader --save-dev
  2. 编写html文件
<!--footer.html-->
<footer>
  <img src="icon.png" alt="footer" width="256"/>
</footer>
1
2
3
4
  1. 在配置文件中webpack.config.js中配置
module: {
    rules: [{
      test: /\.html$/,
      use: {
        loader: 'html-loader'
      }
    }]
  }
1
2
3
4
5
6
7
8

4.在main.js中引用

import footer from './footer.html'
document.write(footer)
1
2
  1. 命令行运行npm run build可以看到图片正常加载
  2. 如果添加a标签的href
<footer>
  <img src="icon.png" alt="footer" width="256"/>
  <a href="icon.png">click me~</a>
</footer>
1
2
3
4

点击按钮,跳转资源会404,因为html-loader只会默认处理img的src标签的打包,如果我们要处理其他标签的属性也进行打包处理,需要在配置文件中额外配置

module: {
    // 其他模块加载规则的配置,数组
    rules: [{
      //加载html资源
      test: /\.html$/,
      use: {
        loader: 'html-loader',
        options: {
          // 默认只有img:src,如果要对其他属性进行处理这里需要添加
          // attrs: ['img:src', 'a:href']这是旧版本的写法,新版本这么写
          attributes: {
            list: [
              {
                tag: 'img',
                attribute: 'src',
                type: 'src'
              },
              {
                tag: 'a',
                attribute: 'href',
                type: 'src'
              }
            ]
          }
        }
      }
    }]
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

之后a链接跳转可以正常显示。

# 动手实践:开发一个自己的Loader:markdown-loader

目的:可以在代码中直接导入markdown文件,导入的结果就是转换过后的HTML字符串

import about from './about.md'

console.log(about) // 转换过后的HTML字符串
1
2
3
  1. 安装npm i marked --save-dev
  2. 在webpack.config.js目录下添加一个markdown-loader.js
const marked = require('marked')
// 导出一个函数,输入是资源文件的内容
module.exports = source => {
  console.log(source)
  // 这个值就是html字符串,也就是转换过后的结果
  const html = marked(source)
  return html
}
1
2
3
4
5
6
7
8
  1. 在webpack.config.js中配置
{
  test: /\.md$/,
  // 这里除了可以写npm包名称,也可以写相对路径
  use: './markdown-loader'
}
1
2
3
4
5
  1. 准备about.md
# 目录
## 目标
自己做一个markdown-loader
1
2
3
  1. 在main.js中引用
import about from './about.md'
document.write(about)
1
2

尝试运行会正常输出,但是会报错,原因是,webpack内部像一个管道,可以在过程中依次使用过个loader,但是要求结果必须是一个JavaScript代码,这里返回的html结构不是一个JavaScript代码,所以解决办法有两种:

# html结构换成JavaScript代码

  1. 导入模块,并使用
const marked = require('marked')
module.exports = source => {
  const html = marked(source)
  // 不能直接导出html变量,因为必须要转换成JavaScript代码
  // 这里使用stringify转换成一个标准的JSON字符串,其换行符和引号都会正确转义
  // 然后再对其进行拼接,就不会有问题了
  return `module.exports = ${JSON.stringify(html)}`
  // or ES Modules的形式也可以
  return `export default ${JSON.stringify(html)}`
}
1
2
3
4
5
6
7
8
9
10

这里在网页中显示正常

# 使用其他的loader对代码进行处理

  1. 这里将html直接导出,安装一个npm i html-loader --save-dev,在webpack.config.js中使用
{
  test: /\.md$/,
  //先执行markdown-loader,再执行html-loader
  use: [
    'html-loader',
    './markdown-loader'
  ]
}
1
2
3
4
5
6
7
8
更新时间: 2021-12-18 20:56