UNPKG

@plugin-light/project-config-vue

Version:
584 lines (575 loc) 24.9 kB
'use strict'; 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;