@plugin-light/project-config-vue
Version:
开箱即用的项目配置,适用于 Vue2.x 项目
584 lines (575 loc) • 24.9 kB
JavaScript
;
var fs = require('fs');
var path = require('path');
var shared = require('@plugin-light/shared');
var tComm = require('t-comm');
var webpackLoaderCrossGameStyle = require('@plugin-light/webpack-loader-cross-game-style');
var webpackLoaderCrossPlatform = require('@plugin-light/webpack-loader-cross-platform');
var webpackLoaderIfdef = require('@plugin-light/webpack-loader-ifdef');
var webpackLoaderPublish = require('@plugin-light/webpack-loader-publish');
var webpackPluginModifyHtml = require('@plugin-light/webpack-plugin-modify-html');
var FileManagerPlugin = require('filemanager-webpack-plugin');
var PrerenderSpaPlugin = require('prerender-spa-plugin');
var webpackBundleAnalyzer = require('webpack-bundle-analyzer');
var HooksScriptPlugin = require('webpack-hooks-shellscripts');
var WorkboxPlugin = require('workbox-webpack-plugin');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
const VUE2_VUE3_COMMON_CDN_URLS = [
shared.CDN_MAP.AXIOS,
shared.CDN_MAP.VUE_LAZY_LOAD,
shared.CDN_MAP.E_MONITOR,
shared.CDN_MAP.AEGIS_WEB,
];
const DEFAULT_CDN_URLS = [
shared.CDN_MAP.VUE_V2,
shared.CDN_MAP.VUE_ROUTER_V3,
shared.CDN_MAP.VUEX_V3,
...VUE2_VUE3_COMMON_CDN_URLS,
];
const VUE3_CDN_URLS = [
shared.CDN_MAP.VUE_V3,
shared.CDN_MAP.VUE_ROUTER_V4,
shared.CDN_MAP.VUEX_V4,
...VUE2_VUE3_COMMON_CDN_URLS,
];
const DEFAULT_PROJECT_MAP = {
'project/pvp-match-apply': 'project/match-apply',
'project/pvp-match-apply-mobile': 'project/match-apply-mobile',
'project/gp-match-apply': 'project/match-apply',
'project/gp-match-apply-mobile': 'project/match-apply-mobile',
'project/lol-match-apply': 'project/match-apply',
'project/lol-match-apply-mobile': 'project/match-apply-mobile',
'project/qp-match-apply': 'project/match-apply',
'project/qp-match-apply-mobile': 'project/match-apply-mobile',
};
const DEFAULT_HANDLE_IF_DEF_FILES = /(press-ui|component|press-plus|@tencent\/pmd-).*(\.vue|\.ts|\.js|\.css|\.scss)$/;
const DEFAULT_OUTPUT_DIR_MODE = 'dist/${VUE_APP_DIR}/static';
function initWorkboxPlugin(useWorkBoxPlugin = {}) {
return new WorkboxPlugin.GenerateSW({
swDest: `./service-worker-${shared.getProjectName()}-${shared.getSubProjectName()}.js`,
clientsClaim: true,
include: [/.*(.js|.css|.svg|.png|.jpg|.jpeg)/],
skipWaiting: true,
maximumFileSizeToCacheInBytes: 13 * 1024 * 1024,
importScripts: [],
inlineWorkboxRuntime: true,
runtimeCaching: [{
urlPattern: new RegExp('^https://image-1251917893\\.file\\.myqcloud.com.*(\\.js|\\.css|\\.svg|\\.png|\\.jpg|\\.jpeg)'),
handler: 'StaleWhileRevalidate',
options: {
cacheableResponse: {
statuses: [0, 200],
},
},
}, {
urlPattern: /.*\.html$/,
handler: 'NetworkOnly',
}],
...useWorkBoxPlugin,
});
}
const LOADER_MAP = {
ifdef: webpackLoaderIfdef.LOADER,
crossGameStyle: webpackLoaderCrossGameStyle.LOADER,
crossPlatform: webpackLoaderCrossPlatform.LOADER,
crossPlatformProd: webpackLoaderCrossPlatform.LOADER_PROD,
publishUtil: webpackLoaderPublish.LOADER,
};
const curDirname = shared.getRootDir();
// 获取真实的vue-app名字
function getRealVueAppDir(shadowProjectMap = {}) {
const vueAppDir = process.env.VUE_APP_DIR || '';
if (DEFAULT_PROJECT_MAP[vueAppDir]) {
return DEFAULT_PROJECT_MAP[vueAppDir];
}
if (shadowProjectMap[vueAppDir]) {
return shadowProjectMap[vueAppDir];
}
return vueAppDir || '';
}
const publishPara = process.env.publish;
function getAppName() {
const arr = (process.env.VUE_APP_DIR || '').split('/');
return arr[arr.length - 1];
}
// 输出路径,构建的ssr包放在ssr目录下,构建的static包放在static目录下
function getOutputPath(outputDirMode) {
if (process.env.VUE_APP_PLUGIN) {
return path__namespace.resolve(curDirname, 'dist', process.env.VUE_APP_DIR || '', 'plugin');
}
if (process.env.VUE_APP_SSR) {
return path__namespace.resolve(curDirname, 'dist', process.env.VUE_APP_DIR || '', 'ssr');
}
const middle = outputDirMode.replace('${VUE_APP_DIR}', process.env.VUE_APP_DIR || '');
return path__namespace.resolve(curDirname, middle);
}
// 入口:entry.js, entry-server.js, entry-client.js
function getEntry(shadowProjectMap) {
return path__namespace.resolve(curDirname, 'src', getRealVueAppDir(shadowProjectMap), 'main.js'); // plugin的入口用自己的
}
// 获取目下所有项目文件夹名称并创建webpack别名
function getAllAppNameAlias(shadowProjectMap, usePMDBusinessAlias = false, lessAlias = false) {
const files = fs__namespace.readdirSync(path__namespace.resolve(curDirname, 'src'));
const result = {
foldername: [],
filename: [], // 文件名,有后缀
};
files.forEach((file) => {
const pathname = path__namespace.join(curDirname, 'src', file);
const stat = fs__namespace.lstatSync(pathname);
if (!stat.isDirectory()) {
result.filename.push(file);
}
else {
result.foldername.push(file);
}
});
const alias = {
src: path__namespace.resolve(curDirname, 'src'),
'@': path__namespace.resolve(curDirname, 'src', getRealVueAppDir(shadowProjectMap)), // 由环境变量确定当前的项目
};
if (!lessAlias) {
result.foldername.forEach((dir) => {
alias[dir] = path__namespace.resolve(curDirname, 'src', dir);
});
}
if (usePMDBusinessAlias) {
return {
...alias,
'src/component': 'pmd-business',
};
}
return alias;
}
// 需要构建时自动注入的cdn url
function getCdnInject({ isVue3 = false, useUniSimpleRouterCDN = false, customCdnUrls = [], }) {
const objs = {
mqq: 'mqq',
wx: 'wx',
msdkShare: 'msdkShare',
vue: 'Vue',
'vue-router': 'VueRouter',
vuex: 'Vuex',
axios: 'axios',
'vue-lazyload': 'VueLazyload',
'aegis-web-sdk': 'window Aegis',
};
// 必要的 cdn 写这里。构建时注入 html 中。(不是所有页面都要的,页面自己加载)
let cdnUrls = DEFAULT_CDN_URLS;
if (isVue3) {
cdnUrls = VUE3_CDN_URLS;
}
if (customCdnUrls?.length) {
cdnUrls = customCdnUrls;
}
if (useUniSimpleRouterCDN) {
objs['uni-simple-router'] = 'Router';
cdnUrls.push(shared.CDN_MAP.UNI_SIMPLE_ROUTER);
}
return { objs, cdnUrls };
}
function getWebpackBaseConfig(options) {
const { isUseVueLoader, isVue3, useXSS, useIfDefLoader, handleIfDefFiles, shadowProjectMap = {}, } = tComm.merge({}, {
isUseVueLoader: true,
isVue3: false,
useXSS: true,
useIfDefLoader: true,
handleIfDefFiles: DEFAULT_HANDLE_IF_DEF_FILES,
}, options || {});
let terserPureFuncs = ['console.log', 'console.table'];
let transpileDependencies = shared.DEFAULT_TRANSPILE_DEPENDENCIES;
if (options?.terserPureFuncs) {
terserPureFuncs = options.terserPureFuncs || [];
}
if (options?.transpileDependencies) {
transpileDependencies = options.transpileDependencies;
}
const outputDirMode = options?.outputDirMode ?? DEFAULT_OUTPUT_DIR_MODE;
const config = {
publicPath: process.env.VUE_APP_PUBLICPATH,
outputDir: getOutputPath(outputDirMode),
assetsDir: '',
lintOnSave: process.env.NODE_ENV === 'development',
productionSourceMap: true,
transpileDependencies,
parallel: false,
css: {
extract: false,
},
devServer: {
port: options?.port ?? (process.env.port || process.env.npm_config_port || 443),
https: options?.https ?? true,
proxy: {
'/pvp/share/getsharecfg': {
target: 'https://igame.qq.com',
changeOrigin: true,
ws: true,
},
'/pvp/share/gethash': {
target: 'https://igame.qq.com',
changeOrigin: true,
ws: true,
},
},
disableHostCheck: (process.env.NODE_ENV || '').startsWith('development'),
},
configureWebpack: {
entry: getEntry(shadowProjectMap),
name: getAppName(),
resolve: {
alias: getAllAppNameAlias(shadowProjectMap, options?.usePMDBusinessAlias, options?.lessAlias),
extensions: ['js', 'vue', 'json', 'ts'],
},
// 可用来测试webpack运行时机制
// optimization: {
// minimize: false,
// runtimeChunk: { name: 'runtime' },
// },
...(shared.checkDebugMode() ? {
optimization: {
minimize: false,
},
} : {}),
plugins: [
options?.useWorkBoxPlugin ? initWorkboxPlugin(options.useWorkBoxPlugin) : undefined,
].filter(item => item),
},
chainWebpack(config) {
config.module.rule('vue')
.uses
.delete('cache-loader');
config.module.rule('js')
.uses
.delete('cache-loader');
config.module.rule('ts')
.uses
.delete('cache-loader');
config.module.rule('tsx')
.uses
.delete('cache-loader');
if (useIfDefLoader) {
config.module
.rule('ifdef-loader')
// 根据项目实际配置文件类型
.test(handleIfDefFiles)
// 不要配成下面这样,会卡住
// .test(/\.vue|\.ts|\.js|\.css|\.scss$/)
.use(LOADER_MAP.ifdef)
.loader(LOADER_MAP.ifdef)
.options({
context: {
...shared.DEFAULT_CONTEXT_OBJECT,
H5: true,
WEB: true,
VUE2: true,
__NOT_UNI__: true,
},
type: ['css', 'js', 'html'],
})
.end();
}
// 先暂时去掉eslint
config.module.rule('eslint')
.uses
.clear();
config
.when(process.env.PUBLISH_ENV !== 'test', (config) => {
// 去掉console.log
config.optimization.minimizer('terser')
.tap((args) => {
// remove debugger
args[0].terserOptions.compress.drop_debugger = true;
// 移除 console.log
args[0].terserOptions.compress.pure_funcs = terserPureFuncs;
// 去掉注释 如果需要看chunk-vendors公共部分插件,可以注释掉就可以看到注释了
args[0].terserOptions.output = {
comments: false,
};
return args;
});
});
// config.plugins.delete('preload'); // 这个不禁止,预加载很快会被用到的内容
config.plugins.delete('prefetch'); // 这个禁止掉,首屏不预加载其他路由的js,需要预加载的路由,引入的时候,加上/* webpackPrefetch: true */
config.plugin('preload')
.use('@vue/preload-webpack-plugin')
.tap(() => [{
rel: 'preload',
// 不写这个runtime.js会被打包进去
fileBlacklist: [/\.map$/, /hot-update\.js$/, /runtime\..*\.js$/],
include: 'initial',
}]);
if (shared.checkBundleAnalyze()) {
console.log('进行包分析');
config
.plugin('webpack-bundle-analyzer')
.use(webpackBundleAnalyzer.BundleAnalyzerPlugin)
.end();
}
else if (process.env.NODE_ENV !== 'development') {
config
.plugin('webpack-bundle-analyzer')
.use(new webpackBundleAnalyzer.BundleAnalyzerPlugin({
analyzerMode: 'static',
openAnalyzer: false,
reportFilename: 'my-bundle-analyze.html',
}))
.end();
}
// set preserveWhitespace
config.module
.rule('vue')
.test(/\.vue$/)
.use(LOADER_MAP.crossGameStyle, LOADER_MAP.crossPlatformProd) // 处理样式的loader,必须在vue-loader前执行
.loader(LOADER_MAP.crossGameStyle, LOADER_MAP.crossPlatformProd)
.end();
if (isUseVueLoader) {
config.module
.rule('vue')
.test(/\.vue$/)
.use('vue-loader')
.loader('vue-loader')
.tap((vueOptions) => {
if (vueOptions) {
vueOptions.compilerOptions.whitespace = 'preserve';
}
return vueOptions;
})
.tap((vueOptions) => {
if (useXSS) {
vueOptions.compilerOptions.directives = {
html(node, directiveMeta) {
(node.props || (node.props = [])).push({
name: 'innerHTML',
value: `xss ? xss(_s(${directiveMeta.value})) : ${directiveMeta.value}`,
});
},
};
}
return vueOptions;
})
.end();
}
config.module
.rule('js')
.test(/\.[jt]s$/)
.use(LOADER_MAP.crossPlatform) // 处理样式的loader,必须在vue-loader前执行
.loader(LOADER_MAP.crossPlatform)
.end();
if (process.env.NODE_ENV === 'production') {
config.devtool('hidden-module-source-map'); // 对外隐藏map信息、使用源码定位出错位置
}
else {
config.devtool('eval-source-map'); // 生成速度更快
}
config
.when(process.env.NODE_ENV !== 'development', (config) => {
config
.plugin('ScriptExtHtmlWebpackPlugin')
.after('html')
.use('script-ext-html-webpack-plugin', [{
// `runtime` must same as runtimeChunk name. default is `runtime`
inline: /runtime\..*\.js$/,
}])
.end();
config
.optimization.splitChunks({
chunks: 'all',
maxInitialRequests: 10,
cacheGroups: {
libs: {
name: 'chunk-libs',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial', // only package third parties that are initially dependent
},
elementUI: {
name: 'chunk-elementUI',
// the weight needs to be larger than libs and app or it will be packaged into libs or app
priority: 20,
test: /[\\/]node_modules[\\/]_?element-ui(.*)/, // in order to adapt to cnpm
},
comm: {
name: 'chunk-comm',
test: /[\\/]src[\\/]_?comm(.*)/,
minChunks: 2,
minSize: 10 * 1024,
maxSize: 244 * 1024,
priority: 5,
reuseExistingChunk: true,
},
component: {
name: 'chunk-component',
test: /[\\/]src[\\/]_?component(.*)/,
minChunks: 2,
minSize: 10 * 1024,
maxSize: 244 * 1024,
priority: 5,
reuseExistingChunk: true,
},
logic: {
name: 'chunk-logic',
test: /[\\/]src[\\/]_?logic(.*)/,
minChunks: 2,
minSize: 10 * 1024,
maxSize: 244 * 1024,
priority: 5,
reuseExistingChunk: true,
},
core_js: {
name: 'core-js',
test: /[\\/]node_modules[\\/]_??core-js(.*)/,
chunks: 'initial',
priority: 30,
reuseExistingChunk: true,
},
pmd_npm: {
name: 'pmd-npm',
test: /node_modules[\\/]_?@tencent[\\/]_?pmd-/,
chunks: 'initial',
priority: 30,
reuseExistingChunk: true,
},
},
});
config.optimization.runtimeChunk('single');
});
let serving = false;
process.argv.forEach((i) => {
if (i.indexOf('--serving') == 0) {
serving = true;
}
});
// 指定html模板文件
config.plugin('html')
.tap((args) => {
args[0].template = path__namespace.resolve(curDirname, 'src', getRealVueAppDir(shadowProjectMap) || 'comm', 'index.html'); // 业务目录下自己的模板。默认在vue-cli3/public/index.html
args[0].minify = false; // 不需要压缩,没几个字节,不压缩方便后续处理
return args;
});
// config.when(process.env.NODE_ENV !== 'hotreload', config => {
if (!serving) {
console.log(`--------not serving now: ${process.env.NODE_ENV}----------`);
// html后处理:title替换,cdnurl自动注入。自定义HtmlmodifyPlugin插件
config.plugin('Htmlmodify')
.use(new webpackPluginModifyHtml.HtmlModifyPlugin({
onEnd: {
html: [{
source: path__namespace.resolve(curDirname, 'src', getRealVueAppDir(shadowProjectMap), 'index.html'),
destination: `${getOutputPath(outputDirMode)}/index.html`,
ssr: process.env.VUE_APP_SSR,
urls: getCdnInject({
isVue3,
customCdnUrls: options?.customCdnUrls,
}).cdnUrls,
}],
},
}))
.tap((args) => args);
if (process.env.npm_config_prerender) {
console.log('开始预渲染');
config
.plugin('prerender-spa-plugin')
.use(PrerenderSpaPlugin, [
{
// 生成文件的路径,也可以与webpakc打包的一致。
// 这个目录只能有一级,如果目录层次大于一级,在生成的时候不会有任何错误提示,在预渲染的时候只会卡着不动。
// 预渲染项目不要设置 outputDirMode
staticDir: path__namespace.join(curDirname, './dist'),
outputDir: getOutputPath(outputDirMode),
indexPath: `${getOutputPath(outputDirMode)}/index.html`,
routes: ['/', '/index'],
renderer: new (PrerenderSpaPlugin.PuppeteerRenderer)({
inject: {
initLocation: false, // 是否初始化定位组件,为false不会渲染定位相关
},
// headless: false,
renderAfterTime: 10000,
// 在 main.js 中 document.dispatchEvent(new Event('render-event')),两者的事件名称要对应上。
// renderAfterDocumentEvent: 'render-event',
}),
},
])
.end();
}
// 打包构建后的文件
config.plugin('FileManager')
.use(new FileManagerPlugin({
onEnd: [{
archive: [{
source: getOutputPath(outputDirMode),
destination: `${getOutputPath(outputDirMode)}/static_${getAppName()}_${tComm.getIPAddressStr()}.tar`,
format: 'tar',
}],
}],
}))
.tap((args) => args);
/* 申明外部全局变量
不经常改动的库、我们通过cdn引入. require('Vue')或者import('Vue') 时webpack不对他们进行打包,
可以减少代码的大小、也可以减少服务器的带宽,更能把这些文件缓存到客户端,客户端加载的会更快。
*/
config.externals(getCdnInject({
isVue3,
useUniSimpleRouterCDN: options?.useUniSimpleRouterCDN,
}).objs);
if (publishPara == 'test') {
console.log('编译并发布到测试环境');
config
.plugin('webpack-hooks-shellscripts')
.use(HooksScriptPlugin({
beforeRun: [],
afterEmit: [
`node ${LOADER_MAP.publishUtil}`,
],
}))
.end();
}
else if (publishPara == 'prod') {
console.log('编译并发布到正式环境');
config
.plugin('webpack-hooks-shellscripts')
.use(HooksScriptPlugin({
beforeRun: [],
afterEmit: [
`node ${LOADER_MAP.publishUtil} prod`,
],
}))
.end();
}
else {
console.log('只编译不发布');
config
.plugin('webpack-hooks-shellscripts')
.use(HooksScriptPlugin({
beforeRun: [],
afterEmit: [],
}))
.end();
}
// })
}
},
};
return config;
}
const WEBPACK_BASE_CONFIG = {
...getWebpackBaseConfig(),
};
exports.WEBPACK_BASE_CONFIG = WEBPACK_BASE_CONFIG;
exports.getWebpackBaseConfig = getWebpackBaseConfig;