美文网首页
Webpack版本升级与打包优化

Webpack版本升级与打包优化

作者: nimw | 来源:发表于2019-05-06 10:57 被阅读0次

    1. 9.0版本

    1.1 动态路由修改

    1. 打包分析
    Show chunks: All (3.3 MB)
    js/app.f6e590defd09f0af0533.js (1.23 MB)
    js/0.dcb7a6fef441f8b29aad.js (759.71 KB)
    js/1.12b87a568748b35be091.js (753 KB)
    js/vendor.3a71817ce04ee0d52abd.js (477.57 KB)
    js/2.b41119393c5fed1db5a7.js (50.59 KB)
    js/3.1e900085df4e92b118d0.js (44.05 KB)
    js/4.cd0ae5676c3c19029aaa.js (30.54 KB)
    js/5.43b81f7e8267bd84e89e.js (2.27 KB)
    js/6.8d54cf0b69937a8c4ca4.js (1.73 KB)
    js/manifest.aab2c6e6e57bd770452c.js (1.58 KB)
    
    image.png

    (1) 动态路由生成js文件没有自定义名
    (2) 0.js1.js代码几乎都是重复的。
    (3) 2.js3.js4.js动态路由生成的代码体积太小。

    1. 设置打包配置chunkFilename
      webpack.config.production.js
        output: {
            path: config.path.dist,
    -        filename: 'js/[name].[chunkhash].js',
    -        chunkFilename: 'js/[id].[chunkhash].js',
    +        filename: 'js/[name].[chunkhash:5].js',
    +        chunkFilename: 'js/[name].[chunkhash:5].js',
            publicPath: '?op=resource&encode=utf8&resource=/com/fr/wei/plugin/h5reportnew/dist/'
        },
    
    1. 自定义chunkName并进行路由合并
      routerConfig.web.js
    -import Login from 'bundle-loader?lazy!../../platform/view/Login/'
    -import Directory from 'bundle-loader?lazy!../../platform/view/Directory'
    -import ReportPage from 'bundle-loader?lazy!../../fr/page/ReportPage'
    -import FormPage from 'bundle-loader?lazy!../../fr/page/FormPage'
    -import WebPage from 'bundle-loader?lazy!../../platform/view/WebPage'
    -import ChangePassword from 'bundle-loader?lazy!../../platform/view/ChangePassword/ChangePassword'
    -import CprPage from 'bundle-loader?lazy!../../fr/page/CprPage'
    +import Login from 'bundle-loader?lazy&name=platform!../../platform/view/Login/'
    +import Directory from 'bundle-loader?lazy&name=platform!../../platform/view/Directory'
    +import ReportPage from 'bundle-loader?lazy&name=FRPage!../../fr/page/ReportPage'
    +import FormPage from 'bundle-loader?lazy&name=FRPage!../../fr/page/FormPage'
    +import WebPage from 'bundle-loader?lazy&name=platform!../../platform/view/WebPage'
    +import ChangePassword from 'bundle-loader?lazy&name=platform!../../platform/view/ChangePassword/ChangePassword'
    +import CprPage from 'bundle-loader?lazy&name=platform!../../fr/page/CprPage'
    
    1. 打包分析
    Show chunks: All (2.57 MB)
    js/app.3027a.js (1.23 MB)
    js/FRPage.792bb.js (771.79 KB)
    js/vendor.6f983.js (477.57 KB)
    js/platform.aee33.js (128.05 KB)
    js/manifest.0559a.js (1.46 KB)
    
    image.png

    1.2 图表资源提前加载

    1. 图表资源加载分析
      ChartView组件的didMount中请求加载图表资源,导致图表空白1-2s才能渲染出来。
    2. 提前加载图表资源
    <link rel="stylesheet" type="text/css" href="<%= htmlWebpackPlugin.options.sourcePath + '/com/fr/web/core/css/leaflet.css' %>"/>
    <script type="text/javascript" src="<%= htmlWebpackPlugin.options.sourcePath + '/com/fr/mobile/js/appChart.js' %>"></script>
    <script type="text/javascript" src="<%= htmlWebpackPlugin.options.sourcePath + '/com/fr/web/core/js/vancharts-all.js' %>"></script>
    
    1. 时间对比


      测试数据.png

    1.3 打包区分不同入口

    1. 多入口分析
      项目支持多入口,不同入口打开同一个html页面。页面初始加载app.jsvendor.jsmanifest.js资源。当路由跳转到platformFRPage对应页面时,动态加载platform.jsFRPage.js
      当从模板页(cptfrm)进入项目时,FRPage.js仍通过动态路由的方式加载,加载时机有些延迟,增加了页面打开时间(客户反映提升不大)。
    2. 打包生成多个html页面,不同入口打开不同的html页面。当从登录页进入项目时,使用动态路由;当直接打开cpt或者frm模板时,不使用动态路由,提前加载对应js资源。
    3. 打包分析
    Show chunks: All (4.55 MB)
    js/sync.3bd37d4.js (1.98 MB)
    js/app.cf1d197.js (1.23 MB)
    js/FRPage.2285cd3.js (770.97 KB)
    js/vendor.72c6bc3.js (478.69 KB)
    js/platform.d5fb09a.js (128.02 KB)
    js/manifest.43d0f75.js (1.46 KB)
    
    image.png

    1.4 代码分割

    1. Performance分析图
      有几个图表的表单.png
    2. 生成的app.js体积很大,但CommonsChunkPlugin只适用于多入口文件公共模块代码分割。本项目入口只有一个index.web.js,不符合CommonsChunkPlugin插件的应用场景。
    3. webpack升级
      (1) Cyclic dependency error
      解决:https://github.com/marcelklehr/toposort/issues/20
      (2) 其他见10.0 webpack升级遇到问题及解决方案。
    4. 代码分割
    optimization: {
        splitChunks: {
            chunks: 'all',
            maxInitialRequests: 6,
            automaticNameDelimiter: '-',
            cacheGroups: {
                //base代码分割
                baseComponent: {
                    test: /[\\/]public[\\/]base[\\/]components/,
                    priority: 5,
                    name: 'baseComponent'
                },
                appBase: {
                    test: /[\\/]public[\\/]base/,
                    priority: 0,
                    name: 'appBase'
                },
                //react
                react: {
                    test: /[\\/]node_modules[\\/](react\.*|redux\.*)|[\\/]lib[\\/]reactweb/,
                    priority: -5,
                    name: 'react',
                },
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10,
                    name: 'vendor'
                }
            }
        }
    }
    
    1. 时间对比


      测试数据

    2. 10.0版本打包优化

    2.1 知识扩展

    1. babel-node 命令
    2. minimist 轻量级的命令行参数解析引擎
    3. define-plugin

    2.2 问题分析

    1. happypack报错
      Cannot read property 'length' of undefined
    if (resolve.length === 4) {
                    ^
    TypeError: Cannot read property 'length' of undefined
        at resolveLoader (...\node_modules\happypack\lib\WebpackUtils.js:138:17)
        at ...\node_modules\happypack\lib\WebpackUtils.js:126:7
        at ...\node_modules\happypack\node_modules\async\lib\async.js:713:13
    

    分析:happypack 4.X不兼容webpack 4.X
    解决:升级happypack5.X版本。

    1. file-loader报错
      Cannot read property 'fileLoader' of undefined
    ERROR in ./node_modules/rs-styles/images/sampleStamp.png
    Module build failed: TypeError: Cannot read property 'fileLoader' of undefined
        at Object.module.exports (/Users/uri/Documents/connect/dashboard/node_modules/file-loader/index.js:14:28)
        at Object.module.exports (/Users/uri/Documents/connect/dashboard/node_modules/url-loader/index.js:31:23)
    

    分析:url-loader使用了旧版本的file-loader
    解决:更新升级file-loader版本。
    参考:https://github.com/webpack/webpack/issues/6419

    1. file-loader报错
      file-loader fails with JSON files in Webpack 4
    Module parse failed: Unexpected token m in JSON at position 0
    You may need an appropriate loader to handle this file type.
    SyntaxError: Unexpected token m in JSON at position 0
        at JSON.parse (<anonymous>)
        at JsonParser.parse (/Users/jeremy/Documents/Development/webpack-file-loader-test/node_modules/webpack/lib/JsonParser.js:15:21)
    

    分析:在webpack4.X版本中,默认支持json文件,无需loader处理。
    解决:删除json处理的loader({test: /\.json$/,use: 'json-loader'})。
    参考:https://github.com/webpack-contrib/file-loader/issues/259
    4. 动态import()报错(☆☆)

    ERROR in ./pages/Home/index.tsx 5:16
    Module parse failed: Unexpected token (5:16)
    You may need an appropriate loader to handle this file type.
    | import { BeatLoader } from 'react-spinners';
    | export const LoadableHomePage = Loadable({
    >   loader: () => import(
    |   /* webpackChunkName: "homepage" */
    |   './page'),
    

    方法一:npm update acorn --depth 20npm dedupe、删除node_modules、删除package.lock.jsonnpm install
    方法二:npm install webpack@4.28.4
    参考:https://github.com/webpack/webpack/issues/8656

    1. uglifyjs-webpack-plugin压缩动态import()语法报错
      分析:uglifyjs-webpack-plugin只支持ES5代码压缩,不支持ES6代码压缩。
      解决:使用terser-webpack-plugin替换掉uglifyjs-webpack-plugin
      参考:https://webpack.docschina.org/plugins/terser-webpack-plugin
    2. import()动态加载魔法注释webpackChunkName失效
      分析:①在项目中使用了module:metro-react-native-babel-preset,该presetbabelReactNative应用提供的,ReactNative应用默认使用它转化代码。②该preset支持动态import(),无需再使用@babel/plugin-syntax-dynamic-import。③ 该preset会在babel打包过程中删除注释内容。
      image.png
      解决:babel配置中添加comments: true
      参考:https://github.com/webpack/webpack/issues/4861
      https://babeljs.io/docs/en/options#comments
      https://www.npmjs.com/package/metro-react-native-babel-preset
    3. 打包生成的bundle中含有动态import()之外的js文件
      image.png
      分析:optimization.splitChunks在进行代码分割时,会默认将不同chunk引入的相同modules进行分割,避免这些代码重复打包到不同的bundle
      解决:配置optimization.splitChunks中的cacheGroups,将共用代码提取到自定义的group中。
    optimization: {
      splitChunks: {
         chunks: 'all',
         cacheGroups: {
           commons: {
            minChunks: 2,
            priority: -20,
            name: 'commons',
          }
        }
      }
    }
    
    1. webpack打包资源过大警告
    WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
    This can impact web performance.
    Assets: 
      js/commons.532a8.js (515 KiB)
      js/vendor.b2172.js (414 KiB)
      js/BIPage.dafcb.js (477 KiB)
      js/app.c64e5.js (1.2 MiB)
    
    WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
    Entrypoints:
      app (1.77 MiB)
          js/react.97c29.js
          js/vendor.b2172.js
          js/app.c64e5.js
    

    分析:webpack推荐打包生成的asset单个资源大小和入口资源大小在244 KiB以内。
    隐藏:performance: { hints: false }
    参考:https://github.com/webpack/webpack/issues/3486

    1. Tree Shaking为何不生效?
      分析:关于lodash,已经通过babel-plugin-lodash实现了Tree Shaking。对于业务模块,由于react-native使用的打包工具metro不支持Tree Shaking,因此不支持。
      解决:如果babel配置文件中使用module:metro-react-native-babel-preset,则可以支持Tree Shaking。但这样会导致react-native项目无法打包。
      参考:https://www.npmjs.com/package/metro-react-native-babel-preset
      https://github.com/facebook/metro/issues/227

    2.3 打包优化

    2.3.1 代码分割

    1. 同步代码分割
      通过optimization.splitChunks.cacheGroups,对basereactwebreact以及node_modules分别进行同步代码分割,生成appBase.jsreactweb.jsreact.js以及vendors.js
    optimization: {
        splitChunks: {
            chunks: 'all',
            maxInitialRequests: 5,
            automaticNameDelimiter: '-',
            cacheGroups: {
                //同步模块代码分割
                appBase: {
                    test: /[\\/]public[\\/]base/,
                    priority: 0,
                    name: 'appBase',
                    chunks: 'initial'
                },
                reactWeb: {
                    test: /[\\/]lib[\\/]reactweb/,
                    priority: 0,
                    name: 'reactWeb',
                    chunks: 'initial'
                },
    
                //node_modules代码分割
                react: {
                    test: /[\\/]node_modules[\\/](react\.*|redux\.*)/,
                    priority: -5,
                    name: 'react',
                },
                vendors: {
                    test: /[\\/]node_modules[\\/]/,
                    priority: -10,
                    name: 'vendor'
                }
            }
        }
    }
    
    1. 异步代码分割
      通过动态import(),对LoginDirectoryReportPageFormPageBIPageWebPageChangePassword进行异步代码分割
    Loadable({
        loader: () => import(/*webpackChunkName: "Login"*/'../../platform/view/Login'),
        loading: () => null,
    })
    
    1. 打包
    image.png
    1. 动态import()异步代码分割生成的一些bundler体积非常小。可以利用魔法注释中的webpackChunkName合并bundler

    2.3.2 Base Tree Shaking

    一. 问题分析

    1. base文件夹中绝大部分模块(modules)只在BIPageReportPageFormPage等异步Chunks中使用。为什么没有被打包到这些异步模块中,而是全部被同步代码分割出来了呢?
      即:为什么动态import()异步代码分割对base中模块modules失效?

    如果没有对base的同步代码分割,这些base模块会被打包到入口app.js

    1. 分析
      一个模块如果被动态import()对应的异步chunk引入,同时被同步chunk引入,则该module会被打包进入同步chunk对应的bundler

    二. 逐个切断依赖关系

    1. 使app chunk依赖树中不引入base/index模块。
      能够最大限度的降低app.jsbundler体积。但app中的依赖树比较复杂,逐个修改的工作量比较大。依赖树中只要有一条线指向base/index模块,就不能解决问题。
    2. 如何查找app chunk依赖树中哪些modules引入了base/index.js
      (1) 注释掉那些动态import()引入的chunks,只打包app对应的chunk
      (2) 通过分析工具可以查看module之间的依赖关系。
      (3) 找到依赖base/index.js的模块,修改依赖关系。
      image.png
    image.png

    app bundlerplatform bundler过滤base modules。在进入模板页之前,减少js bundler的体积。

    三. babel插件

    1. 三个插件

    (1) babel-plugin-module-resolver 插件

    // Use this:
    import MyUtilFn from 'utils/MyUtilFn';
    // Instead of that:
    import MyUtilFn from '../../../../utils/MyUtilFn';
    

    可以实现webpack中的resolve.alias的功能。
    参考:custom-aliases-in-react-native-with-babel
    (2) babel-plugin-import

    import { TimePicker } from "antd"
    ↓ ↓ ↓ ↓ ↓ ↓
    var _button = require('antd/lib/time-picker');
    

    可以实现webpack中第三方库的Tree Shaking功能。
    (3) babel-plugin-transform-imports

    //Causes this code:
    import { MyModule } from 'my-library';
    import { App } from 'my-library/components';
    import { Header, Footer } from 'my-library/components/App';
    //to become:
    import MyModule from 'my-library/MyModule';
    import App from 'my-library/components/App';
    import Header from 'my-library/components/App/Header';
    import Footer from 'my-library/components/App/Footer';
    

    可以实现第三方库与本地代码的Tree Shaking

    第一个插件的作用是实现resolve.alias。后两个插件的作用是实现tree shaking。后两个插件和第一个插件共同使用时,后两个插件不生效。

    1. 最佳实践
      由于babel-plugin-module-resolver与另外两个插件共用使用时,另外两个插件不生效。因此,H5端使用webpack自身支持的resolve.alias和另外两个插件的其中之一(推荐babel-plugin-transform-imports)实现Tree ShakingApp端使用babel-plugin-module-resolver 插件实现alias
      image.png
    2. 注意事项

    (1) 使用babel-plugin-transform-imports转换插件实现TreeShaking,需要保证一个module对应一个文件。

    module.exports = function(importName, matches) {
        if(components.includes(importName)) {
            return `base/components/${importName}`
        }
        if(transMap[importName]) {
            return transMap[importName]
        }
        console.error(importName + ` must define the transform import format!`);
    };
    

    (2) 修改transformImport.js中转换函数后需要清除缓存文件

    image.png
    使用默认的缓存目录 node_modules/.cache/babel-loader,如果在任何根目录下都没有找到node_modules目录,将会降级回退到操作系统默认的临时文件目录。

    2.3.3 CSS资源

    1. 项目中外部式样式和嵌入式样式比较少,对页面加载影响微乎其微。
    2. 因为与app共用代码。项目中绝大部分样式是通过StyleSheet.create({})以行内形式嵌入,导致打包输出的js文件体积增加。但如果不修改写法,行内样式无法抽出。

    2.3.4 图片资源

    1. 图片资源预想的是将小图片转化为base64字符串或者使用CSS spirit。后来发现遇到问题如下:
      image.png

    (1) 由于现有icon.js的写法。使用base64字符串会将没有使用到的小图标也打包到js文件中。造成js中图标体积约400KB
    (2) CSS spirit图片拼接比较麻烦,现有的以webpack-spritesmithpostcss-sprites为代表的插件也是以css文件为基础,需要修改现有Icon组件的实现方式。即使解决了该问题,雪碧图拼接会将所有小图标拼成一个大图片,包括使用不到的图标。

    3. 代码逻辑

    1. 区分不同入口逻辑
      js分包的角度来看,动态import()异步资源加载不适用于用户直接打开模板时的入口,js分包已经做了入口区分。从代码逻辑上来看,不同入口代码逻辑也应该做区分,减少直接打开模板生成的bundler体积。
    2. 异步阻塞逻辑优化
      页面渲染之前的异步回调会推迟页面初始渲染时间。

    相关文章

      网友评论

          本文标题:Webpack版本升级与打包优化

          本文链接:https://www.haomeiwen.com/subject/duoloqtx.html