「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」
前言:源于在用小程序原生语言开发时,想接入scss预编译语言来提高写wxss的效率,快速编写wxss。
背景
由于是使用的原生小程序语言,受限小程序的语法及目录规则,但也想利用更现代化的编程手段,首先从css接入scss开始,来让我们利用工程化的手段来实现scss在小程序的接入。
开发思路
- 目录改造,首先需要先改造下微信小程序默认给的目录结构,dist的目录是最终输出目录,供微信使用
-
配置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等构建工具,较为进阶的使用,而不是当一个配置工程师,能够更深入的了解构建功能的能力,服务我们的项目,深入其原理。
如果有更好的方式,也可以更深入的交流,虚心接受任何建议,此文只是自己的一个记录,期望能够对他人也能提供帮助。