Skip to content

增加loader,less-loader、style-loader

本节来给自己写的webpack编写一下loader

  1. 实现less-loader:将less语法转换为css
  2. 实现style-loader:将css链接到html的header里

编写loader

准备工作

安装第三方包less,好把less转换为css

shell
yarn add less@^3.9.0 -D

image-20220205094148514

修改配置文件

js
// webpack\webpack-dev-3\webpack.config.js
let path = require('path')
module.exports = {
    mode: 'development',

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

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

    module: {
        rules: [
            {
                test: /\.less$/,
                // 引入自己写的loader
                use: [
                    path.resolve(__dirname, 'loader', 'style-loader'),
                    path.resolve(__dirname, 'loader', 'less-loader')
                ]
            }
        ]
    }
}

新建index.less

js
// webpack\webpack-dev-3\src\index.less

body {
    background-color: green;
}

引入index.less

js
// webpack\webpack-dev-3\src\index.js
let str = require('./a.js')
require('./index.less')
console.log(str); // aaabbb

编写less-loader

js
// webpack\webpack-dev-3\loader\less-loader.js
let less = require('less')

// less-loader
function loader(source) {

    let css = '' // 转换后的css

    // 虽然是以回调的方式写的,其实是同步执行的
    less.render(source, (err, c) => {
        css = c.css
    })

    // webpack中是使用eval执行的代码片段
    // 编写less中的 \n 应该替换成 \\n
    // 否则就会出错
    css = css.replace(/\n/g, '\\n')

    return css
}
module.exports = loader

编写style-loader

js
// webpack\webpack-dev-3\loader\style-loader.js
// style-loader
function loader(source) {
    let style = `
        let style = document.createElement('style')
        style.innerHTML = ${JSON.stringify(source)}
        document.head.appendChild(style)
    `
    return style
}

module.exports = loader

修改自己写的webpack中的代码

加载自定义loader

咱们自己写好了loader,自己的webpack中应该识别一下,当遇到这些文件时,用自己写的loader去转换文件

js
// webpack\cheny-pack\lib\Complier.js

let path = require('path')
let fs = require('fs')

let babylon = require('babylon') // 把源码转换为ast语法树
let t = require('@babel/types') // 生成ast节点,可以生成各种类型
let traverse = require('@babel/traverse').default // 遍历ast语法树,替换节点
let generator = require('@babel/generator').default // 根据ast语法树,生成源代码

let ejs = require('ejs') // 模板引擎

class Complier {
    constructor(config) {
        // 将配置文件挂载到实例上,所有实例都能拿到了
        // webpack.config.js
        this.config = config

        // 1. 保存入口文件的路径
        this.entryId; // './src/index.js'

        // 2. 需要保存所有模块的依赖
        // 解析文件的依赖,变成webpack打包传递的参数,key value的形式
        // key 是路径名,value就是模块代码
        this.modules = {}

        this.entry = config.entry // 入口路径
        this.root = process.cwd() // 工作路径 运行 npx cheny-pack 时候的路径
    }

    run() {
        // 执行,并且创建模块的依赖关系
        // 从入口开始执行
        this.buildModule(path.resolve(this.root, this.entry), true) // true 表示解析的是主模块

        console.log(this.modules, this.entryId);

        // 发射一个文件 打包后的文件
        this.emitFile()
    }

    // 创建模块的依赖关系
    buildModule(modulePath, isEntry) {
        // 模块的key是相对路径 value是模块中的代码,不过需要做一些替换
        // require都变成了webpack_require等

        let source = this.getSource(modulePath) // 模块内容
        // 模块id是一个相对路径 总路径 减去 rootPath
        // 总路径modulePath F:\code\note_code\webpack\webpack-dev-3\src\index.js
        // 工作路径this.root F:\code\note_code\webpack\webpack-dev-3
        let moduleName = './' + path.relative(this.root, modulePath) // ./src/index.js  模块id

        if (isEntry) { // 如果是主入口的话,记录一下主入口的id
            this.entryId = moduleName // 保存入口的名字
        }

        // path.dirname(moduleName)获取父路径 ./src/index.js -> ./src
        // 解析模块代码,替换相应的内容
        let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName))


        // 把相对路径和模块中的内容对应起来
        this.modules[moduleName] = sourceCode

        // 在这里需要递归继续解析依赖项
        dependencies.forEach(dep => { // 加载附模块
            // 参数是一个绝对路径
            this.buildModule(path.join(this.root, dep), false) // false 表示不是主入口文件
        })


    }
    // 解析文件
    // 需要把source源码进行改造 返回一个依赖列表
    // 因为每一个模块里面还可能会引另外的模块,所以需要一个依赖列表
    parse(source, parentPath) { // AST 解析语法树

        let ast = babylon.parse(source) // 源码先解析成ast语法树
        // https://astexplorer.net/

        let dependencies = [] // 存放当源代码依赖的数组,即当前文件引入的第三方模块

        // 遍历ast语法树
        traverse(ast, {
            CallExpression(p) { // 调用表达式,比如 a() require() ...
                let node = p.node // 拿到对应的节点
                // 如果是require 就改为 __webpack_require__
                if (node.callee.name === 'require') {
                    node.callee.name = '__webpack_require__'

                    let moduleName = node.arguments[0].value // 取到的就是模块的引用名字 let a = require('./a') -> a
                    // 有时候我们引用的时候,没有带后缀,我们先把后缀加上
                    // path.extname('./a') === ''  path.extname('./a.js') === '.js' 
                    moduleName = moduleName + (path.extname(moduleName) ? '' : '.js') // ./a.js
                    // webpack的模板所有的路径都需要加上 ./src 所以需要把 ./a.js 变成 ./src/a.js
                    moduleName = './' + path.join(parentPath, moduleName) // ./src\a.js

                    // 把依赖的第三方模块 塞进依赖数组中
                    dependencies.push(moduleName)

                    // 替换原来的ast节点
                    node.arguments = [t.stringLiteral(moduleName)]
                }
            }
        })

        // 生成新的代码
        let sourceCode = generator(ast).code


        return { sourceCode, dependencies }
    }

    // 发射文件
    emitFile() {
        // 用转换后的数据,渲染模板
        // 拿到输出路径
        let main = path.join(this.config.output.path, this.config.output.filename)
        // 获取到模板资源
        let templateStr = this.getSource(path.join(__dirname, 'main.ejs'))

        // 根据处理过的模块 渲染新代码
        let code = ejs.render(templateStr, {
            entryId: this.entryId,
            modules: this.modules
        })

        // 如果是多入口打包,所以声明一个assets
        this.assets = {}
        this.assets[main] = code

        console.log(main);

        // 生成打包结果
        this.writeFile(main, this.assets[main])

    }

    // 生成打包结果
    writeFile(filePath, code) {
        // filePath: F:\code\note_code\webpack\webpack-dev-3\dist\bundle.js
        // 先判断文件夹是否存在,不存在的话先创建文件夹
        // path.dirname(filePath) F:\code\note_code\webpack\webpack-dev-3\dist
        let outputFolder = path.dirname(filePath)
        if (!fs.existsSync(outputFolder)) {
            fs.mkdirSync(outputFolder) // 没有文件夹先创建文件夹
        }
        // 输出打包文件
        fs.writeFileSync(filePath, code)
    }

    // 获取模块代码
    // 我们是通过路径来读取代码的,实际上可能这个代码会匹配到自己配置的loader
    // 比如 .less .js 等
    getSource(modulePath) {
        let content = fs.readFileSync(modulePath, 'utf8') // 读取到的源文件
        // 看看是否需要用自定义的loader处理
        let rules = this.config.module.rules // 拿到配置的所有loader
        // 如果路径匹配上了,就用自己的loader去加载文件

        for (let i = 0; i < rules.length; i++) {
            let rule = rules[i] // 配置的每一种规则 比如 /\.js$/ /\.css$/ /\.less$/ 等
            let { test, use } = rule // 将规则和配置的loader解构出来
            // loader是倒着执行,拿到最后的索引
            let count = use.length - 1

            if (test.test(modulePath)) { // 匹配到当前这个模块需要使用loader来处理
                // 倒着依次执行loader
                function normalLoader() {
                    let loader = require(use[count]) // 配置loader时使用的是绝对路径
                    // 递归调用loader
                    content = loader(content)
                    count-- // 倒着挨个执行loader

                    if (count >= 0) {
                        normalLoader()
                    }
                }

                // 刚进来 先执行一次,然后就递归执行loader
                normalLoader()
            }
        }


        return content
    }
}

module.exports = Complier

运行一下,测试结果 npx cheny-pack

打包结果

js
// webpack\cheny-pack\lib\main.ejs

(function (modules) { // webpackBootstrap
    // The module cache
    var installedModules = {};

    // The require function
    function __webpack_require__(moduleId) {

        // Check if module is in cache
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }
        // Create a new module (and put it into the cache)
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };

        // Execute the module function
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

        // Flag the module as loaded
        module.l = true;

        // Return the exports of the module
        return module.exports;
    }

    // Load entry module and return exports
    return __webpack_require__(__webpack_require__.s = "./src\index.js");
})
    ({
        
    "./src\index.js":
    (function (module, exports, __webpack_require__) {
        eval(`// webpack\webpack-dev-3\src\index.js
let str = __webpack_require__("./src\\a.js");

__webpack_require__("./src\\index.less");

console.log(str); // aaabbb`);
    }), 
        
    "./src\a.js":
    (function (module, exports, __webpack_require__) {
        eval(`// webpack\webpack-dev-3\src\a.js
let b = __webpack_require__("./src\\base\\b.js");

module.exports = 'aaa' + b; // aaabbb`);
    }), 
        
    "./src\base\b.js":
    (function (module, exports, __webpack_require__) {
        eval(`// webpack\webpack-dev-3\src\base\b.js
module.exports = 'bbb';`);
    }), 
        
    "./src\index.less":
    (function (module, exports, __webpack_require__) {
        eval(`let style = document.createElement('style');
style.innerHTML = "body {\\n  background-color: green;\\n}\\n";
document.head.appendChild(style);`);
    }), 
        
    });

将这个文件引入html中,发现是好使的,编写成功。

image-20220205101939847

总结

自己手写less-loader,style-loader

  1. less-loader

    js
    let less = require('less')
    
    // less-loader
    function loader(source) {
    
        let css = '' // 转换后的css
    
        // 虽然是以回调的方式写的,其实是同步执行的
        less.render(source, (err, c) => {
            css = c.css
        })
    
        // webpack中是使用eval执行的代码片段
        // 编写less中的 \n 应该替换成 \\n
        // 否则就会出错
        css = css.replace(/\n/g, '\\n')
    
        return css
    }
    module.exports = loader
  2. style-loader

    js
    // style-loader
    function loader(source) {
        let style = `
            let style = document.createElement('style')
            style.innerHTML = ${JSON.stringify(source)}
            document.head.appendChild(style)
        `
        return style
    }
    
    module.exports = loader

配置loader时,加载自己的loader,配置的是全局路径

js
let path = require('path')
module.exports = {
    module: {
        rules: [
            {
                test: /\.less$/,
                // 引入自己写的loader
                use: [
                    path.resolve(__dirname, 'loader', 'style-loader'),
                    path.resolve(__dirname, 'loader', 'less-loader')
                ]
            }
        ]
    }
}

自定义打包工具时,在读取文件资源的时候,看看路径是否是需要自定义loader来加载

js
// 获取模块代码
// 我们是通过路径来读取代码的,实际上可能这个代码会匹配到自己配置的loader
// 比如 .less .js 等
getSource(modulePath) {
    let content = fs.readFileSync(modulePath, 'utf8') // 读取到的源文件
    // 看看是否需要用自定义的loader处理
    let rules = this.config.module.rules // 拿到配置的所有loader
    // 如果路径匹配上了,就用自己的loader去加载文件

    for (let i = 0; i < rules.length; i++) {
        let rule = rules[i] // 配置的每一种规则 比如 /\.js$/ /\.css$/ /\.less$/ 等
        let { test, use } = rule // 将规则和配置的loader解构出来
        // loader是倒着执行,拿到最后的索引
        let count = use.length - 1

        if (test.test(modulePath)) { // 匹配到当前这个模块需要使用loader来处理
            // 倒着依次执行loader
            function normalLoader() {
                let loader = require(use[count]) // 配置loader时使用的是绝对路径
                // 递归调用loader
                content = loader(content)
                count-- // 倒着挨个执行loader

                if (count >= 0) {
                    normalLoader()
                }
            }
            // 刚进来 先执行一次,然后就递归执行loader
            normalLoader()
        }
    }
    return content
}

参考

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