Fiverr สร้างความสมดุลระหว่างการแบ่งปันทรัพยากรและความเป็นอิสระของส่วนประกอบในระบบส่วนหน้าแบบกระจายอำนาจ

สถาปัตยกรรมส่วนหน้าของเราที่ Fiverr กำหนดแอปพลิเคชันเกตเวย์หลายรายการ โดยให้บริการประสบการณ์ "แนวตั้ง" แต่ละประเภทธุรกิจจะสร้างหน้าเว็บจากหลายองค์ประกอบจากแหล่งที่มาที่หลากหลาย ซึ่งมีสภาพแวดล้อมรันไทม์เดียวกัน — หน้าต่าง หรือ ขอบเขตทั่วโลก

ในองค์กรของเรา ส่วนประกอบจำนวนมากเหล่านี้ใช้สแต็กเทคโนโลยีที่คล้ายกันอย่างยิ่ง (React, Redux, Lodash…) แต่เรายังคงต้องการให้พวกมันเป็นอิสระ และไม่พึ่งพาทรัพยากรที่อยู่นอกเหนือการควบคุม ส่วนประกอบบางอย่างอาจคงสภาพเดิมโดยไม่มีการบำรุงรักษาเป็นเวลานาน ในขณะที่ส่วนประกอบอื่นๆ มีการเปลี่ยนแปลงบ่อยครั้ง — ส่วนประกอบและคุณสมบัติใหม่ๆ ถูกสร้างขึ้นตลอดเวลา เราต้องการรองรับพวกเขาทั้งหมด

จะมีความขัดแย้งอยู่เสมอระหว่างสภาพแวดล้อมที่รองรับโค้ดแบบเดิมและความปรารถนาที่จะสร้างสรรค์สิ่งใหม่ ๆ ด้วยเทคโนโลยีใหม่ ๆ

สถาปัตยกรรมแพลตฟอร์มของเราได้พัฒนาไปสู่โครงสร้างที่ฉันรู้สึกว่าสามารถบำรุงรักษาได้ มีประสิทธิภาพ และช่วยให้สามารถพัฒนาได้ง่าย

ทั่วทั้งระบบเรารับรู้ถึงทรัพยากรที่ใช้บ่อยที่สุดและจัดหมวดหมู่ออกเป็น 3 กลุ่มที่แตกต่างกัน:

  1. ผู้จำหน่าย — ไลบรารีและเฟรมเวิร์กขนาดใหญ่ ซึ่งมักจะเป็นโครงการโอเพ่นซอร์สที่รู้จักกันดี ซึ่งมีรอบการเปิดตัวเวอร์ชันหลักที่ต่ำพอสมควร
  2. การเกิดซ้ำสูง—โมดูลที่ได้รับการปรับแต่งให้ตรงตามความต้องการในการพัฒนาของเราเป็นอย่างดีจนกลายเป็นลักษณะที่สองของนักพัฒนาและคาดว่าจะพร้อมใช้งานในทุกสภาพแวดล้อม รวมถึง i18n ของเรา โซลูชันและผู้รายงานสถิติเช่นการติดตามด้านเทคนิคและธุรกิจ
  3. ยูทิลิตี้เบ็ดเตล็ด — ยูทิลิตี้เหล่านี้อาจมีการนำกลับมาใช้ใหม่ได้หลายระดับ ซึ่งรวมถึงฟังก์ชันตัวช่วย คลาสที่มีประโยชน์ ตรรกะทางธุรกิจที่ทำซ้ำได้ หรือการดำเนินการทางเทคนิค ซึ่งไม่เกี่ยวข้องกับฟีเจอร์หรือโดเมนใดๆ อย่างเด็ดขาด

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,