Vue2 vue.config.js 配置

发布于 2024-09-01 18:23:44 字数 49168 浏览 46 评论 0

配置环境变量

  • package.json 里的 scripts 配置项 中添加 --mode xxx 来选择不同环境
  • 在项目根目录中新建 .env , .env.production , .env.analyz 等文件。
  • 只有以 VUE_APP_ 开头的变量会被 webpack.DefinePlugin 静态嵌入到客户端侧的包中,代码中可以通过 process.env.VUE_APP_BASE_API 访问
  • NODE_ENVBASE_URL 是两个特殊变量,在代码中始终可用
  • .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": {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "crm": "vue-cli-service build --mode crm",
  "lint": "vue-cli-service lint"
}

使用环境变量

<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 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": "/"
        }
      }
    }
  }
};

配置 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 接口为 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: process.env.VUE_APP_BASE_API || 'http://127.0.0.1:8080',
                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);
    }
}

prefetch、preload 和路由懒加载

项目打包之后会出现很多 link 标签, 将你的所有打包好的文件都 预加载进来,导致路由懒加载失效。

<link href=js/chunk-cdfd6c6c.25e21cb5.js rel=prefetch>
<link href=css/app.b2839875.css rel=preload as=style>
<link href=css/chunk-vendors.779f7d1d.css rel=preload as=style>
<link href=js/app.1a2f81ff.js rel=preload as=script>
<link href=js/chunk-vendors.3a58506f.js rel=preload as=script>

通过配置 webpack 去掉这一行为

chainWebpack(config) {
    config.plugins.delete('preload') 
    config.plugins.delete('prefetch')  
}

prefetching

prefetch 是一种 resource hint,用来告诉浏览器在页面加载完成后,利用空闲时间提前获取用户未来可能会访问的内容。这和路由懒加载的理念冲突

// vue.config.js
module.exports = {
  chainWebpack: config => {
    // 移除 prefetch 插件
    config.plugins.delete('prefetch')

    // 或者
    // 修改它的选项:
    config.plugin('prefetch').tap(options => {
      options[0].fileBlacklist = options[0].fileBlacklist || []
      options[0].fileBlacklist.push(/myasyncRoute(.)+?\.js$/)
      return options
    })
  }
}

这样就解决了路由无法懒加载的问题

但是 !!! 这个预加载是不会影响当前页面的加载性能的,因此预加载是可以被保留的,那在什么情况下我们需要禁用预加载呢?当然是对流量损耗敏感(移动端)的应用场景,在首页对子页面进行全面的预加载,而用户可能只需要跳转其中的一两个子页面甚至停留在首页,造成大量的流量浪费。因此,需要做到控制特定的路由预加载。首先,先移除 prefetch 插件,然后按需添加预加载。

import(/* webpackPrefetch: true */ './someAsyncComponent.vue')

preloading

preloading 是一种 resource hint,用来指定页面加载后很快会被用到的资源,所以在页面加载的过程中,我们希望在浏览器开始主体渲染之前尽早 preload。

preloading 用于提高资源加载的优先级,当页面开始加载时,我们总是想核心的代码或资源得到优先处理,因此可以通过 preloading 提高优先级。

import(/* webpackPreload: true */ 'ChartingLibrary');

总结

  • 懒加载优化了首屏加载的速率
  • prefetch 预加载优化了子页面加载的速率

修复 Lazy loading routes Error: Cyclic dependency

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"));
  }
};

添加骨架屏(vue-skeleton-webpack-plugin)

npm install vue-skeleton-webpack-plugin

// skeleton.js
import Vue from 'vue';
import Skeleton from './Skeleton.vue';

export default new Vue({
  components: {
    Skeleton,
  },
  render: h => h(Skeleton),
});

// Skeleton.vue
<template>
  <div class="skeleton-wrapper">
    <section class="skeleton-block">
      <!-- eslint-disable vue/max-len -->
      <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA4MCAyNjEiPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNMCAwaDEwODB2MjYwSDB6Ii8+PGZpbHRlciBpZD0iYSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgeD0iLTUwJSIgeT0iLTUwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48ZmVPZmZzZXQgZHk9Ii0xIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlQ29sb3JNYXRyaXggaW49InNoYWRvd09mZnNldE91dGVyMSIgdmFsdWVzPSIwIDAgMCAwIDAuOTMzMzMzMzMzIDAgMCAwIDAgMC45MzMzMzMzMzMgMCAwIDAgMCAwLjkzMzMzMzMzMyAwIDAgMCAxIDAiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDEpIj48dXNlIGZpbGw9IiMwMDAiIGZpbHRlcj0idXJsKCNhKSIgeGxpbms6aHJlZj0iI2IiLz48dXNlIGZpbGw9IiNGRkYiIHhsaW5rOmhyZWY9IiNiIi8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCA0NGg1MzN2NDZIMjMweiIvPjxyZWN0IHdpZHRoPSIxNzIiIGhlaWdodD0iMTcyIiB4PSIzMCIgeT0iNDQiIGZpbGw9IiNGNkY2RjYiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCAxMThoMzY5djMwSDIzMHpNMjMwIDE4MmgzMjN2MzBIMjMwek04MTIgMTE1aDIzOHYzOUg4MTJ6TTgwOCAxODRoMjQydjMwSDgwOHpNOTE3IDQ4aDEzM3YzN0g5MTd6Ii8+PC9nPjwvc3ZnPg==">
      <img src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2aWV3Qm94PSIwIDAgMTA4MCAyNjEiPjxkZWZzPjxwYXRoIGlkPSJiIiBkPSJNMCAwaDEwODB2MjYwSDB6Ii8+PGZpbHRlciBpZD0iYSIgd2lkdGg9IjIwMCUiIGhlaWdodD0iMjAwJSIgeD0iLTUwJSIgeT0iLTUwJSIgZmlsdGVyVW5pdHM9Im9iamVjdEJvdW5kaW5nQm94Ij48ZmVPZmZzZXQgZHk9Ii0xIiBpbj0iU291cmNlQWxwaGEiIHJlc3VsdD0ic2hhZG93T2Zmc2V0T3V0ZXIxIi8+PGZlQ29sb3JNYXRyaXggaW49InNoYWRvd09mZnNldE91dGVyMSIgdmFsdWVzPSIwIDAgMCAwIDAuOTMzMzMzMzMzIDAgMCAwIDAgMC45MzMzMzMzMzMgMCAwIDAgMCAwLjkzMzMzMzMzMyAwIDAgMCAxIDAiLz48L2ZpbHRlcj48L2RlZnM+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwIDEpIj48dXNlIGZpbGw9IiMwMDAiIGZpbHRlcj0idXJsKCNhKSIgeGxpbms6aHJlZj0iI2IiLz48dXNlIGZpbGw9IiNGRkYiIHhsaW5rOmhyZWY9IiNiIi8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCA0NGg1MzN2NDZIMjMweiIvPjxyZWN0IHdpZHRoPSIxNzIiIGhlaWdodD0iMTcyIiB4PSIzMCIgeT0iNDQiIGZpbGw9IiNGNkY2RjYiIHJ4PSI0Ii8+PHBhdGggZmlsbD0iI0Y2RjZGNiIgZD0iTTIzMCAxMThoMzY5djMwSDIzMHpNMjMwIDE4MmgzMjN2MzBIMjMwek04MTIgMTE1aDIzOHYzOUg4MTJ6TTgwOCAxODRoMjQydjMwSDgwOHpNOTE3IDQ4aDEzM3YzN0g5MTd6Ii8+PC9nPjwvc3ZnPg==">
    </section>
  </div>
</template>

<script>
export default {
  name: 'Skeleton',
};
</script>

<style scoped>
.skeleton-block {
  display: flex;
  flex-direction: column;
  padding: 16px;
}
</style>
const path = require('path');
const SkeletonWebpackPlugin = require('vue-skeleton-webpack-plugin');

module.exports = {
  configureWebpack: {
    plugins: [
      new SkeletonWebpackPlugin({
        webpackConfig: {
          entry: {
            app: path.join(__dirname, './src/skeleton.js'),
          },
        },
        minimize: true,
        quiet: true,
      }),
    ],
  },
};

压缩图片(image-webpack-loader)

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({
          bypassOnDebug: true
          // 或
          mozjpeg: { progressive: true, quality: 65 },
          optipng: { enabled: false },
          pngquant: { quality: [0.65, 0.9], speed: 4 },
          gifsicle: { interlaced: false }
          // webp: { quality: 75 } //ios 不支持
        });
    }
  }
};

自动生成雪碧图(webpack-spritesmith 插件)

默认 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 字体(svgtofont)

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 配置,执行 npm run font

"prebuild": "npm run font",
"font": "node scripts/svg2font.js",

使用 SVG 组件(svg-sprite-loader)

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(purgecss-webpack-plugin)

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

  • 方案一 :@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];
      }
    };
    

添加打包分析(webpack-bundle-analyzer)

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 中添加

<!-- 使用 CDN 的 CSS 文件 -->
<% for (var i in htmlWebpackPlugin.options.cdn &&
htmlWebpackPlugin.options.cdn.css) { %>
<link rel="stylesheet" href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" />
<% } %>

<!-- 使用 CDN 的 JS 文件 -->
<% 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(uglifyjs-webpack-plugin)

方法一

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 显式转译一个依赖,可以在这个选项中列出来。配置需要转译的第三方库

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

npm i --save-dev babel-plugin-transform-remove-console

在 babel.config.js 中配置:

const plugins = [];
if(['production', 'prod'].includes(process.env.NODE_ENV)) {  
  plugins.push("transform-remove-console")
}

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

利用 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 压缩(compression-webpack-plugin)

npm i --save-dev compression-webpack-plugin

const CompressionWebpackPlugin = require('compression-webpack-plugin');
const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;
const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);
module.exports = {
    configureWebpack: config => {
        if (IS_PROD) {
            const plugins = [];
            plugins.push(
                new CompressionWebpackPlugin({
                    filename: '[path].gz[query]',
                    algorithm: 'gzip',
                    test: productionGzipExtensions,
                    threshold: 10240,
                    minRatio: 0.8
                })
            );
            config.plugins = [
                ...config.plugins,
                ...plugins
            ];
        }
    }
}

还可以开启比 gzip 体验更好的 Zopfli 压缩详见 brotli-webpack-plugin

npm i --save-dev @gfx/zopfli brotli-webpack-plugin

const CompressionWebpackPlugin = require('compression-webpack-plugin');
const zopfli = require("@gfx/zopfli");
const BrotliPlugin = require("brotli-webpack-plugin");
const productionGzipExtensions = /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i;
const IS_PROD = ["production", "prod"].includes(process.env.NODE_ENV);
module.exports = {
    configureWebpack: config => {
        if (IS_PROD) {
            const plugins = [];
            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_SRC ;挂载环境变量中的配置信息,然后在 js 中使用 src=process.env.VUEAPPSRC ;挂载环境变量中的配置信息,然后在 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 提供全局变量(style-resources-loader)

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 兼容

在 main.js 中添加 :

import 'core-js/stable'; 
import 'regenerator-runtime/runtime';

配置 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];
  }
};

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
23 人气
更多

推荐作者

linfzu01

文章 0 评论 0

可遇━不可求

文章 0 评论 0

枕梦

文章 0 评论 0

qq_3LFa8Q

文章 0 评论 0

JP

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文