css编译相关loader以及loader实现原理

1,508 阅读3分钟

一、编译less文件,需要哪些loader?

{
    test: /.css$/,
    use: ['style-loader''css-loader''less-loader']
}

从执行顺序上说,loader先执行后面的loader,再执行前面的loader。

也就是说,针对less文件的执行顺序为:less-loader、css-loader、style-loader。

二、loader的实现原理

A loader is a node module that exports a function. This function is called when a resource should be transformed by this loader. The given function will have access to the Loader API using the this context provided to it.

loader是node中的一个模块,对外export的是一个函数。当一个文件资源需要被转化的时候,就会调用该loader。这个函数通过提供给它的this上下文,来访问loader的API。

1、这是一个什么都不做的loader.

module.exports = function(source) {

    return source;

}

loader函数有一个参数,该参数为文件的内容。以字符串的方式,给到我们开发者。

上面的source。就是一个包含某个文件内容的字符串。

2、loader函数辅助包(loader-utils)

loader配置中,不仅仅有loader的名字,还有一些options的配置选项,该包的主要作用,提供一些额外的功能,比如获取配置的options。

npm下载到本地

npm install --save loader-utils

使用获取options

const loaderUtils = require('loader-utils');

 

nodule.exports = function(source) {

    const options = loaderUtils.getOptions(this);

    console.log(options);

    return source;

}

3、loader函数校验包(schema-utils)

我们进行配置的options,是否符合loader的预期,这需要做一些校验,而schema-utils包,就是做相关的校验工作的。

npm下载到本地

npm install --save schema-utils

校验获取的options。

const loaderUtils = require('loader-utils');
const schemaUtils = require('schema-utils');
// 这个schema表示校验规则,options必须传递一个testProps的属性,其值的类型为number类型。

const schema = {

    type: 'object',

    properties: {

        testProps: {

            type: 'number',

        },

    },

};

module.exports =  function (source) {
 
    const options = loaderUtils.getOptions(this);
 
    console.log(options)


// 校验函数:参数一,校验规则。参数二,获取的options。

    schemaUtils.validate(schema, options, {

        name: 'test-loader',

        baseDataPath: 'options',

    });

    console.log(source)

    return `/** my name is liwudi **/\n${source}`;

}

三、css编译部分的loader解析

1、各个loader的功能是什么

  • css-loader:会对 @import 和 url() 进行处理,就像js解析 import/require() 一样。

  •  style-loader:把css插入到dom中。

  • less-loader:会把less编译为css的loader。

2、解析

less-loader解析:

import less from 'less';
const { css, imports } = less.render(source, lessOptions);

css就是最终得到的结果。也就是正式的把less编译成css字符串了。 less-loader的原理很简单,就是调用less库提供的方法,转译less语法后输出,如下:

// less-loader实现(经简化)

 const less = require('less');

  module.exports = function(content) {

     const callback = this.async(); // 转译比较耗时,采用异步方式

     const options = this.getOptions(); // 获取配置文件中less-loader的options

     less.render(

          content,

          createOptions(options), // less转译的配置

         (err, output) => {

             callback(err, output.css); // 将生成的css代码传递给下一个loader

          }//第三个参数是个callback,如果不传callback,则render方法会返回一个promise

     );

   };

编译结果的处理

function processResult(loaderContext, resultPromise) {

  const { callback } = loaderContext;

  resultPromise

    .then(({ css, map, imports }) => {

      imports.forEach(loaderContext.addDependency, loaderContext);

      return {

        // Removing the sourceMappingURL comment.

        // See removeSourceMappingUrl.js for the reasoning behind this.

        css: removeSourceMappingUrl(css),

        map: typeof map === 'string' ? JSON.parse(map) : map,

      };

    }, (lessError) => {

      throw formatLessError(lessError);

    })

    .then(({ css, map }) => {

      callback(null, css, map);

    }, callback);

}

编译后的结果包括css,map和imports三个,css是less编译成的css内容,map则是sourceMap相关信息,imports是编译过程中所有的依赖文件路径。 拿到编译结果后,首先是调用addDependency把所有imports中的文件添加到依赖里面,这个方法的作用时在watch模式时,依赖的这些文件变化时会出发编译更新。 然后是removeSourceMappingUrl(css),这个方法的作用是移除结果中的sourceMappingURL=,理由是less-loader无法知道最终的sourceMap会在哪里。 最后调用callback(null, css, map)把结果传给下一个loader去执行,less-loader的的工作就完成了

css-loader解析:

css-loader的作用主要是解析css文件中的@import和url语句,处理css-modules,并将结果作为一个js模块返回 假如我们有a.css、b.css、c.css:

// a.css

@import './b.css'; // 导入b.css

.a {
  font-size: 16px;
}

// b.css
@import './c.css'; // 导入c.css
.b {
  color: red;
}

// c.css
.c {
  font-weight: bolder;
}

css-loader对a.css的编译输出:

// css-loader输出

exports = module.exports = require("../../../node_modules/css-loader/lib/css-base.js")(false);

// imports

// 文件需要的依赖js模块,这里为空

···

// module

exports.push([ // 模块导出内容

  module.id,

  ".src-components-Home-index__c--3riXS {\n  font-weight: bolder;\n}\n.src-components-Home-index__b--I-yI3 {\n  color: red;\n}\n.src-components-Home-index__a--3EFPE {\n  font-size: 16px;\n}\n",

  ""

]);

// exports

exports.locals = { // css-modules的类名映射

  "c": "src-components-Home-index__c--3riXS",

  "b": "src-components-Home-index__b--I-yI3",

  "a": "src-components-Home-index__a--3EFPE"

};

可以理解为css-loader将a.css、b.css和c.css的样式内容以字符串的形式拼接在一起,并将其作为js模块的导出内容。

// css-loader源码(经简化)

// https://github.com/webpack-contrib/css-loader/blob/master/src/index.js

import postcss from 'postcss';

module.exports = async function (content, map, meta) {

  const options = this.getOptions(); // 获取配置

  const plugins = []; // 转译源码所需的postcss插件

  shouldUseModulesPlugins(options, this) && plugins.push(modulesPlugins); // 处理css-modules

  shouldUseImportPlugin(options, this) && plugins.push(importPlugin); // 处理@import语句

  shouldUseURLPlugin(options, this) && plugins.push(urlPlugin); // 处理url()语句

  shouldUseIcssPlugin(options, this) && plugins.push(icssPlugin); // 处理icss相关逻辑

  if (meta && meta.ast) { // 复用前面loader生成的CSS AST(如postcss-loader)
    content = meta.ast.root;
  }
  const result = await postcss(plugins).process(content); // 使用postcss转译源码

  const importCode = getImportCode(); // 需要导入的依赖语句

  const moduleCode = getModuleCode(result); // 模块导出内容

  const exportCode = getExportCode(); // 其他需要导出的信息,如css-modules的类名映射等
  
  const callback = this.async(); // 异步返回

  callback(null, `${importCode}${moduleCode}${exportCode}`);

};

style-loader解析:

经过css-loader的转译,我们已经得到了完整的css样式代码,style-loader的作用就是将结果以style标签的方式插入DOM树中。

// style-loader

import loaderUtils from 'loader-utils';

module.exports = function (content) {

  // do nothing

};

module.exports.pitch = function (remainingRequest) {

  /*

  * 用require语句获取css-loader返回的js模块的导出

  * 用'!!'前缀跳过配置中的loader,避免重复执行

  * 用remainingRequest参数获取loader链的剩余部分,在本例中是css-loader、less-loader

  * 用loaderUtils的stringifyRequest方法将request语句中的绝对路径转为相对路径

  */

  const requestPath = loaderUtils.stringifyRequest(this, '!!' + remainingRequest);

  // 本例中requestPath为:

  // '!!../node_modules/css-loader/index.js!../node_modules/less-loader/dist/cjs.js!src/styles/index.less'

  return `

    const content = require(${requestPath})

    const style = document.createElement('style');

    style.innerHTML = content;

    document.head.appendChild(style);

  `;

};

关于postcss:github.com/postcss/pos…

image2021-7-28_18-41-0.png