4.6.Webpack4-手写loader

450 阅读3分钟

三十九、1.loader:概念

  1. loader是指用来将一段代码转换成另一段代码的webpack加载器
  2. 创建文件夹loader-webpack,初始化$ yarn init -y,安装包:webpack webpack-cli
  3. loaders/loader1.js
function loader(source){ // loader的参数就是原代码
  console.log(source);
  return source; // 什么都不做,直接返回
}
module.exports = loader;
  1. webpack.config.js
let path = require('path');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
}
  1. 引用loader1的三个方法
    1. webpack.config.js +
  module: {
    rules: [
      {
        test: /\.js$/,
        use: path.resolve(__dirname, 'loaders', 'loader1.js')
      }
    ]
  }
2. webpack.config.js +
  resolveLoader: {
    // 别名
    alias: {
      loader1: path.resolve(__dirname, 'loaders', 'loader1.js')
    }
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'loader1'
      }
    ]
  }
3. webpack.config.js +
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')] // 先从node_modules找,找不到再从loaders找
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: 'loader1'
      }
    ]
  }

四十、2.loader配置

  1. loader的执行顺序从下到上,从右到左;可以通过enforce的'pre'|'normal'|'inline'|'post'改变执行顺序
  2. inline-loader执行:require('!!inline-loader!./a.js')
    1. -! 不会通过pre和normal loader处理
    2. ! 不会通过normal loader处理
    3. !! 什么都不要
  3. loader.pitch 执行顺序 loader3-pitch loader2-pitch loader1-pitch loader1 loader2 loader3
  4. loader特点
    1. 第一个loader要返回js脚本
    2. 每个loader只做一件内容,为了使loader在更多场景链式调用
    3. 每一个loader都是一个模块
    4. 每个loader都是无状态的,确保loader在不同模块转换之间不保存状态。

四十一、3.babel-loader实现

  1. 安装包 @babel/core @babel/preset-env loader-utils
  2. webpack.config.js
  devtool: 'source-map',
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader:'babel-loader',
          options: {
            presets: [
              '@babel/preset-env'
            ]
          }
        }
      }
    ]
  }
  1. loaders/babel-loader.js
// #3. 引入babel loaderUtils
let babel = require("@babel/core");
let loaderUtils = require('loader-utils');
function loader(source) { // this loaderContext #1. loader函数
  // console.log(Object.keys(this)); // #4. 查看上下文
  // #5. 获取options { presets: [ '@babel/preset-env' ] }
  let options = loaderUtils.getOptions(this); 
  // console.log(options);
  let cb = this.async(); // #7. api实现异步回调
  // #6. babel转化
  babel.transform(source, { 
    ...options,
    sourceMap: true,
    filename: this.resourcePath.split('/').pop() // 文件名
  }, function (err, result) {
    cb(err, result.code, result.map); // #8. 异步回调
  })
}
module.exports = loader; // #2. 导出loader
  1. @babel/core @babel/preset-env 'babel-loader' options presets babel require loader-utils包 getOptions this loaderContext Object.keys .transform source ...options sourceMap cb async rsult.code resource.map class Kft new devtool filename resourcePath split pop

四十二、4.banner-loader实现:给js文件添加注释内容

  1. 给js文件自动添加注释 /** 注释 */
  2. webpack.config.js
  watch: true,
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: 'banner-loader', // 添加注释
          options: {
            text: 'kft',
            filename: path.resolve(__dirname, 'banner.js')
          }
        }
      }
    ]
  }
  1. banner.js
注释内容:作者Fun腾时间2019
  1. loaders/banner-loader.js
// #3. 引入 loader-utils schema-utils(校验) fs
let loaderUtils = require('loader-utils');
let validateOptions = require('schema-utils');
let fs = require('fs');
function loader(source) { // #1. 创建loader函数
  this.cacheable && this.cacheable(); // #9. 每次打包时是否缓存,默认缓存
  // #4. 获取上下文中的options
  let options = loaderUtils.getOptions(this); 
  // console.log(options); // { text: 'kft', filename: '/Desktop/loader-webpack/banner.js' }
  let cb = this.async();
  // #5. 定义校验参数 
  let schema = {
    type: 'object',
    properties: {
      text: {
        type: 'string'
      },
      filename: {
        type: 'string'
      }
    }
  }
  // #6. 进行校验
  validateOptions(schema, options, 'banner-loader');
  // #7. 读取注释内容,合并source
  if (options.filename) {
    this.addDependency(options.filename); // #8. 监控时,自动添加文件依赖
    fs.readFile(options.filename, 'utf8', function (err, data) {
      cb(err, `/**${data}**/${source}`);
    })
  } else {
    cb(null, `/**${options.text}**/${source}`);
  }
}
module.exports = loader; // #2. 导出
  1. 自创banner-loader 给所有js添加注释 banner.js schema-utils包校验模块 schema type object properties text type string filename type string validateOptions fs readFile cb cb(err,/**%/) addDependency自动添加文件依赖 cacheable(false)不缓存

四十三、5.file-loader图片模块、url-loader

  1. file-loader:根据图片生成一个md5戳,发射到dist下,返回当前图片路径
  • 安装包 loader-utils'
  • webpack.config.js +
      {
        test: /\.(jpg|png|gif|jpeg)$/,
        use: 'file-loader'
      }
  • loaders/file-loader.js
let loaderUtils = require('loader-utils');
function loader(source) {
  let filename = loaderUtils.interpolateName(this, '[hash].[ext]', { content: source }) // 根据当前图片格式生成路径
  this.emitFile(filename, source); // 发射文件
  return `module.exports="${filename}"`;
}
loader.raw = true;
module.exports = loader;
  1. url-loader:file-loader会处理路径,根据limit判断转换base64图片还是使用file-loader生成路径
  • 安装包 mime loader-utils'
  • webpack.config.js
      {
        test: /\.(jpg|png|gif|jpeg)$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 1*1024
          }
        }
      }
  • loaders/url-loader.js
let loaderUtils = require('loader-utils');
let mime = require('mime');
function loader(source) {
  let { limit } = loaderUtils.getOptions(this);
  if (limit && limit > source.length) {
    return `module.exports="data:${mime.getType(this.resourcePath)};base64,${source.toString('base64')}"`
  } else {
    return require('./file-loader').call(this, source)
  }
}
loader.raw = true;
module.exports = loader;
  1. img creatElement src appenChild import p .jpg file-loader根据图片生成一个md5戳,发射到dist下,返回当前图片路径 file-loader loader .raw二进制 exports loaderUtils filename interpolateName emitFile url-loader会处理路径(file-loader会处理路径,根据大小返回base64还是文件) url-loader loaderUtils getOptions limit source.length return module.exports data:;base64,${source.toString('base64') mime require file-loader

四十四、6.less-loader:处理less和style-loader:插入脚本

  1. webpack.config.js +
      {
        test: /\.less$/,
        use: ['style-loader','css-loader','less-loader']
      }
  1. loaders/less-loader.js
let less = require('less');
function loader(source) {
  let css;
  console.log
  less.render(source, (err, r) => {
    if (err) return console.log(err);
    css = r.css;
  });
  return css;
}
module.exports = loader;
  1. loaders/css-loader.js
function loader(source){
  // 复杂,下一阶段处理
  return source;
}
module.exports = loader;
  1. loaders/style-loader.js
function loader(source){
  // 在style-loader导出一个脚本
  let str = `
    let style = document.createElement('style');
    style.innerHTML = ${JSON.stringify(source)};
    document.head.appendChild(style);
  `
  return str;
}
module.exports = loader;
  1. 样式问题 index.less .less style-loader css-loader less-loader less包 .render source err r r.css css-loader 在style-loader导出一个脚本 str style= createElement style innerHTML JSON.stringify head appendChild

四十五、7.css-loader

  1. 引入的css中有背景图,对css进行分段,'url(...)之前内容' + url(...) + 'url(...)之后内容' /url\((.+?)\)/g
  2. 需要有url-loader或file-loader配合使用
  3. loaders/css-loader.js
function loader(source) {
  let reg = /url\((.+?)\)/g;
  let pos = 0;
  let current;
  let arr = ['let list= []'];
  while (current = reg.exec(source)) {
    let [matchUrl, g] = current;
    // console.log(matchUrl, g);
    let last = reg.lastIndex - matchUrl.length;
    arr.push(`list.push(${JSON.stringify(source.slice(pos, last))})`);
    pos = reg.lastIndex;
    // 把 g 替换成require的写法 => url(require('xxx'))
    arr.push(`list.push('url('+ require(${g}) +')')`)
  }
  arr.push(`list.push(${JSON.stringify(source.slice(pos))})`);
  arr.push(`module.exports = list.join('')`);
  return arr.join('\r\n');
}
module.exports = loader;
// 'url(之前' + ...jpg + ')之后' -> 'url(之前' + url(require(...jpg)) + ')之后'
// let list= []
// list.push("body {\n  background: red;\n  background: ")
// list.push('url('+require(./logo.png)+')')
// list.push(";\n}\n")
// module.exports = list.join('')
  1. loaders/style-loader.js 使用pitch
let loaderUtils = require('loader-utils');
function loader(source) {
  let str = `
    let style = document.createElement('style');
    style.innerHTML = ${JSON.stringify(source)};
    document.head.appendChild(style);
  `
  return str;
}
// style-loader less-loader!css-loader/ ./index.less
loader.pitch = function (remainingRequest) {
  // remainingRequest剩余请求
  // console.log(remainingRequest) // "/Users/Desktop/loader-webpack/loaders/css-loader.js!/Users/Desktop/loader-webpack/loaders/less-loader.js!/Users/Desktop/loader-webpack/src/index.less" 转换成相对路径 "../loaders/css-loader.js!../loaders/less-loader.js!./index.less"
  // loaderUtils.stringifyRequest方法:绝对路径转为相对路径
  let str = `
    let style = document.createElement('style');
    style.innerHTML = require(${loaderUtils.stringifyRequest(this, '!!' + remainingRequest)}); // 使用!!,防止重复调用style-loader
    document.head.appendChild(style);
  `
  return str; 
}
module.exports = loader;
  1. 背景图 分段 'url(之前' + ...jpg + ')之后' reg /url\((.+?)\)/g pos while exec current matchUrl g console last lastetIndex length arr list push JSON pos last g 替换成require push url push slice pos join style-loader pitch remainingRequest less-loader '!!' loaderUtils stringifyRequest绝对路径转为相对路径

webpack.config.js

let path = require('path');
module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  },
  resolveLoader: {
    modules: ['node_modules', path.resolve(__dirname, 'loaders')]
  },
  devtool: 'source-map',
  module: {
    rules: [
      // {
      //   test: /\.js$/,
      //   use: {
      //     loader:'babel-loader',
      //     options: {
      //       presets: [
      //         '@babel/preset-env'
      //       ]
      //     }
      //   }
      // },

      // {
      //   test: /\.js$/,
      //   use: {
      //     loader: 'banner-loader', // 添加注释
      //     options: {
      //       text: 'kft',
      //       filename: path.resolve(__dirname, 'banner.js')
      //     }
      //   }
      // }

      // {
      //   test: /\.(jpg|png|gif|jpeg)$/,
      //   use: 'file-loader'
      // }

      {
        test: /\.(jpg|png|gif|jpeg)$/,
        use: {
          loader: 'url-loader',
          options: {
            limit: 0
          }
        }
      },
      {
        test: /\.less$/,
        use: ['style-loader', 'css-loader', 'less-loader']
      }
    ]
  }
}

package.json

{
  "name": "loader-webpack",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "@babel/core": "^7.5.0",
    "@babel/preset-env": "^7.5.0",
    "less": "^3.9.0",
    "loader-utils": "^1.2.3",
    "mime": "^2.4.4",
    "schema-utils": "^1.0.0",
    "webpack": "^4.35.2",
    "webpack-cli": "^3.3.5"
  }
}

loader-webpack