重学webpack4之loader开发

934 阅读2分钟

往期文章:

loader

loader只是一个导出为函数的js模块

module.exports = function(source) {
  return source
}

多loader串行执行,顺序从后往前

use: ['style-loader', 'css-loader', 'less-loader']

为什么是从后往前?

函数组合的两种情况

  • Unix中的pipline
  • compose(webpack采用这种) 扒洋葱
compose = (f,g) => (...args) => f(g(...args))
实现一个小例子:loader-order
├── dist
│   ├── index.js
│   └── main.js
├── loaders
│   ├── a-loader.js
│   └── b-loader.js
├── package\ copy.json
├── package.json
├── src
│   └── index.js
└── webpack.config.js

package.json

{
  "name": "loader-order",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "webpack"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "loader-utils": "^2.0.0",
    "webpack": "^4.43.0",
    "webpack-cli": "^3.3.12"
  }
}

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.join(__dirname, 'dist'),
    filename: 'main.js'
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        use: [
          path.resolve('./loaders/a-loader'),
          path.resolve('./loaders/b-loader')
        ]
      }
    ]
  }
}

a-loader.js

const loaderUtils = require('loader-utils');

module.exports = function(source) {
  console.log('Loader a is excuted!');

  const url = loaderUtils.interpolateName(this, '[name].[ext]', source);
  console.log(url);
  this.emitFile(url, source);
  return source;
}

b-loader.js

module.exports = function(source) {
  console.log('Loader b is excuted!');
  return source;
}

index.js

const a = 1

npm run build,打印日志

Loader b is excuted!
Loader a is excuted!

使用loader-runner本地开发调试loader

  • loader-runner允许可以在不安装webpack的情况下运行loaders
  • 作为webpack依赖,webpack中使用它执行loader,并进行loader的开发和调试

实现个小例子:raw-loader

├── package.json
├── run-loader.js
└── src
    ├── async-loader.js
    ├── async.txt
    ├── demo.txt
    └── raw-loader.js

package.json

{
  "name": "raw-loader",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "node ./run-loader.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "loader-runner": "^3.1.0"
  },
  "devDependencies": {
    "loader-utils": "^2.0.0"
  }
}

run-loader.js

const { runLoaders } = require('loader-runner')
const fs = require('fs')
const path = require('path')

runLoaders({
  resource: path.join(__dirname, './src/demo.txt'),
  loaders: [
    {
      loader: path.join(__dirname, './src/raw-loader.js'),
      // 给loader传递参数
      options: {
        name: 'test'
      }
    },
    {
      loader: path.join(__dirname, './src/async-loader.js')  
    }
  ],
  context: {
    emitFile: () => {}
  },
  readResource: fs.readFile.bind(fs)
}, (err, result) => {
  err ? console.log(err) : console.log(result)
})

demo.txt

foobar

raw-loader.js

const loaderUtils = require('loader-utils')
const fs = require('fs')
const path = require('path')

module.exports = function(source) {
  const { name } = loaderUtils.getOptions(this)
  console.log(name)

  // 同步 loader
  const json = JSON.stringify(source)
        .replace('foo', '')
        .replace(/\u2028/g, '\\u2028')
        .replace(/\u2029/g, '\\u2029');
  
  //return `export default ${json}`
  
  // throw new Error('Error');

  // this.callback(new Error('Error'), json)
  
  this.callback(null, json, 2, 3, 4)
}

async.txt

async

async-loader.js

const fs = require('fs')
const path = require('path')

module.exports = function(source) {
  const callback = this.async()
  
  fs.readFile(path.join(__dirname, './async.txt'), 'utf-8', (err, data) => {
    if (err) {
      callback(err, '')
    }
    callback(null, data)
  })
}

npm run test,日志打印

test
{
  result: [ '"async"', 2, 3, 4 ],
  resourceBuffer: <Buffer 66 6f 6f 62 61 72>,
  cacheable: true,
  fileDependencies: [
    '/Users/bytedance/mywork/new-life/webpack-rest/编写loader和插件/raw-loader/src/demo.txt'
  ],
  contextDependencies: [],
  missingDependencies: []
}

loader 参数获取

  • 通过loader-utils 的 getOptions 方法获取
module.exports = function(source) {
  const { name } = loaderUtils.getOptions(this)
}

异步loader

module.exports = function(source) {
  const callback = this.async()
  callback(null, source)
}

loader中使用缓存

  • webpack默认开启loader缓存
    • 可以使用 this.cacheable(false) 关闭缓存
  • 缓存条件:loader的结果在相同输入下有确定输出
    • 有依赖的loader 无法使用缓存

loader 如何进行文件输出

  • this.emitFile进行文件写入
const loaderUtils = require('loader-utils')
const url = loaderUtils.interpolateName(this, "[name].[ext]", {
  source,
})
this.emitFile(path.join(__dirname, url), source);
const path = `__webpack_public_path__+${JSON.stringify(url)};`
return `export default ${path}`

最后来个图片处理的loader

├── dist
│   ├── index.css
│   └── sprite.jpg
├── package.json
├── run-loader.js
└── src
    ├── images
    │   ├── 1.jpg
    │   └── 2.jpg
    ├── index.css
    └── sprite-loader.js

package.json

{
  "name": "sprite-loader",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "node ./run-loader.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "devDependencies": {
    "run-loader": "^0.0.6"
  },
  "dependencies": {
    "loader-utils": "^2.0.0",
    "spritesmith": "^3.4.0"
  }
}

run-loader.js

const { runLoaders } = require('loader-runner')
const fs = require('fs')
const path = require('path')

runLoaders({
  resource: path.join(__dirname, './src/index.css'),
  loaders: [
    path.join(__dirname, './src/sprite-loader')
  ],
  readResource: fs.readFile.bind(fs)
}, (err, result) => (err ? console.log(err) : null))

index.css

图片后面加上 ?__sprite,表示需要合并的图片

.img1 {
  background: url(./images/1.jpg?__sprite);
}
.img2 {
  background: url(./images/2.jpg?__sprite);
}

sprite-loader.js

const Spritesmith = require('spritesmith')
const fs = require('fs')
const path = require('path')

module.exports = function(source) {
  const callback = this.async()
  const imgs = source.match(/url\((\S*)\?__sprite/g)
  const matchedImgs = []

  for (let i = 0; i < imgs.length; i++) {
    const img = imgs[i].match(/url\((\S*)\?__sprite/)[1]
    matchedImgs.push(path.join(__dirname, img))
  }

  Spritesmith.run({
    src: matchedImgs
  }, (err, result) => {
    fs.writeFileSync(path.join(process.cwd(), 'dist/sprite.jpg'), result.image)
    source = source.replace(/url\((\S*)\?__sprite/g, mach => {
      return `url("dist/sprite.jpg"`
    })
    
    fs.writeFileSync(path.join(process.cwd(), 'dist/index.css'), source)
    callback(null, source)
  })
}

npm run test,结果:

  • sprite.jpg
  • index.css
.img1 {
  background: url("dist/sprite.jpg");
}
.img2 {
  background: url("dist/sprite.jpg");
}

这里先不处理 图片的position了

❤️ 加入我们

字节跳动 · 幸福里团队

Nice Leader:高级技术专家、掘金知名专栏作者、Flutter中文网社区创办者、Flutter中文社区开源项目发起人、Github社区知名开发者,是dio、fly、dsBridge等多个知名开源项目作者

微信扫码:期待您的加入,一起用技术改变生活!!!

期待您的加入,一起用技术改变生活!!!

招聘链接: job.toutiao.com/s/JHjRX8B