• React组件应用于Spring MVC工程


    背景

    公司前端工程技术栈好处于 React+Mobx 与 Spring MVC(freemarker+jQuery) 两种技术栈共存的阶段,两种技术栈页面存在一些相同的业务功能点,如果分别开发和维护,就需要双倍的人力成本,因此,下文将尝试将 React 业务组件在 webpack 、 babel 等利器的帮助下应用于 Spring MVC 项目。

    应用

    一、简单封装组件挂载与卸载方法

    React 业务组件就是 FunctionComponent 或者 ClassComponent ,需要利用 react-dom 中的 render 方法处理,转化成 Fiber 双向链表树,形成虚拟 DOM ,最后转成实际的 HTMLElement 追加到页面上。因此,在 Spring MVC 中使用需要抛出挂载与卸载的方法:

    // 引入polyfill,后面会将为什么不用@babel/polyfill
    import 'react-app-polyfill/ie9';
    import 'react-app-polyfill/stable';
    import React from 'react';
    import ReactDOM from 'react-dom';
    import { MediaPreview } from './src/MediaPreview';
    
    // 引入组件库全部样式,后面会做css tree shaking处理
    import '@casstime/bricks/dist/bricks.development.css';
    import './styles/index.scss';
    
    ;(function () {
      window.MediaPreview = (props, container) => {
        return {
          // 卸载
          close: function () {
            ReactDOM.unmountComponentAtNode(container);
          },
          // 挂载
          open: function (activeIndex) {
            ReactDOM.render(React.createElement(MediaPreview, { ...props, visible: true, activeIndex: activeIndex || 0 }), container);
            // 或者
            // ReactDOM.render(, container);
          },
        };
      };
    })();

    二、 babel 转译成 ES5 语法规范, polyfill 处理兼容性 api

    babel 在转译的时候,会将源代码分成 syntax 和 api 两部分来处理

    • syntax :类似于展开对象、 optional chain 、 let 、 const 等语法;
    • api :类似于 [1,2,3].includes 、 new URL() , new URLSearchParams() 、 new Map() 等函数、方法;

    babel 很轻松就转译好 syntax ,但对于 api 并不会做任何处理,如果在不支持这些 api 的浏览器中运行,就会报错,因此需要使用 polyfill 来处理 api ,处理兼容性 api 有以下方案:

    @babel/preset-env 中有一个配置选项 useBuiltIns ,用来告诉 babel 如何处理 api 。由于这个选项默认值为 false ,即不处理 api

    • 设置 useBuiltIns 的值为“ entry ”,同时在入口文件最上方引入 @babel/polyfill ,或者不指定 useBuiltIns ,也可设置 useBuiltIns 的值为 false ,在 webpack entry 引入 @babel/polyfill 。这种模式下, babel 会将所有的 polyfill 全部引入,导致结果的包大小会很大,然后利用 webpack tree shaking 剔除没有被使用的代码块;
    • 使用按需加载,将 useBuiltIns 改成“ usage ”, babel 就可以按需加载 polyfill ,并且不需要手动引入 @babel/polyfill ,但依赖需要安装它;
    • 上述两种方法存在两个问题,① polyfill 注入的方法会改变全局变量的原型,可能带来意想不到的问题。②转译 syntax 时,会注入一些辅助函数来帮忙转译,这些 helper 函数会在每个需要转译的文件中定义一份,导致最终的产物里有大量重复的 helper 。引入 @babel/plugin-transform-runtime 将 helper 和 api 都改为从一个统一的地方引入,并且引入的对象和全局变量是完全隔离的;
    • 在入口文件最上方或者 webpack entry 引入 react-app-polyfill ,并启用 webpack tree shaking ;

    方案一:全量引入 @babel/polyfill , webpack 做 tree shaking

    根目录配置 babel.config.json

    {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": {
              "chrome": "58",
              "ie": "9"
            }
          },
          "useBuiltIns": "entry",
          "corejs": "3" // 指定core-js版本
        ],
        "@babel/preset-react",
        "@babel/preset-typescript"
      ],
      "plugins": []
    }

    如果在执行构建时报如下警告,表示在使用 useBuiltIns 选项时没有指定 core-js 版本

    webpack.config.js 配置

    /* eslint-disable @typescript-eslint/no-var-requires */
    const package = require('./package.json');
    const path = require('path');
    
    module.exports = {
      mode: 'production',
      entry: [
        './index.tsx',
      ],
      output: {
        path: __dirname + '/dist',
        filename: `media-preview.v${package.version}.min.js`,
        library: {
          type: 'umd',
        },
      },
      module: {
        rules: [
          {
            test: /\.(m?js|ts|js|tsx|jsx)$/,
            exclude: /(node_modules|lib|dist)/,
            use: [
              {
                loader: 'babel-loader',
                options: {
                  cacheDirectory: true,
                },
              },
            ],
          },
          {
            test: /\.(scss|css|less)/,
            use: [
              'style-loader',
              'css-loader',
              'sass-loader',
            ],
          },
          {
            test: /\.(png|jpg|jepg|gif)$/i,
            use: [
              {
                loader: 'url-loader',
                options: {
                  limit: 8 * 1024 * 1024, // 大小超过8M就不使用base64编码了
                  name: 'static/media/[name].[hash:8].[ext]',
                  fallback: require.resolve('file-loader'),
                },
              },
            ],
          },
          {
            test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
            use: [
              {
                loader: 'url-loader',
                options: {
                  limit: 8 * 1024 * 1024,
                  name: 'static/fonts/[name].[hash:8].[ext]',
                  fallback: require.resolve('file-loader'),
                },
              },
            ],
          },
        ],
      },
      plugins: [],
      resolve: {
        extensions: ['.ts', '.tsx', '.js', '.json'],
      },
    };

    构建生成的产物含有一堆图片和字体文件,并且都重复了双份,其实期望的结果是这些资源都被base64编码在代码中,但没有生效。

    原因是当在 webpack 5 中使用旧的 assets loader (如 file-loader / url-loader / raw-loader 等)和 asset 模块时,你可能想停止当前 asset 模块的处理,并再次启动处理,这可能会导致 asset 重复,你可以通过将 asset 模块的类型设置为 'javascript/auto' 来解决。

    module.exports = {
      module: {
       rules: [
          {
            test: /\.(png|jpg|jepg|gif)$/i,
            use: [
              {
                loader: 'url-loader',
                options: {
                  limit: 8 * 1024 * 1024, // 大小超过8M就不使用base64编码了
                  name: 'static/media/[name].[hash:8].[ext]',
                  fallback: require.resolve('file-loader'),
                },
              },
            ],
            type: 'javascript/auto',
          },
          {
            test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
            use: [
              {
                loader: 'url-loader',
                options: {
                  limit: 8 * 1024 * 1024,
                  name: 'static/fonts/[name].[hash:8].[ext]',
                  fallback: require.resolve('file-loader'),
                },
              },
            ],
            type: 'javascript/auto',
          },
       ]
      },
    }

    传送门: 资源模块(asset module)

    再次构建,生成的产物在 IE 浏览器中应用会报语法错误,代码中有使用箭头函数语法。不是说 babel 会将高级语法转译成 ES5 语法吗?为什么还会出现语法错误呢?

    这是因为 webpack 注入的运行时代码默认是按 web 平台构建编译的,但是编译的语法版本不是 ES5 ,因此需要告知 webpack 为目标( target )指定一个环境

    module.exports = {
      // ...
      target: ['web', 'es5'], // Webpack 将生成 web 平台的运行时代码,并且只使用 ES5 相关的特性
    };

    传送门:构建目标(Targets)

    再次构建在 IE 浏览器中应用,出现下面问题, IE 浏览器不支持 new URL 构造函数,为什么呢? @babel/polyfill 不是会处理具有兼容性问题的 api 吗?

    原因在于 @babel/polyfill 中 core-js 部分并没有提供 URL 构造函数的垫片,安装 url-polyfill ,在入口文件或者 webpack entry 引入它,再次构建

    module.exports = {
      // ...
      entry: ['url-polyfill', './index.tsx'],
    };

    产物在 IE10 和 IE11 运行正常,但是在 IE9 会报错, url-polyfill 使用了 IE9 不支持的“ checkValidity ”属性或方法

    element-internals-polyfill 实现了 ElementInternals ,为 Web 开发人员提供了一种允许自定义元素完全参与 HTML 表单的方法。

    但是,该垫片中有使用 new WeakMap 构造函数, WeakMap 在IE中也存在兼容性问题,一个个去找对应的 polyfill 就跟套娃似的,还不如换其他方案

    方案二:按需引入 @babel/polyfill

    不用在入口文件最上方或者 webpack entry 引入 @babel/polyfill ,只需要安装即可

    babel.config.json

    {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": {
              "chrome": "58",
              "ie": "9"
            }
          },
          "useBuiltIns": "usage"
        ],
        "@babel/preset-react",
        "@babel/preset-typescript"
      ],
      "plugins": []
    }

    方案二和方案一都是使用 @babel/polyfill ,构建产物在IE执行依旧会报一样的错误, URL 构造函数不支持

    方案三: @babel/plugin-transform-runtime

    安装 yarn add @babel/plugin-transform-runtime @babel/runtime-corejs3 -D ,存在兼容性 api 由 @babel/runtime-corejs3 提供垫片

    {
      "presets": [
        [
          "@babel/preset-env",
          {
            "targets": {
              "chrome": "58",
              "ie": "9"
            },
          }
        ],
        "@babel/preset-react",
        "@babel/preset-typescript"
      ],
      "plugins": [
        [
        "@babel/plugin-transform-runtime",
          {
            "absoluteRuntime": true,
            "corejs": 3, // 指定corejs版本,安装@babel/runtime-corejs3就指定3版本
            "helpers": true,
            "regenerator": true,
            "version": "7.0.0-beta.0"
          }
        ]
      ]
    }

    构建产物在 IE 运行同样会报上述方案的错误,原因是安装的 @babel/runtime-corejs3 没有提供 URL 构造函数的垫片

    方案四:入口引入 react-app-polyfill , webpack 做 tree shaking

    安装

    yarn add react-app-polyfill

    在入口文件最上方或者 webpack entry 引入

    // 入口文件引入
    import 'react-app-polyfill/ie9';
    import 'react-app-polyfill/stable';
    
    // webpack entry
    entry: [‘react-app-polyfill/ie9’, 'react-app-polyfill/stable', './index.tsx'],

    设置 mode: 'production' 就会默认启用 tree shaking

    执行构建,产物在 IE9+ 都可以运行成功,说明 react-app-polyfill 很好的提供了 new URL 、 checkValidity 等垫片,查阅源代码也可验证

    三、 css tree shaking

    业务组件中使用了基础组件库中的很多组件,比如 import { Modal, Carousel, Icon } from '@casstime/bricks'; ,虽然这些基础组件都有对应的样式文件(比如 Modal 组件有自己的对应的 _modal.scss ),但这些样式文件中有依赖样式变量 _variables.scss ,依赖混合 _mixins.scss 等等,因此一个个导入样式需要捋清除依赖关系,非常不方便。于是我在入口文件出引入整个基础组件的样式 import '@casstime/bricks/dist/bricks.development.css'; ,这样也会导致引入了很多无关的样式,产物的大小会随之增大,需要对其做 css tree shaking 处理。

    安装:

    yarn add purgecss-webpack-plugin mini-css-extract-plugin glob-all -D

    因为打包时 CSS 默认放在 JS 文件内,因此要结合 webpack 分离 CSS 文件插件 mini-css-extract-plugin 一起使用,先将 CSS 文件分离,再进行 CSS Tree Shaking 。

    /* eslint-disable @typescript-eslint/no-var-requires */
    const package = require('./package.json');
    const path = require('path');
    const PurgeCSSPlugin = require('purgecss-webpack-plugin');
    const MiniCssExtractPlugin = require('mini-css-extract-plugin');
    const glob = require('glob-all');
    
    const PATHS = {
      src: path.join(__dirname, 'src'),
    };
    
    function collectSafelist() {
      return {
        standard: ['icon', /^icon-/],
        deep: [/^icon-/],
        greedy: [/^icon-/],
      };
    }
    
    module.exports = {
      target: ['web', 'es5'],
      mode: 'production',
      // 'element-internals-polyfill', 'url-polyfill',
      entry: ['./index.tsx'],
      output: {
        path: __dirname + '/dist',
        filename: `media-preview.v${package.version}.min.js`,
        library: {
          type: 'umd',
        },
      },
      module: {
        rules: [
          {
            test: /\.(m?js|ts|js|tsx|jsx)$/,
            exclude: /(node_modules|lib|dist)/,
            use: [
              {
                loader: 'babel-loader',
                options: {
                  cacheDirectory: true,
                },
              },
            ],
          },
          {
            test: /\.(scss|css|less)/,
            use: [
              'style-loader',
              MiniCssExtractPlugin.loader,
              {
                loader: 'css-loader',
                options: {
                  // url: false
                  // modules: {
                  //   localIdentName: '[name]_[local]_[hash:base64:5]'
                  // },
                  // 1、【name】:指代的是模块名
                  // 2、【local】:指代的是原本的选择器标识符
                  // 3、【hash:base64:5】:指代的是一个5位的hash值,这个hash值是根据模块名和标识符计算的,因此不同模块中相同的标识符也不会造成样式冲突。
                },
              },
              {
                loader: 'postcss-loader',
                options: {
                  postcssOptions: {
                    // parser: 'postcss-js',
                    // execute: true,
                    plugins: [['postcss-preset-env']], // 跟Autoprefixer类型,为样式添加前缀
                  },
                },
              },
              'sass-loader',
            ],
          },
          {
            test: /\.(png|jpg|jepg|gif)$/i,
            use: [
              {
                loader: 'url-loader',
                options: {
                  limit: 8 * 1024 * 1024, // 大小超过8M就不使用base64编码了
                  name: 'static/media/[name].[hash:8].[ext]',
                  fallback: require.resolve('file-loader'),
                },
              },
            ],
            type: 'javascript/auto',
          },
          {
            test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
            use: [
              {
                loader: 'url-loader',
                options: {
                  limit: 8 * 1024 * 1024,
                  name: 'static/fonts/[name].[hash:8].[ext]',
                  fallback: require.resolve('file-loader'),
                },
              },
            ],
            type: 'javascript/auto',
          },
        ],
      },
      plugins: [
        new MiniCssExtractPlugin({
          filename: `media-preview.v${package.version}.min.css`,
        }),
        /**
         * PurgeCSSPlugin用于清除⽆⽤ css,必须和MiniCssExtractPlugin搭配使用,不然不会生效。
         * paths属性用于指定哪些文件中使用样式应该保留,没有在这些文件中使用的样式会被剔除
         */
        new PurgeCSSPlugin({
          paths: glob.sync(
            [
              `${PATHS.src}/**/*`,
              path.resolve(__dirname, '../../node_modules/@casstime/bricks/lib/components/carousel/*.js'),
              path.resolve(__dirname, '../../node_modules/@casstime/bricks/lib/components/modal/*.js'),
              path.resolve(__dirname, '../../node_modules/@casstime/bricks/lib/components/icon/*.js'),
            ],
            { nodir: true },
          ),
          safelist: collectSafelist, // 安全列表,指定不剔除的样式
        }),
      ],
      resolve: {
        extensions: ['.ts', '.tsx', '.js', '.json'],
      },
    };

    由于 Icon 组件使用的图标是根据 type 属性确认的,比如  ,则使用到了 icon-close 样式类,虽然 PurgeCSSPlugin 配置指定 icon.js 文件中使用样式应该保留,但因为 icon-${type} 是动态的, PurgeCSSPlugin 并不知道 icon-close 被使用了,会被剔除掉,因此需要配置 safelist ,指定不被剔除的样式。

    最终产物由 1.29M 降低到 952KB ,其实构建后产物中还有比较多冗余重复的代码,如果使用公共模块抽取还会进一步减小产物体积大小,但是会拆分成好多个文件,不方便在 Spring MVC 项目的引入使用,构建产物由一个 js 或者一个 js 和一个 css 组成最佳

    四、处理样式兼容性

    1、 scss 中使用具有兼容性样式

    在书写 scss 样式文件时,常常会用到一些具有兼容性问题的样式属性,比如 transform、transform-origin ,在IE内核浏览器中需要添加ms-前缀,谷歌内核浏览器需要添加 webkit- 前缀,因此构建时需要相应的 loader 或者 plugin 处理,这里我们采用 postcss 来处理

    安装

    yarn add postcss postcss-preset-env -D

    loader 配置

    module.exports = {
        module: [
            // ...
            {
            test: /\.(scss|css|less)/,
            use: [
              'style-loader',
              MiniCssExtractPlugin.loader,
              {
                loader: 'css-loader',
                options: {
                  // url: false
                  // modules: {
                  //   localIdentName: '[name]_[local]_[hash:base64:5]'
                  // },
                  // 1、【name】:指代的是模块名
                  // 2、【local】:指代的是原本的选择器标识符
                  // 3、【hash:base64:5】:指代的是一个5位的hash值,这个hash值是根据模块名和标识符计算的,因此不同模块中相同的标识符也不会造成样式冲突。
                },
              },
              {
                loader: 'postcss-loader',
                options: {
                  postcssOptions: {
                    // parser: 'postcss-js',
                    // execute: true,
                    plugins: [['postcss-preset-env']], // 跟Autoprefixer类型,为样式添加前缀
                  },
                },
              },
              'sass-loader',
            ],
          },
        ]
    }

    2、处理 tsx 脚本中动态注入兼容性问题的样式

    在某些场景下,可能会用脚本来控制 UI 交互,比如控制拖拽平移 element.style.transform = 'matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)'; ,对于这类具有兼容性问题的动态样式也是需要处理的。可以考虑以下几种方案:

    • 自行实现 loader 或者 plugin 转化脚本的样式,或者寻找对应的第三方库;
    • 平时编写的动态样式就处理好其兼容性;

    由于我们的业务组件相对简单,直接在编写时做好了兼容性处理

    element.style.transform = 'matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)';
    element.style.msTransform = 'matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)';
    element.style.oTransform = 'matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)';
    element.style.webkitTransform = 'matrix(1.0, 2.0, 3.0, 4.0, 5.0, 6.0)';

    五、附录

    常见 polyfill 清单

    No.NamePackageSource MapNetwork
    1ECMAScript6es6-shim:white_check_mark::cn:
    2Proxyes6-proxy-polyfill:cn:
    3ECMAScript7es7-shim:white_check_mark::cn:
    4ECMAScriptcore-js-bundle:white_check_mark::cn:
    5Regeneratorregenerator-runtime:white_check_mark::cn:
    6GetCanonicalLocales@formatjs/intl-getcanonicallocales:cn:
    7Locale@formatjs/intl-locale:cn:
    8PluralRules@formatjs/intl-pluralrules:cn:
    9DisplayNames@formatjs/intl-displaynames:cn:
    10ListFormat@formatjs/intl-listformat:cn:
    11NumberFormat@formatjs/intl-numberformat:cn:
    12DateTimeFormat@formatjs/intl-datetimeformat:cn:
    13RelativeTimeFormat@formatjs/intl-relativetimeformat:cn:
    14ResizeObserverresize-observer-polyfill:white_check_mark::cn:
    15IntersectionObserverintersection-observer:cn:
    16ScrollBehaviorscroll-behavior-polyfill:white_check_mark::cn:
    17WebAnimationweb-animations-js:white_check_mark::cn:
    18EventSubmitterevent-submitter-polyfill:cn:
    19Dialogdialog-polyfill:cn:
    20WebComponents@webcomponents/webcomponentsjs:white_check_mark::cn:
    21ElementInternalselement-internals-polyfill:cn:
    22AdoptedStyleSheetsconstruct-style-sheets-polyfill:white_check_mark::cn:
    23PointerEvents@wessberg/pointer-events:white_check_mark::cn:
    24TextEncoderfastestsmallesttextencoderdecoder-encodeinto:white_check_mark::cn:
    25URLurl-polyfill:cn:
    26URLPatternurlpattern-polyfill:cn:
    27Fetchwhatwg-fetch:white_check_mark::cn:
    28EventTargetevent-target-polyfill:white_check_mark::cn:
    29AbortControlleryet-another-abortcontroller-polyfill:white_check_mark::cn:
    30Clipboardclipboard-polyfill:white_check_mark::cn:
    31PWAManifestpwacompat:cn:
    32Shareshare-api-polyfill:cn:
  • 相关阅读:
    【附源码】计算机毕业设计SSM特种设备学习考试系统学习子系统的开发
    ImportError cannot import name ‘Map‘ from ‘pyecharts‘
    透明多级分流系统(架构扫盲贴)
    SuperMap iClient for Leaflet对EPSG:4509图加载&滑动查询
    基于Php幼儿园管理系统
    Java项目:SSM动漫影视网站系统
    数据结构与算法(Java篇)笔记--选择排序
    C++拷贝构造函数
    【Java】Java中的引用类型
    Java版分布式微服务云开发架构 Spring Cloud+Spring Boot+Mybatis 电子招标采购系统功能清单
  • 原文地址:https://blog.csdn.net/JAVAQXQ/article/details/126184433