webpack實戰(zhàn) -- vue全家桶

最近學(xué)習(xí)webpack相關(guān)知識,特此記錄下學(xué)習(xí)過的文檔以及搭建過程氧卧;如有錯誤该镣,記得告訴我呀廓译。項目地址:地址,求星星

// 1埃仪、clone代碼到本地
cd vue-demo
npm i
// 通過package.json的scripts可以看到區(qū)分了不同環(huán)境的啟動命令
npm run dev:local // 例如:啟動
npm run build // 打包

[TOC]

vue項目搭建

傳送門:相關(guān)代碼都在這里哦卵蛉!

tips: 當(dāng)前項目搭建時環(huán)境及使用的部分工具版本(版本不同可能導(dǎo)致使用方法不同):node v11.6.0, npm v6.10.0, webpack: ^4.35.0, webpack-cli: ^3.3.5, 其他請看package.json

初始化項目

1.新建vue-demo, cd vue-demo, npm init初始化項目颁股;

2.安裝相關(guān)依賴:

webpack: npm i webpack webpack-cli webpack-dev-server webpack-merge --save-dev

vue: npm i vue --savenpm i vue-loader vue-template-compiler --save-dev

html解析:npm i html-webpack-plugin --save-dev

css毙玻、scss相關(guān):npm i css-loader style-loader node-sass sass-loader --save-dev

css后處理:npm i postcss-loader autoprefixer --save-dev

圖片路徑處理:npm i file-loader url-loader --save-dev

以下為打包時用到的插件豌蟋,放在webpack.prod.js:

清理dist文件夾:npm i clean-webpack-plugin --save-dev

3.創(chuàng)建相關(guān)文件如下:

vue-demo
    |--build
        |--webpack.base.js
        |--webpack.dev.js
        |--webpack.prod.js
    |--src
        |--static
            |--images
            |--scss
                |--index.scss
        |--views
            |--app.vue
        |--index.js
        |--index.html
    |--postcss.config.js
    |--favicon.png
// webpack.base.js 公用配置文件
const webpack = require('webpack');
const path = require("path");
const VueLoaderPlugin = require('vue-loader/lib/plugin'); // vue-loader
const HtmlWebpackPlugin = require('html-webpack-plugin'); // html
module.exports = {
    entry: { 
        index: path.resolve(__dirname, '../src/index.js'), 
    },
    resolve: {
        alias: { // 別名
            '@src': path.resolve(__dirname, '../src'),
            '@views': path.resolve(__dirname, '../src/views'),
            '@scss': path.resolve(__dirname, '../src/static/scss'),
            '@images': path.resolve(__dirname, '../src/static/images'),
        },
        extensions: ['.js', '.vue'], // 配置擴展名
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
            {
                test: /\.(scss|css)$/,
                use: ['style-loader', 'css-loader', 'postcss-loader', 'sass-loader'],
            },
            {
                test: /\.(png|svg|jpg|jpeg|gif)$/,
                // 使用url-loader, 它接受一個limit參數(shù)廊散,單位byte桑滩;
                // 當(dāng)文件小于limit:將文件轉(zhuǎn)為Data URI格式內(nèi)聯(lián)到引用的地方
                // 當(dāng)文件大于limit:將調(diào)用 file-loader, 把文件復(fù)制到輸出目錄,并將引用的文件路徑改寫成輸出后的路徑
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 20 * 1024,
                            // 分離圖片至imgs文件夾
                            name: "imgs/[name].[ext]",
                        }
                    },
                ]
            },
        ]
    },
    plugins: [ // 插件
        new VueLoaderPlugin(),
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, '../src/index.html'), // html模板
            favicon: path.resolve(__dirname, '../favicon.png'),
        }),
    ],
};
// webpack.dev.js 開發(fā)環(huán)境配置文件
const path = require('path');
const merge = require('webpack-merge'); // 合并配置文件
const common = require('./webpack.base.js');

module.exports = merge(common, {
    mode: 'development',
    devtool: 'inline-source-map',
    devServer: { // 開發(fā)服務(wù)器
        port: '3000',
        // open: false, // 可以設(shè)置是否每次啟動都自動打開瀏覽器頁面
        contentBase: '../dist',
        host: '0.0.0.0', // 可通過IP訪問,也可以通過localhost訪問
        useLocalIp: true, // browser open with your local IP
    },
    output: { // 輸出
        filename: 'js/[name].[hash].js', // 每次保存 hash 都變化
        path: path.resolve(__dirname, '../dist')
    },
    module: {},
});
// webpack.prod.js 生產(chǎn)環(huán)境的配置文件
const path = require('path');
const merge = require('webpack-merge');
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); // 清理dist文件夾
const common = require('./webpack.base.js');

module.exports = merge(common, {
    mode: 'production',
    output: {
        filename: 'js/[name].[contenthash:8].js', // 若文件內(nèi)容無變化运准,則contenthash不變
        path: path.resolve(__dirname, '../dist')
    },
    module: {},
    plugins: [
        new CleanWebpackPlugin(),
    ],
});
// index.js
import Vue from 'vue'
import App from './views/app.vue'
import '@scss/index.scss'
new Vue({
    el: '#app',
    render: h => h(App),
});
<!-- app.vue -->
<template>
    <div id="app">
        hello, vue-demo
    </div>
</template>

<script>
export default {
    name: 'app'
}
</script>

<style lang="scss" scoped>
#app {
  text-align: center;
  color: #333;
  margin-top: 100px;
  display: flex;
}
</style>
<!-- index.html -->
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>webpack-vue-demo</title>
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>
// postcss.config.js css兼容前綴
module.exports = {
    plugins: {
        'autoprefixer': {
            overrideBrowserslist: [
                'Android 4.1',
                'iOS 7.1',
                'Chrome > 31',
                'ff > 31',
                'ie >= 8'
            ]
        }
    }
}

4.npm命令:

"scripts": {
  "start": "webpack-dev-server --hot --progress --config build/webpack.dev.js",
  "build": "webpack --progress --config build/webpack.prod.js"
},

現(xiàn)在幌氮,執(zhí)行npm start即可體驗項目啦~npm run build可以打包項目

引入babel7

@babel/preset-env 語法裝換,配置 polyfill及按需加載胁澳;@babel/plugin-transform-runtime復(fù)用輔助函數(shù)

1.安裝依賴:

npm i babel-loader @babel/core @babel/cli --save-dev

npm i @babel/preset-env @babel/plugin-transform-runtime --save-dev

npm i @babel/polyfill @babel/runtime --save

2.配置loader:

// webpack.base.js
// module.rules中添加
{
    test: /\.js$/,
    use: ['babel-loader'],
    exclude: /node_modules/, // 排除不要加載的文件夾
    include: path.resolve(__dirname, '../src') // 指定需要加載的文件夾
},

3.配置babel该互,在根目錄下添加文件.babelrc.js

module.exports = {
    presets: [
        [
            "@babel/preset-env",
            {
                "corejs": 2,
                "modules": false, // 模塊使用 es modules 组砚,不使用 commonJS 規(guī)范 
                "useBuiltIns": 'usage', // 默認(rèn) false, 可選 entry , usage宝鼓;usage表示按需加載
            }
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": false, // 默認(rèn)值蚣旱,可以不寫
                "helpers": true, // 默認(rèn)周蹭,可以不寫
                "regenerator": false, // 通過 preset-env 已經(jīng)使用了全局的 regeneratorRuntime, 不再需要 transform-runtime 提供的 不污染全局的 regeneratorRuntime
                "useESModules": true, // 使用 es modules helpers, 減少 commonJS 語法代碼
            }
        ],
    ]
}

引入其他工具及組件庫

eslint:

安裝依賴:npm i babel-eslint eslint eslint-friendly-formatter eslint-loader eslint-plugin-vue -D

// 根目錄下新建文件 .eslintrc.js:
module.exports = {
    //一旦配置了root捎稚,ESlint停止在父級目錄中查找配置文件
    root: true,
    //想要支持的JS語言選項
    parserOptions: {
        //啟用ES6語法支持(如果支持es6的全局變量{env: {es6: true}}妇押,則默認(rèn)啟用ES6語法支持)
        //此處也可以使用年份命名的版本號:2015
        ecmaVersion: 6,
        //默認(rèn)為script
        sourceType: "module",
        //支持其他的語言特性
        ecmaFeatures: {},
        parser: "babel-eslint"
    },
    //代碼運行的環(huán)境蕴坪,每個環(huán)境都會有一套預(yù)定義的全局對象聚假,不同環(huán)境可以組合使用
    env: {
        amd: true, // 否則會出現(xiàn)'require' is not defined 提示
        es6: true,
        browser: true,
        jquery: true
    },
    //訪問當(dāng)前源文件中未定義的變量時锦庸,no-undef會報警告机蔗。
    //如果這些全局變量是合規(guī)的,可以在globals中配置甘萧,避免這些全局變量發(fā)出警告
    globals: {
        //配置給全局變量的布爾值萝嘁,是用來控制該全局變量是否允許被重寫
        test_param: true,
        window: true,
    },
    //支持第三方插件的規(guī)則,插件以eslint-plugin-作為前綴扬卷,配置時該前綴可省略
    //檢查vue文件需要eslint-plugin-vue插件
    plugins: ["vue"],
    //集成推薦的規(guī)則
    extends: ["eslint:recommended", "plugin:vue/essential"],
    globals: {
        process: false,
    },
    //啟用額外的規(guī)則或者覆蓋默認(rèn)的規(guī)則
    //規(guī)則級別分別:為"off"(0)關(guān)閉牙言、"warn"(1)警告、"error"(2)錯誤--error觸發(fā)時怪得,程序退出
    rules: {
        //關(guān)閉“禁用console”規(guī)則
        "no-console": "off",
        //縮進不規(guī)范警告嬉挡,要求縮進為2個空格,默認(rèn)值為4個空格
        "indent": ["warn", 4, {
            //設(shè)置為1時強制switch語句中case的縮進為2個空格
            "SwitchCase": 1,
        }],
        // 函數(shù)定義時括號前面要不要有空格
        "space-before-function-paren": [0, "always"],
        //定義字符串不規(guī)范錯誤汇恤,要求字符串使用雙引號
        // quotes: ["error", "double"],
        //....
        //更多規(guī)則可查看http://eslint.cn/docs/rules/
    }
}
// webpack.base.js
// module.rules中添加
{
    test: /\.(js|vue)$/,
    loader: 'eslint-loader',
    enforce: 'pre',
    // 指定檢查的目錄
    include: [path.resolve(__dirname, '../src')],
    // eslint檢查報告的格式規(guī)范
    options: {
        formatter: require('eslint-friendly-formatter')
    }
},

運行項目庞钢,根據(jù)eslint提示修改不規(guī)范的代碼

vue-router

npm i @babel/plugin-syntax-dynamic-import -D

npm i vue-router -S

// .babelrc.js
module.exports = {
    plugins: [
        "@babel/plugin-syntax-dynamic-import", // 支持路由懶加載:()=>import('...')
        ...
    ],
    ...
}
// src/index.js改成如下
import '@scss/index.scss';
import Vue from 'vue';
import router from '@src/router/index.js';
import App from '@views/app.vue';

new Vue({
    el: '#app',
    router,
    render: h => h(App),
});
// 新增文件 src/router/index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import Test from '../views/test';
import NoFound from '@views/noFound';

Vue.use(VueRouter);

export default new VueRouter({
    // mode: 'history',  // 使用history防止url中出現(xiàn)#
    routes: [
        {
            path: '/',
            name: 'test',
            component: Test
        }, {
            path: '/test1',
            name: 'test1',
            component: () =>
                import(/* webpackChunkName: "test1" */ '@views/test1.vue'),
        }, {
            path: '*',
            name: 'noFound',
            component: NoFound
        }
    ]
});

新增文件 src/views/app.vue

<template>
    <div id="app">
        <div class="header">
            <router-link to="/">首頁</router-link>
            <router-link to="/test1">test1</router-link>
            <router-link to="/a">noFound</router-link>
        </div>
        <router-view></router-view>
    </div>
</template>

<script>
export default {
    name: 'app',
};
</script>

<style lang="scss" scoped>
#app {
    font-family: "Avenir", Helvetica, Arial, sans-serif;
    text-align: center;
    margin-top: 60px;
    transform: rotate(0deg);
}
</style>

新增文件 src/views/noFound.vue

<template>
    <div>
        noFound
    </div>
</template>

<script>
export default {
    name: 'noFound',
};
</script>

新增文件 src/views/test.vue

<template>
    <div>
        首頁:test
        <div>{{msg}}</div>
    </div>
</template>

<script>
export default {
    name: 'test',
    data() {
        return {
            msg: '首頁信息'
        }
    },
};
</script>

修改文件 src/views/test1.vue成一下內(nèi)容;并且在src/static/images下面新增1.jpg,2.jpg,smart.gif

<template>
    <div>
        <div>
            test1, count : 
            <span class="red">{{loading ? 'loading...' : count}}</span>
        </div>
        <div>
            <div class="btn" @click="addCount">add count</div>
        </div>
        <div class="wrap">
            <div>
                <div>gif</div>
                <img src="@images/smart.gif" alt="">
            </div>
            <div>
                <div>1</div>
                <img src="@images/1.jpg" alt="">
            </div>
            <div>
                <div>2</div>
                <img src="@images/2.jpg" alt="">
            </div>
            <div>
                <div>3</div>
                <div class="img-bg-1"></div>
            </div>
            <div>
                <div>4</div>
                <div class="img-bg-2"></div>
            </div>
        </div>
    </div>
</template>

<script>
export default {
    name: 'test',
    data() {
        return {
            loading: false,
            count: 1,
        }
    },
    methods: {
        addCount() {
            if (this.loading) return ;
            return new Promise((resolve) => {
                this.loading = true;
                setTimeout(() => {
                    const {count} = this;
                    this.count = count + 1;
                    this.loading = false;
                    resolve();
                }, 2000);
            });
        },
    },
};
</script>

<style lang="scss" scoped>
$red: #a00;
.red {
    color: $red;
}
img {
    width: 100px;
}
@mixin img-bg {
    width: 100%;
    height: 120px;
    background-size: 100px;
}
.img-bg-1 {
    background: url(~@images/1.jpg) no-repeat center top;
    @include img-bg();
}
.img-bg-2 {
    background: url(~@images/2.jpg) no-repeat center top;
    @include img-bg();
}
.wrap {
    display: flex;
    &>div {
        flex: 1;
    }
}
</style>

修改src/static/scss/index.scss

.btn {
    display: inline-block;
    padding: 5px 10px;
    border: 1px solid #ddd;
    border-radius: 4px;
    cursor: pointer;
}

vuex

npm i vuex vuex-router-sync -S

// 修改src/index.js
...
import { sync } from 'vuex-router-sync';
import store from '@src/store/index';

// 鏈接vuex和vue-router
sync(store, router);

new Vue({
    ...
    store, // 新增這一行
    ...
});

新增如下文件:

|--src
    |--store
        |--actions.js
        |--getters.js
        |--index.js
        |--mutations.js
        |--state.js
// actions.js
export const changeMsg = ({ commit }) => {
    commit({
        type: 'mutationsMsg', // 對應(yīng)mutation.js中的mutationsMsg方法
        globalMsg: '我是修改后的全局?jǐn)?shù)據(jù)~~~'
    });
};
// getters.js
export const gettersMsg = state => state.globalMsg;
// index.js
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as mutations from './mutations';
import * as getters from './getters';
import state from './state';
Vue.use(Vuex);
const store = new Vuex.Store({
    state,
    getters,
    actions,
    mutations
});
export default store;
// mutations.js
export const mutationsMsg = (state, payload) => {
    state.globalMsg = payload.globalMsg;
}
// state.js
const state = {
    globalMsg: '我是全局?jǐn)?shù)據(jù)',
}
export default state;

具體使用

// test.uve
<template>
    <div>
        首頁:test
        <div>{{gettersMsg}}</div>
        <div class="btn" @click="changeMsg">點擊改變數(shù)據(jù)</div>
    </div>
</template>

<script>
import { mapGetters, mapActions } from 'vuex';
export default {
    name: 'test',
    data() {
        return {};
    },
    computed: { ...mapGetters(['gettersMsg']) },
    methods: { ...mapActions(['changeMsg']) }
};
</script>

antd

npm i babel-plugin-import less less-loader -D

npm i ant-design-vue -S

// .babelrc.js plugins數(shù)組中增加
plugins: [
    ...,
    ["import", { "libraryName": "ant-design-vue", "libraryDirectory": "es", "style": true }], // ant組件按需加載
]
// webpack.base.js rules中加入
{
    test: /\.less$/,
    use: [
        'style-loader',
        'css-loader',
        {
            loader: 'less-loader', // compiles Less to CSS
            options: { // ant自定義主題
                modifyVars: {
                    'primary-color': '#63937d',
                    'link-color': '#11b96c',
                    'item-hover-bg': '#547c6a',
                    'item-active-bg': '#466657',
                },
                javascriptEnabled: true,
            },
        }
    ]
}
// src/index.js 新增一句
import '@src/plugins';
// 新增文件 src/plugins/index.js
import './ant';
// 新增文件 src/plugins/ant/index.js
import Vue from 'vue';
import {
    Button,
    Icon,
    Layout,
    Breadcrumb,
    Dropdown,
    Divider,
    Menu,
    Pagination,
    Steps,
    Checkbox,
    DatePicker,
    Input,
    InputNumber,
    Radio,
    Select,
    Switch,
    TimePicker,
    Popover,
    Tabs,
    Tag,
    Tooltip,
    Alert,
    message,
    Modal,
    Popconfirm,
    Spin,
    ConfigProvider,
    LocaleProvider
} from 'ant-design-vue';

Vue.use(Button);
Vue.use(Icon);
Vue.use(Layout);
Vue.use(Breadcrumb);
Vue.use(Dropdown);
Vue.use(Divider);
Vue.use(Menu);
Vue.use(Pagination);
Vue.use(Steps);
Vue.use(Checkbox);
Vue.use(DatePicker);
Vue.use(Input);
Vue.use(InputNumber);
Vue.use(Radio);
Vue.use(Select);
Vue.use(Switch);
Vue.use(TimePicker);
Vue.use(Popover);
Vue.use(Tabs);
Vue.use(Tag);
Vue.use(Tooltip);
Vue.use(Alert);
Vue.use(Modal);
Vue.use(Popconfirm);
Vue.use(Spin);
Vue.use(ConfigProvider);
Vue.use(LocaleProvider);
message.config({
    duration: 2,
    maxCount: 3,
});
Vue.prototype.$message = message;
Vue.prototype.$Modal = Modal;

使用

// 修改views/test.vue
<a-button type="primary">點擊</a-button>

優(yōu)化

根據(jù)不同分環(huán)境配置參數(shù)因谎、設(shè)置全局變量基括、優(yōu)化打包信息

開發(fā)和生產(chǎn)環(huán)境都區(qū)分三種接口配置:本地、測試财岔、正式

// package.json scripts改成:
"dev:local": "webpack-dev-server --env.CUSTOM_ENV=local --hot --progress --config build/webpack.dev.js",
"dev:pre": "webpack-dev-server --env.CUSTOM_ENV=pre --hot --progress --config build/webpack.dev.js",
"dev": "webpack-dev-server --env.CUSTOM_ENV=pro --hot --progress --config build/webpack.dev.js",
"build:local": "webpack --progress --env.CUSTOM_ENV=local --config build/webpack.prod.js",
"build:pre": "webpack --progress --env.CUSTOM_ENV=pre --config build/webpack.prod.js",
"build": "webpack --progress --env.CUSTOM_ENV=pro --config build/webpack.prod.js"
// .eslintrc.js 新增配置
modules.exports = {
    ...
    globals: {
        ...
        process: false,
    },
    ...
}

新增文件 build/_config.js

let config = {
    isDev: false, // 是否為開發(fā)環(huán)境
    mode: 'production', // webpack mode:production风皿、development
    domain: '', // API
    env: 'pro', // 對應(yīng)接口:local、pre匠璧、pro
};

module.exports = (env, mode) => {
    config.isDev = mode === 'development';
    config.mode = mode;
    config.env = env.CUSTOM_ENV;
    let domain = '';
    switch(env.CUSTOM_ENV) {
        case 'local': // 本地開發(fā)環(huán)境接口
            domain = 'local-api.domain.com';
            break;
        case 'pre': // 測試環(huán)境接口
            domain = 'pre-api.domain.com';
            break;
        default: // 正式環(huán)境接口
            domain = 'api.domain.com';
    }
    config.domain = domain;
    return config;
};
// 修改webpack.base.js
const getConfig = require('./_config');

module.exports = (env, mode) => {
    const envConfig = getConfig(env, mode);
    return {
        stats: {
            children: false, // 清理控制臺不必要的打印信息
        },
        // 沿用之前的配置
        entry: { 
            ...
        },
        ...
        plugins: [
            ...,
            new webpack.DefinePlugin({ // 自定義全局變量
                'process.env.CUSTOM_ISDEV': JSON.stringify(envConfig.isDev),
                'process.env.CUSTOM_MODE': JSON.stringify(envConfig.mode),
                'process.env.CUSTOM_DOMAIN': JSON.stringify(envConfig.domain),
                'process.env.CUSTOM_ENV': JSON.stringify(envConfig.env),
            }),
        ]
    }
}
// 修改webpack.dev.js
module.exports = env => {
    const mode = 'development';
    const commonConfig = common(env, mode);
    return merge(commonConfig, {
        mode,
        devtool: 'inline-source-map',
        ...
    }
}
// 修改webpack.prod.js
module.exports = env => {
    const mode = 'production';
    const commonConfig = common(env, mode);
    return merge(commonConfig, {
        mode,
        ...
    }
}
// src/index.js 新增
if (process.env.CUSTOM_MODE !== 'production') {
    console.log('CUSTOM_ISDEV:', process.env.CUSTOM_ISDEV);
    console.log('CUSTOM_MODE:', process.env.CUSTOM_MODE);
    console.log('CUSTOM_DOMAIN:', process.env.CUSTOM_DOMAIN);
    console.log('CUSTOM_ENV:', process.env.CUSTOM_ENV);
}

優(yōu)化打包信息:通過webpack.base.js中config.stats桐款、命令行中的--progress 優(yōu)化

可視化打包工具測試:webpack-bundle-analyzer

npm i webpack-bundle-analyzer -D

package.json的scripts中增加:

"build:stats": "webpack --progress --env.CUSTOM_ENV=pro --env.STATS --config build/webpack.prod.js --profile --json > stats.json"

修改webpack.prod.js:

...
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

...
plugins: [
    ...,
    // 分析包大小
    ...(env.STATS ? [new BundleAnalyzerPlugin()] : [])
]
...

使用:npm run build:stats生成stats.json文件,再在項目根目錄中執(zhí)行 webpack-bundle-analyzer 后夷恍,瀏覽器會打開對應(yīng)網(wǎng)頁可以看到相關(guān)分析結(jié)果

持久化緩存

npm i script-ext-html-webpack-plugin -D

// 修改webpack.dev.js
const webpack = require('webpack');

plugins: [
    ...,
    new webpack.NamedModulesPlugin(), // 將文件路徑作為 id
]
// 修改webpack.prod.js
...
const webpack = require('webpack');
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
const seen = new Set();
const nameLength = 4;

module.exports = env => {
    ...
    return merge(commonConfig, {
        ...
        optimization: {
            splitChunks: {
                chunks: 'all',
                cacheGroups: {
                    libs: { // 基礎(chǔ)類庫:它是構(gòu)成我們項目必不可少的一些基礎(chǔ)類庫魔眨,比如 vue+vue-router+vuex+axios 這種標(biāo)準(zhǔn)的全家桶,它們的升級頻率都不高,但每個頁面都需要它們
                        name: 'chunk-libs',
                        test: /[\\/]node_modules[\\/]/,
                        priority: 10,
                        chunks: 'initial' // 只打包初始時依賴的第三方
                    },
                    antUI: { // UI 組件庫
                        name: 'chunk-ant', // 單獨將 ant 拆包
                        priority: 20, // 權(quán)重要大于 libs 和 app 不然會被打包進 libs 或者 app
                        test: /[\\/]node_modules[\\/]ant-design-vue[\\/]/
                    },
                    commons: { // 自定義組件/函數(shù)
                        name: 'chunk-commons',
                        test: path.resolve(__dirname, '../src/components/components-global'), // 可自定義拓展你的規(guī)則遏暴,比如注冊全局組件的目錄
                        minChunks: 2, // 最小共用次數(shù)
                        priority: 5,
                        reuseExistingChunk: true
                    },
                }
            },
            // runtimeChunk:提取 manifest侄刽,使用script-ext-html-webpack-plugin等插件內(nèi)聯(lián)到index.html減少請求
            runtimeChunk: true,
            /*
            * moduleIds: 固定moduleId;使用文件路徑的hash作為 moduleId朋凉,解決moduleId遞增變化導(dǎo)致的無法長期緩存問題
            * 相當(dāng)于在plugins中使用new webpack.HashedModuleIdsPlugin()
            */
            moduleIds: 'hashed',
        },
        ...,
        plugins: [
            ...,
            // 注意一定要在HtmlWebpackPlugin之后引用, inline 的name 和runtimeChunk 的 name保持一致;將runtime~index.xxx.js內(nèi)聯(lián)到html中
            new ScriptExtHtmlWebpackPlugin({
                inline: /runtime.*\.js$/
            }),
            // NamedChunkPlugin:結(jié)合自定義nameResolver固定chunkId
            new webpack.NamedChunksPlugin(chunk => {
                if (chunk.name) {
                    return chunk.name;
                }
                const modules = Array.from(chunk.modulesIterable);
                if (modules.length > 1) {
                    const hash = require('hash-sum');
                    const joinedHash = hash(modules.map(m => m.id).join('_'));
                    let len = nameLength;
                    while (seen.has(joinedHash.substr(0, len))) len++;
                    seen.add(joinedHash.substr(0, len));
                    return `chunk-${joinedHash.substr(0, len)}`;
                } else {
                    return modules[0].id;
                }
            })
        ]
    });
}

生產(chǎn)環(huán)境抽取css并壓縮優(yōu)化及js壓縮

npm i mini-css-extract-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin -D

刪除webpack.base.js rules中關(guān)于scss和css相關(guān)的處理

// 修改webpack.base.js
...
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 生產(chǎn)環(huán)境抽離css
module.exports = (env, mode) => {
    const envConfig = getConfig(env, mode);
    const isDev = envConfig.isDev;
    return {
        ...,
        module: {
            ...,
            rules: [
                ...,
                {
                    test: /\.(scss|css)$/,
                    include: [
                        path.resolve(__dirname, '../src')
                    ],
                    use: [
                        isDev ? 'style-loader' : {
                            loader: MiniCssExtractPlugin.loader,
                            options: {
                                publicPath: '../' // 讓css能成功加載到圖片
                            }
                        },
                        'css-loader', 'postcss-loader', 'sass-loader'
                    ],
                },
                {
                    test: /\.less$/,
                    use: [
                        isDev ? 'style-loader' : {
                            loader: MiniCssExtractPlugin.loader,
                            options: {
                                publicPath: '../'
                            }
                        },
                        'css-loader',
                        'postcss-loader',
                        {
                            loader: 'less-loader',
                            options: { // ant自定義主題
                                modifyVars: {
                                    'primary-color': '#000000',
                                    'link-color': '#17c9e6',
                                    'item-hover-bg': '#F7F7F7',
                                    'item-active-bg': '#f3f3f3',
                                },
                                javascriptEnabled: true,
                            },
                        }
                    ],
                }
            ]
        },
        ...
    }
}
// 修改webpack.prod.js
...
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 抽離css
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); // css壓縮與優(yōu)化
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

...
optimization: {
    ...
    minimizer: [
        new UglifyJsPlugin({ // 壓縮js
            cache: true,
            parallel: true,
            // sourceMap: true
        }),
        new OptimizeCSSAssetsPlugin(), // 壓縮css州丹,導(dǎo)致webpack4自帶的js壓縮無效,需添加UglifyJsPlugin
    ],
},
...
plugins: [
    ...
    // 增加css抽取
    new MiniCssExtractPlugin({
        filename: 'css/[name].[contenthash:8].css',
        // chunkFilename: 'css/[id].[contenthash:8].css'
    }),
]

使用DllPlugin和DllReferencePlugin提升編譯速度

npm i add-asset-html-webpack-plugin -D

package.json scripts中增加:

"dll": "npm run _dll:pro && npm run _dll:dev",
"_dll:pro": "webpack --mode production --progress --config build/webpack.dll.js",
"_dll:dev": "webpack --mode development --progress --config build/webpack.dll.js",
// 新增文件 build/webpack.dll.js
const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = (env, argv) => {
    const isDev = argv.mode === 'development';
    const dir = isDev ? '../dll/dev' : '../dll/pro';
    return {
        // mode: 'production', // ???????
        entry: {
            // 將數(shù)組中的模塊作為入口編譯成動態(tài)鏈接庫
            'vendor': ['vue', 'vue-router', 'vuex', 'vuex-router-sync', '@ant-design/icons/lib/dist']
        },
        output: {
            // 指定生成文件所在目錄
            // 由于每次打包生產(chǎn)環(huán)境時會清空 dist 文件夾杂彭,因此這里我將它們存放在了dll文件夾下
            path: path.resolve(__dirname, dir),
            // 指定文件名并添加hash
            filename: '[name].[contenthash:6].dll.js',
            // 存放動態(tài)鏈接庫的全局變量名稱墓毒,加上 _dll_ 是為了防止全局變量沖突:例如對應(yīng) vendor 來說就是 _dll_vendor
            // 這個名稱需要與 DllPlugin 插件中的 name 屬性值對應(yīng)起來
            library: '_dll_[name]'
        },
        plugins: [
            new CleanWebpackPlugin(),
            // 接入 DllPlugin
            new webpack.DllPlugin({
                // 描述動態(tài)鏈接庫的 manifest.json 文件輸出時的文件名稱
                path: path.join(__dirname, dir, '[name].manifest.json'),
                // 動態(tài)鏈接庫的全局變量名稱,需要和 output.library 中保持一致
                // 該字段的值也就是輸出的 manifest.json 文件 中 name 字段的值
                // 例如 venfor.manifest.json 中就有 "name": "venfor_dll"
                name: '_dll_[name]'
            })
        ]
    }
}
// 修改webpack.base.js
...
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin');

...
plugins: [
    // 告訴 Webpack 使用了哪些動態(tài)鏈接庫
    new webpack.DllReferencePlugin({
        // 描述 vendor 動態(tài)鏈接庫的文件內(nèi)容
        manifest: require(isDev ? '../dll/dev/vendor.manifest.json' : '../dll/pro/vendor.manifest.json')
    }),
    ...,
    // 在HtmlWebpackPlugin后使用:用于將vendor插入打包后的頁面亲怠,并將vendor移動到dist文件夾下面
    new AddAssetHtmlPlugin([
        {
            // 要添加到編譯中的文件的絕對路徑
            filepath: path.resolve(__dirname, isDev ? '../dll/dev/vendor.*.dll.js' : '../dll/pro/vendor.*.dll.js'), // 匹配到帶hash的文件
            // 文件輸出目錄:會在dist文件夾下面再生成dll文件夾
            outputPath: 'dll',
            // 腳本或鏈接標(biāo)記的公共路徑
            publicPath: 'dll',
            includeSourcemap: false,
        }
    ]),
]

首次使用需要執(zhí)行npm run dll構(gòu)建出測試/正式環(huán)境下的dll文件蚁鳖,html中會自動引入相應(yīng)的dll文件;以后只要沒有修改dll配置赁炎,就不需要重新構(gòu)建dll文件醉箕,只有修改了webpack.dll.js,才需要重新執(zhí)行npm run dll徙垫。也就是按照上面的描述配置好后讥裤,先執(zhí)行npm run dll,得到dll文件夾下面的文件姻报,之后就可以和之前一樣按照npm run dev開發(fā)己英、npm run build打包了

這里之所以需要區(qū)分環(huán)境構(gòu)建不同dll是因為在mode為production時,devtools無法查看vuex數(shù)據(jù)吴旋,尚未找到其他解決方案

使用happypack多進程加速編譯

npm i happypack -D

// 修改webpack.base.js
...
const HappyPack = require('happypack');
// 構(gòu)造出共享進程池损肛,進程池中包含5個子進程
const happyThreadPool = HappyPack.ThreadPool({ size: 5 });

rules: [
    ...
    // 修改babel-loader
    {
        test: /\.js$/,
        use: 'happypack/loader?id=babel',
        exclude: /node_modules/,
        include: path.resolve(__dirname, '../src')
    },
    {
        test: /\.less$/,
        use: [
            isDev ? 'style-loader' : {
                loader: MiniCssExtractPlugin.loader,
                options: {
                    publicPath: '../'
                }
            },
            'happypack/loader?id=less'
        ],
    }
],
...
plugins: [
    new HappyPack({
        // 用唯一的標(biāo)識符 id 來代表當(dāng)前的 HappyPack 是用來處理一類特定的文件
        id: 'babel',
        // 如何處理 .js 文件,用法和 Loader 配置中一樣
        loaders: ['babel-loader'],
        // 使用共享進程池中的子進程去處理任務(wù)
        threadPool: happyThreadPool,
    }),
    new HappyPack({
        id: 'less',
        loaders: ['css-loader', 'postcss-loader', {
                loader: 'less-loader',
                options: { // ant自定義主題
                    modifyVars: {
                        'primary-color': '#000000',
                        'link-color': '#17c9e6',
                        'item-hover-bg': '#F7F7F7',
                        'item-active-bg': '#f3f3f3',
                    },
                    javascriptEnabled: true,
                },
            }
        ],
        threadPool: happyThreadPool,
    }),
    ...
]

tips:這里沒有對scss的處理使用happypack荣瑟,因為使用后發(fā)現(xiàn)vue文件中style帶scoped會失效治拿,尚未找到原因及解決方案

這里使用了happypack之后構(gòu)建速度反而變慢了,原因笆焰?劫谅??

其他小調(diào)整

// 修改webpack.base.js
...
performance: { // 控制 webpack 如何通知「資源(asset)和入口起點超過指定文件限制」
    hints: 'warning',
    maxAssetSize: (isDev ? 20 : 1) * 1024 * 1024, // 單文件:bytes
    maxEntrypointSize: (isDev ? 20 : 3) * 1024 * 1024, // 入口所有文件:bytes
},
...,
resolve: {
    ...,
    modules: [path.resolve(__dirname, '../node_modules')], // 使用絕對路徑指明第三方模塊存放的位置嚷掠,以減少搜索步驟
},

src/index.js中要把import '@src/plugins';放到import '@scss/index.scss';前面捏检,才不會讓ant的樣式覆蓋了自定義的樣式

學(xué)習(xí)目錄

<font size=3 color=#63937d>webpack相關(guān):</font>

<font size=3 color=#63937d>遇到的問題:</font>

以下為本項目構(gòu)建過程中遇到的疑問查找到的相關(guān)參考:

1能犯、什么時候用babel-polyfill鲫骗,什么時候用babel-runtime?

(1)transform-runtime不會污染全局悲雳,但是不能使用實例方法挎峦,如Array.find

(2)babel-polyfill會污染全局空間香追,并可能導(dǎo)致不同版本間的沖突合瓢,而babel-runtime不會透典。從這點看應(yīng)該用babel-runtime。
但記住峭咒,babel-runtime有個缺點,它不模擬實例方法凑队,即內(nèi)置對象原型上的方法,所以類似Array.prototype.find漩氨,你通過babel-runtime是無法使用的西壮。最后,請不要一次引入全部的polyfills(如require('babel-polyfill'))叫惊,這會導(dǎo)致代碼量很大款青。請按需引用最好。

(3)按需引入polyfill存在風(fēng)險霍狰,可能無法為某些第三方組件提供其依賴的polyfill:https://juejin.im/post/5cb9833b6fb9a068a84fe4d0,

遺留問題:

1抡草、依賴包中tree-shaking后的依賴文件能夠被編譯嗎,文件中使用的es6新特性能夠被polyfill檢測到蔗坯?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末康震,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子宾濒,更是在濱河造成了極大的恐慌签杈,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鼎兽,死亡現(xiàn)場離奇詭異答姥,居然都是意外死亡,警方通過查閱死者的電腦和手機谚咬,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門鹦付,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人择卦,你說我怎么就攤上這事敲长±杉蓿” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵祈噪,是天一觀的道長泽铛。 經(jīng)常有香客問我,道長盔腔,這世上最難降的妖魔是什么月褥? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮舀透,結(jié)果婚禮上决左,老公的妹妹穿的比我還像新娘。我一直安慰自己惑芭,他們只是感情好挚躯,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布码荔。 她就那樣靜靜地躺著,像睡著了一般越败。 火紅的嫁衣襯著肌膚如雪硼瓣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天亿傅,我揣著相機與錄音瘟栖,去河邊找鬼。 笑死酬滤,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的盯串。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼冠摄,長吁一口氣:“原來是場噩夢啊……” “哼耗拓!你這毒婦竟也來了奏司?” 一聲冷哼從身側(cè)響起樟插,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎搪缨,沒想到半個月后鸵熟,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡痹届,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年队腐,在試婚紗的時候發(fā)現(xiàn)自己被綠了奏篙。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡为严,死狀恐怖第股,靈堂內(nèi)的尸體忽然破棺而出盹靴,到底是詐尸還是另有隱情瑞妇,我是刑警寧澤梭冠,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布控漠,位于F島的核電站,受9級特大地震影響偶翅,放射性物質(zhì)發(fā)生泄漏碉渡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一形导、第九天 我趴在偏房一處隱蔽的房頂上張望习霹。 院中可真熱鬧,春花似錦阎曹、人聲如沸煞檩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽桐早。三九已至,卻和暖如春友存,著一層夾襖步出監(jiān)牢的瞬間陶衅,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工膨俐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人焚刺。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓乳愉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蔓姚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容