Fiverr สร้างความสมดุลระหว่างการแบ่งปันทรัพยากรและความเป็นอิสระของส่วนประกอบในระบบส่วนหน้าแบบกระจายอำนาจ
สถาปัตยกรรมส่วนหน้าของเราที่ Fiverr กำหนดแอปพลิเคชันเกตเวย์หลายรายการ โดยให้บริการประสบการณ์ "แนวตั้ง" แต่ละประเภทธุรกิจจะสร้างหน้าเว็บจากหลายองค์ประกอบจากแหล่งที่มาที่หลากหลาย ซึ่งมีสภาพแวดล้อมรันไทม์เดียวกัน — หน้าต่าง หรือ ขอบเขตทั่วโลก
ในองค์กรของเรา ส่วนประกอบจำนวนมากเหล่านี้ใช้สแต็กเทคโนโลยีที่คล้ายกันอย่างยิ่ง (React, Redux, Lodash…) แต่เรายังคงต้องการให้พวกมันเป็นอิสระ และไม่พึ่งพาทรัพยากรที่อยู่นอกเหนือการควบคุม ส่วนประกอบบางอย่างอาจคงสภาพเดิมโดยไม่มีการบำรุงรักษาเป็นเวลานาน ในขณะที่ส่วนประกอบอื่นๆ มีการเปลี่ยนแปลงบ่อยครั้ง — ส่วนประกอบและคุณสมบัติใหม่ๆ ถูกสร้างขึ้นตลอดเวลา เราต้องการรองรับพวกเขาทั้งหมด
จะมีความขัดแย้งอยู่เสมอระหว่างสภาพแวดล้อมที่รองรับโค้ดแบบเดิมและความปรารถนาที่จะสร้างสรรค์สิ่งใหม่ ๆ ด้วยเทคโนโลยีใหม่ ๆ
สถาปัตยกรรมแพลตฟอร์มของเราได้พัฒนาไปสู่โครงสร้างที่ฉันรู้สึกว่าสามารถบำรุงรักษาได้ มีประสิทธิภาพ และช่วยให้สามารถพัฒนาได้ง่าย
ทั่วทั้งระบบเรารับรู้ถึงทรัพยากรที่ใช้บ่อยที่สุดและจัดหมวดหมู่ออกเป็น 3 กลุ่มที่แตกต่างกัน:
- ผู้จำหน่าย — ไลบรารีและเฟรมเวิร์กขนาดใหญ่ ซึ่งมักจะเป็นโครงการโอเพ่นซอร์สที่รู้จักกันดี ซึ่งมีรอบการเปิดตัวเวอร์ชันหลักที่ต่ำพอสมควร
- การเกิดซ้ำสูง—โมดูลที่ได้รับการปรับแต่งให้ตรงตามความต้องการในการพัฒนาของเราเป็นอย่างดีจนกลายเป็นลักษณะที่สองของนักพัฒนาและคาดว่าจะพร้อมใช้งานในทุกสภาพแวดล้อม รวมถึง i18n ของเรา โซลูชันและผู้รายงานสถิติเช่นการติดตามด้านเทคนิคและธุรกิจ
- ยูทิลิตี้เบ็ดเตล็ด — ยูทิลิตี้เหล่านี้อาจมีการนำกลับมาใช้ใหม่ได้หลายระดับ ซึ่งรวมถึงฟังก์ชันตัวช่วย คลาสที่มีประโยชน์ ตรรกะทางธุรกิจที่ทำซ้ำได้ หรือการดำเนินการทางเทคนิค ซึ่งไม่เกี่ยวข้องกับฟีเจอร์หรือโดเมนใดๆ อย่างเด็ดขาด
1. ผู้ขาย
แนวทางของเราในการจัดหาทรัพยากรของผู้ขายนั้นเรียบง่าย: มีแนวโน้มอย่างมากที่หน้าเดียวจะประกอบด้วยหลายโปรแกรมที่ต้องการการพึ่งพาที่เหมือนกัน ซึ่งมักจะเป็นโปรแกรมขนาดใหญ่ เราต้องการให้แน่ใจว่าพวกเขาจะสามารถใช้ทรัพยากรเดียวกันได้ และเพื่อทำเช่นนั้น เราจำเป็นต้องทำให้ทรัพยากรเหล่านี้พร้อมใช้งานทั่วโลก แนวทางนี้มีไว้เพื่อสร้างแคชเบราว์เซอร์ที่เหมาะสมที่สุดและการคอมไพล์ (JIT) น้อยที่สุด
คอมโพเนนต์ประกาศการขึ้นต่อกันที่พวกเขาตั้งใจจะใช้จากรายการที่กำหนดไว้ล่วงหน้า (รายการนั้นจะถูกเก็บรักษาไว้เป็นการขึ้นต่อกันทั่วไป) เป็นสัญญาประเภทหนึ่งระหว่างส่วนประกอบและบริการ ตัวอย่างเช่น; องค์ประกอบ A จะประกาศ: react, react-dom, lodash
และองค์ประกอบ B จะประกาศ react, react-dom, redux
พวกเขายังตรวจสอบให้แน่ใจว่าได้รวมการขึ้นต่อกันเหล่านั้นเป็น "Webpack ภายนอก" สำหรับบิลด์ที่กำหนดเป้าหมายเบราว์เซอร์ที่เกี่ยวข้อง ดังนั้นจึงไม่รวมอยู่ในโซลูชันที่รวมกลุ่มของโค้ด สุดท้ายนี้ พวกเขาต้องถือว่าการอัปเกรดในอนาคตภายในเวอร์ชันหลักนั้นอยู่นอกเหนือการควบคุม ดังนั้นฟีเจอร์ทดลองมักจะอยู่นอกเหนือการควบคุม
vendors.json
{ "lodash": "_", "react": "React", "react-dom": "ReactDOM", "react-redux": "ReactRedux", "redux": "Redux" }
ภายนอก.js
module.exports = Object.entries(require('./vendors.json')) .reduce( (collection, [route, name]) => Object.assign( collection, { [route]: { 'commonjs': route, 'commonjs2': route, 'amd': route, 'root': name, 'var': name, }, } ), {} );
แอปพลิเคชันเกตเวย์ที่ใช้ส่วนประกอบเหล่านี้เป็นเจ้าของรันไทม์ พวกเขาปลอมแปลงเอกสาร HTML พวกเขารู้จักผู้ขายทั้งหมดโดยใช้แพ็คเกจ "ผู้ขาย" เดียวกัน พวกเขายังสามารถอัปเกรดแพตช์และเวอร์ชันรองได้อย่างอิสระ โดยถือว่าจะไม่เกิดความเสียหายร้ายแรง พวกเขาจะคำนึงถึงข้อกำหนดของส่วนประกอบทั้งหมด จะโหลดเฉพาะสคริปต์ของผู้จำหน่ายที่จำเป็น และเปิดเผยสคริปต์เหล่านั้นบนหน้าต่างหรือภายในส่วนกลาง
สิ่งที่เราพบว่ามีประโยชน์อย่างยิ่งในแนวทางนี้คือ เนื่องจากเราส่งสคริปต์ของผู้จำหน่ายทั้งหมดจากที่เดียว เราจึงสามารถซ้อนชื่อสากลได้ เดิมทีเราเปิดเผย React เป็น React แต่เราคิดว่าเราควรเปิดเผยเวอร์ชันหลักที่เราใช้อยู่จะดีกว่า ดังนั้น React จะแสดงเป็น window.React15.
เมื่อเห็นว่าส่วนประกอบทั้งหมดใช้ Webpack ภายนอก ชื่อสากลจึงไม่ชัดเจนในโค้ดนั้นเอง — โดยเป็นเพียง import React from 'react’
และการกำหนดค่าจะชี้ไปที่เวอร์ชันที่ต้องการ วิธีการนี้ช่วยให้เรา ค่อยๆ โยกย้าย และให้หน้าเดียวกันแสดง React ทั้งสองเวอร์ชัน โดยไม่มีการชนกัน อย่างไรก็ตาม เราได้ตัดสินใจกำหนดเวลาการย้ายข้อมูลในบริษัท เพื่อหลีกเลี่ยงสคริปต์ของผู้ขายที่จะขยายออกไปเมื่อเวลาผ่านไป
2. การเกิดซ้ำสูง
ท้ายที่สุด เราตัดสินใจแยกกลุ่มนี้ออกและแบ่งเครื่องมือระหว่างโซลูชัน 2 รายการที่มีอยู่ ได้แก่ “ผู้ให้บริการ” และ “ยูทิลิตี้เบ็ดเตล็ด” โดยขึ้นอยู่กับขนาด ความสมบูรณ์ และความเป็นไปได้ ของการเปลี่ยนแปลงที่แตกหัก ในขณะที่เขียน “ผู้ขาย” มีเพียงสองรายการที่เราพึ่งพาซึ่งก่อนหน้านี้เราพิจารณาถึงกลุ่ม “การเกิดซ้ำสูง” โซลูชัน i18n ของเรา และวิธีการตกแต่งสำหรับ ดึงข้อมูล API ของเบราว์เซอร์
ยูทิลิตี้ส่วนที่เหลือของเรา ไม่ว่าจะใช้บ่อยเพียงใด ได้ถูกรวมเข้ากับไลบรารียูทิลิตี้ เพื่อใช้งาน และรวมกลุ่มเป็นส่วนประกอบแต่ละส่วน (ดูด้านล่าง)
3. สาธารณูปโภคเบ็ดเตล็ด
เนื่องจากเราต้องการให้แต่ละแพ็คเกจใช้สิ่งที่ต้องการอย่างแท้จริง โดยไม่ต้องใช้โบลตแวร์ ความท้าทายอยู่ที่การสร้างไลบรารียูทิลิตี้เดียวซึ่งสามารถเขย่าให้เหลือเพียงความจำเป็นเปล่าๆ และไม่มีการซ้ำซ้อนในขณะที่ทำหน้าที่เป็นจุดเดียวในการค้นหาสำหรับการดำเนินการของผู้ช่วยเหลือทั่วไป .
เราถือว่าจำเป็นต้องใช้โมดูลทั่วไปจากวิธีการภายในไลบรารียูทิลิตี้เอง แต่หากเราต้องส่งออกแต่ละโมดูลแยกกันเป็นโซลูชันแบบรวม เราจะทำให้เกิดการทำซ้ำโค้ดภายในส่วนประกอบ แพ็คเกจขนาดเล็กแต่ละชุดจะสร้างการทำซ้ำ แต่ก็มีแนวโน้มที่จะทำให้เกิดการคูณเวอร์ชันด้วย เราต้องการหลีกเลี่ยงสิ่งนี้อย่างแน่นอน
โซลูชันของเรานำเสนอไลบรารียูทิลิตี้ในรูปแบบของ โมดูล Harmony แบบดิบ กลุ่มผู้บริโภคแต่ละกลุ่มมีทุกสิ่งที่ต้องการจากโมดูลเหล่านั้น และการเชื่อมโยงภายในระหว่างโมดูลต่างๆ ก็ยังคงไม่เสียหายเช่นกัน ซึ่งจะทำให้แต่ละส่วนประกอบมีโซลูชันการรวมกลุ่มที่เหมาะสมที่สุด
โซลูชันนี้ช่วยให้เราแนะนำการเปลี่ยนแปลงที่สำคัญในโปรแกรมอรรถประโยชน์ของเราในลักษณะที่ไม่ทำลายล้าง ผู้บริโภคอัปเดตตามจังหวะของตนเอง ความจริงที่ว่ายูทิลิตี้ทั้งหมดได้รับการบำรุงรักษาในไลบรารีเดียวจะสร้างสภาพแวดล้อมที่สะดวกสบาย โดยที่ผู้บริโภคและยูทิลิตี้อื่นๆ จะใช้เวอร์ชันเดียวกันสำหรับแต่ละโปรแกรมยูทิลิตี้ภายในบันเดิล (ปิด) ท้ายที่สุด วิธีการนี้ช่วยให้สามารถพัฒนาวิธีการรวมกลุ่มเพื่อรักษาความสอดคล้องภายในโมดูลที่ใช้ไป: การเลิกใช้งานโพลีฟิล, การส่งออกเวอร์ชัน ES ที่แตกต่างกัน และการรวมกลุ่มเป็นกลุ่มอยู่ในการควบคุมของผู้บริโภค ตัวอย่างเช่น การตัดสินใจให้บริการโค้ด ES6 แก่เบราว์เซอร์ที่รองรับ ในตอนนี้ได้รวมยูทิลิตี้ทั้งหมดไว้ด้วย
มีข้อ จำกัด ที่กำหนดไว้ในไลบรารียูทิลิตี้ - ต้องใช้เฉพาะคุณสมบัติ Javascript ที่ได้รับการสนับสนุนอย่างกว้างขวางเท่านั้น คุณสมบัติทดลองอาจไม่ครอบคลุมโดยกระบวนการ "transpile" ของผู้บริโภค (เช่นปลั๊กอิน babel)
ยูทิลิตี้
export const resolve = (string = '', context = global) => string .split('.') .reduce((prev, current) => typeof prev === 'object' ? prev[current] : prev, context);
รูตยูทิลิตี้
export * from './deepAssign'; export * from './env'; export * from './inTimeRange'; export * from './multiEventListener'; export * from './pluck'; export * from './resolve'; export * from './select'; export * from './sendEvents';
โมดูลทั้งหมดจะถูกส่งออกเป็น โมดูลที่มีชื่อ การเก็บชื่อวิธีการมีประโยชน์สำหรับการบำรุงรักษาข้ามโปรเจ็กต์ และสำคัญต่อความสามารถของนักพัฒนาในการเข้าและออกจากโปรเจ็กต์ นอกจากนี้ยังป้องกันการชนกันของการตั้งชื่อ
ด้วยเหตุนี้ และเพื่อลดความซับซ้อนของการใช้ ไม่ว่าจะเป็นเนมสเปซหรือซ้อนอยู่ภายในไดเร็กทอรี โมดูลจะถูกเปิดเผยบนรากของไลบรารียูทิลิตี้
import { inTimeRange, env, resolve } from '@fiverr/util';
ไลบรารียูทิลิตี้ได้รับการปรับให้เหมาะสมสำหรับการสร้างหน่วยการทำงาน และล้างโค้ดฟีเจอร์จากการดำเนินการทางเทคนิค พื้นที่เก็บข้อมูลนี้ยังครอบคลุมอยู่ในสภาพแวดล้อมการทดสอบด้วยเบราว์เซอร์จริง (รวมถึง Edge!) สร้างเอกสารอัตโนมัติ และโดยทั่วไปแล้วได้รับการออกแบบมาเพื่อให้เหมาะสมที่สุดสำหรับการพัฒนาหน่วยการทำงานของโค้ด นักพัฒนาได้รับการสนับสนุนให้ใช้แม้กับโมดูลที่ไม่มีศักยภาพในการนำกลับมาใช้ใหม่
เรารู้สึกว่าเราพบความสมดุลที่ดี โดยที่การพึ่งพาจำนวนมากเพลิดเพลินกับการแคชและแบ่งปันที่เหมาะสมที่สุด ในขณะที่ไลบรารียูทิลิตี้ที่บำรุงรักษาได้มอบสภาพแวดล้อมการพัฒนาที่รวดเร็วพร้อมความสามารถในการแนะนำการเปลี่ยนแปลงที่ทำลายได้อย่างปลอดภัย
โลกของเว็บแอปพลิเคชันที่ซับซ้อนกำลังเติบโตอย่างรวดเร็ว ฉันมั่นใจว่ามีองค์กรจำนวนมากที่เผชิญกับความท้าทายที่คล้ายกัน เราหวังว่าภาพประกอบของโซลูชันของ Fiverr นี้จะช่วยให้ผู้อื่นปรับแต่งโซลูชันของตนเองได้
โบนัส Gotcha — Webpack ไม่รวม / รวม
เป็นเรื่องปกติที่จะแยกเอาต์พุตของการขึ้นต่อกันออกจากกระบวนการ "transpile" (เช่น babel) ผู้ใช้โมดูล Harmony แบบดิบจะต้อง "แยก" ยูทิลิตี้ออกจากกระบวนการของตน ประเด็นนี้ยังใช้กับการตั้งค่า Jest และทรานสไพเลอร์อื่นๆ ด้วย
const MODULES_TO_INCLUDE = ['@fiverr/util']; const exclude = new RegExp(`node_modules/(?!(${MODULES_TO_INCLUDE.join('|')})/).*`); ... module: { rules: [{ loader: 'babel-loader', exclude,