你還在為代碼中放入長長的模版字符串所苦惱嗎,如下圖代碼片段:

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
至此,這項工作基本告一段落
