staven630
vue-cli4-config
JavaScript

vue-cli4配置vue.config.js持续更新

Last updated Jul 8, 2026
2.7k
Stars
600
Forks
45
Issues
0
Stars/day
Attention Score
53
Language breakdown
No language data available.
Files click to expand
README

禁止私自转载。运营公众号、社交账号,请坚持原创。勿做知识剽窃者!

vue-cli4 全面配置(持续更新)

  细致全面的 vue-cli4 配置信息。涵盖了使用 vue-cli 开发过程中大部分配置需求。

  不建议直接拉取此项目作为模板,希望能按照此教程按需配置,或者复制 vue.config.js 增删配置,并自行安装所需依赖。

  vue-cli3 配置见 vue-cli3 分支

  Vite 配置见 vite-config

其他系列

Blog

Nuxt.js 全面配置

目录

✅ 配置多环境变量

  通过在 package.json 里的 scripts 配置项中添加--mode xxx 来选择不同环境

  只有以 VUEAPP 开头的变量会被 webpack.DefinePlugin 静态嵌入到客户端侧的包中,代码中可以通过 process.env.VUEAPPBASEAPI 访问

  NODEENV 和 BASEURL 是两个特殊变量,在代码中始终可用

配置

  在项目根目录中新建.env, .env.production, .env.analyz 等文件

  • .env
  serve 默认的本地开发环境配置
NODE_ENV = "development"
BASE_URL = "./"
VUEAPPPUBLIC_PATH = "./"
VUEAPPAPI = "https://test.staven630.com/api"
  • .env.production
  build 默认的环境配置(正式服务器)
NODE_ENV = "production"
BASE_URL = "https://prod.staven630.com/"
VUEAPPPUBLIC_PATH = "https://prod.oss.com/staven-blog"
VUEAPPAPI = "https://prod.staven630.com/api"

ACCESSKEYID = "xxxxxxxxxxxxx" ACCESSKEYSECRET = "xxxxxxxxxxxxx" REGION = "oss-cn-hangzhou" BUCKET = "staven-prod" PREFIX = "staven-blog"

  • .env.crm
  自定义 build 环境配置(预发服务器)
NODE_ENV = "production"
BASE_URL = "https://crm.staven630.com/"
VUEAPPPUBLIC_PATH = "https://crm.oss.com/staven-blog"
VUEAPPAPI = "https://crm.staven630.com/api"

ACCESSKEYID = "xxxxxxxxxxxxx" ACCESSKEYSECRET = "xxxxxxxxxxxxx" REGION = "oss-cn-hangzhou" BUCKET = "staven-crm" PREFIX = "staven-blog"

IS_ANALYZE = true;

  修改 package.json

"scripts": {
  "build": "vue-cli-service build",
  "crm": "vue-cli-service build --mode crm"
}
使用环境变量
<template>
  <div class="home">
    <!-- template中使用环境变量 -->
     API: {{ api }}
  </div>
</template>

▲ 回顶部

✅ 配置基础 vue.config.js

const ISPROD = ["production", "prod"].includes(process.env.NODEENV);

module.exports = { publicPath: ISPROD ? process.env.VUEAPPPUBLICPATH : "./", // 默认'/',部署应用包时的基本 URL // outputDir: process.env.outputDir || 'dist', // 'dist', 生产环境构建文件的目录 // assetsDir: "", // 相对于outputDir的静态资源(js、css、img、fonts)目录 lintOnSave: false, runtimeCompiler: true, // 是否使用包含运行时编译器的 Vue 构建版本 productionSourceMap: !IS_PROD, // 生产环境的 source map parallel: require("os").cpus().length > 1, pwa: {} };

▲ 回顶部

✅ 配置 proxy 代理解决跨域问题

  假设 mock 接口为https://www.easy-mock.com/mock/5bc75b55dc36971c160cad1b/sheets/1

module.exports = {
  devServer: {
    // overlay: { // 让浏览器 overlay 同时显示警告和错误
    //   warnings: true,
    //   errors: true
    // },
    // open: false, // 是否打开浏览器
    // host: "localhost",
    // port: "8080", // 代理断就
    // https: false,
    // hotOnly: false, // 热更新
    proxy: {
      "/api": {
        target:
          "https://www.easy-mock.com/mock/5bc75b55dc36971c160cad1b/sheets", // 目标代理接口地址
        secure: false,
        changeOrigin: true, // 开启代理,在本地创建一个虚拟服务端
        // ws: true, // 是否启用websockets
        pathRewrite: {
          "^/api": "/"
        }
      }
    }
  }
};

  访问

▲ 回顶部

✅ 修复 HMR(热更新)失效

  如果热更新失效,如下操作:

module.exports = {
  chainWebpack: config => {
    // 修复HMR
    config.resolve.symlinks(true);
  }
};

▲ 回顶部

✅ 修复 Lazy loading routes Error: Cyclic dependency https://github.com/vuejs/vue-cli/issues/1669

module.exports = {
  chainWebpack: config => {
    // 如果使用多页面打包,使用vue inspect --plugins查看html是否在结果数组中
    config.plugin("html").tap(args => {
      // 修复 Lazy loading routes Error
      args[0].chunksSortMode = "none";
      return args;
    });
  }
};

▲ 回顶部

✅ 添加别名 alias

const path = require("path");
const resolve = dir => path.join(__dirname, dir);
const ISPROD = ["production", "prod"].includes(process.env.NODEENV);

module.exports = { chainWebpack: config => { // 添加别名 config.resolve.alias .set("vue$", "vue/dist/vue.esm.js") .set("@", resolve("src")) .set("@assets", resolve("src/assets")) .set("@scss", resolve("src/assets/scss")) .set("@components", resolve("src/components")) .set("@plugins", resolve("src/plugins")) .set("@views", resolve("src/views")) .set("@router", resolve("src/router")) .set("@store", resolve("src/store")) .set("@layouts", resolve("src/layouts")) .set("@static", resolve("src/static")); } };

▲ 回顶部

✅ 压缩图片

npm i -D image-webpack-loader

  在某些版本的 OSX 上安装可能会因缺少 libpng 依赖项而引发错误。可以通过安装最新版本的 libpng 来解决。

brew install libpng
module.exports = {
  chainWebpack: config => {
    if (IS_PROD) {
      config.module
        .rule("images")
        .use("image-webpack-loader")
        .loader("image-webpack-loader")
        .options({
          mozjpeg: { progressive: true, quality: 65 },
          optipng: { enabled: false },
          pngquant: { quality: [0.65, 0.9], speed: 4 },
          gifsicle: { interlaced: false }
          // webp: { quality: 75 }
        });
    }
  }
};

▲ 回顶部

✅ 自动生成雪碧图

  默认 src/assets/icons 中存放需要生成雪碧图的 png 文件。首次运行 npm run serve/build 会生成雪碧图,并在跟目录生成 icons.json 文件。再次运行命令时,会对比 icons 目录内文件与 icons.json 的匹配关系,确定是否需要再次执行 webpack-spritesmith 插件。

npm i -D webpack-spritesmith
let has_sprite = true;
let files = [];
const icons = {};

try { fs.statSync(resolve("./src/assets/icons")); files = fs.readdirSync(resolve("./src/assets/icons")); files.forEach(item => { let filename = item.toLocaleLowerCase().replace(/_/g, "-"); icons[filename] = true; });

} catch (error) { fs.mkdirSync(resolve("./src/assets/icons")); }

if (!files.length) { has_sprite = false; } else { try { let iconsObj = fs.readFileSync(resolve("./icons.json"), "utf8"); iconsObj = JSON.parse(iconsObj); has_sprite = files.some(item => { let filename = item.toLocaleLowerCase().replace(/_/g, "-"); return !iconsObj[filename]; }); if (has_sprite) { fs.writeFileSync(resolve("./icons.json"), JSON.stringify(icons, null, 2)); } } catch (error) { fs.writeFileSync(resolve("./icons.json"), JSON.stringify(icons, null, 2)); has_sprite = true; } }

// 雪碧图样式处理模板 const SpritesmithTemplate = function(data) { // pc let icons = {}; let tpl = .ico { display: inline-block; background-image: url(${data.sprites[0].image}); background-size: ${data.spritesheet.width}px ${data.spritesheet.height}px; };

data.sprites.forEach(sprite => { const name = "" + sprite.name.toLocaleLowerCase().replace(/_/g, "-"); icons[${name}.png] = true; tpl = ${tpl} .ico-${name}{ width: ${sprite.width}px; height: ${sprite.height}px; background-position: ${sprite.offsetx}px ${sprite.offsety}px; } ; }); return tpl; };

module.exports = { configureWebpack: config => { const plugins = []; if (has_sprite) { plugins.push( new SpritesmithPlugin({ src: { cwd: path.resolve(__dirname, "./src/assets/icons/"), // 图标根路径 glob: "*/.png" // 匹配任意 png 图标 }, target: { image: path.resolve(__dirname, "./src/assets/images/sprites.png"), // 生成雪碧图目标路径与名称 // 设置生成CSS背景及其定位的文件或方式 css: [ [ path.resolve(__dirname, "./src/assets/scss/sprites.scss"), { format: "functionbasedtemplate" } ] ] }, customTemplates: { functionbasedtemplate: SpritesmithTemplate }, apiOptions: { cssImageRef: "../images/sprites.png" // css文件中引用雪碧图的相对位置路径配置 }, spritesmithOptions: { padding: 2 } }) ); }

config.plugins = [...config.plugins, ...plugins]; } };

▲ 回顶部

✅ SVG 转 font 字体

npm i -D svgtofont

  根目录新增 scripts 目录,并新建 svg2font.js 文件:

const svgtofont = require("svgtofont");
const path = require("path");
const pkg = require("../package.json");

svgtofont({ src: path.resolve(process.cwd(), "src/assets/svg"), // svg 图标目录路径 dist: path.resolve(process.cwd(), "src/assets/fonts"), // 输出到指定目录中 fontName: "icon", // 设置字体名称 css: true, // 生成字体文件 startNumber: 20000, // unicode起始编号 svgicons2svgfont: { fontHeight: 1000, normalize: true }, // website = null, 没有演示html文件 website: { title: "icon", logo: "", version: pkg.version, meta: { description: "", keywords: "" }, description: `, links: [ { title: &quot;Font Class&quot;, url: &quot;index.html&quot; }, { title: &quot;Unicode&quot;, url: &quot;unicode.html&quot; } ], footerInfo: } }).then(() =&gt; { console.log(&quot;done!&quot;); });</code></pre>

&emsp;&emsp;添加 package.json scripts 配置:

<pre><code class="lang-javascript">&quot;prebuild&quot;: &quot;npm run font&quot;, &quot;font&quot;: &quot;node scripts/svg2font.js&quot;,</code></pre>

&emsp;&emsp;执行:

<pre><code class="lang-javascript">npm run font</code></pre>

▲ 回顶部

<span id="svg">✅ 使用 SVG 组件</span>

<pre><code class="lang-bash">npm i -D svg-sprite-loader</code></pre>

&emsp;&emsp;新增 SvgIcon 组件。

<pre><code class="lang-javascript">&lt;template&gt; &lt;svg class=&quot;svg-icon&quot; aria-hidden=&quot;true&quot;&gt; &lt;use :xlink:href=&quot;iconName&quot; /&gt; &lt;/svg&gt; &lt;/template&gt;

&lt;style scoped&gt; .svg-icon { width: 1em; height: 1em; vertical-align: -0.15em; fill: currentColor; overflow: hidden; } &lt;/style&gt;</code></pre>

&emsp;&emsp;在 src 文件夹中创建 icons 文件夹。icons 文件夹中新增 svg 文件夹(用来存放 svg 文件)与 index.js 文件:

<pre><code class="lang-js">import SvgIcon from &quot;@/components/SvgIcon&quot;; import Vue from &quot;vue&quot;;

// 注册到全局 Vue.component(&quot;svg-icon&quot;, SvgIcon);

const requireAll = requireContext =&gt; requireContext.keys().map(requireContext); const req = require.context(&quot;./svg&quot;, false, /\.svg$/); requireAll(req);</code></pre>

&emsp;&emsp;在 main.js 中导入 icons/index.js

<pre><code class="lang-javascript">import &quot;@/icons&quot;;</code></pre>

&emsp;&emsp;修改 vue.config.js

<pre><code class="lang-javascript">const path = require(&quot;path&quot;); const resolve = dir =&gt; path.join(__dirname, dir);

module.exports = { chainWebpack: config =&gt; { const svgRule = config.module.rule(&quot;svg&quot;); svgRule.uses.clear(); svgRule.exclude.add(/node_modules/); svgRule .test(/\.svg$/) .use(&quot;svg-sprite-loader&quot;) .loader(&quot;svg-sprite-loader&quot;) .options({ symbolId: &quot;icon-[name]&quot; });

const imagesRule = config.module.rule(&quot;images&quot;); imagesRule.exclude.add(resolve(&quot;src/icons&quot;)); config.module.rule(&quot;images&quot;).test(/\.(png|jpe?g|gif|svg)(\?.*)?$/); } };</code></pre>

▲ 回顶部

<span id="removecss">✅ 去除多余无效的 css</span>

&emsp;&emsp;注:谨慎使用。可能出现各种样式丢失现象。

  • 方案一:@fullhuman/postcss-purgecss
<pre><code class="lang-bash">npm i -D postcss-import @fullhuman/postcss-purgecss</code></pre>

&emsp;&emsp;更新 postcss.config.js

<pre><code class="lang-javascript">const autoprefixer = require(&quot;autoprefixer&quot;); const postcssImport = require(&quot;postcss-import&quot;); const purgecss = require(&quot;@fullhuman/postcss-purgecss&quot;); const ISPROD = [&quot;production&quot;, &quot;prod&quot;].includes(process.env.NODEENV); let plugins = []; if (IS_PROD) { plugins.push(postcssImport); plugins.push( purgecss({ content: [ &quot;./layouts/*/.vue&quot;, &quot;./components/*/.vue&quot;, &quot;./pages/*/.vue&quot; ], extractors: [ { extractor: class Extractor { static extract(content) { const validSection = content.replace( /&lt;style([\s\S]*?)&lt;\/style&gt;+/gim, &quot;&quot; ); return ( validSection.match(/[A-Za-z0-9-/:]*[A-Za-z0-9-/]+/g) || [] ); } }, extensions: [&quot;html&quot;, &quot;vue&quot;] } ], whitelist: [&quot;html&quot;, &quot;body&quot;], whitelistPatterns: [ /el-.*/, /-(leave|enter|appear)(|-(to|from|active))$/, /^(?!cursor-move).+-move$/, /^router-link(|-exact)-active$/ ], whitelistPatternsChildren: [/^token/, /^pre/, /^code/] }) ); } module.exports = { plugins: [...plugins, autoprefixer] };</code></pre>

  • 方案二:purgecss-webpack-plugin
<pre><code class="lang-bash">npm i -D glob-all purgecss-webpack-plugin</code></pre>

<pre><code class="lang-javascript">const path = require(&quot;path&quot;); const glob = require(&quot;glob-all&quot;); const PurgecssPlugin = require(&quot;purgecss-webpack-plugin&quot;); const resolve = dir =&gt; path.join(__dirname, dir); const ISPROD = [&quot;production&quot;, &quot;prod&quot;].includes(process.env.NODEENV);

module.exports = { configureWebpack: config =&gt; { const plugins = []; if (IS_PROD) { plugins.push( new PurgecssPlugin({ paths: glob.sync([resolve(&quot;./*/.vue&quot;)]), extractors: [ { extractor: class Extractor { static extract(content) { const validSection = content.replace( /&lt;style([\s\S]*?)&lt;\/style&gt;+/gim, &quot;&quot; ); return ( validSection.match(/[A-Za-z0-9-/:]*[A-Za-z0-9-/]+/g) || [] ); } }, extensions: [&quot;html&quot;, &quot;vue&quot;] } ], whitelist: [&quot;html&quot;, &quot;body&quot;], whitelistPatterns: [ /el-.*/, /-(leave|enter|appear)(|-(to|from|active))$/, /^(?!cursor-move).+-move$/, /^router-link(|-exact)-active$/ ], whitelistPatternsChildren: [/^token/, /^pre/, /^code/] }) ); } config.plugins = [...config.plugins, ...plugins]; } };</code></pre>

▲ 回顶部

<span id="analyze">✅ 添加打包分析</span>

<pre><code class="lang-javascript">const BundleAnalyzerPlugin = require(&quot;webpack-bundle-analyzer&quot;) .BundleAnalyzerPlugin;

module.exports = { chainWebpack: config =&gt; { // 打包分析 if (IS_PROD) { config.plugin(&quot;webpack-report&quot;).use(BundleAnalyzerPlugin, [ { analyzerMode: &quot;static&quot; } ]); } } };</code></pre>

▲ 回顶部

<span id="externals">✅ 配置 externals 引入 cdn 资源</span>

&emsp;&emsp;防止将某些 import 的包(package)打包到 bundle 中,而是在运行时(runtime)再去从外部获取这些扩展依赖

<pre><code class="lang-javascript">module.exports = { configureWebpack: config =&gt; { config.externals = { vue: &quot;Vue&quot;, &quot;element-ui&quot;: &quot;ELEMENT&quot;, &quot;vue-router&quot;: &quot;VueRouter&quot;, vuex: &quot;Vuex&quot;, axios: &quot;axios&quot; }; }, chainWebpack: config =&gt; { const cdn = { // 访问https://unpkg.com/element-ui/lib/theme-chalk/index.css获取最新版本 css: [&quot;//unpkg.com/element-ui@2.10.1/lib/theme-chalk/index.css&quot;], js: [ &quot;//unpkg.com/vue@2.6.10/dist/vue.min.js&quot;, // 访问https://unpkg.com/vue/dist/vue.min.js获取最新版本 &quot;//unpkg.com/vue-router@3.0.6/dist/vue-router.min.js&quot;, &quot;//unpkg.com/vuex@3.1.1/dist/vuex.min.js&quot;, &quot;//unpkg.com/axios@0.19.0/dist/axios.min.js&quot;, &quot;//unpkg.com/element-ui@2.10.1/lib/index.js&quot; ] };

// 如果使用多页面打包,使用vue inspect --plugins查看html是否在结果数组中 config.plugin(&quot;html&quot;).tap(args =&gt; { // html中添加cdn args[0].cdn = cdn; return args; }); } };</code></pre>

&emsp;&emsp;在 html 中添加

<pre><code class="lang-">&lt;!-- 使用CDN的CSS文件 --&gt; &lt;% for (var i in htmlWebpackPlugin.options.cdn &amp;&amp; htmlWebpackPlugin.options.cdn.css) { %&gt; &lt;link rel=&quot;stylesheet&quot; href=&quot;&lt;%= htmlWebpackPlugin.options.cdn.css[i] %&gt;&quot; /&gt; &lt;% } %&gt;

&lt;!-- 使用CDN的JS文件 --&gt; &lt;% for (var i in htmlWebpackPlugin.options.cdn &amp;&amp; htmlWebpackPlugin.options.cdn.js) { %&gt;

&lt;% } %&gt;</code></pre>

▲ 回顶部

<span id="multiple-pages">✅ 多页面打包 multi-page</span>

&emsp;&emsp;多入口页面打包,建议在 src 目录下新建 pages 目录存放多页面模块。

  • pages.config.js
&emsp;&emsp; 配置多页面信息。src/main.js 文件对应 main 字段,其他根据参照 pages 为根路径为字段。如下:

<pre><code class="lang-javascript">module.exports = { &#39;admin&#39;: { template: &#39;public/index.html&#39;, filename: &#39;admin.html&#39;, title: &#39;后台管理&#39;, }, &#39;mobile&#39;: { template: &#39;public/index.html&#39;, filename: &#39;mobile.html&#39;, title: &#39;移动端&#39;, }, &#39;pc/crm&#39;: { template: &#39;public/index.html&#39;, filename: &#39;pc-crm.html&#39;, title: &#39;预发服务&#39;, } }</code></pre>

  • vue.config.js
&emsp;&emsp;vue.config.js 的 pages 字段为多页面提供配置

<pre><code class="lang-javascript">const glob = require(&quot;glob&quot;); const pagesInfo = require(&quot;./pages.config&quot;); const pages = {};

glob.sync(&#39;./src/pages/**/main.js&#39;).forEach(entry =&gt; { let chunk = entry.match(/\.\/src\/pages\/(.*)\/main\.js/)[1]; const curr = pagesInfo[chunk]; if (curr) { pages[chunk] = { entry, ...curr, chunk: [&quot;chunk-vendors&quot;, &quot;chunk-common&quot;, chunk] } } })

module.exports = { chainWebpack: config =&gt; { // 防止多页面打包卡顿 config =&gt; config.plugins.delete(&quot;named-chunks&quot;); return config; }, pages };</code></pre>

&emsp;&emsp;如果多页面打包需要使用 CDN,使用 vue inspect --plugins 查看 html 是否在结果数组中的形式。上例中 plugins 列表中存在'html-main','html-pages/admin','html-pages/mobile', 没有'html'。因此不能再使用 config.plugin("html")。

<pre><code class="lang-javascript">const path = require(&quot;path&quot;); const resolve = dir =&gt; path.join(__dirname, dir); const ISPROD = [&quot;production&quot;, &quot;prod&quot;].includes(process.env.NODEENV);

const glob = require(&quot;glob&quot;); const pagesInfo = require(&quot;./pages.config&quot;); const pages = {};

glob.sync(&#39;./src/pages/**/main.js&#39;).forEach(entry =&gt; { let chunk = entry.match(/\.\/src\/pages\/(.*)\/main\.js/)[1]; const curr = pagesInfo[chunk]; if (curr) { pages[chunk] = { entry, ...curr, chunk: [&quot;chunk-vendors&quot;, &quot;chunk-common&quot;, chunk] } } });

module.exports = { publicPath: ISPROD ? process.env.VUEAPPPUBLICPATH : &quot;./&quot;, // configureWebpack: config =&gt; { config.externals = { vue: &quot;Vue&quot;, &quot;element-ui&quot;: &quot;ELEMENT&quot;, &quot;vue-router&quot;: &quot;VueRouter&quot;, vuex: &quot;Vuex&quot;, axios: &quot;axios&quot; }; }, chainWebpack: config =&gt; { const cdn = { // 访问https://unpkg.com/element-ui/lib/theme-chalk/index.css获取最新版本 css: [&quot;//unpkg.com/element-ui@2.10.1/lib/theme-chalk/index.css&quot;], js: [ &quot;//unpkg.com/vue@2.6.10/dist/vue.min.js&quot;, // 访问https://unpkg.com/vue/dist/vue.min.js获取最新版本 &quot;//unpkg.com/vue-router@3.0.6/dist/vue-router.min.js&quot;, &quot;//unpkg.com/vuex@3.1.1/dist/vuex.min.js&quot;, &quot;//unpkg.com/axios@0.19.0/dist/axios.min.js&quot;, &quot;//unpkg.com/element-ui@2.10.1/lib/index.js&quot; ] };

// 防止多页面打包卡顿 config =&gt; config.plugins.delete(&quot;named-chunks&quot;);

// 多页面cdn添加 Object.keys(pagesInfo).forEach(page =&gt; { config.plugin(html-${page}).tap(args =&gt; { // html中添加cdn args[0].cdn = cdn;

// 修复 Lazy loading routes Error args[0].chunksSortMode = &quot;none&quot;; return args; }); }); return config; }, pages };</code></pre>

▲ 回顶部

<span id="moment">✅ 删除 moment 语言包</span>

&emsp;&emsp;删除 moment 除 zh-cn 中文包外的其它语言包,无需在代码中手动引入 zh-cn 语言包。

<pre><code class="lang-javascript">const webpack = require(&quot;webpack&quot;);

module.exports = { chainWebpack: config =&gt; { config .plugin(&quot;ignore&quot;) .use( new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn$/) );

return config; } };</code></pre>

▲ 回顶部

<span id="log">✅ 去掉 console.log</span>

方法一:使用 babel-plugin-transform-remove-console 插件

<pre><code class="lang-bash">npm i -D babel-plugin-transform-remove-console</code></pre>

在 babel.config.js 中配置

<pre><code class="lang-">const ISPROD = [&quot;production&quot;, &quot;prod&quot;].includes(process.env.NODEENV);

const plugins = []; if (IS_PROD) { plugins.push(&quot;transform-remove-console&quot;); }

module.exports = { presets: [&quot;@vue/app&quot;, { useBuiltIns: &quot;entry&quot; }], plugins };</code></pre>

方法二:

<pre><code class="lang-javascript">const UglifyJsPlugin = require(&quot;uglifyjs-webpack-plugin&quot;); module.exports = { configureWebpack: config =&gt; { if (IS_PROD) { const plugins = []; plugins.push( new UglifyJsPlugin({ uglifyOptions: { compress: { warnings: false, drop_console: true, drop_debugger: false, pure_funcs: [&quot;console.log&quot;] //移除console } }, sourceMap: false, parallel: true }) ); config.plugins = [...config.plugins, ...plugins]; } } };</code></pre>

&emsp;&emsp;如果使用 uglifyjs-webpack-plugin 会报错,可能存在 node_modules 中有些依赖需要 babel 转译。

&emsp;&emsp;而 vue-cli 的transpileDependencies配置默认为[], babel-loader 会忽略所有 nodemodules 中的文件。如果你想要通过 Babel 显式转译一个依赖,可以在这个选项中列出来。配置需要转译的第三方库。

▲ 回顶部

<span id="splitchunks">利用 splitChunks 单独打包第三方模块</span>

<pre><code class="lang-javascript">const ISPROD = [&quot;production&quot;, &quot;prod&quot;].includes(process.env.NODEENV);

module.exports = { configureWebpack: config =&gt; { if (IS_PROD) { config.optimization = { splitChunks: { cacheGroups: { common: { name: &quot;chunk-common&quot;, chunks: &quot;initial&quot;, minChunks: 2, maxInitialRequests: 5, minSize: 0, priority: 1, reuseExistingChunk: true, enforce: true }, vendors: { name: &quot;chunk-vendors&quot;, test: /[\\/]node_modules[\\/]/, chunks: &quot;initial&quot;, priority: 2, reuseExistingChunk: true, enforce: true }, elementUI: { name: &quot;chunk-elementui&quot;, test: /[\\/]node_modules[\\/]element-ui[\\/]/, chunks: &quot;all&quot;, priority: 3, reuseExistingChunk: true, enforce: true }, echarts: { name: &quot;chunk-echarts&quot;, test: /[\\/]nodemodules\\/?echarts[\\/]/, chunks: &quot;all&quot;, priority: 4, reuseExistingChunk: true, enforce: true } } } }; } }, chainWebpack: config =&gt; { if (IS_PROD) { config.optimization.delete(&quot;splitChunks&quot;); } return config; } };</code></pre>

▲ 回顶部

<span id="gzip">✅ 开启 gzip 压缩</span>

<pre><code class="lang-bash">npm i -D compression-webpack-plugin</code></pre>

<pre><code class="lang-javascript">const CompressionWebpackPlugin = require(&quot;compression-webpack-plugin&quot;);

const ISPROD = [&quot;production&quot;, &quot;prod&quot;].includes(process.env.NODEENV); const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;

module.exports = { configureWebpack: config =&gt; { const plugins = []; if (IS_PROD) { plugins.push( new CompressionWebpackPlugin({ filename: &quot;[path].gz[query]&quot;, algorithm: &quot;gzip&quot;, test: productionGzipExtensions, threshold: 10240, minRatio: 0.8 }) ); } config.plugins = [...config.plugins, ...plugins]; } };</code></pre>

&emsp;&emsp;还可以开启比 gzip 体验更好的 Zopfli 压缩详见https://webpack.js.org/plugins/compression-webpack-plugin

<pre><code class="lang-bash">npm i -D @gfx/zopfli brotli-webpack-plugin</code></pre>

<pre><code class="lang-javascript">const CompressionWebpackPlugin = require(&quot;compression-webpack-plugin&quot;); const zopfli = require(&quot;@gfx/zopfli&quot;); const BrotliPlugin = require(&quot;brotli-webpack-plugin&quot;);

const ISPROD = [&quot;production&quot;, &quot;prod&quot;].includes(process.env.NODEENV); const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;

module.exports = { configureWebpack: config =&gt; { const plugins = []; if (IS_PROD) { plugins.push( new CompressionWebpackPlugin({ algorithm(input, compressionOptions, callback) { return zopfli.gzip(input, compressionOptions, callback); }, compressionOptions: { numiterations: 15 }, minRatio: 0.99, test: productionGzipExtensions }) ); plugins.push( new BrotliPlugin({ test: productionGzipExtensions, minRatio: 0.99 }) ); } config.plugins = [...config.plugins, ...plugins]; } };</code></pre>

▲ 回顶部

<span id="stylelint">✅ 开启 stylelint 检测scss, css语法</span>

<pre><code class="lang-bash">npm i -D stylelint stylelint-config-standard stylelint-config-prettier stylelint-webpack-plugin</code></pre>

在文件夹创建stylelint.config.js,详细配置在这里 <pre><code class="lang-javascript">module.exports = { ignoreFiles: [&quot;*/.js&quot;, &quot;src/assets/css/element-variables.scss&quot;, &quot;theme/&quot;], extends: [&quot;stylelint-config-standard&quot;, &quot;stylelint-config-prettier&quot;], rules: { &quot;no-empty-source&quot;: null, &quot;at-rule-no-unknown&quot;: [ true, { ignoreAtRules: [&quot;extend&quot;] } ] } };</code></pre> 启用webpack配置

<pre><code class="lang-javascript">const StylelintPlugin = require(&quot;stylelint-webpack-plugin&quot;);

module.exports = { configureWebpack: config =&gt; { const plugins = []; if (IS_DEV) { plugins.push( new StylelintPlugin({ files: [&quot;src//.vue&quot;, &quot;src/assets//.scss&quot;], fix: true //打开自动修复(谨慎使用!注意上面的配置不要加入js或html文件,会发生问题,js文件请手动修复) }) ); } config.plugins = [...config.plugins, ...plugins]; } }</code></pre> ▲ 回顶部

<span id="globalscss">✅ 为 sass 提供全局样式,以及全局变量</span>

&emsp;&emsp;可以通过在 main.js 中 Vue.prototype.$src = process.env.VUEAPPPUBLIC_PATH;挂载环境变量中的配置信息,然后在js中使用$src 访问。

&emsp;&emsp;css 中可以使用注入 sass 变量访问环境变量中的配置信息

<pre><code class="lang-javascript">const ISPROD = [&quot;production&quot;, &quot;prod&quot;].includes(process.env.NODEENV);

module.exports = { css: { extract: IS_PROD, sourceMap: false, loaderOptions: { scss: { // 向全局sass样式传入共享的全局变量, $src可以配置图片cdn前缀 // 详情: https://cli.vuejs.org/guide/css.html#passing-options-to-pre-processor-loaders prependData: @import "@scss/variables.scss"; @import "@scss/mixins.scss"; @import "@scss/function.scss"; $src: "${process.env.VUEAPPOSS_SRC}"; } } } };</code></pre>

在 scss 中引用

<pre><code class="lang-css">.home { background: url($src+&quot;/images/500.png&quot;); }</code></pre>

▲ 回顶部

<span id="globalless">✅ 为 less 提供全局样式,以及全局变量</span>

> npm i -D less less-loader

&emsp;&emsp;在src/assets/less目录下新建variables.less文件,并定义全局less变量 <pre><code class="lang-less">@primary-color: #1890ff; @normal-color: #d9d9d9; @text-color: #303753;</code></pre> &emsp;&emsp;vue.config.js中为其添加相应less配置。 <pre><code class="lang-js">const path = require(&#39;path&#39;) const fs = require(&#39;fs&#39;) const postcss = require(&#39;postcss&#39;) const resolve = dir =&gt; path.resolve(__dirname, dir)

const ISPROD = [&#39;prod&#39;, &#39;production&#39;].includes(process.env.NODEENV)

function getLessVaribles(fileUrl, list = {}) { if (!fs.existsSync(fileUrl)) return {}; let lessFile = fs.readFileSync(fileUrl, &#39;utf8&#39;); return postcss.parse(lessFile).nodes.reduce((acc, curr) =&gt; { acc[${curr.name.replace(/\:/, '')}] = ${curr.params}; return acc; }, list); }

const modifyVars = getLessVaribles(resolve(&#39;./src/assets/less/variables.less&#39;));

module.exports = { css: { extract: IS_PROD, // sourceMap: false, loaderOptions: { less: { modifyVars, javascriptEnabled: true, } } } }</code></pre>

<span id="globalstylus">✅ 为 stylus 提供全局变量</span>

<pre><code class="lang-bash">npm i -D style-resources-loader</code></pre>

<pre><code class="lang-javascript">const path = require(&quot;path&quot;); const resolve = dir =&gt; path.resolve(__dirname, dir); const addStylusResource = rule =&gt; { rule .use(&quot;style-resouce&quot;) .loader(&quot;style-resources-loader&quot;) .options({ patterns: [resolve(&quot;src/assets/stylus/variable.styl&quot;)] }); }; module.exports = { chainWebpack: config =&gt; { const types = [&quot;vue-modules&quot;, &quot;vue&quot;, &quot;normal-modules&quot;, &quot;normal&quot;]; types.forEach(type =&gt; addStylusResource(config.module.rule(&quot;stylus&quot;).oneOf(type)) ); } };</code></pre>

▲ 回顶部

<span id="prerender">预渲染 prerender-spa-plugin</span>

<pre><code class="lang-bash">npm i -D prerender-spa-plugin</code></pre>

<pre><code class="lang-javascript">const PrerenderSpaPlugin = require(&quot;prerender-spa-plugin&quot;); const path = require(&quot;path&quot;); const resolve = dir =&gt; path.join(__dirname, dir); const ISPROD = [&quot;production&quot;, &quot;prod&quot;].includes(process.env.NODEENV);

module.exports = { configureWebpack: config =&gt; { const plugins = []; if (IS_PROD) { plugins.push( new PrerenderSpaPlugin({ staticDir: resolve(&quot;dist&quot;), routes: [&quot;/&quot;], postProcess(ctx) { ctx.route = ctx.originalRoute; ctx.html = ctx.html.split(/&gt;[\s]+&lt;/gim).join(&quot;&gt;&lt;&quot;); if (ctx.route.endsWith(&quot;.html&quot;)) { ctx.outputPath = path.join(__dirname, &quot;dist&quot;, ctx.route); } return ctx; }, minify: { collapseBooleanAttributes: true, collapseWhitespace: true, decodeEntities: true, keepClosingSlash: true, sortAttributes: true }, renderer: new PrerenderSpaPlugin.PuppeteerRenderer({ // 需要注入一个值,这样就可以检测页面当前是否是预渲染的 inject: {}, headless: false, // 视图组件是在API请求获取所有必要数据后呈现的,因此我们在dom中存在“data view”属性后创建页面快照 renderAfterDocumentEvent: &quot;render-event&quot; }) }) ); } config.plugins = [...config.plugins, ...plugins]; } };</code></pre>

&emsp;&emsp;mounted()中添加 document.dispatchEvent(new Event('render-event'))

<pre><code class="lang-javascript">new Vue({ router, store, render: h =&gt; h(App), mounted() { document.dispatchEvent(new Event(&quot;render-event&quot;)); } }).$mount(&quot;#app&quot;);</code></pre>

为自定义预渲染页面添加自定义 title、description、content
  • 删除 public/index.html 中关于 description、content 的 meta 标签。保留 title 标签
  • 配置 router-config.js
<pre><code class="lang-javascript">module.exports = { &quot;/&quot;: { title: &quot;首页&quot;, keywords: &quot;首页关键词&quot;, description: &quot;这是首页描述&quot; }, &quot;/about.html&quot;: { title: &quot;关于我们&quot;, keywords: &quot;关于我们页面关键词&quot;, description: &quot;关于我们页面关键词描述&quot; } };</code></pre>
  • vue.config.js
<pre><code class="lang-js">const path = require(&quot;path&quot;); const PrerenderSpaPlugin = require(&quot;prerender-spa-plugin&quot;); const routesConfig = require(&quot;./router-config&quot;); const resolve = dir =&gt; path.join(__dirname, dir); const ISPROD = [&quot;production&quot;, &quot;prod&quot;].includes(process.env.NODEENV);

module.exports = { configureWebpack: config =&gt; { const plugins = [];

if (IS_PROD) { // 预加载 plugins.push( new PrerenderSpaPlugin({ staticDir: resolve(&quot;dist&quot;), routes: Object.keys(routesConfig), postProcess(ctx) { ctx.route = ctx.originalRoute; ctx.html = ctx.html.split(/&gt;[\s]+&lt;/gim).join(&quot;&gt;&lt;&quot;); ctx.html = ctx.html.replace( /&lt;title&gt;(.*?)&lt;\/title&gt;/gi, <title>${ routesConfig[ctx.route].title }</title><meta name="keywords" c /><meta name="description" c /> ); if (ctx.route.endsWith(&quot;.html&quot;)) { ctx.outputPath = path.join(__dirname, &quot;dist&quot;, ctx.route); } return ctx; }, minify: { collapseBooleanAttributes: true, collapseWhitespace: true, decodeEntities: true, keepClosingSlash: true, sortAttributes: true }, renderer: new PrerenderSpaPlugin.PuppeteerRenderer({ // 需要注入一个值,这样就可以检测页面当前是否是预渲染的 inject: {}, headless: false, // 视图组件是在API请求获取所有必要数据后呈现的,因此我们在dom中存在“data view”属性后创建页面快照 renderAfterDocumentEvent: &quot;render-event&quot; }) }) ); }

config.plugins = [...config.plugins, ...plugins]; } };</code></pre>

▲ 回顶部

<span id="ie">✅ 添加 IE 兼容</span>

&emsp;&emsp;在 main.js 中添加

<pre><code class="lang-javascript">import &#39;core-js/stable&#39;; import &#39;regenerator-runtime/runtime&#39;;</code></pre>

配置 babel.config.js

<pre><code class="lang-javascript">const plugins = [];

module.exports = { presets: [[&quot;@vue/app&quot;, { useBuiltIns: &quot;entry&quot; }]], plugins: plugins };</code></pre>

▲ 回顶部

<span id="alioss">✅ 静态资源自动打包上传阿里 oss、华为 obs</span>

&emsp;&emsp;开启文件上传 ali oss,需要将 publicPath 改成 ali oss 资源 url 前缀,也就是修改 VUEAPPPUBLICPATH。具体配置参见阿里 oss 插件 webpack-oss华为 obs 插件 huawei-obs-plugin

<pre><code class="lang-bash">npm i -D webpack-oss</code></pre>

<pre><code class="lang-javascript">const AliOssPlugin = require(&quot;webpack-oss&quot;); const ISPROD = [&quot;production&quot;, &quot;prod&quot;].includes(process.env.NODEENV);

const format = AliOssPlugin.getFormat();

module.exports = { publicPath: ISPROD ? ${process.env.VUEAPPPUBLICPATH}/${format} : &quot;./&quot;, // 默认&#39;/&#39;,部署应用包时的基本 URL configureWebpack: config =&gt; { const plugins = [];

if (IS_PROD) { plugins.push( new AliOssPlugin({ accessKeyId: process.env.ACCESSKEYID, accessKeySecret: process.env.ACCESSKEYSECRET, region: process.env.REGION, bucket: process.env.BUCKET, prefix: process.env.PREFIX, exclude: /.*\.html$/, format }) ); } config.plugins = [...config.plugins, ...plugins]; } };</code></pre>

▲ 回顶部

<span id="allconfig">✅ 完整配置</span>

<pre><code class="lang-javascript">const SpritesmithPlugin = require(&quot;webpack-spritesmith&quot;); const BundleAnalyzerPlugin = require(&quot;webpack-bundle-analyzer&quot;) .BundleAnalyzerPlugin; const webpack = require(&quot;webpack&quot;);

const path = require(&quot;path&quot;); const fs = require(&quot;fs&quot;); const resolve = dir =&gt; path.join(__dirname, dir); const ISPROD = [&quot;production&quot;, &quot;prod&quot;].includes(process.env.NODEENV);

const glob = require(&#39;glob&#39;) const pagesInfo = require(&#39;./pages.config&#39;) const pages = {}

glob.sync(&#39;./src/pages/**/main.js&#39;).forEach(entry =&gt; { let chunk = entry.match(/\.\/src\/pages\/(.*)\/main\.js/)[1]; const curr = pagesInfo[chunk]; if (curr) { pages[chunk] = { entry, ...curr, chunk: [&quot;chunk-vendors&quot;, &quot;chunk-common&quot;, chunk] } } })

let has_sprite = true; let files = []; const icons = {};

try { fs.statSync(resolve(&quot;./src/assets/icons&quot;)); files = fs.readdirSync(resolve(&quot;./src/assets/icons&quot;)); files.forEach(item =&gt; { let filename = item.toLocaleLowerCase().replace(/_/g, &quot;-&quot;); icons[filename] = true; });

} catch (error) { fs.mkdirSync(resolve(&quot;./src/assets/icons&quot;)); }

if (!files.length) { has_sprite = false; } else { try { let iconsObj = fs.readFileSync(resolve(&quot;./icons.json&quot;), &quot;utf8&quot;); iconsObj = JSON.parse(iconsObj); has_sprite = files.some(item =&gt; { let filename = item.toLocaleLowerCase().replace(/_/g, &quot;-&quot;); return !iconsObj[filename]; }); if (has_sprite) { fs.writeFileSync(resolve(&quot;./icons.json&quot;), JSON.stringify(icons, null, 2)); } } catch (error) { fs.writeFileSync(resolve(&quot;./icons.json&quot;), JSON.stringify(icons, null, 2)); has_sprite = true; } }

// 雪碧图样式处理模板 const SpritesmithTemplate = function (data) { // pc let icons = {} let tpl = .ico { display: inline-block; background-image: url(${data.sprites[0].image}); background-size: ${data.spritesheet.width}px ${data.spritesheet.height}px; }

data.sprites.forEach(sprite =&gt; { const name = &#39;&#39; + sprite.name.toLocaleLowerCase().replace(/_/g, &#39;-&#39;) icons[${name}.png] = true tpl = ${tpl} .ico-${name}{ width: ${sprite.width}px; height: ${sprite.height}px; background-position: ${sprite.offsetx}px ${sprite.offsety}px; } }) return tpl }

module.exports = { publicPath: ISPROD ? process.env.VUEAPPPUBLICPATH : &quot;./&quot;, // 默认&#39;/&#39;,部署应用包时的基本 URL // outputDir: process.env.outputDir || &#39;dist&#39;, // &#39;dist&#39;, 生产环境构建文件的目录 // assetsDir: &quot;&quot;, // 相对于outputDir的静态资源(js、css、img、fonts)目录 configureWebpack: config =&gt; { const plugins = [];

if (has_sprite) { // 生成雪碧图 plugins.push( new SpritesmithPlugin({ src: { cwd: path.resolve(__dirname, &#39;./src/assets/icons/&#39;), // 图标根路径 glob: &#39;*/.png&#39; // 匹配任意 png 图标 }, target: { image: path.resolve(__dirname, &#39;./src/assets/images/sprites.png&#39;), // 生成雪碧图目标路径与名称 // 设置生成CSS背景及其定位的文件或方式 css: [ [ path.resolve(__dirname, &#39;./src/assets/scss/sprites.scss&#39;), { format: &#39;functionbasedtemplate&#39; } ] ] }, customTemplates: { functionbasedtemplate: SpritesmithTemplate }, apiOptions: { cssImageRef: &#39;../images/sprites.png&#39; // css文件中引用雪碧图的相对位置路径配置 }, spritesmithOptions: { padding: 2 } }) ) }

config.externals = { vue: &quot;Vue&quot;, &quot;element-ui&quot;: &quot;ELEMENT&quot;, &quot;vue-router&quot;: &quot;VueRouter&quot;, vuex: &quot;Vuex&quot;, axios: &quot;axios&quot; };

config.plugins = [...config.plugins, ...plugins]; }, chainWebpack: config =&gt; { // 修复HMR config.resolve.symlinks(true);

// config.plugins.delete(&#39;preload&#39;); // config.plugins.delete(&#39;prefetch&#39;);

config .plugin(&quot;ignore&quot;) .use( new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /zh-cn$/) );

// 添加别名 config.resolve.alias .set(&quot;vue$&quot;, &quot;vue/dist/vue.esm.js&quot;) .set(&quot;@&quot;, resolve(&quot;src&quot;)) .set(&quot;@apis&quot;, resolve(&quot;src/apis&quot;)) .set(&quot;@assets&quot;, resolve(&quot;src/assets&quot;)) .set(&quot;@scss&quot;, resolve(&quot;src/assets/scss&quot;)) .set(&quot;@components&quot;, resolve(&quot;src/components&quot;)) .set(&quot;@middlewares&quot;, resolve(&quot;src/middlewares&quot;)) .set(&quot;@mixins&quot;, resolve(&quot;src/mixins&quot;)) .set(&quot;@plugins&quot;, resolve(&quot;src/plugins&quot;)) .set(&quot;@router&quot;, resolve(&quot;src/router&quot;)) .set(&quot;@store&quot;, resolve(&quot;src/store&quot;)) .set(&quot;@utils&quot;, resolve(&quot;src/utils&quot;)) .set(&quot;@views&quot;, resolve(&quot;src/views&quot;)) .set(&quot;@layouts&quot;, resolve(&quot;src/layouts&quot;));

const cdn = { // 访问https://unpkg.com/element-ui/lib/theme-chalk/index.css获取最新版本 css: [&quot;//unpkg.com/element-ui@2.10.1/lib/theme-chalk/index.css&quot;], js: [ &quot;//unpkg.com/vue@2.6.10/dist/vue.min.js&quot;, // 访问https://unpkg.com/vue/dist/vue.min.js获取最新版本 &quot;//unpkg.com/vue-router@3.0.6/dist/vue-router.min.js&quot;, &quot;//unpkg.com/vuex@3.1.1/dist/vuex.min.js&quot;, &quot;//unpkg.com/axios@0.19.0/dist/axios.min.js&quot;, &quot;//unpkg.com/element-ui@2.10.1/lib/index.js&quot; ] };

// 如果使用多页面打包,使用vue inspect --plugins查看html是否在结果数组中 // config.plugin(&quot;html&quot;).tap(args =&gt; { // // html中添加cdn // args[0].cdn = cdn;

// // 修复 Lazy loading routes Error // args[0].chunksSortMode = &quot;none&quot;; // return args; // });

// 防止多页面打包卡顿 config =&gt; config.plugins.delete(&#39;named-chunks&#39;)

// 多页面cdn添加 Object.keys(pagesInfo).forEach(page =&gt; { config.plugin(html-${page}).tap(args =&gt; { // html中添加cdn args[0].cdn = cdn;

// 修复 Lazy loading routes Error args[0].chunksSortMode = &quot;none&quot;; return args; }); })

if (IS_PROD) { // 压缩图片 config.module .rule(&quot;images&quot;) .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/) .use(&quot;image-webpack-loader&quot;) .loader(&quot;image-webpack-loader&quot;) .options({ mozjpeg: { progressive: true, quality: 65 }, optipng: { enabled: false }, pngquant: { quality: [0.65, 0.90], speed: 4 }, gifsicle: { interlaced: false } });

// 打包分析 config.plugin(&quot;webpack-report&quot;).use(BundleAnalyzerPlugin, [ { analyzerMode: &quot;static&quot; } ]); }

// 使用svg组件 const svgRule = config.module.rule(&quot;svg&quot;); svgRule.uses.clear(); svgRule.exclude.add(/node_modules/); svgRule .test(/\.svg$/) .use(&quot;svg-sprite-loader&quot;) .loader(&quot;svg-sprite-loader&quot;) .options({ symbolId: &quot;icon-[name]&quot; });

const imagesRule = config.module.rule(&quot;images&quot;); imagesRule.exclude.add(resolve(&quot;src/icons&quot;)); config.module.rule(&quot;images&quot;).test(/\.(png|jpe?g|gif|svg)(\?.*)?$/);

return config; }, pages, css: { extract: IS_PROD, sourceMap: false, loaderOptions: { scss: { // 向全局sass样式传入共享的全局变量, $src可以配置图片cdn前缀 // 详情: https://cli.vuejs.org/guide/css.html#passing-options-to-pre-processor-loaders prependData: @import "@scss/variables.scss"; @import "@scss/mixins.scss"; @import "@scss/function.scss"; $src: "${process.env.VUEAPPBASE_API}"; ` } } }, lintOnSave: false, runtimeCompiler: true, // 是否使用包含运行时编译器的 Vue 构建版本 productionSourceMap: !IS_PROD, // 生产环境的 source map parallel: require("os").cpus().length > 1, pwa: {}, devServer: { // overlay: { // 让浏览器 overlay 同时显示警告和错误 // warnings: true, // errors: true // }, // open: false, // 是否打开浏览器 // host: "localhost", // port: "8080", // 代理断就 // https: false, // hotOnly: false, // 热更新 proxy: { "/api": { target: "https://www.easy-mock.com/mock/5bc75b55dc36971c160cad1b/sheets", // 目标代理接口地址 secure: false, changeOrigin: true, // 开启代理,在本地创建一个虚拟服务端 // ws: true, // 是否启用websockets pathRewrite: { "^/api": "/" } } } } };

© 2026 GitRepoTrend · staven630/vue-cli4-config · Updated daily from GitHub