如何使用 Router 为你页面带来更快的加载速度-腾讯云开发者社区-百姓标王
前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >如何使用 Router 为你页面带来更快的加载速度

如何使用 Router 为你页面带来更快的加载速度

作者头像
19组清风
发布2024-03-24 08:47:25
发布2024-03-24 08:47:25
2510
举报
文章被收录于专栏:Remix.run 提供了开箱即用的上述功能,你无需任何繁琐的 SSR 应用配置即可快速在你的应用程序中体验上述功能。

快速上手

说了那么多理论知识,接下来我们就来简单体验下 Data Apis 应该如何使用。

项目demo。

createBrowserRouter

在 V6 之前通常我们会直接使用 <BrowserRouter /> 组件来作为我们应用程序的根节点,我相信大多数同学 React 应用仍是这样在使用路由。

在 V6 后提供了一种新的方式来创建路由对象 createBrowserRoute Api ,只有使用了 createBrowserRoute Api 创建的路由对象才被允许使用路由的 data apis。

自然,我们首先应该使用 createBrowserRoute 来创建一个所谓的路由对象:

代码语言:javascript
复制
// 默认数据获取方法
const getDeferredData = () => {
  return new Promise((r) => {
    setTimeout(() => {
      r({ name: '19Qingfeng' });
    }, 2000);
  });
};

const getNormalData = () => {
  return new Promise((r) => {
    setTimeout(() => {
      r({ name: 'wang.haoyu' });
    }, 2000);
  });
};
// 创建数据路由对象
const router = createBrowserRouter([
  {
    path: '/',
    Component: App,
    children: [
      {
        index: true,
        Component: Normal,
        loader: async () => {
          const data = await getNormalData();
          return json({
            data
          });
        }
      },
      {
        path: 'deferred',
        Component: Deferred,
        loader: () => {
          const deferredDataPromise = getDeferredData();
          return defer({
            deferredDataPromise
          });
        }
      }
    ]
  }
]);

上边的代码中我们使用 createBrowserRouter 创建了一个携带数据的路由对象。

  • 根路径 / : 该路径默认会渲染 Normal 组件,并且将组件与数据进行了解耦,拥有一个名为 getNormalData 的数据获取方法。
  • /deferred 路径: 该路径渲染 Deferred 组件,同样拥有一个 getDeferredData 的数据获取方式。

创建路由对象时,根路径和 deferred 路径乍一看大同小异。不过还是稍稍有些不同的:

  • 跟路径下的 loaderFunction 使用了 await 关键字,不难想象 Normal 组件的渲染是需要等待 loader 中的异步操作结束才可直接渲染。
  • /deferred 路径下的 loader 并未使用 await 关键字,而是使用了 defer 包裹了 getDeferredData 方法返回的数据请求(该方法返回一个 Promise),我们并未使用 await 去等待它完成,自然 Deferred 路径的渲染也并不会被阻塞。

RouterProvider

在调用 createBrowserRouter 获得 router 对象时,我们仍然需要在我们的根组件将创建的路由对象传递给我们的应用程序,此时就需要使用到 RouterProvider Api:

代码语言:javascript
复制
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { RouterProvider } from 'react-router-dom';
import router from './routes/router.tsx';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider router={router}></RouterProvider>,
  </React.StrictMode>
);

这一步我们需要将创建的路由对象传入 RouterProvider ,同时将 RouterProvider 作为应用程序的根组件传递给 createRoot Api。

useLoaderData/Suspense/Await

要使用 Router Data Apis 其实我们仅仅需要在原始的应用程序中更换上述两个创建路由对象时的 Api 即可。

接下来的部分,我们已经在路由定义时将数据请求和组件拆分开来,那么在组件渲染中我们如何获取这部分数据请求返回的数据。

ReactRouter 中提供了一个 useLoaderData 的 hook 来为我们在组件中获取路由中 loader 的加载数据:

代码语言:javascript
复制
import { useLoaderData } from 'react-router';

function Normal() {
  // 直接使用 useLoaderData 获取当前组件对应 loader 返回的数据
  const { data } = useLoaderData();

  return (
    <div>
      <h3>Hello</h3>
      <p>{data.name}</p>
    </div>
  );
}

export default Normal;

这一过程看起来行云流水般的丝滑。首先在定义路由列表时将数据和渲染拆分开来,请求到来时会同步触发数据请求和页面渲染。

当我们在页面渲染途中需要路由中定义的数据时,只需要简单的通过 useLoaderData 来获取对应数据即可。

当我们首次访问根路径时,应用会同时触发根路径下的 loaderFunction 等待 loaderFunction 执行完毕后使用 loaderFunction 中返回的数据进行页面渲染。

不过上边的截图中明显可以看到,在访问根路径时页面会有部分的白屏之后才开始直接渲染页面。

这是因为我们在根路径下的 loader 定义成了阻塞的异步逻辑:

代码语言:javascript
复制
        loader: async () => {
          const data = await getNormalData();
          return json({
            data
          });
        }

页面渲染需要依赖 loader 中的数据,而 loader 的执行又是一种异步的阻塞逻辑,自然首次打开页面时需要等待这部分的 loader 执行完毕才可以渲染。

虽然说这一步我们已经将页面的渲染和数据获取通过 loader 的方式拆分开来,不过由于渲染需要依赖 loader 中的数据又会造成阻塞的方式,这样的用户体验自然也是比较糟糕的。

值得庆幸的是 ReactRouter 中为我们提供了两种方式来处理这个问题:

  • 首先,第一种方式是在每次页面切换 loader 加载时,支持在顶层传入一个 fallbackElement 来渲染加载时的骨架。
代码语言:javascript
复制
// main.tsx
ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <RouterProvider
      router={router}
      fallbackElement={<div>Loading...</div>}
    ></RouterProvider>
  </React.StrictMode>
);

通过为 RouterProvider 定义 fallbackElement 元素可以在整个页面切换时增加骨架展位从而给予用户及时的加载反馈。

这种方式虽然简单,但是 fallbackElement 的方式是页面级别的加载。

有时我们的页面只有部分模块内容需要依赖 loader 的数据完成才可以渲染真正有意义的内容,大多数时候页面中的其他元素都是静态(不依赖于数据加载)的模块。

粗暴的使用 fallbackElement 在 loader 执行时阻塞整个页面的渲染并不是在站点体验上的最优解。

同时 fallbackElement 只会在页面首次加载时才会生效,当你的页面拥有多个 page 进行 SPA 跳转时,需要配合 navigation.state 来判断页面是否处于跳转加载态。

  • 另外一种方式,可以更好的解决 fallbackElement 带来的全局 loading 问题,ReactRouter 中提供了 Await Component 以及 defer function 来为我们解决上述的问题。

这次,让我们访问 /deferred 路径:

上边的截图中可以看到,页面在加载时可以分为两个部分:

  • 没有任何数据依赖的部分,在页面加载时会直接渲染到屏幕中。
  • 依赖数据的部分首次,首先渲染为 loading deferred data 加载状,等待 loader 加载完毕后会重新渲染为真正含有意义的部分 19Qingfeng。

需要额外留意的是,大家不要将这一部分和在 useEffect 中发起数据请求混淆。deffer router 的优势正是在于对于发起数据请求的时机优化:

让我们再次聚焦到 <Deffer /> 组件上:

代码语言:javascript
复制
// router
{
        path: 'deferred',
        Component: Deferred,
        loader: () => {
          const deferredDataPromise = getDeferredData();
          return defer({
            deferredDataPromise
          });
        }
}

// deffer component
import { Suspense } from 'react';
import { Await, useLoaderData } from 'react-router';

function Deferred() {
  const { deferredDataPromise } = useLoaderData()

  return (
    <>
      <div>This is deferred Page!</div>
      <div>
        <h3>Hello</h3>
        <Suspense fallback={'loading deferred data'}>
          <Await resolve={deferredDataPromise}>
            {(data) => {
              return <p>{data.name}</p>;
            }}
          </Await>
        </Suspense>
      </div>
    </>
  );
}

export default Deferred;

由于我们在路由定义时,/deferred 路径对应的 loader 中并不存在任何阻塞逻辑,同时我们通过 defer 方法返回了数据请求的 promise,此时我们并没有在 loader 中等待这个 promise 状态完成。

之后,我们在组件中使用 Suspense 配合 Await 组件来实现页面部分元素的 loading 态从而对于页面进行一种渐进式加载方式:

  • Suspense Await 中的组件会等待 defer 依赖的 promise 状态完成后渲染成为对应的元素。
  • 页面中不依赖 loader 中的数据元素会立即渲染到浏览器中。

直到这一步,我们使用 defer 配合 Await 在页面渲染和数据请求中真正做到了同步进行,给予用户更好的加载体验。

React Router 是如何实现 Defer 这一过程

Loaders 调用时机

上边的章节中我们讲到 ReactRouter 数据路由的优势以及如何在我们的站点中使用数据路由来优化我们的页面。

接下来的这个章节,我们就来简单聊聊 ReactRouter 的 Data apis 的实现思路。

首先,loader 的定义、执行不难理解,只要在用户访问到当前路径时 ReactRouter 会寻找到当前路径下匹配到的所有 route 对象,自然我们只需要在渲染 route.Component 前调用执行所有的 loader 即可:

packages/react-router-dom/index.tsx

createBrowserRouter 内部会通过 createRouter 创建一个路由对象(该路由对象类似 without data apis router,用来控制页面路由对象)。

创建完成后会立即调用内部的 initialize 方法初始化路由 state:

重点就在于 initialize 的 startNavigation 的方法,在 SPA 应用下默认 state.initialized 是 false 会进入 startNavigation 方法。

所谓 startNavigation 即是 data route apis 中内部的跳转方法,每次跳转 ReactRouter 内部都会在内部实际调用该方法。

初始化时,调用 startNavigation 会传入第二个参数 state.location (当前页面路由),即会触发当前路由 Router 逻辑。

startNavigation 中会进行一系列操作,比如通过 router match 来寻找当前 state.location 下的 route 对象等等,重点就在于 handleLoaders 方法。

handleLoaders 方法正是执行当前匹配路径的所有 loaders 方法,当执行完所有 loaders 获取当前路由的路由数据。

可以清楚的看到在调用 handleLoaders 方法时是 await 的阻塞逻辑,自然也就和我们上述根路径的 case match 上了。

简单来说,客户端代码在执行 createBrowserRouter 方法后就会立即进行 initialize 方法从而对于当前 location 路径寻找匹配的 route 对象执行当前路由下的 loader 方法。

当然,当我们调用 usenavigate() 返回值跳转时,同样也是通过 startNavigation 重新调用这一过程。

同时,在 initialize 方法执行完毕后会返回 createBrowserRouter 内部定义的 router 对象,该方法内部控制了当前路由的对象和保存了 router 的各个实例方法(跳转等)。

Loader Data 是如何关联页面渲染的

上一步我们清楚了在页面加载后,会调用 startNavigation 方法执行所有 loader 获取 loaderFunction 返回的数据。

这次,让我们再次聚焦回到 startNavigation 方法中:

startNavigation 在结尾会获取到当前 location 的 match (当前所有匹配的路由对象)以及 loaderData (当前所有匹配的 loader 返回值)。

之后会在结尾调用,completeNavigation 方法。顾名思义,该方法为完成跳转的方法,在 completeNavigation 中首先会进行一系列 actionData、loaderData、block 之类的判断,这部分逻辑并不是我们的重点,重点在于:

completeNavigation 默认会调用 updateState 去更新最新的路由数据。

可以看到 updateState 方法会合并获得最新的 state 状态(包含当前 location 下的最新的 loaderData 以及 match 等等),同时调用 subscribers 订阅方法来调用 subscriber 方法传入最新的 routerState。

那么,更新后的数据会被哪里订阅呢?不知道大家还记不记得我们通过 createBrowserRouter 方法创建的 router 对象会被传入 <RouterProvider router={router} /> 中。

RouterProvider 组件中会订阅 initialize 返回的 router 对象,当调用 updateState 更新后会通知更新 RouterProvider 的 setState 改变该组件的 state 状态。

当 router state 改变时触发 stateState 方法,更新 RouterProvider 的 state 值,同时该组件中会通过 DataRouterStateContext.Provider 将最新的 router state 传递给子组件中。

因为我们的应用程序都是被 RouterProvider 包裹,自然当我们调用 useLoaderData 时只需要通过 context 的形式即可在组件中获得最新的 state 。

这一步整个流程就变的清晰了,当页面路由改变时

  1. 触发 startNavigation 寻找当前匹配的 route 对象。
  2. 执行当前匹配 route 对象的 loaderFunction 获得返回值。
  3. startNavigation 执行完成后会调用 completeNavigation 更新 router 的 state。
  4. RouterProvider 中由于 subscriber 了 router state 的变化,自然 RouterProvider 也会同步更新当前组件顶层的 state,同时通过 provider 的方式传递给所有子组件最新的 state。
  5. 最后,当我们在组件中调用 useLoaderData 时,由于 provider 中的 value 发生变化,useLoaderData 也会获得最新的 loaderData。

Defer & Await

了解了 ReactRouter 中 loader 是如何被调用以及如何将 loaderData 关联到页面数据上后我们来看看 defer 的大致实现过程。

Defer

其实 defer 的实现 ReactRouter 做了非常多的边界 case ,比如在页面快速切换时取消上一次的 defer Promise 等等之类的边界判断。

这里我们仅仅关心正常的 defer 是如何被执行的,关注一个大概的执行流程即可。有兴趣的同学可以自行翻阅 ReactRouter 的源代码去向详细阅读了解。

首先 defer 的存放位置在 packages/router/utils.ts 中:

我们可以看到 defer 方法返回的是一个 DeferredData 的实例:

DeferredData 这个类中,在初始化时会为 defer 包裹的对象中每个值调用 trackPromise 方法:

trackPromise 方法会为 defer 中的每个值标记 _tracked 为 true 表示该 Promise 已经被 ReactRouter 追踪。

同时 trackPromise 会返回返回一个新的 Promise:

abortPromise 表示 ReactRouter 中取消 defer 请求的逻辑,我们暂时无需关注它。

重点在于,当 defer 中的 promise 完成/失败后都会调用 this.onSettle 方法:

onSettle 方法会为 defer 方法中每个 promise 的值在 fulfilled 后根据返回的 data/error 标记对应的 _error 以及 _data 属性分别为错误信息/resoved 的值。

所以,简单来说 defer 方法会为包裹的 object 中每个值分别打上 tag 带上 _tracked 以及在 Promise 变为完成态后为 promise 标记 _data 或者 _error 属性。

Await

defer 往往是需要使用 Await 来配合使用。

Await 的实现就稍微显得简单了些,首先我们在看看 Await 组件中的 AwaitErrorBoundary 他会接受外部传入的 resolve ,通常这个 resolve 会是 useLoaderData 获取的 defer 方法返回的 promise。

它的实现就类似于我们通常使用的 ErrorBoundary 组件,AwaitErrorBoundary 组件的 render 函数中会首先获取到外部传入的 resolve 。

如果当前 resolve 已经被标记 ReactRouter 追踪(_tracked 为 true),那么此时会根据 _data/_error 来判断该 Promise 的状态:

  • Promise 拥有 _data 表示正常完成状态,正常完成状态时标记 AwaitRenderStatus 为成功。
  • Promise 拥有 _error 时候为完成(rejected)状态,标记状态为失败。
  • 否则,会标记状态为 pending ,同时在 render 中 throw 该 promise。

在成功和失败状态下 render 方法一目了然,当失败时会渲染 AwaitContext.Provider 传入当前 promise,同时将 children 重制为 errorElement。

当成功时,会正常将 children 传入为外部的 children。

自然,由于 pendding 状态的 Promise 会向外 throw promise ,我们在使用 Await 组件时需要配合 Suspense 组件。

由于我们在子组件(Await) 中 throw 出了当前 Promise,Supense 对于子组件会开启 fallback 进行异步加载等待 Promise 完成后又会更新状态重新渲染子组件(reRender 时 Await 中的 primise 已经不为 pendding,自然就会进入成功/失败的渲染逻辑了)。

之后,在了解了 AwaitErrorBoundary 的逻辑后,我们再来回到 ResolveAwait 组件:

ResolveAwait 做的事情也非常简单,判断传入的 children 是否为 function,如果为 function 的情况会调用 useAsyncValue 获取到 AwaitContext.Provider 的值调用该函数,如果不为函数则会直接渲染该组件(我们需要在组件内部自己通过 useAsyncvalue 获取外层 reolve 的返回值)。

这也是 ReactRouter Await Children 组件的两种传入方式,具体可以参考 文档说明。

而所谓的 useAsyncValue 自然就是对应了 AwaitContext.Provider 传入的 value,获取了 primise._data 从而获取到 promise resolve 的值。

当然,与 useAsyncValue 对应的也存在 useAsyncError ,我们可以在 errorElement 中通过 useAsyncError 获取 promise rejected 的原因。

到这里,defer、Await 的执行流程我们就大概理清楚了:

  • 在 loader 中会通过调用 derfer 为每个 value 打上标记,标记 tracked 为 true,同时会为每个 Promise 在 reject/resolve 后为当前 promise 标记 _error/_data 为失败的原因/成功的数据。
  • 当我们在组件中使用 useLoaderData 获取到 defer 返回的数据时,对于每一个 value 需要通过 Suspense/Await 组件进行包裹使用。
  • Await 组件中会根据 promise 的 _tracked/_error/_data 判断当前 Promise 的状态从而进行不同 UI 的渲染。

Server Side Render

服务端 ReactRouter 简介

上述过程中对于 ReactRouter V6.4 新增的 data apis 的原理进行了浅析,我们了解到了在客户端执行时 data apis 的执行过程。

不过,上边的流程仅仅是针对于客户端渲染,也就是我们通常的 CSR 过程。

在 SSR 服务端渲染下,其实还是有非常多的不同的,比如通常在服务端中我们会在 createStateRouter 来处理服务端路由。

从而让路由的 loader 不会打包进入客户端代码,而是仅在我们的 Server 上运行 loaderFunction。

每次页面请求到来时,服务端会同步执行 React 组件渲染以及在服务端执行 loaderFunction ,客户端完全不进行任何 Loader 的感知。

在 createRouter 方法中如果存在 hydrationData 的话首次渲染是会标记 initialized 为 true 的。

我们刚才也提高过,如果 hydrationData 为 true 时,是不会在初始化时调用 startNavigation 的,自然也不会触发 laoder 的运行。

所谓的 hydrationData 及时在 SSR 时服务端数据和客户端数据交互的桥梁,ReactRouter 默认会通过 __staticRouterHydrationData 在 window 上传递。

当然,服务端渲染的 ReactRouter 又是另一个话题了。如果你有服务端需求强烈建议大家可以尝试下 Remix。

Remix 中已经通过了一系列封装来为我们提供开箱即用的 ReactRouter Server Side Render。

Remix Defer

关于 Remix 在服务端渲染时做了许多构建相关的处理,简单来说他会在服务端构建时确定好每个路由需要的静态资源列表,说实话我也没看完这部分,笔者这里就不再展开了。

唯一想提到的就是上文我们说过,我们可以在客户端通过 defer 返回的对象中使用 Promise 来延迟我们部分页面的加载。

同时,我们也提到过在服务端渲染时通常 loaderFunction 并不会在客户端执行,而是在服务器上执行当前路由对应的 loaderFunction。

那么,如果我们通过 streaming 配合 defer 使用时,不知道大家有没有想过 Remix 是如何格式化服务端 loaderFunction 的 defer 呢?

上述这么说可能有些抽象,有些同学不了解 remix 可能并不清楚我在说什么。

简单来说 Remix 会在服务器上执行 loaderFunction,如果 loaderFunction 中返回 defer 的 promise,比如:

代码语言:javascript
复制
const fetchPromiseData = () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({});
    }, 10000);
  });
};
// 当前路由的 loaderFunction
export const loader: LoaderFunction = async ({ request, context }) => {
  return defer({
    data: fetchPromiseData(),
  });
};

remix 会在服务端执行 loader ,然后将服务端 pendding 状态的 promise 传递给客户端,客户端会判断服务端 Promise 状态:

  • pendding 时,渲染 Suspense 中的 fallbackElement。
  • resolve 时,会渲染 Await 组件的 children 同时获取 promise 的数据。
  • rejected 时,会渲染 errorElement。

一切看起来都和客户端一模一样,不过重点就在于 Remix 将服务端 loaderFunction 中 defer 返回的 Promise 序列化后返回给客户端,客户端也会得到这部分序列化后的 Promise ,听起来非常神奇对吧。

如果你直接使用 ReactRouter 作为你的服务端渲染应用,这部分 Promise 的序列化是需要你自己进行实现的。

实际上这部分 Promise 的序列化是在 Remix 的 <Scripts /> 组件中实现的:

在页面初始化渲染时,借助 <Await />__remixContext 的自定义 api 来实现了类似序列化的 Promise 在 Server 和 Client 中来维持相同状态通知。

具体 Remix 的相关 Defer 解读这里我就不再展开了,有兴趣的同学我们可以在评论区一起交流。

如果大家 Remix 有兴趣的话,之后我也会为大家带来 Remix 的文章分享。

写在结尾

如果有兴趣学习 ReactRouter 路由渲染原理部分的同学可以参考我的这篇 从0到1手把手带你实现一款Vue-Router,其实关于路由 Render 的原理 Vue 和 React 是大同小异的实现思路。

文章中为大家分享了 React Data Apis 的优势、用法以及原理浅析,希望文章中的内容可以帮助到大家。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-03-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • Why is the data apis better?
    • Client Side Render
      • Server Side Render
      • 快速上手
        • createBrowserRouter
          • RouterProvider
            • useLoaderData/Suspense/Await
            • React Router 是如何实现 Defer 这一过程
              • Loaders 调用时机
                • Loader Data 是如何关联页面渲染的
                  • Defer & Await
                    • Defer
                    • Await
                  • Server Side Render
                    • 服务端 ReactRouter 简介
                    • Remix Defer
                • 写在结尾
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档

                相关内容推荐

                通化网站seo推广优化网站优化哪儿好企业网站优化的方式河北网站优化公司价格正规网站seo优化费用崇明区公司官方网站优化定制优化网站到谷歌第一页网站刷百度排名优化企业营销网站怎么优化枣庄网站优化操作许昌百度网站优化平台东莞网站优化排名软件搜索引擎优化对网站的影响安顺市网站优化渠道网站优化过程中一些细节福建南平网站首页优化网站优化基础知识88点沁阳百度网站优化哪家不错手机网站优化市场海林优化网站如何优化网站高并发访问自贡企业网站优化服务青羊网站优化有哪些河间市网站seo优化排名丹东公司网站seo优化哪家好宜春市网站排名优化网站托管网站优化流程宁阳地区网站优化服务企业网站提升排名优化北京百度网站优化旅游网站建设与优化推广自主网站优化通化网站优化排名推广网站建设优化选择s火25星海口网站优化搜索html标签优化网站亳州网站seo优化推广无锡网站优化对策临沂网站优化策略中国网站优化哪个比较好厦门网站建设优化哪家好合肥网站优化推广报价前端网站优化方案电子商务网站外链优化有哪些洛阳律师网站优化网站的推广与优化论文威海正规网站优化代理哪里有卖江门首页网站关键词优化公司做网站优化的习惯邹平网站做优化网站优化推广搜行者seo横岗网站优化公司项城网站推广优化哪家靠谱濮阳网站优化培训贵阳网站界面优化台儿庄优化网站哪家专业上海网站排名优化菜鸟下拉网站标题导航优化网站优化软件怎么样网站优化外包怎样任城谷歌网站优化浙江优化网站推广安阳网站优化哪家专业济源知名网站优化哪家好杭州seo网站优化费用徐州网站界面优化dede网站访问速度优化桐乡网站排名优化五原网站优化多少钱崂山区网站优化软件企业网站seo原理及优化方法网站关键词密度怎么优化茶山seo网站优化哪家好密云网站快速优化优化网站方法密切云速捷选择鹿泉网站关键词优化冀州网站seo优化网站设计优化用什么方式深圳公司网站推广优化襄阳网站关键字优化系统商务行业网站seo优化咨询靠谱网站优化不烧钱私服网站好优化吗栖霞区网站关键字优化门户网站怎么做优化网站怎么过度优化网站谷歌搜索引擎优化响应式网站建设智能优化吕梁网站排名优化传统行业网站优化专业团队做网站优化seo网站优化如何提升网站的排名新乡网站优化哪家价格便宜谷歌推广和seo优化网站网站链接优化指标网站不收录怎么优化鄂州网站排名优化公司盘锦网站seo优化推广茂名网站优化常识网站竞价推广优化方案铁岭网站优化推广的优势建设网站如何优化搜索引擎网站定制优化是什么意思天门网站推广优化开发内江湖南网站优化推广网站种植牙优化软件常熟市网站公告优化价格表网站优化包括哪几个优化保健品网站优化有效果浙江省网站seo优化排名湛江网站性能优化麟游网站优化宁夏网站优化流程宝安网站seo优化天长网站优化服务邢台网站优化机构禹州网站的优化上海网站优化外包公司亲测有效驻马店优化网站报价为什么seo优化网站没有收录seo优化网站厂家新站网站优化方式张店网站优化软件安丘网站优化效果网站的优化就选火1星惠香蜜湖网络推广和网站优化福建外贸网站谷歌优化网站界面优化主要是什么房山公司网站排名优化巢湖网站优化有哪些宝山区网站优化价格费用seo网站优化报告新乡seo网站优化技术好seo优化服务器改网站兰州比较好的网站优化商城网站优化的软件汾阳市网站seo优化排名南充大企业网站优化贵阳网站优化推广公司聊城网站优化的关键点聚搜信息日照智能网站优化哪家便宜优化网站的基本因素朝阳网站优化的公司许昌优化网站排名哪家服务好网站域名优化优选大将军19网站优化alexa什么意思正规网站优化哪家强枣庄网站优化怎么样白银网站优化推广费用网站优化价钱是多少楚雄网站优化哪家好路桥seo网站优化网站产品优化鸭云速捷靠谱奉贤区百度网站优化方案海淀企业网站优化巩义网站seo优化多少钱太原个人网站优化平谷哪家网站优化好郑州免费优化网站长寿seo网站优化价格福建给网站优化具体做什么的外贸b2b网站优化用户如何做好网站优化资阳外贸网站优化随州网站seo优化家装类网站如何运维优化丰台优化网站公司网站排名优化软件优选火3星电商网站诊断及优化方案朝阳网站推广优化兰州网站整站优化怎么做的罗定网站优化软件重庆网站优化指导海南网站优化推广许昌网站排名优化哪里好前洲网站优化公司武夷山网站推广优化初期网站优化秦皇岛外贸网站优化优化网站方法只信b火19星夏津外贸网站优化重庆质量网站优化设计宜春怎么优化网站桐城网站优化哪家正规枣庄百度360搜狗网站优化推广标题优化的网站东莞网站优化哪个好网站文章优化怎么弄优化网站是什么岗位宣城企业网站推广优化网站平台怎么进行优化闵行区360网站优化金堂网站优化方案网站进行优化霸屏鹤山优化网站排名优化网站排名必选云速捷力荐清远网站seo关键词优化推广府谷县网站优化南通百度seo网站优化公司网站优化方案报价

                合作伙伴

                百姓标王

                龙岗网络公司
                深圳网站优化
                龙岗网站建设
                坪山网站建设
                百度标王推广
                天下网标王
                SEO优化按天计费
                SEO按天计费系统