小程序工程化系列——scss to wxss 插件

1,643 阅读4分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

前言:源于在用小程序原生语言开发时,想接入scss预编译语言来提高写wxss的效率,快速编写wxss。

背景

由于是使用的原生小程序语言,受限小程序的语法及目录规则,但也想利用更现代化的编程手段,首先从css接入scss开始,来让我们利用工程化的手段来实现scss在小程序的接入。

开发思路

  • 目录改造,首先需要先改造下微信小程序默认给的目录结构,dist的目录是最终输出目录,供微信使用

image.png

  • 配置webpack,这里我们选用webpack来作为我们的构建工具,用来转换scss。

  • 由于微信小程序的的目录结构是根据微信的要求,所以没有一个统一的入口,是一个多入口编译的场景。因此需要自己编写插件,来遍历scss文件,并做转换。

具体步骤

安装依赖

yarn add webpack webpack-cli node-sass replace-ext sass-loader webpack-fix-style-only-entries clean-webpack-plugin copy-webpack-plugin

配置webpack

  • 首先我们先把src目录直接copy到dist目录,利用CopyWebpackPlugin插件来做这个事情.
        new CopyWebpackPlugin({
            patterns: [
                {
                    from: resolve(__dirname, './src'),
                    to: resolve(__dirname, './dist')
                }
            ]
        })
  • 由于小程序中每个页面和每个组件都有一个wxss文件,所以webpack打包编译的时候需要配置多入口来输出多个wxss文件

    • 这里使用EntryPlugin这个插件,来配置多个入口,总体思路就是遍历我们的开发目录,获取所有的scss文件

编写multiScssEntryPlugin的插件

新建一个plugins文件夹,并新建MultiScssEntryPlugin.js文件。

插件原理

  • 通过entryOption钩子。我们在该阶段,修改我们的配置文件,以此配置多个编译入口。
  • 根据我们要编译scss文件的目录,进行遍历所有的文件,过滤出符合我们条件的文件路径
  • 使用EntryPlugin将我们得到符合条件的文件,进行依次配置
  • 监听watchRun钩子函数,当文件有变动时,进行编译
const EntryPlugin = require('webpack/lib/EntryPlugin');
const path = require('path');
const fs = require('fs');
const replaceExt = require('replace-ext');

function entryToPlugin(context,item) {
    return new EntryPlugin(context, item)
}
// 配置多入口
function applyEntry(context, entry, compiler) {
    if (typeof entry === 'string') {
        entryToPlugin(context, entry).apply(compiler);
    } else if (Array.isArray(entry)) {
        entry.forEach(item => {
            console.log(item)
            entryToPlugin(context, item).apply(compiler);
        })
    } else if (typeof entry === 'object') {
        Object.keys(entry).forEach(name => {
            const item = entry[name];
            if (Array.isArray(item)) {
                item.forEach(subItem => {
                    entryToPlugin(context, subItem).apply(compiler);
                })
            } else {
                entryToPlugin(context, item).apply(compiler);
            }
        })
    }
}
// 遍历文件,获取需要转移的scss文件
function getFileForassetExtension(entries, filePath, exts) {
    // 根据文件路径读取文件,返回文件列表
    const files = fs.readdirSync(filePath);
    // 遍历获取到的文件列表
    files.forEach((filename) => {
        // 获取当前文件的绝对路径
        const filedir = path.join(filePath, filename);
        // 根据文件路径获取文件信息,返回一个fs.stats对象
        const stats = fs.statSync(filedir);
        const isFile = stats.isFile(); // 是文件
        const isDir = stats.isDirectory(); // 是文件夹

        if (isFile && ~exts.indexOf(path.extname(filedir))) {
            const curDirname = filedir.slice(0, filedir.lastIndexOf('.'));
            if (!~entries.indexOf(curDirname)) {
                entries.push(curDirname);
            }
        }
        if (isDir) {
            getFileForassetExtension(entries, filedir, exts); // 利用递归,继续遍历下面的文件夹
        }
    })
}
// 扁平化所有的入口文件
function inflateEntries(entries, compiler, {assetExtensions}) {
    entries.forEach(item => {
        getFileForassetExtension(entries, item, [...assetExtensions])
    })
}
function all(entry, extensions) {
    const items = [];
    for (const ext of extensions) {
        const file = replaceExt(entry, ext);
        if (fs.existsSync(file)) {
            items.push(file);
        }
    }

    return items;
}
class MultiScssEntryPlugin {
    constructor(options = {}) {
        this.ext = options.ext || [];
        this.entries = this.ext;
    }
    // 配置多入口
    applyEntry(compiler, done) {
        const {context} = compiler.options;
        // 把所有需要编译的scss文件都合并到一个entry钟,交给entryPlugin处理
        const assets = this.entries
        .reduce((items, item) => [...items, ...all(item, ['.scss'])], [])
        .filter(item => item !== null)
        .map(item => `./${path.relative(context, item)}`);
        applyEntry(context, assets, compiler);
        if (done) {
            done();
        }
    }
    apply(compiler) {
        const {context, entry} = compiler.options;
        inflateEntries(this.entries, compiler, {
            assetExtensions: ['.scss'],
        })
        compiler.hooks.entryOption.tap('MultiScssEntryPlugin', () => {
            this.applyEntry(compiler);
        });

        compiler.hooks.watchRun.tap('MultiScssEntryPlugin', (compiler, done) => {
            this.applyEntry(compiler, done);
        })
    }
}

module.exports = MultiScssEntryPlugin;

使用该插件:

 new MultiScssEntryPlugin({
            ext: [join(resolve('src'))],
 })

处理默认输出的[name].js文件

利用FixStyleOnlyEntriesPlugin插件,我们可以删除webpack打包出的[name].js。

输出文件

由于是多入口,所以也需要陪住一个多输出文件,这里直接只用[name].js来实现。因为如果我们只设置一个入口文件的话,最后构建会报错:Conflict: Multiple chunks emit assets to the same filename index.js

{
 output: {
       path: resolve('dist'),
       filename: '[name].js'
 },
}

输出产物将scss文件移除掉,可以在CopyWebpackPlugin插件中配置

 new CopyWebpackPlugin({
            patterns: [
                {
                    from: resolve(__dirname, './src'),
                    to: resolve(__dirname, './dist'),
                    globOptions: {
                        ignore: ['**/*.scss'],
                    }
                }
            ]
        }),

完整的配置文件

const { resolve, join} = require('path');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
const FixStyleOnlyEntriesPlugin = require('webpack-fix-style-only-entries');
const MultiScssEntryPlugin = require('./plugins/MultiScssEntryPlugin');
module.exports = {
    mode: 'development',
    entry: './index.js',
    output: {
        path: resolve('dist'),
        filename: '[name].js'
    },
    module: {
        rules: [
            {
                test: /\.scss$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            useRelativePath: true,
                            name: '[path][name].wxss',
                            context: resolve('src')
                        }
                    },
                    {
                        loader: 'sass-loader'
                    }
                ]
            }
        ]
    },
    plugins: [
        new FixStyleOnlyEntriesPlugin(),
        new CleanWebpackPlugin(),
        new CopyWebpackPlugin({
            patterns: [
                {
                    from: resolve(__dirname, './src'),
                    to: resolve(__dirname, './dist'),
                    globOptions: {
                        ignore: ['**/*.scss'],
                    }
                }
            ]
        }),
        new MultiScssEntryPlugin({
            ext: [join(resolve('src'))],
        })
    ]
}

总结

在想到在原生小程序中引入scss时,由于不像我们其他的框架都有完整的构建工具及脚手架,而且为了不破坏现有原生开发的整体逻辑,无法去引入现有的一些跨端框架时,我们可以自己去完成这些编译工作,只做增强型的构建工具,而不影响现有逻辑。

并且通过这种方式,能够很好的锻炼我们针对webpack等构建工具,较为进阶的使用,而不是当一个配置工程师,能够更深入的了解构建功能的能力,服务我们的项目,深入其原理。

如果有更好的方式,也可以更深入的交流,虚心接受任何建议,此文只是自己的一个记录,期望能够对他人也能提供帮助。