font-spider-webpack-plugin
Version:
Font-spider webpack plugin
265 lines (257 loc) • 9.45 kB
JavaScript
import { Compilation, sources } from 'webpack';
import { validate } from 'schema-utils';
import fs from 'fs';
import path from 'path';
import fontSpider from 'font-spider';
import chalk from 'chalk';
import cheerio from 'cheerio';
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
/**
* 字体文件正则
*/
const regxFont = /\.(woff2|woff|eot|ttf|svg)$/i;
/**
* @font-face正则
*/
const regxFace = /@font-face[\s]*\{(.+?)\}/gi;
/**
* 临时文件目录
*/
const tmpPath = path.resolve(__dirname, '../../tmp');
/**
* 清空目录
*
* @export
* @param {string} dirPath - 目录地址
*/
function emptyDir(dirPath) {
if (fs.existsSync(dirPath)) {
let files = [];
files = fs.readdirSync(dirPath);
files.forEach(function (file) {
const curPath = `${dirPath}/${file}`;
if (fs.statSync(curPath).isDirectory()) {
emptyDir(curPath);
}
else {
fs.unlinkSync(curPath);
}
});
}
}
/**
* 写入临时文件
*
* @export
* @param {string} dirPath - 文件地址
* @param {(string | Buffer)} source - 内容
*/
function writeTmp(dirPath, source) {
if (!fs.existsSync(tmpPath)) {
fs.mkdirSync(tmpPath);
}
if (dirPath.indexOf('/') !== -1) {
const dirs = dirPath.split('/');
dirs.pop();
dirs.reduce((prev, next) => {
const dir = `${prev}/${next}`;
if (!fs.existsSync(path.resolve(__dirname, `../${dir}`))) {
fs.mkdirSync(path.resolve(__dirname, `../${dir}`));
}
return dir;
}, 'tmp');
}
fs.writeFileSync(path.join(tmpPath, dirPath), source);
}
/**
* 获取路径中的文件名
*
* @export
* @param {string} [pathname='']
* @returns
*/
function getFilename(pathname = '') {
return pathname.split('/').pop() || pathname;
}
// 选项对象的 schema
const schema = {
type: 'object',
properties: {
fonts: {
type: 'array',
},
fontSpiderOptions: {
type: 'object',
},
},
};
/**
* 字蛛webpack插件
*
* @class FontSpiderWebpackPlugin
*/
class FontSpiderWebpackPlugin {
constructor(options = {}) {
Object.defineProperty(this, "manifest", {
enumerable: true,
configurable: true,
writable: true,
value: new Map()
});
validate(schema, options, {
name: 'FontSpiderWebpackPlugin',
baseDataPath: 'options',
});
this.options = options;
}
apply(compiler) {
const pluginName = this.constructor.name;
// Webpack 5
if (compiler.hooks.initialize) {
compiler.hooks.compilation.tap(pluginName, (compilation) => {
compilation.hooks.processAssets.tapPromise({
name: pluginName,
stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
}, (assets) => this.optimize(assets, compilation));
});
}
else {
compiler.hooks.emit.tapPromise(pluginName, (compilation) => {
return this.optimize(compilation.assets, compilation);
});
}
}
/**
* 处理资源
*
* @param {{ [index: string]: sources.Source }} assets - 资源列表
* @param {Compilation} compilation - Compilation模块
* @memberof FontSpiderWebpackPlugin
*/
optimize(assets, compilation) {
return __awaiter(this, void 0, void 0, function* () {
if (Object.keys(assets).length === 0) {
return;
}
emptyDir(tmpPath);
const scripts = new Map();
const allFontFaces = [];
Object.entries(assets).forEach(([pathname, source]) => {
const content = source.source();
if (pathname.endsWith('.js')) {
scripts.set(pathname, content);
}
else if (pathname.endsWith('.css')) {
const result = content
.toString()
.replace(/[\r\n]/g, '')
.match(regxFace);
if (result) {
allFontFaces.push(...result);
}
}
else if (regxFont.test(pathname)) {
const filename = getFilename(pathname);
this.manifest.set(filename, pathname);
writeTmp(filename, content);
}
});
if (scripts.size === 0) {
return;
}
const files = new Map();
const fonts = this.options.fonts;
allFontFaces.forEach((value) => {
const $ = cheerio.load('');
const family = value.match(/font-family:[\s]*['|"]*(.+?)['|"]*;/);
if (!family || (fonts && !fonts.includes(family[1]))) {
return;
}
value = value.replace(/url\(['|"]*(.+?)['|"]*\)/g, ($0, $1) => {
const filename = getFilename($1);
if (filename) {
return `url(./${filename})`;
}
return $0;
});
$('head').append(`<style>${value}body{${family[0]}}</style>`);
scripts.forEach((scriptValue) => {
$('body').append(scriptValue.replace(/</g, '<').replace(/>/g, '>'));
});
files.set(family[1], $.html());
});
const htmls = [];
files.forEach((value, key) => {
const url = path.join(tmpPath, `${key}.html`);
fs.writeFileSync(url, value);
htmls.push(url);
});
try {
yield this.compression(htmls, compilation);
}
catch (e) {
console.log(chalk.red('font spider字体提取压缩异常'));
Promise.reject(e);
}
});
}
/**
* 压缩字体
*
* @param {string[]} htmls - 页面路径列表
* @param {Compilation} compilation - Compilation模块
* @memberof FontSpiderWebpackPlugin
*/
compression(htmls, compilation) {
return __awaiter(this, void 0, void 0, function* () {
const fontSpiderOptions = this.options.fontSpiderOptions;
const originalFonts = yield fontSpider.spider(htmls, fontSpiderOptions || {
silent: true,
backup: false,
});
if (!originalFonts || originalFonts.length === 0) {
throw Error('没有提取出任何引用的字体包所要渲染的字符');
}
console.log('字体分析提取完毕,进行压缩...');
const fonts = yield fontSpider.compressor(originalFonts, fontSpiderOptions || { backup: false });
fonts.forEach((font) => {
console.log('');
console.log(chalk.green('已提取') +
chalk.bgGreen.black(font.chars.length) +
chalk.green('个') +
chalk.bgGreen.black(font.family) +
chalk.green('字体'));
font.files.forEach((file) => {
const filename = getFilename(file.url);
const source = new sources.RawSource(fs.readFileSync(file.url));
const pathname = this.manifest.get(filename);
compilation.updateAsset(pathname, source);
console.log(chalk.white(`${file.format} 优化后的文件体积为 ${chalk.green(`${(file.size / 1024).toFixed(2)}KiB`)}`));
});
});
console.log('');
console.log(chalk.green('字体压缩成功'));
});
}
}
export { FontSpiderWebpackPlugin as default };