【学习笔记📒】webpack|手写loader — 模板编译 tpl-loader

1,208 阅读3分钟

Webpack 基础知识点

Webpack 三大件

"devDependencies": {
  "webpack": "^4.30.0",
  "webpack-cli": "^3.3.0",
  "webpack-dev-server": "^3.7.2"
}

核心概念

  • mode
  • entry
  • output
  • loader
  • plugin
  • devServer

Loader

什么是Loader

  • loader 用于对模块的源代码进行转换:把源模块转换成通用模块
  • loader 让 webpack 能够去处理那些非 JavaScript 文件webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

注意,loader 能够 import 导入任何类型的模块(例如 .css 文件),这是 webpack 特有的功能,其他打包程序或任务执行器的可能并不支持。我们认为这种语言扩展是有很必要的,因为这可以使开发人员创建出更准确的依赖关系图。

  • webpack loader的顺序是 从下到上从右到左。 当链式调用多个 loader 的时候,请记住它们会以相反的顺序执行。取决于数组写法格式从右向左或者从下向上执行。

在 webpack.config.js 中的配置

在更高层面,在 webpack 的配置中 loader 有两个目标:

  1. test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件
  2. use 属性,表示进行转换时,应该使用哪个 loader。 webpack.config.js:
const path = require('path');

const config = {
  output: {
    filename: 'my-first-webpack.bundle.js'
  },
  module: {
    rules: [
      { test: /\.txt$/, use: 'raw-loader' }
    ]
  }
};

module.exports = config;

以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性testuse。这告诉 webpack 编译器(compiler) 如下信息:

“嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 '.txt' 的路径」时,在你对它打包之前,先使用 raw-loader 转换一下。”

简单用法

🌟🌟 只能传入一个参数

当一个 loader 在资源中使用,这个 loader 只能传入一个参数 - 这个参数是一个包含资源文件内容的字符串

🌟🌟 返回值

同步 loader 可以简单的返回一个代表模块转化后的值

在更复杂的情况下,loader 也可以通过使用 this.callback(err, values...) 函数,返回任意数量的值。错误要么传递给这个 this.callback 函数,要么扔进同步 loader 中。

🌟🌟 loader 会返回一个或者两个值。第一个值的类型是 JavaScript 代码的字符串或者 buffer。第二个参数值是 SourceMap,它是个 JavaScript 对象。

复杂用法

链式调用多个 loader 的时候,请记住它们会以相反的顺序执行。取决于数组写法格式从右向左或者从下向上执行。

  • 最后的 loader 最早调用,将会传入原始资源内容。
  • 第一个 loader 最后调用,期望值是传出 JavaScript 和 source map(可选)。
  • 中间的 loader 执行时,会传入前一个 loader 传出的结果。

所以,在接下来的例子,foo-loader 被传入原始资源,bar-loader 将接收 foo-loader 的产出,返回最终转化后的模块和一个 source map(可选)

webpack.config.js

{
  test: /\.js/,
  use: [
    'bar-loader',
    'foo-loader'
  ]
}

用法准则

编写 loader 时应该遵循以下准则。它们按重要程度排序,有些仅适用于某些场景。

  • 简单易用

每个 loader 只做单一任务。

  • 使用链式传递。

loader 可以被链式调用意味着不一定要输出 JavaScript。只要下一个 loader 可以处理这个输出,这个 loader 就可以返回任意类型的模块。

  • 模块化的输出。

保证输出模块化。loader 生成的模块与普通模块遵循相同的设计原则。

  • 确保无状态。

  • 使用 loader utilities。

充分利用 loader-utils 包。它提供了许多有用的工具,但最常用的一种工具是获取传递给 loader 的选项schema-utils 包配合 loader-utils,用于保证 loader 选项,进行与 JSON Schema 结构一致的校验

loader.js

import { getOptions } from 'loader-utils';
import validateOptions from 'schema-utils';

const schema = {
  type: 'object',
  properties: {
    test: {
      type: 'string'
    }
  }
}

export default function(source) {
  const options = getOptions(this);

  validateOptions(schema, options, 'Example Loader');

  // 对资源应用一些转换……

  return `export default ${ JSON.stringify(source) }`;
};
  • 记录 loader 的依赖。

  • 解析模块依赖关系。

  • 提取通用代码。

不要在模块代码中插入绝对路径,因为当项目根路径变化时,文件绝对路径也会变化。loader-utils 中的 stringifyRequest 方法,可以将绝对路径转化为相对路径。

  • 避免绝对路径。
  • 使用 peer dependencies。

手写 模板编译 tpl-loader

package.json

按照这里的版本安装依赖,否则会因为版本问题报错。

{
  "name": "tpl-loader-creator",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "webpack-dev-server"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "^7.14.0",
    "babel-loader": "^8.2.2",
    "html-webpack-plugin": "^4.5.0",
    "webpack": "^4.30.0",
    "webpack-cli": "^3.3.0",
    "webpack-dev-server": "^3.7.2"
  }
}

webpack.config.js

const { resolve } = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin') // 是一个构造函数

module.exports = {
  mode: 'development',
  entry: resolve(__dirname, 'src/app.js'),
  output: {
    path: resolve(__dirname, 'build'),
    filename: 'app.js'
  },
  devtool: 'source-map',
  module: {
    rules: [
      // 模块规则(配置 loader、解析器等选项)

      {
        test: /\.tpl$/,
        // 这里是匹配条件,每个选项都接收一个正则表达式或字符串
        // test 和 include 具有相同的作用,都是必须匹配选项
        // exclude 是必不匹配选项(优先于 test 和 include)
        // 最佳实践:
        // - 只在 test 和 文件名匹配 中使用正则表达式
        // - 在 include 和 exclude 中使用绝对路径数组
        // - 尽量避免 exclude,更倾向于使用 include
        use: [
          // 应用多个 loader 和选项
          // loader的顺序是 `从下到上`,`从右到左`。
          'babel-loader',
          {
            loader: './loaders/tpl-loader',
            options: {
              log: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: resolve(__dirname, 'index.html')
    })
  ],
  devServer: {
    port: '3333'
  }
}

info.tpl

<div>
  <h1>{{ name }}</h1>
  <p>{{ age }}</p>
  <p>{{ career }}</p>
  <p>{{ hobby }}</p>
</div>

app.js

import tpl from './info.tpl'

const oApp = document.querySelector('#app')

const info = tpl({
	name: '小朝',
	age: 18,
	career: '前端开发工程师',
	hobby: '美食'
})

oApp.innerHTML = info

tpl-loader/index.js

const { tplReplace } = require('../util')
const { getOptions } = require('loader-utils')

function tplLoader(source) {
	source = source.replace(/\s+/g, '')
	const { log } = getOptions(this)
	const _log = log
		? `console.log('compiled the file which is from ${this.resourcePath}')`
		: ''

	/**
	 * source
	 * <div><h1>{{name}}</h1><p>{{age}}</p><p>{{career}}</p><p>{{hobby}}</p></div>
	 */

	/**
	 * options
	 * {name: "小朝", age: 18, career: "前端开发工程师", hobby: "美食"}
	 */

	return `
    export default options => {
      // 需要被 babel-loader 转成 js程序
      ${tplReplace.toString()}
      ${_log.toString()}
      console.log('*******')
      console.log('${source}')
      console.log(options)
      return tplReplace('${source}', options)
    }
  `
}

module.exports = tplLoader

🌟🌟 loader 返回的结果

注意:如果是处理顺序排在最后一个的 loader,那么它的返回值将最终交给 webpack 的 require,换句话说,它一定是一段可执行的 JS 脚本 (用字符串来存储),更准确来说,是一个 node 模块的 JS 脚本

// 处理顺序排在最后的 loader
module.exports = function (source) {
    // 这个 loader 的功能是把源模块转化为字符串交给 require 的调用方
    return `module.exports = ${JSON.stringify(source)}`
}

util.js:

function tplReplace (template, replaceObject) {
  return template.replace(/\{\{(.*?)\}\}/g, function(node, key) {
    return replaceObject[key]
  })
}

module.exports = {
  tplReplace
}

参考资料