# Alchemy第十周教程- 使用 Lens 协议创建去中心化 Twitter

By [Alice's Web3](https://paragraph.com/@horseverse) · 2022-09-25

---

在本课中，将学习：

*   如何使用 Apollo GraphQL 客户端设置 Next.js 应用程序
    
*   如何使用 Lens 协议 API 获取存储在 Polygon 区块链上的个人资料、帖子和其他数据
    
*   MintKudos API 简介——以便您可以将您的 PoK 代币集成到您的 dapp 中！
    
*   Lit 协议简介——如果您想加密某些帖子以仅显示给各个社区成员
    
*   如何使用 Repl.it 部署你的去中心化社交媒体应用程序前端网站
    
*   扩展此项目的多个挑战选项！
    

话不多说了，我们开始今天的课程吧。

1.设置依赖安装Apollo
--------------

我们今天的课程需要在VScode中去执行，首先我们需要建立一个项目

    # 创建一个road-to-lens 这是注释 不要输入命令行
    npx create-next-app road-to-lens
    # 安装graphql
    npm install @apollo/client graphql
    # 运行项目验证
    npm run dev
    

当你出现这样的结果，恭喜你已经成功完成第一步了。

![](https://storage.googleapis.com/papyrus_images/f2e6d03237bf7af023c18b697aeeaed092c0c70c5e5e40acc5b94007e508ee6e.png)

2.在 index.js 页面上使用 Lens 推荐的配置文件尝试 Apollo GraphQL
------------------------------------------------

### 2.1新建apollo-client.js

我们在当前项目新建一个apollo-client.js输入下面的内容：

    // ./apollo-client.js
    
    import { ApolloClient, InMemoryCache } from "@apollo/client";
    
    const client = new ApolloClient({
        uri: "https://api.lens.dev",
        cache: new InMemoryCache(),
    });
    
    export default client;
    

修改我们的/pages/\_app.js：

    // pages/_app.js
    
    import '../styles/globals.css'
    import { ApolloProvider } from "@apollo/client";
    import client from "../apollo-client";
    
    function MyApp({ Component, pageProps }) {
      return (
        <ApolloProvider client={client}>
          <Component {...pageProps} />
        </ApolloProvider>
      );
    }
    
    export default MyApp
    

修改/pages/index.js

    import { useQuery } from "@apollo/client";
    import recommendedProfilesQuery from '../queries/recommendedProfilesQuery.js';
    import Profile from '../components/Profile.js';
    
    export default function Home() {
      const {loading, error, data} = useQuery(recommendedProfilesQuery);
    
    
      if (loading) return 'Loading..';
      if (error) return `Error! ${error.message}`;
    
      return (
        <div>
          {data.recommendedProfiles.map((profile, index) => {
            console.log(`Profile ${index}:`, profile);
            return <Profile key={profile.id} profile={profile} displayFullProfile={false} />;
          })}
        </div>
      )
    }
    

新建查询js

    mkdir queries
    touch queries/recommendedProfilesQuery.js
    

将下面的代码写入recommendedProfilesQuery.js

    // queries/recommendedProfilesQuery.js
    
    import {gql} from '@apollo/client';
    
    export default gql`
      query RecommendedProfiles {
        recommendedProfiles {
              id
            name
            bio
            attributes {
              displayType
              traitType
              key
              value
            }
              followNftAddress
            metadata
            isDefault
            picture {
              ... on NftImage {
                contractAddress
                tokenId
                uri
                verified
              }
              ... on MediaSet {
                original {
                  url
                  mimeType
                }
              }
              __typename
            }
            handle
            coverPicture {
              ... on NftImage {
                contractAddress
                tokenId
                uri
                verified
              }
              ... on MediaSet {
                original {
                  url
                  mimeType
                }
              }
              __typename
            }
            ownedBy
            dispatcher {
              address
              canUseRelay
            }
            stats {
              totalFollowers
              totalFollowing
              totalPosts
              totalComments
              totalMirrors
              totalPublications
              totalCollects
            }
            followModule {
              ... on FeeFollowModuleSettings {
                type
                amount {
                  asset {
                    symbol
                    name
                    decimals
                    address
                  }
                  value
                }
                recipient
              }
              ... on ProfileFollowModuleSettings {
              type
              }
              ... on RevertFollowModuleSettings {
              type
              }
            }
        }
      }
    `;
    

新建组件js

    mkdir components
    touch components/Profile.js
    

将下面代码写入Profile.js

    // components/Profile.js
    
    import Link from "next/link";
    export default function Profile(props) {
      const profile = props.profile;
    
      // When displayFullProfile is true, we show more info.
      const displayFullProfile = props.displayFullProfile;
    
      return (
        <div className="p-8">
          <Link href={`/profile/${profile.id}`}>
            <div className="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl">
              <div className="md:flex">
                <div className="md:shrink-0">
                  {profile.picture ? (
                    <img
                      src={
                        profile.picture.original
                          ? profile.picture.original.url
                          : profile.picture.uri
                      }
                      className="h-48 w-full object-cover md:h-full md:w-48"
                    />
                  ) : (
                    <div
                      style={{
                        backgrondColor: "gray",
                      }}
                      className="h-48 w-full object-cover md:h-full md:w-48"
                    />
                  )}
                </div>
                <div className="p-8">
                  <div className="uppercase tracking-wide text-sm text-indigo-500 font-semibold">
                    {profile.handle}
                    {displayFullProfile &&
                      profile.name &&
                      " (" + profile.name + ")"}
                  </div>
                  <div className="block mt-1 text-sm leading-tight font-medium text-black hover:underline">
                    {profile.bio}
                  </div>
                  <div className="mt-2 text-sm text-slate-900">{profile.ownedBy}</div>
                  <p className="mt-2 text-xs text-slate-500">
                    following: {profile.stats.totalFollowing} followers:{" "}
                    {profile.stats.totalFollowers}
                  </p>
                </div>
              </div>
            </div>
          </Link>
        </div>
      );
    }
    

我们将项目运行起来出现下面的结果说明你已经离成功不远了，但是现在页面的样式太丑了，让我们来优化下。

![](https://storage.googleapis.com/papyrus_images/bf4f19eb1467b07d5b089725ce9aeb64dbb97d769cd5a15c44ee8a3affef06ac.png)

### 2.2优化页面样式

    # 安装 Tailwind
    npm install -D tailwindcss postcss autoprefixer
    # 当你执行完下面这个命令，系统会给你生成一个tailwind.config.js
    npx tailwindcss init -p
    

在生成的tailwind.config.js中写入一下内容：

    // tailwind.config.js
    
    module.exports = {
      content: [
        "./pages/**/*.{js,ts,jsx,tsx}",
        "./components/**/*.{js,ts,jsx,tsx}",
      ],
      theme: {
        extend: {},
      },
      plugins: [],
    }
    

同时将我们的globals.css文件末尾加入下面的内容：

    /* ./styles/globals.css */
    @tailwind base;
    @tailwind components;
    @tailwind utilities;
    

最终我们的结果展示如下：

![](https://storage.googleapis.com/papyrus_images/645311ec9ba9971e075888c39869d20d57c1b649ec96c2e8fde8f2d3a0276012.png)

3\. 创建个人资料页面
------------

首先我们创建个人资料文件夹：

    mkdir pages/profile
    road-to-lens % touch pages/profile/\[id\].js
    touch queries/fetchProfileQuery.js
    

在fetchProfileQuery.js中写入下面的代码：

    // queries/fetchProfileQuery.js
    
    import { gql } from '@apollo/client';
    
    export default gql`
    query($request: SingleProfileQueryRequest!) {
        profile(request: $request) {
            id
            name
            bio
            attributes {
              displayType
              traitType
              key
              value
            }
            followNftAddress
            metadata
            isDefault
            picture {
              ... on NftImage {
                contractAddress
                tokenId
                uri
                verified
              }
              ... on MediaSet {
                original {
                  url
                  mimeType
                }
              }
              __typename
            }
            handle
            coverPicture {
              ... on NftImage {
                contractAddress
                tokenId
                uri
                verified
              }
              ... on MediaSet {
                original {
                  url
                  mimeType
                }
              }
              __typename
            }
            ownedBy
            dispatcher {
              address
              canUseRelay
            }
            stats {
              totalFollowers
              totalFollowing
              totalPosts
              totalComments
              totalMirrors
              totalPublications
              totalCollects
            }
            followModule {
              ... on FeeFollowModuleSettings {
                type
                amount {
                  asset {
                    symbol
                    name
                    decimals
                    address
                  }
                  value
                }
                recipient
              }
              ... on ProfileFollowModuleSettings {
                type
              }
              ... on RevertFollowModuleSettings {
                type
              }
            }
        }
      }
    `;
    

在id.js文件中写入下面的代码：

    // pages/profile/[id].js
    
    import { useQuery } from "@apollo/client";
    import { useRouter } from "next/router";
    import fetchProfileQuery from "../../queries/fetchProfileQuery.js";
    
    import Profile from "../../components/Profile.js";
    
    export default function ProfilePage() {
      const router = useRouter();
      const { id } = router.query;
    
      console.log("fetching profile for", id);
      const { loading, error, data } = useQuery(fetchProfileQuery, {
        variables: { request: { profileId: id } },
      });
    
      if (loading) return "Loading..";
      if (error) return `Error! ${error.message}`;
    
      console.log("on profile page data: ", data);
    
      return <Profile profile={data.profile} displayFullProfile={true}/>
    }
    

当我们上面代码编写完毕后，可以输入下面的链接进行验证

*   [http://localhost:3000/profile/0x9752](http://localhost:3000/profile/0x9752)
    
*   [http://localhost:3000/profile/0x25c4](http://localhost:3000/profile/0x25c4)
    

![](https://storage.googleapis.com/papyrus_images/e7b5b7ff799e4efc737a047d817f8939409220c465d2a6c03f053171ccb2697b.png)

如果到目前为止一切顺利的话，你已经完成了一半了。

4.在个人资料页面上加载用户帖子
----------------

将fetchProfileQuery.js覆盖为下面的：

    import { gql } from "@apollo/client";
    
    export default gql`
      query (
        $request: SingleProfileQueryRequest!
        $publicationsRequest: PublicationsQueryRequest!
      ) {
        publications( request: $publicationsRequest) {
          items {
            __typename
            ... on Post {
              ...PostFields
            }
            ... on Comment {
              ...CommentFields
            }
            ... on Mirror {
              ...MirrorFields
            }
          }
          pageInfo {
            prev
            next
            totalCount
          }
        }
        profile(request: $request) {
          id
          name
          bio
          attributes {
            displayType
            traitType
            key
            value
          }
          followNftAddress
          metadata
          isDefault
          picture {
            ... on NftImage {
              contractAddress
              tokenId
              uri
              verified
            }
            ... on MediaSet {
              original {
                url
                mimeType
              }
            }
            __typename
          }
          handle
          coverPicture {
            ... on NftImage {
              contractAddress
              tokenId
              uri
              verified
            }
            ... on MediaSet {
              original {
                url
                mimeType
              }
            }
            __typename
          }
          ownedBy
          dispatcher {
            address
            canUseRelay
          }
          stats {
            totalFollowers
            totalFollowing
            totalPosts
            totalComments
            totalMirrors
            totalPublications
            totalCollects
          }
          followModule {
            ... on FeeFollowModuleSettings {
              type
              amount {
                asset {
                  symbol
                  name
                  decimals
                  address
                }
                value
              }
              recipient
            }
            ... on ProfileFollowModuleSettings {
              type
            }
            ... on RevertFollowModuleSettings {
              type
            }
          }
        }
      }
    
      fragment MediaFields on Media {
        url
        mimeType
      }
    
      fragment ProfileFields on Profile {
        id
        name
        bio
        attributes {
          displayType
          traitType
          key
          value
        }
        isFollowedByMe
        isFollowing(who: null)
        followNftAddress
        metadata
        isDefault
        handle
        picture {
          ... on NftImage {
            contractAddress
            tokenId
            uri
            verified
          }
          ... on MediaSet {
            original {
              ...MediaFields
            }
          }
        }
        coverPicture {
          ... on NftImage {
            contractAddress
            tokenId
            uri
            verified
          }
          ... on MediaSet {
            original {
              ...MediaFields
            }
          }
        }
        ownedBy
        dispatcher {
          address
        }
        stats {
          totalFollowers
          totalFollowing
          totalPosts
          totalComments
          totalMirrors
          totalPublications
          totalCollects
        }
        followModule {
          ... on FeeFollowModuleSettings {
            type
            amount {
              asset {
                name
                symbol
                decimals
                address
              }
              value
            }
            recipient
          }
          ... on ProfileFollowModuleSettings {
            type
          }
          ... on RevertFollowModuleSettings {
            type
          }
        }
      }
    
      fragment PublicationStatsFields on PublicationStats {
        totalAmountOfMirrors
        totalAmountOfCollects
        totalAmountOfComments
      }
    
      fragment MetadataOutputFields on MetadataOutput {
        name
        description
        content
        media {
          original {
            ...MediaFields
          }
        }
        attributes {
          displayType
          traitType
          value
        }
      }
    
      fragment Erc20Fields on Erc20 {
        name
        symbol
        decimals
        address
      }
    
      fragment CollectModuleFields on CollectModule {
        __typename
        ... on FreeCollectModuleSettings {
          type
          followerOnly
          contractAddress
        }
        ... on FeeCollectModuleSettings {
          type
          amount {
            asset {
              ...Erc20Fields
            }
            value
          }
          recipient
          referralFee
        }
        ... on LimitedFeeCollectModuleSettings {
          type
          collectLimit
          amount {
            asset {
              ...Erc20Fields
            }
            value
          }
          recipient
          referralFee
        }
        ... on LimitedTimedFeeCollectModuleSettings {
          type
          collectLimit
          amount {
            asset {
              ...Erc20Fields
            }
            value
          }
          recipient
          referralFee
          endTimestamp
        }
        ... on RevertCollectModuleSettings {
          type
        }
        ... on TimedFeeCollectModuleSettings {
          type
          amount {
            asset {
              ...Erc20Fields
            }
            value
          }
          recipient
          referralFee
          endTimestamp
        }
      }
    
      fragment PostFields on Post {
        id
        profile {
          ...ProfileFields
        }
        stats {
          ...PublicationStatsFields
        }
        metadata {
          ...MetadataOutputFields
        }
        createdAt
        collectModule {
          ...CollectModuleFields
        }
        referenceModule {
          ... on FollowOnlyReferenceModuleSettings {
            type
          }
        }
        appId
        hidden
        mirrors(by: null)
        hasCollectedByMe
      }
    
      fragment MirrorBaseFields on Mirror {
        id
        profile {
          ...ProfileFields
        }
        stats {
          ...PublicationStatsFields
        }
        metadata {
          ...MetadataOutputFields
        }
        createdAt
        collectModule {
          ...CollectModuleFields
        }
        referenceModule {
          ... on FollowOnlyReferenceModuleSettings {
            type
          }
        }
        appId
        hidden
        hasCollectedByMe
      }
    
      fragment MirrorFields on Mirror {
        ...MirrorBaseFields
        mirrorOf {
          ... on Post {
            ...PostFields
          }
          ... on Comment {
            ...CommentFields
          }
        }
      }
    
      fragment CommentBaseFields on Comment {
        id
        profile {
          ...ProfileFields
        }
        stats {
          ...PublicationStatsFields
        }
        metadata {
          ...MetadataOutputFields
        }
        createdAt
        collectModule {
          ...CollectModuleFields
        }
        referenceModule {
          ... on FollowOnlyReferenceModuleSettings {
            type
          }
        }
        appId
        hidden
        mirrors(by: null)
        hasCollectedByMe
      }
    
      fragment CommentFields on Comment {
        ...CommentBaseFields
        mainPost {
          ... on Post {
            ...PostFields
          }
          ... on Mirror {
            ...MirrorBaseFields
            mirrorOf {
              ... on Post {
                ...PostFields
              }
              ... on Comment {
                ...CommentMirrorOfFields
              }
            }
          }
        }
      }
    
      fragment CommentMirrorOfFields on Comment {
        ...CommentBaseFields
        mainPost {
          ... on Post {
            ...PostFields
          }
          ... on Mirror {
            ...MirrorBaseFields
          }
        }
      }
    `;
    

在components/Post.js 新建一个Post.js并且写入一下代码：

    // components/Post.js
    export default function Post(props) {
      const post = props.post;
    
      return (
        <div className="p-8">
          <div className="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl">
            <div className="md:flex">
              <div className="p-8">
                <p className="mt-2 text-xs text-slate-500 whitespace-pre-line">
                  {post.metadata.content}
                </p>
              </div>
            </div>
          </div>
        </div>
      );
    }
    

将\[id\].js覆盖为下面的：

    import { useQuery, useMutation } from "@apollo/client";
    import { useRouter } from "next/router";
    import fetchProfileQuery from "../../queries/fetchProfileQuery.js";
    import Profile from "../../components/Profile.js";
    import Post from "../../components/Post.js";
    
    export default function ProfilePage() {
      const router = useRouter();
      const { id } = router.query;
    
      console.log("fetching profile for", id);
      const { loading, error, data } = useQuery(fetchProfileQuery, {
        variables: {
          request: { profileId: id },
          publicationsRequest: {
            profileId: id,
            publicationTypes: ["POST"],
          },
        },
      });
    
      if (loading) return "Loading..";
      if (error) return `Error! ${error.message}`;
    
      return (
        <div className="flex flex-col p-8 items-center">
          <Profile profile={data.profile} displayFullProfile={true} />
          {data.publications.items.map((post, idx) => {
            return <Post key={idx} post={post}/>;
          })}
        </div>
      );
    }
    

验证结果 [http://localhost:3000/profile/0x28a2](http://localhost:3000/profile/0x28a2)，恭喜，你正在成为一个去中心化的社交媒体开发者。

---

*Originally published on [Alice's Web3](https://paragraph.com/@horseverse/alchemy-lens-twitter)*
