React组件应用于Spring|React组件应用于Spring MVC工程

背景
公司前端工程技术栈好处于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(, 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版本
React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

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编码在代码中,但没有生效。
React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

原因是当在 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语法吗?为什么还会出现语法错误呢?
React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

这是因为webpack注入的运行时代码默认是按web平台构建编译的,但是编译的语法版本不是ES5,因此需要告知 webpack 为目标(target)指定一个环境
module.exports = { // ... target: ['web', 'es5'], // Webpack 将生成 web 平台的运行时代码,并且只使用 ES5 相关的特性 };

传送门:构建目标(Targets)
再次构建在IE浏览器中应用,出现下面问题,IE浏览器不支持new URL构造函数,为什么呢?@babel/polyfill不是会处理具有兼容性问题的api吗?
React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

原因在于@babel/polyfillcore-js部分并没有提供URL构造函数的垫片,安装url-polyfill,在入口文件或者webpack entry引入它,再次构建
module.exports = { // ... entry: ['url-polyfill', './index.tsx'], };

产物在IE10IE11运行正常,但是在IE9会报错,url-polyfill使用了IE9不支持的“checkValidity”属性或方法
React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

element-internals-polyfill实现了ElementInternals,为 Web 开发人员提供了一种允许自定义元素完全参与 HTML表单的方法。
React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

但是,该垫片中有使用new WeakMap构造函数,WeakMap在IE中也存在兼容性问题,一个个去找对应的polyfill就跟套娃似的,还不如换其他方案
React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

方案二:按需引入@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组件应用于Spring|React组件应用于Spring MVC工程
文章图片

方案四:入口引入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等垫片,查阅源代码也可验证
React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

三、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,指定不被剔除的样式。
React组件应用于Spring|React组件应用于Spring MVC工程
文章图片

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

四、处理样式兼容性 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)';

五、附录 【React组件应用于Spring|React组件应用于Spring MVC工程】常见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

    推荐阅读