webpack4+react16+react-router-dom4从零配置到优化,实现路由按需加载(上)

2,177 阅读11分钟

之前一直用vue+webpack在写前端项目,最近换了个环境后,项目都是用react来开发的,所以也想捣鼓下框架啥的东西,最近也看了下webpack4,感觉这玩意儿越来越集成化了,有些插件已经被丢弃了或者集成了,不过在实际项目开发中,也存在一些依赖版本方面的问题,本文将带你走进webpack的世界,一起看看那些坑吧。

本人写的实例,已上传至GitHub,点击项目地址可以查看详情,欢迎star哦。

前言

webpack作为时下前端最流行的自动化构建工具,其版本更新也是一路备受关注,目前大多数新项目都会采用webpack4.0+去构建,接下来在了解如何用webpack从零开始搭建自己的项目之前,我们先还是熟悉下webpack的基本概念:

webpack: 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle文件被浏览器识别使用。

WebPack和Grunt以及Gulp相比有什么特性

gulpgrunt是基于流的一种管理工具,通过建立一个个task任务,配置执行用户需要的功能,如格式检验,代码压缩等,值得一提的是,经过这两者处理的代码只是局部变量名被替换简化,整体并没有发生改变,还是你的代码。

webpack是基于入口文件,识别模块依赖关系,分析代码,转换代码,编译代码,输出代码,经过打包后的代码基本已经是认不出来了,有点像压缩的jq那样的感觉,webpack它本身也是一个node的模块,webpack.config.js是以commonjs形式书写的(node中的模块化是commonjs规范的)。

webpack核心概念:

  • entry: 一个可执行模块或库的入口文件。
  • Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件,Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
  • chunk: 代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
  • loader: 模块转换器,例如把es6转换为es5,scss转换为css。
  • plugin: 扩展插件,用于扩展webpack的功能,在webpack构建生命周期的节点上加入扩展hook为webpack加入功能。
  • Output:输出结果,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果。

熟悉了webpck的一些基本概念后,接下来,我们从零开始真正的配置下webpack.config.js文件。

一、webpack.config配置

1、创建项目

mkdir react-webpack4
cd react-webpack4
mkdir src
mkdir dist
npm init -y

// 安装webpack和相关模块
yarn add webpack webpack-cli webpack-dev-server -D   //webpack4.X 把webpack拆分了
touch webpack.config.js         // mac上创建文件 
echo test>webpack.config.js     // win上创建文件
module.exports = {
    entry: ["./src/index.js"],  //  项目文件入口
    output: {
        // 输出目录
        path: path.resolve(__dirname, "../dist")
    },
    module: {},
    plugins:[],
    devServer: {}
} 

webpack.config.js的文件结构就是如上,但是在项目开发时, 往往开发环境和生产环境在配置上会有些不同,所以为了区分开来,我们在项目根目录下创建一个build的文件夹,在该文件夹下面创建以下三个文件:

webpack.base.config.js  // 基本配置
webpack.dev.config.js   // 开发环境
webpack.prod.config.js  // 生产环境

接下来先看下webpack.base.config.js基本配置:

const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const HappyPack = require('happypack');
const os = require('os');
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });

module.exports = {
    entry: ["./src/index.js"],
    output: {
        // 输出目录
        path: path.resolve(__dirname, "../dist")
    },
    resolve: {
        extensions: [".js", ".jsx"], // 扩展
        alias: {
            '@': path.resolve(__dirname, 'src'),
            '@pages': path.resolve(__dirname, 'src/pages'),
            '@router': path.resolve(__dirname, 'src/router'),
            '@assets': path.resolve(__dirname, 'src/assets')
        }
    },
    module: {
        rules: [
            {
                test: /\.(js|jsx?)$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: "happypack/loader?id=happyBabel"
                    }
                ]
            },
            {
                test: /\.(le|c)ss$/,
                use: [
                    MiniCssExtractPlugin.loader,
                    "css-loader", // 编译css
                    "postcss-loader", // 使用 postcss 为 css 加上浏览器前缀
                    "less-loader", // 编译less
                ]
            },
            {
                test: /\.(png|jpg|jpeg|gif|svg)/,
                use: {
                    loader: "url-loader",
                    options: {
                        outputPath: "images/", // 图片输出的路径
                        limit: 10 * 1024
                    }
                }
            },
            {
                test: /\.(eot|woff2?|ttf|svg)$/,
                use: [
                    {
                        loader: 'url-loader',
                        options: {
                            name: '[name]-[hash:5].min.[ext]',
                            limit: 5000, // 使用base64进行转换, 大小限制小于5KB, 否则使用svg输出
                            publicPath: 'fonts/',
                            outputPath: 'fonts/'
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            title:'webpack配置',  //  项目标题
            filename: "index.html", // 最终创建的文件名
            template: path.resolve(__dirname, '..', "src/index.html"), // 指定模板路径
            minify: {
                collapseWhitespace: true // 去除空白
            }
        }),
        // 单独生成css文件和js文件分离开来 加快页面渲染
        new MiniCssExtractPlugin({
            filename: "[name]-[hash:5].css",
            chunkFilename: "[id]-[hash:5].css"
        }),
        // happypack
        new HappyPack({
            //用id来标识 happypack处理那里类文件
            id: 'happyBabel',
            //如何处理  用法和loader 的配置一样
            loaders: [{
                loader: 'babel-loader?cacheDirectory=true',
            }],
            //共享进程池threadPool: HappyThreadPool 代表共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多。
            threadPool: happyThreadPool,
            //允许 HappyPack 输出日志
            verbose: true,
        }),

    ],
    performance: false // 关闭性能提示
};

以上配置,可能你见过,但是不一定知道有些东西用来干嘛的,做个解释:

1: HtmlWebpackPlugin()

yarn add  html-webpack-plugin 
    new HtmlWebpackPlugin({
        title:'webpack配置',  //  网站标题
        filename: "index.html", // 最终创建的文件名
        template: path.resolve(__dirname, '..', "src/index.html"), // 指定模板路径
        minify: {
            collapseWhitespace: true // 去除空白
        }
    }),

使用 HtmlWebpackPlugin插件,来生成 html, 并将每次打包的js自动插入到你的 index.html 里面去,而且它还可以基于你的某个 html 模板来创建最终的 index.html,也就是说可以指定模板

2、MiniCssExtractPlugin()

yarn add mini-css-extract-plugin 

如果不做该配置,我们的css是直接打包进js里面的,我们希望能单独生成css文件。 因为单独生成css,css可以和js并行下载,提高页面加载效率。 特别说明:目前 extract-text-webpack-plugin 最新版本不支持 Webpack 4.3.0 版本. Webpack 4.2.0 一下可用。 目前从 extract-text-webpack-plugin issues 了解, 未来 extract-text-webpack-plugin 将废弃,作者建议使用 mini-css-extract-plugin

    {
        test: /\.(le|c)ss$/,
        use: [
            MiniCssExtractPlugin.loader,
            "css-loader", // 编译css
            "postcss-loader", // 使用 postcss 为 css 加上浏览器前缀
            "less-loader", // 编译less
        ]
    },
new MiniCssExtractPlugin({
            filename: "[name]-[contenthash:5].css",
            chunkFilename: "[id]-[contenthash:5].css"
 }),

这里补充说明下,webpack中对于输出文件名可以有三种hash值:

hash: 如果都使用hash的话,因为这是工程级别的,即每次修改任何一个文件,所有文件名的hash至都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效。

chunkhash: 根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响,但是同一个模块,就算将js和css分离,其哈希值也是相同的,修改一处,js和css哈希值都会变,同hash,没有做到缓存意义。

contenthash:表示由文件内容产生的hash值,内容不同产生的contenthash值也不一样。在项目中,通常做法是把项目中css都抽离出对应的css文件来加以引用。(只要文件内容不一样,产生的哈希值就不一样)

所以css文件最好使用contenthash。

3、使用happypack并发执行任务

yarn  add happypack

运行在 Node.之上的Webpack是单线程模型的,也就是说Webpack需要一个一个地处理任务,不能同时处理多个任务。 Happy Pack 就能让Webpack做到这一点,它将任务分解给多个子进程去并发执行,子进程处理完后再将结果发送给主进程。

plugins:[
      new HappyPack({
      //用id来标识 happypack处理那里类文件
        id: 'happyBabel',
        //如何处理  用法和loader 的配置一样
        loaders: [{
            loader: 'babel-loader?cacheDirectory=true',
        }],
        //共享进程池threadPool: HappyThreadPool 代表共享进程池,即多个 HappyPack 实例都使用同一个共享进程池中的子进程去处理任务,以防止资源占用过多。
        threadPool: happyThreadPool,
        //允许 HappyPack 输出日志
        verbose: true,
  })
]

以上是对webpack.base.config.js基础配置的一个解释说明,接下来咱们再看看再开发环境:webpack.dev.config.js

const path = require("path");
const merge = require('webpack-merge')
const commonConfig = require('./webpack.base.config.js')
const webpack = require("webpack");
const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin');
module.exports = merge(commonConfig, {
    mode: "development", //webpack 4.0 要申明这个
    devtool: 'cheap-module-eval-soure-map',
    output: {
        // 输出目录
        path: path.resolve(__dirname, "../dist"),
        // 文件名称
        filename: "bundle.js",
        chunkFilename: '[name].js'
    },
    plugins: [
        //开启HMR(热替换功能,替换更新部分,不重载页面!) 相当于在命令行加 --hot
        new webpack.HotModuleReplacementPlugin(),
        new webpack.DefinePlugin({
            'ENV': JSON.stringify("development"),
            'process.env': {
                VUEP_BASE_URL: '/'
            }
        }),
              //识别某些类别的webpack错误
        new FriendlyErrorsPlugin({
            // 运行成功
            compilationSuccessInfo: {
            message: ['你的应用程序在这里运行http://localhost:8001'],
            notes: ['有些附加说明要在成功编辑时显示']
            },
            // 运行错误
            onErrors: function(severity, errors){
            //您可以收听插件转换和优先级的错误,严重性可以是'错误''警告'
            },
            //是否每次编译之间清除控制台,默认为true
            clearConsole: true,  
            //添加格式化程序和变换器(见下文)
            additionalFormatters: [],
            additionalTransformers: []
        })
    ],
    devServer: {
        contentBase: path.resolve(__dirname, "../dist"), //  指定访问资源目录
        historyApiFallback: true, //  该选项的作用所有的404都连接到index.html
        disableHostCheck: true,   //  绕过主机检查
        inline: true,             //  改动后是否自动刷新
        host: 'localhost',        //  访问地址
        port: 8001,               // 访问端口
        overlay: true,         //  出现编译器错误或警告时在浏览器中显示全屏覆盖
        stats: "errors-only",     // 显示捆绑软件中的错误
        compress: true,           // 对所有服务启用gzip压缩
        open: true,               // 自动打开浏览器
        progress: true,            // 显示编译进度
        proxy: {
            // 代理到后端的服务地址
            "/api": "http://localhost:8000"
        }
    }
});

4 webpack-dev-server

    devServer: {
        contentBase: path.resolve(__dirname, "../dist"), //  指定访问资源目录
        historyApiFallback: true, //  该选项的作用所有的404都连接到index.html
        disableHostCheck: true,   //  绕过主机检查
        inline: true,             //  改动后是否自动刷新
        host: 'localhost',        //  访问地址
        port: 8001,               // 访问端口
        overlay: true,         //  出现编译器错误或警告时在浏览器中显示全屏覆盖
        stats: "errors-only",     // 显示捆绑软件中的错误
        compress: true,           // 对所有服务启用gzip压缩
        open: true,               // 自动打开浏览器
        progress: true,            // 显示编译进度
        proxy: {
            // 代理到后端的服务地址
            "/api": "http://localhost:8000"
        }
    }

webpack4已经集成了open-browser-webpack-plugin 和 progress-bar-webpack-plugin这两个插件,所以不需要再单独引入,直接将 devServer.open 和devServer.progress设置为true即可。

5、mode

mode: "development/production", webpack 4.0+ 要申明这个

6、output

output: {
    // 输出目录
    path: path.resolve(__dirname, "../dist"),
    // 文件名称
    filename: "bundle.js",
    chunkFilename: '[name].js'
},

7、FriendlyErrorsPlugin

yarn add friendly-errors-webpack-plugin

Friendly-errors-webpack-plugin识别某些类别的webpack错误,并清理,聚合和优先级,以提供更好的开发人员体验。

    //识别某些类别的webpack错误
    new FriendlyErrorsPlugin({
        // 运行成功
        compilationSuccessInfo: {
        message: ['你的应用程序在这里运行http://localhost:8001'],
        notes: ['有些附加说明要在成功编辑时显示']
        },
        // 运行错误
        onErrors: function(severity, errors){
        //您可以收听插件转换和优先级的错误, 严重性可以是'错误''警告'
        },
        //是否每次编译之间清除控制台,默认为true
        clearConsole: true,  
        //添加格式化程序和变换器(见下文)
        additionalFormatters: [],
        additionalTransformers: []
    })

最后,咱们看看再实际生产环境中,webpack.dev.config.js的配置:

const path = require("path");
const webpack = require("webpack");
const merge = require('webpack-merge')
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const commonConfig = require('./webpack.base.config.js')
const PurifyCSS = require('purifycss-webpack')
const glob = require('glob-all')
const WorkboxPlugin = require('workbox-webpack-plugin') // 引入 PWA 插件

module.exports = merge(commonConfig, {
    mode: "production", //webpack 4.0 要申明这个
    output: {
        // 输出目录
        path: path.resolve(__dirname, "../dist"),
        // 文件名称
        filename: '[name].[contenthash:5].js',
        chunkFilename: '[name].[contenthash:5].js'
    },
    devtool: 'cheap-module-source-map',
    optimization: {
        usedExports: true,   //js Tree Shaking 清除到代码中无用的js代码,只支持import方式引入,不支持commonjs的方式引入
        splitChunks: {
            chunks: "all", // 所有的 chunks 代码公共的部分分离出来成为一个单独的文件
            cacheGroups: {
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors'
                }
            }
        },
    },
    plugins: [
        new CleanWebpackPlugin(),// 打包时删除之前的文件
        // 清除无用 css---生产环境---csstree-shaking
        new PurifyCSS({
            paths: glob.sync([
                // 清除无用 css---生产环境-- 对于 css的tree shaking 使用 purifycss-webpack 配合 glob-all来实现 。
                path.resolve(__dirname, '..', 'src/*.html'),
                path.resolve(__dirname, '..', 'src/*.js'),
                path.resolve(__dirname, '..', 'src/**/*.jsx'),
            ])
        }),
        // PWA配置,生产环境才需要,PWA优化策略,在你第一次访问一个网站的时候,如果成功,做一个缓存,当服务器挂了之后,你依然能够访问这个网页 ,这就是PWA。那相信你也已经知道了,这个只需要在生产环境,才需要做PWA的处理,以防不测。
        new WorkboxPlugin.GenerateSW({
            clientsClaim: true,
            skipWaiting: true
        }),
    ]
});

8、插件 CleanWebpackPlugin

你执行yarn run build的时候,每次都会在dist目录下边生成一堆文件,但是上一次的打包的文件还在,这个我们需要每次打包时清除 dist 目录下旧版本文件

yarn add  clean-webpack-plugin

注意: 这个引入的坑,最新版的需要这样引入,而不是直接

//  报错:const CleanWebpackPlugin =  require('clean-webpack-plugin')
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
 
plugins: [
    new CleanWebpackPlugin() 
]

9、使用 source-map,对devtool进行优化

webpack中devtool选项用来控制是否生成,以及如何生成 source map。简言之,source map就是帮助我们定位到错误信息位置的文件。正确的配置source map,能够提高开发效率,更快的定位到错误位置。

devtool:"cheap-module-eval-source-map",// 开发环境配置
devtool:"cheap-module-source-map",   // 线上生成配置

10、css Tree Shaking 以去除项目代码中用不到的 CSS 样式,仅保留被使用的样式代码,达到减轻包的体积。

yarn  add glob-all purify-css purifycss-webpack 
const PurifyCSS = require('purifycss-webpack')
const glob = require('glob-all')
plugins:[
    // 清除无用 css
    new PurifyCSS({
      paths: glob.sync([
        // 要做 CSS Tree Shaking 的路径文件
        path.resolve(__dirname, './src/*.html'), // 请注意,我们同样需要对 html 文件进行 tree shaking
        path.resolve(__dirname, './src/*.js')
      ])
    })
]

11、js Tree Shaking

在webpack4中已经移除了UglifyJsPlugin,只需要配置mode为"production",即可显式激活 UglifyjsWebpackPlugin 插件。

清除到代码中无用的js代码,只支持import方式引入,不支持commonjs的方式引入

只要mode是production就会生效,develpoment的tree shaking是不生效的,因为webpack为了方便你的调试

  optimization: {
    usedExports:true,
  }

12、PWA优化策略

yarn add workbox-webpack-plugin

PWA的作用就是当你第一次成功访问网站是,做一个缓存,那么在服务器挂了的情况下,你依然可以访问这个网页,这个只需要在生产环境下处理,以防不测。

const WorkboxPlugin = require('workbox-webpack-plugin')
new WorkboxPlugin.GenerateSW({
    clientsClaim: true,
    skipWaiting: true
}),

以上是关于本人在webpack在开发环境和生产环境的一些配置和优化,欢迎大家继续补充和指正,大家对webpack有兴趣的也可以先去官网了解下webpack的一些知识点。

由于篇幅太长,本文分为两部分来写,这一篇主要讲解下webpack方面的配置。

下一篇 将写一些关于基于react-router4.0实现路由的按需加载。