实现一个webpack loader
在React,ES6开发模式越来越普及的今天,webpack就成了前端构建的一个标配。webpack有两大重要部分组成: loader和plugin。loader是用在应用源码上的转换原件,比如最常用到的babel-loader/jsx-loader/file-loader/css-loader/url-loader等等。
loader可链式执行,一种文件类型可以用多个loader(比如css文件,可能就需要用到css-loader和style-loader),loader之间用"!“分隔,当前loader处理完,把处理结果带到下一个loader,最后一个loader返回一个String或者String Buffer返回给compiler。
loader调用方式大体有3种形式:
-
引用时调用
// a.js require("style-loader/url!css-loader!./xxx.css");
-
webpack直接调用
// webpack.config.js // ... module: { loaders: [ // ... { test: /\.css$/, loader: "style-loader!css-loader" } ] }
-
指定loaders数组
// webpack.config.js // ... module: { loaders: [ // ... { test: /\.css$/, loaders: [ "style-loader", "css-loader" ] } ] }
webpack官网上说"A loader is a node module exporting a function”,也就是说一个loader就是一个暴露出去的node模块,既然是一个node module,也就基本可以写成下面的样子:
module.exports = function() {
// ...
};
需要注意的是,在该模块被调用时,传入的第一个参数是文件的内容,所以我们可以再改改:
/**
* @param content 将被处理的内容
*
**/
module.exports = function(content) {
// ...
// 运行下一个loader
this.callback(content);
};
知道了大体写法,现在我们就来实现一个简单的loader,主要功能就是把css中的px单位转换成rem单位
// px2rem-loader/index.js
"use strict";
// 用来获取调用loader时传入的参数等等
var loaderUtils = require("loader-utils");
// css解析模块
var css = require("css");
// 乘除模块,防止在计算中出现精度丢失的问题
var privateMath = {
mul: function(num1, num2) {
var m = 0,
s1 = num1.toString(),
s2 = num2.toString();
try {
m += s1.split(".")[1].length
} catch (e) {}
try {
m += s2.split(".")[1].length
} catch (e) {}
return Number(s1.replace(".", "")) * Number(s2.replace(".", "")) / Math.pow(10, m);
},
div: function(num1, num2) {
var t1, t2, r1, r2;
try {
t1 = num1.toString().split('.')[1].length;
} catch (e) {
t1 = 0;
}
try {
t2 = num2.toString().split(".")[1].length;
} catch (e) {
t2 = 0;
}
r1 = Number(num1.toString().replace(".", ""));
r2 = Number(num2.toString().replace(".", ""));
return (r1 / r2) * Math.pow(10, t2 - t1);
}
};
module.exports = function(content) {
// 把当成css内容解析成AST对象
var contentAST = css.parse(content);
// 使用loader时的queryString(相关参数)
var query = loaderUtils.parseQuery(this.query);
// 最小px值,当数组小于它是忽略计算
var minSize = query.minSize || 1;
// 基数(最后计算出的结果 = (原先的大小 / base / scale) + "rem")
var base = query.base || 37.5;
// 忽略的样式规则名称
var ignore = query.ignore.length ? query.ignore.split("|") : [];
// 缩放比
var scale = query.scale || 1;
// 匹配10px或者10.5px这种单位
var pxUnitReg = /\d+[\.{1}\d+]?px/gi;
var tmp;
// 遍历样式树
contentAST.stylesheet.rules.forEach(function(rule) {
// 遍历样式表
rule.declarations.forEach(function(style) {
if (ignore.indexOf(style.property) < 0) {
style.value = style.value.replace(pxUnitReg, function(match) {
tmp = parseFloat(match);
if(tmp > minSize) {
return privateMath.div(tmp, privateMath.mul(base, scale)) + "rem";
}
});
}
});
});
// 再把处理好的AST对象转成css String
content = css.stringify(contentAST);
// 调用下一个loader
this.callback(null, content);
};
到这里,一个简单的load就算实现了,一起来看下调用把:
// webpack.config.js
const webpack = require("webpack");
module.exports = {
entry: "./src/js/entry.js",
output: {
path: __dirname,
filename: "build/bundle.js"
},
module: {
loaders: [{
test: /\.js$/,
loader: 'babel-loader?presets[]=es2015'
}, {
test: /\.css$/,
loader: 'style-loader!css-loader!px2rem-loader?base=37.5&scale=2&minSize=1&ignore=border|margin|padding'
}]
},
plugins: [
]
};
之前的css:
打包之后: