关于spring-mvc:React组件应用于Spring-MVC工程

3次阅读

共计 11453 个字符,预计需要花费 29 分钟才能阅读完成。

背景

公司前端工程技术栈好处于 React+MobxSpring MVC(freemarker+jQuery)两种技术栈共存的阶段,两种技术栈页面存在一些雷同的业务性能点,如果别离开发和保护,就须要双倍的人力老本,因而,下文将尝试将 React 业务组件在 webpackbabel 等利器的帮忙下利用于 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(<MediaPreview {...{ ...props, visible: true, activeIndex: activeIndex || 0}} />, container);
      },
    };
  };
})();

二、babel转译成 ES5 语法标准,polyfill解决兼容性api

babel在转译的时候,会将源代码分成 syntaxapi两局部来解决

  • syntax:相似于开展对象、optional chainletconst等语法;
  • api:相似于 [1,2,3].includesnew 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-runtimehelperapi 都改为从一个对立的中央引入,并且引入的对象和全局变量是齐全隔离的;
  • 在入口文件最上方或者 webpack entry 引入react-app-polyfill,并启用webpack tree shaking
计划一:全量引入 @babel/polyfillwebpacktree 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/polyfillcore-js局部并没有提供 URL 构造函数的垫片,装置 url-polyfill,在入口文件或者webpack entry 引入它,再次构建

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

产物在 IE10IE11运行失常,然而在 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-polyfillwebpacktree 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 URLcheckValidity 等垫片,查阅源代码也可验证

三、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 type="close"/>,则应用到了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. Name Package Source Map Network
1 ECMAScript6 es6-shim 🇺🇳 🇨🇳
2 Proxy es6-proxy-polyfill 🇺🇳 🇨🇳
3 ECMAScript7 es7-shim 🇺🇳 🇨🇳
4 ECMAScript core-js-bundle 🇺🇳 🇨🇳
5 Regenerator regenerator-runtime 🇺🇳 🇨🇳
6 GetCanonicalLocales @formatjs/intl-getcanonicallocales 🇺🇳 🇨🇳
7 Locale @formatjs/intl-locale 🇺🇳 🇨🇳
8 PluralRules @formatjs/intl-pluralrules 🇺🇳 🇨🇳
9 DisplayNames @formatjs/intl-displaynames 🇺🇳 🇨🇳
10 ListFormat @formatjs/intl-listformat 🇺🇳 🇨🇳
11 NumberFormat @formatjs/intl-numberformat 🇺🇳 🇨🇳
12 DateTimeFormat @formatjs/intl-datetimeformat 🇺🇳 🇨🇳
13 RelativeTimeFormat @formatjs/intl-relativetimeformat 🇺🇳 🇨🇳
14 ResizeObserver resize-observer-polyfill 🇺🇳 🇨🇳
15 IntersectionObserver intersection-observer 🇺🇳 🇨🇳
16 ScrollBehavior scroll-behavior-polyfill 🇺🇳 🇨🇳
17 WebAnimation web-animations-js 🇺🇳 🇨🇳
18 EventSubmitter event-submitter-polyfill 🇺🇳 🇨🇳
19 Dialog dialog-polyfill 🇺🇳 🇨🇳
20 WebComponents @webcomponents/webcomponentsjs 🇺🇳 🇨🇳
21 ElementInternals element-internals-polyfill 🇺🇳 🇨🇳
22 AdoptedStyleSheets construct-style-sheets-polyfill 🇺🇳 🇨🇳
23 PointerEvents @wessberg/pointer-events 🇺🇳 🇨🇳
24 TextEncoder fastestsmallesttextencoderdecoder-encodeinto 🇺🇳 🇨🇳
25 URL url-polyfill 🇺🇳 🇨🇳
26 URLPattern urlpattern-polyfill 🇺🇳 🇨🇳
27 Fetch whatwg-fetch 🇺🇳 🇨🇳
28 EventTarget event-target-polyfill 🇺🇳 🇨🇳
29 AbortController yet-another-abortcontroller-polyfill 🇺🇳 🇨🇳
30 Clipboard clipboard-polyfill 🇺🇳 🇨🇳
31 PWAManifest pwacompat 🇺🇳 🇨🇳
32 Share share-api-polyfill 🇺🇳 🇨🇳
正文完
 0