63

图片描述

webpack,打包所有的资源

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

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

在此对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推出热刷新后,前端开发者在开环境下体验大幅提高。
  • 没有热刷新能力,我们修改一个组件后

clipboard.png

  • 加入热刷新后

clipboard.png

主要看一下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、首先引入AppContainre
import { 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热调试的流程图:

clipboard.png

加入 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 array
plugins: [
    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/],
            }),
        

加入预加载preload

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

加入预渲染


const PrerenderSPAPlugin = require('prerender-spa-plugin')

new PrerenderSPAPlugin({
            routes: ['/','/home','/shop'],
            staticDir: resolve(__dirname, '../dist'),
          }),

我这套webpack配置,无论多复杂的环境,都是可以搞定的
  • webpack真的非常非常重要,如果用不好,就永远是个初级前端
  • 只要webpack不更新到5,以后就不出webpack的文章了
  • webpack4大结局,谢谢
  • 以后会出一些偏向跨平台技术,原生javascriptTSGolang等内容的文章

PeterTan
14.4k 声望30k 粉丝