vipbic 是一个专注前端开发、网址导航、社区讨论综合网站,该网站使用前后端分离,运用 vue-cli3 本项目配置
cnpm install
npm run dev
npm run test
npm run publish
npm run build
├── 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 配置文件
<% 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/[email protected]/dist/vue.min.js',
            'https://cdn.jsdelivr.net/npm/[email protected]/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
    })
}
new CompressionWebpackPlugin({
      algorithm: 'gzip',
      test: /\.(js|css)$/, // 匹配文件名
      threshold: 10000, // 对超过 10k 的数据压缩
      deleteOriginalAssets: false, // 不删除源文件
      minRatio: 0.8 // 压缩比
})
安装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 中插件安装栏搜索 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 也能跳转对应路径
在根目录新建
# 开发环境
NODE_ENV='development'
VUE_APP_SSO='http://http://localhost:9080'
NODE_ENV = 'production' # 如果我们在.env.test 文件中把 NODE_ENV 设置为 test 的话,那么打包出来的目录结构是有差异的
VUE_APP_MODE = 'test'
VUE_APP_SSO='http://http://localhost:9080'
outputDir = test
NODE_ENV = 'production'
VUE_APP_SSO='http://http://localhost:9080'
"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
其中响应拦截
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','参数')
const Mock = require('mockjs')
const produceNewsData = []
Mock.mock('/mock/menu', produceNewsData)
Mock 支持随机数据,具体参看官网列子 http://mockjs.com/examples.html
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
安装
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`等,`*`表示全部
                })
            ]
        }
    }
}
刷新当前页面适合在只改变了路由的 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()
具体实例 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)
    }
}
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提问
|  |      1xnode      2020-05-12 14:04:31 +08:00 收藏一下下班慢慢看 | 
|  |      3yamedie      2020-05-12 14:13:44 +08:00 收藏学习了, 这套配置 vue-cli4 同样适用吧? 另外有个奇怪的命名 succeeCode | 
|  |      4Ritter      2020-05-12 14:17:01 +08:00 good | 
|  |      5bonnenuit OP @yamedie 这个是后端返回的成功的 codo 码,比如后端返回{code:1,data:'数据',msg:'请求成功'} | 
|  |      8icharm      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> 莫名奇怪的 | 
|  |      10yazoox      2020-05-12 17:14:24 +08:00 via Android 很棒啊! 楼主,有没有 reactjs(+redux/saga, typescript) 的? | 
|  |      11feiandxs      2020-05-12 17:14:51 +08:00 脚手架多多益善 但每个人最后都还是搞了自己的一份 哈哈哈哈 | 
|      12jjianwen68      2020-05-12 17:16:25 +08:00 前端变的好复杂 | 
|      13yukiloh      2020-05-12 17:23:10 +08:00 vue-cli 网上很多资料也不写版本,2 和 3+又是不同的配置文件 我临时客串开发的时候,静态资源特别头疼,开发时候写../assets/xxx 到最后项目部署了我都不知道咋分离,只能全局替换../assets/→https://xxxx/assets 还有 publicPath: './',到底是 /还是./完全懵逼,我用 nginx 代理为 /abc 到该项目,但他根路径还是要写成./ | 
|  |      14WangXiaoyu1996      2020-05-12 17:41:59 +08:00 @icharm 这个似乎是禁用浏览器的 js 之后会展示文本 | 
|  |      15icanfork      2020-05-12 17:47:33 +08:00 | 
|      16oasis2008f      2020-05-12 17:57:06 +08:00 @belin520 哈哈 好多人可能都没用过不能运行 JS 的浏览器了 | 
|  |      17icanfork      2020-05-12 18:05:10 +08:00 @oasis2008f #16 不过好像不碍事,我遇到一些在北京大城市,拿着很高工资前端,不知道什么是同步异步,问 Vue 怎么引入一个外部 JS (其实就是 HTML 里面插入<script>最传统的方式)。但是完成工作内容是完全没有问题的。 | 
|  |      18Tlin      2020-05-12 18:37:15 +08:00 不错不错啊,下一秒就是我的了  哈哈 | 
|  |      21bonnenuit OP @jjianwen68 同感😭 | 
|  |      23bonnenuit OP  1 @yukiloh publicPath 它是在静态资源路径前面加上路径的值,publicPath:'https://www.baidu.com/', 打包完后 https://www.baidu.com/assets/xxx.js | 
|  |      24a4854857      2020-05-12 20:04:46 +08:00  1 不错的模板.收藏了 | 
|  |      29vipbic      2020-05-13 22:58:39 +08:00 很不错的一个 vue 脚手架模板 |