Webpack
从官方的说法来看 Webpack 是一个 JS 模块打包工具,可以用它打包 Web 网站的 JS 代码库,也可以用来打包第三方代码库。不像 RequireJs 只支持 AMD,NodeJS 是 CommonJS, SeaJS 只支持 CMD,如今还有 ES6 Module ...。在我看来易于编码的才是好工程,但浏览器不一定认识它。Webpack就像个黑盒,输入我的工程,输出在浏览器运行。这样开发者就可灵活根据自己喜好编码。
Webpack工作流程
简单来说可以概括为以下几步:
- 参数解析
- 找到入口文件
- 调用
Loader编译文件 - 遍历
AST,收集依赖 - 生成
Chunk - 输出文件
其中,真正起编译作用的便是 Loader,本文也就 Loader 中的 style-loader 进行引出。
webpack loader
webpack本身只能打包Javascript文件,对于其他资源例如 css,图片,或者其他的语法集比如jsx,是没有办法加载的。 这就需要对应的loader将资源转化,加载进来。比如,
你的工程中,样式文件都使用了less语法,是不能被浏览器识别的,这时候我们就需要使用对应的loader,来把less语法转换成浏览器可以识别的css语法。本文通过分析style-loader原来让大家对webpack loader 有个更深的认识。
style-loader
style-loader 的功能就是在在 DOM 里插入一个 <style> 标签,并且将 CSS 写入这个标签内。
const style = document.createElement('style'); // 新建一个 style 标签
style.type = 'text/css';
style.appendChild(document.createTextNode(content)) // CSS 写入 style 标签
document.head.appendChild(style); // style 标签插入 head 中
大概就是上面代码功能的扩展以及附加一些功能。
style-loader的webpack配置使用
module: {
rules: [
{
test: /.(css)$/,
use: [
{
loader: 'style-loader',
options: {},
},
{ loader: 'css-loader' },
],
},
],
},
上面代码是一段简单的style-loader 和 css-loader 这两个 loader的使用。
源码解析
style-loader 主要可以分为:
- 打包阶段
runtime阶段
打包阶段
先看下引入了哪些依赖:
var _path = _interopRequireDefault(require("path"));
var _loaderUtils = _interopRequireDefault(require("loader-utils"));
var _schemaUtils = require("schema-utils");
var _isEqualLocals = _interopRequireDefault(require("./runtime/isEqualLocals"));
var _options = _interopRequireDefault(require("./options.json"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
这里定义了一个 _interopRequireDefault 方法,传入的是一个 require()。
这个方法的作用是:如果引入的是 es6 模块,直接返回,如果是 commonjs 模块,则将引入的内容放在一个对象的 default 属性上,然后返回这个对象。
loader-utils: webpack工具类,style-loader只用到了里面两个方法getOptions和stringifyRequest。前一个方法检索被调用loader的配置选项,后一个方法将一个请求转化为非绝对路径的可被require或import的字符串;
schema-utils: 此类用于检验 loader传入的参数和定义的参数类型是否匹配。
接下来主方法:
const loaderApi = () => {};
loaderApi.pitch = function loader(request) {
...
};
var _default = loaderApi;
exports.default = _default;
默认的 loader 都是从右向左像管道一样执行,而 pitch 是从左到右执行的。
为什么 style-loader 需要这样呢?
我们知道默认 loader 的执行是从右向左的,并且会将上一个 loader 处理的结果传递给下一个 loader,如果按照这种默认行为,css-loader 会返回一个 js 字符串给 style-loader。
style-loader 的作用是将 CSS 代码插入到 DOM 中,如果按照顺序从 css-loader 接收到一个 js 字符串的话,就无法获取到真实的 CSS 样式了。所以正确的做法是先执行 style-loader,在它里面去执行 css-loader ,拿到经过处理的 CSS 内容,再插入到 DOM 中。
接下来看看 loader 的主体内容:
//获取 webpack 配置里的 options
const options = _loaderUtils.default.getOptions(this);
//校验 options
(0, _schemaUtils.validate)(_options.default, options, {
name: 'Style Loader',
baseDataPath: 'options'
});
// style 标签插入的位置,默认是 head
const insert = typeof options.insert === 'undefined' ? '"head"' : typeof options.insert === 'string' ? JSON.stringify(options.insert) : options.insert.toString();
//设置以哪种方式插入 DOM 中
const injectType = options.injectType || 'styleTag';
const esModule = typeof options.esModule !== 'undefined' ? options.esModule : true;
//hot 需要
const namedExport = esModule && options.modules && options.modules.namedExport;
const runtimeOptions = {
injectType: options.injectType,
attributes: options.attributes,
insert: options.insert,
base: options.base
};
switch (injectType) {
case 'linkTag': {}
case 'lazyStyleTag':
case 'lazySingletonStyleTag': {}
case 'styleTag':
case 'singletonStyleTag':
default: {}
}
根据不同的 injectType 会 return 不同的 js 代码,在 runtime 的时候执行。
这里现在我们只看下默认情况,代码中很多热更新的处理,我们理出主要代码:
return `${
esModule ? `
import api from ${_loaderUtils.default.stringifyRequest(this, `!${_path.default.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`)};
import content${namedExport ? ', * as locals' : ''} from ${_loaderUtils.default.stringifyRequest(this, `!!${request}`)};`
:
`var api = require(${_loaderUtils.default.stringifyRequest(this, `!${_path.default.join(__dirname, 'runtime/injectStylesIntoStyleTag.js')}`)});
var content = require(${_loaderUtils.default.stringifyRequest(this, `!!${request}`)});
content = content.__esModule ? content.default : content;`
}
var options = ${JSON.stringify(runtimeOptions)};
options.insert = ${insert};
options.singleton = ${isSingleton};
var update = api(content, options);
${hmrCode}
${esModule ? namedExport ?
`export * from ${_loaderUtils.default.stringifyRequest(this, `!!${request}`)};`
:
'export default content.locals || {};' : 'module.exports = content.locals || {};'}
`;
首先调用require方法获取css文件的内容,将其赋值给content,如果content是字符串,则将content赋值为数组,即:[[module.id], content, ''],接着我们覆盖了options的insert、singleton属性,由于我们暂时只看默认的,所以insert=head,singleton=false;
_loaderUtils.default.stringifyRequest(this, `!!${request}`) 这个方法的作用是将绝对路径转换成相对路径。
content、update 的实际内容是:
var content = require("!!../../node_modules/css-loader/dist/cjs.js!./xxx.css");
var update = require("!../../node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js")(content, options);
意思也就是调用 injectStylesIntoStyleTage 模块来处理经过 css-loader 处理过的样式内容 content。
上述代码都是 style-loader 返回的,真正执行是在 runtime 阶段。
runtime 阶段
将样式插入 DOM 的操作实际是在 runtime 阶段进行的,还是以默认情况举例,看看 injectStylesIntoStyleTage 做了什么。
module.exports
module.exports = function (list, options) {
options = options || {}; // Force single-tag solution on IE6-9, which has a hard limit on the # of <style>
// tags it will allow on a page
if (!options.singleton && typeof options.singleton !== 'boolean') {
options.singleton = isOldIE();
}
list = list || [];
// 主要方法
var lastIdentifiers = modulesToDom(list, options);
// 更新时用
return function update(newList) {
newList = newList || [];
if (Object.prototype.toString.call(newList) !== '[object Array]') {
return;
}
for (var i = 0; i < lastIdentifiers.length; i++) {
var identifier = lastIdentifiers[i];
var index = getIndexByIdentifier(identifier);
stylesInDom[index].references--;
}
var newLastIdentifiers = modulesToDom(newList, options);
for (var _i = 0; _i < lastIdentifiers.length; _i++) {
var _identifier = lastIdentifiers[_i];
var _index = getIndexByIdentifier(_identifier);
if (stylesInDom[_index].references === 0) {
stylesInDom[_index].updater();
stylesInDom.splice(_index, 1);
}
}
lastIdentifiers = newLastIdentifiers;
};
};
可以看到 module.exportupdate方法是更新使用,我们这里最关注的还是modulesToDom方法。
function modulesToDom(list, options) {
var idCountMap = {};
var identifiers = [];
for (var i = 0; i < list.length; i++) {
var item = list[i];
var id = options.base ? item[0] + options.base : item[0];
var count = idCountMap[id] || 0;
var identifier = "".concat(id, " ").concat(count);
idCountMap[id] = count + 1;
var index = getIndexByIdentifier(identifier);
var obj = {
css: item[1],
media: item[2],
sourceMap: item[3]
};
if (index !== -1) {
stylesInDom[index].references++;
stylesInDom[index].updater(obj);
} else {
stylesInDom.push({
identifier: identifier,
updater: addStyle(obj, options),
references: 1
});
}
identifiers.push(identifier);
}
return identifiers;
}
将传递进来的内容转换为了styles数组,在文件顶部,定义了stylesInDom对象,主要是用来记录已经被加入DOM中的styles, 接下来看addStyle:
function addStyle(obj, options) {
var style;
var update;
var remove;
if (options.singleton) {
var styleIndex = singletonCounter++;
style = singleton || (singleton = insertStyleElement(options));
update = applyToSingletonTag.bind(null, style, styleIndex, false);
remove = applyToSingletonTag.bind(null, style, styleIndex, true);
} else {
style = insertStyleElement(options);
update = applyToTag.bind(null, style, options);
remove = function remove() {
removeStyleElement(style);
};
}
update(obj);
return function updateStyle(newObj) {
if (newObj) {
if (newObj.css === obj.css && newObj.media === obj.media && newObj.sourceMap === obj.sourceMap) {
return;
}
update(obj = newObj);
} else {
remove();
}
};
}
可以看到它返回一个函数,其主要内容是判断传入的对象是否与原对象相等,如果相等,则什么都不做,否则调用update函数,如果对象为空,则调用remove函数。而update与remove是在else中被赋值的,在赋值之前,我们首先看insertStyleElement函数:
function insertStyleElement(options) {
var style = document.createElement('style');
var attributes = options.attributes || {};
if (typeof attributes.nonce === 'undefined') {
var nonce = typeof __webpack_nonce__ !== 'undefined' ? __webpack_nonce__ : null;
if (nonce) {
attributes.nonce = nonce;
}
}
Object.keys(attributes).forEach(function (key) {
style.setAttribute(key, attributes[key]);
});
if (typeof options.insert === 'function') {
options.insert(style);
} else {
var target = getTarget(options.insert || 'head');
if (!target) {
throw new Error("Couldn't find a style target. This probably means that the value for the 'insert' parameter is invalid.");
}
target.appendChild(style);
}
return style;
}
创建一个style标签,并将其插入insert中,即head中,回到之前的地方,我们定义了update和remove,之后我们手动调用update函数,即applyToTag
function applyToTag(style, options, obj) {
var css = obj.css;
var media = obj.media;
var sourceMap = obj.sourceMap;
if (media) {
style.setAttribute('media', media);
} else {
style.removeAttribute('media');
}
if (sourceMap && typeof btoa !== 'undefined') {
css += "\n/*# sourceMappingURL=data:application/json;base64,".concat(btoa(unescape(encodeURIComponent(JSON.stringify(sourceMap)))), " */");
} // For old IE
/* istanbul ignore if */
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
while (style.firstChild) {
style.removeChild(style.firstChild);
}
style.appendChild(document.createTextNode(css));
}
}
即给刚创建的style标签更新内容,而remove函数指向removeStyleElement函数.
最后说一下,style-loader会返回一个字符串,而在浏览器中调用时,会将创建一个style标签,将其加入head中,并将css的内容放入style中,同时每次该文件更新也会相应的更新Style结构,如果该css文件内容被删除,则style的内容也会被相应的删除,更我们一开始说的一样新建一个 style 标签,然后写入内容。