Free Arch: babel as a service

哈德韦

共 6119字,需浏览 13分钟

 ·

2022-01-01 05:46

在昨天分享的《FreeArch: 将 React 教程的井字棋游戏搬到微信小程序》里提到的,通过实现一个 react-view,可以部分解决个人版微信小程序不能使用 webview 的缺憾。



虽然完全可以在前端使用 babel standalone,动态转译 tsx 代码。但是由于 babel-standlone 压缩后的体积,也超过了小程序的代码限制,所以放在前端,只能在开发模式使用,没有办法发布。

于是就要后端来提供 babel 服务了。可能网上有很多类似这样的服务,但为了更好的定制化,还是自己写一个吧。利用万能 BFF,分分钟部署一个。



在线演示


https://sls.pa-ca.me/nest/graphql


先看一下,直接转译一个最简单的:



考虑到最终使用场景,是需要转译 replit (其实代码保存在 GitHub)的源文件,因为还提供了指定源文件的 url 进行转译的功能:



万能 BFF 源代码

https://github.com/Jeff-Tian/serverless-space/tree/main/src/babel-service


项目背景

项目采用了 nestjs 框架,对外提供 GraphQL 服务。nestjs graphql 项目,基本上是 module -> resolver -> service 这样的分层架构。


实现步骤

对于要实现一个 babel service,在 nest 项目中就是添加一个 babel service module,然后再添加一个 babel resolver 接收前端的请求,最后由 service 完成转译。为了省事儿,准备直接在项目里放置一个 babel.min.js 文件,用 require 方式获取 Babel 对象。


测试先行

尽管大概思路是非常清楚和简单粗暴,但是实践起来,还是要测试先行。实际的 TDD 过程就不详解了,为了叙述省事儿,略过了很多细枝末节。但是先写测试,再写实现,是一个最简略的版本。


自动化单元测试

首先想到的是,我们的 service,应该具备转译指定的代码的能力,即输入源代码,输出转译后的代码;然后,需要有从 url 转译的能力;最后,是一个特别定制化的需求,可以在指定 url 之后,添加一点额外代码后再进行转译。一共是 3 个测试用例:


https://github.com/Jeff-Tian/serverless-space/blob/main/src/babel-service/babel.service.spec.ts

import { BabelService } from "./babel.service";import { testTargetUrl, transformedText } from "../test/constants";import axios from 'axios'

describe('babel', () => { const mockHttpService = { get: (url) => ({ toPromise: () => axios.get(url) }) } as any

const sut = new BabelService(mockHttpService);

it('transforms', async () => { const res = await sut.transform('class A {}') expect(res).toStrictEqual(transformedText) })

it('transforms from url', async () => { const res = await sut.transformFromUrl(testTargetUrl) expect(res).toMatch(/"use strict";/) })

it('transform from url with extra', async () => { const res = await sut.transformFromUrl(testTargetUrl, "ReactDOM.render(, document.getElementById('root'))")

expect(res).toMatch(/"use strict";/) })})


实现上,首先把 babel.min.js 放在了项目目录里:


https://github.com/Jeff-Tian/serverless-space/blob/main/src/babel-service/babel.min.js


接着在 babel service 里引用它,并实现相应的测试用例中描述的功能:


https://github.com/Jeff-Tian/serverless-space/blob/main/src/babel-service/babel.service.ts

import {HttpService, Injectable} from "@nestjs/common";

const Babel = require('./babel.min.js')

@Injectable()export class BabelService { constructor(private readonly httpService: HttpService) {

}

async transform(code) { return Babel.transform(code.replace(/import.+;/g, '').replace(/export/g, ''), { presets: ['env', 'react'], plugins: [] })?.code?.replace(/"div"/g, '"view"').replace(/"ol"/g, '"view"') }

async transformFromUrl(url, extra = '') { const {data: code} = await this.httpService.get(url).toPromise()

return this.transform(code + extra) }}


自动化端到端测试

端到端测试,是描述了当前端发出的这样的请求,那么返回这样的响应的测试文件。这里写了两个用例,分别对应于单元测试中的前两个用例:


https://github.com/Jeff-Tian/serverless-space/blob/main/e2e/babel.e2e-spec.ts

import {INestApplication} from "@nestjs/common"import {Test} from "@nestjs/testing"import request from "supertest"import {AppModule} from "../src/app.module"import {testTargetUrl, transformedText} from '../src/test/constants'

jest.setTimeout(50000)

describe('Babel', () => { let app: INestApplication

beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [AppModule], }) .compile()

app = moduleRef.createNestApplication() await app.init() })

it('transforms', async () => { return request(app.getHttpServer()) .post('/graphql') .send({ query: `query transformTsx { transform (sourceCode: "class A {}") { text } }` }) .expect({ data: { transform: { text: transformedText } } }) .expect(200) })

it('transforms from url', async () => { return request(app.getHttpServer()) .post('/graphql') .send({ query: `query transformTsx { transform (url: "${testTargetUrl}") { text } }` }) .expect(res => { expect(res.body).toMatchObject({data: {transform: {text: /"use strict";/}}}) }) .expect(200) })})

要实现这个,就定义了新的 GraphQL 的schema(使用 code first 方式,实现上是写了一个 model 文件,本质上是一个 typescript 类),并分别添加了 module 和 resolver。


在一开始跑第一个端到端测试时,失败是理所当然。后来添加了 module 和 resolver 之后,第一个用例还是失败了!这时才知道,忘了在入口的 module 中引入我们的 babel module。


https://github.com/Jeff-Tian/serverless-space/commit/f665e8afcf0354a8fe2363cfe6015483ee2554a5#diff-089f4f2474b64391c42b6e66aed33977e132058d92108f0a63234a7862e1f8b8

@Module({-    imports: [CatsModule, RecipesModule, YuqueModule, ZhihuModule, GraphqlPluginModule, GraphQLModule.forRoot(graphqlOptions)],+    imports: [CatsModule, RecipesModule, YuqueModule, ZhihuModule, BabelModule, GraphqlPluginModule, GraphQLModule.forRoot(graphqlOptions)],})export class AppModule {}


看,这就是自动化测试的价值。没有测试,很容易遗漏一些环节,导致发布上线后不能如期工具,最终只能由用户来报告问题了。自动化测试可以在代码部署前快速发现问题,但并不是说有了自动化测试之后,就不需要人肉测试了,但是人肉只需要做些冒烟验证工作。


人肉冒烟测试


在单元测试和端到端测试都通过之后,部署上线了。然后人肉验证,发现挂了!

这是该项目后期需要改进的地方。明明端到端测试都过了,上线后却挂了!不过这个概率很小,在以前的迭代过程中从来没有发生过这样的问题。


这次挂掉的原因是本次迭代里有一个骚操作,即在 src 下加入了一个 babel.min.js 文件。跑自动化测试时,是用的 src 下的文件。但是部署上线后,却是 dist 目录中的文件,并且这个 babel.min.js 文件在编译过程(nest build)中,并没有从 src 自动拷贝到 dist 目录下,导致上线后找不到这个文件!


以前没有出现过,因为总是添加 typescript 文件,它们都在 nest build 过程中自动变成 js 形式生成在 dist 目录下了。


为了迅速修复,在 package.json 的 build 脚本上添加了将 babel.min.js 拷贝到 dist 目录下的步骤:


https://github.com/Jeff-Tian/serverless-space/commit/e4ee6de47a146a772f4fe8548134d361ce3f1806#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519

-    "build": "nest build",+    "build": "nest build&&cp src/babel-service/babel.min.js dist/src/babel-service/babel.min.js",



再次线上冒烟测试,如演示部分所示,通过。


项目待改进点

加入自动化冒烟测试?这种测试是通过运行编译后的代码来运行。


要么,直接找到 nest 配置,可以自动将 js 文件在编译过程中拷贝到 dist 中的对应位置。


要么,不用这种 babel.min.js 文件,而且采用引用 babel npm 包的方式。




浏览 52
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报