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

news/2024/5/20 8:27:26

前言

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

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

vue-cli3 配置见 vue-cli3 分支。

其他系列

★ Blog

★ Nuxt.js 全面配置

目录

  • √ 配置多环境变量
  • √ 配置基础 vue.config.js
  • √ 配置 proxy 跨域
  • √ 修复 HMR(热更新)失效
  • √ 修复 Lazy loading routes Error: Cyclic dependency
  • √ 添加别名 alias
  • √ 压缩图片
  • √ 自动生成雪碧图
  • √ SVG 转 font 字体
  • √ 使用 SVG 组件
  • √ 去除多余无效的 css
  • √ 添加打包分析
  • √ 配置 externals 引入 cdn 资源
  • √ 多页面打包 multi-page
  • √ 删除 moment 语言包
  • √ 去掉 console.log
  • √ 利用 splitChunks 单独打包第三方模块
  • √ 开启 gzip 压缩
  • √ 开启 stylelint 检测scss, css语法
  • √ 为 sass 提供全局样式,以及全局变量
  • √ 为 stylus 提供全局变量
  • √ 预渲染 prerender-spa-plugin
  • √ 添加 IE 兼容
  • √ 静态资源自动打包上传阿里 oss、华为 obs
  • √ 完整依赖

✅ 配置多环境变量

通过在 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 mapparallel: require("os").cpus().length > 1,pwa: {}
};

▲ 回顶部

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

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

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

▲ 回顶部

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

module.exports = {chainWebpack: config => {// 如果使用多页面打包,使用vue inspect --plugins查看html是否在结果数组中config.plugin("html").tap(args => {// 修复 Lazy loading routes Errorargs[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) {// pclet 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中添加cdnargs[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) { %>
<scripttype="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中添加cdnargs[0].cdn = cdn;// 修复 Lazy loading routes Errorargs[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 压缩详见https://webpack.js.org/plugins/compression-webpack-plugin

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-loadersprependData: `@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}` : "./", // 默认'/',部署应用包时的基本 URLconfigureWebpack: 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) {// pclet 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`] = truetpl = `${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 => {// 修复HMRconfig.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中添加cdnargs[0].cdn = cdn;// 修复 Lazy loading routes Errorargs[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-loadersprependData: `@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 mapparallel: 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, // 是否启用websocketspathRewrite: {"^/api": "/"}}}}
};

 仅供参考!!


http://www.mrgr.cn/p/21078184

相关文章

STM32 DMA直接存储器存取

单片机学习&#xff01; 目录 文章目录 前言 一、DMA简介 1.1 DMA是什么 1.2 DMA作用 1.3 DMA通道 1.4 软硬件触发 1.5 芯片资源 二、存储器映像 2.1 存储器 2.2 STM32存储器 三、DMA框图 3.1 内核与存储器 3.2 寄存器 3.3 DMA数据转运 3.4 DMA总线作用 3.5 DMA请求 3.6 DMA结构…

c语言程序设计——实验报告七

实验项目名称:实验7数组的基本使用 实验项目类型:验证性 实验日期:2024年4月22日一、实验目的 1.熟练掌握数组的定义格式和数组元素的表示方法 2.熟悉数组的初始化方法和赋值方法 3.掌握字符数组存放字符串的方法和字符串函数的使用 4.熟悉数组元素的操作,特别是输入与输出…

做外贸用什么邮箱比较好?

外贸公司在推进公司业务时需要频繁进行跨国沟通&#xff0c;选择一款专业且功能强大的企业邮箱作为业务沟通工具至关重要。外贸企业邮箱需要满足5个基本内容&#xff0c;国际收发能力、安全稳定性、专业形象展示、功能完备性、客户服务与技术支持。本文将探讨做外贸时适合使用的…

【Java基础】字符串的内存情况及应用场景

String 特点 用双引号引起来的一串字符&#xff0c;字符串不变&#xff0c;它们的值在创建后不能被更改String str"hello";存储在堆中的字符串常量池已经定义过的字符串&#xff0c;再次定义时直接使用已有的字符串String类重写了**boolean equals(object obj)**方…

[附源码]秦时明月6.2魔改版_搭建架设教程_附GM工具_安卓苹果

本教程仅限学习使用,禁止商用,一切后果与本人无关,此声明具有法律效应!!!! 教程是本人亲自搭建成功的,绝对是完整可运行的,踩过的坑都给你们填上了 一. 演示视频 https://githubs.xyz/boot?app=50二. 环境 联网环境: centos7.6 , 放开所有端口 单机环境: VM虚拟机…

Pycharm的Python脚本模板

# @Time : ${DATE} ${TIME} # @Author : HeJinYang # @Description:

力扣每日一题106:从中序与后序遍历序列构造二叉树

题目 中等 相关标签 相关企业 给定两个整数数组 inorder 和 postorder &#xff0c;其中 inorder 是二叉树的中序遍历&#xff0c; postorder 是同一棵树的后序遍历&#xff0c;请你构造并返回这颗 二叉树 。 示例 1: 输入&#xff1a;inorder [9,3,15,20,7], postorder …

uniapp+vue基于移动端的药品进销存系统r275i

最后我们通过需求分析、测试调整&#xff0c;与药品进销存管理系统管理系统的实际需求相结合&#xff0c;设计实现了药品进销存管理系统管理系统。 系统功能需求包含业务需求、功能需求用户需求&#xff0c;系统功能需求分析是在了解用户习惯、开发人员技术和实力等各个因素的前…

SAP 长文本语言代码维护

在SAP中&#xff0c;我们发现长文本都是有语言代码的&#xff0c;如果需要新增一个语言代码的话&#xff0c;需要通过程序RSCPCOLA进行维护处理 具体实现步骤如下&#xff1a; 1. 输入事务码SE38&#xff0c;输入程序名RSCPCOLA&#xff0c;然后点击执行按钮 2. 维护信函语言…

Windows平台git clone文件路径太长报错

解决文件路径太长导致的报错问题问题描述 在Windows下拉取一些比较大的开源项目经常会提示文件路径太长(filename too long),然后死活都不成功 解决办法 1.配置git git config --system core.longpaths true2.修改文件C:\Program Files\Git\etc\gitconfig(需要以管理员身份…

Linux进程间通信:system V共享内存

目录 一、什么是共享内存 1.1创建共享内存 1.2释放共享内存 1.2.1shmctl 1.2.2shmat 1.2.3 shmdt 二、共享内存的实现及使用 2.1ShmClient 2.2Shm_Server 2.3Fifo.hpp 2.4Comm.hpp 一、什么是共享内存 标准系统V也叫system V的本地通信方式一般有三种&#xff1a; …

GitHub two-factor authentication开启教程

GitHub two-factor authentication开启教程问题描述 最近登录GitHub个人页面动不动就有一个提示框”...... two-factor authentication will be required for your account starting Jan 4, 2024 ......“,点击去看了一下原来是GitHub对所有的用户登录都要开启双重身份认证,…

Windows程序读取不了中文路径问题

解决win32接口无法解析中文路径的问题问题描述 今天调试发现win32接口GetFileAttributesW居然不支持中文路径,于是寻找解决方案,找了半天,尝试用boost的fileystem库发现能用,而且boost能跨平台! 不支持中文 win32接口获取文件属性,当传入参数带有中文字符时,它获取的属性…

栈和队列的4道面试题【详细解析】【代码实现】

栈和队列的面试题 1.有效的括号&#xff08;栈实现&#xff09; 题目&#xff1a; 有效的括号 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必…

紧跟生成式AI暴雨发布新时代推理服务器

近日&#xff0c;暴雨发布最新训推一体AI服务器&#xff0c;以大容量内存和灵活的高速互连选项满足各种AI应用场景&#xff0c;最大可能支持扩展插槽&#xff0c;从而大幅提升智能算力性能&#xff0c;以最优的性能和成本为企业的模型训练推理落地应用提供更好的通用算力。 AIG…

Java里的String使用

1.Java WinForm项目 public static void main(String[] args) {String testString"22";String testString2"1096";String testString3"22";Student studentnew Student();student.Age"22";Test(student.Age);Test2(student.Age); }pu…

【Qt QML】QLibrary加载共享库中的类

QLibrary是一个用于加载动态链接库&#xff08;或称为共享库&#xff09;的类。它提供了一种独立于平台的方式来访问库中的功能。 在QLibrary中&#xff0c;可以通过构造函数或setFileName()方法设置要加载的库文件名。当加载库文件时&#xff0c;QLibrary会搜索所有平台特定的…

trunk聚合

Eth-Trunk又叫以太网链路聚合Eth-Trunk,它通过将多条以太网物理链路捆绑在一起成为一条逻辑链路。达到增加链路带宽的目的。在实现增大带宽目的的同时,Eth-Trunk采用备份链路的机制,可以有效的提高设备之间链路的可靠性。每个聚合组唯一对应着一个逻辑接口,这个逻辑接口称之…

buuctf-pwn-[OGeek2019]babyrop

查看一下保护情况丢进ida里分析主函数调用了一个含有alarm的函数,这个函数会设置一个定时器,到时间自动退出程序 为了方便调试,我们直接patch掉这个函数 接着分析,主函数读入了一个随机数,并将其传入sub_804871F函数 sub_804871F函数读取输入,并检查输入的是否和随机数相…

Go 语言基础之指针、复合类型【数组、切片、指针、map、struct】

1、数组 特别需要注意的是&#xff1a;在 Go 语言中&#xff0c;数组长度也是数组类型的一部分&#xff01;所以尽管元素类型相同但是长度不同的两个数组&#xff0c;它们的类型并不相同。 1.1、数组的初始化 1.1.1、通过初始化列表{}来设置值 var arr [3]int // int类型的数…