微前端,前端这次词就不用多做解释了,这个概念的重点在于这个“微”字, 从字面意义上看,微是小的意思,小是相对于大的一个用于比较的形容词,所以通常是在项目庞大的情况下,才会考虑将它变小,去考虑将它拆分成若干个小项目。这就是做微前端所要达到的主要目标,将庞大的项目拆分成多个独立运行、独立部署和独立开发的小项目,使得项目利于维护和更新,然后在运行时,作为一个整体来呈现。
项目过于庞大的可能存在一系列问题,比如构建速度慢、应用加载慢、定位问题麻烦、项目可维护性差等等。
过往的案例中,通常会使用iframe作为微前端的一种解决方案,但iframe有一些明显的缺点,比如浏览器的前进后退,由于iframe的url不会显示在浏览器的地址栏,就会使前进后退看上去有点奇怪,因为如果iframe存在历史记录,就会先对iframe的历史记录进行前进后退操作,但在地址栏上不会体现出来;并且我们也不能通过地址栏在主应用中直接打开子应用中某个页面,并且刷新也存在问题(子应用的状态会丢失);还有iframe是与外部窗口隔离开的,如果有弹窗需要遮罩层,就会使样式很奇怪,因为遮罩层只会遮住iframe的部分;然后iframe与外部窗口通信也不是很方便。当然iframe还是有优点的,比如资源隔离,样式之间不会互相影响,还可以加载外部页面,这在早期也算比较好用的一种方案。
<!-- index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>主应用</title>
</head>
<body>
<style>
body {
font-size: 22px;
color: #666;
background-color: #f4f5ff;
}
</style>
<p>我是主页面的内容</p>
<iframe src="./p1.html"></iframe>
</body>
</html>
<!-- p1.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>P1应用</title>
</head>
<body>
<style>
body {
font-size: 20px;
color: orange;
background-color: #e4e5ff;
}
</style>
<p>
我是p1的内容
<button onclick="window.location.href='./p2.html'">点击跳转p2</button>
<button onclick="window.location.href='https://126.com'">点击跳转126邮箱</button>
</p>
</body>
</html>
<!-- p2.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>P2应用</title>
</head>
<body>
<style>
body {
font-size: 20px;
color: blue;
background-color: #e4e5ff;
}
</style>
<p>
我是p2的内容
<button onclick="window.location.href='./p1.html'">点击跳转p1</button>
</p>
</body>
</html>
虽然以前没有微前端的概念,但其实很多平台类的应用都存在对微前端的实践,他们会在主应用中提供入口,让用户可以进入其他应用去使用其他功能,而不是把所有的功能、所有的东西都放在一个应用里面。
single-spa是近几年出来的微前端框架,带火了微前端的概念,我还没在项目中正式使用过,现在刚开始接触学习。它主要参考了近几年流行框架中路由的概念,来推出的一种方案。
借助single-spa脚手架,我们可以搭建三类项目:
相关的概念,比如:Root Config、Application、Parcel、Layout Engine,都可以在single-spa的官网上查阅。
知道了这些,就可以开始动手搭建简单的demo项目了。
# ying.ye @ xieyingdeMacBook-Pro in ~/CodeProjects/mfe-demo1 [10:38:13]
$ npx create-single-spa
npx: 393 安装成功,用时 79.396 秒
? Directory for new project platform
? Select type to generate single-spa root config
? Which package manager do you want to use? yarn
? Will this project use Typescript? No
? Would you like to use single-spa Layout Engine No
? Organization name (can use letters, numbers, dash or underscore) becky
directory,就是项目创建在哪个目录下,默认是点,就是当前目录,为了方便管理所有微前端应用,我把这个项目创建在platform目录,这个目录它会自动创建,select type,应用的类型,选择single-spa root config,接下来是包管理器,选择yarn,当然选其他的也可以,是否使用ts,因为是简单的demo,就不用了,是否使用single-spa-layout,暂时选择不用,最后是organization name,就是组织名称,我用我的英文名becky。
项目初始化完成后,看一下项目的内容,整体结构的话和普通脚手架生成的项目并没有太大的不同,我们可以先关注src目录下的两个文件,index.ejs和becky-root-config.js。index.ejs就是之前说的子应用所共用的html页面,becky-root-config.js就是前面所说的root config文件,它的名字是由organization name和root config组合,使用横杆拼接起来的。
我们可以先运行一下这个项目,打开localhost:9000。页面上有一串welcome之类的文字,就是一个欢迎页面。但是我们刚刚看了,项目里就两个文件,那这些内容是哪里来的呢?我们可以再仔细看下两个文件的内容。
首先是index.ejs。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Root Config</title>
<!--
Remove this if you only support browsers that support async/await.
This is needed by babel to share largeish helper code for compiling async/await in older
browsers. More information at https://github.com/single-spa/create-single-spa/issues/112
-->
<script src="https://cdn.jsdelivr.net/npm/regenerator-runtime@0.13.7/runtime.min.js"></script>
<!--
This CSP allows any SSL-enabled host and for arbitrary eval(), but you should limit these directives further to increase your app's security.
Learn more about CSP policies at https://content-security-policy.com/#directive
-->
<meta http-equiv="Content-Security-Policy" content="default-src 'self' https: localhost:*; script-src 'unsafe-inline' 'unsafe-eval' https: localhost:*; connect-src https: localhost:* ws://localhost:*; style-src 'unsafe-inline' https:; object-src 'none';">
<meta name="importmap-type" content="systemjs-importmap" />
<!-- If you wish to turn off import-map-overrides for specific environments (prod), uncomment the line below -->
<!-- More info at https://github.com/joeldenning/import-map-overrides/blob/master/docs/configuration.md#domain-list -->
<!-- <meta name="import-map-overrides-domains" content="denylist:prod.example.com" /> -->
<!-- Shared dependencies go into this import map. Your shared dependencies must be of one of the following formats:
1. System.register (preferred when possible) - https://github.com/systemjs/systemjs/blob/master/docs/system-register.md
2. UMD - https://github.com/umdjs/umd
3. Global variable
More information about shared dependencies can be found at https://single-spa.js.org/docs/recommended-setup#sharing-with-import-maps.
-->
<script type="systemjs-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js"
}
}
</script>
<link rel="preload" href="https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js" as="script">
<!-- Add your organization's prod import map URL to this script's src -->
<!-- <script type="systemjs-importmap" src="/importmap.json"></script> -->
<% if (isLocal) { %>
<script type="systemjs-importmap">
{
"imports": {
"@becky/root-config": "//localhost:9000/becky-root-config.js"
}
}
</script>
<% } %>
<!--
If you need to support Angular applications, uncomment the script tag below to ensure only one instance of ZoneJS is loaded
Learn more about why at https://single-spa.js.org/docs/ecosystem-angular/#zonejs
-->
<!-- <script src="https://cdn.jsdelivr.net/npm/zone.js@0.11.3/dist/zone.min.js"></script> -->
<script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2.2.0/dist/import-map-overrides.js"></script>
<% if (isLocal) { %>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.js"></script>
<% } else { %>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/amd.min.js"></script>
<% } %>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<main></main>
<script>
System.import('@becky/root-config');
</script>
<import-map-overrides-full show-when-local-storage="devtools" dev-libs></import-map-overrides-full>
</body>
</html>
可以看到,这里使用了一些在普通项目里很少有看到使用的东西,比如类型为systemjs-importmap的script标签,它里面的内容是json,然后在其他script标签中,还使用了System.import方法。这涉及到两块内容:importmap和systemjs。
importmap直译过来是导入映射,与模块的使用有关,一般我们在项目中导入模块,会调用require方法,或者使用import语句或方法,引入的模块通常需要使用npm之类的包管理器进行管理。但是import map提供了一种支持,让我们可以直接在页面上管理模块,不需要通过打包构建。不过由于这个特性比较新,很多浏览器不支持,可以看一个小的示例。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>import maps demo</title>
<script type="importmap">
{
"imports": {
"react": "https://cdn.skypack.dev/react@17.0.1",
"react-dom": "https://cdn.skypack.dev/react-dom",
"moment": "https://cdn.skypack.dev/moment"
}
}
</script>
</head>
<body>
<div id="root"></div>
<script type="module">
import React from 'react';
import ReactDOM from 'react-dom';
import moment from 'moment';
ReactDOM.render(`Hello World: ${moment().format('YYYY-MM-DD HH:mm:ss')}`, document.getElementById('root'));
</script>
</body>
</html>
单独运行这个页面,可以看到不会报错,是正常运行显示的,说明我们完全不需要构建就可以使用import语句导入模块。
Import maps 本质上是一个配置文件,可以让开发者将模块标识符映射到一到多个文件,描述了依赖的解析方式,某种程度上,Import maps 给浏览器端带来了包管理,但是目前支持 Import Maps 的浏览器还很少。
简单来说importmap的作用就是使浏览器端支持模块的解析,而不需要应用构建步骤,这使得前端开发更便捷了,但是import maps现在来使用的话存在一个缺点,就是需要所有模块都导出成 ESModule,当前社区当中的很多模块都没有导出成 ESModule,有些模块甚至没有经过编译,所以目前使用仍然有一定困难。
systemjs可以说是import maps的一种兼容方案,同样有模块管理的功能,在浏览器端实现了对 CommonJS、AMD、UMD 等各种模块的加载;它提供了一套自己的加载方法,我们可以对刚才的demo做一些改动,来看看效果。
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>import maps demo</title>
<script type="systemjs-importmap">
{
"imports": {
"react": "https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js",
"moment": "https://cdn.jsdelivr.net/npm/moment@2.29.4/moment.min.js"
}
}
</script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/system.js"></script>
<script src="https://cdn.jsdelivr.net/npm/systemjs@6.8.3/dist/extras/use-default.js"></script>
</head>
<body>
<div id="root"></div>
<script type="module">
const React = await System.import('react');
const ReactDOM = await System.import('react-dom');
const moment = await System.import('moment');
// console.log(moment);
ReactDOM.render(`Hello World: ${moment().format('YYYY-MM-DD HH:mm:ss')}`, document.getElementById('root'));
</script>
</body>
</html>
首先将script的type改为systemjs-importmap,然后我们改为使用umd的模块,并在页面上使用script标签引入systemjs,在浏览器中引入system.js后,会去解析类型为systemjs-importmap的script标签里的import映射。最后对这个类型为module的script标签中的内容进行修改,将import语句都改为使用System.import方法。因为调用System.import得到的是一个promise,我们直接使用await来获取最终的内容。
log一下变量,可以看到调用System.import方法直接获取的是整个模块,为了使用更方面,我们可以在页面上引入systemjs的use-default脚本,将模块的default部分提取出来。
再单独运行一下这个页面,可以看到,呈现的效果是一样的。
关于importmap和systemjs就先到这里不细讲了,因为具体的我也还没仔细看。
回到index.ejs,我们看到,使用System.import导入了一个@becky/root-config,再看上面的importmap,我们可以看到,这个标识符对应的就是becky-root-config.js文件。也就是说页面上引用了root config打包构建后的文件。
index.ejs文件的最后是一个import-map-overrides-full的标签,它是single-spa提供的一个开发工具,show-when-local-storage="devtools",说明只要将localstorage中的devtools设置为true,就可以看到页面的右下角有一个图标。
点击这个图标可以打开一个面板,上面展示了浏览器管理的两个模块,就是我们在importmap中定义的,通过这个工具,我们可以对应用中定义的importmap映射地址进行修改替换,方便本地调试。当然我们也可以通过安装浏览器插件的方式来进行调试,对于chrome可以安装一个single-spa-inspector的插件来管理所有主应用下的子应用。
index.ejs看完了,再来看下root-config文件,这个文件很关键,我们通过这个文件来注册应用。可以看到顶部导入了两个方法,一个是registerApplication,见名知意,就是注册应用的方法,一个是start,就是启动项目的方法。
import { registerApplication, start } from "single-spa";
registerApplication({
name: "@single-spa/welcome",
app: () =>
System.import(
"https://unpkg.com/single-spa-welcome/dist/single-spa-welcome.js"
),
activeWhen: ['/'],
});
//
// registerApplication({
// name: '@becky/navbar',
// app: () => System.import("@becky/navbar"),
// activeWhen: "/"
// })
start({
urlRerouteOnly: true,
});
可以看到,这里注册了一个子应用的示例,参数是一个对象,包含了三个属性,name、app和activeWhen,name就是名称,
@single-spa/welcome,app就是对应的应用,可以看到是导入了一个single-spa官方的用作示例的欢迎页,最后是activeWhen,就是这个应用在什么条件下激活,默认是“/”,所以根路由被激活时就会显示,所以我们刚刚访问localhost:9000页面看到的内容就是它。
activeWhen可以是一个字符串、或者函数,也可以是一个数组,包含函数或字符串类型的元素,本质上是一个函数,如果是字符串或者数组中的字符串元素,实际会被处理成用location进行判断,判断location.pathname是否是这个字符串开头的,如果是,路由就被激活,如果activeWhen是一个函数或者数组中包含的函数元素,则这个函数有一个默认传参是location,根据函数返回值判断路由对应的应用是否被激活。
所以activeWhen: ['/']相当于activeWhen: [(location) => location.pathname.startWith('/')]
可以看出single-spa是通过js文件接入子应用的。
检查元素看一下,可以看到页面上有一个div元素,id为single-spa-application:后边再跟一个应用名@single-spa/welcome,div里边就是欢迎页的内容。
现在我们在root-config文件中注册一个简单的应用,比如写一个app1,使用registerApplication注册。因为是项目中的文件,我们直接调用require,或者()=>import,activeWhen我们就用“/app1”。运行一下,访问路由/app1,可以看到app1被挂载了,我们写的内容也显示在页面上了。
// src/app1.js
export const bootstrap = (props) => {
return Promise
.resolve()
.then(() => {
console.log('App1 bootstrapped!');
})
}
export const mount = (props) => {
console.log(props);
return Promise
.resolve()
.then(() => {
const ele = document.createElement('div');
ele.id="becky-app1";
ele.innerText='App1 mounted!!';
document.body.append(ele);
console.log('App1 mounted!');
})
}
export const unmount = (props) => {
return Promise
.resolve()
.then(() => {
console.log('App1 unmounted!');
})
}
// src/becky-root-config.js
registerApplication({
name: '@becky/app11',
app: require('./app1'), // ()=>import('./app1)
activeWhen: '/app1'
});
回到app1.js的内容,可以看到导出了几个方法,bootstrap、mount和unmount,通过single-spa官网对应用的定义,我们了解到,导出应用必须要定义这三个生命周期方法,来对应用的启动、挂载和卸载的过程做一些处理。当然如果需要的话,我们还可以定义unload生命周期方法。这些生命周期方法可以接收到一些属性,我们可以通过log来打印查看。
当然实际项目肯定没这么简单,我们再来创建几个项目,来作为子应用。同样使用single-spa的脚手架来创建。
# ying.ye @ xieyingdeMacBook-Pro in ~/CodeProjects/mfe-demo1 [10:38:13]
$ npx create-single-spa
npx: 393 安装成功,用时 44.124 秒
? Directory for new project navbar
? Select type to generate single-spa application / parcel
? Which framework do you want to use? react
? Which package manager do you want to use? yarn
? Will this project use Typescript? No
? Organization name (can use letters, numbers, dash or underscore) becky
? Project name (can use letters, numbers, dash or underscore) navbar
同样的,需要选择directory,就是项目创建在什么目录下,默认是点,我把这个项目创建在navbar目录,作为整个应用的导航栏,select type,应用的类型,选择application or parcel,使用的ui框架,选择react,接下来是包管理器,选择yarn,是否使用ts,简单的demo就不用了,暂时选择不用,organization name,还是使用我的英文名becky,最后是project name,在root config项目创建的时候脚手架给设置了默认项目名root config,普通的子应用我们可以自己设置项目名,还是设置navbar。
同样的步骤我们再创建几个项目。
在子项目中执行yarn:start --port 8081命令,默认start用的端口是8080,因为要跑多个子项目,我们通过--port来指定端口,通过8081我们并不能直接访问目标页面,打开localhost:8081,我们可以看到一个页面,上面的文字提示我们需要在一个root config应用下来预览这个子应用页面,或者执行yarn start:standalone命令来预览,前面已经跑起来一个root config项目了,我们可以尝试直接把这个子应用注册到root config中。
先复制localhost:8081页面上提示我们复制的URL地址。
这次注册我们直接使用System.import,因为这个地址包含的内容是打包后的模块。
// src/becky-root-config.js
registerApplication({
name: '@becky/navbar',
app: () => System.import("http://localhost:8081/becky-navbar.js"),
activeWhen: "/"
})
此时刷新页面后,会发现页面是空白的,控制台报了一个错误,提示无法解析‘react’这个标识符,这是因为我们的子应用使用了react框架,但是子应用使用webpack构建时是使用externals把react设置为外部模块,所以无法解析,此时我们可以选择在root config项目下的index.ejs文件中,使用importmap来引入react。
<!-- index.ejs -->
<script type="systemjs-importmap">
{
"imports": {
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.0/lib/system/single-spa.min.js",
"react": "https://cdn.jsdelivr.net/npm/react@16.13.1/umd/react.production.min.js",
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@16.13.1/umd/react-dom.production.min.js"
}
}
</script>
再次刷新页面,我们就可以看到页面有@becky/navbar is mounted!的字样,代表子应用挂载成功了。
为了管理方便,我们把navbar的模块也放到importmap中。
<!-- src/index.ejs -->
<% if (isLocal) { %>
<script type="systemjs-importmap">
{
"imports": {
"@becky/root-config": "//localhost:9000/becky-root-config.js",
"@becky/navbar": "http://localhost:8081/becky-navbar.js"
}
}
</script>
<% } %>
// src/becky-root-config.js
registerApplication({
name: '@becky/navbar',
app: () => System.import("@becky/navbar"),
activeWhen: "/"
})
刚刚我们创建的子应用可以都这样直接调用registerApplication来注册,但是这样一个个注册不太方便,页面布局也不太直观,而且刚刚直接访问根路径时,我们看到navbar是在顶部,但是访问app1的时候,navbar又在app1的下面,为了方便布局和注册应用,我们可以使用single-spa-layout这个库。通过使用这个库,我们还可以定义应用加载时的过渡效果。
# 在root config项目下安装依赖
yarn add single-spa-layout
然后对我们的index.ejs做一些修改。我们可以把官网的demo复制过来,然后做一点修改,把刚刚创建的几个子应用运行一下,并且把对应模块放到importmap中。
<!-- index.ejs -->
<% if (isLocal) { %>
<script type="systemjs-importmap">
{
"imports": {
"@becky/root-config": "//localhost:9000/becky-root-config.js",
"@becky/navbar": "http://localhost:8081/becky-navbar.js",
"@becky/settings": "http://localhost:8082/becky-settings.js",
"@becky/students": "http://localhost:8083/becky-students.js"
}
}
</script>
<% } %>
<!-- ... -->
<template id="single-spa-layout">
<single-spa-router>
<nav class="topnav">
<application name="@becky/navbar"></application>
</nav>
<div class="main-content">
<route path="settings">
<application name="@becky/settings"></application>
</route>
<route path="students">
<application name="@becky/students"></application>
</route>
</div>
</single-spa-router>
</template>
然后修改我们的root config文件。同样的把官网中的demo代码复制过来。
// src/becky-root-config.js
import {
constructApplications,
constructRoutes,
constructLayoutEngine,
} from 'single-spa-layout';
const routes = constructRoutes(document.querySelector('#single-spa-layout'));
const applications = constructApplications({
routes,
loadApp({ name }) {
return System.import(name);
},
});
const layoutEngine = constructLayoutEngine({ routes, applications });
applications.forEach(registerApplication);
刷新页面,通过访问我们使用route标签定义的路由,可以激活不同的子应用。检查元素时,可以看到子应用挂载的布局与我们定义的single-spa-router中的内容是保持一致的。
此时一个简单的demo就完成了。
接下来我们可以修改一下navbar,就不用手动去改地址栏了。
打开navbar项目,直接查看src下面的文件,这个test文件是用于测试的,可以先不管,becky-navbar.js就是定义应用的主文件,可以看到使用了single-spa-react这个库,来创建single-spa应用,要修改navbar的展示内容,我们主要看rootComponent这个参数,它指定了应用的根组件,也就是说子应用的内容都是在这个组件里面,我们看到根组件就是这个root.component.js。
打开这个文件,我们做一点修改。首先我们看这个根组件,它接收了一个props参数,里面包含了跟应用相关的一些信息,比如props.name,就是应用名称,刚刚显示在页面上了。
export default function Root(props) {
const onClick = (path) => {
window.singleSpaNavigate(path);
}
return (
<section>
<li>
<a href="" onClick={ () => onClick('/settings')}>Settings</a>
</li>
<li>
<a href="" onClick={ () => onClick('/students')}>Students</a>
</li>
</section>
);
}
完成修改后就可以看到效果了,点击不同的链接可以跳转不同的路由。
我们可以继续尝试在子应用中配置子应用自己的路由。比如students项目,我们给他添加路由功能。
首先添加react-router-dom依赖,再对root.component进行改造。再添加几个react组件,并配置一张路由表。由于子应用的路由是基于主应用的路由,所以给BrowserRouter配置一个basename属性,/students。
// src/root.component.js
import { BrowserRouter } from "react-router-dom";
import App from './components/App';
export default function Root(props) {
return (
<BrowserRouter basename={"/students"}>
<App />
</BrowserRouter>
);
}
// src/components/App.js
import React from 'react';
import {NavLink, useRoutes, useInRouterContext} from 'react-router-dom';
import routes from "../routes";
function App(props) {
console.log('xxx', useInRouterContext())
// 根据路由表生成对应的路由规则
const element = useRoutes(routes);
return (
<div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/* 路由链接 */}
<NavLink className="list-group-item" to="/list">List</NavLink>
<NavLink className="list-group-item" end to="/detail">Detail</NavLink>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel-body">
{/* 在展示路由组件的位置注册路由 */}
{element}
</div>
</div>
</div>
</div>
</div>
);
}
export default App;
// src/components/Detail.js
import React from 'react';
function Detail(props) {
return (
<h3>我是Detail的内容</h3>
);
}
export default Detail;
// src/components/List.js
import React from 'react';
function List(props) {
return (
<h3>我是List的内容</h3>
);
}
export default List;
// src/routes/index.js
import List from "../components/List";
import Detail from "../components/Detail";
export default [
{
path: '/list',
element: <List/>
},
{
path: '/detail',
element: <Detail/>,
}
]
改造完成后,重启students项目。再继续看,可以正常运行并完成路由跳转。
简单的demo就到此为止了。
最后我们再看一下webpack的配置。文件很简单,因为single-spa这个框架做了一层封装,我们可以在students项目把配置打印出来看一下。
主要看一下output和externals,externals配置的是引用外部的模块,可以看到single-spa、react、react-dom这些三方库都是引用的外部模块,也就是利用了我们在importmap中配置的模块映射,还有@becky开头的模块标识符也是引用了外部的模块,这些也是在importmap中配置了。
然后是output,看到输出的文件是becky-students.js,也就是importmap映射的子应用的文件。libraryTarget是system,他指定了使用system的模式处理模块,我们可以从network再看一下页面加载的becky-student.js的内容,可以看到文件的开头就调用了System.register注册了模块,这又涉及到了systemjs的内容。
简单说了下微前端、以及single-spa的基础使用。有兴趣同学的可以继续查阅single-spa的官网文档、以及其他优秀的文章。
最后再说些微前端的缺点,比如子应用如果维护在不同的代码库里,这可能会造成代码分散,不利于整体的管理,对管理者要求更高,如果有新需求迭代,需要盘点确认涉及哪些子应用,还可能出现三方库版本不同所造成的维护问题。使用single-spa的话由于没有沙箱环境,可能会出现样式互相干扰的情况,需要去处理。由于我还没有具体的实践经验,那在实际使用中还可能出现一些问题,比如如何去拆分一个大型项目。
关于是否使用微前端,通过何种方式、何种框架来实践微前端,可以借鉴他人经验,根据实际情况来整体考量。
我在我的RubyonRails应用程序中使用ActiveRecord::Store模块时遇到了一个奇怪的问题。据我了解,该模块在后台使用“序列化”方法,因此它只是使用ruby内置的psychgem将您的数据序列化为yaml格式。大多数时候它工作正常,但有时我会收到500错误并显示以下消息:LoadError(cannotloadsuchfile--enc/trans/single_byte):~/.rbenv/versions/1.9.3-p286/lib/ruby/1.9.1/psych/visitors/emitter.rb:27:in`write'~/.rbenv/versi
在Rails3.x应用程序中,我正在使用net::ssh并向远程pc运行一些命令。我想向用户的浏览器显示实时日志。比如,如果两个命令在net中运行::ssh执行即echo"Hello",echo"Bye"被传递然后"Hello"应该在执行后立即显示在浏览器中。这是代码我在rubyonrails应用程序中使用ssh连接和运行命令Net::SSH.start(@servers['local'],@machine_name,:password=>@machine_pwd,:timeout=>30)do|ssh|ssh.open_channeldo|channel|channel.requ
我正在为Jekyll编写一个转换器插件,需要访问一些页眉(YAML前端)属性。只有内容被传递给主要的转换器方法,似乎无法访问上下文。例子:moduleJekyllclassUpcaseConverter关于如何在转换器插件中访问页眉数据有什么想法吗? 最佳答案 基于Jekyll源代码,无法在转换器中检索YAML前端内容。根据您的情况,我看到了两种可行的解决方案。您的文件扩展名可以具有足够的描述性,以提供您本应包含在前言中的信息。看起来Converter插件的设计就是这么基本的。如果修改Jekyll是一个选项,您可以更改Convert
一、简介之前在Vue项目中使用过element的上传组件,实现了点击上传+拖拽上传的两种上传功能。然后我就在想是否可以通过原生的html+js来实现文件的点击上传和拖拽上传,说干就干。首先是点击获取上传文件自然没的说,只需要借助input标签即可,但原生的点击上传按钮,实在是过于简陋,所以我的想法是通过一个div,模拟成上传按钮,然后监听其点击事件,通过input.click()去模拟点击真正的上传元素。然后是拖拽获取上传文件,这个稍有难度,我的想法是通过HTML5新增的drag拖放API+dataTransfer来实现文件的拖拽获取,但是由于是html5新增的,所以可能在某些低版本IE浏览器
我目前一直在研究如何根据我们想要的每个条件来分离CanCan的角色。在我们的应用程序中,有很多类别(例如数学、英语、历史等),每个类别中都有很多类(class)。每个用户可以在每个类别中扮演许多不同的角色。例如,约翰可以是数学的“读者”,这意味着他可以阅读所有数学类(class)。约翰也可以是英语的“作家”,这意味着他可以阅读所有英语类(class),在类别英语中创建类(class),并仅编辑/删除他创建的英语类(class)。如果这些是John仅有的角色,他将无法在导航栏中看到类别历史记录,并且将被拒绝访问历史记录中的类(class)。这些是关系的建立方式:classUser在mod
一、介绍一下vercelvercel是一个站点托管平台,提供CDN加速,同类的平台有Netlify和GithubPages,相比之下,vercel国内的访问速度更快,并且提供Production环境和development环境,对于项目开发非常的有用的,并且支持持续集成,一次push或者一次PR会自动化构建发布,发布在development环境,都会生成不一样的链接可供预览。但是vercel只是针对个人用户免费,teams是收费的首先vercel零配置部署,第二访问速度比github-page好很多,并且构建很快,还是免费使用的,对于部署个人前端项目路、接口服务非常方便vercel类似于git
🐱个人主页:不叫猫先生🙋♂️作者简介:前端领域新星创作者、阿里云专家博主,专注于前端各领域技术,共同学习共同进步,一起加油呀!💫系列专栏:vue3从入门到精通、TypeScript从入门到实践📢资料领取:前端进阶资料以及文中源码可以找我免费领取🔥前端学习交流:博主建立了一个前端交流群,汇集了各路大神,一起交流学习,期待你的加入!(文末有我wx或者私信)目录前言一、vue自定义指令directive讲解二、基于DOM的实现方式1.思路整理2.新建index.vue3.新建`directives`文件4.在`directives`文件下创建`index.ts`文件5.在`main.ts`中全局引
一、乱花迷人眼我就是被迷的那双眼。有时候需求来了,用熟悉的套路进行开发,确实很节省时间也能保证功能的稳定,但是这些开发的惯性无形中阻碍了我对技术的探索。我一直想改造详情页,解放重复功能开发的劳动力,但是详情页一眼望都是内容平铺,好像并没有什么可做的代码设计。后来我拨开繁花,发现详情页的组件化不必想的过于复杂,后台系统风格统一即可。因为大部分的详情页面是内容的展示,偶尔会出现少量的操作功能。将风格统一的部分进行组件化处理,操作功能使用回调函数放回当前页面,避免组件里做过多的业务逻辑。看,这不就成了。项目基于React框架开发的,所以代码写法是JSX语法,组件开发使用的hooks函数式组件,UI框
这篇文章网络结构ESRT(EfficientSuper-ResolutionTransformer)还是蛮复杂的,是一个CNN和Transformer结合的结构。文章提出了一个高效SRTransformer结构,是一个轻量级的Transformer。作者考虑到图像超分中一张图像内相似的细节部分可以作为参考补充,(类似于基于参考图像Ref的超分),于是引入了Transformer,可以在图像中建模一种长期依赖关系。而ViT这些方法计算量太大,太占内存,于是提出了这个轻量版的Transformer结构(ET)ET只使用了transformer中的encoder,并且作者还使用了featurespi
2023届EDA领域校招总结,完结撒花!!!目录前言一、EDA公司介绍二、项目面试1.自我介绍2.项目深入3.专业经验4.成果和技能5.对面试官有什么问题三、C++面试1、高频考点2、其他知识点3、算法题四、逻辑综合面试1.逻辑综合知识详解2.开源逻辑综合ABC五、简历制作总结前言2022/08/26:本人2023年6月毕业,于2022年7-10月参加秋招,面试总结纯属个人经验,仅供参考面试的是EDA前端软件开发岗位,也会掺杂一些EDA其他流程的面试在面试过程中发现自己准备的很乱,没有一个清晰的思路,现在把自己面试的所有经历和题型整理出来,在这里做一个小的总结,不仅帮助自己整理思路,也给大家做