一份完整的 vue-cli3 项目基础配置项

2020-05-12 14:02:17 +08:00
 bonnenuit

一份完整的 vue-cli3 项目基础配置项,可用作于 PC 网站开发、移动端网页,后台管理系统

网站例子

vipbic 是一个专注前端开发、网址导航、社区讨论综合网站,该网站使用前后端分离,运用 vue-cli3 本项目配置

安装依赖

cnpm install

开发模式

npm run dev

打包测试环境

npm run test

测试和生产一起打包

npm run publish

打包生产环境

npm run build

项目配置功能

  1. 配置全局 cdn,包含 js 、css
  2. 开启 Gzip 压缩,包含文件 js 、css
  3. 去掉注释、去掉 console.log
  4. 压缩图片
  5. 本地代理
  6. 设置别名,vscode 也能识别
  7. 配置环境变量开发模式、测试模式、生产模式
  8. 请求路由动态添加
  9. axios 配置
  10. 添加 mock 数据
  11. 配置全局 less
  12. 只打包改变的文件
  13. 开启分析打包日志
  14. 拷贝文件
  15. 添加可选链运算符
  16. 配置 px 转换 rem

附加功能

  1. vue 如何刷新当前页面
  2. 封装 WebSocket
  3. 自定义指令 directive

目录结构

├── public                      静态模板资源文件
├── src                         项目文件
├──|── assets                   静态文件 img 、css 、js    
├──|── components               全局组件
├──|── http                     请求配置
├──|── layout                   布局文件
├──|── mock                     测试数据
├──|── modules                  放置动态是添加路由的页面
├──|── plugin                   插件
├──|── router                   路由
├──|── store                    vuex 数据管理
├──|── utils                    工具文件
├──|── view                     页面文件
├──|── App.vue                  
├──|── main.js                  
├── .env.development            开发模式配置
├── .env.production             正式发布模式配置
├── .env.test                   测试模式配置
├── entrance.js                 入口文件
├── vue.config.js               config 配置文件

完整代码

github

html 模板配置 cdn

<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
    <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="preload" as="style" />
    <link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet" />
<% } %>

<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
    <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>

// cdn 预加载使用
const externals = {
    'vue': 'Vue',
    'vue-router': 'VueRouter'
}
const cdn = {
    // 开发环境
    dev: {
        css: [
            'https://unpkg.com/element-ui/lib/theme-chalk/index.css'
        ],
        js: []
    },
    // 生产环境
    build: {
        css: [
            'https://unpkg.com/element-ui/lib/theme-chalk/index.css'
        ],
        js: [
            'https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.min.js',
            'https://cdn.jsdelivr.net/npm/vue-router@3.0.1/dist/vue-router.min.js'
        ]
    }
}
chainWebpack: config => {
    config.plugin('html').tap(args => {
        if (process.env.NODE_ENV === 'production') {
            args[0].cdn = cdn.build
        }
        if (process.env.NODE_ENV === 'development') {
            args[0].cdn = cdn.dev
        }
        return args
    })
}

开启 Gzip 压缩,包含文件 js 、css

new CompressionWebpackPlugin({
      algorithm: 'gzip',
      test: /\.(js|css)$/, // 匹配文件名
      threshold: 10000, // 对超过 10k 的数据压缩
      deleteOriginalAssets: false, // 不删除源文件
      minRatio: 0.8 // 压缩比
})

去掉注释、去掉 console.log

安装cnpm i uglifyjs-webpack-plugin -D

const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
new UglifyJsPlugin({
	uglifyOptions: {
		output: {
			comments: false, // 去掉注释
		},
		warnings: false,
		compress: {
			drop_console: true,
			drop_debugger: false,
			pure_funcs: ['console.log'] //移除 console
		}
	}
})

压缩图片

chainWebpack: config => {
	// 压缩图片
	config.module
		.rule('images')
		.test(/\.(png|jpe?g|gif|svg)(\?.*)?$/)
		.use('image-webpack-loader')
		.loader('image-webpack-loader')
		.options({ bypassOnDebug: true })
}

本地代理

devServer: {
	open: false, // 自动启动浏览器
	host: '0.0.0.0', // localhost
	port: 6060, // 端口号
	https: false,
	hotOnly: false, // 热更新
	proxy: {
		'^/sso': {
			target: process.env.VUE_APP_SSO, // 重写路径
			ws: true, //开启 WebSocket
			secure: false, // 如果是 https 接口,需要配置这个参数
			changeOrigin: true
		}
	}
}

设置 vscode 识别别名

在 vscode 中插件安装栏搜索 Path Intellisense 插件,打开 settings.json 文件添加 以下代码 "@": "${workspaceRoot}/src",安以下添加

{
    "workbench.iconTheme": "material-icon-theme",
    "editor.fontSize": 16,
    "editor.detectIndentation": false,
    "guides.enabled": false,
    "workbench.colorTheme": "Monokai",
    "path-intellisense.mappings": {
        "@": "${workspaceRoot}/src"
    }
}

在项目 package.json 所在同级目录下创建文件 jsconfig.json

{
    "compilerOptions": {
        "target": "ES6",
        "module": "commonjs",
        "allowSyntheticDefaultImports": true,
        "baseUrl": "./",
        "paths": {
          "@/*": ["src/*"]
        }
    },
    "exclude": [
        "node_modules"
    ]
}

如果还没看懂的客官请移步在 vscode 中使用别名 @按住 ctrl 也能跳转对应路径

配置环境变量开发模式、测试模式、生产模式

在根目录新建

.env.development

# 开发环境
NODE_ENV='development'

VUE_APP_SSO='http://http://localhost:9080'

.env.test

NODE_ENV = 'production' # 如果我们在.env.test 文件中把 NODE_ENV 设置为 test 的话,那么打包出来的目录结构是有差异的
VUE_APP_MODE = 'test'
VUE_APP_SSO='http://http://localhost:9080'
outputDir = test

.env.production

NODE_ENV = 'production'

VUE_APP_SSO='http://http://localhost:9080'

package.json

"scripts": {
    "build": "vue-cli-service build", //生产打包
    "lint": "vue-cli-service lint",
    "dev": "vue-cli-service serve", // 开发模式
    "test": "vue-cli-service build --mode test", // 测试打包
    "publish": "vue-cli-service build && vue-cli-service build --mode test" // 测试和生产一起打包
 }

请求路由动态添加

router/index.js,核心

router.beforeEach((to, from, next) => {
    const { hasRoute } = store.state; // hasRoute 设置一个状态,防止重复请求添加路由
    if (hasRoute) {
        next()
    } else {
        dynamicRouter(to, from, next, selfaddRoutes)
    }
})

如果动态添加路由抛警告,路由重复添加,需要添加 router.matcher = new VueRouter().matcher

axios 配置

其中响应拦截

const succeeCode = 1; // 成功
/**
 * 状态码判断 具体根据当前后台返回业务来定
 * @param {*请求状态码} status 
 * @param {*错误信息} err 
 */
const errorHandle = (status, err) => {
    switch (status) {
        case 401:
            vm.$message({ message: '你还未登录', type: 'warning' });
            break;
        case 404:
            vm.$message({ message: '请求路径不存在', type: 'warning' });
            break;
        default:
            console.log(err);
    }
}
/**
 * 响应拦截
 */
http.interceptors.response.use(response => {
    if (response.status === 200) {
        // 你只需改动的是这个 succeeCode,因为每个项目的后台返回的 code 码各不相同
        if (response.data.code === succeeCode) {
            return Promise.resolve(response);
        } else {
            vm.$message({ message: '警告哦,这是一条警告消息', type: 'warning' });
            return Promise.reject(response)
        }
    } else {
        return Promise.reject(response)
    }
}, error => {
    const { response } = error;
    if (response) {
        // 请求已发出,但是不在 2xx 的范围 
        errorHandle(response.status, response.data.msg);
        return Promise.reject(response);
    } else {
        // 处理断网的情况
        if (!window.navigator.onLine) {
            vm.$message({ message: '你的网络已断开,请检查网络', type: 'warning' });
        }
        return Promise.reject(error);
    }
})

http/request.js

import http from './src/http/request'
Vue.prototype.$http = http;
// 使用
this.$http.windPost('url','参数')

添加 mock 数据

const Mock = require('mockjs')
const produceNewsData = []
Mock.mock('/mock/menu', produceNewsData)

Mock 支持随机数据,具体参看官网列子 http://mockjs.com/examples.html

配置全局 less

pluginOptions: {
	// 配置全局 less
	'style-resources-loader': {
		preProcessor: 'less',
		patterns: [resolve('./src/style/theme.less')]
	}
}

只打包改变的文件

安装cnpm i webpack -D

const { HashedModuleIdsPlugin } = require('webpack');
configureWebpack: config => {	
	const plugins = [];
	plugins.push(
		new HashedModuleIdsPlugin()
	)
}

开启分析打包日志

安装cnpm i webpack-bundle-analyzer -D

chainWebpack: config => {
	config
		.plugin('webpack-bundle-analyzer')
		.use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
}

拷贝文件

安装npm i copy-webpack-plugin -D

const CopyWebpackPlugin = require('copy-webpack-plugin');
configureWebpack: config => {
    const plugins = [];
     plugins.push(
        new CopyWebpackPlugin([{ from: './NLwdLAxhwv.txt'}])
    )
}

from 为文件的路径,还有一个 to 属性是输出的文件夹路径,不写则默认复制到打包后文件的根目录

可选链运算符

安装依赖

cnpm install  @babel/plugin-proposal-optional-chaining -S

在 babel.config.js 中 的 plugins 中添加 "@babel/plugin-proposal-optional-chaining"

module.exports = {
    presets: [
        '@vue/cli-plugin-babel/preset'
    ],
    plugins: [
        '@babel/plugin-proposal-optional-chaining'
    ]
}

测试

    const obj = {
        foo: {
            bar: {
                baz: 42,
                fun: () => {
                    return 666;
                }
            }
        }
    };
    let baz = obj?.foo?.bar?.baz;
    let fun = obj?.foo?.bar?.fun();
    console.log(baz); // 42
    console.log(fun) // 666

配置 px 转换 rem

安装

cnpm i lib-flexible -S
cnpm i postcss-pxtorem -D

入口 js

import 'lib-flexible/flexible.js'

如果不需要转 rem,注释即可,也不要引入 flexible.js

css: {
    loaderOptions: {
        postcss: {
            plugins: [
                require('postcss-pxtorem')({
                    rootValue : 75, // 换算的基数 1rem = 75px 这个是根据 750px 设计稿来的,如果是 620 的就写 62
                    // 忽略转换正则匹配项。插件会转化所有的样式的 px 。比如引入了三方 UI,也会被转化。目前我使用 selectorBlackList 字段,来过滤
                    //如果个别地方不想转化 px 。可以简单的使用大写的 PX 或 Px 。
                    selectorBlackList  : ['weui','mu'], //
                    propList : ['*'], // 需要做转化处理的属性,如`hight`、`width`、`margin`等,`*`表示全部
                })
            ]
        }
    }
}

vue 如何刷新当前页面

刷新当前页面适合在只改变了路由的 id 的页面,比如查看详情页面,当路由 id 发生时候,并不会去触发当前页面的钩子函数 查看App.vue

<template>
	<div class="app">
        <router-view v-if="isRouterAlive"></router-view>
    </div>
</template>
<script>
export default {
	name: "App",
	provide() {
		return {
			reload: this.reload
		};
	},
	data() {
		return {
			isRouterAlive: true
		};
	},
	methods: {
        // 重载页面 适合添加数据或者路由 id 改变
		reload() {
			this.isRouterAlive = false;
			this.$nextTick(()=>{
                this.isRouterAlive = true;
            });
		}
	}
};
</script>

然后其它任何想刷新自己的路由页面,都可以这样: this.reload()

封装 WebSocket

具体实例 utils\websocket.js

const WSS_URL = `wss://wss.xxxx.com/ws?appid=xxx`
let setIntervalWesocketPush = null

export default class websocket {
    createSocket() {
        if (!this.Socket) {
            console.log('建立 websocket 连接')
            this.Socket = new WebSocket(WSS_URL)
            this.Socket.onopen = this.onopenWS
            this.Socket.onmessage = this.onmessageWS
            this.Socket.onerror = this.onerrorWS
            this.Socket.onclose = this.oncloseWS
        } else {
            console.log('websocket 已连接')
        }
    }

    /**打开 WS 之后发送心跳 */
    onopenWS() {
        this.sendPing() //发送心跳
    }

    /**连接失败重连 */
    onerrorWS() {
        clearInterval(setIntervalWesocketPush)
        this.Socket.close()
        createSocket() //重连
    }

    /**WS 数据接收统一处理 */
    onmessageWS(res) {
        console.log(res)
    }

    /**
     * 发送数据
     * readyState = 3  连接已经关闭或者根本没有建立
     * readyState = 2  连接正在进行关闭握手,即将关闭
     * readyState = 1  连接成功建立,可以进行通信
     * readyState = 0  正在建立连接,连接还没有完成
     * @param {*发送内容} content 
     */
    sendWSPush(content) {
        if (this.Socket !== null && this.Socket.readyState === 3) {
            this.Socket.close()
            this.createSocket() //重连
        } else if (this.Socket.readyState === 1) {
            this.Socket.send(content)
        } else if (this.Socket.readyState === 0) {
            setTimeout(() => {
                this.Socket.send(content)
            }, 5000)
        }
    }

    /**关闭 WS */
    oncloseWS() {
        clearInterval(setIntervalWesocketPush)
        console.log('websocket 已断开')
    }


    /**发送心跳 */
    sendPing() {
        this.Socket.send('ping')
        setIntervalWesocketPush = setInterval(() => {
            this.Socket.send('ping')
        }, 5000)
    }
}

自定义指令 directive

import Vue from 'vue';
const has = {
    inserted: function (el, binding) {
        // 添加指令 传入的  value
        if (!binding.value) {
            el.parentNode.removeChild(el);
        }
    }
}
Vue.directive('has',has)
	<el-button type="primary" v-has="true">主要按钮 1</el-button>

项目截图

如有疑问

github提问

4193 次点击
所在节点    Vue.js
29 条回复
xnode
2020-05-12 14:04:31 +08:00
收藏一下下班慢慢看
bonnenuit
2020-05-12 14:06:26 +08:00
@xnode 👌👌👌
yamedie
2020-05-12 14:13:44 +08:00
收藏学习了, 这套配置 vue-cli4 同样适用吧?

另外有个奇怪的命名 succeeCode
Ritter
2020-05-12 14:17:01 +08:00
good
bonnenuit
2020-05-12 14:47:27 +08:00
@yamedie 这个是后端返回的成功的 codo 码,比如后端返回{code:1,data:'数据',msg:'请求成功'}
bonnenuit
2020-05-12 14:48:11 +08:00
@Ritter 🙏🙏🙏
gz233
2020-05-12 16:40:43 +08:00
@bonnenuit success code?
icharm
2020-05-12 16:53:43 +08:00
使用 vue-cli 新建了一个默认的项目,页面有这个标签
<noscript>
<strong>We're sorry but default doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
莫名奇怪的
bonnenuit
2020-05-12 17:03:15 +08:00
@gz233 其实这个主要是`response.data.code`后端返回的状态码,然后我做了个全局处理
yazoox
2020-05-12 17:14:24 +08:00
很棒啊!
楼主,有没有 reactjs(+redux/saga, typescript) 的?
feiandxs
2020-05-12 17:14:51 +08:00
脚手架多多益善

但每个人最后都还是搞了自己的一份
哈哈哈哈
jjianwen68
2020-05-12 17:16:25 +08:00
前端变的好复杂
yukiloh
2020-05-12 17:23:10 +08:00
vue-cli 网上很多资料也不写版本,2 和 3+又是不同的配置文件
我临时客串开发的时候,静态资源特别头疼,开发时候写../assets/xxx
到最后项目部署了我都不知道咋分离,只能全局替换../assets/→https://xxxx/assets
还有 publicPath: './',到底是 /还是./完全懵逼,我用 nginx 代理为 /abc 到该项目,但他根路径还是要写成./
WangXiaoyu1996
2020-05-12 17:41:59 +08:00
@icharm 这个似乎是禁用浏览器的 js 之后会展示文本
belin520
2020-05-12 17:47:33 +08:00
@icharm #8 所以现在的前端会一把梭,但是连前端最基础的知识都忘记了(或者没有了解)

<noscript>标签,当浏览器不支持 JavaScript 的时候显示标签内内容
oasis2008f
2020-05-12 17:57:06 +08:00
@belin520 哈哈 好多人可能都没用过不能运行 JS 的浏览器了
belin520
2020-05-12 18:05:10 +08:00
@oasis2008f #16 不过好像不碍事,我遇到一些在北京大城市,拿着很高工资前端,不知道什么是同步异步,问 Vue 怎么引入一个外部 JS (其实就是 HTML 里面插入<script>最传统的方式)。但是完成工作内容是完全没有问题的。
Tlin
2020-05-12 18:37:15 +08:00
不错不错啊,下一秒就是我的了 哈哈
bonnenuit
2020-05-12 19:07:27 +08:00
@yazoox 写长了 vue,就 react 不是那么用心了,react 配置就靠你啦😁😁
bonnenuit
2020-05-12 19:10:22 +08:00
@feiandxs 前端业务越来越复杂,就一人一个脚手架了😀😀😀

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/670903

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX