欢迎光临 简单路由器
Svelte 5 的简单而灵活的 SPA 路由器
说明
Svelte Simple路由器是一款用于Svelte 5的本土单页应用程序(SPA)路由器,在利用Svelte的回放和过渡的同时,为无缝客户端导航而设计.
它既支持History API,也支持散列路由,提供灵活的URL管理,并具有稳健的路径匹配,查询参数处理和动态插值. 包装了巢景,路由守护,懒惰地加载,动态路由等功能,并与斯维尔特的"过渡API"和"视图过渡API"完全融合后,可以进行平滑和互动的导航体验.
Svelte Simple Router是开发者想要在其Svelte应用程序中添加路由而不出现不必要的复杂性的完美解决方案.
先决条件
注:斯维尔特简易路由器是一个斯维尔特5本地文库,不会与斯维尔特的前作版本相配合.
- 丝绒 5.0.0
安装
pnpm add @dvcol/svelte-simple-router
开始
最小设置需要 RouterView 组件和路径列表.
这个 RouterView 构成部分将使与现有路线有关的构成部分到位.
<script lang="ts">
import type { Route, RouterOptions } from '@dvcol/svelte-simple-router/models';
import { RouterView } from '@dvcol/svelte-simple-router/components';
import GoodbyeComponent from '~/components/goodbye/GoodbyeComponent.svelte';
import HelloComponent from '~/components/hello/HelloComponent.svelte';
const RouteName = {
Hello: 'hello',
Goodbye: 'goodbye',
Home: 'home',
Any: 'any',
} as const;
type RouteNames = (typeof RouteName)[keyof typeof RouteName];
const routes: Readonly<Route<RouteNames>[]> = [
{
name: RouteName.Home,
path: '/',
redirect: {
name: RouteName.Hello,
},
},
{
name: RouteName.Hello,
path: `/${RouteName.Hello}`,
component: HelloComponent,
},
{
name: RouteName.Goodbye,
path: `/${RouteName.Goodbye}`,
component: GoodbyeComponent,
},
{
name: RouteName.Any,
path: '*',
redirect: {
name: RouteName.Hello,
},
},
] as const;
const options: RouterOptions<RouteNames> = {
routes,
} as const;
</script>
<RouterView {options} />
高级使用
路由器上下文
这个 RouterContext 组件将路由器实例注入组件树.
它可用于共享路由器实例 RouterView 组件不需要通过它作为道具.
<script lang="ts">
import { RouterContext } from '@dvcol/svelte-simple-router/components';
</script>
<RouterContext>
<!-- children -->
</RouterContext>
调试器
这个 RouterDebugger 财务报告和审定财务报表 RouteDugger 组件将显示当前路由和路由器状态.
它可用于调试路由器配置和当前路由.
它需要放在一个 RouterView 或者说 RouterContext 组件.
<script lang="ts">
import { RouterView } from '@dvcol/svelte-simple-router/components';
import { RouteDebugger, RouterDebugger } from '@dvcol/svelte-simple-router/components/debug';
</script>
<RouterView>
<RouterDebugger />
<RouteDebugger />
</RouterView>
网状路线
这个 RouterView 组件可以嵌入到另一个下 RouterView 或者说 RouterContext 组件.
名称 RouterView 组件可用于在同一路由上制取不同的组件.
每个 RouterView 从最近的路由器上下文抓取 RouterContext 或者说 RouterView 组件.
注:兄弟会 RouterView 或者说 RouterContext 组件将即时化一个新的路由器实例.
<script lang="ts">
import type { Route, RouterOptions } from '@dvcol/svelte-simple-router/models';
import { RouterContext, RouterView } from '@dvcol/svelte-simple-router/components';
import ChildComponent from '~/components/goodbye/ChildComponent.svelte';
import ParentComponent from '~/components/hello/ParentComponent.svelte';
const RouteName = {
Parent: 'parent',
Child: 'child',
} as const;
type RouteNames = (typeof RouteName)[keyof typeof RouteName];
export const routes: Readonly<Route<RouteNames>[]> = [
{
name: RouteName.Parent,
path: `/${RouteName.Parent}`,
component: ParentComponent,
},
{
name: RouteName.Nested,
path: `/${RouteName.Parent}/${RouteName.Child}`,
components: {
default: ParentComponent,
nested: ChildComponent,
},
},
] as const;
export const options: RouterOptions<RouteNames> = {
routes,
} as const;
</script>
<RouterContext {options}>
<RouterView>
<!-- will render ParentComponent -->
</RouterView>
<RouterView name="nested">
<!-- will only render ChildComponent on /parent/child, and nothing on /parent -->
</RouterView>
</RouterContext>
路由器转换
闪光转换
这个 RouterView 组件可以使用 transition 道具 以激活路径转换.
它将路由组件包入带有可选内容的 div 中 in 财务报告和审定财务报表 out 过渡.
提供了默认的淡出/ 比例转换, 但您可以通过自己的 过渡。 。 。 .
注意: 默认情况下, 第一个输入的转换被忽略, 您可以通过设置 skipFirst 选项到 false。 。 。 .
<script lang="ts">
import { RouterView } from '@dvcol/svelte-simple-router/components';
import { transition } from '@dvcol/svelte-simple-router/utils';
...
</script>
<RouterView {transition} {options} />
查看过渡 API
如果你想用这个 视图过渡 API 你可以通过一个 viewTransitionName 键 transition 页:1 RouterView 组件.
若为 viewTransitionName 是一个字符串,将用作 viewTransitionName,否则将生成一个独特的 id sr-container-<router-id>-<view-id>。 。 。 .
然后,你可以使用一个或一个组合 生命周期钩,如 onChange 或者说 onStart 以触发过渡.
<script lang="ts">
import { onChange, onError, onLoaded } from '@dvcol/svelte-simple-router/router';
let resolve: () => void;
const starTransition = () => {
const { promise: transition, resolve: end } = Promise.withResolvers<void>();
resolve = end;
const { promise: wait, resolve: start } = Promise.withResolvers<void>();
document.startViewTransition(async () => {
start();
await transition;
});
return wait;
};
onChange(async () => {
if (resolve) resolve();
return starTransition();
});
onError(() => resolve?.());
onLoaded(() => resolve?.());
</script>
DOM 动作
链接动作
这个 link action 拦截在多姆元素上点击事件并触发路由器导航.
链接动作将防止默认行为并仅在满足以下条件时使用路由器: 1
- 该元素属于路由器范畴
- 事件是左键或输入键
- 此事件没有修改键
- 目标不是外部链接(对于锁定元素)
- 目标不是新建标签或窗口( 对于锁定元素)
此外:
- 动作将数据属性与通过作为参数的选项合并.
- 通过的选项优先于数据属性.
- 如果属性期望一个 JSON 对象, 它会被解析 .
- 如果提供了名称或路径参数,它们将被用于导航并忽略href.
- 名称优先于路径.
- 如果主机不是一个主机元素,则角色和标签索引属性将被设定.
- 如果解析度: 真通过, 目标路径将被解析, 任何懒惰的组件将被装入悬浮/ 聚焦
- 如果解析:视图名称被通过,则只有目标视图会被解析/加载到悬浮/焦点上.
注:动作需要路由器上下文出现在组件树上.
<script lang="ts">
import { link } from '@dvcol/svelte-simple-router/router';
</script>
<a href="/path/:param?query=value" use:link>simple link</a>
<a href='goodbye' name use:link>named link</a>
<a href='/path/:param' data-query='{"query":"value"}' use:link>link with query</a>
<a href='/path/:param' use:link="{ params: { param: 'value' } }">link with params</a>
<div href='/path/:param' use:link="{ params: { param: 'value' } }">div link</div>
<button href='/path/:param' use:link="{ params: { param: 'value' } }">button link</button>
链接附件
这个 link attachment 拦截在多姆元素上点击事件并触发路由器导航.
类似于 link action,参见 链接动作 更多信息.
<script lang="ts">
import { useLink } from '@dvcol/svelte-simple-router/attachment';
</script>
<a href="/path/:param?query=value" {@attach useLink()}>simple link</a>
<a href='goodbye' name {@attach useLink()}>named link</a>
<a href='/path/:param' data-query='{"query":"value"}' {@attach useLink()}>link with query</a>
<a href='/path/:param' {@attach useLink({ params: { param: 'value' } })}>link with params</a>
<div href='/path/:param' {@attach useLink({ params: { param: 'value' } })}>div link</div>
<button href='/path/:param' {@attach useLink({ params: { param: 'value' } })}>button link</button>
链接动作
这个 links action 拦截在多姆元素上点击事件并上行导航多姆树,直到它到达链接元素并触发路由器导航取而代之.
链接动作将承认父节点为路由器链接,如果它满足下列任何条件:
- 该元素是一个锚定元素
- 元素有一个
data-router-link属性 - 要件满足了
apply选择器函数作为参数传递
当一个节点被确认为路由器链接时,动作将表现为 link 行动(适用所有限制).
此外:
- 该动作需要有效的href或数据属性来导航.
- 一旦动作到达主机元素或
boundary元素(或选择函数),它将停止评价多姆树.
注:动作需要路由器上下文出现在组件树上. 注:与用法:link,用法:links不使链接属性正常化(作用,tabindex,href).
<script lang="ts">
import { links } from '@dvcol/svelte-simple-router/router';
</script>
<div use:links>
<div>
<a href="/path/:param?query=value">simple link</a>
</div>
<div data-router-link data-name="Hello">
<span>simple span</span>
</div>
</div>
链接附件
这个 links attachment 拦截在多姆元素上点击事件并上行导航多姆树,直到它到达链接元素并触发路由器导航取而代之.
类似于 links action,参见 链接动作 更多信息.
<script lang="ts">
import { useLinks } from '@dvcol/svelte-simple-router/attachment';
</script>
<div {@attach useLinks()}>
<div>
<a href="/path/:param?query=value">simple link</a>
</div>
<div data-router-link data-name="Hello">
<span>simple span</span>
</div>
</div>
积极行动
这个 active action 当路由匹配时,在元素中添加活动状态(类,样式或属性).
此外:
- 如果附着在锚定元素上,它会尝试与href属性相匹配.
- 如果提供了路径或名称选项,它们将优先于元素属性.
- 名字总是优先于路径.
- 当路线不匹配时,会恢复原来的风格.
注:动作需要路由器上下文出现在组件树上.
<script lang="ts">
import { active } from '@dvcol/svelte-simple-router/action';
</script>
<a href="/path" use:active>simple link</a>
<a href="/path" data-name="route-name" use:active>named link</a>
<button use:active="{ path: '/path' }">button link</button>
<div use:active="{ name: 'route-name' }">div link</div>
活动附件
这个 active attachment 当路由匹配时,在元素中添加活动状态(类,样式或属性).
类似于 active 动作,见 积极的动作 更多信息.
<script lang="ts">
import { useActive } from '@dvcol/svelte-simple-router/attachment';
</script>
<a href="/path" {@attach useActive()}>simple link</a>
<a href="/path" data-name="route-name" {@attach useActive()}>named link</a>
<button {@attach useActive({ path: '/path' })}>button link</button>
<div {@attach useActive({ name: 'route-name' })}>div link</div>
程序导航
钩子
hasRouter打开useRouter- 返回路由器实例
必须在 RouterView 或者说 RouterContext。 。 。 .
<script lang="ts">
import { useRouter } from '@dvcol/svelte-simple-router/router';
const router = useRouter();
</script>
hasView打开useView- 返回视图实例
必须在 RouterView。 。 。 .
<script lang="ts">
import { useView } from '@dvcol/svelte-simple-router/router';
const view = useView();
</script>
useRoute- 返回当前route, (中文(简体) ),location编辑routing状态
必须在 RouterView 或者说 RouterContext。 。 。 .
<script lang="ts">
import { useRoute } from '@dvcol/svelte-simple-router/router';
const { route, location, routing } = $derived(useRoute());
const reactiveRoute = $derived(route);
const reactiveLocation = $derived(location);
const reactiveRoutingState = $derived(routing);
const pathParams = $derived(location.params);
const queryParams = $derived(location.query);
</script>
useNavigate- 返回函数以启动导航逻辑.
必须在 RouterView 或者说 RouterContext。 。 。 .
<script lang="ts">
import { useNavigate } from '@dvcol/svelte-simple-router/router';
const { resolve, push, replace, back, forward, go } = useNavigate();
</script>
beforeEach- 在Mount上返回一个记录(和自动清理)在每次导航事件前触发的听众的钩子
必须在 RouterView 或者说 RouterContext。 。 。 .
<script lang="ts">
import { beforeEach } from '@dvcol/svelte-simple-router/router';
beforeEach((event) => {
console.info('before navigation', event);
});
</script>
onStart- 返回在Mount上设置的钩子,该钩子登记(和自动清理)每个导航事件开始时触发的听众
必须在 RouterView 或者说 RouterContext。 。 。 .
<script lang="ts">
import { onStart } from '@dvcol/svelte-simple-router/router';
onStart((event) => {
console.info('start of navigation', event);
});
</script>
onEnd- 在Mount上返回一个记录(和自动清理)每个导航事件结束时触发的收听器的钩子
必须在 RouterView 或者说 RouterContext。 。 。 .
<script lang="ts">
import { onEnd } from '@dvcol/svelte-simple-router/router';
onEnd((event) => {
console.info('end of navigation', event);
});
</script>
onChange- 返回在 Mount 上的钩子,该钩子在视图变化开始时会触发一个收听器(和自动清理).
必须在 RouterView。 。 。 .
<script lang="ts">
import { onChange } from '@dvcol/svelte-simple-router/router';
onChange((event) => {
console.info('start of view change', event);
});
</script>
onLoading- 返回 Mount 上的钩子,该钩子记录(和自动清理)一个在视图开始装入Async组件时触发的收听器.
必须在 RouterView。 。 。 .
<script lang="ts">
import { onLoading } from '@dvcol/svelte-simple-router/router';
onLoading((event) => {
console.info('loading view', event);
});
</script>
onLoaded- 返回一个登上Mount的钩子,该钩子登记(和自动清理)一个在视图完成加载组件时触发的收听器.
必须在 RouterView。 。 。 .
<script lang="ts">
import { onLoaded } from '@dvcol/svelte-simple-router/router';
onLoaded((event) => {
console.info('view loaded', event);
});
</script>
onError- 返回一个在Mount上的钩子,该钩子对一个在导航或视图变化中发生错误时触发的听众进行登记(和自动清理).
必须在 RouterView。 。 。 .
<script lang="ts">
import { NavigationEvent, onError, ViewChangeEvent } from '@dvcol/svelte-simple-router/router';
onError((err, event) => {
if (event instanceof NavigationEvent) {
console.error('Navigation Error', { err, event });
} else if (event instanceof ViewChangeEvent) {
console.error('View change error', { err, event });
} else {
console.error('Unknown Error', { err, event });
}
});
</script>
onViewError- 返回一个在Mount上的钩子,该钩子记录(和自动清理)一个在视图变化中发生错误时触发的听众.
必须在 RouterView。 。 。 .
<script lang="ts">
import { onViewError } from '@dvcol/svelte-simple-router/router';
onViewError((err, event) => {
console.error('View change error', { err, event });
});
</script>
onRouterError- 返回一个在Mount上的钩子,该钩子记录(和自动清理)一个在导航过程中发生错误时触发的听众.
必须在 RouterView 或者说 RouterContext。 。 。 .
<script lang="ts">
import { onRouterError } from '@dvcol/svelte-simple-router/router';
onRouterError((err, event) => {
console.error('Navigation error', { err, event });
});
</script>
路由器实例
对于更复杂的用法,您可以从上下文中抓取路由器实例并调用 push 或者说 replace 方法。 。 .
见 路由器模型 更多信息.
<script lang="ts">
import { useRouter } from '@dvcol/svelte-simple-router/router';
const router = useRouter();
const onPush = () => {
router.push({ path: '/route-path' });
};
const onReplace = () => {
router.replace({ name: 'route-name' });
};
</script>
外部组件树
如果您需要访问一个组件以外的路由器实例, 您可以即时访问一个路由器实例并将其传递给 RouterContext 或者说 RouterView 组件.
import { Router } from '@dvcol/svelte-simple-router/router';
export const router = new Router();
<script lang="ts">
import { RouterView } from '@dvcol/svelte-simple-router/components';
import { router } from './router';
</script>
<RouterView {router} />
路由器导航支持 选项数字 :
name或者说path以导航到一个命名或路径.params以通过路由参数.query以通过查询参数.state以通过历史状态对象.meta以传递任意对象到路由(将与路由元合并).title设置文档标题(将覆盖路由标题).stripQuery从 url 中删除当前查询参数.stripHash从url中删除散列(仅在路径模式中).stripTrailingHash从url中去除后继散列(仅以散列模式).
您也可以覆盖路由器的导航 选项数字 :
base设置路由器的基础路径.hash以启用散列路由.strict以便严格匹配路径(即/路径不会匹配/路径/孩子).force以强制导航,即使路线相同.caseSensitive以启用对大小写敏感的路由名称.failOnNotFound以在找不到路径时丢出错误.metaAsState以导航时的状态通过 .nameAsTitle设置路由名称的文档标题.followGuardRedirects以跟随后卫重定向.
动态路线
你可以充满活力 添加或删除 来自路由器实例的路由.
注意,虽然内行路由图是被动的,但添加或去除路由不会触发路由器状态再同步.
要强制重同步,你可以调用 sync 方法 在路由器实例.
你也可以使用这个 RouteView 以动态方式声明路径.
当组件被添加到组件树上时,路径会被添加到路由器实例中.
当组件从组件树上移除时,会从路由器实例中移除路由.
路由视频可以通过 a route 对象,并将添加到最近的路由器实例中.
此外,如果缺少组件/组件/直接,它将试图从组件的子女和片段推断出这些组件/组件/直接.
当一个名字里面 RouterView中,儿童将被添加到 name 键(通过上下文提供或发现).
RouteView支持与RouterView组件相同的出错并加载片段.
此外,被命名的儿童可以作为片段传递到该部分,并将被注入该部分 components 对象.
如果片段相同 name 作为 RouterView 孩子们会被注射到 components 对象 default 键.
说明数字 : 输入不是被动的,所以您需要卸出并重新挂载组件来触发路由更新. 如果需要频繁更新路由,建议直接使用路由器实例.
<script lang="ts">
import type { PartialRoute } from '@dvcol/svelte-simple-router/models';
import { RouterContext, RouterView, RouteView } from '@dvcol/svelte-simple-router/components';
import { toLazyComponent } from '@dvcol/svelte-simple-router/utils';
import ChildComponent from '~/components/goodbye/ChildComponent.svelte';
import ParentComponent from '~/components/hello/ParentComponent.svelte';
const LazyComponent = toLazyComponent(() => import('./LazyComponent.svelte'));
const parent: PartialRoute = {
name: 'parent',
path: '/parent',
};
const child: PartialRoute = {
name: 'child',
path: '/parent/child',
};
</script>
<RouterContext {options}>
<RouterView>
<RouteView route={parent}>
<!-- Will render the children in this 'default' RouterView -->
<ParentComponent />
<!-- Will render this snippet in the 'nested' RouterView -->
{#snippet nested()}
<ChildComponent />
{/snippet}
</RouteView>
</RouterView>
<RouterView name="nested">
<RouteView route={parent}>
<!-- Will render the children in this 'nested' RouterView, and nothing in the default when on '/parent/child' -->
<ChildComponent />
</RouteView>
<!-- Inline example -->
<RouteView route={{ path: '/other' }} children={ParentComponent} nested={LazyComponent} />
</RouterView>
</RouterContext>
警卫和听众
导航听众
这个 route 财务报告和审定财务报表 router 支持几个导航卫士和听众:
Url更改后及路由组件被制取前, 守护会触发.
如果有警卫回来 false对象 Error 或者说 throws中,导航将被中止,错误将被抛出.
如果一个守护者用 path 或者说 name 属性,如果找到任何路径,则导航将重定向到指定的路径 followGuardRedirects 已启用.
这个 router (单位:千美元)动态 或通过 选项和 RouterView 还支持几个事件听众:
onStart- 当导航被触发,但在线路被解决之前执行(启动并重定向起火).onEnd- 当导航被触发并解决路线时执行(在导航成功和失败时开火,但在取消/调整时不开火).onError- 当导航被触发但发生错误时执行.
说明: onError 听者传到一个 RouterView 将同时聆听路由器和观看事件。 如果你想只听路由器事件,你可以把听众传到 router 选项或实例.
装入收听器
这个 RouterView 支持数个在导航结束并开始改变视图后触发的视图更改收听器.
onChange- 当一个视图开始改变时执行.onLoading- 当视图开始装入同步组件时执行.onLoaded- 当视图完成装入组件时执行 .onError- 当视图更改时发生错误时执行.
说明: onError 听者传到一个 RouterView 将同时聆听路由器和观看事件。 如果你想只听观看事件,你可以把听众传给 view 直接出现.
懒惰路线
这个 Route 对象支持懒惰地加载路由组件.
当 component 属性是一个懒惰的导入,路由器会在渲染之前解决匹配的组件.
在解决该部分的同时 loading 如果有,则组件将被调出 loading 片段,如果有,或什么都没有.
同样,如果在组件解析过程中发生错误,则 error 如果有,则组件将被调出 error 片段,如果有,或什么都没有.
路由器将尝试通过检查其名称(用于检测组件箭头函数)和构建器名称(用于检测async箭头函数)来推断一个组件是否属于懒惰的导入,但是对于更复杂的情况,您可以使用 toLazyComponent 包装.
嵌入式懒惰组件需要使用包装或手动命名函数 component。 。 。 .
<script lang="ts">
import type { Route } from '@dvcol/svelte-simple-router/models';
import { RouterView } from '@dvcol/svelte-simple-router/components';
import { toLazyComponent } from '@dvcol/svelte-simple-router/utils';
const routes: Readonly<Route[]> = [
{
name: 'lazy',
path: '/lazy',
component: () => import('./LazyComponent.svelte'),
loading: () => import('./LoadingComponent.svelte'),
error: () => import('./ErrorComponent.svelte'),
},
{
name: 'lazy-snippet',
path: '/lazy-snippet',
component: () => import('./LazyComponent.svelte'),
},
{
name: 'lazy-nested',
path: '/lazy-nested',
components: {
default: async () => import('./LazyComponent.svelte'),
nested: toLazyComponent(() => import('./NestedComponent.svelte')),
},
},
] as const;
</script>
<RouterView {routes}>
{#snippet loading()}
<h1>Default Loading...</h1>
{/snippet}
{#snippet error(err)}
<h1>Default Error: {err}</h1>
{/snippet}
</RouterView>
注意:加载指示器只有在路线解决后才会触发,而不是在初始导航时触发.
如果您要在初始导航中显示加载指标, 您可以使用 routing 换成片段.
这意味着导航将是一个三步走的过程:
- 这个
routing片段将被打开 . - 接下来
loading将提供组件. - 然后,路由组件或出错组件将被提供.
路线
- 路由路径支持参数. 参数由一个冒号来定义,然后是参数名称.
/path/:param
- 参数可以通过添加一个问号来选择.
/path/:param:?
参数还可以通过添加具有类型限制 :{string} 或者说 :{number} 在参数名称之前.
路由器只有在参数符合类型约束时才会匹配路由.
/path/:{string}:param:?
将匹配 /path/param 但没有 /path/1。 。 。 .
/path/:{number}:param
将匹配 /path/1 但没有 /path/param。 。 。 .
- 路由支持通配符. 这个
*字符将匹配任何路径段.
/path/*
将匹配 /path/param 财务报告和审定财务报表 /path/param/param。 。 。 .
/path/*/:{number}:param/*
将匹配 /path/any/12 财务报告和审定财务报表 /path/other/12/path/end。 。 。 .
- 路线可以有一个 重定向 属性以重定向到另一个路径(通过路径或名称).
- 路线可以 组件、装入或出错 属性以分别制取路由组件、加载组件或出错组件(见 懒散的路线) (中文(简体) ).
- 路线可以有一个 元数据a 属性以存储路径附加的额外信息.
- 路线可以有一个 标题 属性,用于在路径活动时设置文档标题(如果
nameAsTitle已启用). - 路线可以 默认查询或参数 用于导航路线.
- 路线可以 守护属性 在解决路线之前运行(参见 警卫和听众) (中文(简体) ).
- 路线可以自己提供 匹配器 对象以解析活动路由.
- 路线可以 儿童 由路由器实例解析的路径.
路由器
除默认导航选项外(参见 程序导航,路由器实例支持多个 选项数字 :
history设置路由器将使用的历史实例(默认为全局. history).location设置路由器将使用的位置实例(默认为 global. location).listen倾听popstate或者说navigation事件和触发同步.priority当多个路径匹配当前路径时设置优先级.caseSensitive以启用对大小写敏感的路由名称.- 和各种 警卫和听众。 。 。 .
作者
- 吉图布: @dvcol (英语)
显示您的支持
如果这个计划能帮助你的话,就给你一个!
许可证
这个项目是 麻省理工学院 有执照.
这个 README 是用 QQ 生成的 读取- md- 生成器