如何引入.graphql文件并优雅的使用fragment

你还在为代码中放入长长的模版字符串所苦恼吗,如下图代码片段:

ps:这个是grqphql client在nodejs后端项目的实践,如果你是在前端使用graphql,并使用了webpack,那么这些问题你都不用担心,因为有现成的轮子供你使用,参见相关loader:https://github.com/apollographql/graphql-tag/blob/master/loader.js,

由于项目开发紧张,我们最开始就是采用这图上这种模式,查询语句和业务代码全放在一起,结果是代码阅读和修改极其麻烦,且查询语句没有字段类型提示,graphql提供的fragment也不能使用(这个特性可以复用很多相同的片段)。

 

随着业务的不断增加,这个问题越来越凸显,我觉得必须想个办法处理一下,思路是:将graphql查询语句抽出来放在以.grqphql结尾的文件中,然后需要的时候再引入进来。webstorm有个插件正好可以实现语法高亮和类型提示,甚至可以再ide里面进行查询,参考:https://plugins.jetbrains.com/plugin/8097-js-graphql。但是问题来了,怎么再业务代码里面引入这个.graphql文件呢? 直接require肯定是不行的,因为这不是js或c++模块,这个确实也有现成的轮子,见:https://github.com/prisma/graphql-import, 但是这个工具在typescript环境下却有不少问题,见相关issue,怎么办呢?业务又催得紧,然后就用了最简单粗暴的方法: fs.readFileSync('./user.graphql','utf8'), 虽然不够优雅但也解决了燃眉之急。

 

上面这个办法虽然解决了查询语句和业务代码耦合在一起的问题,但是依然不能使用fragment,随着查询语句越来越多,很多片段都是一样的,后来更新的时候不得不同时修改几处代码,我想实现的效果是将fragment也抽离出来放在以.grqphql结尾的文件中,然后再另一个graphql文件中引入,最终拼在一起返回给业务代码

// a.grapqhl
fragment info on User { name mail }
// b.graphql
#import 'a.graphql'
query user{
   queryUser {
       ...info
    } 
}
// c.js

const b = loadGql('./b.graphql')

返回的b应该是个字符串,像下面这这样子:

fragment info on User{
    name
    mail
}

query user {
   queryUser{
        ....info
    }
}    

 

那么loadGql改怎么实现呢?google一番后,发现有个轮子可以参考下: https://github.com/samsarahq/graphql-loader/blob/master/src/loader.ts 但是这轮子需要配合webpack,不能直接在nodejs环境下直接使用,那就把它改造一下吧,上改造后的代码:

import { validate as graphqlValidate } from "graphql/validation/validate"
import { resolve, join, dirname } from "path"
import { Stats, writeFile,readFileSync, readFile } from "fs"
import pify = require("pify")
import {
    DocumentNode,
    DefinitionNode,
    print as graphqlPrint,
    parse as graphqlParse,
    Source,
    visit,
} from "graphql"

export default function loadGql(filePath: string): string | null {
    if (!filePath) return null
    try {
        const source = readFileSync(filePath, 'utf8')
        if(!source) return null
        const document = loadSource(source, filePath)

        const content = graphqlPrint(document)
        return content
    } catch (err) {
        console.log(err)
        return null
    }
}

function loadSource(
    source: string,
    filePath: string,
) {
    let document: any = graphqlParse(new Source(source, "GraphQL/file"))
    document = extractImports(source, document, filePath)
    return document
}

async function stat(
    loader: any,
    filePath: string,
): Promise<Stats> {
    const fsStat: (path: string) => Promise<Stats> = pify(
        loader.fs.stat.bind(loader.fs),
    )
    return fsStat(filePath)
}


function extractImports(source: string, document: DocumentNode, filePath: string): DocumentNode {
    const lines = source.split(/(\r\n|\r|\n)/)

    const imports: Array<string> = []
    lines.forEach(line => {
        // Find lines that match syntax with `#import "<file>"`
        if (line[0] !== "#") {
            return
        }

        const comment = line.slice(1).split(" ")
        if (comment[0] !== "import") {
            return
        }

        const filePathMatch = comment[1] && comment[1].match(/^[\"\'](.+)[\"\']/)
        if (!filePathMatch || !filePathMatch.length) {
            throw new Error("#import statement must specify a quoted file path")
        }

        const itemPath = resolve(dirname(filePath), filePathMatch[1])
        imports.push(itemPath)
    })

    const files = imports
    const contents = files.map(path => [
        readFileSync(path, 'utf8'),
        path,
    ])

    const nodes = contents.map(([content, fileContext]) => {
            return loadSource(content, fileContext)
        }
    )

    const fragmentDefinitions = nodes.reduce((defs, node) => {
        defs.push(...node.definitions)
        return defs
    }, [] as DefinitionNode[])

    const newAst = visit(document, {
        enter(node, key, parent, path, ancestors) {
            if (node.kind === 'Document') {
                const documentNode: DocumentNode = {
                    definitions: [...fragmentDefinitions, ...node.definitions],
                    kind: 'Document',
                }
                return documentNode
            }
            return node
        },
    })

    return newAst
}

ps:代码为typescript,使用需转换成js

至此,这项工作基本告一段落

posted @ 2019-03-03 13:27  WinjayYu  阅读(3062)  评论(0编辑  收藏  举报