webpack實戰全記錄

最近學習webpack相關知識,特此記錄下學習過的文檔以及搭建過程;如有錯誤,記得告訴我呀。項目地址:地址,求星星

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

vue項目搭建

從零開始搭建vue框架(含vue全家桶)

傳送門:相關代碼都在這裏哦!

tips: 當前項目搭建時環境及使用的部分工具版本(版本不同可能導致使用方法不同):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.安裝相關依賴:

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相關: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.創建相關文件如下:

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參數,單位byte;
                // 當文件小於limit:將文件轉爲Data URI格式內聯到引用的地方
                // 當文件大於limit:將調用 file-loader, 把文件複製到輸出目錄,並將引用的文件路徑改寫成輸出後的路徑
                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 開發環境配置文件
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: { // 開發服務器
        port: '3000',
        // open: false, // 可以設置是否每次啓動都自動打開瀏覽器頁面
        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 生產環境的配置文件
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', // 若文件內容無變化,則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"
},

現在,執行npm start即可體驗項目啦~npm run build可以打包項目

引入babel7

@babel/preset-env 語法裝換,配置 polyfill及按需加載;@babel/plugin-transform-runtime複用輔助函數

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 規範 
                "useBuiltIns": 'usage', // 默認 false, 可選 entry , usage;usage表示按需加載
            }
        ]
    ],
    plugins: [
        [
            "@babel/plugin-transform-runtime",
            {
                "corejs": false, // 默認值,可以不寫
                "helpers": true, // 默認,可以不寫
                "regenerator": false, // 通過 preset-env 已經使用了全局的 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}},則默認啓用ES6語法支持)
        //此處也可以使用年份命名的版本號:2015
        ecmaVersion: 6,
        //默認爲script
        sourceType: "module",
        //支持其他的語言特性
        ecmaFeatures: {},
        parser: "babel-eslint"
    },
    //代碼運行的環境,每個環境都會有一套預定義的全局對象,不同環境可以組合使用
    env: {
        amd: true, // 否則會出現'require' is not defined 提示
        es6: true,
        browser: true,
        jquery: true
    },
    //訪問當前源文件中未定義的變量時,no-undef會報警告。
    //如果這些全局變量是合規的,可以在globals中配置,避免這些全局變量發出警告
    globals: {
        //配置給全局變量的布爾值,是用來控制該全局變量是否允許被重寫
        test_param: true,
        window: true,
    },
    //支持第三方插件的規則,插件以eslint-plugin-作爲前綴,配置時該前綴可省略
    //檢查vue文件需要eslint-plugin-vue插件
    plugins: ["vue"],
    //集成推薦的規則
    extends: ["eslint:recommended", "plugin:vue/essential"],
    globals: {
        process: false,
    },
    //啓用額外的規則或者覆蓋默認的規則
    //規則級別分別:爲"off"(0)關閉、"warn"(1)警告、"error"(2)錯誤--error觸發時,程序退出
    rules: {
        //關閉“禁用console”規則
        "no-console": "off",
        //縮進不規範警告,要求縮進爲2個空格,默認值爲4個空格
        "indent": ["warn", 4, {
            //設置爲1時強制switch語句中case的縮進爲2個空格
            "SwitchCase": 1,
        }],
        // 函數定義時括號前面要不要有空格
        "space-before-function-paren": [0, "always"],
        //定義字符串不規範錯誤,要求字符串使用雙引號
        // quotes: ["error", "double"],
        //....
        //更多規則可查看http://eslint.cn/docs/rules/
    }
}
// webpack.base.js
// module.rules中添加
{
    test: /\.(js|vue)$/,
    loader: 'eslint-loader',
    enforce: 'pre',
    // 指定檢查的目錄
    include: [path.resolve(__dirname, '../src')],
    // eslint檢查報告的格式規範
    options: {
        formatter: require('eslint-friendly-formatter')
    }
},

運行項目,根據eslint提示修改不規範的代碼

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中出現#
    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成一下內容;並且在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', // 對應mutation.js中的mutationsMsg方法
        globalMsg: '我是修改後的全局數據~~~'
    });
};
// 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: '我是全局數據',
}
export default state;

具體使用

// test.uve
<template>
    <div>
        首頁:test
        <div>{{gettersMsg}}</div>
        <div class="btn" @click="changeMsg">點擊改變數據</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數組中增加
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>

優化

根據不同分環境配置參數、設置全局變量、優化打包信息

開發和生產環境都區分三種接口配置:本地、測試、正式

// 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, // 是否爲開發環境
    mode: 'production', // webpack mode:production、development
    domain: '', // API
    env: 'pro', // 對應接口: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': // 本地開發環境接口
            domain = 'local-api.domain.com';
            break;
        case 'pre': // 測試環境接口
            domain = 'pre-api.domain.com';
            break;
        default: // 正式環境接口
            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);
}

優化打包信息:通過webpack.base.js中config.stats、命令行中的–progress 優化

可視化打包工具測試: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文件,再在項目根目錄中執行 webpack-bundle-analyzer 後,瀏覽器會打開對應網頁可以看到相關分析結果

持久化緩存

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: { // 基礎類庫:它是構成我們項目必不可少的一些基礎類庫,比如 vue+vue-router+vuex+axios 這種標準的全家桶,它們的升級頻率都不高,但每個頁面都需要它們
                        name: 'chunk-libs',
                        test: /[\\/]node_modules[\\/]/,
                        priority: 10,
                        chunks: 'initial' // 只打包初始時依賴的第三方
                    },
                    antUI: { // UI 組件庫
                        name: 'chunk-ant', // 單獨將 ant 拆包
                        priority: 20, // 權重要大於 libs 和 app 不然會被打包進 libs 或者 app
                        test: /[\\/]node_modules[\\/]ant-design-vue[\\/]/
                    },
                    commons: { // 自定義組件/函數
                        name: 'chunk-commons',
                        test: path.resolve(__dirname, '../src/components/components-global'), // 可自定義拓展你的規則,比如註冊全局組件的目錄
                        minChunks: 2, // 最小共用次數
                        priority: 5,
                        reuseExistingChunk: true
                    },
                }
            },
            // runtimeChunk:提取 manifest,使用script-ext-html-webpack-plugin等插件內聯到index.html減少請求
            runtimeChunk: true,
            /*
            * moduleIds: 固定moduleId;使用文件路徑的hash作爲 moduleId,解決moduleId遞增變化導致的無法長期緩存問題
            * 相當於在plugins中使用new webpack.HashedModuleIdsPlugin()
            */
            moduleIds: 'hashed',
        },
        ...,
        plugins: [
            ...,
            // 注意一定要在HtmlWebpackPlugin之後引用, inline 的name 和runtimeChunk 的 name保持一致;將runtime~index.xxx.js內聯到html中
            new ScriptExtHtmlWebpackPlugin({
                inline: /runtime.*\.js$/
            }),
            // NamedChunkPlugin:結合自定義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;
                }
            })
        ]
    });
}

生產環境抽取css並壓縮優化及js壓縮

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

刪除webpack.base.js rules中關於scss和css相關的處理

// 修改webpack.base.js
...
const MiniCssExtractPlugin = require('mini-css-extract-plugin'); // 生產環境抽離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壓縮與優化
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');

...
optimization: {
    ...
    minimizer: [
        new UglifyJsPlugin({ // 壓縮js
            cache: true,
            parallel: true,
            // sourceMap: true
        }),
        new OptimizeCSSAssetsPlugin(), // 壓縮css,導致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: {
            // 將數組中的模塊作爲入口編譯成動態鏈接庫
            'vendor': ['vue', 'vue-router', 'vuex', 'vuex-router-sync', '@ant-design/icons/lib/dist']
        },
        output: {
            // 指定生成文件所在目錄
            // 由於每次打包生產環境時會清空 dist 文件夾,因此這裏我將它們存放在了dll文件夾下
            path: path.resolve(__dirname, dir),
            // 指定文件名並添加hash
            filename: '[name].[contenthash:6].dll.js',
            // 存放動態鏈接庫的全局變量名稱,加上 _dll_ 是爲了防止全局變量衝突:例如對應 vendor 來說就是 _dll_vendor
            // 這個名稱需要與 DllPlugin 插件中的 name 屬性值對應起來
            library: '_dll_[name]'
        },
        plugins: [
            new CleanWebpackPlugin(),
            // 接入 DllPlugin
            new webpack.DllPlugin({
                // 描述動態鏈接庫的 manifest.json 文件輸出時的文件名稱
                path: path.join(__dirname, dir, '[name].manifest.json'),
                // 動態鏈接庫的全局變量名稱,需要和 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 使用了哪些動態鏈接庫
    new webpack.DllReferencePlugin({
        // 描述 vendor 動態鏈接庫的文件內容
        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',
            // 腳本或鏈接標記的公共路徑
            publicPath: 'dll',
            includeSourcemap: false,
        }
    ]),
]

首次使用需要執行npm run dll構建出測試/正式環境下的dll文件,html中會自動引入相應的dll文件;以後只要沒有修改dll配置,就不需要重新構建dll文件,只有修改了webpack.dll.js,才需要重新執行npm run dll。也就是按照上面的描述配置好後,先執行npm run dll,得到dll文件夾下面的文件,之後就可以和之前一樣按照npm run dev開發、npm run build打包了

這裏之所以需要區分環境構建不同dll是因爲在mode爲production時,devtools無法查看vuex數據,尚未找到其他解決方案

使用happypack多進程加速編譯

npm i happypack -D

// 修改webpack.base.js
...
const HappyPack = require('happypack');
// 構造出共享進程池,進程池中包含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({
        // 用唯一的標識符 id 來代表當前的 HappyPack 是用來處理一類特定的文件
        id: 'babel',
        // 如何處理 .js 文件,用法和 Loader 配置中一樣
        loaders: ['babel-loader'],
        // 使用共享進程池中的子進程去處理任務
        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,因爲使用後發現vue文件中style帶scoped會失效,尚未找到原因及解決方案

這裏使用了happypack之後構建速度反而變慢了,原因???

其他小調整

// 修改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的樣式覆蓋了自定義的樣式

學習目錄

webpack相關:

遇到的問題:

以下爲本項目構建過程中遇到的疑問查找到的相關參考:

1、什麼時候用babel-polyfill,什麼時候用babel-runtime?

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

(2)babel-polyfill會污染全局空間,並可能導致不同版本間的衝突,而babel-runtime不會。從這點看應該用babel-runtime。
但記住,babel-runtime有個缺點,它不模擬實例方法,即內置對象原型上的方法,所以類似Array.prototype.find,你通過babel-runtime是無法使用的。最後,請不要一次引入全部的polyfills(如require(‘babel-polyfill’)),這會導致代碼量很大。請按需引用最好。

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

遺留問題:

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章