背景
公司前端技術棧還處於React+Mobx
與Spring MVC(freemarker+jQuery)
共存的階段,兩種技術棧頁面難免會存在一些相同的業務功能,如果分別開發和維護,需要投入較大人力成本,因此,我們嘗試將React
業務元件應用於Spring MVC
專案,一處開發多處使用,降低不必要的成本投入。
應用
一、簡單封裝元件掛載與解除安裝方法
Spring MVC
是面向DOM api
的程式設計,需要給元件封裝掛載和解除安裝的方法。React
業務元件可以利用react-dom
中的render
方法掛載到對應的容器元素上,利用unmountComponentAtNode
方法解除安裝掉容器元素下的元素。
// 引入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
在轉譯的時候,會將原始碼分成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
全部引入,構建產物體積會很大,需要啟用tree shaking
清除沒有使用的程式碼; - 啟用按需載入,將
useBuiltIns
改成“usage
”,babel
就可以按需載入polyfill
,並且不需要手動引入@babel/polyfill
,但依然需要安裝它; - 上述兩種方法存在兩個問題,①
polyfill
注入的方法會改變全域性變數的原型(篡改原型鏈),可能帶來意料之外的問題。② 轉譯syntax
時,會注入一些輔助函式來幫忙轉譯,這些helper
函式會在每個需要轉譯的檔案中定義一份,導致最終的產物含有大量重複的helper
。因此,引入@babel/plugin-transform-runtime
將helper
和api
都改為從一個統一的地方引入,並且引入的物件和全域性變數是完全隔離的,既不會篡改原型鏈,亦不會出現重複的helper
; - 在入口檔案最上方或者
webpack entry
引入react-app-polyfill
,啟用tree shaking
;
方案一:全量引入@babel/polyfill
,啟用tree shaking
入口檔案新增@babel/polyfill
// index.tsx
import '@babel/polyfill';
// coding...
根目錄配置babel.config.json
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": "58",
"ie": "9"
}
},
"useBuiltIns": "entry",
"corejs": "3" // 指定core-js版本,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',
},
]
},
}
再次構建,生成的產物在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
墊片庫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
中也存在相容性問題,一個個去補充缺失的墊片方法簡直跟套娃似的,還不如換其他方案
方案二:按需引入@babel/polyfill
不用在入口檔案最上方或者webpack entry
引入@babel/polyfill
,只需要設定"useBuiltIns": "usage"
,並安裝@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
,由 @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
,啟用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
。
接下來就該 PurgeCSS
上場了。PurgeCSS
是一個用來刪除未使用的 CSS
程式碼的工具。當你構建一個網站時,你可能會決定使用一個 CSS
框架,例如 TailwindCSS、Bootstrap、MaterializeCSS、Foundation
等,但是,你所用到的也只是框架的一小部分而已,大量 CSS
樣式並未被使用。PurgeCSS
透過分析你的內容和 CSS
檔案,首先它將 CSS
檔案中使用的選擇器與內容檔案中的選擇器進行匹配,然後它會從 CSS
中刪除未使用的選擇器,從而生成更小的 CSS
檔案。
對應webpack
外掛purgecss-webpack-plugin
,該外掛的使用依賴樣式抽離外掛mini-css-extract-plugin
,只有先將樣式抽離成獨立檔案後才能將 CSS
檔案中使用的樣式選擇器與內容檔案中的樣式選擇器進行匹配,刪除 CSS
中未使用的選擇器,從而生成更小的 CSS
檔案。
purgecss-webpack-plugin
的使用需要指定paths
屬性,告訴purgecss
需要分析的檔案列表,這些檔案中使用的選擇器與抽離的樣式檔案中的選擇器進行匹配,從而剔除未使用的選擇器。
安裝:
yarn add purgecss-webpack-plugin mini-css-extract-plugin glob-all -D
webpack.config.js
:
/* 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, // 為了不將font抽離,目標產物只有js和css
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
降低到752KB
,其實構建後產物中還有比較多冗餘重複的程式碼,如果使用公共模組抽取還會進一步減小產物體積大小,但是會拆分成好多個檔案,不方便在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
清單