头图

理论基础:

在软件开发分层设计中, resolver 是一个关键组件,其主要作用是负责从请求中获取数据并进行业务逻辑处理,然后将处理结果传递到下一个层。例如,在 GraphQL 中, resolver 的角色尤为重要,它介于客户端请求和数据库查询之间,决定了数据的获取和处理方式。

什么是 Resolver?

Resolver 一词,直译为解决器。在软件开发背景下, resolver 是一段用来解析某些数据的代码逻辑。他们的主要职责是处理数据请求、应用业务逻辑,并返回最终结果。在前后端分离的架构中,以及使用 GraphQL 的场景中, resolver 是必不可少的核心部分。

有人可能会问,为何不能直接从数据库获取数据然后直接返回?原因在于直接操作数据层可能违背分层设计原则,导致体系结构混乱,难以维护和扩展。 resolver 的存在解决了这些问题,它独立处理业务逻辑,使得数据层和表现层分离,各司其职。

Resolver 的具体实践

为了更清晰地了解 resolver 的实际应用,下面以一个电子商务平台为例,说明 resolver 在处理用户请求中的角色。

用户注册功能

假设我们需要实现一个用户注册功能。用户通过前端提交注册信息,后端需要接收到该信息,并存储在数据库中。数据需要经过校验,比如用户名不能重复,密码要符合安全规范等。

前端请求示例

{
    "query": `
        mutation {
            register(
                username: "new_user",
                password: "SafePassword123"
            ) {
                user {
                    id
                    username
                }
                token
            }
        }
    `
}

Resolver 在后端的实现

我们将这个功能的实现分为以下几个步骤:

  1. 接收请求:首先由一个通用请求接收器接收到前端的请求。
  2. 数据校验和处理:利用 resolver 校验用户名和密码,处理业务逻辑,比如加密密码等。
  3. 存储数据:数据校验和处理完成后,将信息存储在数据库中。
  4. 返回结果:最终将处理结果返回给前端。

后端 resolver 示例 (JavaScript)

const bcrypt = require('bcrypt');

const resolvers = {
    Mutation: {
        register: async (_, { username, password }, { db }) => {
            // 校验用户名是否已存在
            const existingUser = await db.user.findUnique({ where: { username } });
            if (existingUser) {
                throw new Error('Username is already taken');
            }

            // 加密用户密码
            const hashedPassword = await bcrypt.hash(password, 10);

            // 将新用户信息存储在数据库中
            const newUser = await db.user.create({
                data: {
                    username,
                    password: hashedPassword
                }
            });
            
            // 返回新注册的用户信息
            return {
                user: {
                    id: newUser.id,
                    username: newUser.username
                },
                token: generateToken(newUser) // 假设我们有一个函数来生成 token
            };
        }
    }
};

module.exports = resolvers;

分层设计中的重要性

使用 resolver 不仅能清晰划分数据处理和业务逻辑,还提升了代码的维护性和扩展性。想象一个复杂的系统,如果每个模块的设计彼此纠缠,每增加一个功能点可能就需要改动很多地方。而通过 resolver,我可以利用其单一职责原则,仅在相关 resolver 中进行修改,而不用担心其他部分代码被改变。

在上述例子中, resolver 负责所有的用户注册逻辑:从接收输入、校验、加密再到存储和返回结果。数据库操作被隔离在 resolver 内部,不会直接暴露给其他层,确保了不同层次间的依赖最小化。

复杂业务场景中的 Resolver

当遇到更复杂的业务场景时, resolver 的设计就显得尤为重要。以商品推荐系统为例。我们需要一个推荐系统展示给用户与其浏览过的商品类型相关的推荐商品列表。

用户请求示例

{
    "query": `
        query {
            recommendedProducts(userId: 123) {
                id
                name
                price
                relatedProducts {
                    id
                    name
                }
            }
        }
    `
}

推荐系统 Resolver 的设计

const resolvers = {
    Query: {
        recommendedProducts: async (_, { userId }, { db }) => {
            // 获取用户浏览记录
            const userHistory = await db.history.findMany({ where: { userId } });
            
            if (!userHistory.length) {
                throw new Error('No browsing history found for this user');
            }

            const viewedProductIds = userHistory.map(item => item.productId);

            // 获取相关商品
            const relatedProducts = await db.product.findMany({
                where: { id: { in: viewedProductIds } }
            });

            // 复杂推荐逻辑,例如基于用户浏览频率、商品相似性等,可以在这里实现
            const recommendedProducts = applyRecommendationAlgorithm(relatedProducts);

            return recommendedProducts.map(product => ({
                id: product.id,
                name: product.name,
                price: product.price,
                relatedProducts: fetchRelatedProducts(product.id) // 假设 fetchRelatedProducts 是另一个 resolver
            }));
        }
    }
};

module.exports = resolvers;

这段代码展示了一个复杂的商场场景,其中一个 resolver 处理多个数据源的逻辑。这部分代码一方面从用户历史记录获取数据,另一方面应用复杂的推荐算法返回结果。这样的设计确保了代码的可读性和可维护性,可以较为轻松地修改推荐算法而不会影响其他系统功能。

真实世界案例研究

考虑 GitHub 在实现 GraphQL API 时的示例。 GitHub 提供了丰富的数据接口,从用户信息、仓库信息到提交记录等。为了有效响应各种查询请求,团队设计了大量的 resolver 来处理各类复杂逻辑。

使用 GitHub GraphQL API 查询用户仓库信息

用户请求示例

{
    "query": `
        query {
            user(login: "octocat") {
                repositories(first: 10) {
                    nodes {
                        name
                        description
                        primaryLanguage {
                            name
                        }
                    }
                }
            }
        }
    `
}

这里的请求是查询用户名为 octocat 的用户的前十个仓库。你可以想象在 GitHub 后端这部分的 resolver 是如何处理用户数据、仓库信息的。

简化后的后端 resolver 实现 (TypeScript)

const resolvers = {
    Query: {
        user: async (_, { login }, { db }) => {
            // 查找用户信息
            const user = await db.user.findUnique({ where: { login } });
            if (!user) {
                throw new Error('User not found');
            }
            return user;
        }
    },
    User: {
        repositories: async (user, { first }, { db }) => {
            // 查找用户的 repositories 信息
            const repositories = await db.repository.findMany({
                where: { ownerId: user.id },
                take: first
            });
            return {
                nodes: repositories
            };
        }
    },
    Repository: {
        primaryLanguage: async (repository, _, { db }) => {
            // 查找 repository 的 primary language 信息
            const primaryLanguage = await db.language.findUnique({
                where: { id: repository.primaryLanguageId }
            });
            return primaryLanguage;
        }
    }
};

export default resolvers;

在这段代码中,主要亮点在于 resolver 如何解耦不同的数据查询逻辑。User resolver 查询用户基本信息,User.repositories resolver 进一步查询用户的仓库,Repository.primaryLanguage resolver 再次细化到具体仓库的主要编程语言。这种层层解耦的方式不仅提升了系统的可维护性,还提高了代码的复用性。这种分层设计非常适合大规模系统, GitHub 正是通过这种方式来管理其庞大的数据量和业务逻辑的。

结论和建议

resolver 在软件分层设计中扮演重要的桥梁角色,其最主要的功能是处理业务逻辑和数据交互。通过上面例子和案例的介绍,可以看出 resolver 在不同场景中的应用和重要性。它们不仅分担了复杂的业务逻辑,也为系统的可扩展性和维护性提供了坚实的基础。

在设计和实现 resolver 时,推荐以下几点:

  1. 单一职责原则:每个 resolver 只负责一个业务逻辑模块,这样能确保各模块间的松耦合。
  2. 错误处理:在 resolver 内部进行详细的错误处理,记录日志并抛出合适的错误信息,以便于调试和用户体验。
  3. 结合缓存:对于频繁访问或计算量大的 resolver,可以结合缓存机制提高效率。
  4. 统一数据格式:确保 resolver 返回的数据格式一致,有利于前端显示和后端进一步处理。

index.html 里的设置:

默认加载:

跳转到明细页面之后,title 值跟着变化:

http://localhost:4200/electronics-spa/en/USD/product/300938/p...

入口是 JavaScript 文件:

拼装出最后显示在 UI 上的 title 值:

再次 F8 之后:

从后台加载的 product 数据里提取出各种属性:


注销
1k 声望1.6k 粉丝

invalid