Webpack

Webpack is officially a JS module packaging tool, which can be used to package the Web site JS code base, but also can be used to package the third-party code base. Unlike RequireJs which only supports AMD, NodeJS is CommonJS, SeaJS only supports CMD, and now ES6 Module… . What’s easy to code in my opinion is good engineering, but browsers don’t necessarily know it. Webpack is like a black box, I put my project in and the output runs in the browser. This gives developers the flexibility to code as they like.

Webpack workflow

Simply speaking, it can be summarized as the following steps:

  1. Argument parsing
  2. Find the entry file
  3. callLoaderCompile the file
  4. traverseAST, collect dependencies
  5. generateChunk
  6. The output file

Among them, the real compiler is Loader, this article also introduces the style-loader in Loader.

webpack loader

Webpack itself can only package Javascript files, there is no way to load other resources such as CSS, images, or other syntax sets such as JSX. This requires the corresponding loader to convert resources and load them in. For example, in your project, the style files use less syntax, which is not recognized by the browser. In this case, we need to use the corresponding loader to convert the less syntax into the CSS syntax that the browser can recognize. This article through the analysis of style-loader original let everyone have a deeper understanding of Webpack Loader.

style-loader

Style-loader inserts a

const style = document.createElement('style'); // Create a new style tag style.type = 'text/ CSS '; Style. The appendChild (document. CreateTextNode (content)) / / CSS to style tags document. The head. The appendChild (style); // The style tag is inserted into the headCopy the code

This is probably an extension of the above code and some additional functionality.

style-loaderWebpack configuration

 module: {    
     rules: [      
         {        
             test: /.(css)$/,        
             use: [          
                 {            
                     loader: 'style-loader',            
                     options: {},          
                 },          
                 { loader: 'css-loader' },        
             ],      
         },    
     ],  
 },
Copy the code

The above code is a simple style-loader and CSS-loader to use the two loaders.

The source code parsing

Style-loader can be divided into:

  • Packaging phase
  • runtimephase

Packaging phase

Let’s take a look at what dependencies are introduced:

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 }; }

Copy the code

Here we define an _interopRequireDefault method, passing in a require().

This method returns an es6 module if it is imported, or a CommonJS module if it is imported, places the imported content on the default property of an object and returns the object.

Loader-utils: webpack utility class,style-loader only uses getOptions and stringifyRequest methods. The former retrieves the configuration options of the loader being called, while the latter converts a request to a non-absolute path to a string that can be required or imported;

Schema-utils: This class is used to check whether the parameter types passed by the loader match those defined by the loader.

Then the main method:

const loaderApi = () => {};

loaderApi.pitch = function loader(request) {
   ...
};

var _default = loaderApi;
exports.default = _default;

Copy the code

By default, loaders are piped from right to left, whereas pitch is piped from left to right.

Why does style-loader need this?

We know that the default loader executes from right to left and passes the result of the previous loader to the next loader. If this default behavior is followed, csS-loader will return a JS string to style-loader.

Style-loader inserts CSS code into the DOM. If you receive a JS string from the STYle-loader in sequence, you will not get the actual CSS style. So the correct thing to do is to execute style-loader, execute CSS-loader in it, get the processed CSS content, and then insert it into the DOM.

Let’s look at the body of the loader:

/ / get webpack configuration options in the const options = _loaderUtils. Default. The getOptions (this); // validate options (0, _schemautils.validate)(_options.default, options, {name: 'Style Loader', baseDataPath: 'options'}); // The default position of the style tag is head const insert = typeof options. Insert === 'undefined'? '"head"' : typeof options.insert === 'string' ? JSON.stringify(options.insert) : options.insert.toString(); / / set to which way is inserted into the DOM const injectType = options. InjectType | | 'styleTag'; const esModule = typeof options.esModule ! == 'undefined' ? options.esModule : true; / / hot need const namedExport = esModule && options. The 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: {} }Copy the code

InjectType returns different JS code, which is executed at Runtime.

Now let’s just look at the default case, which deals with many hot updates in the code. Let’s break out the main code:

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 || {}; '} `;Copy the code

First call the require method to get the contents of the CSS file and assign it to content, or to an array if content is a string, that is: [[module.id], content, “], then we overwrite the insert, singleton attributes of options, insert=head, singleton=false;

_loaderUtils.default.stringifyRequest(this, `!! ${request} ‘) converts an absolute path to a relative path.

The actual contents of the content and update are:

var content = require("!! . /.. /node_modules/css-loader/dist/cjs.js! ./xxx.css");Copy the code
var update = require("! . /.. /node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js")(content, options);Copy the code

Meaning is also called injectStylesIntoStyleTage module to handle through CSS – loader processing style content the content.

The above code is returned by style-loader and is actually executed at runtime.

runtimephase

Insert the style of the DOM at runtime stage is the actual operation, or to default, for example, take a look at what did 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 lastifiers = modulesToDom(list, options); / / update the 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; }; };Copy the code

Note that the Module. exportupDate method is used for update, and the modulesToDom method is the one we’ll focus on here.

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; }Copy the code

Convert the content passed in to a Styles array. At the top of the file, we define a stylesInDom object, which records the styles that have been added to the DOM.

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(); }}; }Copy the code

As you can see, it returns a function that determines whether the object passed in is equal to the original object, if so, does nothing, otherwise the update function is called, and if the object is empty, the remove function is called. Update and remove are assigned in the else. Before assigning, we first look at the insertStyleElement function: 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; }Copy the code

Create a style tag and insert it into insert, head. Back where we were before, we defined update and remove, after which we manually called the update function, 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)); }}Copy the code

This updates the content of the newly created style tag, and the remove function points to the removeStyleElement function.

As a final note, style-loader returns a string, and when called from a browser, it creates a style tag, adds it to the head, and adds the CSS content to the style structure. Each time the file is updated, the style structure is updated accordingly. If the CSS content is deleted, The “style” tag will also be deleted. Create a new “style” tag and write the content.