father4 预处理编译 less 文件

发布于 2023/11/18, 编辑于 2024/5/14

本文主要内容为,介绍前端开发中使用 father 进行打包组件库、工具库时,如何预处理编译 less 文件

一、引言

在前端开发中,我们经常会遇到需要对一些组件库进行二次封装的需求,特别是当我们使用阿里系的组件库,如 antd 或 antd-mobile 时。在这种情况下,我们通常会使用 fatherdumi 这一套官方推荐的方案。然而,father 官方并没有提供处理 scss 或 less 的示例,因此我们需要自行实现 father 的样式预处理。本文将介绍如何实现这一目标。

二、father 的工作原理

在深入讨论如何实现样式预处理之前,我们首先需要了解一下 father 在前端打包中的工作原理。

father 不仅可以用于打包前端代码,它也可以用于打包通用的 ts/js 代码库。实际上,father 是一个集成了多个打包工具的打包工具,例如,它集成了 babel 和 esbuild 两种编译器。

当我们需要打包的代码为前端代码(如前端组件库等)时,father 会使用 babel 作为 js/ts 的编译器。当我们需要打包的代码为 node 库时,father 则会使用速度更快的 esbuild。

此外,father 还提供了一些插件功能,例如 extraBabelPluginsaddLoaderextraBabelPlugins 可以让我们使用 babel 插件,而 addLoader 功能则类似于 webpack 的 loaders 功能,可以用于处理非 js/ts 文件。

三、实现样式预处理

了解了 father 的工作原理后,我们就可以开始实现样式预处理了。我们的任务可以分为两部分:

  1. addLoader 功能,实现一个 less 转 css 的插件。
  2. 使用 extraBabelPlugins,实现一个插件,将 js/ts 中的 .less 文件内容转为 .css

接下来,我们将详细介绍如何实现这两个任务。

3.1. 第一步、实现 babel 插件

这一步主要是为了把 js/ts 文件中的与 less 相关的文本替换为 css,例如:import "./index.less" 替换为 import "./index.css"

这一步比较简单,直接实现一个 babel 插件用于进行 .less 替换为 .css 的文本操作即可,代码如下:

// .fatherrc.ts
import { defineConfig } from 'father';

export default defineConfig({
    
    ...

    esm: { output: 'dist', transformer: 'babel' }, // 必须要使用 babel 模式
    extraBabelPlugins: [
        [
            './babel-less-to-css.js', // 把 js/ts 文件中的 '.less' 字符转为 '.css'
            {
                test: '\\.less',
            },
        ],
    ],

    ...

});
// babel-less-to-css.js
module.exports = function () {
    return {
        visitor: {
            ImportDeclaration(path) {
                if (/\.less$/.test(path.node.source.value)) {
                    path.node.source.value = path.node.source.value.replace(/\.less/, '.css');
                }
            },
        },
    };
};

3.2. 第二步、实现 loader 插件

这一步要实现一个把 less 文件转换为 css 文件的插件,其中:

  1. 使用 less.js 来进行对 less 到 css 的转义。
  2. 使用 postcss 来处理 less 中的相对路径和别名路径的处理以及处理浏览器兼容问题。

这两步本身其实已经和 father 没什么太大的关系,都是 webpack 中常用的对 less 文件的处理方法,所以这里直接给出示例代码,看代码就可以知道如何在 father 中实现这些功能:

// .fatherrc.ts
import { defineConfig } from 'father';

export default defineConfig({

    ...

    plugins: [
        './loader.ts', // 实现 loader 功能
    ],

    ...

});
// loader.ts
import type { IApi } from 'father';
import { addLoader, ILoaderItem } from 'father/dist/builder/bundless/loaders';

export default async (api: IApi) => {
    const loaders: ILoaderItem[] = await api.applyPlugins({
        key: 'addPostcssLoader',
        initialValue: [
            {
                key: 'less-to-css',
                test: /\.less$/,
                loader: require.resolve('./loader-less-to-css'), // less 文件转 css 文件
            },
        ],
    });

    loaders.forEach((loader) => addLoader(loader));
};
// loader-less-to-css.js
const path = require('path');
const less = require('less');
const postcss = require('postcss');
const syntax = require('postcss-less');
const atImport = require('postcss-import');
const autoprefixer = require('autoprefixer');

const loader = function (lessContent) {
    const cb = this.async();
    this.setOutputOptions({
        ext: '.css',
    });
    postcss([
        autoprefixer({
            // 提升兼容性
            overrideBrowserslist: ['last 10 versions'],
        }),
        atImport({
            resolve: (id) => {
                const currentPath = this.resource;
                if (id.startsWith('@')) {
                    // 处理别名路径,把 @ 替换成 src
                    const srcPath = path.join(__filename, './src');
                    const targetPath = id.replace(/^@/, srcPath);
                    return targetPath;
                } else {
                    // 处理相对路径
                    const relativePath = id;
                    const targetPath = path.resolve(currentPath, '..', relativePath);
                    return targetPath;
                }
            },
        }),
    ])
        .process(lessContent, { syntax })
        .then((result) => {
            // less 转 css
            less.render(result.content, (err, css) => {
                if (err) {
                    console.error(err);
                    return;
                }
                cb(null, css.css);
            });
        })
        .catch((err) => {
            console.error(err);
        });
};

module.exports = loader;

经过上述处理,就可以实现 less 的预处理,编译为 css 文件了。

四、补充

4.1 使用 babel 的原因

事实上 father 支持 babel、esbuild 和 SWC 三种构建方式,而本文使用的是 babel 模式。

father 的配置文档 中提到,platformbroswer 的时候,transformer 默认使用 babel 进行 js 的编译器,这意味着,father 官方也是推荐使用 babel 来编译前端的代码的。

即使使用 esbuild 有更快的打包速度,但是 esbuild 处理的是文件的二进制格式,很多现存的前端编译插件无法直接应用到其中;而 babel 则是生成 AST 语法树,其兼容性和拓展性也是 esbuild 和 SWC 无法比拟的。

不过最关键的还是:father 还没有实现自定义 esbuild 插件!它只提供了 babel 的自定义插件能力,这样一来其实我们别无选择。

点击这里前往 Github 查看原文,交流意见~

文档信息

版权声明:自由转载 - 非商用 - 非衍生 - 保持署名(创意共享3.0许可证