当前位置:   article > 正文

前端Router原理&简单实现_前端面试题,route原理 怎么实现的

前端面试题,route原理 怎么实现的

本文源自于面试题:如果让你设计前端Router,你会怎么去做?

接下去将从常见的前端路由模式,以及 vue 和 react 两个框架的router开始,实现简单的router。

路由模式

Hash模式

Hash模式是 html5 以前常用的路由模式,具有以下特点:

  • 在实际的URL路径前,使用哈希字符(#)进行分割。

  • hash路由向服务器发送请求,请求的是#以前的URL,因此不需要在服务器层面做任何处理。

  • 使用的是监听hashChange的方法来监听hash的变化。

  • 浏览器兼容性好。

History模式

相比较Hash模式,History具有以下特点:

  • 没有烦人的'#'号,SEO友好。

  • 在SPA应用中,需要服务器端的配置(Nginx try_files),否则会发生404错误。

  • 使用的是html5 新增的popstate事件去监听pathname的变化。

  • 向下兼容到IE10。

react在此基础上创建了Browser Router

Memory模式

基于内存的路由,Vue应用场景主要是用于处理服务器端渲染,为SSR提供路由历史。可以当做测试用例去看待

其他

还有native router、static router。

Vue Router

vue-router 原理

插件的注入

  1. import { createApp } from 'vue';
  2. import App from './App.vue';
  3. import router from './router';
  4. createApp(App).use(router).mount('#app');

可以看到 vue-router 是通过app.use()来使用的,这是 vue 用来安装一个插件的方法,那么什么是插件?

插件是一个拥有install()方法的对象,也可以是一个安装函数本身。

所以 vue-router 的重点就是 install 方法做了什么。

首先使用app.component方法注册两个组件,RouterLinkRouterView。它们的使用在这里就不多赘述。

  1. app.component('RouterLink', RouterLink);
  2. app.component('RouterView', RouterView);

然后在全局注册了$router实例,并且this指向我们当前的router,所以我们可以通过this.$router访问到这个实例。

  1. const router = this;
  2. app.config.globalProperties.$router = router;

接着对全局注册的$route进行了监听,通过get获取到我们当前的route

  1. Object.defineProperty(app.config.globalProperties, '$route', {
  2. enumerable: true,
  3. get: () => vue.unref(currentRoute),
  4. });

再往下是针对 vue3 多实例的一个处理,判断浏览器环境,并且初始化导航的情况下去执行后面的内容,目的是避免多实例对router创建造成的影响。

  1. if (isBrowser &&
  2. !started &&
  3. currentRoute.value === START_LOCATION_NORMALIZED) {
  4. started = true;
  5. push(routerHistory.location).catch(err => {
  6. warn('Unexpected error when starting the router:', err);
  7. });
  8. }

接下去是一些添加响应式的操作,其实就是对于路由上下文进行的监听。

  1. const reactiveRoute = {};
  2. for (const key in START_LOCATION_NORMALIZED) {
  3. Object.defineProperty(reactiveRoute, key, {
  4. get: () => currentRoute.value[key],
  5. enumerable: true,
  6. });
  7. }

其中,START_LOCATION_NORMALIZED定义如下:

  1. const START_LOCATION_NORMALIZED = {
  2. path: '/',
  3. name: undefined,
  4. params: {},
  5. query: {},
  6. hash: '',
  7. fullPath: '/',
  8. matched: [],
  9. meta: {},
  10. redirectedFrom: undefined,
  11. };

再往后,使用了app.provide方法,将上面监听、创建的变量挂载到app实例上,以便我们后面去访问。

这里其实也是针对 vue3 的兼容处理,因为 composition API 中无法获取到this

  1. app.provide(routerKey, router);
  2. app.provide(routeLocationKey, vue.shallowReactive(reactiveRoute));
  3. app.provide(routerViewLocationKey, currentRoute);

最后就是app.unmount触发时的处理,这里也做了多实例的兼容。

这里用了installedApps,它是一个Set数据结构,当对应的app实例触发卸载生命周期时删除集合中的app

当没有app时,将我们的状态变为初始值。

  1. const unmountApp = app.unmount;
  2. installedApps.add(app);
  3. app.unmount = function () {
  4. installedApps.delete(app);
  5. if (installedApps.size < 1) {
  6. pendingLocation = START_LOCATION_NORMALIZED;
  7. removeHistoryListener && removeHistoryListener();
  8. removeHistoryListener = null;
  9. currentRoute.value = START_LOCATION_NORMALIZED;
  10. started = false;
  11. ready = false;
  12. }
  13. unmountApp();
  14. };

路由的实现

主要就是createWebHashHistorycreateMemoryHistorycreateWebHistory三个API。

在路由模式里已经说了实现思路,即如何监听URL的变化,进行对应的更新。

  • Hash

主要就是在路径前拼接'#'

  1. function createWebHashHistory(base) {
  2. base = location.host ? base || location.pathname + location.search : '';
  3. if (!base.includes('#'))
  4. base += '#';
  5. if (!base.endsWith('#/') && !base.endsWith('#')) {
  6. warn(`A hash base must end with a "#":\n"${base}" should be "${base.replace(/#.*$/, '#')}".`);
  7. }
  8. return createWebHistory(base);
  9. }
  • History

  1. function createWebHistory(base) {
  2. base = normalizeBase(base);
  3. const historyNavigation = useHistoryStateNavigation(base);
  4. const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location, historyNavigation.replace);
  5. function go(delta, triggerListeners = true) {
  6. if (!triggerListeners)
  7. historyListeners.pauseListeners();
  8. history.go(delta);
  9. }
  10. const routerHistory = assign({
  11. location: '',
  12. base,
  13. go,
  14. createHref: createHref.bind(null, base),
  15. }, historyNavigation, historyListeners);
  16. Object.defineProperty(routerHistory, 'location', {
  17. enumerable: true,
  18. get: () => historyNavigation.location.value,
  19. });
  20. Object.defineProperty(routerHistory, 'state', {
  21. enumerable: true,
  22. get: () => historyNavigation.state.value,
  23. });
  24. return routerHistory;
  25. }

手写实现

根据使用的方法,我们需要创建一个类,其中包含moderoutes属性。

  1. class VueRouter {
  2. constructor(options) {
  3. this.mode = options.mode || 'hash'; // 默认hash模式
  4. this.routes = options.routes || [];
  5. this.init()
  6. }
  7. }

init方法中,我们要进行路由初始化,对于Hash模式,我们监听到load事件后,就要获取/之后的路径

  1. init() {
  2. if (this.mode === 'hash') {
  3. // 针对没有hash时,设置为/#/
  4. location?.hash ? '' : (location.hash = '/')
  5. window.addEventListener('load', () => {
  6. location.hash.slice(1)
  7. });
  8. }
  9. }

接着我们要创建一个类来维护我们的路由上下文,并在Router类中创建history实例,将获取到的路径加入到上下文中。

  1. class HistoryRoute {
  2. constructor() {
  3. this.current = null;
  4. }
  5. }

所以针对hash路由的初始化如下,包括对于hashChange的监听:

  1. if (this.mode === 'hash') {
  2. // 针对没有hash时,设置为/#/
  3. location?.hash ? '' : (location.hash = '/')
  4. window.addEventListener('load', () => {
  5. this.history.current = location.hash.slice(1)
  6. });
  7. window.addEventListener('hashChange', () => {
  8. this.history.current = location.hash.slice(1)
  9. });
  10. }

照上面的思路,针对history路由的初始化:

  1. else {
  2. location.pathname ? '' : (location.pathname = '/')
  3. window.addEventListener('load', () => {
  4. this.history.current = location.pathname
  5. });
  6. window.addEventListener('popState', () => {
  7. this.history.current = location.pathname
  8. });
  9. }

我们还要将pathcomponent进行关联,以键值对的方式存储,在constructor中,创建Map

  1. this.routesMap = this.createMap(this.routes);
  2. createMap(routes) {
  3. return routes.reduce((pre, cur) => {
  4. pre[cur.path] = cur.component;
  5. return pre;
  6. }, {})
  7. // 返回的形式如下
  8. // {
  9. // ['/home']: Home,
  10. // ['/about']: About
  11. // }
  12. }

接下去我们要提供install方法,并且按照源码里的思路去挂载我们的实例,这里用mixin来实现。

我们在beforeCreate声明周期判断是否挂载到根实例上。

  1. install(v) {
  2. Vue = v;
  3. Vue.mixins({
  4. beforeCreate() {
  5. if (this.$options && this.$options.router) {
  6. // 已经挂载到根实例
  7. this._root = this;
  8. this._router = this.$options.router
  9. } else {
  10. // 子组件
  11. this._root = this.$parent && this.parent._root
  12. }
  13. }
  14. })
  15. }

然后对$router$route进行创建和监听

  1. Object.defineProperty(this, '$router', {
  2. get() {
  3. return this._root._router;
  4. }
  5. })
  6. Object.defineProperty(this, '$route', {
  7. get() {
  8. return this._root._router.history.current;
  9. }
  10. })

最后,要创建两个RouterViewRouterLink两个组件,,通过render(h)的方式创建,其中,RouterLink最终要渲染成a标签,将to属性转变为href属性。

  1. Vue.component('router-link', {
  2. props: {
  3. to: String,
  4. },
  5. render(h) {
  6. let mode = this._self._root._router.mode;
  7. let to = mode === 'hash' ? '#' + this.to : this.to;
  8. return h('a', { attrs: { href: to } }, this.$slots.default);
  9. },
  10. });
  11. Vue.component('router-view', {
  12. render(h) {
  13. let current = this._self._root._router.history.current;
  14. let routeMap = this._self._root._router.routesMap;
  15. return h(routeMap[current]);
  16. },
  17. });

完整代码如下:

  1. let Vue = null; // 通过install注入vue实例
  2. class HistoryRoute {
  3. constructor() {
  4. this.current = null; // 上下文状态维护
  5. }
  6. }
  7. class VueRouter {
  8. constructor(options) {
  9. this.mode = options.mode || 'hash'
  10. this.routes = options.routes || []
  11. this.routesMap = this.createMap(this.routes); // 维护状态
  12. this.history = new HistoryRoute();
  13. this.init();
  14. }
  15. init() {
  16. if (this.mode === 'hash') {
  17. // 先判断用户打开时有没有hash值,没有的话跳转到#/
  18. location.hash ? '' : (location.hash = '/');
  19. window.addEventListener('load', () => {
  20. this.history.current = location.hash.slice(1);
  21. })
  22. window.addEventListener('hashchange', () => {
  23. this.history.current = location.hash.slice(1);
  24. })
  25. } else { // browserRouter
  26. loaction.pathname ? '' : (location.pathname = '/');
  27. window.addEventListener('load', () => {
  28. this.history.current = location.pathname;
  29. })
  30. window.addEventListener('popstate', () => {
  31. this.history.current = location.pathname;
  32. })
  33. }
  34. }
  35. createMap(routes) { // 将component和path匹配
  36. return routes.reduce((pre, cur) => {
  37. pre[cur.path] = cur.component; // 用一个对象,以键值对的方式将component和path存储起来
  38. return pre;
  39. }, {})
  40. // 返回的形式如下
  41. // {
  42. // ['/home']: Home,
  43. // ['/about']: About
  44. // }
  45. }
  46. }
  47. // 作为插件安装,就是vue.use(router)这步的操作
  48. VueRouter.install = (v) => {
  49. Vue = v;
  50. Vue.mixin({
  51. beforeCreate() {
  52. if (this.$options && this.$options.router) {
  53. // 如果是根组件
  54. this._root = this; //把当前实例挂载到_root上
  55. this._router = this.$options.router;
  56. Vue.util.defineReactive(this, 'xxx', this._router.history);
  57. } else {
  58. //如果是子组件
  59. this._root = this.$parent && this.$parent._root;
  60. }
  61. Object.defineProperty(this, '$router', {
  62. get() {
  63. return this._root._router;
  64. },
  65. });
  66. Object.defineProperty(this, '$route', {
  67. get() {
  68. return this._root._router.history.current;
  69. },
  70. });
  71. },
  72. });
  73. Vue.component('router-link', {
  74. props: {
  75. to: String,
  76. },
  77. render(h) {
  78. let mode = this._self._root._router.mode;
  79. let to = mode === 'hash' ? '#' + this.to : this.to;
  80. return h('a', { attrs: { href: to } }, this.$slots.default);
  81. },
  82. });
  83. Vue.component('router-view', {
  84. render(h) {
  85. let current = this._self._root._router.history.current;
  86. let routeMap = this._self._root._router.routesMap;
  87. return h(routeMap[current]);
  88. },
  89. });
  90. }

React Router

react-router原理

先来看基本的使用,可以看出router是类似于ProviderContext的挂载。

  1. // App.tsx
  2. import { Route, Routes, Link } from 'react-router-dom'
  3. import Home from './views/Home'
  4. import About from './views/About'
  5. import './App.css'
  6. function App() {
  7. return (
  8. <>
  9. <h1>React Router Demo</h1>
  10. <Link to="/">Home</Link>
  11. <Link to="about">About</Link >
  12. <br />
  13. <Routes>
  14. <Route path="/" element={<Home />} />
  15. <Route path="about" element={<About />} />
  16. </Routes>
  17. </>
  18. )
  19. }
  20. export default App;
  21. // main.tsx
  22. import React from 'react'
  23. import ReactDOM from 'react-dom/client'
  24. import { BrowserRouter } from 'react-router-dom'
  25. import App from './App.tsx'
  26. import './index.css'
  27. ReactDOM.createRoot(document.getElementById('root')!).render(
  28. <React.StrictMode>
  29. <BrowserRouter>
  30. <App />
  31. </BrowserRouter>
  32. </React.StrictMode>
  33. )

我们去看<Routes>组件的声明,重点是去看createRoutesFromChildren

  1. export function Routes({
  2. children,
  3. location,
  4. }: RoutesProps): React.ReactElement | null {
  5. return useRoutes(createRoutesFromChildren(children), location);
  6. }

可以看到对其中的每个子节点,也就是<Route>组件进行了递归的处理,生成route,其中children属性包含了嵌套的子组件,最终返回一个routes数组。

  1. export function createRoutesFromChildren(
  2. children: React.ReactNode,
  3. parentPath: number[] = []
  4. ): RouteObject[] {
  5. let routes: RouteObject[] = [];
  6. React.Children.forEach(children, (element, index) => {
  7. let treePath = [...parentPath, index];
  8. if (element.type === React.Fragment) {
  9. routes.push.apply(
  10. routes,
  11. createRoutesFromChildren(element.props.children, treePath)
  12. );
  13. return;
  14. }
  15. let route: RouteObject = {...};
  16. if (element.props.children) {
  17. route.children = createRoutesFromChildren(
  18. element.props.children,
  19. treePath
  20. );
  21. }
  22. routes.push(route);
  23. });
  24. return routes;
  25. }

这样我们就知道了RoutesRoute组件是怎么实现嵌套路由的。

接着我们以BrowserRouter为例,去看在App外嵌套BrowserRouter做了什么。

  1. export function BrowserRouter({
  2. basename,
  3. children,
  4. future,
  5. window,
  6. }: BrowserRouterProps) {
  7. let historyRef = React.useRef<BrowserHistory>();
  8. if (historyRef.current == null) {
  9. historyRef.current = createBrowserHistory({ window, v5Compat: true });
  10. }
  11. let history = historyRef.current;
  12. let [state, setStateImpl] = React.useState({
  13. action: history.action,
  14. location: history.location,
  15. });
  16. let { v7_startTransition } = future || {};
  17. let setState = React.useCallback(
  18. (newState: { action: NavigationType; location: Location }) => {
  19. v7_startTransition && startTransitionImpl
  20. ? startTransitionImpl(() => setStateImpl(newState))
  21. : setStateImpl(newState);
  22. },
  23. [setStateImpl, v7_startTransition]
  24. );
  25. React.useLayoutEffect(() => history.listen(setState), [history, setState]);
  26. return (
  27. <Router
  28. basename={basename}
  29. children={children}
  30. location={state.location}
  31. navigationType={state.action}
  32. navigator={history}
  33. future={future}
  34. />
  35. );
  36. }

可以看到重点有两块,一个是创建了history对象维护路由的上下文,第二个是用useLayoutEffect监听history的变化进行更新,最后返回Router组件,所以我们要去看createBrowserHistoryRouter组件做了什么。

createBrowserHistory

这里通过取出location中的pathname,search,hash,调用getUrlBasedHistory,生成history对象。

  1. export function createBrowserHistory(
  2. options: BrowserHistoryOptions = {}
  3. ): BrowserHistory {
  4. function createBrowserLocation(
  5. window: Window,
  6. globalHistory: Window["history"]
  7. ) {
  8. let { pathname, search, hash } = window.location;
  9. return createLocation(
  10. "",
  11. { pathname, search, hash },
  12. (globalHistory.state && globalHistory.state.usr) || null,
  13. (globalHistory.state && globalHistory.state.key) || "default"
  14. );
  15. }
  16. function createBrowserHref(window: Window, to: To) {
  17. return typeof to === "string" ? to : createPath(to);
  18. }
  19. return getUrlBasedHistory(
  20. createBrowserLocation,
  21. createBrowserHref,
  22. null,
  23. options
  24. );
  25. }

history对象包含了一些对URL的操作,包括push,pop,replace等。

  1. function getUrlBasedHistory(...): UrlHistory {
  2. let globalHistory = window.history;
  3. let action = Action.Pop;
  4. let listener: Listener | null = null;
  5. function getIndex(): number {
  6. let state = globalHistory.state || { idx: null };
  7. return state.idx;
  8. }
  9. function handlePop() {
  10. action = Action.Pop;
  11. let nextIndex = getIndex();
  12. let delta = nextIndex == null ? null : nextIndex - index;
  13. index = nextIndex;
  14. if (listener) {
  15. listener({ action, location: history.location, delta });
  16. }
  17. }
  18. function push(to: To, state?: any) {
  19. action = Action.Push;
  20. let location = createLocation(history.location, to, state);
  21. if (validateLocation) validateLocation(location, to);
  22. index = getIndex() + 1;
  23. let historyState = getHistoryState(location, index);
  24. let url = history.createHref(location);
  25. globalHistory.pushState(historyState, "", url);
  26. }
  27. function replace(to: To, state?: any) {
  28. action = Action.Replace;
  29. let location = createLocation(history.location, to, state);
  30. if (validateLocation) validateLocation(location, to);
  31. index = getIndex();
  32. let historyState = getHistoryState(location, index);
  33. let url = history.createHref(location);
  34. globalHistory.replaceState(historyState, "", url);
  35. }
  36. function createURL(to: To): URL {
  37. let base =
  38. window.location.origin !== "null"
  39. ? window.location.origin
  40. : window.location.href;
  41. let href = typeof to === "string" ? to : createPath(to);
  42. return new URL(href, base);
  43. }
  44. let history: History = {
  45. get action() {
  46. return action;
  47. },
  48. get location() {
  49. return getLocation(window, globalHistory);
  50. },
  51. listen(fn: Listener) {
  52. window.addEventListener(PopStateEventType, handlePop);
  53. listener = fn;
  54. return () => {
  55. window.removeEventListener(PopStateEventType, handlePop);
  56. listener = null;
  57. };
  58. },
  59. createHref(to) {
  60. return createHref(window, to);
  61. },
  62. createURL,
  63. encodeLocation(to) {
  64. let url = createURL(to);
  65. return {
  66. pathname: url.pathname,
  67. search: url.search,
  68. hash: url.hash,
  69. };
  70. },
  71. push,
  72. replace,
  73. go(n) {
  74. return globalHistory.go(n);
  75. },
  76. };
  77. return history;
  78. }

Router

这里可以看到,它就是一个Provider包裹App,使我们应用内部能够获取到Router

  1. export function Router(...): React.ReactElement | null {
  2. let basename = basenameProp.replace(/^\/*/, "/");
  3. let navigationContext = React.useMemo(
  4. () => ({
  5. basename,
  6. navigator,
  7. static: staticProp,
  8. future: {
  9. v7_relativeSplatPath: false,
  10. ...future,
  11. },
  12. }),
  13. [basename, future, navigator, staticProp]
  14. );
  15. if (typeof locationProp === "string") {
  16. locationProp = parsePath(locationProp);
  17. }
  18. let {
  19. pathname = "/",
  20. search = "",
  21. hash = "",
  22. state = null,
  23. key = "default",
  24. } = locationProp;
  25. let locationContext = React.useMemo(() => {
  26. let trailingPathname = stripBasename(pathname, basename);
  27. return {
  28. location: {
  29. pathname: trailingPathname,
  30. search,
  31. hash,
  32. state,
  33. key,
  34. },
  35. navigationType,
  36. };
  37. }, [basename, pathname, search, hash, state, key, navigationType]);
  38. return (
  39. <NavigationContext.Provider value={navigationContext}>
  40. <LocationContext.Provider children={children} value={locationContext} />
  41. </NavigationContext.Provider>
  42. );
  43. }

手写实现

BrowserRouter

首先,我们要创建Router的上下文,再获取location中的pathname,用状态去存储。

  1. function BrowserRouter(props) {
  2. const RouterContext = createContext();
  3. const [path, setPath] = useState(() => {
  4. const { pathname } = window.location;
  5. return pathname || '/';
  6. })
  7. }

接下去要监听用户的前进后退行为,即触发popstate事件,将path更新为最新的pathname

  1. useEffect(() => {
  2. window.addEventListener('popstate', handlePopstate);
  3. return window.removeEventListener('popstate', handlePopstate);
  4. }, []);
  5. const handlePopstate = () => {
  6. const { pathname } = window.location;
  7. setPath(pathname);
  8. }

最后返回我们的Provider

  1. return (
  2. <RouterContext.Provider value={path}>
  3. {props.children}
  4. </RouterContext.Provider>
  5. )

此时我们还缺少一些跳转的功能:

  1. const push = (path) => {
  2. setPath(path);
  3. window.history.pushState({ path }, null, path)
  4. };
  5. const goBack = () => {
  6. window.history.go(-1);
  7. }

所以同样要创建HistoryContext并且包裹children

  1. const HistoryContext = createContext();
  2. return (
  3. <RouterContext.Provider value={path}>
  4. <HistoryContext.Provider value={{ push, goBack }}>
  5. {props.children}
  6. </HistoryContext.Provider>
  7. </RouterContext.Provider>
  8. )

然后要考虑怎么去消费我们的Router,就是通过Routes中包裹的Route,判断当前path是否等于componentPath,如果相等就渲染当前组件。

  1. export function Route(props) {
  2. const { element: Component, path: componentPath } = props;
  3. return (
  4. <RouterContext.Consumer>
  5. {(path) => {
  6. componentPath === path ? <Component /> : null;
  7. }}
  8. </RouterContext.Consumer>
  9. )
  10. }

最终代码如下:

  1. // browser-router
  2. function BrowserRouter(props) {
  3. const RouterContext = createContext();
  4. const HistoryContext = createContext();
  5. const [path, setPath] = useState(() => {
  6. const { pathname } = window.location;
  7. return pathname || '/';
  8. });
  9. useEffect(() => {
  10. window.addEventListener('popstate', handlePopstate);
  11. return window.removeEventListener('popstate', handlePopstate);
  12. }, []);
  13. const handlePopstate = () => {
  14. const { pathname } = window.location;
  15. setPath(pathname);
  16. }
  17. const push = (path) => {
  18. setPath(path);
  19. window.history.pushState({ path }, null, path)
  20. };
  21. const goBack = () => {
  22. window.history.go(-1);
  23. }
  24. return (
  25. <RouterContext.Provider value={path}>
  26. <HistoryContext.Provider value={{ push, goBack }}>
  27. {props.children}
  28. </HistoryContext.Provider>
  29. </RouterContext.Provider>
  30. )
  31. }
  32. export default BrowserRouter
  33. export function Route(props) {
  34. const { element: Component, path: componentPath } = props;
  35. return (
  36. <RouterContext.Consumer>
  37. {(path) => {
  38. componentPath === path ? <Component /> : null;
  39. }}
  40. </RouterContext.Consumer>
  41. )
  42. }

HashRouter

区别就是获取的是location.hash,监听的是hashChange事件,改变的是location.hash

  1. const [path, setPath] = useState(() => {
  2. const { hash } = window.location;
  3. if (hash) {
  4. return hash.slice(1)
  5. }
  6. return '/#/'
  7. });
  8. useEffect(() => {
  9. window.addEventListener('hashChange', handleHashChange);
  10. return window.removeEventListener('hashChange', handleHashChange);
  11. }, []);
  12. const handleHashChange = () => {
  13. const { hash } = window.location;
  14. setPath(hash.slice(1));
  15. }
  16. const push = (path) => {
  17. setPath(path);
  18. window.location.hash = path;
  19. };

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/从前慢现在也慢/article/detail/430341
推荐阅读
相关标签
  

闽ICP备14008679号