背景
公司前端工程技术栈好处于React+Mobx
与Spring MVC(freemarker+jQuery)
两种技术栈共存的阶段,两种技术栈页面存在一些雷同的业务性能点,如果别离开发和保护,就须要双倍的人力老本,因而,下文将尝试将React
业务组件在webpack
、babel
等利器的帮忙下利用于Spring MVC
我的项目。
利用
一、简略封装组件挂载与卸载办法
React
业务组件就是FunctionComponent
或者ClassComponent
,须要利用react-dom
中的render
办法解决,转化成Fiber
双向链表树,造成虚构DOM
,最初转成理论的HTMLElement
追加到页面上。因而,在Spring MVC
中应用须要抛出挂载与卸载的办法:
// 引入polyfill,前面会将为什么不必@babel/polyfillimport '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
在转译的时候,会将源代码分成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 entryentry: [‘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 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 |