Artikel singkat tentang bagaimana kami mengalami masalah sinkronisasi pekerjaan antara tim pengembangan klien dan server. Bagaimana kami menghubungkan Thrift untuk menyederhanakan interaksi antara tim kami.
Siapa yang peduli bagaimana kita melakukan ini, dan apa efek "samping" yang kita tangkap, silakan lihat di bawah kucing
Latar belakang
Pada awal 2017, ketika kami memulai proyek baru, kami memilih EmberJS sebagai frontend. Yang, hampir secara otomatis membuat kami bekerja pada skema REST ketika mengatur interaksi bagian klien dan server aplikasi. Karena
EmberData menyediakan alat yang mudah digunakan untuk memisahkan pekerjaan perintah backend dan frontend, dan menggunakan Adaptor memungkinkan Anda untuk memilih "protokol" interaksi.
Pada awalnya, semuanya baik-baik saja - Ember memberi kami kemampuan untuk mengimplementasikan emulasi permintaan server. Data untuk meniru model server dimasukkan ke file-file terpisah. Jika, di suatu tempat, kami mulai bekerja tanpa menggunakan Data Ember, maka Ember memungkinkan Anda untuk menulis emulator pengendali titik akhir terdekat dan mengembalikan data ini. Kami memiliki perjanjian bahwa pengembang backend harus membuat perubahan pada file-file ini untuk menjaga agar data tetap mutakhir agar pengembang frontend berfungsi dengan benar. Tapi seperti biasa, ketika semuanya dibangun di atas "perjanjian" (dan tidak ada alat untuk memverifikasi mereka), ada saatnya ketika "ada sesuatu yang salah."
Persyaratan baru tidak hanya memunculkan tampilan data baru pada klien, tetapi juga memperbarui model data lama. Yang pada akhirnya menyebabkan fakta bahwa menjaga sinkronisasi model pada server dan emulasi dalam file sumber klien menjadi sangat mahal. Sekarang, pengembangan bagian klien, sebagai aturan, dimulai setelah rintisan server siap. Dan pengembangan dilakukan di atas server produksi, dan ini mempersulit kerja tim dan meningkatkan waktu rilis fungsi baru.
Pengembangan proyek
Sekarang kami meninggalkan EmberJS demi VueJS. dan sebagai bagian dari keputusan tentang migrasi, kami mulai mencari solusi untuk masalah ini. Kriteria berikut dikembangkan:
- Kompatibilitas dengan versi protokol yang lebih lama dan lebih baru
- Kenyamanan maksimal untuk pengembang frontend saat bekerja "tanpa server"
- Pemisahan deskripsi API dari data uji
- Sinkronisasi tanda tangan panggilan mudah
- deskripsi tanda tangan yang jelas
- kemudahan modifikasi oleh pengembang frontend dan backend
- otonomi maksimum
- API yang diketik dengan baik sangat diinginkan. Yaitu deteksi tercepat dari fakta perubahan protokol
- Kemudahan pengujian logika server
- Integrasi dengan Spring di sisi server tanpa menari dengan rebana.
Implementasi
Setelah berpikir, diputuskan untuk berhenti di
Thrift . Ini memberi kami bahasa deskripsi API yang sederhana dan jelas.
namespace java ru.company.api namespace php ru.company.api namespace javascrip ru.company.api const string DIRECTORY_SERVICE= "directoryService" exception ObjectNotFoundException{ } struct AdvBreed { 1: string id, 2: string name, 3: optional string title } service DirectoryService { list<AdvBreed> loadBreeds() AdsBreed getAdvBreedById(1: string id) }
Untuk interaksi, kami menggunakan TMultiplexedProcessor, dapat diakses melalui TServlet, menggunakan TJSONProtocol. Saya harus berdansa sedikit untuk membuat Thrift ini berintegrasi dengan Spring. Untuk melakukan ini, saya harus membuat dan mendaftarkan Servlet di ServletContainer secara terprogram.
@Component class ThriftRegister : ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware, ServletContextAware { companion object { private const val unsecureAreaUrlPattern = "/api/v2/thrift-ns" private const val secureAreaUrlPattern = "/api/v2/thrift" } private var inited = false private lateinit var appContext:ApplicationContext private lateinit var servletContext:ServletContext override fun onApplicationEvent(event: ContextRefreshedEvent) { if (!inited) { initServletsAndFilters() inited = true } } private fun initServletsAndFilters() { registerOpenAreaServletAndFilter() registerSecureAreaServletAndFilter() } private fun registerSecureAreaServletAndFilter() { registerServletAndFilter(SecureAreaServlet::class.java, SecureAreaThriftFilter::class.java, secureAreaUrlPattern) } private fun registerOpenAreaServletAndFilter() { registerServletAndFilter(UnsecureAreaServlet::class.java, UnsecureAreaThriftFilter::class.java, unsecureAreaUrlPattern) } private fun registerServletAndFilter(servletClass:Class<out Servlet>, filterClass:Class<out Filter>, pattern:String) { val servletBean = appContext.getBean(servletClass) val addServlet = servletContext.addServlet(servletClass.simpleName, servletBean) addServlet.setLoadOnStartup(1) addServlet.addMapping(pattern) val filterBean = appContext.getBean(filterClass) val addFilter = servletContext.addFilter(filterClass.simpleName, filterBean) addFilter.addMappingForUrlPatterns(null, true, pattern) } override fun setApplicationContext(applicationContext: ApplicationContext) { appContext = applicationContext } override fun setServletContext(context: ServletContext) { this.servletContext = context } }
Apa yang harus diperhatikan di sini. Dalam kode ini, dua area layanan terbentuk. Terlindungi, yang tersedia di alamat "/ api / v2 / penghematan". Dan terbuka, tersedia di "/ api / v2 / thrift-ns". Untuk area ini, berbagai filter digunakan. Dalam kasus pertama, ketika mengakses layanan dengan cookie, objek dibentuk yang mendefinisikan pengguna yang membuat panggilan. Jika tidak mungkin untuk membentuk objek seperti itu, kesalahan 401 dilemparkan, yang diproses dengan benar di sisi klien. Dalam kasus kedua, filter melewatkan semua permintaan untuk layanan, dan jika itu menentukan bahwa otorisasi telah terjadi, maka setelah operasi selesai, itu mengisi cookie dengan informasi yang diperlukan sehingga dapat membuat permintaan ke area yang dilindungi.
Untuk menghubungkan layanan baru, Anda harus menulis sedikit kode tambahan.
@Component class DirectoryServiceProcessor @Autowired constructor(handler: DirectoryService.Iface): DirectoryService.Processor<DirectoryService.Iface>(handler)
Dan daftarkan prosesor
@Component class SecureMultiplexingProcessor @Autowired constructor(dsProcessor: DirectoryServiceProcessor) : TMultiplexedProcessor() }
Bagian terakhir dari kode dapat disederhanakan dengan menggantung antarmuka tambahan pada semua prosesor, yang akan memungkinkan Anda untuk segera mendapatkan daftar prosesor dengan satu parameter desainer, dan telah memberikan tanggung jawab untuk nilai kunci akses ke prosesor ke prosesor itu sendiri.
Bekerja dalam mode "tidak ada server" telah mengalami sedikit perubahan. Pengembang bagian frontend membuat proposal bahwa mereka akan bekerja pada server rintisan PHP. Mereka sendiri menghasilkan kelas untuk server mereka yang mengimplementasikan tanda tangan untuk versi protokol yang diinginkan. Dan mereka mengimplementasikan server dengan set data yang diperlukan. Semua ini memungkinkan mereka untuk bekerja sebelum pengembang sisi server menyelesaikan pekerjaan mereka.
Titik pemrosesan utama di sisi klien adalah penghematan-plugin yang ditulis oleh kami.
import store from '../../store' import { UNAUTHORIZED } from '../../store/actions/auth' const thrift = require('thrift') export default { install (Vue, options) { const DirectoryService = require('./gen-nodejs/DirectoryService') let _options = { transport: thrift.TBufferedTransport, protocol: thrift.TJSONProtocol, path: '/api/v2/thrift', https: location.protocol === 'https:' } let _optionsOpen = { ... } const XHRConnectionError = (_status) => { if (_status === 0) { .... } else if (_status >= 400) { if (_status === 401) { store.dispatch(UNAUTHORIZED) } ... } } let bufers = {} thrift.XHRConnection.prototype.flush = function () { var self = this if (this.url === undefined || this.url === '') { return this.send_buf } var xreq = this.getXmlHttpRequestObject() if (xreq.overrideMimeType) { xreq.overrideMimeType('application/json') } xreq.onreadystatechange = function () { if (this.readyState === 4) { if (this.status === 200) { self.setRecvBuffer(this.responseText) } else { if (this.status === 404 || this.status >= 500) {... } else {... } } } } xreq.open('POST', this.url, true) Object.keys(this.headers).forEach(function (headerKey) { xreq.setRequestHeader(headerKey, self.headers[headerKey]) }) if (process.env.NODE_ENV === 'development') { let sendBuf = JSON.parse(this.send_buf) bufers[sendBuf[3]] = this.send_buf xreq.seqid = sendBuf[3] } xreq.send(this.send_buf) } const mp = new thrift.Multiplexer() const connectionHostName = process.env.THRIFT_HOST ? process.env.THRIFT_HOST : location.hostname const connectionPort = process.env.THRIFT_PORT ? process.env.THRIFT_PORT : location.port const connection = thrift.createXHRConnection(connectionHostName, connectionPort, _options) const connectionOpen = thrift.createXHRConnection(connectionHostName, connectionPort, _optionsOpen) Vue.prototype.$ThriftPlugin = { DirectoryService: mp.createClient('directoryService', DirectoryService, connectionOpen), } } }
Agar plugin ini berfungsi dengan benar, Anda harus menghubungkan kelas-kelas yang dihasilkan.
Metode panggilan server pada klien adalah sebagai berikut:
thriftPlugin.DirectoryService.loadBreeds() .then(_response => { ... }) .catch(error => { ... }) })
Di sini saya tidak mempelajari fitur VueJS itu sendiri, di mana sudah benar untuk menjaga kode yang memanggil server. Kode ini dapat digunakan baik di dalam komponen dan di dalam rute dan di dalam Vuex-action.
Ketika bekerja dengan sisi klien, ada beberapa batasan yang harus diperhitungkan setelah migrasi mental dari integrasi penghematan internal.
- Klien Javascript tidak mengenali nilai null. Oleh karena itu, untuk bidang yang bisa nol, Anda harus menentukan bendera opsional. Dalam hal ini, klien akan menerima nilai ini dengan benar.
- Javascript tidak tahu cara bekerja dengan nilai yang panjang, jadi semua pengidentifikasi integer harus dilemparkan ke string di sisi server
Kesimpulan
Transisi ke Thrift memungkinkan kami untuk memecahkan masalah yang ada dalam interaksi antara pengembangan server dan klien saat bekerja pada versi antarmuka yang lama. Diizinkan untuk memungkinkan pemrosesan kesalahan global di satu tempat.
Pada saat yang sama, dengan bonus tambahan, karena pengetikan API yang ketat, dan oleh karena itu aturan ketat untuk serialisasi / deserialisasi data, kami menerima peningkatan ~ 30% pada saat interaksi pada klien dan server untuk sebagian besar permintaan (ketika membandingkan permintaan yang sama melalui interaksi REST dan THRIFT, dari saat permintaan dikirim ke server hingga respons diterima)