热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

使用Piral创建微前端

三年多一点前,我在LogRocket的一篇博文中揭示了通过某种发现机制形成微前端的想法,驯服前端单体。发现机制(称为提要服务)

三年多一点前,我在 LogRocket 的一篇博文中揭示了通过某种发现机制形成微前端的想法,驯服前端单体。发现机制(称为提要服务)是我参与的解决方案的核心。

那个解决方案就是 Piral,我们在半年后的 O'Reilly 的软件架构会议上正式披露了它。

今天,Piral 是微前端领域最常用和知名的解决方案之一。仅此一项就可以证明另一篇博客文章的合理性——但是,我们也有越来越多的微前端流行,以及对可扩展性的总体需求。

事不宜迟,让我们看看微前端是什么,为什么松散耦合对它们如此重要,以及 Piral 如何解决这个(和其他问题)以实现出色的可扩展性。我们将在以下部分中介绍这一点:


  • 什么是微前端?

  • 其他微前端框架的可扩展性问题

  • 皮拉尔有何不同?

  • 使用 Piral 开发您的应用程序外壳

    • 更改您的模板

    • 更改提要以显示不同的堆

  • 创建应用模拟器

  • 推出微前端

    • 换桩

    • 构建和发布我们的pilet

  • 集成 SWR 以执行 HTTP 请求


什么是微前端?

近年来,微前端越来越流行。原因之一是对大型 Web 应用程序的需求增加。如今,AWS 和 Azure 门户等功能强大的门户以及 Netflix 或 DAZN 等丰富的用户体验并非例外,而是常态。如何构建如此庞大的应用程序?如何缩放它们?

这些问题的一个答案可以是使用微前端。微前端是业务子域的技术表示。基本思想是单独开发一部分 UI。这件作品不需要在屏幕上的一个区域呈现;它实际上可以由多个片段组成,即一个菜单项和该菜单项链接到的页面。唯一的限制是该部分应与业务子域相关。

微前端由不同的组件组成,但这些不是经典的 UI 组件,如下拉菜单或富文本字段框。相反,这些组件是特定于域的组件,并包含一些业务逻辑,例如需要发出哪些 API 请求。

即使是像菜单项这样简单的东西在这个上下文中也是一个域组件,因为它已经知道到页面的链接来自同一个业务域。一旦组件具有一些领域逻辑,它就是一个领域组件——因此可以成为微前端的一部分。

为了实现微前端,存在一整套方法和实践。它们可以在构建时、服务器端和客户端组合在一起。

在本文中,我们将研究客户端的组合,但可以为服务器编写相同的故事。那么微前端实际上是如何扩展的呢?


其他微前端框架的可扩展性问题

许多微前端框架在现实环境中面临可扩展性问题。看其他文章,乍一看技术还不错;例如,如果您阅读使用 single-spa 创建微前端应用程序或使用 Podium 构建 Svelte 微前端,它们很好地介绍了技术和用例。另一个例子可以在使用 Fronts 构建渐进式微前端中看到。

问题是这些框架通常会尝试在视觉上分割 UI。但是,实际上,您永远不会将前端拆分为“导航”、“页眉”、“内容”和“页脚”等部分。这是为什么?

如上一节所述,一个真正的应用程序由来自不同子域的不同部分组成,这些子域组合在一起形成完整的应用程序域。虽然这些子域可以在纸上很好地完全分开,但它们通常在相同的布局元素中出现在最终用户面前。

想想像网上商店这样的东西。如果您有一个用于产品详细信息的子域和另一个处理先前订单的子域,那么您不希望在作为用户的订单历史记录中只看到无意义的产品 ID。相反,您希望订单历史记录中至少显示产品名称和一些详细信息。因此,这些子域在视觉上向最终用户交错。



超过 20 万开发人员使用 LogRocket 来创造更好的数字体验了解更多 →



同样,几乎每个子域都可以为共享的 UI 布局元素做出贡献,例如导航、页眉或页脚。因此,拥有专门处理导航区域的微前端在实践中没有多大意义,因为这个微前端会收到来自其他团队的大量请求——并成为瓶颈。这样做会产生一个隐藏的单体。

现在,有人可能会争辩说,在微前端中没有导航会导致对更改的相同需求,但这次是在应用程序外壳所有者上。这会更糟。

那么解决方案是什么呢?显然,我们需要将这些东西解耦。所以不要使用类似的东西:

import MyMenuItem1 from 'my-micro-frontend1';
import MyMenuItem1 from 'my-micro-frontend2';
import MyMenuItemN from 'my-micro-frontendN';

const MyMenu = () => (<>
);

我们需要注册每个必要的部分,例如来自微前端本身的导航项。这样,我们最终可以得到如下结构:

const MyMenu = () => {const items = useRegisteredMenuItems();
​return (<>{items.map(({ id, Component }) => )});
};

为了避免需要知道微前端的名称和位置,需要一种发现。这只是一个可以从已知位置(例如后端服务)检索的 JSON 文档。

现在我们知道我们需要扩展什么,是时候开始实施了。幸运的是,有一个框架可以让我们在这方面领先一步:Piral。


皮拉尔有何不同?

Piral是一个使用微前端创建超可扩展 Web 应用程序的框架。在许多方面,它具有以下特点:


  • 集成的微前端发现机制

  • 广泛的开发人员体验,可满足您所需的一切

  • 具有跨框架支持的松散耦合组件

这样,各个团队可以专注于他们特定的领域问题,而不是需要对齐和联合发布。Piral 应用程序由三个部分组成:


  1. 应用程序外壳。这可以像包含对微前端发现服务(称为提要)的引用的 HTML 文件一样简单

  2. 微前端的提要(也可以是静态 JSON,但通常你会想要更强大的东西)

  3. 不同的模块(微前端),称为桩

整个设置可以绘制如下:

模块开发人员可以使用命令行实用程序piral-cli来搭建(即,使用一些模板创建)和发布新的 pilet,调试和更新现有模块,或者执行一些 linting 和验证。真正的用户不会将解决方案视为不同的部分——他们实际上是在一个应用程序中将应用程序外壳与堆砌一起使用。从技术上讲,这些桩是从饲料服务中获取的。

很多时候,微前端的开发体验并不理想。要么需要检查并开始多件事情,要么整个过程归结为一个开发-提交-尝试-失败-重启周期。Piral 在这里有所不同——它首先尝试离线。微前端直接在称为模拟器的应用程序外壳的特殊版本中开发。

模拟器只是一个 npm 包,可以安装在任何 npm 项目中。当piral-cli用于调试时,它实际上会将模拟器中的内容作为页面来显示。Pilet 将通过内部 API 提供服务,而不是使用远程提要服务或类似的东西。

尽管如此,在开发过程中加载现有的微前端可能仍然有意义。在这种情况下,仍然可以集成来自现有饲料的桩。




来自 LogRocket 的更多精彩文章:


  • 不要错过来自 LogRocket 的精选时事通讯The Replay

  • 了解LogRocket 的 Galileo 如何消除噪音以主动解决应用程序中的问题

  • 使用 React 的 useEffect优化应用程序的性能

  • 在多个 Node 版本之间切换

  • 了解如何使用 AnimXYZ 为您的 React 应用程序制作动画

  • 探索 Tauri,一个用于构建二进制文件的新框架

  • 比较NestJS 与 Express.js



让我们看看这一切在实践中是如何运作的。


使用 Piral 开发您的应用程序外壳

有多种方法可以使用 Piral 创建应用程序外壳:


  • 迁移现有项目

  • 手动将包添加到新项目

  • 使用piral-cli创建新项目

在这篇文章中,我们将做后者。

在命令行上,我们运行:

npm init piral-instance --bundler esbuild --target my-app-shell --defaults

my-app-shell这将在目录中创建一个新的应用程序外壳。该项目将使用 npm、TypeScript 和esbuild工具作为我们的捆绑器(尽管我们实际上可以选择任何类型的捆绑器,例如 webpack、Parcel 或 Vite 等)。在许多情况下,选择esbuild应该就足够了,并且可以提供最快的安装时间。

现在,我们可以开始调试项目了。进入新目录(例如,cd my-app-shell)并开始调试会话:

npm start

Going tohttp://localhost:1234应该向您展示标准模板:


更改模板

我们现在可以在所有可能的方面更改模板。例如,我们可以将提供的布局更改为没有任何固定的内容图块;只需编辑src/layout.tsx文件并删除defaultTiles和defaultMenuItems. 确保不仅要删除它们的初始化,还要删除对它们的引用。

要获得更详细的信息,我们可以更改DashboardContainerfrom:

DashboardContainer: ({ children }) => (

Hello, world!

Welcome to your new microfrontend app shell, built with:

{defaultTiles}{children}

),

至:

DashboardContainer: ({ children }) => (

Hello, world!

Welcome to your new microfrontend app shell, built with:

{children}

),

这里可以看到的所有组件都有不同的用途。虽然其中许多来自可选插件,但有些——例如ErrorInfoor Layout——已经通过驱动 Piral 的核心库定义。

在上面的例子中,我们为 Piral 的仪表板插件定义了仪表板容器。仪表板插件为我们提供了一个仪表板,默认情况下,它位于/我们页面的主页 ( )。我们可以在这里更改所有内容,包括它的外观和位置(当然,如果我们想要一个仪表板)。

仪表板非常适合门户应用程序,因为它们在一个屏幕上收集大量信息。对于微前端,仪表板也很好——尤其是作为展示。在这个页面上,可能大多数(如果不是全部)微前端想要展示一些东西。

从仪表板容器中删除默认磁贴后,Web 应用程序现在应该看起来更空:

我们的 Web 应用程序空虚的主要原因是我们没有集成任何在某处呈现组件的微前端。此时,脚手架机制将我们的新应用程序外壳连接到 Piral 本身拥有的特殊提要:空提要。

空提要是一种提要,顾名思义,它将始终保持为空。我们可以更改应用程序外壳的代码以转到其他提要。


更改提要以显示不同的堆

为此,您需要打开src/index.tsx文件。在那里,您将看到带有要使用的提要的 URL 的变量:

const feedUrl = &#39;https://feed.piral.cloud/api/v1/pilet/sample&#39;;

转到在另一个应用程序 shell中使用的示例提要,我们实际上可以看到如果由微前端正确填充 shell 的外观。仪表板现在应该如下所示:

我们已经可以在我们的新应用程序外壳中显示来自另一个提要的堆,这一事实实际上非常酷。这预先表明桩是真正独立的,并且与它们的外壳没有很强的关系。

但是,请记住,这种平滑的集成可能并不总是可行的。Pilets 总是可以以一种相当容易集成到其他 Piral 实例中的方式开发。同样,也可以以排除不同应用程序外壳的方式开发 pilet。


创建应用模拟器

在我们为这个应用程序外壳开发一些微前端之前,我们需要创建它的模拟器。停止调试过程并运行以下命令:

npm run build

这将piral build在当前项目上运行。结果是在 中有两个子目录dist:


  1. dist/release

  2. dist/emulator

虽然前者可用于在某处实际部署我们的 Web 应用程序,但后者包含一个.tgz可以上传到注册表的文件,如下所示:

npm publish dist/emulator/my-app-shell-1.0.0.tgz

您可能需要 npm 凭据来发布包,但即使您已经登录到 npm,您也可能不想发布它,而是将其保密,或者在不同的注册表上发布。

要使用自定义注册表测试发布过程,您可以使用Verdaccio。在一个新的 shell 中,启动:

npx verdaccio

这将安装并运行 Verdaccio 的本地版本。您应该会在屏幕上看到类似以下内容:

warn --- http address - http://localhost:4873/ - verdaccio/5.13.1

转到此地址并查看说明。它们应该看起来像:

运行登录命令 ( ) 并填写数据。对于and ,您可以只使用. 什么都会被拿走;可以很简单。npm adduser --registry http://localhost:4873/UsernamePasswordtestEmail``foo@bar.com

现在可以通过以下方式发布到自定义注册表:

npm publish dist/emulator/my-app-shell-1.0.0.tgz --registry http://localhost:4873/

完成后,我们可以为这个 shell 创建微前端!


推出微前端

正如我们对 app shell 所做的那样,我们可以使用piral-cli来搭建一个项目。这个命令现在使用pilet而不是piral-instance. 我们跑:

npm init pilet --source my-app-shell --registry http://localhost:4873/ --bundler esbuild --target my-pilet --defaults

这将创建一个名为的新目录my-pilet,其中包含新微前端的代码。工具设置为esbuild(像以前一样,我们使用 esbuild,因为它安装得非常快,但你也可以选择不同的东西,比如 webpack)。

上面的重要部分是指定--source,它表示用于开发的模拟器。现在一切就绪,我们可以cd my-pilet运行:

npm start

和以前一样,开发服务器托管在http://localhost:1234. 转到那里会产生如下所示的页面:

几乎就像我们使用了empty提要一样空。但是,在这种情况下,新 pilet 的模板已经注册了一个 tile 和一个菜单项。让我们看看如何改变这一点。


换桩

打开src/index.tsx文件并查看代码:

import * as React from &#39;react&#39;;
import type { PiletApi } from &#39;my-app-shell&#39;;export function setup(api: PiletApi) {api.showNotification(&#39;Hello from Piral!&#39;, {autoClose: 2000,});api.registerMenu(() =>Documentation);api.registerTile(() =>

Welcome to Piral!
, {initialColumns: 2,initialRows: 1,});
}

简单来说,pilet 就是一个 Javascript 库;重要的部分是这个库导出的内容。

Pilet 导出一个setup函数(准确地说,还可以导出一个函数teardown)。该函数在微前端连接后使用,并接收单个参数 ,api该参数由应用程序外壳定义和创建。

app shell 的 API(通常称为 Pilet API)是 pilet 可以在应用程序中注册其部件的地方。让我们添加一个页面并稍微更改一下磁贴。

我们从瓷砖开始。我们可以给它一些类,比如teaser实际上有一点背景。此外,我们想为仪表板容器添加一些元数据。我们可以使用initialColumns和initialRows属性来传达所需的大小。

app.registerTile(() =>

Hello LogRocket!
, {initialColumns: 2,initialRows: 2,
})

保存后,该图块将具有一些不同的外观。让我们删除showNotification不再需要的并引入一个新页面:

api.registerPage(&#39;/foo&#39;, () =>

This is my page


);

要链接到此页面,我们可以更改已注册的菜单项。要执行 SPA 导航,我们可以使用熟悉的 React 工具react-router-dom:

api.registerMenu(() =>Foo
);

伟大的!然而,像页面这样的片段并不总是必需的,并且应该只在应该呈现它们时加载。这种延迟加载可以通过将代码放在一个专用文件中来实现,即,Page.tsx并将注册更改为:

const Page = React.lazy(() => import(&#39;./Page&#39;));
api.registerPage(&#39;/foo&#39;, Page);

的内容Page.tsx可以很简单:

import * as React from &#39;react&#39;;export default () => {return (<>Page Title

Lorem ipsum dolor sit ...

Lorem ipsum dolor sit ...

);
};

注册页面后,您现在可以单击导航栏中的“Foo”并查看页面:


构建和发布我们的pilet

现在我们的pilet已经写好了,我们可以实际构建和发布它了。在这一点上,我们还没有创建自己的提要或在任何地方发布应用程序外壳,所以最后一部分实际上有点理论。

要构建 pilet,您可以运行:

npm run build

创建后,您可以使用npx pilet pack. 这将与运行非常相似npm pack。结果是另一个.tgz文件——这次不是模拟器,而是实际的 pilet。小型电视怎么选?全部低于 43 英寸索尼、三星、LG等品牌的小型智能电视推荐tarball 是可以上传到专用服务(例如提要服务)的内容,提供应用程序外壳使用的提要。

可以在piral.cloud上找到非商业和商业产品的示例。

在结束本教程之前,让我们看看如何集成一个常用功能——在本例中,使用 SWR 执行 HTTP 请求。


集成 SWR 以执行 HTTP 请求

有多种方法可以集成常见的问题,例如 SWR。将 swr(或位于其之上的其他库)添加到应用程序 shell 并在那里进行配置后,您有三个选项:


  1. 将其公开为共享库

  2. 通过 Pilet API 公开它

  3. 不暴露它(尽管在我们的 SWR 示例中,这不起作用,因为我们需要对 Hook 的引用)

  4. 将使用的决定swr留给 pilet:它们可以swr作为分布式依赖项共享(即,仅在没有其他 pilet 加载它时才加载它)

集成 SWT 最简单和最可靠的方法是使用第一个选项。为此,我们回到应用程序外壳。

在应用程序外壳的目录中运行:

npm install swr

现在,让我们修改package.json. 保留几乎所有内容,但修改该部分的externals数组,pilets如下所示:

{"name": "my-app-shell","version": "1.1.0",// ..."pilets": {"externals": ["swr"],// ...},// ...
}

请注意,我还更改了版本号。WhatsApp推出新功能,可发送带有标题的文档由于我们将对模拟器进行更新,因此我们需要一个新版本。这将指示 Piralswr与所有 pilet 共享依赖项。

为了实际测试这一点,让我们再次编写npm run build和发布。

npm run build
npm publish dist/emulator/my-app-shell-1.1.0.tgz --registry http://localhost:4873/

有了更新的 shell,让我们进入 pilet 的目录并升级应用程序 shell:

npx pilet upgrade

桩的package.json文件应该已经改变。它现在应该包含对my-app-shellin version1.1.0而不是1.0.0. 此外,您应该会看到和中swr列出的内容。devDependencies``peerDependencies

让我们修改要使用的页面swr:

import * as React from &#39;react&#39;;
import LogRocket from &#39;swr&#39;;

// note: fetcher could have also been globally configured in the app shell
// however, in general the pilet&#39;s don&#39;t know about this and so they may want to
// reconfigure or redefine it like here
const fetcher = (resource, init) => fetch(resource, init).then(res => res.json());

export default () => {const { data, error } = useSWR(&#39;https://jsonplaceholder.typicode.com/users/1&#39;, fetcher);
​if (error) {return

failed to load
;}
​if (!data) {return
loading...
;}
​return (<>Hello {data.name}!);
};

现在我们完成了!不仅在我们的应用程序中成功设置了 SWR,我们还可以在所有微前端中使用它。这既节省了加载 SWR 的带宽,也节省了 SWR 的内部缓存,为所有微前端带来了不错的性能优势。


概括

在这篇文章中,您已经看到了 Piral 入门是多么容易。确保Windows11电脑系统安全设置,防止危险或恶意软件破坏您的系统Piral 为您提供了将 Web 应用程序分发到不同存储库的选项,甚至跨不同的团队。

我们在这篇文章中只探讨了非常基本的设置,但您可以使用 Piral 做更多的事情。探索 Piral 的最佳方式是阅读官方文档。

Piral 比大多数其他解决方案具有更好的扩展性的原因是 Piral 鼓励松散耦合。这样,您将很难将两个东西融合在一起,这有助于您避免功能重叠和隐藏的单体。

无论您打算做什么,请确保已经考虑了要共享哪些依赖项以及要保留哪些依赖项。我们已经看到了一个示例,其中提供swr作为共享依赖项实际上是在几秒钟内设置的。快乐编码


推荐阅读
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • GetWindowLong函数
    今天在看一个代码里头写了GetWindowLong(hwnd,0),我当时就有点费解,靠,上网搜索函数原型说明,死活找不到第 ... [详细]
  • React基础篇一 - JSX语法扩展与使用
    本文介绍了React基础篇一中的JSX语法扩展与使用。JSX是一种JavaScript的语法扩展,用于描述React中的用户界面。文章详细介绍了在JSX中使用表达式的方法,并给出了一个示例代码。最后,提到了JSX在编译后会被转化为普通的JavaScript对象。 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • 这是原文链接:sendingformdata许多情况下,我们使用表单发送数据到服务器。服务器处理数据并返回响应给用户。这看起来很简单,但是 ... [详细]
  • 本文介绍了Java工具类库Hutool,该工具包封装了对文件、流、加密解密、转码、正则、线程、XML等JDK方法的封装,并提供了各种Util工具类。同时,还介绍了Hutool的组件,包括动态代理、布隆过滤、缓存、定时任务等功能。该工具包可以简化Java代码,提高开发效率。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 知识图谱——机器大脑中的知识库
    本文介绍了知识图谱在机器大脑中的应用,以及搜索引擎在知识图谱方面的发展。以谷歌知识图谱为例,说明了知识图谱的智能化特点。通过搜索引擎用户可以获取更加智能化的答案,如搜索关键词"Marie Curie",会得到居里夫人的详细信息以及与之相关的历史人物。知识图谱的出现引起了搜索引擎行业的变革,不仅美国的微软必应,中国的百度、搜狗等搜索引擎公司也纷纷推出了自己的知识图谱。 ... [详细]
  • 本文介绍了Linux系统中正则表达式的基础知识,包括正则表达式的简介、字符分类、普通字符和元字符的区别,以及在学习过程中需要注意的事项。同时提醒读者要注意正则表达式与通配符的区别,并给出了使用正则表达式时的一些建议。本文适合初学者了解Linux系统中的正则表达式,并提供了学习的参考资料。 ... [详细]
  • 本文介绍了在Windows环境下如何配置php+apache环境,包括下载php7和apache2.4、安装vc2015运行时环境、启动php7和apache2.4等步骤。希望对需要搭建php7环境的读者有一定的参考价值。摘要长度为169字。 ... [详细]
  • 本文介绍了Python爬虫技术基础篇面向对象高级编程(中)中的多重继承概念。通过继承,子类可以扩展父类的功能。文章以动物类层次的设计为例,讨论了按照不同分类方式设计类层次的复杂性和多重继承的优势。最后给出了哺乳动物和鸟类的设计示例,以及能跑、能飞、宠物类和非宠物类的增加对类数量的影响。 ... [详细]
  • 本文介绍了如何使用Express App提供静态文件,同时提到了一些不需要使用的文件,如package.json和/.ssh/known_hosts,并解释了为什么app.get('*')无法捕获所有请求以及为什么app.use(express.static(__dirname))可能会提供不需要的文件。 ... [详细]
  • React项目中运用React技巧解决实际问题的总结
    本文总结了在React项目中如何运用React技巧解决一些实际问题,包括取消请求和页面卸载的关联,利用useEffect和AbortController等技术实现请求的取消。文章中的代码是简化后的例子,但思想是相通的。 ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
  • 从零基础到精通的前台学习路线
    随着互联网的发展,前台开发工程师成为市场上非常抢手的人才。本文介绍了从零基础到精通前台开发的学习路线,包括学习HTML、CSS、JavaScript等基础知识和常用工具的使用。通过循序渐进的学习,可以掌握前台开发的基本技能,并有能力找到一份月薪8000以上的工作。 ... [详细]
author-avatar
blue秋夜听雨321
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有