所谓的加载器其实就是一个导出函数的node模块。若是有资源须要该加载器处理,webpack就会自动调用该加载器。这个返回的函数经过上下文提供的this
能够访问Loader API.javascript
在咱们深刻了解不一样种类的加载器以及他们的用法、相关案例以前,咱们先来看看在本地开发和测试加载器的三种方式吧。
想要测试一个加载器,你能够在rules
对象里面用path
去resolve
一个本地文件:css
{ test: /\.js$/ use: [ { loader: path.resolve('path/to/loader.js'), options: {/* ... */} } ] }
想要测试多个加载器,你能够经过resolveLoader.modules
配置项告诉webpack应该去哪儿寻找加载器。
好比项目里面有个目录/loaders
:html
resolveLoader: { modules: [ 'node_modules', path.resolve(__dirname, 'loaders') ] }
最后,若是你以前已经单独为加载器建立了一个仓库,不要忘了使用npm link
将加载器连接到你当前要测试的项目。java
加载器处理资源的时候只是传入一个字符串参数,也就是那个资源文件里面的内容。
同步加载器能够简单地返回一个表明被处理资源的值。不过更复杂状况下,也能够经过this.callback(err, values...)
返回任意数量的值。错误(Errors)要么被传入那个this.callback
函数里面,要么就在同步加载器里面抛出。
加载器应该返回1个或者2个值。第一个应该是String或者Buffer类型值,表明处理中的JS代码;第二个是一个JS对象,就是所谓的SourceMap
.node
多个加载器链式调用时,值得注意的就是,他们的执行顺序是倒叙的---根据数组格式写法,要么从右向左,要么从下向上。webpack
source map
.所以,下面的例子中,foo-loader会首先被调用,处理原始资源,而后返回值传给bar-loader.bar-loader执行完后返回最终的JS结果,有必要的话还会返回Source map.web
{ test: /\.js/, use: [ 'bar-loader', 'foo-loader' ] }
加载器编写须要遵循如下原则。根据重要性排列以下。其中有些只在特定场景下使用。想了解更多能够查看随后的详情介绍。npm
加载器应该只作某个单一的事情。这不但使得维护更容易,同时也容许在更多场景下去链式使用加载器。(其实就是功能比较单一的加载器在复杂的场景下能够被链式的组合使用)json
充分利用加载器能够链式调用这一特性。好比,咱们应该编写5个加载器,让每一个加载器去处理一项任务,而不是编写一个处理5项任务的加载器。这种分离不但使得加载器极其简单,并且有时候还能在你原先没有想到场景下使用。
考虑一下。使用加载器options选项或者query参数提供的数据去渲染一个模板文件的场景。这个加载器会首先读取源模板文件,执行,并最终返回一个包含所有html代码的字符串。然而为了符合上面准则(简单,单一原则),apply-loader
能够用来简单的连接其余开源加载器。api
jade-loader
:将模板转换为一个导出函数的模块。apply-loader
:使用加载器options执行那个函数,并返回原生的html。html-loader
:传入html,并返回一个有效的JS模块。事实上加载器能够连接也就意味着他们不必定都返回JS代码。只要加载器执行对列下一个加载器可以处理,加载器就能返回任意类型的模块。
保持模块化输出。加载器产生的模块应该遵循一样的设计原则。
保证加载器在模块转换的时候不要保持状态。买一次执行加载器都不该该收到其余已编译或者同一个模块往次编译加过的影响。
使用加载器工具包loader-utils
。它提供了不少有用的工具,尤为重要的是获取传入加载器的options的工具。同时使用schema-utils
包能够被用来保证加载器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'); // Apply some transformations to the source... return `export default ${ JSON.stringify(source) }`; };
若是加载器使用了外部资源(好比,从文件系统读入),就必需要指明。在watch模式下,这个信息能够用来清除失效的加载器缓存,并从新编译。下面是使用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(null, header + "\n" + source); }); };
因为模块的类型各类各样,因此指定模块依赖的方式也各有不一样。好比在css中,咱们就使用@import
和url(....)
来指定相关依赖的。模块系统会解析这些依赖的。
下面的两种方法能够完成:
require
语句。this.resolve
函数解析路径。css-loader
加载器就是第一种方案很好的一个案例。它把样式表文件里面的@import和url所有转换为require语句去引入相关资源了。
而less-loader
因为要处理复杂的变量和mixins,因此根本无法把每个@import给转换为require。所以,less-loader
在less编译器的基础上扩展了自定义的路径解析逻辑。而后它利用上述第二种方案里面的this.resolve
去解析相关依赖。
注意:若是语言只接受相对路径的url,你可使用`~`去引用已安装的模块(好比`node_modules`里的模块)。这种状况下看起来是这样子的`url('~some-library/image.jpg')`.
为了不在加载器处理的模块里面生成重复的代码,应该在加载器里面生成一个运行时文件放在独立的模块里面,而后让每个加载器处理的模块require那个共享的运行时文件。
不要在代码里面使用绝对路径,不然一旦项目根目录迁移,之前的哈希名称什么的会破坏掉。loader-utils
里面的stringifyRequest
方法能够把绝对路径转换为相对路径。
若是你的加载器只是在别的包上封装而成的。你应该把那个包指定为一个peerDependency
.这样容许开发者在package.json
里面指定准确的版本号。
好比。sass-loader
指定node-sass
做为peer dependency
:
"peerDependencies": { "node-sass": "^4.0.0" }
如今你已经根据上面的原则编写好本身的加载器了,并在本地运行起来了,接下来干吗呢?我们运行一个简单的单元测试以确保加载器符合咱们的预期吧。这儿咱们使用Jest
这个框架。为了使用import / export
和async / await
咱们把babel-jest
以及一些babel预设都给安装上。如今开始安装并把他们保存为devDependencies
。
npm install --save-dev jest babel-jest babel-preset-env
.babelrc
{ "presets": [[ "env", { "targets": { "node": "4" } } ]] }
咱们的加载器会处理txt格式的文件,而且仅仅只是使用传入加载器options里面的name
参数去替换txt文件里面的[name]
.而后就会输出转换好的JS代码咯。
src/loader.js
import { getOptions } from 'loader-utils'; export default function loader(source) { const options = getOptions(this); source = source.replace(/\[name\]/g, options.name); return `export default ${ JSON.stringify(source) }`; };
而后使用这个加载器去处理下面这个文件:
test/example.txt
Hey [name]!
请注意接下来的这一步哦,咱们要使用Node API和memory-fs
来执行webpack
。这样就避免了把输出结果输出到硬盘上了,经过访问stats
数据,咱们就能够拿到转换好了的模块了。
npm install --save-dev webpack memory-fs
test/compiler.js
import path from 'path'; import webpack from 'webpack'; import memoryfs from 'memory-fs'; export default (fixture, options = {}) => { const compiler = webpack({ context: __dirname, entry: `./${fixture}`, output: { path: path.resolve(__dirname), filename: 'bundle.js', }, module: { rules: [{ test: /\.txt$/, use: { loader: path.resolve(__dirname, '../src/loader.js'), options: { name: 'Alice' } } }] } }); compiler.outputFileSystem = new memoryfs(); return new Promise((resolve, reject) => { compiler.run((err, stats) => { if (err) reject(err); resolve(stats); }); }); }
上面,咱们内联了webpack的配置项。可是你依然可让导出的函数接受一个webpack配置项做为参数,这样就可使用同一个编译器模块去测试不一样的设置了。
如今,咱们就能够编写测试案例,并添加npm脚本,跑起来咯:
test/loader.test.js
import compiler from './compiler.js'; test('Inserts name and outputs JavaScript', async () => { const stats = await compiler('example.txt'); const output = stats.toJson().modules[0].source; expect(output).toBe(`export default "Hey Alice!\\n"`); });
package.json
"scripts": { "test": "jest" }
一切就绪,如今就能够跑起来看看咱们的加载器测试是否经过咯。
PASS test/loader.test.js ✓ Inserts name and outputs JavaScript (229ms) Test Suites: 1 passed, 1 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 1.853s, estimated 2s Ran all test suites.
成功了!如今你就能够去开发,测试,部署你本身的加载器咯。期待你的分享!