V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
bonnenuit
V2EX  ›  Vue.js

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

  •  
  •   bonnenuit · 2020-05-12 14:02:17 +08:00 · 4155 次点击
    这是一个创建于 1656 天前的主题,其中的信息可能已经有所发展或是发生改变。

    一份完整的 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/[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
        })
    }
    

    开启 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提问

    29 条回复    2020-05-13 22:58:39 +08:00
    xnode
        1
    xnode  
       2020-05-12 14:04:31 +08:00
    收藏一下下班慢慢看
    bonnenuit
        2
    bonnenuit  
    OP
       2020-05-12 14:06:26 +08:00
    @xnode 👌👌👌
    yamedie
        3
    yamedie  
       2020-05-12 14:13:44 +08:00
    收藏学习了, 这套配置 vue-cli4 同样适用吧?

    另外有个奇怪的命名 succeeCode
    Ritter
        4
    Ritter  
       2020-05-12 14:17:01 +08:00
    good
    bonnenuit
        5
    bonnenuit  
    OP
       2020-05-12 14:47:27 +08:00
    @yamedie 这个是后端返回的成功的 codo 码,比如后端返回{code:1,data:'数据',msg:'请求成功'}
    bonnenuit
        6
    bonnenuit  
    OP
       2020-05-12 14:48:11 +08:00
    @Ritter 🙏🙏🙏
    gz233
        7
    gz233  
       2020-05-12 16:40:43 +08:00
    @bonnenuit success code?
    icharm
        8
    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
        9
    bonnenuit  
    OP
       2020-05-12 17:03:15 +08:00
    @gz233 其实这个主要是`response.data.code`后端返回的状态码,然后我做了个全局处理
    yazoox
        10
    yazoox  
       2020-05-12 17:14:24 +08:00 via Android
    很棒啊!
    楼主,有没有 reactjs(+redux/saga, typescript) 的?
    feiandxs
        11
    feiandxs  
       2020-05-12 17:14:51 +08:00
    脚手架多多益善

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

    <noscript>标签,当浏览器不支持 JavaScript 的时候显示标签内内容
    oasis2008f
        16
    oasis2008f  
       2020-05-12 17:57:06 +08:00
    @belin520 哈哈 好多人可能都没用过不能运行 JS 的浏览器了
    belin520
        17
    belin520  
       2020-05-12 18:05:10 +08:00
    @oasis2008f #16 不过好像不碍事,我遇到一些在北京大城市,拿着很高工资前端,不知道什么是同步异步,问 Vue 怎么引入一个外部 JS (其实就是 HTML 里面插入<script>最传统的方式)。但是完成工作内容是完全没有问题的。
    Tlin
        18
    Tlin  
       2020-05-12 18:37:15 +08:00
    不错不错啊,下一秒就是我的了 哈哈
    bonnenuit
        19
    bonnenuit  
    OP
       2020-05-12 19:07:27 +08:00
    @yazoox 写长了 vue,就 react 不是那么用心了,react 配置就靠你啦😁😁
    bonnenuit
        20
    bonnenuit  
    OP
       2020-05-12 19:10:22 +08:00
    @feiandxs 前端业务越来越复杂,就一人一个脚手架了😀😀😀
    bonnenuit
        21
    bonnenuit  
    OP
       2020-05-12 19:12:37 +08:00
    @jjianwen68 同感😭
    bonnenuit
        22
    bonnenuit  
    OP
       2020-05-12 19:14:02 +08:00
    @Tlin 能给个 star 就好😋😋
    bonnenuit
        23
    bonnenuit  
    OP
       2020-05-12 19:19:14 +08:00   ❤️ 1
    @yukiloh publicPath 它是在静态资源路径前面加上路径的值,publicPath:'https://www.baidu.com/', 打包完后 https://www.baidu.com/assets/xxx.js
    a4854857
        24
    a4854857  
       2020-05-12 20:04:46 +08:00   ❤️ 1
    不错的模板.收藏了
    icharm
        25
    icharm  
       2020-05-13 09:06:47 +08:00
    @belin520 不是浏览器禁用 js 的问题, 其他页面的 js 都正常运行的
    bonnenuit
        26
    bonnenuit  
    OP
       2020-05-13 13:44:14 +08:00
    @a4854857 能帮到你最好了😋😋
    bonnenuit
        27
    bonnenuit  
    OP
       2020-05-13 13:45:54 +08:00
    @icharm 这个应该不是 vue-cli 的问题了,毕竟存在了这么长的时间,你是用的 IE 浏览器嘛
    icharm
        28
    icharm  
       2020-05-13 16:34:22 +08:00
    @bonnenuit 不是 IE,chrome 最新版,所以说很奇怪
    vipbic
        29
    vipbic  
       2020-05-13 22:58:39 +08:00
    很不错的一个 vue 脚手架模板
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1106 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 18:44 · PVG 02:44 · LAX 10:44 · JFK 13:44
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.