一份關於 vue-cli3 項目常用項配置

  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. 開啓分析打包日誌

vue.config.js

完整的架構配置

const path = require('path');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin') // 去掉註釋
const CompressionWebpackPlugin = require('compression-webpack-plugin'); // 開啓壓縮
const { HashedModuleIdsPlugin } = require('webpack');

function resolve(dir) {
    return path.join(__dirname, dir)
}

const isProduction = process.env.NODE_ENV === 'production';

// cdn預加載使用
const externals = {
    'vue': 'Vue',
    'vue-router': 'VueRouter',
    'vuex': 'Vuex',
    'axios': 'axios',
    "element-ui": "ELEMENT"
}

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',
            'https://cdn.jsdelivr.net/npm/vuex@3.0.1/dist/vuex.min.js',
            'https://cdn.jsdelivr.net/npm/axios@0.18.0/dist/axios.min.js',
            'https://unpkg.com/element-ui/lib/index.js'
        ]
    }
}

module.exports = {

    lintOnSave: false, // 關閉eslint
    productionSourceMap: false,
    publicPath: './', 
    outputDir: process.env.outputDir, // 生成文件的目錄名稱
    chainWebpack: config => {

        config.resolve.alias
            .set('@', resolve('src'))

        // 壓縮圖片
        config.module
            .rule('images')
            .test(/\.(png|jpe?g|gif|svg)(\?.*)?$/)
            .use('image-webpack-loader')
            .loader('image-webpack-loader')
            .options({ bypassOnDebug: true })

        // webpack 會默認給commonChunk打進chunk-vendors,所以需要對webpack的配置進行delete
        config.optimization.delete('splitChunks')

        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
        })
        
        config
            .plugin('webpack-bundle-analyzer')
            .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin)
    },

    configureWebpack: config => {
        const plugins = [];

        if (isProduction) {
            plugins.push(
                new UglifyJsPlugin({
                    uglifyOptions: {
                        output: {
                            comments: false, // 去掉註釋
                        },
                        warnings: false,
                        compress: {
                            drop_console: true,
                            drop_debugger: false,
                            pure_funcs: ['console.log']//移除console
                        }
                    }
                })
            )
            // 服務器也要相應開啓gzip
            plugins.push(
                new CompressionWebpackPlugin({
                    algorithm: 'gzip',
                    test: /\.(js|css)$/,// 匹配文件名
                    threshold: 10000, // 對超過10k的數據壓縮
                    deleteOriginalAssets: false, // 不刪除源文件
                    minRatio: 0.8 // 壓縮比
                })
            )

            // 用於根據模塊的相對路徑生成 hash 作爲模塊 id, 一般用於生產環境
            plugins.push(
                new HashedModuleIdsPlugin()
            )

            // 開啓分離js
            config.optimization = {
                runtimeChunk: 'single',
                splitChunks: {
                    chunks: 'all',
                    maxInitialRequests: Infinity,
                    minSize: 1000 * 60,
                    cacheGroups: {
                        vendor: {
                            test: /[\\/]node_modules[\\/]/,
                            name(module) {
                                // 排除node_modules 然後吧 @ 替換爲空 ,考慮到服務器的兼容
                                const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1]
                                return `npm.${packageName.replace('@', '')}`
                            }
                        }
                    }
                }
            };

            // 取消webpack警告的性能提示
            config.performance = {
                hints: 'warning',
                //入口起點的最大體積
                maxEntrypointSize: 1000 * 500,
                //生成文件的最大體積
                maxAssetSize: 1000 * 1000,
                //只給出 js 文件的性能提示
                assetFilter: function (assetFilename) {
                    return assetFilename.endsWith('.js');
                }
            }

            // 打包時npm包轉CDN
            config.externals = externals;
        }

        return { plugins }
    },

    pluginOptions: {
        // 配置全局less
        'style-resources-loader': {
            preProcessor: 'less',
            patterns: [resolve('./src/style/theme.less')]
        }
    },
    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
            }
        }
    }
}

html 模板配置 cdn

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta >
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
    <title><%= htmlWebpackPlugin.options.title %></title>
    <% 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" />
    <% } %>
</head>

<body>
    <noscript>
        <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled.
            Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
    <% for (var i in
        htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
    <script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
    <% } %>
</body>
</html>

開啓 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 文件

import Vue from 'vue';
import VueRouter from 'vue-router'
Vue.use(VueRouter)

import defaultRouter from './defaultRouter'
import dynamicRouter from './dynamicRouter';

import store from '@/store';

const router = new VueRouter({
    routes: defaultRouter,
    mode: 'hash',
    scrollBehavior(to, from, savedPosition) {
        // keep-alive 返回緩存頁面後記錄瀏覽位置
        if (savedPosition && to.meta.keepAlive) {
            return savedPosition;
        }
        // 異步滾動操作
        return new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve({ x: 0, y: 0 })
            }, 200)
        })
    }
})

// 消除路由重複警告
const selfaddRoutes = function (params) {
    router.matcher = new VueRouter().matcher;
    router.addRoutes(params);
}

// 全局路由攔截
router.beforeEach((to, from, next) => {
    const { hasRoute } = store.state; // 防止路由重複添加
    if (hasRoute) {
        next()
    } else {
        dynamicRouter(to, from, next, selfaddRoutes)
    }
})

export default router;

dynamicRouter.js

import http from '@/http/request';
import defaultRouter from './defaultRouter'
import store from '@/store'

// 重新構建路由對象
const menusMap = function (menu) {
    return menu.map(v => {
        const { path, name, component } = v
        const item = {
            path,
            name,
            component: () => import(`@/${component}`)
        }
        return item;
    })
}


// 獲取路由
const addPostRouter = function (to, from, next, selfaddRoutes) {
    http.windPost('/mock/menu') // 發起請求獲取路由
        .then(menu => {
            defaultRouter[0].children.push(...menusMap(menu));
            selfaddRoutes(defaultRouter);
            store.commit('hasRoute', true);
            next({ ...to, replace: true })
        })
}

export default addPostRouter;

defaultRouter.js 默認路由

const main = r => require.ensure([], () => r(require('@/layout/main.vue')), 'main')
const index = r => require.ensure([], () => r(require('@/view/index/index.vue')), 'index')
const about = r => require.ensure([], () => r(require('@/view/about/about.vue')), 'about')
const detail = r => require.ensure([], () => r(require('@/view/detail/detail.vue')), 'detail')
const error = r => require.ensure([], () => r(require('@/view/404/404.vue')), 'error');
const defaultRouter = [
    {
        path: "/", 
        component: main, // 佈局頁
        redirect: {
            name: "index"
        },
        children:[
            {
                path: '/index',
                component: index,
                name: 'index',
                meta: {
                    title: 'index'
                }
            },
            {
                path: '/about',
                component: about,
                name: 'about',
                meta: {
                    title: 'about'
                }
            },
            {
                path: '/detail',
                component: detail,
                name: 'detail',
                meta: {
                    title: 'detail'
                }
            }
        ]
    },
    {
        path: '/404',
        component: error,
        name: '404',
        meta: {
            title: '404'
        }
    }
]
export default defaultRouter;

axios 配置

import axios from "axios";
import merge from 'lodash/merge'
import qs from 'qs'

/**
 * 實例化
 * config是庫的默認值,然後是實例的 defaults 屬性,最後是請求設置的 config 參數。後者將優先於前者
 */
const http = axios.create({
    timeout: 1000 * 30,
    withCredentials: true, // 表示跨域請求時是否需要使用憑證
});

/**
 * 請求攔截
 */
http.interceptors.request.use(function (config) {
    return config;
}, function (error) {
    return Promise.reject(error);
});


/**
 * 響應攔截
 */
http.interceptors.response.use(response => {
    // 過期之類的操作
    if (response.data && (response.data.code === 401)) {
        // window.location.href = ''; 重定向
    }
    return response
}, error => {
    return Promise.reject(error)
})


/**
 * 請求地址處理
 */
http.adornUrl = (url) => {
    return url;
}

/**
 * get請求參數處理
 * params 參數對象
 * openDefultParams 是否開啓默認參數
 */
http.adornParams = (params = {}, openDefultParams = true) => {
    var defaults = {
        t: new Date().getTime()
    }
    return openDefultParams ? merge(defaults, params) : params
}


/**
 * post請求數據處理
 * @param {*} data 數據對象
 * @param {*} openDefultdata 是否開啓默認數據?
 * @param {*} contentType 數據格式
 *  json: 'application/json; charset=utf-8'
 *  form: 'application/x-www-form-urlencoded; charset=utf-8'
 */
http.adornData = (data = {}, openDefultdata = true, contentType = 'json') => {
    var defaults = {
        t: new Date().getTime()
    }
    data = openDefultdata ? merge(defaults, data) : data
    return contentType === 'json' ? JSON.stringify(data) : qs.stringify(data)
}


/**
 * windPost請求
 * @param {String} url [請求地址]
 * @param {Object} params [請求攜帶參數]
 */
http.windPost = function (url, params) {
    return new Promise((resolve, reject) => {
        http.post(http.adornUrl(url), qs.stringify(params))
            .then(res => {
                resolve(res.data)
            })
            .catch(error => {
                reject(error)
            })
    })
}


/**
 * windJsonPost請求
 * @param {String} url [請求地址]
 * @param {Object} params [請求攜帶參數]
 */
http.windJsonPost = function (url, params) {
    return new Promise((resolve, reject) => {
        http.post(http.adornUrl(url), http.adornParams(params))
            .then(res => {
                resolve(res.data)
            })
            .catch(error => {
                reject(error)
            })
    })
}


/**
 * windGet請求
 * @param {String} url [請求地址]
 * @param {Object} params [請求攜帶參數]
 */
http.windGet = function (url, params) {
    return new Promise((resolve, reject) => {
        http.get(http.adornUrl(url), { params: params })
            .then(res => {
                resolve(res.data)
            })
            .catch(error => {
                reject(error)
            })
    })
}

/**
 * 上傳圖片
 */
http.upLoadPhoto = function (url, params, callback) {
    let config = {}
    if (callback !== null) {
        config = {
            onUploadProgress: function (progressEvent) {
                //屬性lengthComputable主要表明總共需要完成的工作量和已經完成的工作是否可以被測量
                //如果lengthComputable爲false,就獲取不到progressEvent.total和progressEvent.loaded
                callback(progressEvent)
            }
        }
    }
    return new Promise((resolve, reject) => {
        http.post(http.adornUrl(url), http.adornParams(params), config)
            .then(res => {
                resolve(res.data)
            })
            .catch(error => {
                reject(error)
            })
    })
}
export default http;

添加 mock 數據

const Mock = require('mockjs')

// 獲取 mock.Random 對象
const Random = Mock.Random
// mock新聞數據,包括新聞標題title、內容content、創建時間createdTime
const produceNewsData = function () {
    let newsList = []
    for (let i = 0; i < 3; i++) {
        let newNewsObject = {}
        if(i === 0){
            newNewsObject.path = '/add/article';
            newNewsObject.name  = 'add-article';
            newNewsObject.component = 'modules/add/article/article';
        }
        if(i === 1){
            newNewsObject.path = '/detail/article';
            newNewsObject.name  = 'detail-article';
            newNewsObject.component = 'modules/detail/article/article'
        }
        if(i === 2){
            newNewsObject.path = '/edit/article';
            newNewsObject.name  = 'edit-article';
            newNewsObject.component = 'modules/edit/article/article'
        }
        newsList.push(newNewsObject)
    }
    return newsList;
}
Mock.mock('/mock/menu', produceNewsData)

配置全局 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)
}

完整代碼

https://github.com/hangjob/vue-admin

轉自:vipbic

https://segmentfault.com/a/1190000022512358

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ffUcsTnVNtTb-VinH8Llvg