webpack-multiple-plugin
Version:
> 这是一个组成多页面Webpack的插件,基于``html-webpack-plugin``,内部使用了``ejs``插件进行模板编译
521 lines (484 loc) • 15.7 kB
text/typescript
// 用于处理html
import HtmlWebpackPlugin from "html-webpack-plugin";
// 引入webpack
import { Compiler, WebpackOptionsNormalized, EntryOptionPlugin } from "webpack";
// 引入注入类的插件
import WebpackInjectVarPlugin from "./WebpackInjectVarPlugin";
// 处理路径
import path from "path";
// 文件操作
import fs from "fs";
// ejs 使用
import ejs from "ejs";
// 引入类型定义
/**
* ------------------------------------------
* 脚本加载参数
* 设置脚本加载的模式
* ------------------------------------------
*/
enum ScriptLoading {
Blocking = "blocking",
Defer = "defer",
}
/**
* ------------------------------------------
* 引入模式
* 指定在头部引入还是在主体引入
* ------------------------------------------
*/
enum ScriptInject {
Head = "head",
Body = "body",
}
/**
* ------------------------------------------
* 页面参数
* 负责配置多页面的页面配置项
* ------------------------------------------
*/
interface PageOptions {
path: string;
template: string | Function;
scriptLoading?: ScriptLoading | Function | null;
entry?: string[] | string | Function | null;
chunks?: string[] | Function | null;
data?: any | Function | null;
inject?: ScriptLoading | string | Function | null;
outputChunkPath?: string | Function | null;
chunkName?: string | Function;
outputTemplatePath?: string | Function | null;
minify?: boolean | null;
}
/**
* ------------------------------------------
* 选项参数
* ------------------------------------------
*/
interface Options {
// 根路径,替代page 里面path属性的 [ROOT]
root?: string | null;
outputTemplatePath: string;
outputChunkPath: string | null;
templateJoinEntry?: boolean;
pages: PageOptions[];
commons?: CommonOptions[];
scriptLoading?: ScriptLoading;
minify?: boolean;
defaultCommonName?: string | null;
chunkName?: string;
inject?: ScriptInject;
defaultEntryPath?: string;
defaultCommonOrder?: Order;
injectVariable?: boolean;
}
/**
* 排列
*/
enum Order {
start = 0,
end = 1,
}
/**
* ----------------------------------
* 公共模块参数
* ----------------------------------
*/
interface CommonOptions {
name: string;
entry: string | string[];
outputPath?: string;
}
/**--------------------------------
* 主类
* --------------------------------
*/
class WebpackMultiplePlugin {
/**--------------------------------
* 默认配置
* --------------------------------
*/
config: Options = {
root: "",
outputTemplatePath: "[PATH].html",
chunkName: "pages/[PATH]",
outputChunkPath: null,
templateJoinEntry: false,
pages: [],
commons: [],
scriptLoading: ScriptLoading.Blocking,
minify: true,
defaultCommonName: null,
inject: ScriptInject.Head,
defaultEntryPath: "[TEMPLATE_DIR]/index.js",
defaultCommonOrder: Order.start,
injectVariable: true,
};
constructor(config: Options) {
Object.assign(this.config, config);
}
apply(compiler: Compiler) {
let options: WebpackOptionsNormalized = compiler.options;
let context: string = compiler.context;
let {
pages,
outputChunkPath,
outputTemplatePath,
chunkName,
commons,
defaultCommonName,
templateJoinEntry,
root,
minify,
scriptLoading,
inject,
defaultEntryPath,
defaultCommonOrder,
injectVariable,
} = this.config;
let complieChunks: any = {};
let pagesData: any = {};
/**
* ------------------------------------------
* 处理公共模块
* ------------------------------------------
*/
if (commons) {
commons.forEach((common: CommonOptions) => {
let commonName = common.name;
if (commonName) {
let entry = getAbsolutePath(
toCallback(common.entry, { context, path: commonName })
);
if (entry) {
try {
if (Array.isArray(entry)) {
for (let tentry of entry) {
fs.accessSync(tentry);
}
} else {
fs.accessSync(entry);
}
let data: any = {};
data.import = Array.isArray(entry) ? [...entry] : [entry];
complieChunks[commonName] = data;
if (common.outputPath) {
data.filename = common.outputPath;
}
} catch (err) {
throw new Error("file does not exist : " + err);
}
}
}
});
}
/**
* ------------------------------------------
* 页面处理
* ------------------------------------------
*/
let strFillData: any = {
ROOT: root,
PATH: null,
TEMPLATE_DIR: null,
};
if(pages && pages.length > 0){
pages.forEach((page: PageOptions, index: number) => {
// 因为模板是最重要的参数之一,如果没有设置会提示给用户
page.path ?? showError(`pages[${index}].path`, ERROR_TYPE.NO_EXIST);
page.template ??
showError(`pages[${index}].template`, ERROR_TYPE.NO_EXIST);
// 当前配置项,防止与全局混淆
let currentConfig: PageOptions = page;
// 赋值数据填充
strFillData.PATH = page.path;
strFillData.TEMPLATE_DIR = path.dirname(<string>page.template);
// 路径
currentConfig.path = page.path;
// 模板路径
currentConfig.template = <string>(
getAbsolutePath(
fillStr(
toCallback(
page.template,
{ context, path: page.path },
path.resolve(context, path.join(page.path, "index.html"))
)
)
)
);
// 入口
currentConfig.entry = getAbsolutePath(
Array.isArray(page.entry)
? page.entry.map((sentry) =>
toCallback<string>(sentry, { context, path: currentConfig.path })
)
: fillStr(<string>defaultEntryPath)
);
// 注入方式
currentConfig.inject = toCallback(
page.inject,
{ context, path: currentConfig.path },
inject
);
// 输出的chuank名称
currentConfig.chunkName = fillStr(
toCallback(
page.chunkName,
{ context, path: currentConfig.path },
chunkName
)
);
// script标签加载方式
currentConfig.scriptLoading = toCallback<ScriptLoading>(
page.scriptLoading,
{ context, path: currentConfig.path },
scriptLoading
);
// 如果页面有单独定义的模板名称,则使用单独定义的
currentConfig.outputTemplatePath =
fillStr(
toCallback<string>(
page.outputTemplatePath,
{
context,
path: currentConfig.path,
},
outputTemplatePath
)
) ?? null;
// 如果页面有单独定义的chunk名称,则使用单独定义的
currentConfig.outputChunkPath =
fillStr(
toCallback<string>(
page.outputChunkPath,
{ context, path: currentConfig.path },
outputChunkPath
)
) ?? null;
// 更多的chuank
currentConfig.chunks = toCallback<string[]>(
page.chunks,
{ context, path: currentConfig.chunks },
[]
);
// Minify压缩
currentConfig.minify = toCallback(
page.minify,
{ context, path: currentConfig.path },
minify
);
// 注入到模板的数据
currentConfig.data = toCallback(
page.data,
{ context, path: currentConfig.path },
{}
);
// 优化命名
currentConfig.outputChunkPath = optimizeChunkName(
currentConfig.outputChunkPath
);
// 快捷访问变量
let ctemplate: string = currentConfig.template;
let centry: string | string[] = currentConfig.entry;
let cchunkName: string = currentConfig.chunkName;
let coutputChunkPath: string = currentConfig.outputChunkPath;
let coutputTemplatePath: string = currentConfig.outputTemplatePath;
let cchunks: string[] | null = currentConfig.chunks;
let cinject: string = <string>currentConfig.inject;
let cminify: boolean | null = <boolean>currentConfig.minify;
let cscriptLoading: string | null = <string>currentConfig.scriptLoading;
let cdata: object = currentConfig.data;
// 生成chunk列表
if (centry) {
try {
if (Array.isArray(centry)) {
for (let scentry of centry) {
fs.accessSync(<string>scentry);
}
} else {
fs.accessSync(<string>centry);
}
pagesData[<string>cchunkName] = currentConfig;
let chunkEntry: any = {
import: Array.isArray(centry) ? [...centry] : [centry],
};
complieChunks[<string>cchunkName] = chunkEntry;
coutputChunkPath ? (chunkEntry.filename = coutputChunkPath) : void 0;
if (templateJoinEntry) {
complieChunks[<string>cchunkName].import.push(ctemplate);
}
} catch (err) {
showError(String(err), ERROR_TYPE.CUSTOM);
}
}
// 生成html插件,便于输出
if (ctemplate) {
let currentChunks = [<string>cchunkName, ...cchunks];
if (typeof defaultCommonName === "string") {
currentChunks.push(<string>(<unknown>defaultCommonName));
}
// 防止奇怪的路径问题
options.plugins.push(
new HtmlWebpackPlugin({
filename: coutputTemplatePath.replace(/^[\\\/]+/, ""),
inject: <ScriptInject>cinject,
minify: cminify,
scriptLoading: <ScriptLoading>cscriptLoading,
templateContent: (): Promise<string> => {
let p1 = new Promise<string>((res) => {
ejs.renderFile(
ctemplate,
cdata,
{},
(err, str: string): void => {
if (err) {
throw err;
}
res(str);
}
);
});
return p1;
},
chunks: currentChunks,
})
);
}
});
}else{
// 如果出现错误代表是网页发生的错误
showError("pages",ERROR_TYPE.NO_DATA);
}
// 装订chunks数据准备注入页面
compiler.hooks.entryOption.tap("WebpackMultiplePlugin",() : any =>{
let keys: string[] = Object.keys(complieChunks);
let index: number = keys.indexOf(<string>defaultCommonName);
if (index >= 0) {
keys.splice(index, 1);
if (defaultCommonOrder === Order.start) {
keys.unshift(<string>defaultCommonName);
} else {
keys.push(<string>defaultCommonName);
}
}
keys.forEach((key) => {
let data: any = {};
data[key] = complieChunks[key];
EntryOptionPlugin.applyEntryOption(compiler, context, <any>data);
});
})
// 执行变量注入插件
if (injectVariable) {
options.plugins.push(
new WebpackInjectVarPlugin({
data: pagesData,
})
);
}
/**
* 获取绝对路径
* @param {String} subPath
* @returns
*/
function getAbsolutePath(subPath: string | string[]): string | string[] {
if (typeof subPath === "string") {
if (path.isAbsolute(subPath)) {
return subPath;
} else {
if (subPath.indexOf("[ROOT]") >= 0 && !root) {
showError("root", ERROR_TYPE.NO_EXIST);
}
if (typeof root === "string") {
subPath = subPath
.replace(/\[ROOT\]/g, <string>root)
.replace(/[\\\/]{2,}/g, "/");
}
return path.resolve(context, subPath);
}
} else if (Array.isArray(subPath)) {
return <string[]>subPath.map((spath) => {
return getAbsolutePath(spath);
});
} else {
return "";
}
}
/**
* 调用某个函数
* @param {Function} fun
* @param {Object} params
* @param {Object} defaultVal
* @returns
*/
function toCallback<T>(fun: any, params: object, defaultVal?: any): T {
if (typeof fun === "function") {
let result: any = fun(params);
if (typeof result === "function") {
return result(params) || defaultVal;
} else {
return result || defaultVal;
}
} else {
return fun || defaultVal;
}
}
/**
* 优化路径,防止 //或者\\的出现
* @param {String} name
* @returns
*/
function optimizeChunkName(name: string): string {
if (typeof name === "string") {
name = name.replace(/[\\\/]{2,}/, "/");
}
return name;
}
/**
* 数据填充
*/
function fillStr(str: string): string {
if (typeof str === "string" && str) {
let allStrFillDataKey: string[] = Object.keys(strFillData);
allStrFillDataKey.forEach((fillKey) => {
let data: string = strFillData[fillKey];
if (data.indexOf("[" + fillKey + "]") >= 0) {
showError(
`"${str}" in "[${fillKey}]" does not conform to the specification because it cannot contain itself. This writing is wrong because it cannot be parsed normally. If you want to keep running, it will cycle all the time`,
ERROR_TYPE.CUSTOM
);
}
});
function toStr(str: string): string {
str = str.replace(/\[(.+?)\]/g, (current, fillKey) => {
let data = strFillData[fillKey];
let exp: RegExpExecArray | null = /\[(.+?)\]/g.exec(data);
let strFillKeys: string[] = Object.keys(strFillData);
if (data && exp != null && strFillKeys.indexOf(exp[1]) >= 0) {
data = toStr(data);
}
return data ?? current;
});
return str;
}
str = toStr(str);
return str;
}
return str;
}
}
}
// 错误类型 $1 代表报错的变量,在自定义模式中,代表错误信息
enum ERROR_TYPE {
CUSTOM = "$1",
NO_EXIST = `The data "$1" does not exist`,
NO_DATA = `"$1" cannot be empty`,
NO_STANDARD = `The data "$1" does not meet the specification`,
NOT_FILE_EXIST = `The file "$1" does not exist`,
}
function showError(tag: string, error_fill: ERROR_TYPE) {
throw new Error(
"[WebpackMultiplePlugin] : " + error_fill.replace(/\$1/g, tag)
);
}
export default WebpackMultiplePlugin;