微信小程序主包具有2M的最大限制,因此压缩程序源码成为一个优化的可能。下面是一些探索的结论以及为解决问题而做的一些方案。
1. 一个结论
先说一个结论:压缩JS
文件和WXSS
文件对于缩小主包体积是没有作用的,js
和wxss
文件的确是源码中最大的两个部分,但是小程序开发工具在打包上传的时候可以设置自动压缩这两部分,因此我们不必做多余的动作(实验发现做了也是无用的)。
2. 压缩WXML
WXML的压缩和单纯的HTML压缩有两个主要的不同点:
WXML
里面可能存在WXS
脚本,而WXS
的处理不同于WXML
的处理。WXML
的属性是区分大小写的,因此采用常见的gulp插件(如gulp-htmlmin
)压缩WXML
可能改变这种这种属性,同时它也不能处理wxs
。
对于第一种情况,首先的一点建议是总是将wxs脚本单独放在一个文件中,而不是内联在WXML中,这样做的好处是可以将wxs作为一个标签来处理,而wxs文件也可以做单独处理。
由于我们不可以避免的会引入别人的npm包,而别人可能不采用这种规范(如iview
),所以在处理wxml压缩的时候仍然不可避免的要考虑这种情况。
对于第二种情况,既然压缩html
的gulp
插件可能对wxml
有副作用,那么可以考虑以下两个解决方案:
- 自己开发一个gulp插件。
- 自己开发一个内联插件(内联插件介绍)。
下面是我开发的一个压缩WXML的内联插件示例(全部的代码将会在后面展示):
function minifyWXML() {
return through2.obj(function (file, _, cb) {
if (file.isBuffer()) {
let code = file.contents.toString();
const result = code.match(/<wxs(.*?)>[\s\S]*(<\/wxs>)/g); // 提取wxs内容
code = code.replace(/<wxs(.*?)>[\s\S]*(<\/wxs>)/g, '')
.replace(/\n+|\t+/g, ' ') // 删除换行和缩进
.replace(/\s+/g, ' ') // 删除多个空格
.replace(/(>\s)/g, '>') // 删除标签前面和后面的空格
.replace(/(\s<)/g, '<')
.replace(/<!--[\w\W\r\n]*?-->/g, '') // 删除注释
if (result) {
code = result.join('') + '\n' + code; // 将wxs拼接到wxml的前面
}
// eslint-disable-next-line no-param-reassign
file.contents = Buffer.from(code);
}
cb(null, file);
});
}
对于wxml中wsx,这里只是将它提取了出来,然后放到文件的头部,有两个原因:
- 压缩这一个部分可能导致未知的问题,例如函数名被替换,不同人的编码习惯导致压缩后代码出错等。
- wxs中的内容在整个应用中占比是比较小的。
删除换行和缩进的时候一定要留一个空格,因为我们的组件通常是以下方式书写的
若不换行可能导致出现<viewwx:for....>
这种无法识别的标签。
最后得到的结果是这样的(压缩iview的代码),对于一个比较大的项目来说,这种压缩大致可以减少100kb左右。
3. 压缩json
压缩json比较简单,直接使用gulp-jsonminify
即可,这种压缩也可以减少几十kb。
4. 压缩图片
这里不做图片压缩,理由如下:
- 图片应尽量放在cdn里而不是本地。
- 对于少量的图片可以事先设计好,或者使用字体图标。
5. wxss的压缩
css压缩没什么用,这里还是简单介绍一下吧,由于wxss里面的@import和css的import语法不同,因此不能使用通常的css gulp插件压缩,下面是一个我实现的内联插件:
function minifyWXSS() {
return through2.obj(function (file, _, cb) {
if (file.isBuffer()) {
let code = file.contents.toString();
code = code.replace(/\/\*[\s\S]*\*\//g, '');
const result = code.match(/@import\s+["'].*["'];?/g);
code = code.replace(/@import\s+["'].*["'];?/g, '');
code = cleanCSS.minify(code).styles;
if (result) {
code = result.join('\n') + '\n' + code;
}
// eslint-disable-next-line no-param-reassign
file.contents = Buffer.from(code);
}
cb(null, file);
});
}
6. 总结
总的来说就是, 压缩小程序代码就是将所有代码处理后放在一个新的目录下,相当于新建了一个应用,然后改变project.config.json
下的miniprogramRoot
为新目录即可。建议miniprogram_npm
下的文件也一起压缩。这种压缩可以获得100-200多kb的空间。
下面是完整的代码gulpfile文件代码(gulp版本>=4.0.0):
/**
* 微信小程序优化压缩工具
*/
const gulp = require('gulp');
const colors = require('colors');
const pump = require('pump');
const jsonminify = require('gulp-jsonminify');
const del = require('del');
const size = require('gulp-size');
const terser = require('gulp-terser');
const through2 = require('through2');
const CleanCSS = require('clean-css');
const JsSizeBefore = size({ title: 'js压缩前:' });
const JsSizeAfter = size({ title: 'js压缩后:' });
const WXMLSizeBefore = size({ title: 'WXML压缩前:' });
const WXMLSizeAfter = size({ title: 'WXML压缩后:' });
const WXSSSizeBefore = size({ title: 'WXSS压缩前:' });
const WXSSSizeAfter = size({ title: 'WXSS压缩后:' });
const JSONSizeBefore = size({ title: 'JSON压缩前:' });
const JSONSizeAfter = size({ title: 'JSON压缩后:' });
const OtherSize = size({ title: '其它文件:' });
function getSize(SizeObj) {
return SizeObj.size || 0;
}
function getTotalSize() {
const before = getSize(JsSizeBefore.size) + getSize(WXMLSizeBefore.size)
+ getSize(WXSSSizeBefore.size) + getSize(JSONSizeBefore.size) + getSize(OtherSize.size);
const after = getSize(JsSizeAfter.size) + getSize(WXMLSizeAfter.size)
+ getSize(WXSSSizeAfter.size) + getSize(JSONSizeAfter.size) + getSize(OtherSize.size);
return {
beforeSize: `${(before / 1024).toFixed(3)}KB`,
afterSize: `${(after / 1024).toFixed(3)}KB`,
};
}
const cleanCSS = new CleanCSS({
units: {
rpx: true,
},
});
function minifyWXML() {
return through2.obj(function (file, _, cb) {
if (file.isBuffer()) {
let code = file.contents.toString();
const result = code.match(/<wxs(.*?)>[\s\S]*(<\/wxs>)/g);
code = code.replace(/<wxs(.*?)>[\s\S]*(<\/wxs>)/g, '')
.replace(/\n+|\t+/g, ' ')
.replace(/\s+/g, ' ')
.replace(/(>\s)/g, '>')
.replace(/(\s<)/g, '<')
.replace(/<!--[\w\W\r\n]*?-->/g, '');
if (result) {
code = result.join('') + '\n' + code;
}
// eslint-disable-next-line no-param-reassign
file.contents = Buffer.from(code);
}
cb(null, file);
});
}
function minifyWXSS() {
return through2.obj(function (file, _, cb) {
if (file.isBuffer()) {
let code = file.contents.toString();
code = code.replace(/\/\*[\s\S]*\*\//g, '');
const result = code.match(/@import\s+["'].*["'];?/g);
code = code.replace(/@import\s+["'].*["'];?/g, '');
code = cleanCSS.minify(code).styles;
if (result) {
code = result.join('\n') + '\n' + code;
}
// eslint-disable-next-line no-param-reassign
file.contents = Buffer.from(code);
}
cb(null, file);
});
}
// 配置项
const conf = {
// 开发目录
devPath: 'src',
// 编译目录
prodPath: 'dist',
filesPath: {
// js文件
js: '/**/*.js',
// wxss文件
wxss: '/**/**.wxss',
// wxml文件
wxml: '/**/**.wxml',
wxs: '/**/**.wxs',
// json文件
json: '/**/**.json',
// 其它(jpg|jpeg|png|gif)
other: '/**/{**.jpg,**.jpeg,**.png,**.gif,**.webp,**.eot,**.ttf,**.woff,**.woff2,**.svg}',
},
ignore: ['/node_modules/**'],
};
conf.ignore = conf.ignore.map(path => `!${conf.devPath}${path}`);
// 显示时间
const getTime = function () {
return '[' + colors.white(new Date().getHours() + ':' + new Date().getMinutes() + ':' + new Date().getSeconds()) + '] ';
};
// 输出log
const _log = function (msg) {
console.log(getTime() + msg);
};
/**
* 优化js文件
* @param filePath
*/
const _optJS = function (filePath) {
_log(colors.white('对 js 文件进行优化...'));
return pump([gulp.src(filePath, {
base: conf.devPath,
}),
JsSizeBefore,
terser({
keep_classnames: true,
keep_fnames: true,
}).on('error', function (err) {
_log('【错误】 '.red + '文件: ' + err.fileName + ', ' + err.cause);
}),
JsSizeAfter,
gulp.dest(conf.prodPath)]);
};
/**
* 优化wxss文件
* @param filePath
*/
const _optWXSS = function (filePath) {
_log(colors.white('对 wxss 文件进行优化...'));
return gulp.src(filePath, {
base: conf.devPath,
})
.pipe(WXSSSizeBefore)
.pipe(minifyWXSS())
.pipe(WXSSSizeAfter)
.pipe(gulp.dest(conf.prodPath));
};
/**
* 优化json文件
* @param filePath
* @param cb
* @private
*/
const _optJSON = function (filePath) {
_log(colors.white('对 json 文件进行优化...'));
return gulp.src(filePath, {
base: conf.devPath,
})
.pipe(JSONSizeBefore)
.pipe(jsonminify())
.pipe(JSONSizeAfter)
.pipe(gulp.dest(conf.prodPath));
// cb();
};
/**
* 优化wxml文件
* @param filePath
* @private
*/
const _optWXML = function (filePath) {
_log(colors.white('对 wxml 文件进行优化...'));
return gulp.src(filePath, {
base: conf.devPath,
}).pipe(WXMLSizeBefore)
.pipe(minifyWXML())
.pipe(WXMLSizeAfter)
.pipe(gulp.dest(conf.prodPath));
};
/**
* 复制文件
* @param filePath
*/
const _copyFiles = function (filePath) {
return gulp.src(filePath, {
base: conf.devPath,
}).pipe(OtherSize).pipe(gulp.dest(conf.prodPath));
};
// 清理编译目录
gulp.task('clean', function () {
_log(colors.white('开始清理 ' + conf.prodPath + ' 目录...'));
return del([conf.prodPath] + '/**/*').then(function (paths) {
_log(colors.white('清理完毕, 共清理 ' + colors.red(paths.length) + ' 个文件和目录。'));
});
});
// 优化js
gulp.task('js', function () {
const files = [
conf.devPath + conf.filesPath.js,
...conf.ignore,
];
return _optJS(files);
});
// 优化wxss
gulp.task('wxss', function () {
const files = [
conf.devPath + conf.filesPath.wxss,
...conf.ignore,
];
return _optWXSS(files);
});
// 优化wxml
gulp.task('wxml', function () {
const files = [
conf.devPath + conf.filesPath.wxml,
...conf.ignore,
];
return _optWXML(files);
});
// 优化json
gulp.task('json', function () {
const files = [
conf.devPath + conf.filesPath.json,
...conf.ignore,
];
return _optJSON(files);
});
// 优化其它
gulp.task('other', function () {
const files = [
conf.devPath + conf.filesPath.other,
conf.devPath + conf.filesPath.wxs,
conf.devPath + conf.filesPath.wxss,
conf.devPath + conf.filesPath.js,
...conf.ignore];
// eslint-disable-next-line no-useless-concat
_log(colors.white('对 ' + 'jpg/jpeg/png/gif/svg' + ' 文件进行优化...'));
return _copyFiles(files);
});
// 计时
let startTime;
let endTime;
// 开始任务
gulp.task('startProd', function (cb) {
startTime = new Date().getTime();
_log(colors.green('开始优化, 请稍候...'));
cb();
});
gulp.task('endProd', function (cb) {
_log(colors.green('优化完毕, 请查看 ' + conf.prodPath + ' 目录。'));
const totalSize = getTotalSize();
_log(colors.green(`压缩前总尺寸${totalSize.beforeSize},压缩后总尺寸:${totalSize.afterSize}`));
endTime = new Date().getTime();
_log(colors.green('耗时: ' + (endTime - startTime) + 'ms'));
cb();
});
// 编译任务
// gulp.task('default', gulp.series('startProd', 'clean', gulp.parallel('other', 'json', 'wxml', 'wxss', 'js'), 'endProd'));
gulp.task('default', gulp.series('startProd', 'clean', gulp.parallel('other', 'json', 'wxml'), 'endProd'));
参考资料:wxmini-optimize