{"id":24871178,"url":"https://github.com/staven630/vue-cli4-config","last_synced_at":"2025-05-15T07:03:51.914Z","repository":{"id":33865722,"uuid":"157362745","full_name":"staven630/vue-cli4-config","owner":"staven630","description":"vue-cli4配置vue.config.js持续更新","archived":false,"fork":false,"pushed_at":"2023-01-04T21:40:32.000Z","size":6124,"stargazers_count":2691,"open_issues_count":44,"forks_count":615,"subscribers_count":91,"default_branch":"master","last_synced_at":"2025-05-15T07:03:49.508Z","etag":null,"topics":["gzip","prerender-spa-plugin","splitchunks","vue","vue-cli","vue-cli3"],"latest_commit_sha":null,"homepage":"https://staven630.github.io/vue-cli4-config/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/staven630.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-11-13T10:32:42.000Z","updated_at":"2025-05-15T02:56:17.000Z","dependencies_parsed_at":"2023-01-15T03:15:45.524Z","dependency_job_id":null,"html_url":"https://github.com/staven630/vue-cli4-config","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/staven630%2Fvue-cli4-config","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/staven630%2Fvue-cli4-config/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/staven630%2Fvue-cli4-config/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/staven630%2Fvue-cli4-config/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/staven630","download_url":"https://codeload.github.com/staven630/vue-cli4-config/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254291974,"owners_count":22046425,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["gzip","prerender-spa-plugin","splitchunks","vue","vue-cli","vue-cli3"],"created_at":"2025-02-01T04:31:09.592Z","updated_at":"2025-05-15T07:03:51.878Z","avatar_url":"https://github.com/staven630.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"禁止私自转载。运营公众号、社交账号，请坚持原创。勿做知识剽窃者！\n\n# vue-cli4 全面配置(持续更新)\n\u0026emsp;\u0026emsp;细致全面的 vue-cli4 配置信息。涵盖了使用 vue-cli 开发过程中大部分配置需求。\n\n\u0026emsp;\u0026emsp;不建议直接拉取此项目作为模板，希望能按照此教程按需配置，或者复制 vue.config.js 增删配置,并自行安装所需依赖。\n\n\u0026emsp;\u0026emsp;vue-cli3 配置见 [vue-cli3 分支](https://github.com/staven630/vue-cli4-config/tree/vue-cli3)。\n\n\u0026emsp;\u0026emsp;\u003cspan style=\"color: red;\"\u003eVite 配置见 [vite-config](https://github.com/staven630/vite-config)\u003c/span\u003e。\n\n### 其他系列\n★ [Blog](https://github.com/staven630/blog)\n\n★ [Nuxt.js 全面配置](https://github.com/staven630/nuxt-config)\n\n\u003cspan id=\"top\"\u003e目录\u003c/span\u003e\n\n- [√ 配置多环境变量](#env)\n- [√ 配置基础 vue.config.js](#base)\n- [√ 配置 proxy 跨域](#proxy)\n- [√ 修复 HMR(热更新)失效](#hmr)\n- [√ 修复 Lazy loading routes Error： Cyclic dependency](#lazyloading)\n- [√ 添加别名 alias](#alias)\n- [√ 压缩图片](#compressimage)\n- [√ 自动生成雪碧图](#spritesmith)\n- [√ SVG 转 font 字体](#font)\n- [√ 使用 SVG 组件](#svg)\n- [√ 去除多余无效的 css](#removecss)\n- [√ 添加打包分析](#analyze)\n- [√ 配置 externals 引入 cdn 资源](#externals)\n- [√ 多页面打包 multi-page](#multiple-pages)\n- [√ 删除 moment 语言包](#moment)\n- [√ 去掉 console.log](#log)\n- [√ 利用 splitChunks 单独打包第三方模块](#splitchunks)\n- [√ 开启 gzip 压缩](#gzip)\n- [√ 开启 stylelint 检测scss, css语法](#stylelint)\n- [√ 为 sass 提供全局样式，以及全局变量](#globalscss)\n- [√ 为 less 提供全局样式，以及全局变量](#globalless)\n- [√ 为 stylus 提供全局变量](#globalstylus)\n- [√ 预渲染 prerender-spa-plugin](#prerender)\n- [√ 添加 IE 兼容](#ie)\n- [√ 静态资源自动打包上传阿里 oss、华为 obs](#alioss)\n- [√ 完整依赖](#allconfig)\n\n### \u003cspan id=\"env\"\u003e✅ 配置多环境变量\u003c/span\u003e\n\n\u0026emsp;\u0026emsp;通过在 package.json 里的 scripts 配置项中添加--mode xxx 来选择不同环境\n\n\u0026emsp;\u0026emsp;只有以 VUE_APP 开头的变量会被 webpack.DefinePlugin 静态嵌入到客户端侧的包中，代码中可以通过 process.env.VUE_APP_BASE_API 访问\n\n\u0026emsp;\u0026emsp;NODE_ENV 和 BASE_URL 是两个特殊变量，在代码中始终可用\n\n##### 配置\n\n\u0026emsp;\u0026emsp;在项目根目录中新建.env, .env.production, .env.analyz 等文件\n\n- .env\n\n\u0026emsp;\u0026emsp;serve 默认的本地开发环境配置\n\n```javascript\nNODE_ENV = \"development\"\nBASE_URL = \"./\"\nVUE_APP_PUBLIC_PATH = \"./\"\nVUE_APP_API = \"https://test.staven630.com/api\"\n```\n\n- .env.production\n\n\u0026emsp;\u0026emsp;build 默认的环境配置（正式服务器）\n\n```javascript\nNODE_ENV = \"production\"\nBASE_URL = \"https://prod.staven630.com/\"\nVUE_APP_PUBLIC_PATH = \"https://prod.oss.com/staven-blog\"\nVUE_APP_API = \"https://prod.staven630.com/api\"\n\nACCESS_KEY_ID = \"xxxxxxxxxxxxx\"\nACCESS_KEY_SECRET = \"xxxxxxxxxxxxx\"\nREGION = \"oss-cn-hangzhou\"\nBUCKET = \"staven-prod\"\nPREFIX = \"staven-blog\"\n```\n\n- .env.crm\n\n\u0026emsp;\u0026emsp;自定义 build 环境配置（预发服务器）\n\n```javascript\nNODE_ENV = \"production\"\nBASE_URL = \"https://crm.staven630.com/\"\nVUE_APP_PUBLIC_PATH = \"https://crm.oss.com/staven-blog\"\nVUE_APP_API = \"https://crm.staven630.com/api\"\n\nACCESS_KEY_ID = \"xxxxxxxxxxxxx\"\nACCESS_KEY_SECRET = \"xxxxxxxxxxxxx\"\nREGION = \"oss-cn-hangzhou\"\nBUCKET = \"staven-crm\"\nPREFIX = \"staven-blog\"\n\nIS_ANALYZE = true;\n```\n\n\u0026emsp;\u0026emsp;修改 package.json\n\n```javascript\n\"scripts\": {\n  \"build\": \"vue-cli-service build\",\n  \"crm\": \"vue-cli-service build --mode crm\"\n}\n```\n\n##### 使用环境变量\n\n```javascript\n\u003ctemplate\u003e\n  \u003cdiv class=\"home\"\u003e\n    \u003c!-- template中使用环境变量 --\u003e\n     API: {{ api }}\n  \u003c/div\u003e\n\u003c/template\u003e\n\n\u003cscript\u003e\nexport default {\n  name: \"home\",\n  data() {\n    return {\n      api: process.env.VUE_APP_API\n    };\n  },\n  mounted() {\n    // js代码中使用环境变量\n    console.log(\"BASE_URL: \", process.env.BASE_URL);\n    console.log(\"VUE_APP_API: \", process.env.VUE_APP_API);\n  }\n};\n\u003c/script\u003e\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"base\"\u003e✅ 配置基础 vue.config.js\u003c/span\u003e\n\n```javascript\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\n\nmodule.exports = {\n  publicPath: IS_PROD ? process.env.VUE_APP_PUBLIC_PATH : \"./\", // 默认'/'，部署应用包时的基本 URL\n  // outputDir: process.env.outputDir || 'dist', // 'dist', 生产环境构建文件的目录\n  // assetsDir: \"\", // 相对于outputDir的静态资源(js、css、img、fonts)目录\n  lintOnSave: false,\n  runtimeCompiler: true, // 是否使用包含运行时编译器的 Vue 构建版本\n  productionSourceMap: !IS_PROD, // 生产环境的 source map\n  parallel: require(\"os\").cpus().length \u003e 1,\n  pwa: {}\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"proxy\"\u003e✅ 配置 proxy 代理解决跨域问题\u003c/span\u003e\n\n\u0026emsp;\u0026emsp;假设 mock 接口为https://www.easy-mock.com/mock/5bc75b55dc36971c160cad1b/sheets/1\n\n```javascript\nmodule.exports = {\n  devServer: {\n    // overlay: { // 让浏览器 overlay 同时显示警告和错误\n    //   warnings: true,\n    //   errors: true\n    // },\n    // open: false, // 是否打开浏览器\n    // host: \"localhost\",\n    // port: \"8080\", // 代理断就\n    // https: false,\n    // hotOnly: false, // 热更新\n    proxy: {\n      \"/api\": {\n        target:\n          \"https://www.easy-mock.com/mock/5bc75b55dc36971c160cad1b/sheets\", // 目标代理接口地址\n        secure: false,\n        changeOrigin: true, // 开启代理，在本地创建一个虚拟服务端\n        // ws: true, // 是否启用websockets\n        pathRewrite: {\n          \"^/api\": \"/\"\n        }\n      }\n    }\n  }\n};\n```\n\n\u0026emsp;\u0026emsp;访问\n\n```javascript\n\u003cscript\u003e\nimport axios from \"axios\";\nexport default {\n  mounted() {\n    axios.get(\"/api/1\").then(res =\u003e {\n      console.log('proxy:', res);\n    });\n  }\n};\n\u003c/script\u003e\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"hmr\"\u003e✅ 修复 HMR(热更新)失效\u003c/span\u003e\n\n\u0026emsp;\u0026emsp;如果热更新失效，如下操作：\n\n```javascript\nmodule.exports = {\n  chainWebpack: config =\u003e {\n    // 修复HMR\n    config.resolve.symlinks(true);\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"lazyloading\"\u003e✅ 修复 Lazy loading routes Error： Cyclic dependency\u003c/span\u003e [https://github.com/vuejs/vue-cli/issues/1669](https://github.com/vuejs/vue-cli/issues/1669)\n\n```javascript\nmodule.exports = {\n  chainWebpack: config =\u003e {\n    // 如果使用多页面打包，使用vue inspect --plugins查看html是否在结果数组中\n    config.plugin(\"html\").tap(args =\u003e {\n      // 修复 Lazy loading routes Error\n      args[0].chunksSortMode = \"none\";\n      return args;\n    });\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"alias\"\u003e✅ 添加别名 alias\u003c/span\u003e\n\n```javascript\nconst path = require(\"path\");\nconst resolve = dir =\u003e path.join(__dirname, dir);\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\n\nmodule.exports = {\n  chainWebpack: config =\u003e {\n    // 添加别名\n    config.resolve.alias\n      .set(\"vue$\", \"vue/dist/vue.esm.js\")\n      .set(\"@\", resolve(\"src\"))\n      .set(\"@assets\", resolve(\"src/assets\"))\n      .set(\"@scss\", resolve(\"src/assets/scss\"))\n      .set(\"@components\", resolve(\"src/components\"))\n      .set(\"@plugins\", resolve(\"src/plugins\"))\n      .set(\"@views\", resolve(\"src/views\"))\n      .set(\"@router\", resolve(\"src/router\"))\n      .set(\"@store\", resolve(\"src/store\"))\n      .set(\"@layouts\", resolve(\"src/layouts\"))\n      .set(\"@static\", resolve(\"src/static\"));\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"compressimage\"\u003e✅ 压缩图片\u003c/span\u003e\n\n```javascript\nnpm i -D image-webpack-loader\n```\n\n\u0026emsp;\u0026emsp;在某些版本的 OSX 上安装可能会因缺少 libpng 依赖项而引发错误。可以通过安装最新版本的 libpng 来解决。\n\n```javascript\nbrew install libpng\n```\n\n```javascript\nmodule.exports = {\n  chainWebpack: config =\u003e {\n    if (IS_PROD) {\n      config.module\n        .rule(\"images\")\n        .use(\"image-webpack-loader\")\n        .loader(\"image-webpack-loader\")\n        .options({\n          mozjpeg: { progressive: true, quality: 65 },\n          optipng: { enabled: false },\n          pngquant: { quality: [0.65, 0.9], speed: 4 },\n          gifsicle: { interlaced: false }\n          // webp: { quality: 75 }\n        });\n    }\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"spritesmith\"\u003e✅ 自动生成雪碧图\u003c/span\u003e\n\n\u0026emsp;\u0026emsp;默认 src/assets/icons 中存放需要生成雪碧图的 png 文件。首次运行 npm run serve/build 会生成雪碧图，并在跟目录生成 icons.json 文件。再次运行命令时，会对比 icons 目录内文件与 icons.json 的匹配关系，确定是否需要再次执行 webpack-spritesmith 插件。\n\n```bash\nnpm i -D webpack-spritesmith\n```\n\n```javascript\nlet has_sprite = true;\nlet files = [];\nconst icons = {};\n\ntry {\n  fs.statSync(resolve(\"./src/assets/icons\"));\n  files = fs.readdirSync(resolve(\"./src/assets/icons\"));\n  files.forEach(item =\u003e {\n    let filename = item.toLocaleLowerCase().replace(/_/g, \"-\");\n    icons[filename] = true;\n  });\n\n} catch (error) {\n  fs.mkdirSync(resolve(\"./src/assets/icons\"));\n}\n\nif (!files.length) {\n  has_sprite = false;\n} else {\n  try {\n    let iconsObj = fs.readFileSync(resolve(\"./icons.json\"), \"utf8\");\n    iconsObj = JSON.parse(iconsObj);\n    has_sprite = files.some(item =\u003e {\n      let filename = item.toLocaleLowerCase().replace(/_/g, \"-\");\n      return !iconsObj[filename];\n    });\n    if (has_sprite) {\n      fs.writeFileSync(resolve(\"./icons.json\"), JSON.stringify(icons, null, 2));\n    }\n  } catch (error) {\n    fs.writeFileSync(resolve(\"./icons.json\"), JSON.stringify(icons, null, 2));\n    has_sprite = true;\n  }\n}\n\n// 雪碧图样式处理模板\nconst SpritesmithTemplate = function(data) {\n  // pc\n  let icons = {};\n  let tpl = `.ico { \n  display: inline-block; \n  background-image: url(${data.sprites[0].image}); \n  background-size: ${data.spritesheet.width}px ${data.spritesheet.height}px; \n}`;\n\n  data.sprites.forEach(sprite =\u003e {\n    const name = \"\" + sprite.name.toLocaleLowerCase().replace(/_/g, \"-\");\n    icons[`${name}.png`] = true;\n    tpl = `${tpl} \n.ico-${name}{\n  width: ${sprite.width}px; \n  height: ${sprite.height}px; \n  background-position: ${sprite.offset_x}px ${sprite.offset_y}px;\n}\n`;\n  });\n  return tpl;\n};\n\nmodule.exports = {\n  configureWebpack: config =\u003e {\n    const plugins = [];\n    if (has_sprite) {\n      plugins.push(\n        new SpritesmithPlugin({\n          src: {\n            cwd: path.resolve(__dirname, \"./src/assets/icons/\"), // 图标根路径\n            glob: \"**/*.png\" // 匹配任意 png 图标\n          },\n          target: {\n            image: path.resolve(__dirname, \"./src/assets/images/sprites.png\"), // 生成雪碧图目标路径与名称\n            // 设置生成CSS背景及其定位的文件或方式\n            css: [\n              [\n                path.resolve(__dirname, \"./src/assets/scss/sprites.scss\"),\n                {\n                  format: \"function_based_template\"\n                }\n              ]\n            ]\n          },\n          customTemplates: {\n            function_based_template: SpritesmithTemplate\n          },\n          apiOptions: {\n            cssImageRef: \"../images/sprites.png\" // css文件中引用雪碧图的相对位置路径配置\n          },\n          spritesmithOptions: {\n            padding: 2\n          }\n        })\n      );\n    }\n\n    config.plugins = [...config.plugins, ...plugins];\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"font\"\u003e✅ SVG 转 font 字体\u003c/span\u003e\n\n```bash\nnpm i -D svgtofont\n```\n\n\u0026emsp;\u0026emsp;根目录新增 scripts 目录，并新建 svg2font.js 文件：\n\n```javascript\nconst svgtofont = require(\"svgtofont\");\nconst path = require(\"path\");\nconst pkg = require(\"../package.json\");\n\nsvgtofont({\n  src: path.resolve(process.cwd(), \"src/assets/svg\"), // svg 图标目录路径\n  dist: path.resolve(process.cwd(), \"src/assets/fonts\"), // 输出到指定目录中\n  fontName: \"icon\", // 设置字体名称\n  css: true, // 生成字体文件\n  startNumber: 20000, // unicode起始编号\n  svgicons2svgfont: {\n    fontHeight: 1000,\n    normalize: true\n  },\n  // website = null, 没有演示html文件\n  website: {\n    title: \"icon\",\n    logo: \"\",\n    version: pkg.version,\n    meta: {\n      description: \"\",\n      keywords: \"\"\n    },\n    description: ``,\n    links: [\n      {\n        title: \"Font Class\",\n        url: \"index.html\"\n      },\n      {\n        title: \"Unicode\",\n        url: \"unicode.html\"\n      }\n    ],\n    footerInfo: ``\n  }\n}).then(() =\u003e {\n  console.log(\"done!\");\n});\n```\n\n\u0026emsp;\u0026emsp;添加 package.json scripts 配置：\n\n```javascript\n\"prebuild\": \"npm run font\",\n\"font\": \"node scripts/svg2font.js\",\n```\n\n\u0026emsp;\u0026emsp;执行：\n\n```javascript\nnpm run font\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"svg\"\u003e✅ 使用 SVG 组件\u003c/span\u003e\n\n```bash\nnpm i -D svg-sprite-loader\n```\n\n\u0026emsp;\u0026emsp;新增 SvgIcon 组件。\n\n```javascript\n\u003ctemplate\u003e\n  \u003csvg class=\"svg-icon\"\n       aria-hidden=\"true\"\u003e\n    \u003cuse :xlink:href=\"iconName\" /\u003e\n  \u003c/svg\u003e\n\u003c/template\u003e\n\n\u003cscript\u003e\nexport default {\n  name: 'SvgIcon',\n  props: {\n    iconClass: {\n      type: String,\n      required: true\n    }\n  },\n  computed: {\n    iconName() {\n      return `#icon-${this.iconClass}`\n    }\n  }\n}\n\u003c/script\u003e\n\n\u003cstyle scoped\u003e\n.svg-icon {\n  width: 1em;\n  height: 1em;\n  vertical-align: -0.15em;\n  fill: currentColor;\n  overflow: hidden;\n}\n\u003c/style\u003e\n```\n\n\u0026emsp;\u0026emsp;在 src 文件夹中创建 icons 文件夹。icons 文件夹中新增 svg 文件夹（用来存放 svg 文件）与 index.js 文件：\n\n```js\nimport SvgIcon from \"@/components/SvgIcon\";\nimport Vue from \"vue\";\n\n// 注册到全局\nVue.component(\"svg-icon\", SvgIcon);\n\nconst requireAll = requireContext =\u003e requireContext.keys().map(requireContext);\nconst req = require.context(\"./svg\", false, /\\.svg$/);\nrequireAll(req);\n```\n\n\u0026emsp;\u0026emsp;在 main.js 中导入 icons/index.js\n\n```javascript\nimport \"@/icons\";\n```\n\n\u0026emsp;\u0026emsp;修改 vue.config.js\n\n```javascript\nconst path = require(\"path\");\nconst resolve = dir =\u003e path.join(__dirname, dir);\n\nmodule.exports = {\n  chainWebpack: config =\u003e {\n    const svgRule = config.module.rule(\"svg\");\n    svgRule.uses.clear();\n    svgRule.exclude.add(/node_modules/);\n    svgRule\n      .test(/\\.svg$/)\n      .use(\"svg-sprite-loader\")\n      .loader(\"svg-sprite-loader\")\n      .options({\n        symbolId: \"icon-[name]\"\n      });\n\n    const imagesRule = config.module.rule(\"images\");\n    imagesRule.exclude.add(resolve(\"src/icons\"));\n    config.module.rule(\"images\").test(/\\.(png|jpe?g|gif|svg)(\\?.*)?$/);\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"removecss\"\u003e✅ 去除多余无效的 css\u003c/span\u003e\n\n\u0026emsp;\u0026emsp;注：谨慎使用。可能出现各种样式丢失现象。\n\n- 方案一：@fullhuman/postcss-purgecss\n\n```bash\nnpm i -D postcss-import @fullhuman/postcss-purgecss\n```\n\n\u0026emsp;\u0026emsp;更新 postcss.config.js\n\n```javascript\nconst autoprefixer = require(\"autoprefixer\");\nconst postcssImport = require(\"postcss-import\");\nconst purgecss = require(\"@fullhuman/postcss-purgecss\");\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\nlet plugins = [];\nif (IS_PROD) {\n  plugins.push(postcssImport);\n  plugins.push(\n    purgecss({\n      content: [\n        \"./layouts/**/*.vue\",\n        \"./components/**/*.vue\",\n        \"./pages/**/*.vue\"\n      ],\n      extractors: [\n        {\n          extractor: class Extractor {\n            static extract(content) {\n              const validSection = content.replace(\n                /\u003cstyle([\\s\\S]*?)\u003c\\/style\u003e+/gim,\n                \"\"\n              );\n              return (\n                validSection.match(/[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g) || []\n              );\n            }\n          },\n          extensions: [\"html\", \"vue\"]\n        }\n      ],\n      whitelist: [\"html\", \"body\"],\n      whitelistPatterns: [\n        /el-.*/,\n        /-(leave|enter|appear)(|-(to|from|active))$/,\n        /^(?!cursor-move).+-move$/,\n        /^router-link(|-exact)-active$/\n      ],\n      whitelistPatternsChildren: [/^token/, /^pre/, /^code/]\n    })\n  );\n}\nmodule.exports = {\n  plugins: [...plugins, autoprefixer]\n};\n```\n\n- 方案二：purgecss-webpack-plugin\n\n```bash\nnpm i -D glob-all purgecss-webpack-plugin\n```\n\n```javascript\nconst path = require(\"path\");\nconst glob = require(\"glob-all\");\nconst PurgecssPlugin = require(\"purgecss-webpack-plugin\");\nconst resolve = dir =\u003e path.join(__dirname, dir);\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\n\nmodule.exports = {\n  configureWebpack: config =\u003e {\n    const plugins = [];\n    if (IS_PROD) {\n      plugins.push(\n        new PurgecssPlugin({\n          paths: glob.sync([resolve(\"./**/*.vue\")]),\n          extractors: [\n            {\n              extractor: class Extractor {\n                static extract(content) {\n                  const validSection = content.replace(\n                    /\u003cstyle([\\s\\S]*?)\u003c\\/style\u003e+/gim,\n                    \"\"\n                  );\n                  return (\n                    validSection.match(/[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g) || []\n                  );\n                }\n              },\n              extensions: [\"html\", \"vue\"]\n            }\n          ],\n          whitelist: [\"html\", \"body\"],\n          whitelistPatterns: [\n            /el-.*/,\n            /-(leave|enter|appear)(|-(to|from|active))$/,\n            /^(?!cursor-move).+-move$/,\n            /^router-link(|-exact)-active$/\n          ],\n          whitelistPatternsChildren: [/^token/, /^pre/, /^code/]\n        })\n      );\n    }\n    config.plugins = [...config.plugins, ...plugins];\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"analyze\"\u003e✅ 添加打包分析\u003c/span\u003e\n\n```javascript\nconst BundleAnalyzerPlugin = require(\"webpack-bundle-analyzer\")\n  .BundleAnalyzerPlugin;\n\nmodule.exports = {\n  chainWebpack: config =\u003e {\n    // 打包分析\n    if (IS_PROD) {\n      config.plugin(\"webpack-report\").use(BundleAnalyzerPlugin, [\n        {\n          analyzerMode: \"static\"\n        }\n      ]);\n    }\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"externals\"\u003e✅ 配置 externals 引入 cdn 资源\u003c/span\u003e\n\n\u0026emsp;\u0026emsp;防止将某些 import 的包(package)打包到 bundle 中，而是在运行时(runtime)再去从外部获取这些扩展依赖\n\n```javascript\nmodule.exports = {\n  configureWebpack: config =\u003e {\n    config.externals = {\n      vue: \"Vue\",\n      \"element-ui\": \"ELEMENT\",\n      \"vue-router\": \"VueRouter\",\n      vuex: \"Vuex\",\n      axios: \"axios\"\n    };\n  },\n  chainWebpack: config =\u003e {\n    const cdn = {\n      // 访问https://unpkg.com/element-ui/lib/theme-chalk/index.css获取最新版本\n      css: [\"//unpkg.com/element-ui@2.10.1/lib/theme-chalk/index.css\"],\n      js: [\n        \"//unpkg.com/vue@2.6.10/dist/vue.min.js\", // 访问https://unpkg.com/vue/dist/vue.min.js获取最新版本\n        \"//unpkg.com/vue-router@3.0.6/dist/vue-router.min.js\",\n        \"//unpkg.com/vuex@3.1.1/dist/vuex.min.js\",\n        \"//unpkg.com/axios@0.19.0/dist/axios.min.js\",\n        \"//unpkg.com/element-ui@2.10.1/lib/index.js\"\n      ]\n    };\n\n    // 如果使用多页面打包，使用vue inspect --plugins查看html是否在结果数组中\n    config.plugin(\"html\").tap(args =\u003e {\n      // html中添加cdn\n      args[0].cdn = cdn;\n      return args;\n    });\n  }\n};\n```\n\n\u0026emsp;\u0026emsp;在 html 中添加\n\n```\n\u003c!-- 使用CDN的CSS文件 --\u003e\n\u003c% for (var i in htmlWebpackPlugin.options.cdn \u0026\u0026\nhtmlWebpackPlugin.options.cdn.css) { %\u003e\n\u003clink rel=\"stylesheet\" href=\"\u003c%= htmlWebpackPlugin.options.cdn.css[i] %\u003e\" /\u003e\n\u003c% } %\u003e\n\n\u003c!-- 使用CDN的JS文件 --\u003e\n\u003c% for (var i in htmlWebpackPlugin.options.cdn \u0026\u0026\nhtmlWebpackPlugin.options.cdn.js) { %\u003e\n\u003cscript\n  type=\"text/javascript\"\n  src=\"\u003c%= htmlWebpackPlugin.options.cdn.js[i] %\u003e\"\n\u003e\u003c/script\u003e\n\u003c% } %\u003e\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"multiple-pages\"\u003e✅ 多页面打包 multi-page\u003c/span\u003e\n\n\u0026emsp;\u0026emsp;多入口页面打包，建议在 src 目录下新建 pages 目录存放多页面模块。\n\n- pages.config.js\n\n\u0026emsp;\u0026emsp; 配置多页面信息。src/main.js 文件对应 main 字段，其他根据参照 pages 为根路径为字段。如下:\n\n```javascript\nmodule.exports = {\n  'admin': {\n    template: 'public/index.html',\n    filename: 'admin.html',\n    title: '后台管理',\n  },\n  'mobile': {\n    template: 'public/index.html',\n    filename: 'mobile.html',\n    title: '移动端',\n  },\n  'pc/crm': {\n    template: 'public/index.html',\n    filename: 'pc-crm.html',\n    title: '预发服务',\n  }\n}\n```\n\n- vue.config.js\n\n\u0026emsp;\u0026emsp;vue.config.js 的 pages 字段为多页面提供配置\n\n```javascript\nconst glob = require(\"glob\");\nconst pagesInfo = require(\"./pages.config\");\nconst pages = {};\n\nglob.sync('./src/pages/**/main.js').forEach(entry =\u003e {\n  let chunk = entry.match(/\\.\\/src\\/pages\\/(.*)\\/main\\.js/)[1];\n  const curr = pagesInfo[chunk];\n  if (curr) {\n    pages[chunk] = {\n      entry,\n      ...curr,\n      chunk: [\"chunk-vendors\", \"chunk-common\", chunk]\n    }\n  }\n})\n\nmodule.exports = {\n  chainWebpack: config =\u003e {\n    // 防止多页面打包卡顿\n    config =\u003e config.plugins.delete(\"named-chunks\");\n    return config;\n  },\n  pages\n};\n```\n\n\u0026emsp;\u0026emsp;如果多页面打包需要使用 CDN，使用 vue inspect --plugins 查看 html 是否在结果数组中的形式。上例中 plugins 列表中存在'html-main','html-pages/admin','html-pages/mobile'， 没有'html'。因此不能再使用 config.plugin(\"html\")。\n\n```javascript\nconst path = require(\"path\");\nconst resolve = dir =\u003e path.join(__dirname, dir);\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\n\nconst glob = require(\"glob\");\nconst pagesInfo = require(\"./pages.config\");\nconst pages = {};\n\nglob.sync('./src/pages/**/main.js').forEach(entry =\u003e {\n  let chunk = entry.match(/\\.\\/src\\/pages\\/(.*)\\/main\\.js/)[1];\n  const curr = pagesInfo[chunk];\n  if (curr) {\n    pages[chunk] = {\n      entry,\n      ...curr,\n      chunk: [\"chunk-vendors\", \"chunk-common\", chunk]\n    }\n  }\n});\n\nmodule.exports = {\n  publicPath: IS_PROD ? process.env.VUE_APP_PUBLIC_PATH : \"./\", //\n  configureWebpack: config =\u003e {\n    config.externals = {\n      vue: \"Vue\",\n      \"element-ui\": \"ELEMENT\",\n      \"vue-router\": \"VueRouter\",\n      vuex: \"Vuex\",\n      axios: \"axios\"\n    };\n  },\n  chainWebpack: config =\u003e {\n    const cdn = {\n      // 访问https://unpkg.com/element-ui/lib/theme-chalk/index.css获取最新版本\n      css: [\"//unpkg.com/element-ui@2.10.1/lib/theme-chalk/index.css\"],\n      js: [\n        \"//unpkg.com/vue@2.6.10/dist/vue.min.js\", // 访问https://unpkg.com/vue/dist/vue.min.js获取最新版本\n        \"//unpkg.com/vue-router@3.0.6/dist/vue-router.min.js\",\n        \"//unpkg.com/vuex@3.1.1/dist/vuex.min.js\",\n        \"//unpkg.com/axios@0.19.0/dist/axios.min.js\",\n        \"//unpkg.com/element-ui@2.10.1/lib/index.js\"\n      ]\n    };\n\n    // 防止多页面打包卡顿\n    config =\u003e config.plugins.delete(\"named-chunks\");\n\n    // 多页面cdn添加\n    Object.keys(pagesInfo).forEach(page =\u003e {\n      config.plugin(`html-${page}`).tap(args =\u003e {\n        // html中添加cdn\n        args[0].cdn = cdn;\n\n        // 修复 Lazy loading routes Error\n        args[0].chunksSortMode = \"none\";\n        return args;\n      });\n    });\n    return config;\n  },\n  pages\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"moment\"\u003e✅ 删除 moment 语言包\u003c/span\u003e\n\n\u0026emsp;\u0026emsp;删除 moment 除 zh-cn 中文包外的其它语言包，无需在代码中手动引入 zh-cn 语言包。\n\n```javascript\nconst webpack = require(\"webpack\");\n\nmodule.exports = {\n  chainWebpack: config =\u003e {\n    config\n      .plugin(\"ignore\")\n      .use(\n        new webpack.ContextReplacementPlugin(/moment[/\\\\]locale$/, /zh-cn$/)\n      );\n\n    return config;\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"log\"\u003e✅ 去掉 console.log\u003c/span\u003e\n\n##### 方法一：使用 babel-plugin-transform-remove-console 插件\n\n```bash\nnpm i -D babel-plugin-transform-remove-console\n```\n\n在 babel.config.js 中配置\n\n```\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\n\nconst plugins = [];\nif (IS_PROD) {\n  plugins.push(\"transform-remove-console\");\n}\n\nmodule.exports = {\n  presets: [\"@vue/app\", { useBuiltIns: \"entry\" }],\n  plugins\n};\n```\n\n##### 方法二：\n\n```javascript\nconst UglifyJsPlugin = require(\"uglifyjs-webpack-plugin\");\nmodule.exports = {\n  configureWebpack: config =\u003e {\n    if (IS_PROD) {\n      const plugins = [];\n      plugins.push(\n        new UglifyJsPlugin({\n          uglifyOptions: {\n            compress: {\n              warnings: false,\n              drop_console: true,\n              drop_debugger: false,\n              pure_funcs: [\"console.log\"] //移除console\n            }\n          },\n          sourceMap: false,\n          parallel: true\n        })\n      );\n      config.plugins = [...config.plugins, ...plugins];\n    }\n  }\n};\n```\n\n\u0026emsp;\u0026emsp;如果使用 uglifyjs-webpack-plugin 会报错，可能存在 node_modules 中有些依赖需要 babel 转译。\n\n\u0026emsp;\u0026emsp;而 vue-cli 的[transpileDependencies](https://cli.vuejs.org/zh/config/#transpiledependencies)配置默认为[], babel-loader 会忽略所有 node_modules 中的文件。如果你想要通过 Babel 显式转译一个依赖，可以在这个选项中列出来。配置需要转译的第三方库。\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"splitchunks\"\u003e利用 splitChunks 单独打包第三方模块\u003c/span\u003e\n\n```javascript\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\n\nmodule.exports = {\n  configureWebpack: config =\u003e {\n    if (IS_PROD) {\n      config.optimization = {\n        splitChunks: {\n          cacheGroups: {\n            common: {\n              name: \"chunk-common\",\n              chunks: \"initial\",\n              minChunks: 2,\n              maxInitialRequests: 5,\n              minSize: 0,\n              priority: 1,\n              reuseExistingChunk: true,\n              enforce: true\n            },\n            vendors: {\n              name: \"chunk-vendors\",\n              test: /[\\\\/]node_modules[\\\\/]/,\n              chunks: \"initial\",\n              priority: 2,\n              reuseExistingChunk: true,\n              enforce: true\n            },\n            elementUI: {\n              name: \"chunk-elementui\",\n              test: /[\\\\/]node_modules[\\\\/]element-ui[\\\\/]/,\n              chunks: \"all\",\n              priority: 3,\n              reuseExistingChunk: true,\n              enforce: true\n            },\n            echarts: {\n              name: \"chunk-echarts\",\n              test: /[\\\\/]node_modules[\\\\/](vue-)?echarts[\\\\/]/,\n              chunks: \"all\",\n              priority: 4,\n              reuseExistingChunk: true,\n              enforce: true\n            }\n          }\n        }\n      };\n    }\n  },\n  chainWebpack: config =\u003e {\n    if (IS_PROD) {\n      config.optimization.delete(\"splitChunks\");\n    }\n    return config;\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"gzip\"\u003e✅ 开启 gzip 压缩\u003c/span\u003e\n\n```bash\nnpm i -D compression-webpack-plugin\n```\n\n```javascript\nconst CompressionWebpackPlugin = require(\"compression-webpack-plugin\");\n\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\nconst productionGzipExtensions = /\\.(js|css|json|txt|html|ico|svg)(\\?.*)?$/i;\n\nmodule.exports = {\n  configureWebpack: config =\u003e {\n    const plugins = [];\n    if (IS_PROD) {\n      plugins.push(\n        new CompressionWebpackPlugin({\n          filename: \"[path].gz[query]\",\n          algorithm: \"gzip\",\n          test: productionGzipExtensions,\n          threshold: 10240,\n          minRatio: 0.8\n        })\n      );\n    }\n    config.plugins = [...config.plugins, ...plugins];\n  }\n};\n```\n\n\u0026emsp;\u0026emsp;还可以开启比 gzip 体验更好的 Zopfli 压缩详见[https://webpack.js.org/plugins/compression-webpack-plugin](https://webpack.js.org/plugins/compression-webpack-plugin)\n\n```bash\nnpm i -D @gfx/zopfli brotli-webpack-plugin\n```\n\n```javascript\nconst CompressionWebpackPlugin = require(\"compression-webpack-plugin\");\nconst zopfli = require(\"@gfx/zopfli\");\nconst BrotliPlugin = require(\"brotli-webpack-plugin\");\n\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\nconst productionGzipExtensions = /\\.(js|css|json|txt|html|ico|svg)(\\?.*)?$/i;\n\nmodule.exports = {\n  configureWebpack: config =\u003e {\n    const plugins = [];\n    if (IS_PROD) {\n      plugins.push(\n        new CompressionWebpackPlugin({\n          algorithm(input, compressionOptions, callback) {\n            return zopfli.gzip(input, compressionOptions, callback);\n          },\n          compressionOptions: {\n            numiterations: 15\n          },\n          minRatio: 0.99,\n          test: productionGzipExtensions\n        })\n      );\n      plugins.push(\n        new BrotliPlugin({\n          test: productionGzipExtensions,\n          minRatio: 0.99\n        })\n      );\n    }\n    config.plugins = [...config.plugins, ...plugins];\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"stylelint\"\u003e✅ 开启 stylelint 检测scss, css语法\u003c/span\u003e\n```bash\nnpm i -D stylelint stylelint-config-standard stylelint-config-prettier stylelint-webpack-plugin\n```\n\n在文件夹创建stylelint.config.js,详细配置在[这里](https://stylelint.io/user-guide/configuration)\n```javascript\nmodule.exports = {\n  ignoreFiles: [\"**/*.js\", \"src/assets/css/element-variables.scss\", \"theme/\"], \n  extends: [\"stylelint-config-standard\", \"stylelint-config-prettier\"],\n  rules: {\n    \"no-empty-source\": null,\n    \"at-rule-no-unknown\": [\n      true,\n      {\n        ignoreAtRules: [\"extend\"]\n      }\n    ]\n  }\n};\n```\n启用webpack配置\n\n```javascript\nconst StylelintPlugin = require(\"stylelint-webpack-plugin\");\n\nmodule.exports = {\n  configureWebpack: config =\u003e {\n    const plugins = [];\n    if (IS_DEV) {\n      plugins.push(\n        new StylelintPlugin({\n          files: [\"src/**/*.vue\", \"src/assets/**/*.scss\"],\n          fix: true //打开自动修复（谨慎使用！注意上面的配置不要加入js或html文件，会发生问题，js文件请手动修复）\n        })\n      );\n    }\n    config.plugins = [...config.plugins, ...plugins];\n  }\n}\n```\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"globalscss\"\u003e✅ 为 sass 提供全局样式，以及全局变量\u003c/span\u003e\n\n\u0026emsp;\u0026emsp;可以通过在 main.js 中 Vue.prototype.$src = process.env.VUE_APP_PUBLIC_PATH;挂载环境变量中的配置信息，然后在js中使用$src 访问。\n\n\u0026emsp;\u0026emsp;css 中可以使用注入 sass 变量访问环境变量中的配置信息\n\n```javascript\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\n\nmodule.exports = {\n  css: {\n    extract: IS_PROD,\n    sourceMap: false,\n    loaderOptions: {\n      scss: {\n        // 向全局sass样式传入共享的全局变量, $src可以配置图片cdn前缀\n        // 详情: https://cli.vuejs.org/guide/css.html#passing-options-to-pre-processor-loaders\n        prependData: `\n        @import \"@scss/variables.scss\";\n        @import \"@scss/mixins.scss\";\n        @import \"@scss/function.scss\";\n        $src: \"${process.env.VUE_APP_OSS_SRC}\";\n        `\n      }\n    }\n  }\n};\n```\n\n在 scss 中引用\n\n```css\n.home {\n  background: url($src+\"/images/500.png\");\n}\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"globalless\"\u003e✅ 为 less 提供全局样式，以及全局变量\u003c/span\u003e\n\u003e npm i -D less less-loader\n\n\u0026emsp;\u0026emsp;在src/assets/less目录下新建variables.less文件，并定义全局less变量\n```less\n@primary-color: #1890ff;\n@normal-color: #d9d9d9;\n@text-color: #303753;\n```\n\u0026emsp;\u0026emsp;vue.config.js中为其添加相应less配置。\n```js\nconst path = require('path')\nconst fs = require('fs')\nconst postcss = require('postcss')\nconst resolve = dir =\u003e path.resolve(__dirname, dir)\n\nconst IS_PROD = ['prod', 'production'].includes(process.env.NODE_ENV)\n\nfunction getLessVaribles(fileUrl, list = {}) {\n  if (!fs.existsSync(fileUrl)) return {};\n  let lessFile = fs.readFileSync(fileUrl, 'utf8');\n  return postcss.parse(lessFile).nodes.reduce((acc, curr) =\u003e {\n    acc[`${curr.name.replace(/\\:/, '')}`] = `${curr.params}`;\n    return acc;\n  }, list);\n}\n\nconst modifyVars = getLessVaribles(resolve('./src/assets/less/variables.less'));\n\nmodule.exports = {\n  css: {\n    extract: IS_PROD,\n    // sourceMap: false,\n    loaderOptions: {\n      less: {\n        modifyVars,\n        javascriptEnabled: true,\n      }\n    }\n  }\n}\n```\n\n### \u003cspan id=\"globalstylus\"\u003e✅ 为 stylus 提供全局变量\u003c/span\u003e\n\n```bash\nnpm i -D style-resources-loader\n```\n\n```javascript\nconst path = require(\"path\");\nconst resolve = dir =\u003e path.resolve(__dirname, dir);\nconst addStylusResource = rule =\u003e {\n  rule\n    .use(\"style-resouce\")\n    .loader(\"style-resources-loader\")\n    .options({\n      patterns: [resolve(\"src/assets/stylus/variable.styl\")]\n    });\n};\nmodule.exports = {\n  chainWebpack: config =\u003e {\n    const types = [\"vue-modules\", \"vue\", \"normal-modules\", \"normal\"];\n    types.forEach(type =\u003e\n      addStylusResource(config.module.rule(\"stylus\").oneOf(type))\n    );\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"prerender\"\u003e预渲染 prerender-spa-plugin\u003c/span\u003e\n```bash\nnpm i -D prerender-spa-plugin\n```\n\n```javascript\nconst PrerenderSpaPlugin = require(\"prerender-spa-plugin\");\nconst path = require(\"path\");\nconst resolve = dir =\u003e path.join(__dirname, dir);\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\n\nmodule.exports = {\n  configureWebpack: config =\u003e {\n    const plugins = [];\n    if (IS_PROD) {\n      plugins.push(\n        new PrerenderSpaPlugin({\n          staticDir: resolve(\"dist\"),\n          routes: [\"/\"],\n          postProcess(ctx) {\n            ctx.route = ctx.originalRoute;\n            ctx.html = ctx.html.split(/\u003e[\\s]+\u003c/gim).join(\"\u003e\u003c\");\n            if (ctx.route.endsWith(\".html\")) {\n              ctx.outputPath = path.join(__dirname, \"dist\", ctx.route);\n            }\n            return ctx;\n          },\n          minify: {\n            collapseBooleanAttributes: true,\n            collapseWhitespace: true,\n            decodeEntities: true,\n            keepClosingSlash: true,\n            sortAttributes: true\n          },\n          renderer: new PrerenderSpaPlugin.PuppeteerRenderer({\n            // 需要注入一个值，这样就可以检测页面当前是否是预渲染的\n            inject: {},\n            headless: false,\n            // 视图组件是在API请求获取所有必要数据后呈现的，因此我们在dom中存在“data view”属性后创建页面快照\n            renderAfterDocumentEvent: \"render-event\"\n          })\n        })\n      );\n    }\n    config.plugins = [...config.plugins, ...plugins];\n  }\n};\n```\n\n\u0026emsp;\u0026emsp;mounted()中添加 document.dispatchEvent(new Event('render-event'))\n\n```javascript\nnew Vue({\n  router,\n  store,\n  render: h =\u003e h(App),\n  mounted() {\n    document.dispatchEvent(new Event(\"render-event\"));\n  }\n}).$mount(\"#app\");\n```\n\n##### 为自定义预渲染页面添加自定义 title、description、content\n\n- 删除 public/index.html 中关于 description、content 的 meta 标签。保留 title 标签\n\n- 配置 router-config.js\n\n```javascript\nmodule.exports = {\n  \"/\": {\n    title: \"首页\",\n    keywords: \"首页关键词\",\n    description: \"这是首页描述\"\n  },\n  \"/about.html\": {\n    title: \"关于我们\",\n    keywords: \"关于我们页面关键词\",\n    description: \"关于我们页面关键词描述\"\n  }\n};\n```\n\n- vue.config.js\n\n```js\nconst path = require(\"path\");\nconst PrerenderSpaPlugin = require(\"prerender-spa-plugin\");\nconst routesConfig = require(\"./router-config\");\nconst resolve = dir =\u003e path.join(__dirname, dir);\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\n\nmodule.exports = {\n  configureWebpack: config =\u003e {\n    const plugins = [];\n\n    if (IS_PROD) {\n      // 预加载\n      plugins.push(\n        new PrerenderSpaPlugin({\n          staticDir: resolve(\"dist\"),\n          routes: Object.keys(routesConfig),\n          postProcess(ctx) {\n            ctx.route = ctx.originalRoute;\n            ctx.html = ctx.html.split(/\u003e[\\s]+\u003c/gim).join(\"\u003e\u003c\");\n            ctx.html = ctx.html.replace(\n              /\u003ctitle\u003e(.*?)\u003c\\/title\u003e/gi,\n              `\u003ctitle\u003e${\n                routesConfig[ctx.route].title\n              }\u003c/title\u003e\u003cmeta name=\"keywords\" content=\"${\n                routesConfig[ctx.route].keywords\n              }\" /\u003e\u003cmeta name=\"description\" content=\"${\n                routesConfig[ctx.route].description\n              }\" /\u003e`\n            );\n            if (ctx.route.endsWith(\".html\")) {\n              ctx.outputPath = path.join(__dirname, \"dist\", ctx.route);\n            }\n            return ctx;\n          },\n          minify: {\n            collapseBooleanAttributes: true,\n            collapseWhitespace: true,\n            decodeEntities: true,\n            keepClosingSlash: true,\n            sortAttributes: true\n          },\n          renderer: new PrerenderSpaPlugin.PuppeteerRenderer({\n            // 需要注入一个值，这样就可以检测页面当前是否是预渲染的\n            inject: {},\n            headless: false,\n            // 视图组件是在API请求获取所有必要数据后呈现的，因此我们在dom中存在“data view”属性后创建页面快照\n            renderAfterDocumentEvent: \"render-event\"\n          })\n        })\n      );\n    }\n\n    config.plugins = [...config.plugins, ...plugins];\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"ie\"\u003e✅ 添加 IE 兼容\u003c/span\u003e\n\u0026emsp;\u0026emsp;在 main.js 中添加\n\n```javascript\nimport 'core-js/stable'; \nimport 'regenerator-runtime/runtime';\n```\n\n配置 babel.config.js\n\n```javascript\nconst plugins = [];\n\nmodule.exports = {\n  presets: [[\"@vue/app\", { useBuiltIns: \"entry\" }]],\n  plugins: plugins\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"alioss\"\u003e✅ 静态资源自动打包上传阿里 oss、华为 obs\u003c/span\u003e\n\n\u0026emsp;\u0026emsp;开启文件上传 ali oss，需要将 publicPath 改成 ali oss 资源 url 前缀,也就是修改 VUE_APP_PUBLIC_PATH。具体配置参见[阿里 oss 插件 webpack-oss](https://github.com/staven630/webpack-oss)、[华为 obs 插件 huawei-obs-plugin](https://github.com/staven630/huawei-obs-plugin)\n\n```bash\nnpm i -D webpack-oss\n```\n\n```javascript\nconst AliOssPlugin = require(\"webpack-oss\");\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\n\nconst format = AliOssPlugin.getFormat();\n\nmodule.exports = {\n  publicPath: IS_PROD ? `${process.env.VUE_APP_PUBLIC_PATH}/${format}` : \"./\", // 默认'/'，部署应用包时的基本 URL\n  configureWebpack: config =\u003e {\n    const plugins = [];\n\n    if (IS_PROD) {\n      plugins.push(\n        new AliOssPlugin({\n          accessKeyId: process.env.ACCESS_KEY_ID,\n          accessKeySecret: process.env.ACCESS_KEY_SECRET,\n          region: process.env.REGION,\n          bucket: process.env.BUCKET,\n          prefix: process.env.PREFIX,\n          exclude: /.*\\.html$/,\n          format\n        })\n      );\n    }\n    config.plugins = [...config.plugins, ...plugins];\n  }\n};\n```\n\n[▲ 回顶部](#top)\n\n### \u003cspan id=\"allconfig\"\u003e✅ 完整配置\u003c/span\u003e\n\n```javascript\nconst SpritesmithPlugin = require(\"webpack-spritesmith\");\nconst BundleAnalyzerPlugin = require(\"webpack-bundle-analyzer\")\n  .BundleAnalyzerPlugin;\nconst webpack = require(\"webpack\");\n\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst resolve = dir =\u003e path.join(__dirname, dir);\nconst IS_PROD = [\"production\", \"prod\"].includes(process.env.NODE_ENV);\n\nconst glob = require('glob')\nconst pagesInfo = require('./pages.config')\nconst pages = {}\n\nglob.sync('./src/pages/**/main.js').forEach(entry =\u003e {\n  let chunk = entry.match(/\\.\\/src\\/pages\\/(.*)\\/main\\.js/)[1];\n  const curr = pagesInfo[chunk];\n  if (curr) {\n    pages[chunk] = {\n      entry,\n      ...curr,\n      chunk: [\"chunk-vendors\", \"chunk-common\", chunk]\n    }\n  }\n})\n\nlet has_sprite = true;\nlet files = [];\nconst icons = {};\n\ntry {\n  fs.statSync(resolve(\"./src/assets/icons\"));\n  files = fs.readdirSync(resolve(\"./src/assets/icons\"));\n  files.forEach(item =\u003e {\n    let filename = item.toLocaleLowerCase().replace(/_/g, \"-\");\n    icons[filename] = true;\n  });\n\n} catch (error) {\n  fs.mkdirSync(resolve(\"./src/assets/icons\"));\n}\n\nif (!files.length) {\n  has_sprite = false;\n} else {\n  try {\n    let iconsObj = fs.readFileSync(resolve(\"./icons.json\"), \"utf8\");\n    iconsObj = JSON.parse(iconsObj);\n    has_sprite = files.some(item =\u003e {\n      let filename = item.toLocaleLowerCase().replace(/_/g, \"-\");\n      return !iconsObj[filename];\n    });\n    if (has_sprite) {\n      fs.writeFileSync(resolve(\"./icons.json\"), JSON.stringify(icons, null, 2));\n    }\n  } catch (error) {\n    fs.writeFileSync(resolve(\"./icons.json\"), JSON.stringify(icons, null, 2));\n    has_sprite = true;\n  }\n}\n\n// 雪碧图样式处理模板\nconst SpritesmithTemplate = function (data) {\n  // pc\n  let icons = {}\n  let tpl = `.ico { \n  display: inline-block; \n  background-image: url(${data.sprites[0].image}); \n  background-size: ${data.spritesheet.width}px ${data.spritesheet.height}px; \n}`\n\n  data.sprites.forEach(sprite =\u003e {\n    const name = '' + sprite.name.toLocaleLowerCase().replace(/_/g, '-')\n    icons[`${name}.png`] = true\n    tpl = `${tpl} \n.ico-${name}{\n  width: ${sprite.width}px; \n  height: ${sprite.height}px; \n  background-position: ${sprite.offset_x}px ${sprite.offset_y}px;\n}\n`\n  })\n  return tpl\n}\n\nmodule.exports = {\n  publicPath: IS_PROD ? process.env.VUE_APP_PUBLIC_PATH : \"./\", // 默认'/'，部署应用包时的基本 URL\n  // outputDir: process.env.outputDir || 'dist', // 'dist', 生产环境构建文件的目录\n  // assetsDir: \"\", // 相对于outputDir的静态资源(js、css、img、fonts)目录\n  configureWebpack: config =\u003e {\n    const plugins = [];\n\n    if (has_sprite) {\n      // 生成雪碧图\n      plugins.push(\n        new SpritesmithPlugin({\n          src: {\n            cwd: path.resolve(__dirname, './src/assets/icons/'), // 图标根路径\n            glob: '**/*.png' // 匹配任意 png 图标\n          },\n          target: {\n            image: path.resolve(__dirname, './src/assets/images/sprites.png'), // 生成雪碧图目标路径与名称\n            // 设置生成CSS背景及其定位的文件或方式\n            css: [\n              [\n                path.resolve(__dirname, './src/assets/scss/sprites.scss'),\n                {\n                  format: 'function_based_template'\n                }\n              ]\n            ]\n          },\n          customTemplates: {\n            function_based_template: SpritesmithTemplate\n          },\n          apiOptions: {\n            cssImageRef: '../images/sprites.png' // css文件中引用雪碧图的相对位置路径配置\n          },\n          spritesmithOptions: {\n            padding: 2\n          }\n        })\n      )\n    }\n\n    config.externals = {\n      vue: \"Vue\",\n      \"element-ui\": \"ELEMENT\",\n      \"vue-router\": \"VueRouter\",\n      vuex: \"Vuex\",\n      axios: \"axios\"\n    };\n\n    config.plugins = [...config.plugins, ...plugins];\n  },\n  chainWebpack: config =\u003e {\n    // 修复HMR\n    config.resolve.symlinks(true);\n\n    // config.plugins.delete('preload');\n    // config.plugins.delete('prefetch');\n\n    config\n      .plugin(\"ignore\")\n      .use(\n        new webpack.ContextReplacementPlugin(/moment[/\\\\]locale$/, /zh-cn$/)\n      );\n\n    // 添加别名\n    config.resolve.alias\n      .set(\"vue$\", \"vue/dist/vue.esm.js\")\n      .set(\"@\", resolve(\"src\"))\n      .set(\"@apis\", resolve(\"src/apis\"))\n      .set(\"@assets\", resolve(\"src/assets\"))\n      .set(\"@scss\", resolve(\"src/assets/scss\"))\n      .set(\"@components\", resolve(\"src/components\"))\n      .set(\"@middlewares\", resolve(\"src/middlewares\"))\n      .set(\"@mixins\", resolve(\"src/mixins\"))\n      .set(\"@plugins\", resolve(\"src/plugins\"))\n      .set(\"@router\", resolve(\"src/router\"))\n      .set(\"@store\", resolve(\"src/store\"))\n      .set(\"@utils\", resolve(\"src/utils\"))\n      .set(\"@views\", resolve(\"src/views\"))\n      .set(\"@layouts\", resolve(\"src/layouts\"));\n\n    const cdn = {\n      // 访问https://unpkg.com/element-ui/lib/theme-chalk/index.css获取最新版本\n      css: [\"//unpkg.com/element-ui@2.10.1/lib/theme-chalk/index.css\"],\n      js: [\n        \"//unpkg.com/vue@2.6.10/dist/vue.min.js\", // 访问https://unpkg.com/vue/dist/vue.min.js获取最新版本\n        \"//unpkg.com/vue-router@3.0.6/dist/vue-router.min.js\",\n        \"//unpkg.com/vuex@3.1.1/dist/vuex.min.js\",\n        \"//unpkg.com/axios@0.19.0/dist/axios.min.js\",\n        \"//unpkg.com/element-ui@2.10.1/lib/index.js\"\n      ]\n    };\n\n    // 如果使用多页面打包，使用vue inspect --plugins查看html是否在结果数组中\n    // config.plugin(\"html\").tap(args =\u003e {\n    //   // html中添加cdn\n    //   args[0].cdn = cdn;\n\n    //   // 修复 Lazy loading routes Error\n    //   args[0].chunksSortMode = \"none\";\n    //   return args;\n    // });\n\n    // 防止多页面打包卡顿\n    config =\u003e config.plugins.delete('named-chunks')\n\n    // 多页面cdn添加\n    Object.keys(pagesInfo).forEach(page =\u003e {\n      config.plugin(`html-${page}`).tap(args =\u003e {\n        // html中添加cdn\n        args[0].cdn = cdn;\n\n        // 修复 Lazy loading routes Error\n        args[0].chunksSortMode = \"none\";\n        return args;\n      });\n    })\n\n    if (IS_PROD) {\n      // 压缩图片\n      config.module\n        .rule(\"images\")\n        .test(/\\.(png|jpe?g|gif|svg)(\\?.*)?$/)\n        .use(\"image-webpack-loader\")\n        .loader(\"image-webpack-loader\")\n        .options({\n          mozjpeg: { progressive: true, quality: 65 },\n          optipng: { enabled: false },\n          pngquant: { quality: [0.65, 0.90], speed: 4 },\n          gifsicle: { interlaced: false }\n        });\n\n      // 打包分析\n      config.plugin(\"webpack-report\").use(BundleAnalyzerPlugin, [\n        {\n          analyzerMode: \"static\"\n        }\n      ]);\n    }\n\n    // 使用svg组件\n    const svgRule = config.module.rule(\"svg\");\n    svgRule.uses.clear();\n    svgRule.exclude.add(/node_modules/);\n    svgRule\n      .test(/\\.svg$/)\n      .use(\"svg-sprite-loader\")\n      .loader(\"svg-sprite-loader\")\n      .options({\n        symbolId: \"icon-[name]\"\n      });\n\n    const imagesRule = config.module.rule(\"images\");\n    imagesRule.exclude.add(resolve(\"src/icons\"));\n    config.module.rule(\"images\").test(/\\.(png|jpe?g|gif|svg)(\\?.*)?$/);\n\n    return config;\n  },\n  pages,\n  css: {\n    extract: IS_PROD,\n    sourceMap: false,\n    loaderOptions: {\n      scss: {\n        // 向全局sass样式传入共享的全局变量, $src可以配置图片cdn前缀\n        // 详情: https://cli.vuejs.org/guide/css.html#passing-options-to-pre-processor-loaders\n        prependData: `\n          @import \"@scss/variables.scss\";\n          @import \"@scss/mixins.scss\";\n          @import \"@scss/function.scss\";\n          $src: \"${process.env.VUE_APP_BASE_API}\";\n          `\n      }\n    }\n  },\n  lintOnSave: false,\n  runtimeCompiler: true, // 是否使用包含运行时编译器的 Vue 构建版本\n  productionSourceMap: !IS_PROD, // 生产环境的 source map\n  parallel: require(\"os\").cpus().length \u003e 1,\n  pwa: {},\n  devServer: {\n    // overlay: { // 让浏览器 overlay 同时显示警告和错误\n    //   warnings: true,\n    //   errors: true\n    // },\n    // open: false, // 是否打开浏览器\n    // host: \"localhost\",\n    // port: \"8080\", // 代理断就\n    // https: false,\n    // hotOnly: false, // 热更新\n    proxy: {\n      \"/api\": {\n        target:\n          \"https://www.easy-mock.com/mock/5bc75b55dc36971c160cad1b/sheets\", // 目标代理接口地址\n        secure: false,\n        changeOrigin: true, // 开启代理，在本地创建一个虚拟服务端\n        // ws: true, // 是否启用websockets\n        pathRewrite: {\n          \"^/api\": \"/\"\n        }\n      }\n    }\n  }\n};\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstaven630%2Fvue-cli4-config","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstaven630%2Fvue-cli4-config","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstaven630%2Fvue-cli4-config/lists"}