【微前端实战总结篇】

news2024/11/25 1:37:11

微前端现有的落地方案可以分为三类,自组织模式、基座模式以及模块加载模式。

一、为什么需要微前端?

这里我们通过3W(what,why,how)的方式来讲解什么是微前端:

1.What?什么是微前端?

微前端就是将不同的功能按照不同的维度拆分成多个子应用。通过主应用来加载这些子应用。

微前端的核心在于拆, 拆完后再合!

2.Why?为什么去使用他?

  • 不同团队间开发同一个应用技术栈不同怎么破?
  • 希望每个团队都可以独立开发,独立部署怎么破?
  • 项目中还需要老的应用代码怎么破?

我们是不是可以将一个应用划分成若干个子应用,再将子应用打包成一个个的lib呢?当路径切换时加载不同的子应用,这样每个子应用都是独立的,技术栈也就不用再做限制了!从而解决了前端协同开发的问题。

3.How?怎样落地微前端?

  • 2018年 Single-SPA诞生了, single-spa是一个用于前端微服务化的JavaScript前端解决方案 (本身没有处理样式隔离、js执行隔离) 实现了路由劫持和应用加载;
  • 2019年 qiankun基于Single-SPA, 提供了更加开箱即用的 API (single-spa + sandbox + import-html-entry),它 做到了技术栈无关,并且接入简单(有多简单呢,像iframe一样简单)

总结:子应用可以独立构建,运行时动态加载,主子应用完全解耦,并且技术栈无关,靠的是协议接入(这里提前强调一下:子应用必须导出 bootstrap、mount、unmount三个方法)。

这里先回答一下大家可能会有的疑问:

这不是iframe吗?

如果使用的是iframe,当iframe中的子应用切换路由时用户刷新页面就尴尬了。

应用间如何通信?

  • 基于URL来进行数据传递,但是这种传递消息的方式能力较弱
  • 基于CustomEvent实现通信;
    • 基于props主子应用间通信;
  • 使用全局变量、Redux进行通信

如何处理公共依赖?

  • CDN - externals
  • webpack联邦模块

二、SingleSpa实战

官网 https://zh-hans.single-spa.js.org/docs/configuration

1.构建子应用

首先创建一个vue子应用,并通过single-spa-vue来导出必要的生命周期:

vue create spa-vue  
npm install single-spa-vue
// main.js

import singleSpaVue from 'single-spa-vue';

const appOptions = {
el: '#vue',
router,
render: h => h(App)
}

// 在非子应用中正常挂载应用
if(!window.singleSpaNavigate){
delete appOptions.el;
new Vue(appOptions).$mount('#app');
}

const vueLifeCycle = singleSpaVue({
Vue,
appOptions
});


// 子应用必须导出以下生命周期:bootstrap、mount、unmount
export const bootstrap = vueLifeCycle.bootstrap;
export const mount = vueLifeCycle.mount;
export const unmount = vueLifeCycle.unmount;
export default vueLifeCycle;

配置子路由基础路径

// router.js
const router = new VueRouter({
mode: 'history',
base: '/vue', //改变路径配置
routes
})

2.配置库打包

将子模块打包成类库

//vue.config.js
module.exports = {
configureWebpack: {
// 把属性挂载到window上方便父应用调用 window.singleVue.bootstrap/mount/unmount
output: {
library: 'singleVue',
libraryTarget: 'umd'
},
devServer:{
port:10000
}
}
}

3.主应用搭建

<div id="nav">
<router-link to="/vue">vue项目router-link>
<div id="vue">div>
div>

将子应用挂载到id="vue"标签中

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import {registerApplication,start} from 'single-spa'

Vue.config.productionTip = false

async function loadScript(url) {
return new Promise((resolve,reject)=>{
let script = document.createElement('script')
script.src = url
script.onload = resolve
script.onerror = reject
document.head.appendChild(script)
})
}

// 注册应用
registerApplication('myVueApp',
async ()=>{
console.info('load')
// singlespa问题
// 加载文件需要自己构建script标签 但是不知道应用有多少个文件
// 样式不隔离
// 全局对象没有js沙箱的机制 比如加载不同的应用 每个应用都用同一个环境
// 先加载公共的
await loadScript('http://localhost:10000/js/chunk-vendors.js')
await loadScript('http://localhost:10000/js/app.js')

return window.singleVue // bootstrap mount unmount
},
// 用户切换到/vue下 我们需要加载刚才定义的子应用
location=>location.pathname.startsWith('/vue'),
)

start()

new Vue({
router,
render: h => h(App)
}).$mount('#app')

4.动态设置子应用publicPath

if(window.singleSpaNavigate){
__webpack_public_path__ = 'http://localhost:10000/'
}

三、qiankun实战

qiankun是目前比较完善的一个微前端解决方案,它已在蚂蚁内部经受过足够大量的项目考验及打磨,十分健壮。这里附上官网。

https://qiankun.umijs.org/zh/guide

1.主应用编写

<template>
<!--注意这里不要写app 否则跟子应用的加载冲突
<div id="app">-->
<div>
<el-menu :router="true" mode="horizontal">
<!-- 基座中可以放自己的路由 -->
<el-menu-item index="/">Home</el-menu-item>

<!-- 引用其他子应用 -->
<el-menu-item index="/vue">vue应用</el-menu-item>
<el-menu-item index="/react">react应用</el-menu-item>
</el-menu>
<router-view />

<!-- 其他子应用的挂载节点 -->
<div id="vue" />
<div id="react" />
</div>
</template>

<style>

</style>

注册子应用


import { registerMicroApps,start } from 'qiankun'
// 基座写法
const apps = [
{
name: 'vueApp', // 名字
// 默认会加载这个HTML,解析里面的js动态执行 (子应用必须支持跨域)
entry: '//localhost:10000',
container: '#vue', // 容器
activeRule: '/vue', // 激活的路径 访问/vue把应用挂载到#vue上
props: { // 传递属性给子应用接收
a: 1,
}
},
{
name: 'reactApp',
// 默认会加载这个HTML,解析里面的js动态执行 (子应用必须支持跨域)
entry: '//localhost:20000',
container: '#react',
activeRule: '/react' // 访问/react把应用挂载到#react上
},
]

// 注册
registerMicroApps(apps)
// 开启
start({
prefetch: false // 取消预加载
})

2.子Vue应用

// src/router.js

const router = new VueRouter({
mode: 'history',
// base里主应用里面注册的保持一致
base: '/vue',
routes
})
// main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'

Vue.config.productionTip = false

let instance = null
function render() {
instance = new Vue({
router,
render: h => h(App)
}).$mount('#app') // 这里是挂载到自己的HTML中 基座会拿到挂载后的HTML 将其插入进去
}

// 独立运行微应用
// https://qiankun.umijs.org/zh/faq#%E5%A6%82%E4%BD%95%E7%8B%AC%E7%AB%8B%E8%BF%90%E8%A1%8C%E5%BE%AE%E5%BA%94%E7%94%A8%EF%BC%9F
if(!window.__POWERED_BY_QIANKUN__) {
render()
}

// 如果被qiankun使用 会动态注入路径
if(window.__POWERED_BY_QIANKUN__) {
// qiankun 将会在微应用 bootstrap 之前注入一个运行时的 publicPath 变量,你需要做的是在微应用的 entry js 的顶部添加如下代码:
__webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}

// 子应用的协议 导出供父应用调用 必须导出promise
export async function bootstrap(props) {} // 启动可以不用写 需要导出方法
export async function mount(props) {
render()
}
export async function unmount(props) {
instance.$destroy()
}

这里不要忘记子应用的钩子导出。

// vue.config.js

module.exports = {
devServer:{
port:10000,
headers:{
'Access-Control-Allow-Origin':'*' //允许访问跨域
}
},
configureWebpack:{
// 打umd包
output:{
library:'vueApp',
libraryTarget:'umd'
}
}
}

3.子React应用

再起一个子应用,为了表明技术栈无关特性,这里使用了一个React项目:

// app.js

import logo from './logo.svg';
import './App.css';
import {BrowserRouter,Route,Link} from 'react-router-dom'

function App() {
return (
// /react跟主应用配置保持一致
<BrowserRouter basename="/react">
<Link to="/">首页</Link>
<Link to="/about">关于</Link>

<Route path="/" exact render={()=>(
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
)} />

<Route path="/about" exact render={()=>(
<h1>About Page</h1>
)}></Route>
</BrowserRouter>
);
}

export default App;
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';

function render() {
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
}

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

// 独立运行
if(!window.__POWERED_BY_QIANKUN__){
render()
}

// 子应用协议
export async function bootstrap() {}
export async function mount() {
render()
}
export async function unmount() {
ReactDOM.unmountComponentAtNode(document.getElementById("root"));
}

重写react中的webpack配置文件 (config-overrides.js)

yarn add react-app-rewired --save-dev

修改package.json文件

// react-scripts 改成 react-app-rewired
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},

在根目录新建配置文件

// 配置文件重写
touch config-overrides.js
// config-overrides.js

module.exports = {
webpack: (config) => {
// 名字和基座配置的一样
config.output.library = 'reactApp';
config.output.libraryTarget = "umd";
config.output.publicPath = 'http://localhost:20000/'
return config
},
devServer: function (configFunction) {
return function (proxy, allowedHost) {
const config = configFunction(proxy, allowedHost);

// 配置跨域
config.headers = {
"Access-Control-Allow-Origin": "*",
};
return config;
};
},
};

配置.env文件

根目录新建.env

PORT=20000
# socket发送端口
WDS_SOCKET_PORT=20000

React路由配置

import { BrowserRouter, Route, Link } from "react-router-dom"

const BASE_NAME = window.__POWERED_BY_QIANKUN__ ? "/react" : "";

function App() {
return (
<BrowserRouter basename={BASE_NAME}><Link to="/">首页Link><Link to="/about">关于Link><Route path="/" exact render={() => <h1>hello homeh1>}>Route><Route path="/about" render={() => <h1>hello abouth1>}>Route>BrowserRouter>
);
}

四、飞冰微前端实战

官方接入指南 https://micro-frontends.ice.work/docs/guide

4.1 react主应用编写

$ npm init ice icestark-layout @icedesign/stark-layout-scaffold
$ cd icestark-layout
$ npm install
$ npm start
// src/app.jsx中加入

const appConfig: IAppConfig = {

...

icestark: {
type: 'framework',
Layout: FrameworkLayout,
getApps: async () => {
const apps = [
{
path: '/vue',
title: 'vue微应用测试',
sandbox: false,
url: [
// 测试环境
// 请求子应用端口下的服务,子应用的vue.config.js里面 需要配置headers跨域请求头
"http://localhost:3001/js/chunk-vendors.js",
"http://localhost:3001/js/app.js",
],
},
{
path: '/react',
title: 'react微应用测试',
sandbox: true,
url: [
// 测试环境
// 请求子应用端口下的服务,子应用的webpackDevServer.config.js里面 需要配置headers跨域请求头
"http://localhost:3000/static/js/bundle.js",
],
}
];
return apps;
},
appRouter: {
LoadingComponent: PageLoading,
},
},
};
// 侧边栏菜单
// src/layouts/menuConfig.ts 改造

const asideMenuConfig = [
{
name: 'vue微应用测试',
icon: 'set',
path: '/vue'
},
{
name: 'React微应用测试',
icon: 'set',
path: '/react'
},
]

4.2 vue子应用接入

# 创建一个子应用
vue create vue-child
// 修改vue.config.js

module.exports = {
devServer: {
open: true, // 设置浏览器自动打开项目
port: 3001, // 设置端口
// 支持跨域 方便主应用请求子应用资源
headers: {
'Access-Control-Allow-Origin' : '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
}
},
configureWebpack: {
// 打包成lib包 umd格式
output: {
library: 'icestark-vue',
libraryTarget: 'umd',
},
}
}

src/main.js改造

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

import {
isInIcestark,
getMountNode,
registerAppEnter,
registerAppLeave,
setLibraryName
} from '@ice/stark-app'

let vue = createApp(App)
vue.use(store)
vue.use(router)

// 注意:`setLibraryName` 的入参需要与 webpack 工程配置的 output.library 保持一致
// 重要 不加不生效 和 vue.config.js中配置的一样
setLibraryName('icestark-vue')

export function mount({ container }) {
// ![](https://img-blog.csdnimg.cn/img_convert/9a8ce21b17bc7e9e5fc0abaa530f4200.png)
console.log(container,'container')
vue.mount(container);
}

export function unmount() {
vue.unmount();
}

if (!isInIcestark()) {
vue.mount('#app')
}

router改造

import { getBasename } from '@ice/stark-app';


const router = createRouter({
// 重要 在主应用中的基准路由
base: getBasename(),
routes
})

export default router

4.3 react子应用接入

create-react-app react-child
// src/app.js

import { isInIcestark, getMountNode, registerAppEnter, registerAppLeave } from '@ice/stark-app';

export function mount(props) {
ReactDOM.render(<App />, props.container);
}

export function unmount(props) {
ReactDOM.unmountComponentAtNode(props.container);
}

if (!isInIcestark()) {
ReactDOM.render(<App />, document.getElementById('root'));
}

if (isInIcestark()) {
registerAppEnter(() => {
ReactDOM.render(<App />, getMountNode());
})
registerAppLeave(() => {
ReactDOM.unmountComponentAtNode(getMountNode());
})
} else {
ReactDOM.render(<App />, document.getElementById('root'));
}

npm run eject后,改造 config/webpackDevServer.config.js

hot: '',
port: '',
...

// 支持跨域
headers: {
'Access-Control-Allow-Origin' : '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
},

五、CSS隔离方案

子应用之间样式隔离:

Dynamic Stylesheet动态样式表,当应用切换时移除掉老应用样式,再添加新应用样式,保证在一个时间点内只有一个应用的样式表生效

主应用和子应用之间的样式隔离:

  • BEM(Block Element Modifier) 约定项目前缀
  • CSS-Modules 打包时生成不冲突的选择器名
  • Shadow DOM 真正意义上的隔离
  • css-in-js

<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>shadow dom</title>
</head>
<body>
<p>hello world</p>
<div id="shadow"></div>
<script>
let shadowDOM = document.getElementById('shadow').attachShadow({mode: 'closed'}) // 外界无法访问
let pEle = document.createElement('p')
pEle.innerHTML = 'hello shadowDOM'
let styleEle = document.createElement('style')
styleEle.textContent = `p{color:red} `

// ![](https://img-blog.csdnimg.cn/img_convert/21e62bdfc6d28a07c2b65490909b8179.png)
shadowDOM.appendChild(styleEle)
shadowDOM.appendChild(pEle)

// react vue 里面的弹框等因为挂载到body上 所以用shadowDOM不行
// 会挂载到全局污染样式
//document.body.appendChild(pEle)

</script>
</body>
</html>

shadow DOM 内部的元素始终不会影响到它的外部元素,可以实现真正意义上的隔离

六、JS沙箱机制

当运行子应用时应该跑在内部沙箱环境中

  • 快照沙箱,当应用沙箱挂载或卸载时记录快照,在切换时依据快照恢复环境 (无法支持多实例)
  • Proxy 代理沙箱,不影响全局环境

1.快照沙箱

  1. 激活时将当前window属性进行快照处理
  2. 失活时用快照中的内容和当前window属性比对
  3. 如果属性发生变化保存到modifyPropsMap中,并用快照还原window属性
  4. 再次激活时,再次进行快照,并用上次修改的结果还原window属性
class SnapshotSandbox {
constructor() {
this.proxy = window;
this.modifyPropsMap = {}; // 修改了哪些属性
this.active();
}
active() {
this.windowSnapshot = {}; // window对象的快照
for (const prop in window) {
if (window.hasOwnProperty(prop)) {
// 将window上的属性进行拍照
this.windowSnapshot[prop] = window[prop];
}
}
Object.keys(this.modifyPropsMap).forEach(p => {
window[p] = this.modifyPropsMap[p];
});
}
inactive() {
for (const prop in window) { // diff 差异
if (window.hasOwnProperty(prop)) {
// 将上次拍照的结果和本次window属性做对比
if (window[prop] !== this.windowSnapshot[prop]) {
// 保存修改后的结果
this.modifyPropsMap[prop] = window[prop];
// 还原window
window[prop] = this.windowSnapshot[prop];
}
}
}
}
}
let sandbox = new SnapshotSandbox();
((window) => {
window.a = 1;
window.b = 2;
window.c = 3
console.log(a,b,c)
sandbox.inactive();
console.log(a,b,c)
})(sandbox.proxy);

快照沙箱只能针对单实例应用场景,如果是多个实例同时挂载的情况则无法解决,这时只能通过Proxy代理沙箱来实现

2.Proxy 代理沙箱

class ProxySandbox {
constructor() {
const rawWindow = window;
const fakeWindow = {}
const proxy = new Proxy(fakeWindow, {
set(target, p, value) {
target[p] = value;
return true
},
get(target, p) {
return target[p] || rawWindow[p];
}
});
this.proxy = proxy
}
}
let sandbox1 = new ProxySandbox();
let sandbox2 = new ProxySandbox();
window.a = 1;
((window) => {
window.a = 'hello';
console.log(window.a)
})(sandbox1.proxy);
((window) => {
window.a = 'world';
console.log(window.a)
})(sandbox2.proxy);

每个应用都创建一个proxy来代理window对象,好处是每个应用都是相对独立的,不需要直接更改全局的window属性

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1797537.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

面向AI应用开发实战分享 - 基础篇

“前端转AI&#xff0c;第一讲来了” 引言 如果你是一名前端开发&#xff0c;同时又对AI开发很感兴趣&#xff0c;那么恭喜你&#xff0c;机会来了。 如果不是也没关系&#xff0c;同样能帮大家了解AI应用的开发思路。 本文将带大家从面向AI开发的基础知识开始&#xff0c;再…

1、旋转在三维空间中的表现形式

有4种表达方式&#xff1a;旋转矩阵SO(3)、四元数、旋转向量和欧拉角。 一、旋转矩阵SO(3) 定义&#xff1a;旋转矩阵是一个33的正交矩阵&#xff0c;且行列式为1。表示&#xff1a;可逆矩阵&#xff0c;逆矩阵和转置矩阵相同&#xff0c;表示相反的旋转。优点&#xff1a;可…

卫星通信频段有哪些

卫星通信使用到的频段涵盖L, S, C, Ku, Ka等&#xff0c;而最常用的频段是C(4~8GHz)和Ku(12~18GHz)频段&#xff0c;而Ka(27-40GHz)频段是后起之秀。目前地球赤道上空有限的地球同步卫星轨位几乎已被各国占满&#xff0c;C和Ku频段内的频率资源被大量使用&#xff0c;而Ka频段的…

1347:【例4-8】格子游戏

【解题思路】 该题为判断无向图是否有环。可以使用并查集来完成。学习并查集时&#xff0c;每个元素都由一个整数来表示。而该问题中每个元素是一个坐标点&#xff0c;由(x, y)两个整数构成。 将二维坐标变为一个整数,通过一个公式将二维坐标换算为一个整数&…

弘君资本:20家退市!港交所迎“新”扫“旧”

港交所行政总裁陈翊庭周三到会彭博亚洲财富峰会时表明&#xff0c;对研制开支大的企业和许多科技企业来说&#xff0c;香港商场仍是具备招引力的上市渠道&#xff0c;上市规矩18C章可满足特专科技企业需求。 值得一提的是&#xff0c;首家特专科技新股晶泰科技已于6月4日正式进…

抖音小红书淘宝拼多多商家订单对接ERP|获取电商平台订单信息(商家授权)

custom-自定义API操作 支持抖音拼多多淘宝小红书 公共参数 名称类型必须描述keyString是调用key&#xff08;必须以GET方式拼接在URL中&#xff09;secretString是调用密钥api_nameString是API接口名称&#xff08;包括在请求地址中&#xff09;[item_search,item_get,item_…

恢复误删和格式化的文件的利器

一、简介 1、一款由Piriform开发的免费文件恢复工具,它能够帮助用户恢复那些不小心从电脑上删除的文件,包括从回收站清空的文件,以及因用户错误操作而从存储设备中删除的图片、音乐、文档等多种格式的文件。Recuva支持对硬盘、闪存卡、U盘等多种存储介质进行扫描与恢复,并且…

AI魔法相机:实时3D重建与场景魔法化

一、产品概述 AI魔法相机是一款创新的硬件产品,它结合了AI技术和3D重建扫描技术,能够实时捕捉并重建3D场景和物理世界。用户只需通过简单的点击操作,即可捕捉现实物体或环境,并将其无缝融合到任何场景中,创造出全新的想象现实。 二、核心功能 实时捕捉:一键式操作,迅速…

Redis限流方案

限流简介 限流算法在分布式领域是一个经常被提起的话题&#xff0c;当系统的处理能力有限时&#xff0c;如何阻止计划外的请求继续对系统施压&#xff0c;是一个需要重视的问题。 除了控制流量&#xff0c;限流还有一个应用目的是用于控制用户行为&#xff0c;避免垃圾请求&a…

java版spring cloud 知识付费平台的功能模块与子模块划分

随着互联网技术的飞速发展&#xff0c;知识付费平台已经成为了我国在线教育领域的一颗新星。这些平台以用户需求为出发点&#xff0c;围绕高质量的内容打造&#xff0c;利用互联网技术为用户提供了一个便捷、高效的学习环境。它们汇聚了丰富的专业知识&#xff0c;覆盖了职业技…

Pulsar 社区周报 | No.2024-05-30 | BIGO 百页小册《Apache Pulsar 调优指南》

“ 各位热爱 Pulsar 的小伙伴们&#xff0c;Pulsar 社区周报更新啦&#xff01;这里将记录 Pulsar 社区每周的重要更新&#xff0c;每周发布。 ” BIGO 百页小册《Apache Pulsar 调优指南》 Hi&#xff0c;Apache Pulsar 社区的小伙伴们&#xff0c;社区 2024 上半年度的有奖问…

STM8单片机变频器设计

变频调速技术是现代电力传动技术的重要发展方向,而作为变频调速系统的核心—变频器的性能也越来越成为调速性能优劣的决定因素,除了变频器本身制造工艺的“先天”条件外,对变频器采用什么样的控制方式也是非常重要的。随着电力电子技术、微电子技术、计算机网络等高新技术的…

springboot 医院预约挂号app-计算机毕业设计源码65042

摘 要 随着互联网时代的到来&#xff0c;同时计算机网络技术高速发展&#xff0c;网络管理运用也变得越来越广泛。因此&#xff0c;建立一个B/S结构的医院预约挂号系统&#xff0c;会使&#xff1b;医院预约挂号系统的管理工作系统化、规范化&#xff0c;也会提高平台形象&…

el-date-picker的结束日期的时分秒为0:0:0时修改成23:59:59

<el-date-pickerv-model"taskTime"type"datetimerange"range-separator"-"start-placeholder"开始时间"end-placeholder"结束时间"change"handleTimeChange" /> js <script setup lang"ts"&…

XR和Steam VR项目合并问题

最近有一个项目是用Steam VR开发的&#xff0c;里面部分场景是用VRTK框架做的&#xff0c;还有一部分是用SteamVR SDK自带的Player预制直接开发的。 这样本身没有问题&#xff0c;因为最终都是通过SteamVR SDK处理的&#xff0c;VRTK也管理好了SteamVR的逻辑&#xff0c;并且支…

java:一个简单的WebFlux的例子

【pom.xml】 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId><version>2.3.12.RELEASE</version> </dependency> <dependency><groupId>org.spr…

从传统工厂到数字化工厂,从这几方面着手

信息技术与制造技术的结合&#xff0c;产生了新的生产实现模式&#xff0c;是制造业迈向智能化的重要标志。实践应用表明&#xff0c;数字化工厂可以缩短制造周期&#xff0c;优化生产流程&#xff0c;提高协同工作能力&#xff0c;降低成本。 从传统工厂到数字化工厂的转型是一…

.NET集成DeveloperSharp操作Redis缓存

&#x1f3c6;作者&#xff1a;科技、互联网行业优质创作者 &#x1f3c6;专注领域&#xff1a;.Net技术、软件架构、人工智能、数字化转型、DeveloperSharp、微服务、工业互联网、智能制造 &#x1f3c6;欢迎关注我&#xff08;Net数字智慧化基地&#xff09;&#xff0c;里面…

优思学院|一文看懂新版FMEA与FMEA的七大步骤

FMEA的起源 FMEA最早起源于20世纪40年代的美国军工行业。当时&#xff0c;美国军方为了提高武器系统的可靠性和安全性&#xff0c;开始使用FMEA来识别和评估潜在的故障模式及其影响。1949年&#xff0c;美国军方发布了《军用程序手册》&#xff08;Military Procedures Handbo…

【TS】进阶

一、类型别名 类型别名用来给一个类型起个新名字。 type s string; let str: s "123";type NameResolver () > string;: // 定义了一个类型别名NameResolver&#xff0c;它是一个函数类型。这个函数没有参数&#xff0c;返回值类型为string。这意味着任何被…