手把手教你搞定 vue-cli4 配置

王老闆的前端發表於2020-03-15

作者:staven630

小編:前端老王

專案:github.com/staven630/v…

細緻全面的 vue-cli4 配置資訊。涵蓋了使用 vue-cli 開發過程中大部分配置需求。

目錄

✅ 配置多環境變數

  通過在 package.json 裡的 scripts 配置項中新增–mode xxx 來選擇不同環境

  只有以 VUE_APP 開頭的變數會被 webpack.DefinePlugin 靜態嵌入到客戶端側的包中,程式碼中可以通過 process.env.VUE_APP_BASE_API 訪問

  NODE_ENV 和 BASE_URL 是兩個特殊變數,在程式碼中始終可用

配置

  在專案根目錄中新建.env, .env.production, .env.analyz 等檔案

  • .env

  serve 預設的本地開發環境配置

NODE_ENV = "development"
BASE_URL = "./"
VUE_APP_PUBLIC_PATH = "./"
VUE_APP_API = "https://test.staven630.com/api"複製程式碼
  • .env.production

  build 預設的環境配置(正式伺服器)

NODE_ENV = "production"
BASE_URL = "https://prod.staven630.com/"
VUE_APP_PUBLIC_PATH = "https://prod.oss.com/staven-blog"
VUE_APP_API = "https://prod.staven630.com/api"

ACCESS_KEY_ID = "xxxxxxxxxxxxx"
ACCESS_KEY_SECRET = "xxxxxxxxxxxxx"
REGION = "oss-cn-hangzhou"
BUCKET = "staven-prod"
PREFIX = "staven-blog"複製程式碼
  • .env.crm

  自定義 build 環境配置(預發伺服器)

NODE_ENV = "production"
BASE_URL = "https://crm.staven630.com/"
VUE_APP_PUBLIC_PATH = "https://crm.oss.com/staven-blog"
VUE_APP_API = "https://crm.staven630.com/api"

ACCESS_KEY_ID = "xxxxxxxxxxxxx"
ACCESS_KEY_SECRET = "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>

<script>
export default {
  name: "home",
  data() {
    return {
      api: process.env.VUE_APP_API
    };
  },
  mounted() {
    // js程式碼中使用環境變數
    console.log("BASE_URL: ", process.env.BASE_URL);
    console.log("VUE_APP_API: ", process.env.VUE_APP_API);
  }
};
</script>複製程式碼

▲ 回頂部

✅ 配置基礎 vue.config.js

const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);

module.exports = {
  publicPath: IS_PROD ? process.env.VUE_APP_PUBLIC_PATH : "./", // 預設'/',部署應用包時的基本 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 介面為www.easy-mock.com/mock/5bc75b…

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": "/"
        }
      }
    }
  }
};複製程式碼

  訪問

<script>
import axios from "axios";
export default {
  mounted() {
    axios.get("/api/1").then(res => {
      console.log('proxy:', res);
    });
  }
};
</script>複製程式碼

▲ 回頂部

✅ 修復 HMR(熱更新)失效

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

module.exports = {
  chainWebpack: config => {
    // 修復HMR
    config.resolve.symlinks(true);
  }
};複製程式碼

▲ 回頂部

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

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 IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);

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.offset_x}px ${sprite.offset_y}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: "function_based_template"
                }
              ]
            ]
          },
          customTemplates: {
            function_based_template: 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: "Font Class",
        url: "index.html"
      },
      {
        title: "Unicode",
        url: "unicode.html"
      }
    ],
    footerInfo: ``
  }
}).then(() => {
  console.log("done!");
});複製程式碼

  新增 package.json scripts 配置:

"prebuild": "npm run font",
"font": "node scripts/svg2font.js",複製程式碼

  執行:

npm run font複製程式碼

▲ 回頂部

✅ 使用 SVG 元件

npm i -D svg-sprite-loader複製程式碼

  新增 SvgIcon 元件。

<template>
  <svg class="svg-icon"
       aria-hidden="true">
    <use :xlink:href="iconName" />
  </svg>
</template>

<script>
export default {
  name: 'SvgIcon',
  props: {
    iconClass: {
      type: String,
      required: true
    }
  },
  computed: {
    iconName() {
      return `#icon-${this.iconClass}`
    }
  }
}
</script>

<style scoped>
.svg-icon {
  width: 1em;
  height: 1em;
  vertical-align: -0.15em;
  fill: currentColor;
  overflow: hidden;
}
</style>複製程式碼

  在 src 資料夾中建立 icons 資料夾。icons 資料夾中新增 svg 資料夾(用來存放 svg 檔案)與 index.js 檔案:

import SvgIcon from "@/components/SvgIcon";
import Vue from "vue";

// 註冊到全域性
Vue.component("svg-icon", SvgIcon);

const requireAll = requireContext => requireContext.keys().map(requireContext);
const req = require.context("./svg", false, /\.svg$/);
requireAll(req);複製程式碼

  在 main.js 中匯入 icons/index.js

import "@/icons";複製程式碼

  修改 vue.config.js

const path = require("path");
const resolve = dir => path.join(__dirname, dir);

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

    const imagesRule = config.module.rule("images");
    imagesRule.exclude.add(resolve("src/icons"));
    config.module.rule("images").test(/\.(png|jpe?g|gif|svg)(\?.*)?$/);
  }
};複製程式碼

▲ 回頂部

✅ 去除多餘無效的 css

  注:謹慎使用。可能出現各種樣式丟失現象。

  • 方案一:@fullhuman/postcss-purgecss
npm i -D postcss-import @fullhuman/postcss-purgecss複製程式碼

  更新 postcss.config.js

const autoprefixer = require("autoprefixer");
const postcssImport = require("postcss-import");
const purgecss = require("@fullhuman/postcss-purgecss");
const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);
let plugins = [];
if (IS_PROD) {
  plugins.push(postcssImport);
  plugins.push(
    purgecss({
      content: [
        "./layouts/**/*.vue",
        "./components/**/*.vue",
        "./pages/**/*.vue"
      ],
      extractors: [
        {
          extractor: class Extractor {
            static extract(content) {
              const validSection = content.replace(
                /<style([\s\S]*?)<\/style>+/gim,
                ""
              );
              return (
                validSection.match(/[A-Za-z0-9-_/:]*[A-Za-z0-9-_/]+/g) || []
              );
            }
          },
          extensions: ["html", "vue"]
        }
      ],
      whitelist: ["html", "body"],
      whitelistPatterns: [
        /el-.*/,
        /-(leave|enter|appear)(|-(to|from|active))$/,
        /^(?!cursor-move).+-move$/,
        /^router-link(|-exact)-active$/
      ],
      whitelistPatternsChildren: [/^token/, /^pre/, /^code/]
    })
  );
}
module.exports = {
  plugins: [...plugins, autoprefixer]
};複製程式碼
  • 方案二:purgecss-webpack-plugin
npm i -D glob-all purgecss-webpack-plugin複製程式碼
const path = require("path");
const glob = require("glob-all");
const PurgecssPlugin = require("purgecss-webpack-plugin");
const resolve = dir => path.join(__dirname, dir);
const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);

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

▲ 回頂部

✅ 新增打包分析

const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
  .BundleAnalyzerPlugin;

module.exports = {
  chainWebpack: config => {
    // 打包分析
    if (IS_PROD) {
      config.plugin("webpack-report").use(BundleAnalyzerPlugin, [
        {
          analyzerMode: "static"
        }
      ]);
    }
  }
};複製程式碼

▲ 回頂部

✅ 配置 externals 引入 cdn 資源

  防止將某些 import 的包(package)打包到 bundle 中,而是在執行時(runtime)再去從外部獲取這些擴充套件依賴

module.exports = {
  configureWebpack: config => {
    config.externals = {
      vue: "Vue",
      "element-ui": "ELEMENT",
      "vue-router": "VueRouter",
      vuex: "Vuex",
      axios: "axios"
    };
  },
  chainWebpack: config => {
    const cdn = {
      // 訪問https://unpkg.com/element-ui/lib/theme-chalk/index.css獲取最新版本
      css: ["//unpkg.com/element-ui@2.10.1/lib/theme-chalk/index.css"],
      js: [
        "//unpkg.com/vue@2.6.10/dist/vue.min.js", // 訪問https://unpkg.com/vue/dist/vue.min.js獲取最新版本
        "//unpkg.com/vue-router@3.0.6/dist/vue-router.min.js",
        "//unpkg.com/vuex@3.1.1/dist/vuex.min.js",
        "//unpkg.com/axios@0.19.0/dist/axios.min.js",
        "//unpkg.com/element-ui@2.10.1/lib/index.js"
      ]
    };

    // 如果使用多頁面打包,使用vue inspect --plugins檢視html是否在結果陣列中
    config.plugin("html").tap(args => {
      // html中新增cdn
      args[0].cdn = cdn;
      return args;
    });
  }
};複製程式碼

  在 html 中新增

<!-- 使用CDNCSS檔案 -->
<% for (var i in htmlWebpackPlugin.options.cdn &&
htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %>

<!-- 使用CDNJS檔案 -->
<% for (var i in htmlWebpackPlugin.options.cdn &&
htmlWebpackPlugin.options.cdn.js) { %>
<script
  type="text/javascript"
  src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"
></script>
<% } %>
複製程式碼

▲ 回頂部

✅ 多頁面打包 multi-page

  多入口頁面打包,建議在 src 目錄下新建 pages 目錄存放多頁面模組。

  • pages.config.js

   配置多頁面資訊。src/main.js 檔案對應 main 欄位,其他根據參照 pages 為根路徑為欄位。如下:

module.exports = {
  'admin': {
    template: 'public/index.html',
    filename: 'admin.html',
    title: '後臺管理',
  },
  'mobile': {
    template: 'public/index.html',
    filename: 'mobile.html',
    title: '移動端',
  },
  'pc/crm': {
    template: 'public/index.html',
    filename: 'pc-crm.html',
    title: '預發服務',
  }
}複製程式碼
  • vue.config.js

  vue.config.js 的 pages 欄位為多頁面提供配置

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

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

module.exports = {
  chainWebpack: config => {
    // 防止多頁面打包卡頓
    config => config.plugins.delete("named-chunks");

    return config;
  },
  pages
};複製程式碼

  如果多頁面打包需要使用 CDN,使用 vue inspect –plugins 檢視 html 是否在結果陣列中的形式。上例中 plugins 列表中存在’html-main’,’html-pages/admin’,’html-pages/mobile’, 沒有’html’。因此不能再使用 config.plugin(“html”)。

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

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

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

module.exports = {
  publicPath: IS_PROD ? process.env.VUE_APP_PUBLIC_PATH : "./", //
  configureWebpack: config => {
    config.externals = {
      vue: "Vue",
      "element-ui": "ELEMENT",
      "vue-router": "VueRouter",
      vuex: "Vuex",
      axios: "axios"
    };
  },
  chainWebpack: config => {
    const cdn = {
      // 訪問https://unpkg.com/element-ui/lib/theme-chalk/index.css獲取最新版本
      css: ["//unpkg.com/element-ui@2.10.1/lib/theme-chalk/index.css"],
      js: [
        "//unpkg.com/vue@2.6.10/dist/vue.min.js", // 訪問https://unpkg.com/vue/dist/vue.min.js獲取最新版本
        "//unpkg.com/vue-router@3.0.6/dist/vue-router.min.js",
        "//unpkg.com/vuex@3.1.1/dist/vuex.min.js",
        "//unpkg.com/axios@0.19.0/dist/axios.min.js",
        "//unpkg.com/element-ui@2.10.1/lib/index.js"
      ]
    };

    // 防止多頁面打包卡頓
    config => config.plugins.delete("named-chunks");

    // 多頁面cdn新增
    Object.keys(pagesInfo).forEach(page => {
      config.plugin(`html-${page}`).tap(args => {
        // html中新增cdn
        args[0].cdn = cdn;

        // 修復 Lazy loading routes Error
        args[0].chunksSortMode = "none";
        return args;
      });
    });
    return config;
  },
  pages
};複製程式碼

▲ 回頂部

✅ 刪除 moment 語言包

  刪除 moment 除 zh-cn 中文包外的其它語言包,無需在程式碼中手動引入 zh-cn 語言包。

const webpack = require("webpack");

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

    return config;
  }
};複製程式碼

▲ 回頂部

✅ 去掉 console.log

方法一:使用 babel-plugin-transform-remove-console 外掛
npm i -D babel-plugin-transform-remove-console複製程式碼

在 babel.config.js 中配置

const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);

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

module.exports = {
  presets: ["@vue/app", { useBuiltIns: "entry" }],
  plugins
};
複製程式碼
方法二:
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
module.exports = {
  configureWebpack: config => {
    if (IS_PROD) {
      const plugins = [];
      plugins.push(
        new UglifyJsPlugin({
          uglifyOptions: {
            compress: {
              warnings: false,
              drop_console: true,
              drop_debugger: false,
              pure_funcs: ["console.log"] //移除console
            }
          },
          sourceMap: false,
          parallel: true
        })
      );
      config.plugins = [...config.plugins, ...plugins];
    }
  }
};複製程式碼

  如果使用 uglifyjs-webpack-plugin 會報錯,可能存在 node_modules 中有些依賴需要 babel 轉譯。

  而 vue-cli 的transpileDependencies配置預設為[], babel-loader 會忽略所有 node_modules 中的檔案。如果你想要通過 Babel 顯式轉譯一個依賴,可以在這個選項中列出來。配置需要轉譯的第三方庫。

▲ 回頂部

利用 splitChunks 單獨打包第三方模組

const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);

module.exports = {
  configureWebpack: config => {
    if (IS_PROD) {
      config.optimization = {
        splitChunks: {
          cacheGroups: {
            common: {
              name: "chunk-common",
              chunks: "initial",
              minChunks: 2,
              maxInitialRequests: 5,
              minSize: 0,
              priority: 1,
              reuseExistingChunk: true,
              enforce: true
            },
            vendors: {
              name: "chunk-vendors",
              test: /[\\/]node_modules[\\/]/,
              chunks: "initial",
              priority: 2,
              reuseExistingChunk: true,
              enforce: true
            },
            elementUI: {
              name: "chunk-elementui",
              test: /[\\/]node_modules[\\/]element-ui[\\/]/,
              chunks: "all",
              priority: 3,
              reuseExistingChunk: true,
              enforce: true
            },
            echarts: {
              name: "chunk-echarts",
              test: /[\\/]node_modules[\\/](vue-)?echarts[\\/]/,
              chunks: "all",
              priority: 4,
              reuseExistingChunk: true,
              enforce: true
            }
          }
        }
      };
    }
  },
  chainWebpack: config => {
    if (IS_PROD) {
      config.optimization.delete("splitChunks");
    }
    return config;
  }
};複製程式碼

▲ 回頂部

✅ 開啟 gzip 壓縮

npm i -D compression-webpack-plugin複製程式碼
const CompressionWebpackPlugin = require("compression-webpack-plugin");

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

module.exports = {
  configureWebpack: config => {
    const plugins = [];
    if (IS_PROD) {
      plugins.push(
        new CompressionWebpackPlugin({
          filename: "[path].gz[query]",
          algorithm: "gzip",
          test: productionGzipExtensions,
          threshold: 10240,
          minRatio: 0.8
        })
      );
    }
    config.plugins = [...config.plugins, ...plugins];
  }
};複製程式碼

  還可以開啟比 gzip 體驗更好的 Zopfli 壓縮詳見webpack.js.org/plugins/com…

npm i -D @gfx/zopfli brotli-webpack-plugin複製程式碼
const CompressionWebpackPlugin = require("compression-webpack-plugin");
const zopfli = require("@gfx/zopfli");
const BrotliPlugin = require("brotli-webpack-plugin");

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

module.exports = {
  configureWebpack: config => {
    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];
  }
};複製程式碼

▲ 回頂部

✅ 開啟 stylelint 檢測scss, css語法

npm i -D stylelint stylelint-config-standard stylelint-config-prettier stylelint-webpack-plugin複製程式碼

在資料夾建立stylelint.config.js,詳細配置在這裡

module.exports = {
  ignoreFiles: ["**/*.js", "src/assets/css/element-variables.scss", "theme/"], 
  extends: ["stylelint-config-standard", "stylelint-config-prettier"],
  rules: {
    "no-empty-source": null,
    "at-rule-no-unknown": [
      true,
      {
        ignoreAtRules: ["extend"]
      }
    ]
  }
};複製程式碼

啟用webpack配置

const StylelintPlugin = require("stylelint-webpack-plugin");

module.exports = {
  configureWebpack: config => {
    const plugins = [];
    if (IS_DEV) {
      plugins.push(
        new StylelintPlugin({
          files: ["src/**/*.vue", "src/assets/**/*.scss"],
          fix: true //開啟自動修復(謹慎使用!注意上面的配置不要加入js或html檔案,會發生問題,js檔案請手動修復)
        })
      );
    }
    config.plugins = [...config.plugins, ...plugins];
  }
}複製程式碼

▲ 回頂部

✅ 為 sass 提供全域性樣式,以及全域性變數

  可以通過在 main.js 中 Vue.prototype.$src = process.env.VUE_APP_PUBLIC_PATH;掛載環境變數中的配置資訊,然後在js中使用$src 訪問。

  css 中可以使用注入 sass 變數訪問環境變數中的配置資訊

const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);

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.VUE_APP_OSS_SRC}";
        `
      }
    }
  }
};複製程式碼

在 scss 中引用

.home {
  background: url($src+"/images/500.png");
}複製程式碼

▲ 回頂部

✅ 為 stylus 提供全域性變數

npm i -D style-resources-loader複製程式碼
const path = require("path");
const resolve = dir => path.resolve(__dirname, dir);
const addStylusResource = rule => {
  rule
    .use("style-resouce")
    .loader("style-resources-loader")
    .options({
      patterns: [resolve("src/assets/stylus/variable.styl")]
    });
};
module.exports = {
  chainWebpack: config => {
    const types = ["vue-modules", "vue", "normal-modules", "normal"];
    types.forEach(type =>
      addStylusResource(config.module.rule("stylus").oneOf(type))
    );
  }
};複製程式碼

▲ 回頂部

預渲染 prerender-spa-plugin

npm i -D prerender-spa-plugin複製程式碼
const PrerenderSpaPlugin = require("prerender-spa-plugin");
const path = require("path");
const resolve = dir => path.join(__dirname, dir);
const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);

module.exports = {
  configureWebpack: config => {
    const plugins = [];
    if (IS_PROD) {
      plugins.push(
        new PrerenderSpaPlugin({
          staticDir: resolve("dist"),
          routes: ["/"],
          postProcess(ctx) {
            ctx.route = ctx.originalRoute;
            ctx.html = ctx.html.split(/>[\s]+</gim).join("><");
            if (ctx.route.endsWith(".html")) {
              ctx.outputPath = path.join(__dirname, "dist", 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: "render-event"
          })
        })
      );
    }
    config.plugins = [...config.plugins, ...plugins];
  }
};複製程式碼

  mounted()中新增 document.dispatchEvent(new Event(‘render-event’))

new Vue({
  router,
  store,
  render: h => h(App),
  mounted() {
    document.dispatchEvent(new Event("render-event"));
  }
}).$mount("#app");複製程式碼
為自定義預渲染頁面新增自定義 title、description、content
  • 刪除 public/index.html 中關於 description、content 的 meta 標籤。保留 title 標籤

  • 配置 router-config.js

module.exports = {
  "/": {
    title: "首頁",
    keywords: "首頁關鍵詞",
    description: "這是首頁描述"
  },
  "/about.html": {
    title: "關於我們",
    keywords: "關於我們頁面關鍵詞",
    description: "關於我們頁面關鍵詞描述"
  }
};複製程式碼
  • vue.config.js
const path = require("path");
const PrerenderSpaPlugin = require("prerender-spa-plugin");
const routesConfig = require("./router-config");
const resolve = dir => path.join(__dirname, dir);
const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);

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

    if (IS_PROD) {
      // 預載入
      plugins.push(
        new PrerenderSpaPlugin({
          staticDir: resolve("dist"),
          routes: Object.keys(routesConfig),
          postProcess(ctx) {
            ctx.route = ctx.originalRoute;
            ctx.html = ctx.html.split(/>[\s]+</gim).join("><");
            ctx.html = ctx.html.replace(
              /<title>(.*?)<\/title>/gi,
              `<title>${
                routesConfig[ctx.route].title
              }</title><meta name="keywords" content="${
                routesConfig[ctx.route].keywords
              }" /><meta name="description" content="${
                routesConfig[ctx.route].description
              }" />`
            );
            if (ctx.route.endsWith(".html")) {
              ctx.outputPath = path.join(__dirname, "dist", 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: "render-event"
          })
        })
      );
    }

    config.plugins = [...config.plugins, ...plugins];
  }
};複製程式碼

▲ 回頂部

✅ 新增 IE 相容

npm i -S @babel/polyfill複製程式碼

  在 main.js 中新增

import "@babel/polyfill";複製程式碼

配置 babel.config.js

const plugins = [];

module.exports = {
  presets: [["@vue/app", { useBuiltIns: "entry" }]],
  plugins: plugins
};複製程式碼

▲ 回頂部

✅ 靜態資源自動打包上傳阿里 oss、華為 obs

  開啟檔案上傳 ali oss,需要將 publicPath 改成 ali oss 資源 url 字首,也就是修改 VUE_APP_PUBLIC_PATH。具體配置參見阿里 oss 外掛 webpack-oss華為 obs 外掛 huawei-obs-plugin

npm i -D webpack-oss複製程式碼
const AliOssPlugin = require("webpack-oss");
const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);

const format = AliOssPlugin.getFormat();

module.exports = {
  publicPath: IS_PROD ? `${process.env.VUE_APP_PUBLIC_PATH}/${format}` : "./", // 預設'/',部署應用包時的基本 URL
  configureWebpack: config => {
    const plugins = [];

    if (IS_PROD) {
      plugins.push(
        new AliOssPlugin({
          accessKeyId: process.env.ACCESS_KEY_ID,
          accessKeySecret: process.env.ACCESS_KEY_SECRET,
          region: process.env.REGION,
          bucket: process.env.BUCKET,
          prefix: process.env.PREFIX,
          exclude: /.*\.html$/,
          format
        })
      );
    }
    config.plugins = [...config.plugins, ...plugins];
  }
};複製程式碼

▲ 回頂部

✅ 完整配置

const SpritesmithPlugin = require("webpack-spritesmith");
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer")
  .BundleAnalyzerPlugin;
const webpack = require("webpack");

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

const glob = require('glob')
const pagesInfo = require('./pages.config')
const pages = {}

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

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.offset_x}px ${sprite.offset_y}px;
}
`
  })
  return tpl
}

module.exports = {
  publicPath: IS_PROD ? process.env.VUE_APP_PUBLIC_PATH : "./", // 預設'/',部署應用包時的基本 URL
  // outputDir: process.env.outputDir || 'dist', // 'dist', 生產環境構建檔案的目錄
  // assetsDir: "", // 相對於outputDir的靜態資源(js、css、img、fonts)目錄
  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: 'function_based_template'
                }
              ]
            ]
          },
          customTemplates: {
            function_based_template: SpritesmithTemplate
          },
          apiOptions: {
            cssImageRef: '../images/sprites.png' // css檔案中引用雪碧圖的相對位置路徑配置
          },
          spritesmithOptions: {
            padding: 2
          }
        })
      )
    }

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

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

    // config.plugins.delete('preload');
    // config.plugins.delete('prefetch');

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

    // 新增別名
    config.resolve.alias
      .set("vue$", "vue/dist/vue.esm.js")
      .set("@", resolve("src"))
      .set("@apis", resolve("src/apis"))
      .set("@assets", resolve("src/assets"))
      .set("@scss", resolve("src/assets/scss"))
      .set("@components", resolve("src/components"))
      .set("@middlewares", resolve("src/middlewares"))
      .set("@mixins", resolve("src/mixins"))
      .set("@plugins", resolve("src/plugins"))
      .set("@router", resolve("src/router"))
      .set("@store", resolve("src/store"))
      .set("@utils", resolve("src/utils"))
      .set("@views", resolve("src/views"))
      .set("@layouts", resolve("src/layouts"));

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

    // 如果使用多頁面打包,使用vue inspect --plugins檢視html是否在結果陣列中
    // config.plugin("html").tap(args => {
    //   // html中新增cdn
    //   args[0].cdn = cdn;

    //   // 修復 Lazy loading routes Error
    //   args[0].chunksSortMode = "none";
    //   return args;
    // });

    // 防止多頁面打包卡頓
    config => config.plugins.delete('named-chunks')

    // 多頁面cdn新增
    Object.keys(pagesInfo).forEach(page => {
      config.plugin(`html-${page}`).tap(args => {
        // html中新增cdn
        args[0].cdn = cdn;

        // 修復 Lazy loading routes Error
        args[0].chunksSortMode = "none";
        return args;
      });
    })

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

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

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

    const imagesRule = config.module.rule("images");
    imagesRule.exclude.add(resolve("src/icons"));
    config.module.rule("images").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.VUE_APP_BASE_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": "/"
        }
      }
    }
  }
};複製程式碼

最後

如果本文對你有幫助的話,給本文點個贊吧。

關注公眾號

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章