egg.ts 用xml管理sql的一种思路

717 阅读2分钟

由于某些信仰原因,小组成员都是反"orm"人员,更喜欢在数据库gui软件中写好sql,然后复制到 egg 项目中,但是随着业务复杂度增加,egg的控制器,就会因为sql变得很丑,vscode也没找到很好扩展,可以在js中格式化sql语句,想到mybatisxml管理,于是就有了想法

效果

<!-- product.xml -->
<sql namespace="product">
  <select id="selectAllProduct">
    select * from product
  </select>
  <update id="updateProductNameById">
    update product set name = ? where id = ?
  </update>
</sql>

调用

image.png

代码提示怎么办?

借助egg-ts-helper的启发,在开发阶段,实时解析xml,并且生成d.ts,也就能做到很舒服的代码提示了

思路

设定 /app/mapper/为存储 sql的地方

  • egg 的Application对象的ready生命周期中扫描 mapper目录下的全部.xml文件
  • 解析 mapper目录 下全部 xml
  • 生成 d.ts, 确保编辑器代码提示

为什么不用 .ts 存? 从扩展性,语义化上,xml更适合吧!

实现

入口

在 egg 入口文件增加scanXML作为扫描 xml文件的入口方法,每次egg-bin热更新后,都会触发ready这个生命周期

export default (app: Application) => {
  app.beforeStart(() => {
   // 忽略⬇
    Bootstrap(app, { prefix: '/liuchu', useRole: false })
  })
  app.ready(async () => {
    scanXML('/app/mapper')
  })
  app.beforeClose(() => {})
}

解析 xml

用到的库 xml2js,具体用法不做赘述,看文档即可

const { readFile, writeFile } = require('fs/promises')
const xml2js = require('xml2js')
const parser = xml2js.Parser()
const path = require('path')

/**
 * 扫描XML存储的sql文件
 * @param {string} mapperDir sql 文件存储的文件夹 相对于 process.cwd()
 */
export const scanXML = (mapperDir: string = '/app/mapper') => {
  // 项目的根目录
  const src = path.join(process.cwd(), mapperDir)
  // 过滤非.xml文件
  const xmls = recurDir(src).filter(file => file.endsWith('.xml'))
  for (let i = 0; i < xmls.length; i++) {
    readFile(xmls[i]).then(data => {
      parser.parseString(data, (_, res) => {
        const { $: rootAttrs, ...rest } = res.sql
        // sql语句缺少id的报错提示
        if (!rootAttrs) throw new Error(`${path.basename(xmls[i])} root node[<sql id="?"><sql>] namespace required`)
        const { namespace } = rootAttrs
        const mapper: IMapper = {}
        for (const [k, v] of Object.entries(rest)) {
          if (!mapper[namespace]) mapper[namespace] = []
          ;(v as any).forEach(node => {
            // <sql></sql> 根节点缺少namespace属性的报错提示
            if (!node.$) throw new Error(`${path.basename(xmls[i])} node <${k} id="?"></${k}> id required`)
            const { _, $ } = node
            const { id } = $
            mapper[namespace].push({
              type: k,
              handlerName: id,
              value: _.replace(/\\n/, '').trim(),
            })
          })
        }
        // 自动生成 type.d.ts 的模板字符
        autoGenerateType(mapper, `${namespace}.d.ts`)
      })
    })
  }
}

IDE代码提示

xml文件中的根节点<sql namespace="product"></sql>,实时生成product.d.ts,写到/typings下, 例如:

type 直接写 sql字符,可以给到更明确的代码提示,省去跳转的麻烦

  declare namespace mapper {
    declare module product {
        
        type TselectAllProduct = 'select * from product';
        export const selectAllProduct: TselectAllProduct;
      
        type TupdateProductNameById = 'update product set name = ? where id = ?';
        export const updateProductNameById: TupdateProductNameById;
    }
  }

实现autoGenerateType方法也很简单

/**
 * 自动生成namespace.d.ts
 * @param mapper
 * @param name
 */
const autoGenerateType = (mapper: IMapper, name: string) => {
  const dest = path.join(process.cwd(), `typings/app/${name}.d.ts`)
  const templ = `
  declare namespace mapper {
    ${Object.keys(mapper).map(namespace => {
      return `declare module ${namespace} {
        ${renderNode(mapper[namespace])} 
    }`
    })}
  }
`
  writeFile(dest, templ, { encoding: 'utf8' })
}

/**
 * render sql node to template string
 * @param {TSqlHubNamespaceNode} nodes xml收集到的sql节点
 */
const renderNode = (nodes: TSqlHubNamespaceNode[]): string => {
  return nodes
    .map(node => {
      return `
        type T${node.handlerName} = '${node.value}';
        export const ${node.handlerName}: T${node.handlerName};
      `
    })
    .join('')
}

到这里基本也就可以实现IDE非常有好的sql代码提示,具体如何运用到生产,略。。。