文:小 boy(沪江网校Web前端工程师)css
本文原创,转载请注明做者及出处html
常常逛 webpack 官网的同窗应该会很眼熟上面的图。正如它宣传的同样,webpack 能把左侧各类类型的文件(webpack 把它们叫做「模块」)统一打包为右边被通用浏览器支持的文件。webpack 就像是魔术师的帽子,放进去一条丝巾,变出来一只白鸽。那这个「魔术」的过程是如何实现的呢?今天咱们从 webpack 的核心概念之一 —— loader 来寻找答案,并着手实现这个「魔术」。看完本文,你能够:前端
在撸一个 loader 前,咱们须要先知道它究竟是什么。本质上来讲,loader 就是一个 node 模块,这很符合 webpack 中「万物皆模块」的思路。既然是 node 模块,那就必定会导出点什么。在 webpack 的定义中,loader 导出一个函数,loader 会在转换源模块(resource)的时候调用该函数。在这个函数内部,咱们能够经过传入 this
上下文给 Loader API 来使用它们。回顾一下头图左边的那些模块,他们就是所谓的源模块,会被 loader 转化为右边的通用文件,所以咱们也能够归纳一下 loader 的功能:把源模块转换成通用模块。node
知道它的强大功能之后,咱们要怎么使用 loader 呢?webpack
既然 loader 是 webpack 模块,若是咱们要使其生效,确定离不开配置。我这里收集了三种配置方法,任你挑选。git
增长 config.module.rules
数组中的规则对象(rule object)。github
let webpackConfig = { //... module: { rules: [{ test: /\.js$/, use: [{ //这里写 loader 的路径 loader: path.resolve(__dirname, 'loaders/a-loader.js'), options: {/* ... */} }] }] } }
增长 config.module.rules
数组中的规则对象以及 config.resolveLoader
。web
let webpackConfig = { //... module: { rules: [{ test: /\.js$/, use: [{ //这里写 loader 名便可 loader: 'a-loader', options: {/* ... */} }, { loader: 'b-loader', options: {/* ... */} }] }] }, resolveLoader: { // 告诉 webpack 该去那个目录下找 loader 模块 modules: ['node_modules', path.resolve(__dirname, 'loaders')] } }
也能够经过 npm link
链接到你的项目里,这个方式相似 node CLI 工具开发,非 loader 模块专用,本文就很少讨论了。npm
配置完成后,当你在 webpack 项目中引入模块时,匹配到 rule (例如上面的 /\.js$/
)就会启用对应的 loader (例如上面的 a-loader 和 b-loader)。这时,假设咱们是 a-loader 的开发者,a-loader 会导出一个函数,这个函数接受的惟一参数是一个包含源文件内容的字符串。咱们暂且称它为「source」。json
接着咱们在函数中处理 source 的转化,最终返回处理好的值。固然返回值的数量和返回方式依据 a-loader 的需求来定。通常状况下能够经过 return
返回一个值,也就是转化后的值。若是须要返回多个参数,则须调用 this.callback(err, values...)
来返回。在异步 loader 中你能够经过抛错来处理异常状况。Webpack 建议咱们返回 1 至 2 个参数,第一个参数是转化后的 source,能够是 string 或 buffer。第二个参数可选,是用来看成 SourceMap 的对象。
一般咱们处理一类源文件的时候,单一的 loader是不够用的(loader 的设计原则咱们稍后讲到)。通常咱们会将多个 loader 串联使用,相似工厂流水线,一个位置的工人(或机器)只干一种类型的活。既然是串联,那确定有顺序的问题,webpack 规定 use 数组中 loader 的执行顺序是从最后一个到第一个,它们符合下面这些规则:
咱们举个例子:
webpack.config.js
{ test: /\.js/, use: [ 'bar-loader', 'mid-loader', 'foo-loader' ] }
在上面的配置中:
了解了基本模式后,咱们先不急着开发。所谓磨刀不误砍柴工,咱们先看看开发一个 loader 须要注意些什么,这样能够少走弯路,提升开发质量。下面是 webpack 提供的几点指南,它们按重要程度排序,注意其中有些点只适用特定状况。
一个 loader 只作一件事,这样不只可让 loader 的维护变得简单,还能让 loader 以不一样的串联方式组合出符合场景需求的搭配。
这一点是第一点的延伸。好好利用 loader 的链式组合的特型,能够收获意想不到的效果。具体来讲,写一个能一次干 5 件事情的 loader ,不如细分红 5 个只能干一件事情的 loader,也许其中几个能用在其余你暂时还没想到的场景。下面咱们来举个例子。
假设如今咱们要实现经过 loader 的配置和 query 参数来渲染模版的功能。咱们在 “apply-loader” 里面实现这个功能,它负责编译源模版,最终输出一个导出 HTML 字符串的模块。根据链式组合的规则,咱们能够结合另外两个开源 loader:
jade-loader
把模版源文件转化为导出一个函数的模块。apply-loader
把 loader options 传给上面的函数并执行,返回 HTML 文本。html-loader
接收 HTMl 文本文件,转化为可被引用的 JS 模块。事实上串联组合中的 loader 并不必定要返回 JS 代码。只要下游的 loader 能有效处理上游 loader 的输出,那么上游的 loader 能够返回任意类型的模块。
保证 loader 是模块化的。loader 生成模块须要遵循和普通模块同样的设计原则。
在屡次模块的转化之间,咱们不该该在 loader 中保留状态。每一个 loader 运行时应该确保与其余编译好的模块保持独立,一样也应该与前几个 loader 对相同模块的编译结果保持独立。
请好好利用 loader-utils
包,它提供了不少有用的工具,最经常使用的一个就是获取传入 loader 的 options。除了 loader-utils
以外包还有 schema-utils
包,咱们能够用 schema-utils
提供的工具,获取用于校验 options 的 JSON Schema 常量,从而校验 loader options。下面给出的例子简要地结合了上面提到的两个工具包:
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'); // 在这里写转换 source 的逻辑 ... return `export default ${ JSON.stringify(source) }`; };
若是咱们在 loader 中用到了外部资源(也就是从文件系统中读取的资源),咱们必须声明这些外部资源的信息。这些信息用于在监控模式(watch mode)下验证可缓存的 loder 以及从新编译。下面这个例子简要地说明了怎么使用 addDependency
方法来作到上面说的事情。
loader.js:
import path from 'path'; export default function(source) { var callback = this.async(); var headerPath = path.resolve('header.js'); this.addDependency(headerPath); fs.readFile(headerPath, 'utf-8', function(err, header) { if(err) return callback(err); //这里的 callback 至关于异步版的 return callback(null, header + "\n" + source); }); };
不一样的模块会以不一样的形式指定依赖。好比在 CSS 中咱们使用 @import
和 url(...)
声明来完成指定,而咱们应该让模块系统解析这些依赖。
如何让模块系统解析不一样声明方式的依赖呢?下面有两种方法:
require
声明。this.resolve
函数来解析路径。对于第一种方式,有一个很好的例子就是 css-loader
。它把 @import
声明转化为 require
样式表文件,把 url(...)
声明转化为 require
被引用文件。
而对于第二种方式,则须要参考一下 less-loader
。因为要追踪 less 中的变量和 mixin,咱们须要把全部的 .less
文件一次编译完毕,因此不能把每一个 @import
转为 require
。所以,less-loader
用自定义路径解析逻辑拓展了 less 编译器。这种方式运用了咱们刚才提到的第二种方式 —— this.resolve
经过 webpack 来解析依赖。
若是某种语言只支持相对路径(例如
url(file)
指向./file
)。你能够用~
将相对路径指向某个已经安装好的目录(例如node_modules
)下,所以,拿url
举例,它看起来会变成这样:url(~some-library/image.jpg)
。
避免在多个 loader 里面初始化一样的代码,请把这些共用代码提取到一个运行时文件里,而后经过 require
把它引进每一个 loader。
不要在 loader 模块里写绝对路径,由于当项目根路径变了,这些路径会干扰 webpack 计算 hash(把 module 的路径转化为 module 的引用 id)。loader-utils
里有一个 stringifyRequest
方法,它能够把绝对路径转化为相对路径。
若是你开发的 loader 只是简单包装另一个包,那么你应该在 package.json 中将这个包设为同伴依赖(peerDependency)。这可让应用开发者知道该指定哪一个具体的版本。
举个例子,以下所示 sass-loader
将 node-sass
指定为同伴依赖:
"peerDependencies": { "node-sass": "^4.0.0" }
以上咱们已经为砍柴磨好了刀,接下来,咱们动手开发一个 loader。
若是咱们要在项目开发中引用模版文件,那么压缩 html 是十分常见的需求。分解以上需求,解析模版、压缩模版其实能够拆分给两给 loader 来作(单一职责),前者较为复杂,咱们就引入开源包 html-loader
,然后者,咱们就拿来练手。首先,咱们给它取个响亮的名字 —— html-minify-loader
。
接下来,按照以前介绍的步骤,首先,咱们应该配置 webpack.config.js
,让 webpack 能识别咱们的 loader。固然,最最开始,咱们要建立 loader 的 文件 —— src/loaders/html-minify-loader.js
。
因而,咱们在配置文件中这样处理:
webpack.config.js
module: { rules: [{ test: /\.html$/, use: ['html-loader', 'html-minify-loader'] // 处理顺序 html-minify-loader => html-loader => webpack }] }, resolveLoader: { // 由于 html-loader 是开源 npm 包,因此这里要添加 'node_modules' 目录 modules: [path.join(__dirname, './src/loaders'), 'node_modules'] }
接下来,咱们提供示例 html 和 js 来测试 loader:
src/example.html
:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> </body> </html>
src/app.js
:
var html = require('./expamle.html'); console.log(html);
好了,如今咱们着手处理 src/loaders/html-minify-loader.js
。前面咱们说过,loader 也是一个 node 模块,它导出一个函数,该函数的参数是 require 的源模块,处理 source 后把返回值交给下一个 loader。因此它的 “模版” 应该是这样的:
module.exports = function (source) { // 处理 source ... return handledSource; }
或
module.exports = function (source) { // 处理 source ... this.callback(null, handledSource) return handledSource; }
注意:若是是处理顺序排在最后一个的 loader,那么它的返回值将最终交给 webpack 的
require
,换句话说,它必定是一段可执行的 JS 脚本 (用字符串来存储),更准确来讲,是一个 node 模块的 JS 脚本,咱们来看下面的例子。
// 处理顺序排在最后的 loader module.exports = function (source) { // 这个 loader 的功能是把源模块转化为字符串交给 require 的调用方 return 'module.exports = ' + JSON.stringify(source); }
整个过程至关于这个 loader 把源文件
这里是 source 模块
转化为
// example.js module.exports = '这里是 source 模块';
而后交给 require 调用方:
// applySomeModule.js var source = require('example.js'); console.log(source); // 这里是 source 模块
而咱们本次串联的两个 loader 中,解析 html 、转化为 JS 执行脚本的任务已经交给 html-loader
了,咱们来处理 html 压缩问题。
做为普通 node 模块的 loader 能够垂手可得地引用第三方库。咱们使用 minimize
这个库来完成核心的压缩功能:
// src/loaders/html-minify-loader.js var Minimize = require('minimize'); module.exports = function(source) { var minimize = new Minimize(); return minimize.parse(source); };
固然, minimize 库支持一系列的压缩参数,好比 comments 参数指定是否须要保留注释。咱们确定不能在 loader 里写死这些配置。那么 loader-utils
就该发挥做用了:
// src/loaders/html-minify-loader.js var loaderUtils = require('loader-utils'); var Minimize = require('minimize'); module.exports = function(source) { var options = loaderUtils.getOptions(this) || {}; //这里拿到 webpack.config.js 的 loader 配置 var minimize = new Minimize(options); return minimize.parse(source); };
这样,咱们能够在 webpack.config.js 中设置压缩后是否须要保留注释:
module: { rules: [{ test: /\.html$/, use: ['html-loader', { loader: 'html-minify-loader', options: { comments: false } }] }] }, resolveLoader: { // 由于 html-loader 是开源 npm 包,因此这里要添加 'node_modules' 目录 modules: [path.join(__dirname, './src/loaders'), 'node_modules'] }
固然,你还能够把咱们的 loader 写成异步的方式,这样不会阻塞其余编译进度:
var Minimize = require('minimize'); var loaderUtils = require('loader-utils'); module.exports = function(source) { var callback = this.async(); if (this.cacheable) { this.cacheable(); } var opts = loaderUtils.getOptions(this) || {}; var minimize = new Minimize(opts); minimize.parse(source, callback); };
你能够在这个仓库查看相关代码,npm start
之后能够去 http://localhost:9000
打开控制台查看 loader 处理后的内容。
到这里,对于「如何开发一个 loader」,我相信你已经有了本身的答案。总结一下,一个 loader 在咱们项目中 work 须要经历如下步骤:
require
指定类型文件时,咱们能让处理流通过指定 laoder。最后,Talk is cheep,赶忙动手撸一个 loader 耍耍吧~