前言:文件依赖关系错综复杂,静态资源请求效率低,模块化支持不友好,浏览器对高级JS兼容程度低?那就是时候了解webpack了

一、简单了解webpack

(1)概念

webpack 是一个JavaScript 应用程序的静态模块打包构建器。在处理应用程序时, webpack 会递归地构建一个依赖关系图,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。webpack提供了友好的模块化支持,以及代码压缩混淆、高级js兼容、性能优化。

(2)核心

1.入口(entry):指定webpack打包编译从哪个文件开始下手

入口起点(entry point)指示 webpack 使用哪个模块,作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

webpack.config.js:

module.exports = {
 entry: {
    main: './src' //打包入口,来指定一个入口起点(或多个入口起点,默认值为 ./src)
  },
 entry: './src', //这个是上面的简写方式,是等价的
 entry: {
  home: "./home.js",
  about: "./about.js",
  contact: "./contact.js"
 },//对象法指定多个入口,如果你想要多个依赖一起注入到一个模块,向 entry 属性传入「文件路径(file path)数组」。
 entry: () => new Promise((resolve) => resolve(['./demo', './demo2'])),//动态入口,当结合 output.library 选项时:如果传入数组,则只导出最后一项
};

2.出口(output):指定webpack打包编译后的路径及文件名

output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。

webpack.config.js:

const path = require('path');

module.exports = {
  output: {
    path: path.resolve(__dirname, 'dist'),//打包文件夹名,默认值为 ./dist
    filename: '[name].js'//入口文件名
  }
};

3.loader(加载器):webpack识别不了的语言通过加载器来翻译

loader 用于转换某些类型的模块,webpack 自身只理解 JavaScript,loader  可以将所有类型的文件转换为 webpack 能够处理的有效模块。loader 能够 import 导入任何类型的模块(如 .css),是 webpack 特有的功能,其他打包工具有可能不支持。

webpack.config.js:

const path = require('path');

const config = {
  module: {
    rules: [//在 webpack 配置中定义 loader 时,要定义在 module.rules 中,里面包含两个"必须属性":test 和 use
      {
        test: /\.txt$/, //test 定义需要使用相应 loader 进行转换的某类文件
        use: 'raw-loader' //use 定义使用哪个 loader 进行转换
      }
    ]
  }
};

module.exports = config;

4.插件(plugins):webpack完成不了的功能通过插件来完成

插件接口功能极其强大,可以用来处理各种各样的任务。通过require() 使用插件,然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。

webpack.config.js:

const HtmlWebpackPlugin = require('html-webpack-plugin'); // 通过 npm 安装
const webpack = require('webpack'); // 用于访问内置插件

const config = {
  plugins: [
    //在一个配置文件中因为不同目的而多次使用同一个插件,需要通过使用 new 操作符来创建它的一个实例
    new webpack.optimize.UglifyJsPlugin(),
    new HtmlWebpackPlugin({template: './src/index.html'})
  ]
};

module.exports = config;

5.模式(mode):通过模式确定是开发环境还是生产环境以便加载不同配置

module.exports = {
  mode: 'production'//通过选择 development(开发) 或 production(生产),启用相应模式下的 webpack 内置的优化
};

6.配置

webpack 需要传入一个配置对象, 根据对象定义的属性进行解析,因此很少有 webpack 配置看起来很完全相同。 webpack 的配置文件,是导出一个对象的 JavaScript 文件。可以通过两种方式(终端、Node.js)使用 webpack。

 webpack 配置是标准的 Node.js CommonJS 模块,你可以:

  • 通过 require(...) 导入其他文件
  • 通过 require(...) 使用 npm 的工具函数
  • 使用 JavaScript 控制流表达式,例如 ?: 操作符
  • 对常用值使用常量或变量
  • 编写并执行函数来生成部分配置

应避免以下做法

  • 在使用 webpack 命令行接口(CLI)(应该编写自己的命令行接口(CLI),或使用 --env)时,访问命令行接口(CLI)参数
  • 导出不确定的值(调用 webpack 两次应该产生同样的输出文件)
  • 编写很长的配置,应该将配置拆分为多个文件

二、动手搭建一个

(1)前提必备

 webpack 配置是标准的 Node.js CommonJS 模块,在安装webpack之前,请确保安装了 Node.js 的最新版本,使用旧版本可能遇到各种问题(可能缺少 webpack 功能或者缺少相关 package 包)。

(2)准备工作

1.安装webpack

对于大多数项目,建议本地安装。这可以在引入破坏式变更(breaking change)的依赖时,更容易分别升级项目。不推荐全局安装 webpack。这会将你项目中的 webpack 锁定到指定版本,并且在使用不同的 webpack 版本的项目中,可能会导致构建失败。

①本地安装
npm install --save-dev webpack //安装最新版本
npm install --save-dev webpack@<version> //安装特定版本

②全局安装 
npm install --global webpack

2. 安装 CLI

如果你使用 webpack 4+ 版本,你还需要安装 CLI,此工具用于在命令行中运行 webpack。

npm install --save-dev webpack-cli //webpack-cli用于在命令行中运行 webpack

(3)上手操作

在已有的项目中:

npm init -y  //初始化webpack  这里会自动生成一个package.json
npm i -D webpack webpack-cli //安装webpack及其脚手架

一个项目包两个项目demo1和demo2(适用于两个项目功能需求很类似有公用的地方有不同的地方):

{
  "name": "webpackstudy",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "demo1-start": "webpack-dev-server --progress --color",
    "demo2-start": "webpack-dev-server --progress --color",
    "demo1-mock": "webpack-dev-server --progress --color",
    "demo2-mock": "webpack-dev-server --progress --color",
    "demo1-te": "webpack --progress --color",
    "demo2-te": "webpack --progress --color"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "axios": "^0.19.0",
    "vue": "^2.6.10",
    "vue-pull-to": "^0.1.8",
    "vue-router": "^3.1.2",
    "vuex": "^3.1.1"
  },
  "devDependencies": {
    "@babel/core": "^7.5.5",
    "@babel/preset-env": "^7.5.5",
    "autoprefixer": "^9.6.1",
    "babel-loader": "^8.0.6",
    "clean-webpack-plugin": "^3.0.0",
    "css-loader": "^3.2.0",
    "file-loader": "^4.2.0",
    "html-webpack-plugin": "^3.2.0",
    "html-withimg-loader": "^0.1.16",
    "jsonc": "^2.0.0",
    "less": "^3.10.2",
    "less-loader": "^5.0.0",
    "mini-css-extract-plugin": "^0.8.0",
    "mocker-api": "^1.8.1",
    "optimize-css-assets-webpack-plugin": "^5.0.3",
    "postcss-loader": "^3.0.0",
    "style-loader": "^1.0.0",
    "terser-webpack-plugin": "^1.4.1",
    "uglifyjs-webpack-plugin": "^2.2.0",
    "url-loader": "^2.1.0",
    "vue-loader": "^15.7.1",
    "vue-style-loader": "^4.1.2",
    "vue-template-compiler": "^2.6.10",
    "webpack": "^4.39.2",
    "webpack-cli": "^3.3.7",
    "webpack-dev-server": "^3.8.0"
  }
}
const path = require('path');
const webpack = require('webpack');
// script、link动态添加每次compile后的hash,防止引用缓存的外部文件问题; 自动创建html入口文件
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 用terser-webpack-plugin替换掉uglifyjs-webpack-plugin解决uglifyjs不支持es6语法问题
const TerserJSPlugin = require('terser-webpack-plugin');
// 此模块至少需要Node v6.9.0和webpack v4.0.0  混淆代码
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
// 这个插件将CSS提取到单独的文件中 支持按需加载CSS和SourceMaps 建立在webpack v4上
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
// 用于优化、压缩CSS资源
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
// 打包时将之前打包的目录里的文件先清除干净,再生成新的
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
//mock数据
const apiMocker = require('mocker-api');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
//获取 npm run 后面的命令
const lifecycle = process.env.npm_lifecycle_event;
//npm run 的文件名
const project = lifecycle.split('-')[0];
//npm run 的环境名
const proMode = lifecycle.split('-')[1] === 'te';
const envMode = lifecycle.split('-')[1];
const webpackConfig = {
    mode: proMode ? 'production' : 'development',
    //打包入口为每个项目的main.js
    entry: path.resolve(__dirname, `${project}/main.js`),
    output: {
        //打包后的文件目录为各自项目名+Dist
        path: path.resolve(__dirname, `${project}Dist`),
        //打包后源代码映射
        // devtool: proMode ?'cheap-module-eval-source-map':'hidden-source-map',
        // devtool: "inline-source-map",
        //打包后的出口js目录
        filename: 'js/[name].[hash].js',
        //分块打包的js目录
        chunkFilename: proMode ? 'js/[name].[contenthash].bundle.js' : 'js/[name].bundle.js',
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                exclude: /node_modules/,
                use: {
                    loader: 'vue-loader'
                }
            },
            {
                test: /\.(le|c)ss$/i,
                use: [
                    proMode ? MiniCssExtractPlugin.loader :
                        'vue-style-loader',
                    'css-loader',
                    'postcss-loader',
                    'less-loader',
                ],
            },
            {
                test: /\.html$/i,
                use: [
                    'html-withimg-loader'
                ]
            },
            {
                test: /\.(png|jpg|gif)$/i,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            limit: 4096,
                            name: 'img/[name].[contenthash].[ext]'
                        },
                    },
                ],
            },
            {
                test: /\.m?js$/,
                exclude: /(node_modules|bower_components)/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env']
                    }
                }
            }
        ]
    },
    //webpack-dev-server 配置npm run 时启动本地服务
    devServer: {
        contentBase: `./${project}Dist`,
        inline: true //实时刷新
    },
    //优化
    optimization: {
        // 分块
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    chunks: 'all'
                },
            }
        }
    },
    plugins: [
        new CleanWebpackPlugin(),
        //定义插件—— 在项目中可以读取到
        new webpack.DefinePlugin({
            'baseUrl':proMode ? 'https:www.baidu.com':JSON.stringify('localhost')
        }),
        new HtmlWebpackPlugin({
            title:'webpack练习',
            filename: 'index.html',
            template: `${project}/index.html`,
            // 对 html 文件进行压缩
            minify: {
                //是否对大小写敏感,默认false
                caseSensitive: false,

                //是否简写boolean格式的属性如:disabled="disabled" 简写为disabled  默认false
                collapseBooleanAttributes: true,
                //是否去除空格,默认false
                collapseWhitespace: true,

                //是否压缩html里的css(使用clean-css进行的压缩) 默认值false;
                minifyCSS: true,

                //是否压缩html里的js(使用uglify-js进行的压缩)
                minifyJS: true,
                //是否移除注释 默认false
                removeComments: true,
                //Prevents the escaping of the values of attributes
                preventAttributesEscaping: true,

                //是否移除属性的引号 默认false
                removeAttributeQuotes: true,

                //从脚本和样式删除的注释 默认false
                removeCommentsFromCDATA: true,

                //是否删除空属性,默认false
                removeEmptyAttributes: false,

                //  若开启此项,生成的html中没有 body 和 head,html也未闭合
                removeOptionalTags: false,

                //删除多余的属性
                removeRedundantAttributes: true,

                //删除script的类型属性,在h5下面script的type默认值:text/javascript 默认值false
                removeScriptTypeAttributes: true,

                //删除style的类型属性, type="text/css" 同上
                removeStyleLinkTypeAttributes: true,

                //使用短的文档类型,默认false
                useShortDoctype: false,
            }
        }),
        new VueLoaderPlugin()
    ],
    //目录映射
    resolve: {
        alias: {
            '@assets': path.resolve(__dirname, `${project}/assets`),
            '@mixins': path.resolve(__dirname, `${project}/mixins`),
            '@tools': path.resolve(__dirname, `${project}/tools`),
            '@components': path.resolve(__dirname, `${project}/components`),
        }
    }
};
if (proMode) {
    webpackConfig.optimization.minimizer = [
        //混淆语法
        new UglifyJsPlugin({
            chunkFilter: (chunk) => {
                if (chunk.name === 'vendor') {
                    return false;
                }
                return true;
            },
            //去掉控制台日志
            uglifyOptions: {
                compress: {
                    drop_console: true
                }
            }
        }),
        new OptimizeCssAssetsPlugin({})
    ];
    // webpackConfig.optimization.minimizer = [new TerserJSPlugin({}),
    //     new OptimizeCssAssetsPlugin({}),];
    webpackConfig.plugins.push(
        new MiniCssExtractPlugin({
            filename: proMode ? '[name].css' : '[name].[hash].css',
            chunkFilename: proMode ? '[id].css' : '[id].[hash].css',
        })
    )
} else {
    // 热更新模块
    webpackConfig.plugins.push(new webpack.HotModuleReplacementPlugin());
    webpackConfig.devtool = 'inline-source-map';
    if (envMode == 'mock') {
        //mock环境,启用mock代理服务
        webpackConfig.devServer.before = (app) => {
            apiMocker(app, path.resolve(`${project}/mock/api.js`));
        };
        //非mock匹配项走测试环境
        webpackConfig.devServer.proxy = process.baseUrl;
    }
}
module.exports = webpackConfig;
05-04 15:20