webpack,打包所有的资源

不知道不觉,webpack已经偷偷更新到4.34版本了,本人决定,这是今年最后一篇写webpack的文章,除非它更新到版本5,本人今年剩下的时间都会放在Golang和二进制数据操作以及后端的生态上

在看本文前,假设你对webpack有一定了解,如果不了解,可以看看我之前的手写ReactVue脚手架的文章

  • 手写优化版React脚手架
  • 手写Vue的脚手架
  • 前端性能优化不完全手册
  • 跨平台webpack配置
  • 都是百星star的优质文章
在此对webpack的性能优化进行几点声明:
  • 在部分极度复杂的环境下,需要双package.json文件,即实行三次打包
  • 在代码分割时,低于18K的文件没必要单独打包成一个chunk,http请求次数过多反而影响性能
  • prerenderPWA互斥,这个问题暂时没有解决
  • babel缓存编译缓存的是索引,即hash值,非常吃内存,每次开发完记得清理内存
  • babel-polyfill按需加载在某些非常复杂的场景下比较适合
  • prefetch,preload对首屏优化提升是明显
  • 代码分割不管什么技术栈,一定要做,不然就是垃圾项目
  • 多线程编译对构建速度提升也很明显
  • 代码分割配合PWA+预渲染+preload是首屏优化的巅峰,但是pwa无法缓存预渲染的html文件

本文的webpack主要针对React技术栈,实现功能如下:

  • 开发模式热更新
  • 识别JSX文件
  • 识别class组件
  • 代码混淆压缩,防止反编译代码,加密代码
  • 配置alias别名,简化import的长字段
  • 同构直出,SSR的热调试(基于Node做中间件)
  • 实现javaScripttree shaking 摇树优化 删除掉无用代码
  • 实现CSStree shaking
  • 识别 async / await 和 箭头函数
  • react-hot-loader记录react页面留存状态state
  • PWA功能,热刷新,安装后立即接管浏览器 离线后仍让可以访问网站 还可以在手机上添加网站到桌面使用
  • preload 预加载资源 prefetch按需请求资源
  • CSS模块化,不怕命名冲突
  • 小图片的base64处理
  • 文件后缀省掉jsx js json
  • 实现React懒加载,按需加载 , 代码分割 并且支持服务端渲染
  • 支持less sass stylus等预处理
  • code spliting 优化首屏加载时间 不让一个文件体积过大
  • 加入dns-prefetchpreload预请求必要的资源,加快首屏渲染(京东策略)
  • 加入prerender,极大加快首屏渲染速度
  • 提取公共代码,打包成一个chunk
  • 每个chunk有对应的chunkhash,每个文件有对应的contenthash,方便浏览器区别缓存
  • 图片压缩
  • CSS压缩
  • 增加CSS前缀 兼容各种浏览器
  • 对于各种不同文件打包输出指定文件夹下
  • 缓存babel的编译结果,加快编译速度
  • 每个入口文件,对应一个chunk,打包出来后对应一个文件 也是code spliting
  • 删除HTML文件的注释等无用内容
  • 每次编译删除旧的打包代码
  • CSS文件单独抽取出来
  • 让babel不仅缓存编译结果,还在第一次编译后开启多线程编译,极大加快构建速度
  • 等等....

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

webpack打包原理

  • 识别入口文件
  • 通过逐层识别模块依赖。(Commonjs、amd或者es6的import,webpack都会对其进行分析。来获取代码的依赖)
  • webpack做的就是分析代码。转换代码,编译代码,输出代码
  • 最终形成打包后的代码
  • 这些都是webpack的一些基础知识,对于理解webpack的工作机制很有帮助。

舒适的开发体验,有助于提高我们的开发效率,优化开发体验也至关重要

  • 组件热刷新、CSS热刷新
  • 自从webpack推出热刷新后,前端开发者在开环境下体验大幅提高。
  • 没有热刷新能力,我们修改一个组件后

  • 加入热刷新后

主要看一下React技术栈,如何在构建中接入热刷新

  • 无论什么技术栈,都需要在dev模式下加上 webpack.HotModuleReplacementPlugin插件
  devServer: {        contentBase: '../build',        open: true,        port: 5000,        hot: true    },

注:也可以使用react-hot-loader来实现,具体参考官方文档

在开发模式下也要代码分割,加快打开页面速度

optimization: {        runtimeChunk: true,        splitChunks: {            chunks: 'all',            minSize: 10000, // 提高缓存利用率,这需要在http2/spdy            maxSize: 0,//没有限制            minChunks: 3,// 共享最少的chunk数,使用次数超过这个值才会被提取            maxAsyncRequests: 5,//最多的异步chunk数            maxInitialRequests: 5,// 最多的同步chunks数            automaticNameDelimiter: '~',// 多页面共用chunk命名分隔符            name: true,            cacheGroups: {// 声明的公共chunk            vendor: {            // 过滤需要打入的模块            test: module => {            if (module.resource) {            const include = [/[\\/]node_modules[\\/]/].every(reg => {            return reg.test(module.resource);            });            const exclude = [/[\\/]node_modules[\\/](react|redux|antd)/].some(reg => {            return reg.test(module.resource);            });            return include && !exclude;            }            return false;            },            name: 'vendor',            priority: 50,// 确定模块打入的优先级            reuseExistingChunk: true,// 使用复用已经存在的模块            },            react: {            test({ resource }) {            return /[\\/]node_modules[\\/](react|redux)/.test(resource);            },            name: 'react',            priority: 20,            reuseExistingChunk: true,            },            antd: {            test: /[\\/]node_modules[\\/]antd/,            name: 'antd',            priority: 15,            reuseExistingChunk: true,            },            },        }    }

简要解释上面这段配置

  • 将node_modules共用部分打入vendor.js bundle中;
  • 将react全家桶打入react.js bundle中;
  • 如果项目依赖了antd,那么将antd打入单独的bundle中;(其实不用这样,可以看我下面的babel配置,性能更高)
  • 最后剩下的业务模块超过3次引用的公共模块,将自动提取公共块
注意 上面的配置只是为了给大家看,其实这样配置代码分割,性能更高
optimization: {        runtimeChunk: true,        splitChunks: {            chunks: 'all',                     }}

react-hot-loader记录react页面留存状态state

yarn add react-hot-loader
  // 在入口文件里这样写 import React from "react";import ReactDOM from "react-dom";import { AppContainer } from "react-hot-loader";-------------------1、首先引入AppContainreimport { BrowserRouter } from "react-router-dom";import Router from "./router"; /*初始化*/renderWithHotReload(Router);-------------------2、初始化 /*热更新*/if (module.hot) {-------------------3、热更新操作  module.hot.accept("./router/index.js", () => {    const Router = require("./router/index.js").default;    renderWithHotReload(Router);  });}  function renderWithHotReload(Router) {-------------------4、定义渲染函数  ReactDOM.render(    <AppContainer>      <BrowserRouter>        <Router />      </BrowserRouter>    </AppContainer>,    document.getElementById("app")  );}
然后你再刷新试试

React的按需加载,附带代码分割功能 ,每个按需加载的组件打包后都会被单独分割成一个文件

        import React from 'react'        import loadable from 'react-loadable'        import Loading from '../loading'         const LoadableComponent = loadable({            loader: () => import('../Test/index.jsx'),            loading: Loading,        });        class Assets extends React.Component {            render() {                return (                    <div>                        <div>这即将按需加载</div>                        <LoadableComponent />                    </div>                )            }        }                export default Assets

* 加入html-loader识别html文件

    {    test: /\.(html)$/,    loader: 'html-loader'    }

配置别名

    resolve: {    modules: [    path.resolve(__dirname, 'src'),     path.resolve(__dirname,'node_modules'),    ],    alias: {    components: path.resolve(__dirname, '/src/components'),    },    } 

加入eslint-loader

    {    enforce:'pre',    test:/\.js$/,    exclude:/node_modules/,    include:resolve(__dirname,'/src/js'),    loader:'eslint-loader'    }

resolve解析配置,为了为了给所有文件后缀省掉 js jsx json,加入配置

resolve: {    extensions: [".js", ".json", ".jsx"]}

加入HTML文件压缩,自动将入门的js文件注入html中,优化HTML文件

 new HtmlWebpackPlugin({            template: './public/index.html',            minify: {                removeComments: true,                collapseWhitespace: true,                removeRedundantAttributes: true,                useShortDoctype: true,                removeEmptyAttributes: true,                removeStyleLinkTypeAttributes: true,                keepClosingSlash: true,                minifyJS: true,                minifyCSS: true,                minifyURLs: true,            }        }),

SSR同构直出热调试

  • , 采用 webpack watch+nodemon 结合的模式实现对SSR热调试的支持。node 服务需要的html/js通过webpack插件动态输出,当nodemon检测到变化后将自动重启,html文件中的静态资源全部替换为dev模式下的资源,并保持socket连接自动更新页面。
  • 实现热调试后,调试流程大幅缩短,和普通非直出模式调试体验保持一致。下面是SSR热调试的流程图:

加入 babel-loader 还有 解析JSX ES6语法的 babel preset

  • @babel/preset-react解析 jsx语法
  • @babel/preset-env解析es6语法
  • @babel/plugin-syntax-dynamic-import解析react-loadableimport按需加载,附带code spliting功能
  • ["import", { libraryName: "antd-mobile", style: true }], Antd-mobile的按需加载
{                            loader: 'babel-loader',                            options: {   //jsx语法                                presets: ["@babel/preset-react",                                    //tree shaking 按需加载babel-polifill                                    ["@babel/preset-env", { "modules": false, "useBuiltIns": "false", "corejs": 2 }]],                                plugins: [                                    //支持import 懒加载                                     "@babel/plugin-syntax-dynamic-import",                                    //andt-mobile按需加载  true是less,如果不用less style的值可以写'css'                                     ["import", { libraryName: "antd-mobile", style: true }],                                    //识别class组件                                    ["@babel/plugin-proposal-class-properties", { "loose": true }],                                ],                                cacheDirectory: true                            },                        }
特别提示,如果电脑性能不高,不建议开启babel缓存索引,非常吃内存,记得每次开发完了清理内存

加入thread-loader,在babel首次编译后开启多线程

    const os = require('os')    {            loader: 'thread-loader',            options: {                workers: os.cpus().length                        }    }

加入单独抽取CSS文件的loader和插件

const MiniCssExtractPlugin = require('mini-css-extract-plugin')    {        test: /\.(less)$/,        use: [            MiniCssExtractPlugin.loader,            {                loader: 'css-loader', options: {                    modules: true,                    localIdentName: '[local]--[hash:base64:5]'                }            },            {loader:'postcss-loader'},            { loader: 'less-loader' }        ]    }         new MiniCssExtractPlugin({            filename:'[name].[contenthash:8].css'        }),

CSStree shaking

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')      ])    })] 

对小图片进行base64处理,减少http请求数量,并对输出的文件统一打包处理

{                    test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,                    loader: 'url-loader',                    options: {                        limit: 8 * 1024,                        name: '[name].[hash:8].[ext]',                    }                }, {                    exclude: /\.(js|json|less|css|jsx)$/,                    loader: 'file-loader',                    options: {                        outputPath: 'media/',                        name: '[name].[hash].[ext]'                    }                }                ]            }]    },    

加入单独抽取CSS文件的loader和插件

const MiniCssExtractPlugin = require('mini-css-extract-plugin')    {        test: /\.(less)$/,        use: [            MiniCssExtractPlugin.loader,            {                loader: 'css-loader', options: {                    modules: true,                    localIdentName: '[local]--[hash:base64:5]'                }            },            {loader:'postcss-loader'},            { loader: 'less-loader' }        ]    }         new MiniCssExtractPlugin({            filename:'[name].[contenthash:8].css'        }),

加入压缩css的插件

    const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')    new OptimizeCssAssetsWebpackPlugin({                cssProcessPluginOptions:{                    preset:['default',{discardComments: {removeAll:true} }]                }            }),

加入每次打包输出文件清空上次打包文件的插件

    const CleanWebpackPlugin = require('clean-webpack-plugin')        new CleanWebpackPlugin()

加入图片压缩

{                test: /\.(jpg|jpeg|bmp|svg|png|webp|gif)$/,                                use:[                    {loader: 'url-loader',                    options: {                        limit: 8 * 1024,                        name: '[name].[hash:8].[ext]',                        outputPath:'/img'                    }},                    {                        loader: 'img-loader',                        options: {                          plugins: [                            require('imagemin-gifsicle')({                              interlaced: false                            }),                            require('imagemin-mozjpeg')({                              progressive: true,                              arithmetic: false                            }),                            require('imagemin-pngquant')({                              floyd: 0.5,                              speed: 2                            }),                            require('imagemin-svgo')({                              plugins: [                                { removeTitle: true },                                { convertPathData: false }                              ]                            })                          ]                        }                      }                ]                                            }

加入代码混淆,反编译

var JavaScriptObfuscator = require('webpack-obfuscator');// ...// webpack plugins arrayplugins: [    new JavaScriptObfuscator ({      rotateUnicodeArray: true  }, ['excluded_bundle_name.js'])],

加入 PWA的插件 , WorkboxPlugin

  • pwa这个技术其实要想真正用好,还是需要下点功夫,它有它的生命周期,以及它在浏览器中热更新带来的副作用等,需要认真研究。可以参考百度的lavas框架发展历史~
const WorkboxPlugin = require('workbox-webpack-plugin')    new WorkboxPlugin.GenerateSW({                 clientsClaim: true, //让浏览器立即servece worker被接管                skipWaiting: true,  // 更新sw文件后,立即插队到最前面                 importWorkboxFrom: 'local',                include: [/\.js$/, /\.css$/, /\.html$/,/\.jpg/,/\.jpeg/,/\.svg/,/\.webp/,/\.png/],            }),        

加入预渲染prerener

new PreloadWebpackPlugin({            rel: 'preload',            as(entry) {                if (/\.css$/.test(entry)) return 'style';                if (/\.woff$/.test(entry)) return 'font';                if (/\.png$/.test(entry)) return 'image';                return 'script';            },            include: 'allChunks'            //include: ['app']        }),
我这套webpack配置,无论多复杂的环境,都是可以搞定的
  • webpack真的非常非常重要,如果用不好,就永远是个初级前端
  • 只要webpack不更新到5,以后就不出webpack的文章了
  • webpack4大结局,谢谢
  • 以后会出一些偏向跨平台技术,原生javascriptTSGolang等内容的文章