三石的webpack(Loader篇)

677 阅读5分钟

概念

loader 是一个Node.js模块,只是必须以函数格式导出来使用;它作用于符合条件的模块并按照一定格式输出符合webpack要求的模块。然后webpack将这些模块打包起来生成对应的js文件

  • 这个函数不可以是 箭头函数,因为 webpack 提供了 loader api 都是挂载到 this 上的。
  • 这个函数必须有 返回值,否则会报错。

使用方式:

  1. 配置方式(推荐):在 webpack.config.js 文件中指定 loader。
  2. 内联方式:在每个 import 语句中显式指定 loader

调用方式:

  • loader 支持链式调用,从右到左(或从下到上)地执行,也就是说,先写的后执行,类似栈。上一个loader的处理结果传给下一个,上一个loader的参数options也可以传给下一个loader
  • loader 可以是同步的,也可以是异步的。

除了常见的通过 package.json 的 main 来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用 loader 字段直接引用一个模块。

loader工作流程

  • webpack.config.js 里配置了一个模块的 Loader;
  • 遇到相应模块文件时,触发了该模块的 loader;
  • loader 接受了一个表示该模块文件内容的 source;
  • loader 使用 webapck 提供的一系列 api 对 source 进行转换,得到一个 result;
  • 将 result 返回或者传递给下一个 Loader,直到处理完毕。

自定义Loader

目标:将代码中的 "world" 替换成指定字符串

// webpack.config.js
module: {
    rules: [
      {
        test: /\.js$/,
        use: [
         {
            loader: path.resolve(__dirname, './myLoaders/first.js'),
            options: {
              name: "三石的世界"
            }
          },
        ]
      },
    ]
  }
// first.js
import { getOptions } from 'loader-utils';

module.exports = function(source, sourceMap?, data?) {
  // 获取到用户给当前 Loader 传入的 options 
  const options = getOptions(this);
  const _source = source.replace('world', options.name )
  return _source
}

目标:替换return 在大多数情况下,我们还是更希望使用 this.callback 方法去导出数据,它有以下几个参数

this.callback(
  err: Error | null, 当loader出错时向外跑出一个Error
  content: string | Buffer, 经过loader编译后需要导出的内容
  sourceMap?: SourceMap, 为方便调试生成的编译后内容的source map
  ast?: AST,本次编译生成的AST静态语法树
);
// first.js
import { getOptions } from 'loader-utils';

module.exports = function(source, sourceMap?, data?) {
  const options = getOptions(this);
  const _source = source.replace('world', options.name )
  this.callback(null, _source)
}

目标:可进行异步操作

  1. 方法1,使用 Promise
// first.js
import { getOptions } from 'loader-utils';

module.exports = function(source) {
  const options = getOptions(this);
  
  function timeout(dalay) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        const _source = source.replace('world', options.name)
        resolve(_source)
      }, delay)
    })
  }
  
  const data = await timeout(1000);
  return data
}
  1. 方法2,通过 this.async 告诉 loader 的解析器这个 loader 将会异步地回调
// first.js
import { getOptions } from 'loader-utils';

module.exports = function(source) {
  const _callback = this.async();
  const options = getOptions(this);
  
  const _source = source.replace('world', options.name)
  setTimeout(() => {
    _callback(null, _source)
  }, 3000)
}

异步 loader 不会影响其他模块的 loader,但是会影响多个 loader 作用于一个模块的 loader
一个 loader 执行完,才会交给下一个 loader 直到没有 loader ,最后交给 webpack
在下例中,异步 first loader 执行完成后,才会执行 second loader

// webpack.config.js
module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: path.resolve(__dirname, './myLoaders/second.js'),
          },
          {
            loader: path.resolve(__dirname, './myLoaders/first.js'),
            options: {
              name: "三石的世界"
            }
          },
        ]
      },
    ]
  }

目标:关闭loader缓存 webpack增量编译机制会观察每次编译时的变更文件,在默认情况下,webpack会对loader的执行结果进行缓存,这样能够大幅度提升构建速度,不过我们也可以手动关闭它(不过为什么呢?)

// first.js
import { getOptions } from 'loader-utils';

module.exports = function(source) {
  this.cacheable(false);
  
  const options = getOptions(this);
  const _source = source.replace('world', options.name )
  this.callback(null, _source)
}

目标:loader的前置执行 在 loader 文件里你可以 exports 一个命名为 pitch 的函数,它会先于 loader 执行

// first.js
import { getOptions } from 'loader-utils';


module.exports.pitch = (remaining, preceding, data) => {
    console.log('***remaining***', remaining)
    console.log('***preceding***', preceding)
    // data会被挂在到当前loader的上下文this上在loaders之间传递
    data.value = "world"
}



module.exports = function(source) {  
  const options = getOptions(this);
  // 替换"world"
  const _source = source.replace(this.data.value, options.name)
  this.callback(null, _source)
}

目标:优化自定义 Loader 路径 如上可以看到,我们引入自定义loader时,都使用绝对路径的引用方式,实在是太繁琐了。
解决方法就是通过配置参数 resolveLoader 来配置 loader 检索路径

// webpack.config.js
resolveLoader: { 
  // 默认是 node_modules 
  modules: ["node_modules", "./myLoaders"]
},
module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          {
            loader: 'second.js',
          },
          {
            loader: 'first.js',
            options: {
              name: "三石的世界"
            }
          },
        ]
      },
    ]
  }

官网参考:编写一个Loader

常见Loader

js相关

babel-loader

将 es6+ 语法转换为 es5语法

cnpm i babel-loader @babel/core @babel/preset-env -D
  • babel-loader 这是使babel和webpack协同工作的模块
  • @bable/core 这是babel编译器核心模块
  • @babel/preset-env 这是babel官方推荐的预置器,可根据用户的环境自动添加所需的插件和补丁来编译Es6代码 三石的未完成webpack.config.js(babel篇)

ts-loader

为webpack提供的 TypeScript loader,打包编译Typescript

安装依赖:

npm install ts-loader --save-dev
npm install typescript --dev

webpack配置:

module.exports = {
  mode: "development",
  devtool: "inline-source-map",
  entry: "./index.ts",
  output: {
    filename: "bundle.js"
  },
  resolve: {
    extensions: [".ts", ".tsx", ".js"]
  },
  module: {
    rules: [
      { test: /.tsx?$/, loader: "ts-loader" }
    ]
  }
};

配置tsconfig.json:

{
  "compilerOptions": {
    // 目标语言的版本
    "target": "esnext",
    // 生成代码的模板标准
    "module": "esnext",
    "moduleResolution": "node",
    // 允许编译器编译JS,JSX文件
    "allowJS": true,
    // 允许在JS文件中报错,通常与allowJS一起使用
    "checkJs": true,
    "noEmit": true,
    // 是否生成source map文件
    "sourceMap": true,
    // 指定jsx模式
    "jsx": "react"
  },
  // 编译需要编译的文件或目录
  "include": [
    "src",
    "test"
  ],
  // 编译器需要排除的文件或文件夹
  "exclude": [
    "node_modules",
    "**/*.spec.ts"
  ]
}

更多配置请看 官网

图片相关

file-loader

用于处理文件类型资源,如jpg,png等图片。返回值为publicPath为准

// file.js
import img from './webpack.png';
console.log(img); // 编译后:https://www.pics.com/webpack_605dc7bf.png

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /.(png|jpe?g|gif)$/i,
        loader: 'file-loader',
        options: {
          // 占位符 [name] 名称,[ext] 后缀
          name: '[name]_[hash:8].[ext]',
          // 资源的使用位置,publicPath + name = css中图片的完整使用路径
          publicPath: "https://www.pics.com",
        },
      },
    ],
  },
};

css文件里的图片路径变成如下:

/* index.less */
.tag {
  background-color: red;
  background-image: url(./webpack.png);
}
/* 编译后:*/
background-image: url(https://www.pics.com/webpack_605dc7bf.png);

url-loader: 

它与file-loader作用相似,也是处理图片的,只不过url-loader可以设置一个根据图片大小进行不同的操作,如果该图片大小大于指定的大小,则将图片进行打包资源,否则将图片转换为base64字符串合并到js文件里。

module.exports = {
  module: {
    rules: [
      {
        test: /.(png|jpg|jpeg)$/,
        use: [
          {
            loader: 'url-loader',
            options: {

              name: '[name]_[hash:8].[ext]',
              // 如果小于10kb则转换为base64打包进js文件
              limit: 10240,
            }
          }
        ]
      }
    ]
  }
}

html-withimg-loader

我们在编译图片时,都是使用file-loaderurl-loader,这两个loader都是查找js文件里的相关图片资源,但是html里面的文件不会查找所以我们html里的图片也想打包进去,这时使用html-withimg-loader

module.exports = {
    module: {
        rules: [
            {
                test: /\.(png|jpeg|jpg)/,
                use: "html-withimg-loader"
            }
        ]
    }
}

// index.html
<html lang="en">
...
<body>
    <h4>我是图片</h4>
    <img src="./src/img/pic.jpg" alt="">
</body>
</html>

image-webpack-loader

用于图片压缩

这个 loader 需要使用 cnpm 才能安装成功。

字体文件的处理和图片是一样的,用的 loader 也是一样的。 字体文件也可以转成 base64 格式的,所以也可以用 url-loader

css相关

style-loader

通过注入<style>标签将CSS插入到DOM中

  • 如果需要将CSS单独提取为一个文件,可使用插件 mini-css-extract-plugin
  • 对于development模式可以使用style-loader,因为它是通过<style></style>标签的方式引入CSS的,加载会更快
  • 不要将 style-loader 和 mini-css-extract-plugin 针对同一个CSS模块一起使用

需要注意loader执行顺序,处理css时,应该将style-loader放到第一位,因为loader都是从下往上执行,最后全部编译完成挂载到style上

css-loader

用于识别.css文件, 仅处理css的各种加载语法(@import和url()函数等),就像 js 解析 import/require() 一样。
处理css必须配合style-loader共同使用,只安装css-loader样式不会生效

less-loader

解析less,转换为css

sass-loader

解析sass,转换为css

postcss-loader

PostCSS 是一个允许使用 JS 插件转换样式的工具集。 这些插件可以检查 CSS,支持 CSS Variables 和 Mixins, 编译尚未被浏览器广泛支持的先进的 CSS 语法,内联图片,以及其它功能。

其中, autoprefixer 插件添加了浏览器前缀,使用 Can I Use 上面的数据。

安装依赖:

npm install postcss-loader autoprefixer --save-dev

webpack配置:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const isDev = process.NODE_ENV === 'development';

module.exports = {
  module: {
    rules: [
      {
        test: /\.(css|less)$/,
        exclude: /node_modules/,
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
            }
          },
          {
            loader: 'postcss-loader'
          },
          {
              loader: 'less-loader',
              options: {
                  lessOptions: {
                      javascriptEnabled: true
                  }
              }
          }
        ]
      }
    ]
  }
}

postcss配置: 在项目根目录创建postcss.config.js,并且设置支持哪些浏览器,必须设置支持的浏览器才会自动添加添加浏览器兼容

// postcss.config.js

module.exports = {
  plugins: [
    require('precss'),
    require('autoprefixer')({
      'browsers': [
        'defaults',
        'not ie < 11',
        'last 2 versions',
        '> 1%',
        'iOS 7',
        'last 3 iOS versions'
      ]
    })
  ]
}

html相关

html-loader

有时候想引入一个html页面代码片段赋值给DOM元素内容使用,这时就用到html-loader

建议安装低版本,高版本可能会不兼容导致报错。

cnpm i html-loader@0.5.5 -D
// index.js

import Content from "../template.html"

document.body.innerHTML = Content
module.exports = {
    module: {
        rules: [
            {
                test: /.html$/,
                use: "html-loader"
            }
        ]
    }
}

文件相关

markdown-loader

markdown编译器和解析器

webpack配置: 只需将 loader 添加到您的配置中,并设置 options。

// file.js

import md from 'markdown-file.md';
// webpack.config.js
const marked = require('marked');
const renderer = new marked.Renderer();

module.exports = {
  module: {
    rules: [
      {
        test: /.md$/,
        use: [
            {
                loader: 'html-loader'
            },
            {
                loader: 'markdown-loader',
                options: {
                    pedantic: true,
                    renderer
                }
            }
        ]
      }
    ],
  },
};

raw-loader

可将文件作为字符串导入

// index.js
import txt from './file.txt';
// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /.txt$/,
        use: 'raw-loader'
      }
    ]
  }
}

性能相关

thread-loader

作用

把这个 loader 放置在其他 loader 之前, 放置在这个 loader 之后的 loader 就会在一个单独的 worker 池(worker pool)中运行

在 worker 池(worker pool)中运行的 loader 是受到限制的。例如:

  • 这些 loader 不能产生新的文件。
  • 这些 loader 不能使用定制的 loader API(也就是说,通过插件)。
  • 这些 loader 无法获取 webpack 的选项设置。 每个 worker 都是一个单独的有 600ms 限制的 node.js 进程。同时跨进程的数据交换也会被限制。

所以,一般仅在耗时的 loader 上使用

安装

npm install --save-dev thread-loader

配置

module.exports = {
  module: {
    rules: [
      {
        test: /.js$/,
        include: path.resolve('src'),
        use: [
          {
            loader: 'thread-loader',
            // 有同样配置的 loader 会共享一个 worker 池
            options: {
              // 产生的 worker 的数量,默认是 cpu 的核心数
              workers: 2,

              // 一个 worker 进程中并行执行工作的数量
              // 默认为 20
              workerParallelJobs: 50,

              // 额外的 node.js 参数
              workerNodeArgs: ['--max-old-space-size', '1024'],

              // 闲置时定时删除 worker 进程
              // 默认为 500ms
              // 可以设置为无穷大, 这样在监视模式(--watch)下可以保持 worker 持续存在
              poolTimeout: 2000,

              // 池分配给 worker 的工作数量
              // 默认为 200
              // 降低这个数值会降低总体的效率,但是会提升工作分布更均一
              poolParallelJobs: 50,

              // 池的名称
              // 可以修改名称来创建其余选项都一样的池
              name: 'my-pool',
            },
          },
          ,
          'expensive-loader',
        ],
      },
    ],
  },
};

当项目较小时,使用多进程打包反而造成打包时间延长,因为进程之间通信产生的开销比多进程能够节约的时间更长

cache-loader

在一些性能开销较大的 loader 之前添加 cache-loader,将结果缓存中磁盘中。默认保存在 node_modueles/.cache/cache-loader 目录下。

module.exports = { 
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                use: ['cache-loader','babel-loader']
            }
        ]
    }
}

参考

吐血整理的webpack入门知识