从零开始 Webpack 4 配置 Vue项目

2019-06-27

超详细!看不懂就来搬代码好了! 怎么可能看不懂~

我们不生产代码,我们只是代码的搬运工。 ——鲁迅

想要详细代码戳我戳我(●’◡’●)

创建项目

打开Terminal并跳转到你希望的目录下,然后创建一个新的文件夹。

> mkdir vue-init

进入该目录。

> cd vue-init

npm 初始化。-y | --yes 默认所有配置项都为yes。

> npm init -y

安装webpack相关包

webpack:前端打包工具
webpack-cli:在webpack 3中,webpack和CLI都在同一个包中,但在第4版中,两者被分开了,因此这里必须安装CLI
webpack-dev-server:用于起一个本地服务器
webpack-merge:用于合并webpack基础配置项

-D | --save-dev将包记录到"devDependencies"中,
-S | --save将包记录到"dependencies"中,
两者的区别即是否会在生产环境中被使用。若在生产环境中需要用到,则记录到dependencies中,反之则记录到devDependencies中。

> npm i webpack webpack-cli webpack-dev-server webpack-merge -D

安装其他需要用到的包

Vue

Vue文件解析

> npm i vue-loader vue-template-compiler  -D

Vue、VueRouter及Axios

> npm i vue vue-router axios -S

Javascript

Js语法解析(如ES6、ES7等浏览器暂不支持的语法)

> npm i @babel/core @babel/preset-env babel-loader -D

Js文件压缩

> npm i uglifyjs-webpack-plugin -D

Style

Css Loader

> npm i css-loader style-loader -D

Sass Loader

> npm i node-sass sass-loader -D

Postcss Loder & Autoprefixer

> npm i postcss-loader autoprefixer -D

分离、压缩Css文件

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

图片

图片loader

> npm i file-loader url-loader -D

图片压缩

> npm i image-webpack-loader -D

其他

HTML模板解析

> npm i html-webpack-plugin -D

清除dist文件夹

> npm i clean-webpack-plugin -D

项目目录

为之后行文方便,在此把项目全部目录列出来。
可以先把同名文件和文件夹创建好,占坑~

vue-init
|-- build
    |-- webpack.base.js
    |-- webpack.dev.js
    |-- webpack.prod.js
|-- dist
|-- public
    |-- json
        |-- pictureList.json
    |-- index.html
    |-- mocker.js
|-- src
    |-- entry
        |-- index.js
    |-- imgs
    |-- pages
        |-- components
            |-- nav.vue
        |-- App.vue
        |-- Index.vue
        |-- List.vue
    |-- style
        |-- app.scss
        |-- common.scss
        |-- index.scss
        |-- nav.scss
|-- .babelrc
|-- .gitignore
|-- package-lock.json
|-- package.json
|-- postcss.config.js

编写webapck配置

url-loader对未设置或者小于limit设置的图片进行转换,以base64的格式被img的src所使用;而对于大于limit byte的图片用file-loader进行解析。
图片打包成base64可以减少网络请求,但代价是样式文件将变大。
因此需要根据实际情况进行取舍。

// webpack.base.js
// 存放通用配置

const webpack = require('webpack'),
    path = require('path'),
    // 用于解析vue文件
    VueLoaderPlugin = require('vue-loader/lib/plugin'),
    // html文件插件,将静态文件引入html中 
    HTMLWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: './src/entry/index.js', // 项目的入口文件
    module: {
        rules:[
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            },
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
            {
                test: /\.(sa|sc|c)ss$/,
                use: [
                    'style-loader',
                    'css-loader',
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.(png|jpg|svg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 5000,
                            name: 'imgs/[name].[ext]'
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new VueLoaderPlugin(),
        // 自动将静态资源引入html文件中
        new HTMLWebpackPlugin({
            template: path.resolve(__dirname, '../public/index.html')
        })
    ]
};

devServer中是开发时所需的重要配置。

// webpack.dev.js
// development环境

const merge = require('webpack-merge'),
    common = require('./webpack.base'),
    path = require('path'),
    apiMocker = require('webpack-api-mocker');

module.exports = merge(common, {
    mode: 'development',
    devtool: 'inline-source-map',
    devServer: {
        // 入口页面所在文件夹
        contentBase: '../public',
        // 将所有请求都打回index.html,因为此处配置是单页面应用
        historyApiFallback: true,
        // 本地mock数据配置,mock数据配置在mocker.js中
        before: function(app) {
            apiMocker(app, path.resolve('public/mocker.js'));
        },
        // 监听端口
        port:8889,
        // 启动后自动打开网址
        open: 'http://localhost:8889/',
        // 让同网域其他设备访问本地服务
        host:'0.0.0.0'
    },
    output: {
        filename: 'js/[name].[hash:5].js',
        path: path.resolve(__dirname, '../dist')
    }
});

webpack.prod.js文件中新增了很多包,使得js和css文件、图片更加规整简洁。

// webpack.prod.js
// production
const merge = require('webpack-merge'),
    common = require('./webpack.base'),
    path = require('path'),
    // 清除dist文件夹
    {CleanWebpackPlugin} = require('clean-webpack-plugin'),
    // 处理、打包css文件 功能类似style-loader
    MiniCssExtractPlugin = require('mini-css-extract-plugin'),
    // 用于压缩css文件
    OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin"),
    // 压缩js文件
    UglifyJsPlugin = require("uglifyjs-webpack-plugin");

module.exports = merge(common, {
    mode: 'production',
    output: {
        filename: 'js/[name].[hash:5].js',
        path: path.resolve(__dirname, '../dist')
    },
    optimization: {
        // 将引入的第三方库文件单独打包
        splitChunks: {
            // 所有引入的文件都打包
            chunks: 'all',
            cacheGroups: {
                vendor: {
                    name: 'vendor',
                    test: /[\\/]node_module[\\/]/,
                    priority: -10,
                    chunks: 'initial'
                }
            }
        }
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            },
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
            {
                test: /\.(sa|sc|c)ss$/,
                use: [
                    {
                        // 使用MiniCssExtractPlugin.loader替代style-loader
                        loader: MiniCssExtractPlugin.loader,
                        options: {
                            // 解决图片路径不对的问题
                            publicPath: '../'
                        }
                    },
                    'css-loader',
                    'postcss-loader',
                    'sass-loader'
                ]
            },
            {
                test: /\.(png|jpg|svg|gif)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 10240,
                            name: 'imgs/[name].[contenthash:5].[ext]'
                        }
                    },
                    {
                        // 对图片进行压缩处理,配置项参考官方文档
                        loader: 'image-webpack-loader',
                        options: {
                            mozjpeg: {
                                progressive: true,
                                quality: 65
                            },
                            optipng: {
                                enabled: false
                            },
                            pngquant: {
                                quality: '65-90',
                                speed: 4
                            },
                            gifsicle: {
                                interlaced: false
                            },
                            webp: {
                            quality: 75
                            }
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new CleanWebpackPlugin(),
        new MiniCssExtractPlugin({
            filename: 'css/[name].[hash:5].css'
        }),
        new OptimizeCSSAssetsPlugin(),
        new UglifyJsPlugin({
            uglifyOptions: {
                compress: {
                    // 去除debugger和console
                    drop_debugger: true,
                    drop_console: true
                }
            },
            cache: true,
            parallel: true,
            sourceMap: false
        })
    ]
});

编写其他配置

"presets"参数由插件名和参数对象组成一个数组进行传递。

// .babelrc
{
    "presets": [
        [
            "@babel/preset-env",
            {
                "targets": {
                    // 此处为浏览器的查询条件
                    // 1%:基于全球使用率统计而选择的浏览器版本范围
                    // last 2 version:每个浏览器的最近两个版本
                    // not ie <= 8:不小于等于ie8的浏览器版本
                    "browsers": ["> 1%", "last 2 version", "not ie <= 8"]
                }
            }
        ]
    ]
}

postcss增加不同浏览器css样式相关前缀。

// postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')
    ]
};

在package.json中,加入运行命令。
--hot热加载。当代码有变化时,会自动刷新页面。
--open启动项目时,自动在浏览器打开网页。
--env.NODE_ENV=development设置环境变量,可以在页面中取到当前所处的环境,进行不同的设置。
--config <filepath>需要读取的配置文件路径。

// package.json
// ...
"scripts": {
    "start": "webpack-dev-server --hot --open --env.NODE_ENV=development --config build/webpack.dev.js",
    "build": "webpack --env.NODE_ENV=production --config build/webpack.prod.js"
},
// ...

编写页面代码

// index.html
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8"/>
        <title>Vue init</title>
    </head>
    <body>
        <!-- 留出坑来给Vue发挥 -->
        <div id="app"></div>
    </body>
</html>

index.js是连接webpack和Vue项目的重要文件,在此对Vue项目进行一些基础配置并作为webpack打包的入口。

// index.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import axios from 'axios';

import '../style/common.scss';

import App from '../pages/App.vue';
import Index from '../pages/Index.vue';
import List from '../pages/List.vue';

// 将VueRouter注册到Vue上
Vue.use(VueRouter);
// 将axios挂载到Vue原型链上,方便使用
Vue.prototype.$ajax = axios;

// 创建路由列表
const router = new VueRouter({
    routes: [{
            path: '/',
            component: Index
        },
        {
            path: '/list',
            component: List
        }
    ]
});

// 创建Vue实例
new Vue({
    el: '#app',
    router,
    render: h => h(App)
});

App.vue作为整个项目兜底的页面,可以将公共的部分都提取到App.vue页,此处还有一个VueRouter的配置需要注意。

// App.vue

<template>
    <div id="app">
        <myNav></myNav>
        <!-- ⚠️给VueRoutter留坑!! -->
        <router-view></router-view>
    </div>
</template>

<script>
// 引入导航栏组件
import myNav from "./components/nav.vue";
export default {
    // 注册组件
    components: {
        myNav
    }
};
</script>

来看index.vue页面。

// index.vue
<template>
<div id="index">
    <h1>hello world!</h1>
    <p>halo Viola</p>
    <div v-for="(img, index) in imgList" :key="index">
        <img :src="img">
    </div>
    <div class="nice-pic"></div>
</div>
</template>

<script>
export default {
    name: "index",
    data: function() {
        return {
            imgList: []
        };
    },
    mounted() {
        this.$ajax.get("/api/pictureList").then(res => {
            let result = res.data;
            if(result.code !== 200) return;
            this.imgList = result.data;
        });
    }
};
</script>

在index.vue页面中用到了异步请求。异步请求是前端开发中最基本的需求之一。如果前端开发还需要等待接口先开发出来,那浪费的时间估计都能完成开发了;所以在本地mock数据是前端必备技能。

// mocker.js

const fs = require('fs');

function fromJSONFile(filename) {
  return (req, res) => {
    // 读取mock数据json文件
    const data = fs.readFileSync(`public/json/${filename}.json`).toString();
    // 解析json文件
    const json = JSON.parse(data);
    return res.json(json);
  };
}

const proxy = {
    // 格式:'请求头 请求路径'
    'GET /api/pictureList': fromJSONFile('pictureList')
};

module.exports = proxy;

页面样式通过单独的scss文件编写(为什么要单独编写样式?见下文 遇到的问题 )

// common.scss
// 引入各个页面的样式
@import './app.scss';
@import './index.scss';
@import './nav.scss';

此处仅列举index.scss作为示例。

// index.scss

// sass语法
$width: 50rem;

#index {
    margin-top: 60px;
    // postcss将自动添加前缀
    transform: rotate(0deg);
    img {
        width: $width;
    }
    .nice-pic {
        width: 80%;
        height: $width;
        margin: 0 auto;
        // 背景图引入,小于10kb将被编译成base64
        background: url('../imgs/1.jpg');
    }
}

遇到的问题

提取Vue文件中的Css

因为Vue文件中的样式代码会默认打包进js中,因而生成的js文件非常庞大,可能会导致首屏加载速度非常慢,直接影响用户体验,所以执念的想要把样式文件分离出来。在网上搜索了好半天,找到了官方文档以及民间方案,但是一一尝试之后都没有达到效果。考虑到这可能是webpack 4及相关包实现的问题,因而不想再在这个问题上纠结,便选择了Plan B。

Plan B即将样式文件放到style文件夹中,就各个页面单独一个样式文件进行维护,再引入enrty/index.js中进行编译,此方案可以达成css和js分离的目的。

在下方还是放出我在网上搜索到的官方+民间方案,诸君若感兴趣可以自行尝试。

官方网页:CSS 提取
民间网页: webpack4实现css打包


参考网页:

wow!~完结撒花 🎉🎉🎉
趁着设计还没给定稿,摸鱼写了代码和博客,接下来就是业务逻辑开发啦~
敬请期待~ 😉