likes
comments
collection
share

webpack5.0 搭建 Vue 开发环境

作者站长头像
站长
· 阅读数 13

webpack 搭建Vue 环境

注意:由于版本兼容问题 请尝试自己搭建的小伙伴 参考本文的package.json

首先附上本文所用的 package.json

{
  "name": "webpack_demo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "build": "webpack --config build/webpack.config.js",
    "dev": "webpack-dev-server --hot --open --config build/webpack.dev.js",
    "prod": "webpack --config build/webpack.prod.js",
    "dll": "webpack --config build/webpack.dll.config.js"
  },
  "devDependencies": {
    "@babel/core": "^7.17.5",
    "@babel/preset-env": "^7.16.11",
    "autoprefixer": "^10.4.2",
    "babel-loader": "^8.2.3",
    "cache-loader": "^4.1.0",
    "clean-webpack-plugin": "^4.0.0",
    "copy-webpack-plugin": "^10.2.4",
    "css-loader": "^6.6.0",
    "extract-text-webpack-plugin": "^4.0.0-beta.0",
    "happypack": "^5.0.1",
    "html-webpack-plugin": "^5.5.0",
    "less": "^4.1.2",
    "less-loader": "^10.2.0",
    "mini-css-extract-plugin": "^2.5.3",
    "optimize-css-assets-webpack-plugin": "^6.0.1",
    "postcss-loader": "^6.2.1",
    "style-loader": "^3.3.1",
    "uglifyjs-webpack-plugin": "^2.2.0",
    "vue-loader": "^15.7.0",
    "vue-style-loader": "^4.1.3",
    "vue-template-compiler": "^2.5.17",
    "webpack": "^5.69.1",
    "webpack-bundle-analyzer": "^4.5.0",
    "webpack-cli": "^4.9.2",
    "webpack-dev-server": "^4.7.4",
    "webpack-merge": "^5.8.0",
    "webpack-parallel-uglify-plugin": "^2.0.0"
  },
  "dependencies": {
    "@babel/polyfill": "^7.12.1",
    "file-loader": "^6.2.0",
    "url-loader": "^4.1.1",
    "vue": "^2.5.17"
  }
}

vue-template-compilervue 版本要一致,不然会报错,别问我怎么知道的!

1.1 初始化项目

  1. 新建一个目录,初始化npm

    yarn init
    
  2. webpack是运行在node环境中的,我们需要安装以下两个npm

    yarn add -D webpack webpack-cli
    
  3. 新建一个文件夹src ,然后新建一个文件main.js,写一点代码测试一下

    console.log('测试')
    
  4. 配置 package.json命令

    "scripts": {
            "build": "webpack ./src/main.js"
    }
    
  5. 执行

    yarn run build
    

    此时如果生成了一个dist文件夹,并且内部含有main.js说明已经打包成功了

1.2 开始配置我们自己的设置

上面一个简单的例子只是webpack自己默认的配置,下面我们要实现更加丰富的自定义配置 新建一个build文件夹,里面新建一个webpack.config.js

// webpack.config.js

const path = require('path');
module.exports = {
	// 开发模式
    mode:'development', 
    // 入口文件
    entry: path.resolve(__dirname,'../src/main.js'),    
    output: {
    	// 打包后的文件名称
        filename: 'output.js',  
        // 打包后的目录
        path: path.resolve(__dirname,'../dist')  
    }
}

更改我们的打包命令

"scripts": {
        "build": "webpack --config build/webpack.config.js"
}

执行 yarn run build 会发现生成了以下目录 其中dist文件夹中的main.js就是我们需要在浏览器中实际运行的文件

1.3 配置html模板

js文件打包好了,但是我们不可能每次在html文件中手动引入打包好的js

这里可能有的朋友会认为我们打包js文件名称不是一直是固定的嘛(output.js)?这样每次就不用改动引入文件名称了呀?实际上我们日常开发中往往会这样配置:

module.exports = {
    // 省略其他配置
    output: {
      filename: '[name].[hash:8].js',      // 打包后的文件名称
      path: path.resolve(__dirname,'../dist')  // 打包后的目录
    }
}

为了缓存,你会发现打包好的js文件的名称每次都不一样。webpack打包出来的js文件我们需要引入到html中,但是每次我们都手动修改js文件名显得很麻烦,因此我们需要一个插件来帮我们完成这件事情

yarn add -D html-webpack-plugin

新建一个build同级的文件夹public,里面新建一个index.html 具体配置文件如下

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    mode:'development', // 开发模式
    entry: path.resolve(__dirname,'../src/main.js'),    // 入口文件
    output: {
      filename: '[name].[hash:8].js',      // 打包后的文件名称
      path: path.resolve(__dirname,'../dist')  // 打包后的目录
    },
    plugins:[
      new HtmlWebpackPlugin({
        template:path.resolve(__dirname,'../public/index.html')
      })
    ]
}

可以发现打包生成的js文件已经被自动引入html文件中

1.3.1 多入口文件如何开发

生成多个html-webpack-plugin实例来解决这个问题

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
    mode:'development', // 开发模式
    entry: {
      main:path.resolve(__dirname,'../src/main.js'),
      header:path.resolve(__dirname,'../src/header.js')
  }, 
    output: {
      filename: '[name].[hash:8].js',      // 打包后的文件名称
      path: path.resolve(__dirname,'../dist')  // 打包后的目录
    },
    plugins:[
      new HtmlWebpackPlugin({
        template:path.resolve(__dirname,'../public/index.html'),
        filename:'index.html',
        chunks:['main'] // 与入口文件对应的模块名
      }),
      new HtmlWebpackPlugin({
        template:path.resolve(__dirname,'../public/header.html'),
        filename:'header.html',
        chunks:['header'] // 与入口文件对应的模块名
      }),
    ]
}

1.3.2 clean-webpack-plugin

每次执行yarn run build 会发现dist文件夹里会残留上次打包的文件,这里我们推荐一个plugin来帮我们在打包输出前清空文件夹clean-webpack-plugin

const {CleanWebpackPlugin} = require('clean-webpack-plugin')
module.exports = {
    // ...省略其他配置
    plugins:[new CleanWebpackPlugin()]
}

1.4 引用CSS

我们的入口文件是js,所以我们在入口js中引入我们的css文件

同时我们也需要一些loader来解析我们的css文件

yarn add -D style-loader css-loader

如果我们使用less来构建样式,则需要多安装两个

yarn add -D less less-loader

配置文件如下

// webpack.config.js
module.exports = {
    // ...省略其他配置
    module:{
      rules:[
        {
          test:/\.css$/,
          use:['style-loader','css-loader'] // 从右向左解析原则
        },
        {
          test:/\.less$/,
          use:['style-loader','css-loader','less-loader'] // 从右向左解析原则
        }
      ]
    }
} 

1.4.1 为css添加浏览器前缀

yarn add -D postcss-loader autoprefixer  

配置如下

// webpack.config.js
module.exports = {
    module:{
        rules:[
            {
                test:/\.less$/,
                use:['style-loader','css-loader','postcss-loader','less-loader'] // 从右向左解析原则
           }
        ]
    }
} 

接下来,我们还需要引入autoprefixer使其生效,这里有两种方式

1,在项目根目录下创建一个postcss.config.js文件,配置如下:

module.exports = {
    plugins: [require('autoprefixer')]  // 引用该插件即可了
}

2,直接在webpack.config.js里配置

// webpack.config.js
module.exports = {
    //...省略其他配置
    module:{
        rules:[{
            test:/\.less$/,
            use:['style-loader','css-loader',{
                loader:'postcss-loader',
                options:{
                    plugins:[require('autoprefixer')]
                }
            },'less-loader'] // 从右向左解析原则
        }]
    }
}

这时候我们发现css通过style标签的方式添加到了html文件中,但是如果样式文件很多,全部添加到html中,难免显得混乱。这时候我们想用把css拆分出来用外链的形式引入css文件怎么做呢?这时候我们就需要借助插件来帮助我们

1.4.2 拆分css

yarn add -D mini-css-extract-plugin

注意:使用 mini-css-extract-plugin记得不能和style-loader一起使用

webpack 4.0以前,我们通过extract-text-webpack-plugin插件,把css样式从js文件中提取到单独的css文件中。webpack4.0以后,官方推荐使用mini-css-extract-plugin插件来打包css文件

配置文件如下

const MiniCssExtractPlugin = require("mini-css-extract-plugin");
module.exports = {
  //...省略其他配置
  module: {
    rules: [
      {
        test: /\.less$/,
        use: [
           MiniCssExtractPlugin.loader,
          'css-loader',
          'less-loader'
        ],
      }
    ]
  },
  plugins: [
    new MiniCssExtractPlugin({
        filename: "[name].[hash].css",
        chunkFilename: "[id].css",
    })
  ]
}

1.4.3 拆分多个css[未使用]

这里需要说的细一点,上面我们所用到的mini-css-extract-plugin会将所有的css样式合并为一个css文件。如果你想拆分为一一对应的多个css文件,我们需要使用到extract-text-webpack-plugin,而目前mini-css-extract-plugin还不支持此功能。我们需要安装@next版本的extract-text-webpack-plugin

yarn add -D extract-text-webpack-plugin@next
// webpack.config.js

const path = require('path');
const ExtractTextWebpackPlugin = require('extract-text-webpack-plugin')
let indexLess = new ExtractTextWebpackPlugin('index.less');
let indexCss = new ExtractTextWebpackPlugin('index.css');
module.exports = {
    module:{
      rules:[
        {
          test:/\.css$/,
          use: indexCss.extract({
            use: ['css-loader']
          })
        },
        {
          test:/\.less$/,
          use: indexLess.extract({
            use: ['css-loader','less-loader']
          })
        }
      ]
    },
    plugins:[
      indexLess,
      indexCss
    ]
}

1.5 打包 图片、字体、媒体、等文件

file-loader就是将文件在进行一些处理后(主要是处理文件名和路径、解析文件url),并将文件移动到输出的目录中 url-loader 一般与file-loader搭配使用,功能与 file-loader 类似,如果文件小于限制的大小。则会返回 base64 编码,否则使用 file-loader 将文件移动到输出的目录中

yarn add file-loader url-loader
// webpack.config.js
module.exports = {
  // 省略其它配置 ...
  module: {
    rules: [
      // ...
      {
        test: /\.(jpe?g|png|gif)$/i, //图片文件
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10240,
              fallback: {
                loader: 'file-loader',
                options: {
                    name: 'img/[name].[hash:8].[ext]'
                }
              }
            }
          }
        ]
      },
      {
        test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10240,
              fallback: {
                loader: 'file-loader',
                options: {
                  name: 'media/[name].[hash:8].[ext]'
                }
              }
            }
          }
        ]
      },
      {
        test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
        use: [
          {
            loader: 'url-loader',
            options: {
              limit: 10240,
              fallback: {
                loader: 'file-loader',
                options: {
                  name: 'fonts/[name].[hash:8].[ext]'
                }
              }
            }
          }
        ]
      },
    ]
  }
}

1.6 用babel转义js文件

为了使我们的js代码兼容更多的环境我们需要安装依赖

yarn add -D babel-loader @babel/preset-env @babel/core
  • 注意 babel-loaderbabel-core的版本对应关系
  1. babel-loader 8.x 对应babel-core 7.x
  2. babel-loader 7.x 对应babel-core 6.x 配置如下
// webpack.config.js
module.exports = {
    // 省略其它配置 ...
    module:{
        rules:[
          {
            test:/\.js$/,
            use:{
              loader:'babel-loader',
              options:{
                presets:['@babel/preset-env']
              }
            },
            exclude:/node_modules/
          },
       ]
    }
}

上面的babel-loader只会将 ES6/7/8语法转换为ES5语法,但是对新api并不会转换 例如(promise、Generator、Set、Maps、Proxy等) 此时我们需要借助babel-polyfill来帮助我们转换

yarn add @babel/polyfill

// webpack.config.js
const path = require('path')
module.exports = {
	// 入口文件
    entry: ["@babel/polyfill",path.resolve(__dirname,'../src/index.js')],    
}

搭建vue开发环境

2.1 解析.vue文件

yarn add -D vue-loader vue-template-compiler vue-style-loader
yarn add -S vue

注意: vue-loader为^15.7.0 不会报错,否则可能会报 找不到 vue-loader错误

vue-loader 用于解析.vue文件 vue-template-compiler 用于编译模板 配置如下

const vueLoaderPlugin = require('vue-loader/lib/plugin')
module.exports = {
    module:{
        rules:[{
            test:/\.vue$/,
            use:['vue-loader']
        },]
     },
    resolve:{
        alias:{
          'vue$':'vue/dist/vue.runtime.esm.js',
          ' @':path.resolve(__dirname,'../src')
        },
        extensions:['*','.js','.json','.vue']
   },
   plugins:[
        new vueLoaderPlugin()
   ]
}

2.2 配置webpack-dev-server进行热更新

yarn add -D webpack-dev-server

配置如下

const Webpack = require('webpack')
module.exports = {
  // ...省略其他配置
  devServer:{
    port:3000,
    hot:true,
    contentBase:'../dist'
  },
  plugins:[
    new Webpack.HotModuleReplacementPlugin()
  ]
}

webpack.config全部配置如下:

const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const Webpack = require('webpack');
const {
    CleanWebpackPlugin
} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const MiniCssExtractPlugin = require("mini-css-extract-plugin");


module.exports = {
    mode: "development",
    // entry: path.resolve(__dirname, '../src/main.js'),
    entry: ["@babel/polyfill", path.resolve(__dirname, '../src/main.js')],
    output: {
        path: path.resolve(__dirname, '../dist'),
        filename: '[name].[hash:8].js',
    },
    devServer: {
        // 端口
        port: 8080,
        // 是否热刷新
        hot: true,
        // 开放资源目录
        static: {
            directory: path.join(__dirname, 'dist'),
        }
    },
    plugins: [
        new VueLoaderPlugin(),
        new Webpack.HotModuleReplacementPlugin(),
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, '../public/index.html')
        }),
        new MiniCssExtractPlugin({
            filename: "[name].[hash].css",
            chunkFilename: "[id].css",
        }),
    ],
    module: {
        rules: [{
                test: /\.vue$/,
                use: ['vue-loader'],
                include: [/src/],
            },
            {
                test: /\.less$/,
                use: [
                 	MiniCssExtractPlugin.loader,
                    'css-loader',
                    'less-loader'
                ],
                include: [/src/],

            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader'],
                include: [/src/],

            },
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                },
                exclude: /node_modules/
            },
            {
                test: /\.(jpe?g|png|gif)$/i, //图片文件
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 10240,
                        fallback: {
                            loader: 'file-loader',
                            options: {
                                name: 'img/[name].[hash:8].[ext]'
                            }
                        }
                    }
                }]
            },
            {
                test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 10240,
                        fallback: {
                            loader: 'file-loader',
                            options: {
                                name: 'media/[name].[hash:8].[ext]'
                            }
                        }
                    }
                }]
            },
            {
                test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 10240,
                        fallback: {
                            loader: 'file-loader',
                            options: {
                                name: 'fonts/[name].[hash:8].[ext]'
                            }
                        }
                    }
                }]
            },
        ]
    }
}

注意devServer里面的contentBase似乎已经被废弃将devServer改为static

2.3 配置打包命令

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

打包文件已经配置完毕,接下来让我们测试一下 首先在src新建一个main.js

import App from './App.vue';
import Vue from 'vue';

new Vue({
    render: h => h(App)
}).$mount('#app');

src新建一个App.vue

<template>
    <div class="test">
        {{ test }}
    </div>
</template>

<script>
export default {
    data() {
        return {
            test: "测试",
        };
    },
};
</script>

新建一个public文件夹,里面新建一个index.html

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

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="app"></div>
</body>

</html>

执行yarn run dev这时候如果浏览器出现 测试 的界面,那么配置成功了

2.4 区分开发环境与生产环境

实际应用到项目中,我们需要区分开发环境与生产环境,我们在原来webpack.config.js的基础上再新增两个文件

  • webpack.dev.js 开发环境配置文件
开发环境主要实现的是热更新,不要压缩代码,完整的sourceMap

  • webpack.prod.js 生产环境配置文件
生产环境主要实现的是压缩代码、提取css文件、合理的sourceMap、分割代码
需要安装以下模块:
yarn add -D  webpack-merge copy-webpack-plugin optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin

  • webpack-merge 合并配置
  • copy-webpack-plugin 拷贝静态资源
  • optimize-css-assets-webpack-plugin 压缩css
  • uglifyjs-webpack-plugin 压缩js

webpack mode设置production的时候会自动压缩js代码。原则上不需要引入uglifyjs-webpack-plugin进行重复工作。但是optimize-css-assets-webpack-plugin压缩css的同时会破坏原有的js压缩,所以这里我们引入uglifyjs进行压缩

2.4.1 webpack.config.js

const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const Webpack = require('webpack');
/** 
 * 清除打包文件
 */
const {
    CleanWebpackPlugin
} = require('clean-webpack-plugin')
/** 
 * html 模板
 */
const HtmlWebpackPlugin = require('html-webpack-plugin')
/** 
 * 拆分css
 */
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

/** 
 * 多线程打包
 */
const Happypack = require('happypack');
/** 
 * 基本的系统操作函数
 */
const os = require('os')
const happyThreadPool = Happypack.ThreadPool({
    size: os.cpus().length
})

// 区分版本
const devMode = process.argv.indexOf('--mode=production') === -1;

module.exports = {
    // 区分版本
    // mode: "development",
    // entry: path.resolve(__dirname, '../src/main.js'),
    // 转译 promise、Generator、Set、Maps、Proxy 等新增语法
    // entry: ["@babel/polyfill", path.resolve(__dirname, '../src/main.js')],
    entry: {
        main: ["@babel/polyfill", path.resolve(__dirname, '../src/main.js')]
    },
    output: {
        path: path.resolve(__dirname, '../dist'),
        //加上可以运行 浏览器404
        // publicPath: '../public/',
        // filename: '[name].[hash:8].js',
        // 区分版本
        filename: 'js/[name].[hash:8].js',
        chunkFilename: 'js/[name].[hash:8].js'
    },
    // devServer: {
    //     // 端口
    //     port: 8080,
    //     // 是否热刷新
    //     hot: true,
    //     // 开放资源目录
    //     static: {
    //         directory: path.join(__dirname, 'dist'),
    //     }
    // },
    plugins: [
        new VueLoaderPlugin(),
        // 热更新模块
        new Webpack.HotModuleReplacementPlugin(),
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, '../src/index.html'),
            //加上 dev 环境  控制台不报错 浏览器报404 不加上 prod 会报错 相同的文件名
            // filename: '[name].[hash:8].html',   
            // 与入口文件对应的模块名
            chunks: ['main']
        }),
        new MiniCssExtractPlugin({
            // filename: "[name].[hash].css",
            // chunkFilename: "[id].css",
            // 区分版本
            filename: devMode ? '[name].css' : '[name].[hash].css',
            chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
        }),
        new Happypack({
            //和 loader 对应的 id 标识
            id: 'happybabel',
            // 用法和 loader 配置一样 注意这里是 loaders
            loaders: [{
                loader: 'babel-loader',
                options: {
                    presets: ['@babel/preset-env']
                },
                cacheDirectory: true

            }],
            // 共享进程池
            threadPool: happyThreadPool
        })
    ],
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.runtime.esm.js',
            ' @': path.resolve(__dirname, '../src'),
            'components': path.resolve(__dirname, 'src/components')
            //   '@pages': path.join(__dirname, "..", "src", "pages"),
            //   "@components": path.join(__dirname, "..", "src", "components"),
        },
        extensions: ['*', '.js', '.json', '.vue']
    },
    module: {
        //不去解析jquery
        noParse: "/jquery/",
        rules: [{
                test: /\.vue$/,
                use: ['vue-loader'],
                include: [path.resolve(__dirname, '../src')],
                exclude: /node_modules/
            },
            {
                test: /\.less$/,
                use: [
                    // 区分版本
                    {
                        loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
                        options: {
                            publicPath: "../dist/css/",
                            hmr: devMode
                        }
                    },
                    // MiniCssExtractPlugin.loader,
                    'css-loader',
                    'less-loader'
                ],
                include: [path.resolve(__dirname, '../src')],
                exclude: /node_modules/

            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader'],
                include: [/src/],

            },
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                },
                exclude: /node_modules/
            },
            {
                test: /\.(jpe?g|png|gif)$/i, //图片文件
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 10240,
                        fallback: {
                            loader: 'file-loader',
                            options: {
                                name: 'img/[name].[hash:8].[ext]'
                            }
                        }
                    }
                }]
            },
            {
                test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 10240,
                        fallback: {
                            loader: 'file-loader',
                            options: {
                                name: 'media/[name].[hash:8].[ext]'
                            }
                        }
                    }
                }]
            },
            {
                test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 10240,
                        fallback: {
                            loader: 'file-loader',
                            options: {
                                name: 'fonts/[name].[hash:8].[ext]'
                            }
                        }
                    }
                }]
            },
        ]
    }
}

注意 new HtmlWebpackPluginfilename在生产打包的情况下不要写成默认的index.html否则可能会出现Multiple assets emit different content to the same filename index.html错误,在dev开发环境下改成默认的index.html否则会出现 控制台不报错,但是浏览器显示404【我是这样的,有大佬知道为什么,烦请告知】

2.4.2 webpack.dev.js

const Webpack = require('webpack')
const path = require('path')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')
module.exports = WebpackMerge.merge(webpackConfig, {
    mode: 'development',
    devServer: {
        // 端口
        port: 8080,
        // 是否热刷新
        hot: true,
        // 开放资源目录
        static: {
            directory: path.join(__dirname, 'dist'),
        }
    },
    devtool: 'eval-cheap-module-source-map',
    plugins: [
        // 热更新模块
        new Webpack.HotModuleReplacementPlugin()
    ]
})

注意 新版本的WebpackMerge不是方法了,需要.merge才可以使用

2.4.3 webpack.prod.js

const path = require('path')
const webpackConfig = require('./webpack.config.js')
/** 
 *  合并配置
 */
const WebpackMerge = require('webpack-merge')
/** 
 * 拷贝静态资源
 */
const CopyWebpackPlugin = require('copy-webpack-plugin')
/** 
 * 压缩css
 */
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
/** 
 *压缩js 
 */
const UglifyJsPlugin = require('uglifyjs-webpack-plugin')
const {
    options
} = require('less')
module.exports = WebpackMerge.merge(webpackConfig, {
    mode: 'production',
    devtool: 'cheap-module-source-map',
    plugins: [
        new CopyWebpackPlugin({
            patterns: [{
                from: path.resolve(__dirname, '../public'),
                to: path.resolve(__dirname, '../dist')
            }]
        }),
    ],
    optimization: {
        minimizer: [
            //压缩js
            new UglifyJsPlugin({
                cache: true,
                parallel: true,
                sourceMap: true
            }),
            new OptimizeCssAssetsPlugin({})
        ],
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                libs: {
                    name: "chunk-libs",
                    test: /[\\/]node_modules[\\/]/,
                    priority: 10,
                    // 只打包初始时依赖的第三方
                    chunks: "initial"
                }
            }
        }
    }
})

注意:unable to locate 'D:/study/webpack/webpack_demo/public/xx/x' glob 因为文件夹里面没有内容。CopyWebpackPlugin不能copy空文件夹

2.5 优化webpack配置

2.5.1 优化打包速度

构建速度指的是我们每次修改代码后热更新的速度以及发布前打包文件的速度。

2.5.1.1 合理的配置mode参数与devtool参数

mode可设置development production两个参数 如果没有设置,webpack4 会将 mode 的默认值设置为 production production模式下会进行tree shaking(去除无用代码)和uglifyjs(代码压缩混淆)

2.5.1.2 缩小文件的搜索范围(配置include exclude alias noParse extensions)

  • alias: 当我们代码中出现 import 'vue'时, webpack会采用向上递归搜索的方式去node_modules 目录下找。为了减少搜索范围我们可以直接告诉webpack去哪个路径下查找。也就是别名(alias)的配置。

  • include exclude 同样配置include exclude也可以减少webpack loader的搜索转换时间。

  • noParse 当我们代码中使用到import jq from 'jquery'时,webpack会去解析jq这个库是否有依赖其他的包。但是我们对类似jquery这类依赖库,一般会认为不会引用其他的包(特殊除外,自行判断)。增加noParse属性,告诉webpack不必解析,以此增加打包速度。

  • extensions webpack会根据extensions定义的后缀查找文件(频率较高的文件类型优先写在前面

    //省略其他配置
    module:{
    	//不去解析jquery
        noParse: "/jquery/",
        rules: [
        	{
                test: /\.vue$/,
                use: ['vue-loader'],
                include: [/src/],
                include: [path.resolve(__dirname, '../src')],
                exclude: /node_modules/
            },
        ]
    },
    resolve: {
            alias: {
                'vue$': 'vue/dist/vue.runtime.esm.js',
                ' @': path.resolve(__dirname, '../src')
            },
            extensions: ['*', '.js', '.json', '.vue']
    },
    

2.5.1.3 使用HappyPack开启多进程Loader转换

webpack构建过程中,实际上耗费时间大多数用在loader解析转换以及代码的压缩中。日常开发中我们需要使用Loaderjs,css,图片,字体等文件做转换操作,并且转换的文件数据量也是非常大。由于js单线程的特性使得这些转换操作不能并发处理文件,而是需要一个个文件进行处理。HappyPack的基本原理是将这部分任务分解到多个子进程中去并行处理,子进程处理完成后把结果发送到主进程中,从而减少总的构建时间

yarn add -D happypack
//省略以上配置。。。
/** 
 * 多线程打包
 */
const Happypack = require('happypack');
/** 
 * 基本的系统操作函数
 */
const os = require('os')
const happyThreadPool = Happypack.ThreadPool({
    size: os.cpus().length
})

module.exports = {
	plugins:[
		new Happypack({
            //和 loader 对应的 id 标识
            id: 'happybabel',
            // 用法和 loader 配置一样 注意这里是 loaders
            loaders: [{
            loader: 'babel-loader',
            options: {
            presets: ['@babel/preset-env']
            },
            cacheDirectory: true
            }],
            // 共享进程池
            threadPool: happyThreadPool
        })
	]
}

注意 不要上来就开启多进程打包,一般遇到性能瓶颈或者明确需要优化打包速度时,可以考虑采用这两种方案。因为多进程也有有开销的,如进程的启动,销毁,通信等。

2.5.1.4 使用webpack-parallel-uglify-plugin 增强代码压缩

上面对于loader转换已经做优化,那么下面还有另一个难点就是优化代码的压缩时间。

yarn add -D webpack-parallel-uglify-plugin
/** 
 * 多进程压缩js 原理还是 UglifyJsPlugin
 */
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')

module.exports = {
	plugins:[
		new ParallelUglifyPlugin({
            cacheDir: './cache',
            uglifyJs: {
            output: {
                //删除所有注释
                comments: false,
                // 最紧凑的输出
                beautify: false
            },
            compress: {
                // 删除所有的 `console` 语句,可以兼容ie浏览器
                drop_console: true,
                // 内嵌定义了但是只用到一次的变量
                collapse_vars: true,
                // 提取出出现多次但是没有定义成变量去引用的静态值
                reduce_vars: true
            }
            }
        }),
	]
}

注意 不要上来就开启多进程打包,一般遇到性能瓶颈或者明确需要优化打包速度时,可以考虑采用这两种方案。因为多进程也有有开销的,如进程的启动,销毁,通信等。

2.5.1.5 抽离第三方模块

对于开发项目中不经常会变更的静态依赖文件。类似于我们的elementUi、vue全家桶等等。因为很少会变更,所以我们不希望这些依赖要被集成到每一次的构建逻辑中去。 这样做的好处是每次更改我本地代码的文件的时候,webpack只需要打包我项目本身的文件代码,而不会再去编译第三方库。以后只要我们不升级第三方包的时候,那么webpack就不会对这些库去打包,这样可以快速的提高打包的速度。

这里我们使用webpack内置的DllPlugin DllReferencePlugin进行抽离 在与webpack配置文件同级目录下新建webpack.dll.config.js 代码如下

// webpack.dll.config.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
    // 你想要打包的模块的数组
    entry: {
        vendor: ['vue']
    },
    output: {
        // 打包后文件输出的位置
        path: path.resolve(__dirname, 'static/js'),
        filename: '[name].dll.js',
        library: '[name]_library'
        // 这里需要和webpack.DllPlugin中的`name: '[name]_library',`保持一致。
    },
    plugins: [
        new webpack.DllPlugin({
            path: path.resolve(__dirname, '[name]-manifest.json'),
            name: '[name]_library',
            context: __dirname
        })
    ]
};

package.json中配置如下命令

"dll": "webpack --config build/webpack.dll.config.js"

接下来在我们的webpack.config.js中增加以下代码

module.exports = {
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require('./vendor-manifest.json')
    }),
    // 拷贝生成的文件到dist目录 这样每次不必手动去cv
    new CopyWebpackPlugin({
        patterns: [{
        from: path.resolve(__dirname, '../static'),
        to: path.resolve(__dirname, '../dist')
        }]
    }),
  ]
};

执行

yarn run dll

会发现生成了我们需要的集合第三地方 代码的vendor.dll.js 我们需要在html文件中手动引入这个js文件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>老yuan</title>
  <script src="static/js/vendor.dll.js"></script>
</head>
<body>
  <div id="app"></div>
</body>
</html>

这样如果我们没有更新第三方依赖包,就不必npm run dll。直接执行npm run dev npm run build的时候会发现我们的打包速度明显有所提升。因为我们已经通过dllPlugin将第三方依赖包抽离出来了

注意在使用时一直报 vendor_library is not defined 的错误  查阅很久资料未解决,所以就放弃了这个配置,有大佬配置成功烦请踢我

2.5.1.6 配置缓存

我们每次执行构建都会把所有的文件都重复编译一遍,这样的重复工作是否可以被缓存下来呢,答案是可以的,目前大部分 loader 都提供了cache 配置项。比如在 babel-loader 中,可以通过设置cacheDirectory 来开启缓存,babel-loader?cacheDirectory=true 就会将每次的编译结果写进硬盘文件(默认是在项目根目录下的node_modules/.cache/babel-loader目录内,当然你也可以自定义)

但如果 loader 不支持缓存呢?我们也有方法,我们可以通过cache-loader ,它所做的事情很简单,就是 babel-loader 开启 cache 后做的事情,将 loader 的编译结果写入硬盘缓存。再次构建会先比较一下,如果文件较之前的没有发生变化则会直接使用缓存。使用方法如官方demo所示,在一些性能开销较大的 loader 之前添加此 loader即可

yarn add -D cache-loader
//省略其他配置
module.exports = {
	module:{
		rules: [{
            test: /\.ext$/,
            use: [
            'cache-loader'
            ],
            include: [path.resolve(__dirname, '../src')],
		}]
	}
}

目前各个文件配置情况

webpack.config.js

const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin');
const Webpack = require('webpack');
/** 
 * 清除打包文件
 */
const {
    CleanWebpackPlugin
} = require('clean-webpack-plugin')
/** 
 * html 模板
 */
const HtmlWebpackPlugin = require('html-webpack-plugin')
/** 
 * 拆分css
 */
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

/** 
 * 多线程打包
 */
const Happypack = require('happypack');
/** 
 * 基本的系统操作函数
 */
const os = require('os')
const happyThreadPool = Happypack.ThreadPool({
    size: os.cpus().length
})

/** 
 * 拷贝静态资源
 */
const CopyWebpackPlugin = require('copy-webpack-plugin');

// 区分版本
const devMode = process.argv.indexOf('--mode=production') === -1;

module.exports = {
    // 区分版本
    // mode: "development",
    // entry: path.resolve(__dirname, '../src/main.js'),
    // 转译 promise、Generator、Set、Maps、Proxy 等新增语法
    // entry: ["@babel/polyfill", path.resolve(__dirname, '../src/main.js')],
    entry: {
        main: ["@babel/polyfill", path.resolve(__dirname, '../src/main.js')]
    },
    output: {
        path: path.resolve(__dirname, '../dist'),
        //加上可以运行 浏览器404
        // publicPath: '../public/',
        // filename: '[name].[hash:8].js',
        // 区分版本
        filename: 'js/[name].[hash:8].js',
        chunkFilename: 'js/[name].[hash:8].js'
    },
    // devServer: {
    //     // 端口
    //     port: 8080,
    //     // 是否热刷新
    //     hot: true,
    //     // 开放资源目录
    //     static: {
    //         directory: path.join(__dirname, 'dist'),
    //     }
    // },
    plugins: [
        new VueLoaderPlugin(),
        // 热更新模块
        new Webpack.HotModuleReplacementPlugin(),
        new CleanWebpackPlugin(),
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, '../src/index.html'),
            //加上 dev 环境  控制台不报错 浏览器报404 不加上 prod 会报错 相同的文件名
            filename: '[name].[hash:8].html',
            // 与入口文件对应的模块名
            chunks: ['main']
        }),
        new MiniCssExtractPlugin({
            // filename: "[name].[hash].css",
            // chunkFilename: "[id].css",
            // 区分版本
            filename: devMode ? '[name].css' : '[name].[hash].css',
            chunkFilename: devMode ? '[id].css' : '[id].[hash].css'
        }),
        new Happypack({
            //和 loader 对应的 id 标识
            id: 'happybabel',
            // 用法和 loader 配置一样 注意这里是 loaders
            loaders: [{
                loader: 'babel-loader',
                options: {
                    presets: ['@babel/preset-env']
                },
                cacheDirectory: true
            }],
            // 共享进程池
            threadPool: happyThreadPool
        }),
        // 抽离的 模块引入
        // new Webpack.DllReferencePlugin({
        //     context: __dirname,
        //     manifest: require('../static/vendor-manifest.json')
        // }),
        // // 拷贝生成的文件到dist目录 这样每次不必手动去cv
        // new CopyWebpackPlugin({
        //     patterns: [{
        //         from: path.resolve(__dirname, '../static'),
        //         to: path.resolve(__dirname, '../dist')
        //     }]
        // }),
    ],
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.runtime.esm.js',
            ' @': path.resolve(__dirname, '../src'),
            'components': path.resolve(__dirname, 'src/components')
            //   '@pages': path.join(__dirname, "..", "src", "pages"),
            //   "@components": path.join(__dirname, "..", "src", "components"),
        },
        extensions: ['*', '.js', '.json', '.vue']
    },
    module: {
        //不去解析jquery
        noParse: "/jquery/",
        rules: [{
                test: /\.vue$/,
                use: ['vue-loader'],
                include: [path.resolve(__dirname, '../src')],
                exclude: /node_modules/
            },
            {
                test: /\.less$/,
                use: [
                    // 区分版本
                    {
                        loader: devMode ? 'vue-style-loader' : MiniCssExtractPlugin.loader,
                        options: {
                            publicPath: "../dist/css/",
                            hmr: devMode
                        }
                    },
                    // MiniCssExtractPlugin.loader,
                    'css-loader',
                    'less-loader'
                ],
                include: [path.resolve(__dirname, '../src')],
                exclude: /node_modules/

            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader'],
                include: [/src/],

            },
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                },
                exclude: /node_modules/
            },
            {
                test: /\.(jpe?g|png|gif)$/i, //图片文件
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 10240,
                        fallback: {
                            loader: 'file-loader',
                            options: {
                                name: 'img/[name].[hash:8].[ext]'
                            }
                        }
                    }
                }]
            },
            {
                test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, //媒体文件
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 10240,
                        fallback: {
                            loader: 'file-loader',
                            options: {
                                name: 'media/[name].[hash:8].[ext]'
                            }
                        }
                    }
                }]
            },
            {
                test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/i, // 字体
                use: [{
                    loader: 'url-loader',
                    options: {
                        limit: 10240,
                        fallback: {
                            loader: 'file-loader',
                            options: {
                                name: 'fonts/[name].[hash:8].[ext]'
                            }
                        }
                    }
                }]
            },
            {
                test: /\.ext$/,
                use: [
                    'cache-loader'
                ],
                include: [path.resolve(__dirname, '../src')],
            }
        ]
    }
}

webpack.dev.js

const Webpack = require('webpack')
const path = require('path')
const webpackConfig = require('./webpack.config.js')
const WebpackMerge = require('webpack-merge')
module.exports = WebpackMerge.merge(webpackConfig, {
    mode: 'development',
    devServer: {
        // 端口
        port: 8080,
        // 是否热刷新
        hot: true,
        // 开放资源目录
        static: {
            directory: path.join(__dirname, 'dist'),
        }
    },
    devtool: 'eval-cheap-module-source-map',
    plugins: [
        // 热更新模块
        new Webpack.HotModuleReplacementPlugin()
    ]
})

webpack.prod.js

const path = require('path')
const webpackConfig = require('./webpack.config.js')
/** 
 *  合并配置
 */
const WebpackMerge = require('webpack-merge')
/** 
 * 拷贝静态资源
 */
const CopyWebpackPlugin = require('copy-webpack-plugin')
/** 
 * 压缩css
 */
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin')
/** 
 *压缩js 
 */
// const UglifyJsPlugin = require('uglifyjs-webpack-plugin')

/** 
 * 多进程压缩js 原理还是 UglifyJsPlugin 开启了多进程 使用前提要有 Happypack
 */
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin')

module.exports = WebpackMerge.merge(webpackConfig, {
    mode: 'production',
    devtool: 'cheap-module-source-map',
    plugins: [
        new CopyWebpackPlugin({
            patterns: [{
                from: path.resolve(__dirname, '../public'),
                to: path.resolve(__dirname, '../dist')
            }]
        }),
    ],
    optimization: {
        minimizer: [
            //压缩js
            // new UglifyJsPlugin({
            //     cache: true,
            //     parallel: true,
            //     sourceMap: true
            // }),
            new ParallelUglifyPlugin({
                cacheDir: './cache',
                uglifyJs: {
                    output: {
                        //删除所有注释
                        comments: false,
                        // 最紧凑的输出
                        beautify: false
                    },
                    compress: {
                        // 删除所有的 `console` 语句,可以兼容ie浏览器
                        drop_console: true,
                        // 内嵌定义了但是只用到一次的变量
                        collapse_vars: true,
                        // 提取出出现多次但是没有定义成变量去引用的静态值
                        reduce_vars: true
                    }
                }
            }),
            new OptimizeCssAssetsPlugin({})
        ],
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                libs: {
                    name: "chunk-libs",
                    test: /[\\/]node_modules[\\/]/,
                    priority: 10,
                    // 只打包初始时依赖的第三方
                    chunks: "initial"
                }
            }
        }
    }
})

2.5.2 优化打包文件体积

打包的速度我们是进行了优化,但是打包后的文件体积却是十分大,造成了页面加载缓慢,浪费流量等,接下来让我们从文件体积上继续优化

2.5.2.1 引入webpack-bundle-analyzer分析打包后的文件

webpack-bundle-analyzer将打包后的内容束展示为方便交互的直观树状图,让我们知道我们所构建包中真正引入的内容

yarn add -D webpack-bundle-analyzer
/** 
 * 分析打包后的文件
 */
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;


//省略其他配置
module.exports = {
	plugins:[
		{
			new BundleAnalyzerPlugin({
                //打开的地址
                analyzerHost: '127.0.0.1',
                // 打开的端口
                analyzerPort: 8080
        	})
		},
	]
}