Skip to content

css-loader、style-loader的pitch

本节来实现一下上节遗留的问题,css-loader的实现

css-loader是用来处理css文件的,可以把css中的import语法解析出来,还能解析引用的图片。

比如:

css
@color: green;

body {
    color: @color;
    background-image: url('./aa.jpg');
    /*
    在解析这个url的时候,如果不使用css-loader,引用的路径还是./aa.jpg
    但是这个路径在打包后的目录是找不到的,所以就需要css-loader转换一下
    把url变为require的形式引入 url(require('./aa.jpg')
    */
}

假如不更改就是下面的效果,找不到这个图片

image-20220207160640003

准备工作

修改index.less

less
// webpack\webpack-loader\src\index.less

@color: green;

body {
    color: @color;
    background-image: url('./aa.jpg');
}

修改配置文件

js
// webpack\webpack-loader\webpack.config.js

let path = require('path')

module.exports = {
    mode: 'development',

    entry: './src/index.js',

    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },

    module: {
        rules: [
            {
                test: /\.less/,
                use: ['style-loader', 'css-loader', 'less-loader']
            },
            {
                test: /\.jpg/,
                // 根据图片生成一个md5戳,发射到打包目录下,然后返回一个打包后的图片路径
                // use: 'file-loader', // 自己写一个file-loader

                use: {
                    // 可以设定一个限制,将图片转换为base64,减少http请求
                    loader: 'url-loader', // 自己写一个url-loader
                    options: {
                        limit: 100 * 1024, // 如果图片小于100k,就直接将图片转换为base64
                        // 大于100k,就会用file-loader去处理图片
                    }
                }
            },
        ]
    },

    resolveLoader: {
        // 找loader的时候,先从node_modules中找,找不到再从我们的loaders目录下找
        modules: ['node_modules', path.resolve(__dirname, 'loaders')]
        /* alias: { // loader的别名
            loader1: path.resolve(__dirname, 'loaders', 'loader1.js')
        } */
    }
}

css-loader

实现css-loader

  1. 使用正则匹配出需要替换的url路径
  2. 使用正则的exec方法依次匹配,截取代码片段
  3. 截取的代码片段巧用数组保存,let arr = ['let list = []']
js
// webpack\webpack-loader\loaders\css-loader.js

// css-loader
function loader(source) {
    console.log('css-loader 加载了');

    // 匹配css中的图片地址的正则
    // background-image: url('./aa.jpg');
    // 捕获组捕获的结果 就是 './aa.jpg'
    let reg = /url\((.+?)\)/g

    let arr = ['let list = []'] // 用来拼接代码

    // 开始拼接替换代码块
    let pos = 0
    let current

    // reg.exec(source) 返回的结果是一个数组 [matchUrl, 捕获组1的内容]
    // matchUrl = url('./aa.jpg')
    // 捕获组1的内容 = './aa.jpg'

    // 使用正则的exec方法,每次匹配到的结果的lastIndex都会变化,默认从0开始

    // 循环替换代码中所有的图片路径
    while (current = reg.exec(source)) {

        let [matchUrl, g] = current

        // 把匹配的内容之前的代码保存一下
        let last = reg.lastIndex - matchUrl.length
        arr.push(`list.push(${JSON.stringify(source.slice(pos, last))})`) // 拼接代码块

        // 把捕获组中的图片路径 改为require的写法
        arr.push(`list.push('url('+require(${g})+')')`)

        // 更新pos,继续查找
        pos = reg.lastIndex
    }

    // 把最后的代码拼接到起来
    arr.push(`list.push(${JSON.stringify(source.slice(pos))})`)

    // 导出最后的结果
    arr.push(`module.exports = list.join('')`)

    // 返回代码片段
    return arr.join('\r\n')
}

module.exports = loader

最后拼接的代码片段如下 console.log(arr.join('\r\n'))

image-20220207164153402

但是如果直接把这个代码片段导出给style-loader,打包后的效果就是下面的样子,代码片段并没有被执行,所以需要修改一下style-loader,以require的方式引用css-loader处理后的代码,就用到了之前学的pitch,阻断loader的加载。

image-20220207164628798

修改style-loader

use: ['style-loader','css-loader','less-loader'],正常这三个loader的加载顺序是倒着来,所以上面css-loader的处理过的代码片段会直接返回给style-loader,而这个代码片段是还没有执行的,所以应该使用require来引用这个结果。

所以想到了一个办法,在style-loader中,使用pitch来阻断后面的loader,然后使用内联loader处理结果,

比如require(css-loader.js!less-loader.js!./index.less),为了避免死循环,所以需要变成这样

!!require(css-loader.js!less-loader.js!./index.less)

js
// webpack\webpack-loader\loaders\style-loader.js
let loaderUtils = require('loader-utils')

// style-loader
function loader(source) {
    console.log('style-loader 加载了');
    // 生成一个合法的标签,塞到head里
    let str = `
        let style = document.createElement('style')
        style.innerHTML = ${JSON.stringify(source)}
        document.head.appendChild(style)
    `
    return str
}

// 需要再style-loader拦截一下剩余两个loader,css-loader和less-loader
// 使用行内loader加载
// 使用pitch

loader.pitch = function (remainingRequest) {
    // remainingRequest 剩余的loader请求
    // css-loader.js!less-loader.js!./index.less
    /* 
    F:\code\note_code\webpack\webpack-loader\loaders\css-loader.js!F:\code\note_code\webpack\webpack-loader\loaders\less-loader.js!F:\code\note_code\webpack\webpack-loader\src\index.less
    */
    // console.log(remainingRequest);

    // relativePathRemainRequest
    // "!!../loaders/css-loader.js!../loaders/less-loader.js!./index.less"
    // loaderUtils.stringifyRequest 绝对路径转成相对路径 
    // 增加 !! 是避免死循环 执行行内loader时如果不加!!,发现加载index.less时,就会再次触发这三个loader
    // 所以 增加 !! 只触发inline loader
    let relativePathRemainRequest = loaderUtils.stringifyRequest(this, '!!' + remainingRequest)

    console.log('style-loader的pitch 加载了');

    // 生成一个合法的标签,塞到head里
    let str = `
        let style = document.createElement('style')
        style.innerHTML = require(${relativePathRemainRequest})
        document.head.appendChild(style)
    `
    return str
}

module.exports = loader

使用!!来只执行内联loader,达到了期望的结果。

image-20220207170647107

打包看下效果。结果已经是转换后的代码了。

image-20220207170552806

总结

本节实现了css-loader

  1. 先使用正则匹配出需要替换的url,变为require的形式

  2. 用数组拼接代码片段,导出代码片段

  3. 正则的exec方法,会改变lastIndex属性,利用这个特点依次拼接代码片段

    js
    // webpack\webpack-loader\loaders\css-loader.js
    
    // css-loader
    function loader(source) {
        console.log('css-loader 加载了');
    
        // 匹配css中的图片地址的正则
        // background-image: url('./aa.jpg');
        // 捕获组捕获的结果 就是 './aa.jpg'
        let reg = /url\((.+?)\)/g
    
        let arr = ['let list = []'] // 用来拼接代码
    
        // 开始拼接替换代码块
        let pos = 0
        let current
    
        // reg.exec(source) 返回的结果是一个数组 [matchUrl, 捕获组1的内容]
        // matchUrl = url('./aa.jpg')
        // 捕获组1的内容 = './aa.jpg'
    
        // 使用正则的exec方法,每次匹配到的结果的lastIndex都会变化,默认从0开始
    
        // 循环替换代码中所有的图片路径
        while (current = reg.exec(source)) {
    
            let [matchUrl, g] = current
    
            // 把匹配的内容之前的代码保存一下
            let last = reg.lastIndex - matchUrl.length
            arr.push(`list.push(${JSON.stringify(source.slice(pos, last))})`) // 拼接代码块
    
            // 把捕获组中的图片路径 改为require的写法
            arr.push(`list.push('url('+require(${g})+')')`)
    
            // 更新pos,继续查找
            pos = reg.lastIndex
        }
    
        // 把最后的代码拼接到起来
        arr.push(`list.push(${JSON.stringify(source.slice(pos))})`)
    
        // 导出最后的结果
        arr.push(`module.exports = list.join('')`)
    
        // 返回代码片段
        return arr.join('\r\n')
    }
    
    module.exports = loader

然后修改了style-loader

  1. 使用pitch,来阻断css-loader、和style-loader的执行

  2. 在pitch里使用内联loader来处理index.less

  3. 因为pitch会阻断,pitch里又有内联loader再次加载index.less,就继续会调用normal loader,而导致死循环

    所以,为了避免死循环,内联loader执行的时候,就只执行内联loader,其他不执行,使用了 !!

  4. 内联loader正好会把css-loader的结果引进来,引进就不是之前在数组中拼接的代码片段了,而是最终module.export的结果

    js
    // webpack\webpack-loader\loaders\style-loader.js
    let loaderUtils = require('loader-utils')
    
    // style-loader
    function loader(source) {
        console.log('style-loader 加载了');
        // 生成一个合法的标签,塞到head里
        let str = `
            let style = document.createElement('style')
            style.innerHTML = ${JSON.stringify(source)}
            document.head.appendChild(style)
        `
        return str
    }
    
    // 需要再style-loader拦截一下剩余两个loader,css-loader和less-loader
    // 使用行内loader加载
    // 使用pitch
    
    loader.pitch = function (remainingRequest) {
        // remainingRequest 剩余的loader请求
        // css-loader.js!less-loader.js!./index.less
        /* 
        F:\code\note_code\webpack\webpack-loader\loaders\css-loader.js!F:\code\note_code\webpack\webpack-loader\loaders\less-loader.js!F:\code\note_code\webpack\webpack-loader\src\index.less
        */
        // console.log(remainingRequest);
    
        // relativePathRemainRequest
        // "!!../loaders/css-loader.js!../loaders/less-loader.js!./index.less"
        // loaderUtils.stringifyRequest 绝对路径转成相对路径 
        // 增加 !! 是避免死循环 执行行内loader时如果不加!!,发现加载index.less时,就会再次触发这三个loader
        // 所以 增加 !! 只触发inline loader
        let relativePathRemainRequest = loaderUtils.stringifyRequest(this, '!!' + remainingRequest)
    
        console.log('style-loader的pitch 加载了');
    
        // 生成一个合法的标签,塞到head里
        let str = `
            let style = document.createElement('style')
            style.innerHTML = require(${relativePathRemainRequest})
            document.head.appendChild(style)
        `
        return str
    }

    image-20220207164628798

    image-20220207170552806

参考

https://www.bilibili.com/video/BV1a4411e7Bz?p=46&spm_id_from=pageDriver