webpack开发自定义功能loader

3,400 阅读5分钟

webpack loader 开发

概述

使用webpack来开发一些自定义的loader,可以让我们更加深刻的认识和了解webpack的工作机制是怎么样的。 每一个loader都是一个函数,该函数在loader转换资源的时候调用。在该函数中可以使用this来访问上下文环境并可以使用一些 loader API, 比如 this.version表loader api的版本号、 this.context表模块所在目录、this.request表被解析出来的erquest字符串、this.query获取配置option字段...等等

loader原则

一般loader开发都会遵循一些原则,比如单一性即每个loader只做单一功能的事情,对于复杂功能应该使用多个loader相互组合的方式来实现。比如我们在开发过程中一般使用 less or scss来写页面的样式。但最终渲染到浏览器中的是css,从less/scss到css至少需要less-load将less编译为css,css-loader将css中的 @import 和 url('./././)中的相对路径改为 require()的格式, style-loader: 将最后的样式使用style标签插入到页面head中 经过三个loader的配合使用最终将我们less/scss转化了浏览器可识别的css. 这样做的好处就是将功能隔离,代码解耦,可以按照所需功能来自由使用所需loader。一般开发loader需要遵循下面这些主要的原则:

  1. 简单原则: loaders 应该只做单一任务。这不仅使每个 loader 易维护,也可以在更多场景链式调用。
  2. 链式:上一个loader处理之后的结果被下一个loader接受处理
  3. 模块化(modular):保证输出模块化。loader 生成的模块与普通模块遵循相同的设计原则。
  4. 无状态(stateless) : 确保 loader 在不同模块转换之间不保存状态。每次运行都应该独立于其他编译模块以及相同模块之前的编译结果。
  5. loader 工具库(Loader Utilities):充分利用 loader-utils 包。它提供了许多有用的工具,比如最常用的一种工具是获取传递给 loader 的选项。

webpack.config.js 设置

我们可以在配置文件中的 module字段之下的rule中添加匹配文件规则,然后对匹配正确的文件使用那些loader,这个套路和我们正常使用loader基本类似,只不过如果在开发阶段loader的路径需要设置为本地目录一般使用 path.resolve,一般长这个样子:

module.exports = {
  //...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: path.resolve('path/to/loader.js'),
            options: {/* ... */}
          }
        ]
      }
    ]
  }
};

如果需要执行多个loader那可以在use数组中追加其他loader即可,需要注意配置的loader执行的顺序也是从右到左,从下到上。

编写自定义loader

按照同异步可以将loader区分为同步loader和异步loader。如果是单个处理结果,可以直接使用同步模式处理直接返回,但如果需要多个结果处理,就必须在异步loader中使用 this.async()告诉当前的上下文context,这是一个异步的loader,需要loader runner 等待异步处理的结果,在异步处理完之后再调用this.callback()传递给下一个loader执行。 这里实现一个小功能,即自己写一个less-laoder和style-loader。将编写的less经过这两个loader处理之后使用style标签插入在页面的head标签内。并在style标签中自定义一个data-origin属性,用来标记这个样式是来自于哪个文件的,以往我做过一些项目都是将很多文件的样式插入在head标签中,可是有的时候需要调试样式就不知道这个样式来自于哪个文件,这里我们在处理的时候会把文件的来源标记在style标签中。

准备

目录结构

这里新建一个webpack 4.0以上的工程,配置完基本的出入口字段之后,新建一个或者多个less文件,并被入口文件引入。 比如这里在工程根目录src文件夹下新建了一个index.less的文件写入简单样式如下:

@red:red;
@yellow:yellow;
@baseSize:20px;
body{
  background-color: @red;
  color:@yellow;
  font-size: @baseSize;
}

上面是一写简单的定义body标签的样式。 在本地新建一个loader文件夹用于存放我们自定义loader文件。

webpack.config.js 配置自定义loader

按照上面介绍过的在开发阶段可以在webpack.config.js文件中按照下面的方式配置来测试我们写的loader。

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: "bundle.js",
    path: path.resolve(__dirname, 'dist')
  },
 module:{
 	rules:[
	 	{
	      test: /\.less$/,
	      use:[
	           {
	             loader:path.resolve('./loader/style-loader.js'),
	           },
	           {
	             loader:path.resolve('./loader/less-loader.js'),
	           }
	      ]
	 }, 
 ]
};

异步loader

异步loader需要有几个注意的2点如下:

  1. 使用this.async来告诉上下文当前loader是一个异步loader需要loader runner等待异步处理结果
  2. this.asycn()方法会返回一个callback回调函数,在异步任务处理完之后调用callback并将需要传递的数据(一般是异步操作处理后的数据)通过callback函数传递给下一个loader

下面看我们自己实现的一个简易版本的less-loader。用于将编写的less编译为css并传递给下一个style-loader。

在该文件夹下新建一个less-loader.js文件,用于处理less文件。代码如下:

let less = require('less');
function loader(source) {
    const callback = this.async();
    less.render(source)
        .then((output)=>{
            callback(null, output.css);
        }, (err)=>{
            // handler err  
        })
}
module.exports = loader;

这里处理的业务很简单,就是拿到原始的less文件内容,将less通过less.redner编译为css并传递给下一个loader即可。 关键的几句代码如下:

  1. this.async告诉当前上下文这是一个异步的loader,需要loader runner等待less.render异步处理的结果
  2. less.render接受less源码,并返回一个promise,在返回的promise中等待less.rendera处理完less之后,使用callback将处理的结果返回给下一个loader 我们可以打印看下接受less的源码和经过less.render处理之后的css样式如下: 在这里插入图片描述

同步loader

遵循简单原则,即一个loader只做一个事情,上面进过less-loader将编写的less编译为css并传递过来之后,我们在这个loader中做的事情就是将接受到的css文件使用style标签包裹起来并加入一个自定义属性data-origin标记这个文件的来源插入入口文件的head内。 在loader文件夹下新建style-loader.js 代码如下:

// webpack 自定义loader
function loader(source) {
    source = JSON.stringify(source);
    const root = process.cwd();
    const resourcePath = this.resource;
    const origin = resourcePath.replace(root, '');
    let style = `
        let style = document.createElement('style');
        style.innerHTML = ${source};
        style.setAttribute('data-origin', '${origin}');
        document.head.appendChild(style);
   `;
    return style
}

关键代码如下:

  1. 使用JSON.stringify将接受到的css变为一个可编辑字符串。
  2. process.cwd获取当前工程根目录
  3. this.resource通过this上的该属性可获取当前处理的源文件绝对路径
  4. 之后就是创建一个style标签,将我们编译完之后的css代码插入style标签内,并自定义一个data-origin 属性,用来标记当前文件在工程中的相对路径
  5. 最后返回一个可执行的js字符串给bundle.js

第五点是因为使用webpack打包之后其实是会将文件结果处理为一段可执行的js字符串再被eval包裹,最后入口页面会引入这个bundle.js的时候会执行这段可执行的js字符串。 大概长得像这个样子: 在这里插入图片描述 编写完逻辑之后,即可在控制台执行

npx webpack

webpack会根据webpack.config.js的配置文件来对工程进行打包。 我们编写的less文件也会经过我们自定义编写的less-laoder 和 style-loader处理完之后插入在入口文件中,效果如下: 在这里插入图片描述 我们可以看到打包之后的入口文件中插入了我们写的less样式,并且在每个style标签中都有一个data-origin自定义属性来标记原文件在工程中的相对地址。

查看在线代码