2019 рдореЗрдВ рдПрдХреНрд╕рдЪреЗрдВрдЬ рдПрд╕рдкреАрдП рдПрдкреНрд▓реАрдХреЗрд╢рди рдЖрд░реНрдХрд┐рдЯреЗрдХреНрдЪрд░

рдЕрднрд┐рд╡рд╛рджрди, рдЦрдмреНрд░реЛрд╡рд┐рддреНрд╕!


рдореИрдВ рдЗрд╕рдХреА рдиреАрдВрд╡ рдХреЗ рдмрд╛рдж рд╕реЗ рдЗрд╕ рд╕рдВрд╕рд╛рдзрди рдХреЛ рдкрдврд╝ рд░рд╣рд╛ рд╣реВрдВ, рд▓реЗрдХрд┐рди рдПрдХ рд▓реЗрдЦ рд▓рд┐рдЦрдиреЗ рдХрд╛ рд╕рдордп рдЕрдм тАЛтАЛрдХреЗрд╡рд▓ рджрд┐рдЦрд╛рдИ рджрд┐рдпрд╛ рд╣реИ, рдЬрд┐рд╕рдХрд╛ рдЕрд░реНрде рд╣реИ рдХрд┐ рд╕рдореБрджрд╛рдп рдХреЗ рд╕рд╛рде рдЕрдкрдиреЗ рдЕрдиреБрднрд╡ рдХреЛ рд╕рд╛рдЭрд╛ рдХрд░рдиреЗ рдХрд╛ рд╕рдордп рд╣реИред рд╢реБрд░реБрдЖрддреА рдбреЗрд╡рд▓рдкрд░реНрд╕ рдХреЗ рд▓рд┐рдП, рдореБрдЭреЗ рдЙрдореНрдореАрдж рд╣реИ рдХрд┐ рд▓реЗрдЦ рдбрд┐рдЬрд╛рдЗрди рдХреА рдЧреБрдгрд╡рддреНрддрд╛ рдореЗрдВ рд╕реБрдзрд╛рд░ рдХрд░рдиреЗ рдореЗрдВ рдорджрдж рдХрд░реЗрдЧрд╛, рдФрд░ рдЕрдиреБрднрд╡реА рд▓реЛрдЧреЛрдВ рдХреЗ рд▓рд┐рдП рдпрд╣ рдПрдХ рдЪреЗрдХрд▓рд┐рд╕реНрдЯ рдХреЗ рд░реВрдк рдореЗрдВ рдХрд╛рдо рдХрд░реЗрдЧрд╛ рддрд╛рдХрд┐ рд╡рд╛рд╕реНрддреБрдХрд▓рд╛ рдХреЗ рд╕реНрддрд░ рдкрд░ рдорд╣рддреНрд╡рдкреВрд░реНрдг рддрддреНрд╡реЛрдВ рдХреЛ рди рднреВрд▓реЗрдВред рдЕрдзреАрд░ рдХреЗ рд▓рд┐рдП, рдЕрдВрддрд┐рдо рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдФрд░ рдбреЗрдореЛ ред



рдорд╛рди рд▓реАрдЬрд┐рдП рдХрд┐ рдЖрдк рдПрдХ "рд╕реНрд╡рдкреНрди рдХрдВрдкрдиреА" рдореЗрдВ рд╣реИрдВ - рдкреНрд░реМрджреНрдпреЛрдЧрд┐рдХреА рдФрд░ рд╕рдВрд╕рд╛рдзрдиреЛрдВ рдХреА рдПрдХ рдирд┐: рд╢реБрд▓реНрдХ рдкрд╕рдВрдж рдХреЗ рд╕рд╛рде рдПрдХреНрд╕рдЪреЗрдВрдЬреЛрдВ рдореЗрдВ рд╕реЗ рдПрдХ "рд╕рдм рдХреБрдЫ" рдЬреИрд╕рд╛ рдХрд┐ рд╣реЛрдирд╛ рдЪрд╛рд╣рд┐рдПред рдлрд┐рд▓рд╣рд╛рд▓, рдХрдВрдкрдиреА рдХреЗ рдкрд╛рд╕ рдпрд╣ рд╕рдм рд╣реИ


рд╡реНрдпрд╛рдкрд╛рд░ рдЕрд╕рд╛рдЗрдирдореЗрдВрдЯ


рдЯреНрд░реЗрдбрд┐рдВрдЧ рдЗрдВрдЯрд░рдлрд╝реЗрд╕ рдХреЗ рд▓рд┐рдП рдПрдХ рдПрд╕рдкреАрдП рдПрдкреНрд▓рд┐рдХреЗрд╢рди рд╡рд┐рдХрд╕рд┐рдд рдХрд░реЗрдВ, рдЬрд┐рд╕рдореЗрдВ рдЖрдк рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ:


  • рдореБрджреНрд░рд╛ рджреНрд╡рд╛рд░рд╛ рд╕рдореВрд╣реАрдХреГрдд рд╡реНрдпрд╛рдкрд╛рд░рд┐рдХ рдЬреЛрдбрд╝реЗ рдХреА рд╕реВрдЪреА рджреЗрдЦреЗрдВ;
  • рдЬрдм рдЖрдк рдореМрдЬреВрджрд╛ рдореВрд▓реНрдп рдкрд░ рдЬрд╛рдирдХрд╛рд░реА рджреЗрдЦрдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рдЯреНрд░реЗрдбрд┐рдВрдЧ рдЬреЛрдбрд╝реА рдкрд░ рдХреНрд▓рд┐рдХ рдХрд░рддреЗ рд╣реИрдВ, рддреЛ 24 рдШрдВрдЯреЗ рдореЗрдВ рдкрд░рд┐рд╡рд░реНрддрди, "рдСрд░реНрдбрд░ рдХрд╛ рдЧрд┐рд▓рд╛рд╕";
  • рдЖрд╡реЗрджрди рдХреА рднрд╛рд╖рд╛ рдХреЛ рдЕрдВрдЧреНрд░реЗрдЬреА / рд░реВрд╕реА рдореЗрдВ рдмрджрд▓реЗрдВ;
  • рд╡рд┐рд╖рдп рдХреЛ рдЕрдВрдзреЗрд░реЗ / рдкреНрд░рдХрд╛рд╢ рдореЗрдВ рдмрджрд▓реЗрдВред

рдХрд╛рд░реНрдп рдХрд╛рдлреА рдЫреЛрдЯрд╛ рд╣реИ, рдЬреЛ рдЖрдкрдХреЛ рд╡реНрдпрд╛рд╡рд╕рд╛рдпрд┐рдХ рдХрд╛рд░реНрдпрдХреНрд╖рдорддрд╛ рдХреЗ рдмрдбрд╝реЗ рд╕рдВрд╕реНрдХрд░рдгреЛрдВ рдХреЛ рд▓рд┐рдЦрдиреЗ рдХреЗ рдмрдЬрд╛рдп рд╡рд╛рд╕реНрддреБрдХрд▓рд╛ рдкрд░ рдзреНрдпрд╛рди рдХреЗрдВрджреНрд░рд┐рдд рдХрд░рдиреЗ рдХреА рдЕрдиреБрдорддрд┐ рджреЗрдЧрд╛ред рдкреНрд░рд╛рд░рдВрднрд┐рдХ рдкреНрд░рдпрд╛рд╕реЛрдВ рдХрд╛ рдкрд░рд┐рдгрд╛рдо рдПрдХ рддрд╛рд░реНрдХрд┐рдХ рдФрд░ рд╡рд┐рдЪрд╛рд░рд╢реАрд▓ рдХреЛрдб рд╣реЛрдирд╛ рдЪрд╛рд╣рд┐рдП рдЬреЛ рдЖрдкрдХреЛ рд╡реНрдпрд╛рд╡рд╕рд╛рдпрд┐рдХ рддрд░реНрдХ рдХреЗ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдХреЗ рд▓рд┐рдП рд╕реАрдзреЗ рдЖрдЧреЗ рдмрдврд╝рдиреЗ рдХреА рдЕрдиреБрдорддрд┐ рджреЗрддрд╛ рд╣реИред


рдЪреВрдВрдХрд┐ рдЧреНрд░рд╛рд╣рдХ рд╕реЗ рдХрд╛рдо рдХреЗ рдмрдпрд╛рди рдореЗрдВ рдХреЛрдИ рддрдХрдиреАрдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдирд╣реАрдВ рд╣реИ, рдЙрдиреНрд╣реЗрдВ рд╡рд┐рдХрд╛рд╕ рдХреЗ рд▓рд┐рдП рд╕рд╣рдЬ рд╣реЛрдиреЗ рджреЗрдВ:


  • рдХреНрд░реЙрд╕-рдмреНрд░рд╛рдЙрдЬрд╝рд░ рд╕рдВрдЧрддрддрд╛ : рд▓реЛрдХрдкреНрд░рд┐рдп рдмреНрд░рд╛рдЙрдЬрд╝рд░реЛрдВ рдХреЗ 2 рдирд╡реАрдирддрдо рд╕рдВрд╕реНрдХрд░рдг (рдмрд┐рдирд╛ IE);
  • рд╕реНрдХреНрд░реАрди рдХреА рдЪреМрдбрд╝рд╛рдИ :> = 1240px;
  • рдбрд┐рдЬрд╛рдЗрди : рдЕрдиреНрдп рдПрдХреНрд╕рдЪреЗрдВрдЬреЛрдВ рдХреЗ рд╕рд╛рде рд╕рд╛рджреГрд╢реНрдп рджреНрд╡рд╛рд░рд╛, рдХреЗ рд░реВрдк рдореЗрдВ рдбрд┐рдЬрд╛рдЗрдирд░ рдХреЛ рдЕрднреА рддрдХ рдХрд╛рдо рдкрд░ рдирд╣реАрдВ рд░рдЦрд╛ рдЧрдпрд╛ рд╣реИред

рдЕрдм рдпрд╣ рдирд┐рд░реНрдзрд╛рд░рд┐рдд рдХрд░рдиреЗ рдХрд╛ рд╕рдордп рд╣реИ рдХрд┐ рдХреМрди рд╕реЗ рдЯреВрд▓ рдФрд░ рд▓рд╛рдЗрдмреНрд░реЗрд░реА рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд┐рдпрд╛ рдЬрд╛ рд░рд╣рд╛ рд╣реИред рдореИрдВ "рдЯрд░реНрдирдХреА" рдФрд░ рдХреЗ рд╡рд┐рдХрд╛рд╕ рдХреЗ рд╕рд┐рджреНрдзрд╛рдВрддреЛрдВ рджреНрд╡рд╛рд░рд╛ рдирд┐рд░реНрджреЗрд╢рд┐рдд рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛ KISS , рдпрд╛рдиреА рдЖрддреНрдордЬреНрдЮрд╛рди рдЬрд┐рдирдореЗрдВ рд╕реЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрдЧреА рдмрд╣реБрдд рд╕рдордп рдЕрдкрд░реНрдпрд╛рдкреНрдд рд╣реИ, рдкреНрд░рд╢рд┐рдХреНрд╖рдг рднрд╡рд┐рд╖реНрдп рд╕рд╛рдереА рдбреЗрд╡рд▓рдкрд░реНрд╕ рдХреЗ рд▓рд┐рдП рд╕рдордп рд╕рд╣рд┐рдд рдХреЗ рд▓рд┐рдП рд╣реА рдЦреБрд▓рд╛ рд╕реНрд░реЛрдд рдкреБрд╕реНрддрдХрд╛рд▓рдп рд▓реЗ рд░рд╣реАред


  • рд╕рдВрд╕реНрдХрд░рдг рдирд┐рдпрдВрддреНрд░рдг рдкреНрд░рдгрд╛рд▓реА : Git + Github;
  • рдмреИрдХрдПрдВрдб : рдПрдкреАрдЖрдИ рдХреЙрдЗрдирдЧреЗрдХреЛ;
  • рд╡рд┐рдзрд╛рдирд╕рднрд╛ / рдкрд░рд┐рд╡рд╣рди : рд╡реЗрдмрдкреИрдХ + рдмреИрдмрд▓;
  • рдкреИрдХреЗрдЬ рдЗрдВрд╕реНрдЯреЙрд▓рд░ : рдпрд╛рд░реНрди (рдПрдирдкреАрдПрдо 6 рдЧрд▓рдд рддрд░реАрдХреЗ рд╕реЗ рдЕрджреНрдпрддрди рдирд┐рд░реНрднрд░рддрд╛рдПрдВ);
  • рдХреЛрдб рдЧреБрдгрд╡рддреНрддрд╛ рдирд┐рдпрдВрддреНрд░рдг : ESLint + Prettier + рд╕реНрдЯрд╛рдЗрд▓рд▓рд┐рдВрдЯ;
  • рджреГрд╢реНрдп : рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ (рдЖрдЗрдП рджреЗрдЦреЗрдВ рдХрд┐ рд╣реБрдХ рдХрд┐рддрдиреЗ рд╕реБрд╡рд┐рдзрд╛рдЬрдирдХ рд╣реИрдВ);
  • рд╕реНрдЯреЛрд░ : MobX;
  • рдСрдЯреЛрдЯреЗрд╕реНрдЯреНрд╕ : рд╕рд╛рдЗрдкреНрд░реЛрд╕.рдЖрдИрдУ (рдореЛрдЪрд╛ / рдХрд░реНрдо + рдЪрд╛рдп + рд╕рд┐рдиреЛрди + рд╕реЗрд▓реЗрдирд┐рдпрдо + рд╡реЗрдмрдбреНрд░рд╛рдЗрд╡рд░ / рдкреНрд░реЛрдЯреЗрдХреНрдЯрд░ рдЬреИрд╕реЗ рдореЙрдбреНрдпреВрд▓рд░ рдЕрд╕реЗрдВрдмрд▓реА рдХреЗ рдмрдЬрд╛рдп рдПрдХ рд╕рдВрдкреВрд░реНрдг рдЬрд╛рд╡рд╛рд╕реНрдХреНрд░рд┐рдкреНрдЯ рд╕рдорд╛рдзрд╛рди);
  • рд╢реИрд▓рд┐рдпреЛрдВ : рдкреЛрд╕реНрдЯрд╕реАрдПрд╕рдПрд╕ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ SCSS (рд╡рд┐рдиреНрдпрд╛рд╕ рд▓рдЪреАрд▓рд╛рдкрди, рд╕реНрдЯрд╛рдЗрд▓рд▓рд┐рдВрдЯ рдХреЗ рд╕рд╛рде рджреЛрд╕реНрдд);
  • рдЪрд╛рд░реНрдЯ : рд╣рд╛рдИрд╕реНрдЯреЙрдХ (рд╕реЗрдЯрдЕрдк рдЯреНрд░реЗрдбрд┐рдВрдЧрд╡реНрдпреВ рдХреА рддреБрд▓рдирд╛ рдореЗрдВ рдмрд╣реБрдд рдЖрд╕рд╛рди рд╣реИ, рд▓реЗрдХрд┐рди рдПрдХ рд╡рд╛рд╕реНрддрд╡рд┐рдХ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рд▓рд┐рдП рдореИрдВ рдмрд╛рдж рдореЗрдВ рд▓реЗ рдЬрд╛рдКрдВрдЧрд╛ );
  • рддреНрд░реБрдЯрд┐ рд░рд┐рдкреЛрд░реНрдЯрд┐рдВрдЧ: рд╕рдВрддрд░реА;
  • рдЙрдкрдпреЛрдЧрд┐рддрд╛рдПрдБ : рд▓реЛрдбрд╢ (рд╕рдордп рдХреА рдмрдЪрдд);
  • рдорд╛рд░реНрдЧ : рдЯрд░реНрдирдХреА;
  • рд╕реНрдерд╛рдиреАрдпрдХрд░рдг : рдЯрд░реНрдирдХреА;
  • рдЕрдиреБрд░реЛрдзреЛрдВ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░реЗрдВ : рдЯрд░реНрдирдХреА;
  • рдкреНрд░рджрд░реНрд╢рди рдореАрдЯреНрд░рд┐рдХ : рдЯрд░реНрдирдХреА;
  • рдЯрдВрдХрдг : рдореЗрд░реА рдкрд╛рд░реА рдкрд░ рдирд╣реАрдВред

рдЗрд╕ рдкреНрд░рдХрд╛рд░, рдЕрдВрддрд┐рдо рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдлрд╝рд╛рдЗрд▓ рдореЗрдВ рдкреБрд╕реНрддрдХрд╛рд▓рдпреЛрдВ рд╕реЗ, рдХреЗрд╡рд▓ рд░рд┐рдПрдХреНрдЯ, MobX, рд╣рд╛рдИрд╕реНрдЯреЙрдХ, рд▓реЙрдбрд╢ рдФрд░ рд╕реЗрдВрдЯрд░реА рджрд┐рдЦрд╛рдИ рджреЗрдВрдЧреЗред рдореБрдЭреЗ рд▓рдЧрддрд╛ рд╣реИ рдХрд┐ рдпрд╣ рдЙрдЪрд┐рдд рд╣реИ, рдХреНрдпреЛрдВрдХрд┐ рдЙрдирдХреЗ рдкрд╛рд╕ рдЙрддреНрдХреГрд╖реНрдЯ рджрд╕реНрддрд╛рд╡реЗрдЬ, рдкреНрд░рджрд░реНрд╢рди рд╣реИрдВ рдФрд░ рдХрдИ рдбреЗрд╡рд▓рдкрд░реНрд╕ рд╕реЗ рдкрд░рд┐рдЪрд┐рдд рд╣реИрдВред


рдХреЛрдб рдЧреБрдгрд╡рддреНрддрд╛ рдирд┐рдпрдВрддреНрд░рдг


рдореИрдВ рдкреИрдХреЗрдЬ рдореЗрдВ рдирд┐рд░реНрднрд░рддрд╛ рдХреЛ рддреЛрдбрд╝рдирд╛ рдЪрд╛рд╣рддрд╛ рд╣реВрдВ ред рд╕рд┐рдореЗрдВрдЯрд┐рдХ рднрд╛рдЧреЛрдВ рдореЗрдВ рдЖрдЧрдЬрдиреА , рдЗрд╕рд▓рд┐рдП рдЧрд┐рдЯ рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдХреА рд╢реБрд░реБрдЖрдд рдХрд░рдиреЗ рдХреЗ рдмрд╛рдж рдкрд╣рд▓рд╛ рдХрджрдо рдЙрди рд╕рднреА рдЪреАрдЬреЛрдВ рдХреЛ рд╕рдореВрд╣рд┐рдд рдХрд░рдирд╛ рд╣реИ рдЬреЛ рдХреЛрдб рд╢реИрд▓реА рдореЗрдВ ./eslint-custom рдлрд╝реЛрд▓реНрдбрд░ рдХреЛ рдкреИрдХреЗрдЬ рдореЗрдВ рдирд┐рд░реНрджрд┐рд╖реНрдЯ рдХрд░рддрд╛ рд╣реИ редjson :


{ "scripts": { "upd": "yarn install --no-lockfile" }, "dependencies": { "eslint-custom": "file:./eslint-custom" } } 

рд╕рд╛рдорд╛рдиреНрдп yarn install рдХреА рдЬрд╛рдВрдЪ рдирд╣реАрдВ рд╣реЛрдЧреА рдХрд┐ рдХреНрдпрд╛ рдПрд╕реНрд▓рд┐рдВрдЯ-рдХрд╕реНрдЯрдо рдХреЗ рдЕрдВрджрд░ рдирд┐рд░реНрднрд░рддрд╛рдПрдВ рдмрджрд▓ рдЧрдИ рд╣реИрдВ, рдЗрд╕рд▓рд┐рдП рдореИрдВ yarn upd рдЕрдкрдбреЗрдЯ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реВрдВрдЧрд╛ред рд╕рд╛рдорд╛рдиреНрдп рддреМрд░ рдкрд░, рдпрд╣ рдЕрднреНрдпрд╛рд╕ рдЕрдзрд┐рдХ рд╕рд╛рд░реНрд╡рднреМрдорд┐рдХ рджрд┐рдЦрд╛рдИ рджреЗрддрд╛ рд╣реИ, рдХреНрдпреЛрдВрдХрд┐ рдбреЗрд╡рд▓рдкрд░реНрд╕ рдХреЛ рдкреИрдХреЗрдЬ рд╕реНрдерд╛рдкрд┐рдд рдХрд░рдиреЗ рдХреА рд╡рд┐рдзрд┐ рдХреЛ рдмрджрд▓рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрдиреЗ рдкрд░, рдкрд░рд┐рдирд┐рдпреЛрдЬрди рдиреБрд╕реНрдЦрд╛ рдХреЛ рдмрджрд▓рдирд╛ рдирд╣реАрдВ рд╣реЛрдЧрд╛ред


рдпрд╛рд░реНрди .рд▓реЙрдХ рдлрд╝рд╛рдЗрд▓ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХрд╛ рдХреЛрдИ рдорддрд▓рдм рдирд╣реАрдВ рд╣реИ, рдХреНрдпреЛрдВрдХрд┐ рд╕рднреА рдирд┐рд░реНрднрд░рддрд╛рдПрдВ рдмрд┐рдирд╛ "рд╕реЗрд╡рд░" ( "react": "16.8.6" ) рд╣реЛрдЧреАред рдЕрдиреБрднрд╡ рдиреЗ рджрд┐рдЦрд╛рдпрд╛ рд╣реИ рдХрд┐ рд╕рдВрд╕реНрдХрд░рдгреЛрдВ рдХреЛ рдореИрдиреНрдпреБрдЕрд▓ рд░реВрдк рд╕реЗ рдЕрдкрдбреЗрдЯ рдХрд░рдирд╛ рдФрд░ рд▓реЙрдХ рдлрд╝рд╛рдЗрд▓ рдкрд░ рдирд┐рд░реНрднрд░ рд░рд╣рдиреЗ рдХреА рддреБрд▓рдирд╛ рдореЗрдВ рд╡реНрдпрдХреНрддрд┐рдЧрдд рдХрд╛рд░реНрдпреЛрдВ рдХреЗ рд▓рд┐рдП рд╕рд╛рд╡рдзрд╛рдиреАрдкреВрд░реНрд╡рдХ рдкрд░реАрдХреНрд╖рдг рдХрд░рдирд╛ рдмреЗрд╣рддрд░ рд╣реИ, рдкреИрдХреЗрдЬ рд▓реЗрдЦрдХреЛрдВ рдХреЛ рдХрд┐рд╕реА рднреА рд╕рдордп рдПрдХ рдорд╛рдореВрд▓реА рдЕрдкрдбреЗрдЯ рдХреЗ рд╕рд╛рде рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рддреЛрдбрд╝рдиреЗ рдХрд╛ рдЕрд╡рд╕рд░ рдкреНрд░рджрд╛рди рдХрд░рддрд╛ рд╣реИ (рднрд╛рдЧреНрдпрд╢рд╛рд▓реА рд▓реЛрдЧ рдЬрд┐рдиреНрд╣реЛрдВрдиреЗ рдЗрд╕рдХрд╛ рд╕рд╛рдордирд╛ рдирд╣реАрдВ рдХрд┐рдпрд╛ рд╣реИ)ред


рдПрд╕реНрд▓рд┐рдВрдЯ-рдХрд╕реНрдЯрдо рдкреИрдХреЗрдЬ рдореЗрдВ, рдирд┐рд░реНрднрд░рддрд╛рдПрдВ рдирд┐рдореНрдирд╛рдиреБрд╕рд╛рд░ рд╣реЛрдВрдЧреА:


eslint-custom / package.json
 { "name": "eslint-custom", "version": "1.0.0", "description": "Custom linter rules for this project", "license": "MIT", "dependencies": { "babel-eslint": "10.0.1", "eslint": "5.16.0", "eslint-config-prettier": "4.1.0", "eslint-plugin-import": "2.17.2", "eslint-plugin-prettier": "3.0.1", "eslint-plugin-react": "7.12.4", "eslint-plugin-react-hooks": "1.6.0", "prettier": "1.17.0", "prettier-eslint": "8.8.2", "stylelint": "10.0.1", "stylelint-config-prettier": "5.1.0", "stylelint-prettier": "1.0.6", "stylelint-scss": "3.6.0" } } 

рддреАрди рдФрдЬрд╛рд░реЛрдВ рдХреЛ рдЬреЛрдбрд╝рдиреЗ рдХреЗ рд▓рд┐рдП, рдЗрд╕рдореЗрдВ 5 рд╕рд╣рд╛рдпрдХ рдкреИрдХреЗрдЬ ( рдПрд╕реНрд▓рд┐рдВрдЯ-рдкреНрд▓рдЧрдЗрди-рдкреНрд░реЗрдЯрд┐рдпрд░, рдПрд╕реНрд▓рд┐рдВрдЯ-рдХреЙрдиреНрдлрд┐рдЧ-рдкреНрд░реЗрдЯрд┐рдпрд░, рд╕реНрдЯрд╛рдЗрд▓рд▓рд┐рдВрдЯ-рдкреНрд░реЗрдЯрд┐рдпрд░, рд╕реНрдЯрд╛рдЗрд▓рд▓рд┐рдВрдЯ-рдХреЙрдиреНрдлрд┐рдЧрд░-рдкреНрд░реЗрдЯрд┐рдпрд░, рдкреНрд░реЗрдЯрд┐рдПрд░-рдПрд╕реЗрд▓рд┐рдВрдЯ - рд▓рд┐рдпрд╛ рдЧрдпрд╛ - рдЖрдкрдХреЛ рдЖрдЬ рдЗрддрдиреА рдХреАрдордд рдЪреБрдХрд╛рдиреА рд╣реЛрдЧреАред рдЕрдзрд┐рдХрддрдо рд╕реБрд╡рд┐рдзрд╛ рдХреЗ рд▓рд┐рдП, рдЖрдпрд╛рддреЛрдВ рдХреА рдХреЗрд╡рд▓ рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рдЫрдБрдЯрд╛рдИ рдкрд░реНрдпрд╛рдкреНрдд рдирд╣реАрдВ рд╣реИ, рд▓реЗрдХрд┐рди, рджреБрд░реНрднрд╛рдЧреНрдп рд╕реЗ, рдпрд╣ рдкреНрд▓рдЧрдЗрди рдПрдХ рдлрд╝рд╛рдЗрд▓ рдХреЛ рд╕реБрдзрд╛рд░рддреЗ рд╕рдордп рд▓рд╛рдЗрдиреЗрдВ рдЦреЛ рджреЗрддрд╛ рд╣реИред


рд╕рднреА рдЙрдкрдХрд░рдгреЛрдВ рдХреЗ рд▓рд┐рдП рд╡рд┐рдиреНрдпрд╛рд╕ рдлрд╛рдЗрд▓ * .js рдкреНрд░рд╛рд░реВрдк ( eslint.config.js , stylelint.config.js ) рдореЗрдВ рд╣реЛрдЧреА рддрд╛рдХрд┐ рдХреЛрдб рд╕реНрд╡рд░реВрдкрдг рдЙрди рдкрд░ рдХрд╛рдо рдХрд░реЗрдЧрд╛ред рдирд┐рдпрдо * .yaml рдкреНрд░рд╛рд░реВрдк рдореЗрдВ рд╣реЛ рд╕рдХрддреЗ рд╣реИрдВ, рд╢рдмреНрджрд╛рд░реНрде рдореЙрдбреНрдпреВрд▓ рджреНрд╡рд╛рд░рд╛ рдЯреВрдЯ рдЧрдпрд╛ред рдХреЙрдиреНрдлрд╝рд┐рдЧрд░реЗрд╢рди рдФрд░ рдирд┐рдпрдореЛрдВ рдХреЗ рдкреВрд░реНрдг рд╕рдВрд╕реНрдХрд░рдг рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдореЗрдВ рд╣реИрдВ ред


рдпрд╣ рдореБрдЦреНрдп package.json рдореЗрдВ рдХрдорд╛рдВрдбреНрд╕ рдЬреЛрдбрд╝рдиреЗ рдХреЗ рд▓рд┐рдП рдмрдиреА рд╣реБрдИ рд╣реИ ...


 { "scripts": { "upd": "yarn install --no-lockfile", "format:js": "eslint --ignore-path .gitignore --ext .js -c ./eslint-custom/eslint.config.js --fix", "format:style": "stylelint --ignore-path .gitignore --config ./eslint-custom/stylelint.config.js --fix" } } 

... рдФрд░ рд╡рд░реНрддрдорд╛рди рдлрд╝рд╛рдЗрд▓ рдХреЛ рд╕рд╣реЗрдЬрддреЗ рд╕рдордп рд╕реНрд╡рд░реВрдкрдг рд▓рд╛рдЧреВ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдЕрдкрдиреА IDE рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд░реЗрдВред рдЗрд╕рдХреА рдЧрд╛рд░рдВрдЯреА рджреЗрдиреЗ рдХреЗ рд▓рд┐рдП, рдПрдХ рдХрдорд┐рдЯ рдмрдирд╛рддреЗ рд╕рдордп, рдЖрдкрдХреЛ рдПрдХ git рд╣реБрдХ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдирд╛ рдЪрд╛рд╣рд┐рдП рдЬреЛ рд╕рднреА рдкреНрд░реЛрдЬреЗрдХреНрдЯ рдлрд╝рд╛рдЗрд▓реЛрдВ рдХреА рдЬрд╛рдБрдЪ рдФрд░ рдкреНрд░рд╛рд░реВрдкрд┐рдд рдХрд░реЗрдЧрд╛ред рдХреЗрд╡рд▓ рд╡реЗ рд╣реА рдХреНрдпреЛрдВ рдирд╣реАрдВ рдЬреЛ рдХрдорд┐рдЯ рдореЗрдВ рдореМрдЬреВрдж рд╣реИрдВ? рд╕рдВрдкреВрд░реНрдг рдХреЛрдб рдЖрдзрд╛рд░ рдХреЗ рд▓рд┐рдП рд╕рд╛рдореВрд╣рд┐рдХ рдЬрд┐рдореНрдореЗрджрд╛рд░реА рдХреЗ рд╕рд┐рджреНрдзрд╛рдВрдд рдХреЗ рд▓рд┐рдП, рддрд╛рдХрд┐ рд╕рддреНрдпрд╛рдкрди рдХреЛ рджрд░рдХрд┐рдирд╛рд░ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдХрд┐рд╕реА рдХреЛ рд▓реБрднрд╛рдпрд╛ рдирд╣реАрдВ рдЧрдпрд╛ред рдРрд╕рд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдПрдХ рдХрдорд┐рдЯ рдмрдирд╛рддреЗ рд╕рдордп, рд╕рднреА рд▓рд┐рдВрдЯрд░ рдЪреЗрддрд╛рд╡рдирд┐рдпреЛрдВ рдХреЛ рддреНрд░реБрдЯрд┐ рдорд╛рдирд╛ рдЬрд╛рдПрдЧрд╛ --max-warnings=0 ред


 { "husky": { "hooks": { "pre-commit": "npm run format:js -- --max-warnings=0 ./ && npm run format:style ./**/*.scss" } } } 

рд╡рд┐рдзрд╛рдирд╕рднрд╛ / рдкрд░рд┐рд╡рд╣рди


рдлрд┐рд░ рд╕реЗ, рдореИрдВ рдореЙрдбреНрдпреВрд▓рд░ рджреГрд╖реНрдЯрд┐рдХреЛрдг рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реВрдВрдЧрд╛ рдФрд░ ./webpack-custom рдлрд╝реЛрд▓реНрдбрд░ рдореЗрдВ Webpack рдФрд░ Babel рдХреА рд╕рднреА рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рдирд┐рдХрд╛рд▓реВрдБрдЧрд╛ред рдХреЙрдиреНрдлрд╝рд┐рдЧрд░реЗрд╢рди рдирд┐рдореНрди рдлрд╝рд╛рдЗрд▓ рд╕рдВрд░рдЪрдирд╛ рдкрд░ рдирд┐рд░реНрднрд░ рдХрд░реЗрдЧрд╛:


 . |-- webpack-custom | |-- config | |-- loaders | |-- plugins | |-- rules | |-- utils | `-- package.json | `-- webpack.config.js 

рдПрдХ рдареАрдХ рд╕реЗ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдмрд┐рд▓реНрдбрд░ рдкреНрд░рджрд╛рди рдХрд░реЗрдЧрд╛:


  • рд╕реБрд╡рд┐рдзрд╛рдЬрдирдХ рдкреНрд░рд╕реНрддрд╛рд╡реЛрдВ (рд╡рд░реНрдЧ рд╕рдЬреНрдЬрд╛рдХрд╛рд░ рдФрд░ MobX рдХреЗ рд▓рд┐рдП рдЙрдирдХреЗ рдЧреБрдгреЛрдВ рд╕рд╣рд┐рдд, рдирд╡реАрдирддрдо EcmaScript рд╡рд┐рдирд┐рд░реНрджреЗрд╢ рдХреА рд╡рд╛рдХреНрдп рд░рдЪрдирд╛ рдФрд░ рдХреНрд╖рдорддрд╛рдУрдВ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рдХреЛрдб рд▓рд┐рдЦрдиреЗ рдХреА рдХреНрд╖рдорддрд╛ рдирд┐рд╢реНрдЪрд┐рдд рд░реВрдк рд╕реЗ рдпрд╣рд╛рдВ рдЙрдкрдпреЛрдЧреА рд╣реИ);
  • рд╣реЙрдЯ рд░реАрд▓реЛрдбрд┐рдВрдЧ рд╡рд╛рд▓рд╛ рд╕реНрдерд╛рдиреАрдп рд╕рд░реНрд╡рд░;
  • рд╡рд┐рдзрд╛рдирд╕рднрд╛ рдкреНрд░рджрд░реНрд╢рди рдореИрдЯреНрд░рд┐рдХреНрд╕;
  • рдЪрдХреНрд░реАрдп рдирд┐рд░реНрднрд░рддрд╛ рдХреЗ рд▓рд┐рдП рдЬрд╛рдБрдЪ;
  • рдкрд░рд┐рдгрд╛рдорд╕реНрд╡рд░реВрдк рдлрд╝рд╛рдЗрд▓ рдХреА рд╕рдВрд░рдЪрдирд╛ рдФрд░ рдЖрдХрд╛рд░ рдХрд╛ рд╡рд┐рд╢реНрд▓реЗрд╖рдг;
  • рдЙрддреНрдкрд╛рджрди рд╡рд┐рдзрд╛рдирд╕рднрд╛ рдХреЗ рд▓рд┐рдП рдЕрдиреБрдХреВрд▓рди рдФрд░ рдЦрдирди;
  • рдореЙрдбреНрдпреВрд▓рд░ рдХреА рд╡реНрдпрд╛рдЦреНрдпрд╛ * .scs рдлрд╛рдЗрд▓реЗрдВ рдФрд░ рд╕рдорд╛рдкреНрдд * рдирд┐рдХрд╛рд▓рдиреЗ рдХреА рдХреНрд╖рдорддрд╛ * ред рдмрдВрдбрд▓ рд╕реЗ рдлрд╛рдЗрд▓реЗрдВ .css ;
  • рдЗрдирд▓рд╛рдЗрди-рдЗрдиреНрд╕рд░реНрдЯ * .svg рдлрд╛рдЗрд▓реЗрдВ;
  • рд▓рдХреНрд╖реНрдп рдмреНрд░рд╛рдЙрдЬрд╝рд░реЛрдВ рдХреЗ рд▓рд┐рдП рдкреЙрд▓реАрдлрд┐рд▓ / рд╕реНрдЯрд╛рдЗрд▓ рдЙрдкрд╕рд░реНрдЧ;
  • рдЙрддреНрдкрд╛рджрди рдкрд░ рдХреИрд╢рд┐рдВрдЧ рдлрд╝рд╛рдЗрд▓реЛрдВ рдХреА рд╕рдорд╕реНрдпрд╛ рдХреЛ рд╣рд▓ рдХрд░рдирд╛ред

рдпрд╣ рднреА рдЖрд╕рд╛рдиреА рд╕реЗ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛ред рдореИрдВ рдЗрд╕ рд╕рдорд╕реНрдпрд╛ рдХреЛ рджреЛ .env рдирдореВрдирд╛ рдлрд╝рд╛рдЗрд▓реЛрдВ рдХреА рдорджрдж рд╕реЗ рд╣рд▓ рдХрд░реВрдВрдЧрд╛ :


.frontend.env.example
 AGGREGATION_TIMEOUT=0 BUNDLE_ANALYZER=false BUNDLE_ANALYZER_PORT=8889 CIRCULAR_CHECK=true CSS_EXTRACT=false DEV_SERVER_PORT=8080 HOT_RELOAD=true NODE_ENV=development SENTRY_URL=false SPEED_ANALYZER=false PUBLIC_URL=false # https://webpack.js.org/configuration/devtool DEV_TOOL=cheap-module-source-map 

.frontend.env.prod.example
 AGGREGATION_TIMEOUT=0 BUNDLE_ANALYZER=false BUNDLE_ANALYZER_PORT=8889 CIRCULAR_CHECK=false CSS_EXTRACT=true DEV_SERVER_PORT=8080 HOT_RELOAD=false NODE_ENV=production SENTRY_URL=false SPEED_ANALYZER=false PUBLIC_URL=/exchange_habr/dist # https://webpack.js.org/configuration/devtool DEV_TOOL=false 

рдЗрд╕ рдкреНрд░рдХрд╛рд░, рд╡рд┐рдзрд╛рдирд╕рднрд╛ рд╢реБрд░реВ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рдирд╛рдо .frontend.env рдФрд░ рд╕рднреА рдорд╛рдкрджрдВрдбреЛрдВ рдХреА рдЕрдирд┐рд╡рд╛рд░реНрдп рдЙрдкрд╕реНрдерд┐рддрд┐ рдХреЗ рд╕рд╛рде рдПрдХ рдлрд╝рд╛рдЗрд▓ рдмрдирд╛рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред рдпрд╣ рджреГрд╖реНрдЯрд┐рдХреЛрдг рдПрдХ рд╣реА рдмрд╛рд░ рдореЗрдВ рдХрдИ рд╕рдорд╕реНрдпрд╛рдУрдВ рдХрд╛ рд╕рдорд╛рдзрд╛рди рдХрд░реЗрдЧрд╛: рд╡реЗрдмрдкреИрдХ рдХреЗ рд▓рд┐рдП рдЕрд▓рдЧ рд╡рд┐рдиреНрдпрд╛рд╕ рдлрд╛рдЗрд▓ рдмрдирд╛рдиреЗ рдФрд░ рдЙрдирдХреА рд╕реНрдерд┐рд░рддрд╛ рдмрдирд╛рдП рд░рдЦрдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рдирд╣реАрдВ рд╣реИ; рд╕реНрдерд╛рдиреАрдп рд░реВрдк рд╕реЗ, рдЖрдк рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ рдХрд┐ рдХрд┐рд╕реА рд╡рд┐рд╢реЗрд╖ рдбреЗрд╡рд▓рдкрд░ рдХреЛ рдХрд┐рддрдиреА рдЬрд╝рд░реВрд░рдд рд╣реИ; рддреИрдирд╛рддреА cp .frontend.env.prod.example .frontend.env рдХреЗрд╡рд▓ рдЙрддреНрдкрд╛рджрди рдЕрд╕реЗрдВрдмрд▓реА ( cp .frontend.env.prod.example .frontend.env ) рдХреЗ рд▓рд┐рдП рдлрд╝рд╛рдЗрд▓ рдХреА рдкреНрд░рддрд┐рд▓рд┐рдкрд┐ cp .frontend.env.prod.example .frontend.env , рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рд╕реЗ рдорд╛рдиреЛрдВ рдХреЛ рд╕рдореГрджреНрдз рдХрд░реЗрдВрдЧреЗ, cp .frontend.env.prod.example .frontend.env рдбреЗрд╡рд▓рдкрд░реНрд╕ рдореЗрдВ рдкреНрд░рд╡реЗрд╢ рдХреЗ рдмрд┐рдирд╛ рдЪрд░ рдХреЗ рдорд╛рдзреНрдпрдо рд╕реЗ рдиреБрд╕реНрдЦрд╛ рдХрд╛ рдкреНрд░рдмрдВрдзрди рдХрд░рдиреЗ рдХреА рдХреНрд╖рдорддрд╛ рд╣реИред рдЗрд╕рдХреЗ рдЕрддрд┐рд░рд┐рдХреНрдд, рд╕реНрдЯреИрдВрдб рдХреЗ рд▓рд┐рдП рдПрдХ рдЙрджрд╛рд╣рд░рдг рдХреЙрдиреНрдлрд╝рд┐рдЧрд░реЗрд╢рди рдмрдирд╛рдирд╛ рд╕рдВрднрд╡ рд╣реЛрдЧрд╛ (рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рд╕реНрд░реЛрдд рдХреЗ рдирдХреНрд╢реЗ рдХреЗ рд╕рд╛рде)ред


CSS_EXTRACT рд╕рдХреНрд╖рдо рдХреЗ рд╕рд╛рде рдлрд╝рд╛рдЗрд▓реЛрдВ рдореЗрдВ рд╢реИрд▓рд┐рдпреЛрдВ рдХреЛ рдЕрд▓рдЧ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдореИрдВ рдорд┐рдиреА-рд╕реАрдПрд╕рдПрд╕-рдПрдХреНрд╕рдЯреНрд░реИрдХреНрдЯ-рдкреНрд▓рдЧрдЗрди рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реВрдВрдЧрд╛ - рдпрд╣ рдЖрдкрдХреЛ рд╣реЙрдЯ рд░реАрд▓реЛрдбрд┐рдВрдЧ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХреА рдЕрдиреБрдорддрд┐ рджреЗрддрд╛ рд╣реИред рдпрд╣реА рд╣реИ, рдпрджрд┐ рдЖрдк рд╕реНрдерд╛рдиреАрдп рд╡рд┐рдХрд╛рд╕ рдХреЗ рд▓рд┐рдП HOT_RELOAD рдФрд░ CSS_EXTRACT рдХреЛ рд╕рдХреНрд╖рдо рдХрд░рддреЗ рд╣реИрдВ , рддреЛ
рд╢реИрд▓рд┐рдпреЛрдВ рдХреА рдлрд╝рд╛рдЗрд▓реЛрдВ рдХреЛ рдмрджрд▓рддреЗ рд╕рдордп рдХреЗрд╡рд▓ рд╢реИрд▓рд┐рдпреЛрдВ рдХреЛ рдкреБрдирдГ рд▓реЛрдб рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛ - рд▓реЗрдХрд┐рди, рджреБрд░реНрднрд╛рдЧреНрдп рд╕реЗ, рд╕рдм рдХреБрдЫ, рдХреЗрд╡рд▓ рдкрд░рд┐рд╡рд░реНрддрд┐рдд рдлрд╝рд╛рдЗрд▓ рдирд╣реАрдВред CSS_EXTRACT рдмрдВрдж рд╣реЛрдиреЗ рдХреЗ рд╕рд╛рде, рдХреЗрд╡рд▓ рдкрд░рд┐рд╡рд░реНрддрд┐рдд рд╢реИрд▓реА рдореЙрдбреНрдпреВрд▓ рдЕрдкрдбреЗрдЯ рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛ред


рд░рд┐рдПрдХреНрдЯ рд╣реБрдХ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП HMR рдХреЛ рдХрд╛рдлреА рдорд╛рдирдХ рд░реВрдк рд╕реЗ рд╢рд╛рдорд┐рд▓ рдХрд┐рдпрд╛ рдЧрдпрд╛ рд╣реИ:


  • webpack.HotModuleReplacementPlugin рдкреНрд▓рдЧрдЗрдиреНрд╕ рдореЗрдВ;
  • hot: true рдорд╛рдкрджрдВрдбреЛрдВ рд╡реЗрдмрдкреИрдХ-рджреЗрд╡-рд╕рд░реНрд╡рд░ рдореЗрдВ hot: true ;
  • react-hot-loader/babel рдХреЛрд▓рд╛рд╣рд▓-рд▓реЛрдбрд░ рдкреНрд▓рдЧ рдЗрди рдореЗрдВ;
  • options.hmr: true mini-css-extract-plugin рдореЗрдВ options.hmr: true ;
  • рдЖрд╡реЗрджрди рдХреЗ рдореБрдЦреНрдп рдШрдЯрдХ рдореЗрдВ export default hot(App) ;
  • рд╕рд╛рдорд╛рдиреНрдп рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛-рдбреЛрдо рдХреЗ рдмрдЬрд╛рдп @ рд╣реЙрдЯ-рд▓реЛрдбрд░ / рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛-рдбреЛрдо (рдЖрд╕рд╛рдиреА рд╕реЗ resolve.alias: { 'react-dom': '@hot-loader/react-dom' } рдорд╛рдзреНрдпрдо рд╕реЗред resolve.alias: { 'react-dom': '@hot-loader/react-dom' } );

рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛-рд╣реЙрдЯ-рд▓реЛрдбрд░ рдХрд╛ рд╡рд░реНрддрдорд╛рди рд╕рдВрд╕реНрдХрд░рдг React.memo рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рддреЗ рд╣реБрдП React.memo рдШрдЯрдХреЛрдВ рдХрд╛ рд╕рдорд░реНрдерди рдирд╣реАрдВ рдХрд░рддрд╛ рд╣реИ, рдЗрд╕рд▓рд┐рдП рдЬрдм MobX рдХреЗ рд▓рд┐рдП рдбреЗрдХреЛрд░реЗрдЯрд░ рд▓рд┐рдЦрддреЗ рд╣реИрдВ, рддреЛ рдЖрдкрдХреЛ рд╕реНрдерд╛рдиреАрдп рд╡рд┐рдХрд╛рд╕ рдХреА рд╕реБрд╡рд┐рдзрд╛ рдХреЗ рд▓рд┐рдП рдЗрд╕реЗ рдзреНрдпрд╛рди рдореЗрдВ рд░рдЦрдирд╛ рд╣реЛрдЧрд╛ред рдЗрд╕рдХреЗ рдХрд╛рд░рдг рдПрдХ рдФрд░ рдЕрд╕реБрд╡рд┐рдзрд╛ рдпрд╣ рд╣реИ рдХрд┐ рдЬрдм рд░рд┐рдПрдХреНрдЯрд░ рдбреЗрд╡рд▓рдкрд░ рдЯреВрд▓реНрд╕ рдореЗрдВ рд╣рд╛рдЗрд▓рд╛рдЗрдЯ рдЕрдкрдбреЗрдЯ рд╕рдХреНрд╖рдо рд╣реЛрддрд╛ рд╣реИ, рддреЛ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рд╕рд╛рде рдХрд┐рд╕реА рднреА рдЗрдВрдЯрд░реИрдХреНрд╢рди рдХреЗ рджреМрд░рд╛рди рд╕рднреА рдШрдЯрдХреЛрдВ рдХреЛ рдЕрдкрдбреЗрдЯ рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИред рдЗрд╕рд▓рд┐рдП, рдкреНрд░рджрд░реНрд╢рди рдЕрдиреБрдХреВрд▓рди рдкрд░ рд╕реНрдерд╛рдиреАрдп рд░реВрдк рд╕реЗ рдХрд╛рдо рдХрд░рддреЗ рд╕рдордп, HOT_RELOAD рд╕реЗрдЯрд┐рдВрдЧ рдХреЛ рдЕрдХреНрд╖рдо рдХрд┐рдпрд╛ рдЬрд╛рдирд╛ рдЪрд╛рд╣рд┐рдПред


рд╡реЗрдмрдкреИрдХ 4 рдореЗрдВ рдЕрдиреБрдХреВрд▓рди рдХрд╛ рдирд┐рд░реНрдорд╛рдг рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рд░реВрдк рд╕реЗ рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ рдЬрдм mode : 'development' | 'production' mode : 'development' | 'production' ред рдЗрд╕ рдорд╛рдорд▓реЗ рдореЗрдВ, рдореИрдВ рдорд╛рдирдХ рдСрдкреНрдЯрд┐рдорд╛рдЗрдЬрд╝реЗрд╢рди (+ рдХреЗ рд╕рдорд╛рд╡реЗрд╢ рдХреЛ keep_fnames: true рдореЗрдВ рдорджрдж рдХрд░рддрд╛ keep_fnames: true рдШрдЯрдХреЛрдВ рдХреЗ рдирд╛рдо рдХреЛ рд╕рд╣реЗрдЬрдиреЗ рдХреЗ рд▓рд┐рдП terser-webpack-plugin рдореЗрдВ keep_fnames: true рдкреИрд░рд╛рдореАрдЯрд░), рдХреНрдпреЛрдВрдХрд┐ рдпрд╣ рдкрд╣рд▓реЗ рд╕реЗ рд╣реА рдЕрдЪреНрдЫреА рддрд░рд╣ рд╕реЗ рдЯреНрдпреВрди рд╣реИред


рдХреНрд▓рд╛рдЗрдВрдЯ рдХреИрд╢рд┐рдВрдЧ рдХреЗ рд╡рд┐рдЦрдВрдбрди рдФрд░ рдирд┐рдпрдВрддреНрд░рдг рдХрд╛ рд╡рд┐рд╢реЗрд╖ рдзреНрдпрд╛рди рджреЗрдирд╛ рдЪрд╛рд╣рд┐рдПред рд╕рд╣реА рд╕рдВрдЪрд╛рд▓рди рдХреЗ рд▓рд┐рдП рдЖрдкрдХреЛ рдЪрд╛рд╣рд┐рдП:


  • рдЙрддреНрдкрд╛рджрди рдореЗрдВред рдЬреЗрдПрд╕ рдФрд░ рд╕реАрдПрд╕рдПрд╕ рдлрд╝рд╛рдЗрд▓реЛрдВ рдХреЗ рд▓рд┐рдП рдлрд╝рд╛рдЗрд▓ рдирд╛рдо рдирд┐рд░реНрджрд┐рд╖реНрдЯ рд╣реИ isProduction ? '[name].[contenthash].js' : '[name].js' isProduction ? '[name].[contenthash].js' : '[name].js' (рдХреНрд░рдорд╢рдГ .css рдХреНрд░рдорд╢рдГ) рддрд╛рдХрд┐ рдлрд╝рд╛рдЗрд▓ рдХрд╛ рдирд╛рдо рдЗрд╕рдХреА рд╕рд╛рдордЧреНрд░реА рдкрд░ рдЖрдзрд╛рд░рд┐рдд рд╣реЛ;
  • рдЕрдиреБрдХреВрд▓рди рдореЗрдВ, рдорд╛рдкрджрдВрдбреЛрдВ рдХреЛ chunkIds: 'named', moduleIds: 'hashed' рддрд╛рдХрд┐ chunkIds: 'named', moduleIds: 'hashed' рдореЗрдВ рдЖрдВрддрд░рд┐рдХ рдореЙрдбреНрдпреВрд▓ рдХрд╛рдЙрдВрдЯрд░ рди рдмрджрд▓реЗ;
  • рдПрдХ рдЕрд▓рдЧ рдЪрдВрдХ рдореЗрдВ рд░рдирдЯрд╛рдЗрдо рдбрд╛рд▓реЗрдВ;
  • рдХреИрд╢ рд╕рдореВрд╣реЛрдВ рдХреЛ рд╡рд┐рднрд╛рдЬрд┐рдд рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рд╕реНрдерд╛рдирд╛рдВрддрд░рд┐рдд рдХрд░реЗрдВ (рдЪрд╛рд░ рдмрд┐рдВрджреБ рдЗрд╕ рдЖрд╡реЗрджрди рдХреЗ рд▓рд┐рдП рдкрд░реНрдпрд╛рдкреНрдд рд╣реИрдВ - рд▓реЙрд╢, рд╕рдВрддрд░реА, рд╣рд╛рдИрдЪрд╛рд░реНрдЯ рдФрд░ рдиреЛрдб рд╕реЗ рдЕрдиреНрдп рдирд┐рд░реНрднрд░рддрд╛ рдХреЗ рд▓рд┐рдП рд╡рд┐рдХреНрд░реЗрддрд╛)ред рдЪреВрдВрдХрд┐ рдкрд╣рд▓реЗ рддреАрди рдХреЛ рд╢рд╛рдпрдж рд╣реА рдХрднреА рдЕрдкрдбреЗрдЯ рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛, рд╡реЗ рдпрдерд╛рд╕рдВрднрд╡ рд▓рдВрдмреЗ рд╕рдордп рддрдХ рдХреНрд▓рд╛рдЗрдВрдЯ рдХреЗ рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдХреИрд╢ рдореЗрдВ рдмрдиреЗ рд░рд╣реЗрдВрдЧреЗред

webpack-custom / config / configOptimization.js
 /** * @docs: https://webpack.js.org/configuration/optimization * */ const TerserPlugin = require('terser-webpack-plugin'); module.exports = { runtimeChunk: { name: 'runtime', }, chunkIds: 'named', moduleIds: 'hashed', mergeDuplicateChunks: true, splitChunks: { cacheGroups: { lodash: { test: module => module.context.indexOf('node_modules\\lodash') !== -1, name: 'lodash', chunks: 'all', enforce: true, }, sentry: { test: module => module.context.indexOf('node_modules\\@sentry') !== -1, name: 'sentry', chunks: 'all', enforce: true, }, highcharts: { test: module => module.context.indexOf('node_modules\\highcharts') !== -1, name: 'highcharts', chunks: 'all', enforce: true, }, vendor: { test: module => module.context.indexOf('node_modules') !== -1, priority: -1, name: 'vendor', chunks: 'all', enforce: true, }, }, }, minimizer: [ new TerserPlugin({ terserOptions: { keep_fnames: true, }, }), ], }; 

рдЗрд╕ рдкрд░рд┐рдпреЛрдЬрдирд╛ рдореЗрдВ рдЕрд╕реЗрдВрдмрд▓реА рдХреЛ рдЧрддрд┐ рджреЗрдиреЗ рдХреЗ рд▓рд┐рдП рдореИрдВ рдереНрд░реЗрдб-рд▓реЛрдбрд░ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рддрд╛ рд╣реВрдВ - рдЬрдм 4 рдкреНрд░рдХреНрд░рд┐рдпрд╛рдУрдВ рдХреЗ рд╕рдорд╛рдирд╛рдВрддрд░ рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ, рддреЛ рдпрд╣ рдЕрд╕реЗрдВрдмрд▓реА рддреНрд╡рд░рдг рдХреЛ 90% рддрдХ рджреЗрддрд╛ рд╣реИ, рдЬреЛ рд╕рдорд╛рди рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рдХреЗ рд╕рд╛рде рд╣реИрдкреНрдкреАрдкреИрдХ рд╕реЗ рдмреЗрд╣рддрд░ рд╣реИред


рд▓реЛрдбрд░ рдХреЗ рд▓рд┐рдП рд╕реЗрдЯрд┐рдВрдЧреНрд╕, рдмрд╛рдмреЗрд▓ рдХреЗ рд▓рд┐рдП, рдЕрд▓рдЧ-рдЕрд▓рдЧ рдлрд╝рд╛рдЗрд▓реЛрдВ (рдЬреИрд╕реЗ .babelrc ) рдореЗрдВ рдбрд╛рд▓рдиреЗ рдХреЗ рд▓рд┐рдП, рдореБрдЭреЗ рд▓рдЧрддрд╛ рд╣реИ, рдЕрдирд╛рд╡рд╢реНрдпрдХ рд╣реИред рд▓реЗрдХрд┐рди рдХреНрд░реЙрд╕-рдмреНрд░рд╛рдЙрдЬрд╝рд░ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░реЗрд╢рди рдореБрдЦреНрдп рдкреИрдХреЗрдЬ рдХреЗ browserslist рдкреИрд░рд╛рдореАрдЯрд░ рдореЗрдВ рд░рдЦрдиреЗ рдХреЗ рд▓рд┐рдП рдЕрдзрд┐рдХ рд╕реБрд╡рд┐рдзрд╛рдЬрдирдХ рд╣реИред рдЬреЗрд╕рди, рдХреНрдпреЛрдВрдХрд┐ рдЗрд╕рдХрд╛ рдЙрдкрдпреЛрдЧ рдСрдЯреЛрдкреНрд░рд┐рдлрд╝рд░ рд╢реИрд▓рд┐рдпреЛрдВ рдХреЗ рд▓рд┐рдП рднреА рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИред


рдкреНрд░рд┐рдЯрд┐рдпрд░ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рдиреЗ рдХреА рд╕реБрд╡рд┐рдзрд╛ рдХреЗ рд▓рд┐рдП, рдореИрдВрдиреЗ AGGREGATION_TIMEOUT рдкреИрд░рд╛рдореАрдЯрд░ рдмрдирд╛рдпрд╛, рдЬреЛ рдЖрдкрдХреЛ рдлрд╛рдЗрд▓реЛрдВ рдореЗрдВ рдкрд░рд┐рд╡рд░реНрддрди рдХрд╛ рдкрддрд╛ рд▓рдЧрд╛рдиреЗ рдФрд░ рджреЗрд╡-рд╕рд░реНрд╡рд░ рдореЛрдб рдореЗрдВ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рдкреБрдирд░реНрдирд┐рд░реНрдорд╛рдг рдХреЗ рдмреАрдЪ рджреЗрд░реА рдХреЛ рд╕реЗрдЯ рдХрд░рдиреЗ рдХреА рдЕрдиреБрдорддрд┐ рджреЗрддрд╛ рд╣реИред рдЪреВрдВрдХрд┐ рдореИрдВрдиреЗ IDE рдХреЛ рд╕рд╣реЗрдЬрддреЗ рд╕рдордп рдлрд╝рд╛рдЗрд▓реЛрдВ рдХреЗ рдкреБрди: рд╕реНрд╡рд░реВрдкрдг рдХреЛ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд┐рдпрд╛ рдерд╛, рдЗрд╕ рдХрд╛рд░рдг 2 рдкреБрдирд░реНрдирд┐рд░реНрдорд╛рдг рд╣реЛрддреЗ рд╣реИрдВ - рдкрд╣рд▓рд╛ рдореВрд▓ рдлрд╝рд╛рдЗрд▓ рдХреЛ рд╕рд╣реЗрдЬрдиреЗ рдХреЗ рд▓рд┐рдП, рджреВрд╕рд░рд╛ рд╕реНрд╡рд░реВрдкрдг рдХреЛ рдкреВрд░рд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдПред 2000 рдорд┐рд▓реАрд╕реЗрдХрдВрдб рдЖрдорддреМрд░ рдкрд░ рд╡реЗрдмрдкреИрдХ рдХреЗ рд▓рд┐рдП рдлрд╝рд╛рдЗрд▓ рдХреЗ рдЕрдВрддрд┐рдо рд╕рдВрд╕реНрдХрд░рдг рдХреА рдкреНрд░рддреАрдХреНрд╖рд╛ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП рдкрд░реНрдпрд╛рдкреНрдд рд╣реИред


рдмрд╛рдХреА рдХреЙрдиреНрдлрд╝рд┐рдЧрд░реЗрд╢рди рд╡рд┐рд╢реЗрд╖ рдзреНрдпрд╛рди рджреЗрдиреЗ рдпреЛрдЧреНрдп рдирд╣реАрдВ рд╣реИ, рдХреНрдпреЛрдВрдХрд┐ рдпрд╣ рд╢реБрд░реБрдЖрддреА рд▓реЛрдЧреЛрдВ рдХреЗ рд▓рд┐рдП рд╕реИрдХрдбрд╝реЛрдВ рдкреНрд░рд╢рд┐рдХреНрд╖рдг рд╕рд╛рдордЧреНрд░реА рдореЗрдВ рдЦреБрд▓рд╛рд╕рд╛ рдХрд┐рдпрд╛ рдЧрдпрд╛ рд╣реИ, рдЗрд╕рд▓рд┐рдП рдЖрдк рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдЖрд░реНрдХрд┐рдЯреЗрдХреНрдЪрд░ рдХреЗ рдбрд┐рдЬрд╛рдЗрди рдХреЗ рд▓рд┐рдП рдЖрдЧреЗ рдмрдврд╝ рд╕рдХрддреЗ рд╣реИрдВред


рд╢реИрд▓реА рдереАрдореНрд╕


рдкрд╣рд▓реЗ, рдереАрдо рдмрдирд╛рдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рд╢реИрд▓рд┐рдпреЛрдВ рдХреЗ рд╡рд╛рдВрдЫрд┐рдд рд╕реЗрдЯ рдХреЛ рд▓реЛрдб рдХрд░рддреЗ рд╣реБрдП, рд╡рд┐рд╖рдпреЛрдВ рдХреЛ рдмрджрд▓рддреЗ рд╕рдордп * .css рдлрд╝рд╛рдЗрд▓реЛрдВ рдХреЗ рдХрдИ рд╕рдВрд╕реНрдХрд░рдг рдмрдирд╛рдиреЗ рдФрд░ рдкреГрд╖реНрда рдХреЛ рдлрд┐рд░ рд╕реЗ рд▓реЛрдб рдХрд░рдирд╛ рдерд╛ред рдЕрдм рдХрд╕реНрдЯрдо рд╕реАрдПрд╕рдПрд╕ рдЧреБрдг рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рд╕рдм рдХреБрдЫ рдЖрд╕рд╛рдиреА рд╕реЗ рд╣рд▓ рд╣реЛ рдЧрдпрд╛ рд╣реИред рдпрд╣ рддрдХрдиреАрдХ рд╡рд░реНрддрдорд╛рди рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рд╕рднреА рд▓рдХреНрд╖рд┐рдд рдмреНрд░рд╛рдЙрдЬрд╝рд░реЛрдВ рджреНрд╡рд╛рд░рд╛ рд╕рдорд░реНрдерд┐рдд рд╣реИ, рд▓реЗрдХрд┐рди IE рдХреЗ рд▓рд┐рдП рдкреЙрд▓реАрдлрд┐рд▓ рднреА рд╣реИрдВред


рдорд╛рди рд▓реАрдЬрд┐рдП рдХрд┐ 2 рдереАрдо рд╣реИрдВ - рд╣рд▓реНрдХрд╛ рдФрд░ рдЧрд╣рд░рд╛, рд░рдВрдЧ рд╕реЗрдЯ рдЬрд┐рд╕рдХреЗ рд▓рд┐рдП рдореЗрдВ рд╣реЛрдЧрд╛


рд╢реИрд▓рд┐рдпреЛрдВ / рд╡рд┐рд╖рдпреЛрдВред scss
 .light { --n0: rgb(255, 255, 255); --n100: rgb(186, 186, 186); --n10: rgb(249, 249, 249); --n10a3: rgba(249, 249, 249, 0.3); --n20: rgb(245, 245, 245); --n30: rgb(221, 221, 221); --n500: rgb(136, 136, 136); --n600: rgb(102, 102, 102); --n900: rgb(0, 0, 0); --b100: rgb(219, 237, 251); --b300: rgb(179, 214, 252); --b500: rgb(14, 123, 249); --b500a3: rgba(14, 123, 249, 0.3); --b900: rgb(32, 39, 57); --g400: rgb(71, 215, 141); --g500: rgb(61, 189, 125); --g500a1: rgba(61, 189, 125, 0.1); --g500a2: rgba(61, 189, 125, 0.2); --r400: rgb(255, 100, 100); --r500: rgb(255, 0, 0); --r500a1: rgba(255, 0, 0, 0.1); --r500a2: rgba(255, 0, 0, 0.2); } .dark { --n0: rgb(25, 32, 48); --n100: rgb(114, 126, 151); --n10: rgb(39, 46, 62); --n10a3: rgba(39, 46, 62, 0.3); --n20: rgb(25, 44, 74); --n30: rgb(67, 75, 111); --n500: rgb(117, 128, 154); --n600: rgb(255, 255, 255); --n900: rgb(255, 255, 255); --b100: rgb(219, 237, 251); --b300: rgb(39, 46, 62); --b500: rgb(14, 123, 249); --b500a3: rgba(14, 123, 249, 0.3); --b900: rgb(32, 39, 57); --g400: rgb(0, 220, 103); --g500: rgb(0, 197, 96); --g500a1: rgba(0, 197, 96, 0.1); --g500a2: rgba(0, 197, 96, 0.2); --r400: rgb(248, 23, 1); --r500: rgb(221, 23, 1); --r500a1: rgba(221, 23, 1, 0.1); --r500a2: rgba(221, 23, 1, 0.2); } 

рдЗрди рдЪрд░ рдХреЛ рд╡рд┐рд╢реНрд╡ рд╕реНрддрд░ рдкрд░ рд▓рд╛рдЧреВ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдЙрдиреНрд╣реЗрдВ рдХреНрд░рдорд╢рдГ document.documentElement рд▓рд┐рдЦрд╛ рдЬрд╛рдирд╛ рдЪрд╛рд╣рд┐рдП, рдЗрд╕ рдлрд╛рдЗрд▓ рдХреЛ рдЬрд╛рд╡рд╛рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдСрдмреНрдЬреЗрдХреНрдЯ рдореЗрдВ рдмрджрд▓рдиреЗ рдХреЗ рд▓рд┐рдП рдПрдХ рдЫреЛрдЯреЗ рд╕реЗ рдкрд╛рд░реНрд╕рд░ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реЛрддреА рд╣реИред рдмрд╛рдж рдореЗрдВ рдореИрдВ рдЖрдкрдХреЛ рдмрддрд╛рдКрдВрдЧрд╛ рдХрд┐ рдЗрд╕реЗ рдЕрднреА рдЬрд╛рд╡рд╛рд╕реНрдХреНрд░рд┐рдкреНрдЯ рдореЗрдВ рд╕рдВрдЧреНрд░рд╣реАрдд рдХрд░рдиреЗ рдХреА рддреБрд▓рдирд╛ рдореЗрдВ рдЕрдзрд┐рдХ рд╕реБрд╡рд┐рдзрд╛рдЬрдирдХ рдХреНрдпреЛрдВ рд╣реИред


webpack- рдХрд╕реНрдЯрдо / рдмрд░реНрддрди / sassVariablesLoader.js
 function convertSourceToJsObject(source) { const themesObject = {}; const fullThemesArray = source.match(/\.([^}]|\s)*}/g) || []; fullThemesArray.forEach(fullThemeStr => { const theme = fullThemeStr .match(/\.\w+\s{/g)[0] .replace(/\W/g, ''); themesObject[theme] = {}; const variablesMatches = fullThemeStr.match(/--(.*:[^;]*)/g) || []; variablesMatches.forEach(varMatch => { const [key, value] = varMatch.split(': '); themesObject[theme][key] = value; }); }); return themesObject; } function checkThemesEquality(themes) { const themesArray = Object.keys(themes); themesArray.forEach(themeStr => { const themeObject = themes[themeStr]; const otherThemesArray = themesArray.filter(t => t !== themeStr); Object.keys(themeObject).forEach(variableName => { otherThemesArray.forEach(otherThemeStr => { const otherThemeObject = themes[otherThemeStr]; if (!otherThemeObject[variableName]) { throw new Error( `checkThemesEquality: theme ${otherThemeStr} has no variable ${variableName}` ); } }); }); }); } module.exports = function sassVariablesLoader(source) { const themes = convertSourceToJsObject(source); checkThemesEquality(themes); return `module.exports = ${JSON.stringify(themes)}`; }; 

рдпрд╣рд╛рдВ, рдЙрд╕ рджреНрд╡рд╛рд░рд╛ рд╕реНрдерд┐рд░рддрд╛ рдХреА рдЬрд╛рдВрдЪ рдХреА рдЬрд╛рддреА рд╣реИ - рдЕрд░реНрдерд╛рдд, рдЪрд░ рдХреЗ рд╕реЗрдЯ рдХрд╛ рдкреВрд░рд╛ рдкрддреНрд░рд╛рдЪрд╛рд░, рдЬрд┐рд╕рдХреЗ рдЕрдВрддрд░ рдХреЗ рд╕рд╛рде рд╡рд┐рдзрд╛рдирд╕рднрд╛ рдЧрд┐рд░рддреА рд╣реИред


рдЗрд╕ рд▓реЛрдбрд░ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рддреЗ рд╕рдордп, рдорд╛рдкрджрдВрдбреЛрдВ рдХреЗ рд╕рд╛рде рдПрдХ рдмрд╣реБрдд рд╕реБрдВрджрд░ рд╡рд╕реНрддреБ рдкреНрд░рд╛рдкреНрдд рдХреА рдЬрд╛рддреА рд╣реИ, рдФрд░ рдереАрдо рдкрд░рд┐рд╡рд░реНрддрди рдЙрдкрдпреЛрдЧрд┐рддрд╛ рдХреЗ рд▓рд┐рдП рдХреБрдЫ рдкрдВрдХреНрддрд┐рдпрд╛рдБ рдкрд░реНрдпрд╛рдкреНрдд рд╣реИрдВ:


src / utils / setTheme.js
 import themes from 'styles/themes.scss'; const root = document.documentElement; export function setTheme(theme) { Object.entries(themes[theme]).forEach(([key, value]) => { root.style.setProperty(key, value); }); } 

рдореИрдВ рдЗрди рд╕реАрдПрд╕рдПрд╕ рдЪрд░ рдХреЛ * .scss рдХреЗ рд▓рд┐рдП рдорд╛рдирдХ рд▓реЛрдЧреЛрдВ рдореЗрдВ рдЕрдиреБрд╡рд╛рдж рдХрд░рдирд╛ рдкрд╕рдВрдж рдХрд░рддрд╛ рд╣реВрдВ :


src / style / constants.scss


рд╡реЗрдмрд╕реНрдЯреЙрд░реНрдо рдЖрдИрдбреАрдИ, рдЬреИрд╕рд╛ рдХрд┐ рд╕реНрдХреНрд░реАрдирд╢реЙрдЯ рдореЗрдВ рджреЗрдЦрд╛ рдЧрдпрд╛ рд╣реИ, рдмрд╛рдИрдВ рдУрд░ рдкреИрдирд▓ рдореЗрдВ рд░рдВрдЧреЛрдВ рдХреЛ рджрд┐рдЦрд╛рддрд╛ рд╣реИ рдФрд░ рд░рдВрдЧ рдкрд░ рдХреНрд▓рд┐рдХ рдХрд░рдХреЗ рдПрдХ рдкреИрд▓реЗрдЯ рдЦреЛрд▓рддрд╛ рд╣реИ рдЬрд╣рд╛рдВ рдЖрдк рдЗрд╕реЗ рдмрджрд▓ рд╕рдХрддреЗ рд╣реИрдВред рдирдпрд╛ рд░рдВрдЧ рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рд░реВрдк рд╕реЗ theme.scss рдореЗрдВ рдкреНрд░рддрд┐рд╕реНрдерд╛рдкрд┐рдд рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ , рд╣реЙрдЯ рд░реАрд▓реЛрдб рдХрд╛рдо рдХрд░реЗрдЧрд╛ рдФрд░ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рддреБрд░рдВрдд рд░реВрдкрд╛рдВрддрд░рд┐рдд рд╣реЛ рдЬрд╛рдПрдЧрд╛ред рдпрд╣ рдареАрдХ 2019 рдореЗрдВ рдЕрдкреЗрдХреНрд╖рд┐рдд рд╡рд┐рдХрд╛рд╕ рд╕реБрд╡рд┐рдзрд╛ рдХрд╛ рд╕реНрддрд░ рд╣реИред


рдХреЛрдб рд╕рдВрдЧрдарди рд╕рд┐рджреНрдзрд╛рдВрдд


рдЗрд╕ рдкрд░рд┐рдпреЛрдЬрдирд╛ рдореЗрдВ, рдореИрдВ рдШрдЯрдХреЛрдВ, рдлрд╝рд╛рдЗрд▓реЛрдВ рдФрд░ рд╢реИрд▓рд┐рдпреЛрдВ рдХреЗ рд▓рд┐рдП рдлрд╝реЛрд▓реНрдбрд░ рдирд╛рдореЛрдВ рдХреА рдирдХрд▓ рдХрд░реВрдВрдЧрд╛, рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП:

 . |-- components | |-- Chart | | `-- Chart.js | | `-- Chart.scss | | `-- package.json 

рддрджрдиреБрд╕рд╛рд░, package.json рдореЗрдВ рд╕рд╛рдордЧреНрд░реА { "main": "Chart.js" } ред рдХрдИ рдирд╛рдорд┐рдд рдирд┐рд░реНрдпрд╛рдд рд╡рд╛рд▓реЗ рдШрдЯрдХреЛрдВ рдХреЗ рд▓рд┐рдП (рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП, рдЙрдкрдпреЛрдЧрд┐рддрд╛рдУрдВ), рдореБрдЦреНрдп рдлрд╝рд╛рдЗрд▓ рдХрд╛ рдирд╛рдо рдПрдХ рдЕрдВрдбрд░рд╕реНрдХреЛрд░ рдХреЗ рд╕рд╛рде рд╢реБрд░реВ рд╣реЛрдЧрд╛:


 . |-- utils | `-- _utils.js | `-- someUtil.js | `-- anotherUtil.js | `-- package.json 

рдФрд░ рдмрд╛рдХреА рдлрд╛рдЗрд▓реЛрдВ рдХреЛ рдирд┐рд░реНрдпрд╛рдд рдХрд┐рдпрд╛ рдЬрд╛рдПрдЧрд╛:


 export * from './someUtil'; export * from './anotherUtil'; 

рдпрд╣ рдЖрдкрдХреЛ рдбреБрдкреНрд▓рд┐рдХреЗрдЯ рдлрд╝рд╛рдЗрд▓ рдирд╛рдореЛрдВ рд╕реЗ рдЫреБрдЯрдХрд╛рд░рд╛ рдкрд╛рдиреЗ рдХреА рдЕрдиреБрдорддрд┐ рджреЗрдЧрд╛ рддрд╛рдХрд┐ рд╢реАрд░реНрд╖ рджрд╕ рдЦреБрд▓реЗ рд╕реВрдЪрдХрд╛рдВрдХ рдореЗрдВ рдЦреЛ рди рдЬрд╛рдПрдВ ред js / style.scss ред рдЖрдк рдЗрд╕реЗ рдЖрдИрдбреАрдИ рдкреНрд▓рдЧрдЗрдиреНрд╕ рдХреЗ рд╕рд╛рде рд╣рд▓ рдХрд░ рд╕рдХрддреЗ рд╣реИрдВ, рд▓реЗрдХрд┐рди рд╕рд╛рд░реНрд╡рднреМрдорд┐рдХ рддрд░реАрдХреЗ рд╕реЗ рдХреНрдпреЛрдВ рдирд╣реАрдВред


рдореИрдВ рд╕рдВрджреЗрд╢ / рд▓рд┐рдВрдХ рдЬреИрд╕реЗ рд╕рд╛рдорд╛рдиреНрдп рд▓реЛрдЧреЛрдВ рдХреЛ рдЫреЛрдбрд╝рдХрд░, рдкреЗрдЬ рджреНрд╡рд╛рд░рд╛ рдШрдЯрдХреЛрдВ рдХреЗ рдкреГрд╖реНрда рдХреЛ рд╕рдореВрд╣рд┐рдд рдХрд░реВрдВрдЧрд╛, рдФрд░ рдпрджрд┐ рд╕рдВрднрд╡ рд╣реЛ рддреЛ, рдирд╛рдо рдХреА рдПрдХрд░реВрдкрддрд╛ рдмрдирд╛рдП рд░рдЦрдиреЗ рдХреЗ рд▓рд┐рдП рдирд╛рдо рдирд┐рд░реНрдпрд╛рдд (рдмрд┐рдирд╛ export default ) рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░реЗрдВ, рд░рд┐рдлреИрдХреНрдЯрд┐рдВрдЧ рдФрд░ рдкрд░рд┐рдпреЛрдЬрдирд╛ рдЦреЛрдЬ рдореЗрдВ рдЖрд╕рд╛рдиреАред


MobX рд░реЗрдВрдбрд░рд┐рдВрдЧ рдФрд░ рд╕реНрдЯреЛрд░реЗрдЬ рдХреЛ рдХреЙрдиреНрдлрд╝рд┐рдЧрд░ рдХрд░реЗрдВ


рд╡реЗрдмрдкреИрдХ рдХреЗ рд▓рд┐рдП рдкреНрд░рд╡реЗрд╢ рдмрд┐рдВрджреБ рдХреЗ рд░реВрдк рдореЗрдВ рдХрд╛рд░реНрдп рдХрд░рдиреЗ рд╡рд╛рд▓реА рдлрд╝рд╛рдЗрд▓ рдЗрд╕ рддрд░рд╣ рджрд┐рдЦрд╛рдИ рджреЗрдЧреА:


src / app.js
 import './polyfill'; import './styles/reset.scss'; import './styles/global.scss'; import { initSentry, renderToDOM } from 'utils'; import { initAutorun } from './autorun'; import { store } from 'stores'; import App from 'components/App'; initSentry(); initAutorun(store); renderToDOM(App); 

рдЬрдм рд╕реЗ рд╡реЗрдзрд╢рд╛рд▓рд╛рдУрдВ рдХреЗ рд╕рд╛рде рдХрд╛рдо рдХрд░рддреЗ рд╣реИрдВ, рдХрдВрд╕реЛрд▓ Proxy {0: "btc", 1: "eth", 2: "usd", 3: "test", Symbol(mobx administration): ObservableArrayAdministration} рдореЗрдВ рдкреНрд░рджрд░реНрд╢рд┐рдд рдХрд░рддрд╛ рд╣реИред рдореИрдВ рдорд╛рдирдХреАрдХрд░рдг рдХреЗ рд▓рд┐рдП рдПрдХ рдЙрдкрдпреЛрдЧрд┐рддрд╛ рдмрдирд╛рдКрдВрдЧрд╛:


src / polyfill.js
 import { toJS } from 'mobx'; console.js = function consoleJsCustom(...args) { console.log(...args.map(arg => toJS(arg))); }; 

рдЗрд╕рдХреЗ рдЕрд▓рд╛рд╡рд╛, рд╡рд┐рднрд┐рдиреНрди рдмреНрд░рд╛рдЙрдЬрд╝рд░реЛрдВ рдХреЗ рд▓рд┐рдП рд╡реИрд╢реНрд╡рд┐рдХ рд╢реИрд▓реА рдФрд░ рд╢реИрд▓реА рд╕рд╛рдорд╛рдиреНрдпреАрдХрд░рдг рдореБрдЦреНрдп рдлрд╝рд╛рдЗрд▓ рдореЗрдВ рдЬреБрдбрд╝реЗ рд╣реБрдП рд╣реИрдВ, рдЕрдЧрд░ рд╕рдВрддрд░реА рдХреЗ рд▓рд┐рдП рдПрдХ рдХреБрдВрдЬреА рд╣реИред in.vv.frontend рддреНрд░реБрдЯрд┐рдпреЛрдВ рдХреЛ рд▓реЙрдЧ рдЗрди рдХрд░рдирд╛ рд╢реБрд░реВ рд╣реЛрддрд╛ рд╣реИ, MobX рднрдВрдбрд╛рд░рдг рдмрдирд╛рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ, рдСрдЯреЛрд░рди рдХреЗ рд╕рд╛рде рдкреИрд░рд╛рдореАрдЯрд░ рдкрд░рд┐рд╡рд░реНрддрди рдХреА рдЯреНрд░реИрдХрд┐рдВрдЧ рд╢реБрд░реВ рдХреА рдЬрд╛рддреА рд╣реИ рдФрд░ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛-рд╣реЙрдЯ-рд▓реЛрдбрд░ рдореЗрдВ рд▓рд┐рдкрдЯреЗ рдШрдЯрдХ рдХреЛ рдорд╛рдЙрдВрдЯ рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИред рдбреЛрдо рдореЗрдВред


рд░рд┐рдкреЙрдЬрд┐рдЯрд░реА рдЕрдкрдиреЗ рдЖрдк рдореЗрдВ рдПрдХ рдиреЙрди-рдСрдмреНрдЬрд░реНрд╡реЗрдмрд▓ рдХреНрд▓рд╛рд╕ рд╣реЛрдЧреА, рдЬрд┐рд╕рдХреЗ рдкреИрд░рд╛рдореАрдЯрд░ рдСрдмреНрдЬрд░реНрд╡реЗрдмрд▓ рдорд╛рдкрджрдВрдбреЛрдВ рдХреЗ рд╕рд╛рде рдиреЙрди-рдСрдмреНрдЬрд░реНрд╡реЗрдмрд▓ рдХреНрд▓рд╛рд╕ рд╣реЛрддреЗ рд╣реИрдВред рдЗрд╕ рдкреНрд░рдХрд╛рд░, рдпрд╣ рд╕рдордЭрд╛ рдЬрд╛рддрд╛ рд╣реИ рдХрд┐ рдорд╛рдкрджрдВрдбреЛрдВ рдХрд╛ рд╕реЗрдЯ рдЧрддрд┐рд╢реАрд▓ рдирд╣реАрдВ рд╣реЛрдЧрд╛ - рдЗрд╕рд▓рд┐рдП, рдЖрд╡реЗрджрди рдЕрдзрд┐рдХ рдЕрдиреБрдорд╛рдирд┐рдд рд╣реЛрдЧрд╛ред рдпрд╣ рдЙрди рдХреБрдЫ рдЬрдЧрд╣реЛрдВ рдореЗрдВ рд╕реЗ рдПрдХ рд╣реИ, рдЬрд╣рд╛рдБ рдЖрдИрдбреАрдПрд╕рдИ рдореЗрдВ рдСрдЯреЛ-рдкреВрд░реНрддрд┐ рдХреЛ рд╕рдХреНрд╖рдо рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП JSDoc рдХрд╛рдо рдЖрддрд╛ рд╣реИред


src / store / RootStore.js
 import { I18nStore } from './I18nStore'; import { RatesStore } from './RatesStore'; import { GlobalStore } from './GlobalStore'; import { RouterStore } from './RouterStore'; import { CurrentTPStore } from './CurrentTPStore'; import { MarketsListStore } from './MarketsListStore'; /** * @name RootStore */ export class RootStore { constructor() { this.i18n = new I18nStore(this); this.rates = new RatesStore(this); this.global = new GlobalStore(this); this.router = new RouterStore(this); this.currentTP = new CurrentTPStore(this); this.marketsList = new MarketsListStore(this); } } 

MobX рд╕реНрдЯреЛрд░ рдХрд╛ рдПрдХ рдЙрджрд╛рд╣рд░рдг GlobalStore рдХреЗ рдЙрджрд╛рд╣рд░рдг рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рд╡рд┐рд╢реНрд▓реЗрд╖рдг рдХрд┐рдпрд╛ рдЬрд╛ рд╕рдХрддрд╛ рд╣реИ, рдЬрд┐рд╕рдХрд╛ рд╡рд░реНрддрдорд╛рди рдореЗрдВ рдПрдХрдорд╛рддреНрд░ рдЙрджреНрджреЗрд╢реНрдп рд╣реЛрдЧрд╛ - рд╡рд░реНрддрдорд╛рди рд╢реИрд▓реА рдереАрдо рдХреЛ рд╕реНрдЯреЛрд░ рдФрд░ рд╕реЗрдЯ рдХрд░рдирд╛ред


src / рд╕реНрдЯреЛрд░ / GlobalStore.js
 import { makeObservable, setTheme } from 'utils'; import themes from 'styles/themes.scss'; const themesList = Object.keys(themes); @makeObservable export class GlobalStore { /** * @param rootStore {RootStore} */ constructor(rootStore) { this.rootStore = rootStore; setTheme(themesList[0]); } themesList = themesList; currentTheme = ''; setTheme(theme) { this.currentTheme = theme; setTheme(theme); } } 

рдХрднреА-рдХрднреА рдХрдХреНрд╖рд╛ рдХреЗ рдкреИрд░рд╛рдореАрдЯрд░ рдФрд░ рд╡рд┐рдзрд┐ рдореИрдиреНрдпреБрдЕрд▓ рд░реВрдк рд╕реЗ рдбреЗрдХреЛрд░реЗрдЯрд░ рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рд╕реЗрдЯ рдХрд░рддреЗ рд╣реИрдВ, рдЙрджрд╛рд╣рд░рдг рдХреЗ рд▓рд┐рдП:


 export class GlobalStore { @observable currentTheme = ''; @action.bound setTheme(theme) { this.currentTheme = theme; setTheme(theme); } } 

рд▓реЗрдХрд┐рди рдореБрдЭреЗ рдЗрд╕рдХрд╛ рдХреЛрдИ рдорддрд▓рдм рдирд╣реАрдВ рд╣реИ, рдХреНрдпреЛрдВрдХрд┐ рдкреБрд░рд╛рдиреЗ рдкреНрд░рд╕реНрддрд╛рд╡ рд╡рд░реНрдЧ рд╕рдЬреНрдЬрд╛рдХрд╛рд░ рдЕрдкрдиреЗ рд╕реНрд╡рдЪрд╛рд▓рд┐рдд рдкрд░рд┐рд╡рд░реНрддрди рдХрд╛ рд╕рдорд░реНрдерди рдХрд░рддреЗ рд╣реИрдВ, рдЗрд╕рд▓рд┐рдП рдирд┐рдореНрдирд▓рд┐рдЦрд┐рдд рдЙрдкрдпреЛрдЧрд┐рддрд╛ рдкрд░реНрдпрд╛рдкреНрдд рд╣реИ:


src / utils / makeObservable.js
 import { action, computed, decorate, observable } from 'mobx'; export function makeObservable(target) { /** *   -   this +    *     * *   -   computed * */ const classPrototype = target.prototype; const methodsAndGetters = Object.getOwnPropertyNames(classPrototype).filter( methodName => methodName !== 'constructor' ); for (const methodName of methodsAndGetters) { const descriptor = Object.getOwnPropertyDescriptor( classPrototype, methodName ); descriptor.value = decorate(classPrototype, { [methodName]: typeof descriptor.value === 'function' ? action.bound : computed, }); } return (...constructorArguments) => { /** * ,   rootStore,   * observable * */ const store = new target(...constructorArguments); const staticProperties = Object.keys(store); staticProperties.forEach(propName => { if (propName === 'rootStore') { return false; } const descriptor = Object.getOwnPropertyDescriptor(store, propName); Object.defineProperty( store, propName, observable(store, propName, descriptor) ); }); return store; }; } 

рдЙрдкрдпреЛрдЧ рдХрд░рдиреЗ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рд▓реЛрдбрд░ рдореЗрдВ рдкреНрд▓рдЧрдЗрдиреНрд╕ рдХреЛ рдПрдбрдЬрд╕реНрдЯ рдХрд░рдиреЗ рдХреА рдЖрд╡рд╢реНрдпрдХрддрд╛ рд╣реИред рдЬреЗрдмреАрдПрд╕ : ['@babel/plugin-proposal-decorators', { legacy: true }], ['@babel/plugin-proposal-class-properties', { loose: true }] , рдФрд░ ESLint рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рдореЗрдВ, parserOptions.ecmaFeatures.legacyDecorators: true рд╕реЗрдЯ parserOptions.ecmaFeatures.legacyDecorators: true рддрджрдиреБрд╕рд╛рд░ parserOptions.ecmaFeatures.legacyDecorators: true ред рдЗрди рд╕реЗрдЯрд┐рдВрдЧреНрд╕ рдХреЗ рдмрд┐рдирд╛, рдкреНрд░реЛрдЯреЛрдЯрд╛рдЗрдк рдХреЗ рдмрд┐рдирд╛ рдХреЗрд╡рд▓ рдХреНрд▓рд╛рд╕ рдбрд┐рд╕реНрдХреНрд░рд┐рдкреНрдЯрд░ рдХреЛ рд▓рдХреНрд╖реНрдп рдбреЗрдХреЛрд░реЗрдЯрд░ рдореЗрдВ рд╕реНрдерд╛рдирд╛рдВрддрд░рд┐рдд рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ, рдФрд░ рдкреНрд░рд╕реНрддрд╛рд╡ рдХреЗ рд╡рд░реНрддрдорд╛рди рд╕рдВрд╕реНрдХрд░рдг рдХреЗ рд╕рд╛рд╡рдзрд╛рдиреАрдкреВрд░реНрд╡рдХ рдЕрдзреНрдпрдпрди рдХреЗ рдмрд╛рд╡рдЬреВрдж, рдореБрдЭреЗ рд╡рд┐рдзрд┐рдпреЛрдВ рдФрд░ рд╕реНрдерд┐рд░ рдЧреБрдгреЛрдВ рдХреЛ рд▓рдкреЗрдЯрдиреЗ рдХрд╛ рдХреЛрдИ рддрд░реАрдХрд╛ рдирд╣реАрдВ рдорд┐рд▓рд╛ рд╣реИред


рд╕рд╛рдорд╛рдиреНрдп рддреМрд░ рдкрд░, рд╕реНрдЯреЛрд░реЗрдЬ рд╕реЗрдЯрдЕрдк рдкреВрд░рд╛ рд╣реЛ рдЬрд╛рддрд╛ рд╣реИ, рд▓реЗрдХрд┐рди MobX рдСрдЯреЛрд░рди рдХреА рдХреНрд╖рдорддрд╛ рдХреЛ рдЕрдирд▓реЙрдХ рдХрд░рдирд╛ рдЕрдЪреНрдЫрд╛ рд╣реЛрдЧрд╛ред рдЗрд╕ рдЙрджреНрджреЗрд╢реНрдп рдХреЗ рд▓рд┐рдП, "рдкреНрд░рд╛рдзрд┐рдХрд░рдг рд╕рд░реНрд╡рд░ рд╕реЗ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рдХреА рдкреНрд░рддреАрдХреНрд╖рд╛ рдХрд░реЗрдВ" рдпрд╛ "рд╕рд░реНрд╡рд░ рд╕реЗ рдЕрдиреБрд╡рд╛рдж рдбрд╛рдЙрдирд▓реЛрдб рдХрд░реЗрдВ" рдЬреИрд╕реЗ рдХрд╛рд░реНрдп, рдлрд┐рд░ рд╕рд░реНрд╡рд░ рдкрд░ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛рдПрдВ рд▓рд┐рдЦрдирд╛ рдФрд░ рдбреЛрдо рдореЗрдВ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЛ рд╕реАрдзреЗ рдкреНрд░рд╕реНрддреБрдд рдХрд░рдирд╛, рд╕рдмрд╕реЗ рдЙрдкрдпреБрдХреНрдд рд╣реИрдВред рдЗрд╕рд▓рд┐рдП, рдореИрдВ рднрд╡рд┐рд╖реНрдп рдореЗрдВ рдереЛрдбрд╝рд╛ рднрд╛рдЧреВрдВрдЧрд╛ рдФрд░ рд╕реНрдерд╛рдиреАрдпрдХрд░рдг рдХреЗ рд╕рд╛рде рдПрдХ рд╕реНрдЯреЛрд░ рдмрдирд╛рдКрдВрдЧрд╛:


src / рд╕реНрдЯреЛрд░реНрд╕ / I18nStore.js
 import { makeObservable } from 'utils'; import ru from 'localization/ru.json'; import en from 'localization/en.json'; const languages = { ru, en, }; const languagesList = Object.keys(languages); @makeObservable export class I18nStore { /** * @param rootStore {RootStore} */ constructor(rootStore) { this.rootStore = rootStore; setTimeout(() => { this.setLocalization('ru'); }, 500); } i18n = {}; languagesList = languagesList; currentLanguage = ''; setLocalization(language) { this.currentLanguage = language; this.i18n = languages[language]; this.rootStore.global.shouldAppRender = true; } } 

рдЬреИрд╕рд╛ рдХрд┐ рдЖрдк рджреЗрдЦ рд╕рдХрддреЗ рд╣реИрдВ, рдЕрдиреБрд╡рд╛рджреЛрдВ рдХреЗ рд╕рд╛рде рдХреБрдЫ * .json рдлрд╛рдЗрд▓реЗрдВ рд╣реИрдВ, рдФрд░ рд╡рд░реНрдЧ рдирд┐рд░реНрдорд╛рдгрдХрд░реНрддрд╛ рдореЗрдВ setTimeout рдХрд╛ рдЙрдкрдпреЛрдЧ рдХрд░рдХреЗ рдЕрддреБрд▓реНрдпрдХрд╛рд▓рд┐рдХ рд▓реЛрдбрд┐рдВрдЧ рдХрд╛ рдЕрдиреБрдХрд░рдг рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИред рдЬрдм рдЗрд╕реЗ рдирд┐рд╖реНрдкрд╛рджрд┐рдд рдХрд┐рдпрд╛ рдЬрд╛рддрд╛ рд╣реИ, рддреЛ рд╣рд╛рд▓ рд╣реА рдореЗрдВ рдмрдирд╛рдпрд╛ рдЧрдпрд╛ GlobalStore рдЗрд╕ рдХреЗ рд╕рд╛рде рдЪрд┐рд╣реНрдирд┐рдд рд╣реЛрддрд╛ рд╣реИ this.rootStore.global.shouldAppRender = true ред


рдЗрд╕ рдкреНрд░рдХрд╛рд░, app.js рд╕реЗ, рдЖрдкрдХреЛ рд░реЗрдВрдбрд░рд┐рдВрдЧ рдлрд╝рдВрдХреНрд╢рди рдХреЛ autorun.js рдлрд╝рд╛рдЗрд▓ рдореЗрдВ рд╕реНрдерд╛рдирд╛рдВрддрд░рд┐рдд рдХрд░рдирд╛ рд╣реЛрдЧрд╛:


src / autorun.js
 /* eslint-disable no-unused-vars */ import { autorun } from 'mobx'; import { renderToDOM } from 'utils'; import App from 'components/App'; const loggingEnabled = true; function logReason(autorunName, reaction) { if (!loggingEnabled || reaction.observing.length === 0) { return false; } const logString = reaction.observing.reduce( (str, { name, value }) => `${str}${name} changed to ${value}; `, '' ); console.log(`autorun-${autorunName}`, logString); } /** * @param store {RootStore} */ export function initAutorun(store) { autorun(reaction => { if (store.global.shouldAppRender) { renderToDOM(App); } logReason('shouldAppRender', reaction); }); } 

InitAutorun рдлрд╝рдВрдХреНрд╢рди рдореЗрдВ, рдХреЙрд▓рдмреИрдХ рдХреЗ рд╕рд╛рде рдХрд┐рд╕реА рднреА рд╕рдВрдЦреНрдпрд╛ рдореЗрдВ рдСрдЯреЛрд░рди рдирд┐рд░реНрдорд╛рдг рд╣реЛ рд╕рдХрддреЗ рд╣реИрдВ рдЬреЛ рдХреЗрд╡рд▓ рддрдм рд╣реА рдХрд╛рдо рдХрд░реЗрдВрдЧреЗ рдЬрдм рд╡реЗ рд╕реНрд╡рдпрдВ рдПрдХ рд╡рд┐рд╢реЗрд╖ рдХреЙрд▓рдмреИрдХ рдХреЗ рднреАрддрд░ рдПрдХ рдЪрд░ рдХреЛ рдмрджрд▓рддреЗ рдФрд░ рдмрджрд▓рддреЗ рд╣реИрдВред рдЗрд╕ рдорд╛рдорд▓реЗ рдореЗрдВ, autorun-shouldAppRender GlobalStore@3.shouldAppRender changed to true; , рдФрд░ DOM рдореЗрдВ рдПрдкреНрд▓рд┐рдХреЗрд╢рди рдХреЗ рдкреНрд░рддрд┐рдкрд╛рджрди рдХрд╛ рдХрд╛рд░рдг рдмрдирд╛ред рдПрдХ рд╢рдХреНрддрд┐рд╢рд╛рд▓реА рдЙрдкрдХрд░рдг рдЬреЛ рдЖрдкрдХреЛ рд╕реНрдЯреЛрд░ рдореЗрдВ рд╕рднреА рдкрд░рд┐рд╡рд░реНрддрдиреЛрдВ рдХреЛ рд▓реЙрдЧ рдХрд░рдиреЗ рдХреА рдЕрдиреБрдорддрд┐ рджреЗрддрд╛ рд╣реИ рдФрд░ рддрджрдиреБрд╕рд╛рд░ рдЙрдиреНрд╣реЗрдВ рдЬрд╡рд╛рдм рджреЗрддрд╛ рд╣реИред


рд╕реНрдерд╛рдиреАрдпрдХрд░рдг рдФрд░ рдкреНрд░рддрд┐рдХреНрд░рд┐рдпрд╛ рд╣реБрдХ


рдЕрдиреНрдп рднрд╛рд╖рд╛рдУрдВ рдореЗрдВ рдЕрдиреБрд╡рд╛рдж рд╕рдмрд╕реЗ рдЕрдзрд┐рдХ рд╕реНрд╡реИрдЪреНрдЫрд┐рдХ рдХрд╛рд░реНрдпреЛрдВ рдореЗрдВ рд╕реЗ рдПрдХ рд╣реИ, рдЫреЛрдЯреА рдХрдВрдкрдирд┐рдпреЛрдВ рдореЗрдВ рдпрд╣ рдЕрдХреНрд╕рд░ рджрд░реНрдЬрдиреЛрдВ рдмрд╛рд░ рдХрдо рдХрд░рдХреЗ рдЖрдВрдХрд╛ рдЬрд╛рддрд╛ рд╣реИ, рдФрд░ рдмрдбрд╝реА рдХрдВрдкрдирд┐рдпреЛрдВ рдореЗрдВ рдпрд╣ рдЕрддреНрдпрдзрд┐рдХ рдЬрдЯрд┐рд▓ рд╣реИред рдЗрд╕рдХреЗ рдХрд╛рд░реНрдпрд╛рдиреНрд╡рдпрди рдкрд░ рдирд┐рд░реНрднрд░ рдХрд░рддрд╛ рд╣реИ, рдХрдВрдкрдиреА рдореЗрдВ рдХрдИ рд╡рд┐рднрд╛рдЧреЛрдВ рдореЗрдВ рдПрдХ рдмрд╛рд░ рдореЗрдВ рдХрд┐рддрдиреА рдирд╕реЛрдВ рдФрд░ рд╕рдордп рдмрд░реНрдмрд╛рдж рдирд╣реАрдВ рд╣реЛрдЧрд╛ред рдореИрдВ рд▓реЗрдЦ рдореЗрдВ рдХреЗрд╡рд▓ рдЕрдиреНрдп рдкреНрд░рдгрд╛рд▓рд┐рдпреЛрдВ рдХреЗ рд╕рд╛рде рднрд╡рд┐рд╖реНрдп рдХреЗ рдПрдХреАрдХрд░рдг рдХреЗ рд▓рд┐рдП рдПрдХ рдмреИрдХрд▓реЙрдЧ рдХреЗ рд╕рд╛рде рдЧреНрд░рд╛рд╣рдХ рднрд╛рдЧ рдХрд╛ рдЙрд▓реНрд▓реЗрдЦ рдХрд░реВрдВрдЧрд╛ред


рдлреНрд░рдВрдЯрдПрдВрдб рдбреЗрд╡рд▓рдкрдореЗрдВрдЯ рдХреА рд╕реБрд╡рд┐рдзрд╛ рдХреЗ рд▓рд┐рдП, рдЖрдкрдХреЛ рдЗрд╕рдореЗрдВ рд╕рдХреНрд╖рдо рд╣реЛрдирд╛ рдЪрд╛рд╣рд┐рдП:


  • рд╕реНрдерд┐рд░рд╛рдВрдХ рдХреЗ рд▓рд┐рдП рдЕрд░реНрде рдирд╛рдо рдирд┐рд░реНрдзрд╛рд░рд┐рдд рдХрд░реЗрдВ;
  • рдбрд╛рдпрдирд╛рдорд┐рдХ рдЪрд░ рдбрд╛рд▓реЗрдВ
  • рдПрдХрд╡рдЪрди / рдмрд╣реБрд╡рдЪрди рдЗрдВрдЧрд┐рдд рдХрд░реЗрдВ;
  • тАФ -;
  • ;
  • / ;
  • ;
  • () ;
  • () , .

, , : messages.js ( ) . . ( / ), . ( , , ) . .


, currentLanguage i18n , , .


src/components/TestLocalization.js
 import React from 'react'; import { observer } from 'utils'; import { useLocalization } from 'hooks'; const messages = { hello: '  {count} {count: ,,}', }; function TestLocalization() { const getLn = useLocalization(__filename, messages); return <div>{getLn(messages.hello, { count: 1 })}</div>; } export const TestLocalizationConnected = observer(TestLocalization); 

, MobX- , , Connected. , ESLint, .


observer mobx-react-lite/useObserver , HOT_RELOAD React.memo ( PureMixin / PureComponent ), useObserver :


src/utils/observer.js
 import { useObserver } from 'mobx-react-lite'; import React from 'react'; function copyStaticProperties(base, target) { const hoistBlackList = { $$typeof: true, render: true, compare: true, type: true, }; Object.keys(base).forEach(key => { if (base.hasOwnProperty(key) && !hoistBlackList[key]) { Object.defineProperty( target, key, Object.getOwnPropertyDescriptor(base, key) ); } }); } export function observer(baseComponent, options) { const baseComponentName = baseComponent.displayName || baseComponent.name; function wrappedComponent(props, ref) { return useObserver(function applyObserver() { return baseComponent(props, ref); }, baseComponentName); } wrappedComponent.displayName = baseComponentName; let memoComponent = null; if (HOT_RELOAD === 'true') { memoComponent = wrappedComponent; } else if (options.forwardRef) { memoComponent = React.memo(React.forwardRef(wrappedComponent)); } else { memoComponent = React.memo(wrappedComponent); } copyStaticProperties(baseComponent, memoComponent); memoComponent.displayName = baseComponentName; return memoComponent; } 

displayName , React- ( stack trace ).


RootStore:


src/hooks/useStore.js
 import React from 'react'; import { store } from 'stores'; const storeContext = React.createContext(store); /** * @returns {RootStore} * */ export function useStore() { return React.useContext(storeContext); } 

, observer:


 import React from 'react'; import { observer } from 'utils'; import { useStore } from 'hooks'; function TestComponent() { const store = useStore(); return <div>{store.i18n.currentLanguage}</div>; } export const TestComponentConnected = observer(TestComponent); 

TestLocalization тАФ useLocalization:


src/hooks/useLocalization.js
 import _ from 'lodash'; import { declOfNum } from 'utils'; import { useStore } from './useStore'; const showNoTextMessage = false; function replaceDynamicParams(values, formattedMessage) { if (!_.isPlainObject(values)) { return formattedMessage; } let messageWithValues = formattedMessage; Object.entries(values).forEach(([paramName, value]) => { messageWithValues = formattedMessage.replace(`{${paramName}}`, value); }); return messageWithValues; } function replacePlurals(values, formattedMessage) { if (!_.isPlainObject(values)) { return formattedMessage; } let messageWithPlurals = formattedMessage; Object.entries(values).forEach(([paramName, value]) => { const pluralPattern = new RegExp(`{${paramName}:\\s([^}]*)}`); const pluralMatch = formattedMessage.match(pluralPattern); if (pluralMatch && pluralMatch[1]) { messageWithPlurals = formattedMessage.replace( pluralPattern, declOfNum(value, pluralMatch[1].split(',')) ); } }); return messageWithPlurals; } export function useLocalization(filename, messages) { const { i18n: { i18n, currentLanguage }, } = useStore(); return function getLn(text, values) { const key = _.findKey(messages, message => message === text); const localizedText = _.get(i18n, [filename, key]); if (!localizedText && showNoTextMessage) { console.error( `useLocalization: no localization for lang '${currentLanguage}' in ${filename} ${key}` ); } let formattedMessage = localizedText || text; formattedMessage = replaceDynamicParams(values, formattedMessage); formattedMessage = replacePlurals(values, formattedMessage); return formattedMessage; }; } 

replaceDynamicParams replacePlurals тАФ , , , , , ..


Webpack тАФ __filename тАФ , , . , тАФ , , . , :


 useLocalization: no localization for lang 'ru' in src\components\TestLocalization\TestLocalization.js hello 

ru.json :


src/localization/ru.json
 { "src\\components\\TestLocalization\\TestLocalization.js": { "hello": "  {count} {count: ,,}" } } 

, . src/localization/en.json ┬л ┬╗ setLocalization I18nStore.


┬л┬╗ React Message:


src/components/Message/Message.js
 import React from 'react'; import { observer } from 'utils'; import { useLocalization } from 'hooks'; function Message(props) { const { filename, messages, text, values } = props; const getLn = useLocalization(filename, messages); return getLn(text, values); } const ConnectedMessage = observer(Message); export function init(filename, messages) { return function MessageHoc(props) { const fullProps = { filename, messages, ...props }; return <ConnectedMessage {...fullProps} />; }; } 

__filename ( id ), , :


 const Message = require('components/Message').init( __filename, messages ); <Message text={messages.hello} values={{ count: 1 }} /> 

тАФ useLocalization ( currentLanguage , Message тАФ . , , .


, ( , , , / production). id , messages.js *.json , . ( / ), production. , , .


MobX + Hooks . , backend, , , .


API


( backend, ) тАФ , , . , . :


src/stores/CurrentTPStore.js
 import _ from 'lodash'; import { makeObservable } from 'utils'; import { apiRoutes, request } from 'api'; @makeObservable export class CurrentTPStore { /** * @param rootStore {RootStore} */ constructor(rootStore) { this.rootStore = rootStore; } id = ''; symbol = ''; fullName = ''; currency = ''; tradedCurrency = ''; low24h = 0; high24h = 0; lastPrice = 0; marketCap = 0; change24h = 0; change24hPercentage = 0; fetchSymbol(params) { const { tradedCurrency, id } = params; const { marketsList } = this.rootStore; const requestParams = { id, localization: false, community_data: false, developer_data: false, tickers: false, }; return request(apiRoutes.symbolInfo, requestParams) .then(data => this.fetchSymbolSuccess(data, tradedCurrency)) .catch(this.fetchSymbolError); } fetchSymbolSuccess(data, tradedCurrency) { const { id, symbol, name, market_data: { high_24h, low_24h, price_change_24h_in_currency, price_change_percentage_24h_in_currency, market_cap, current_price, }, } = data; this.id = id; this.symbol = symbol; this.fullName = name; this.currency = symbol; this.tradedCurrency = tradedCurrency; this.lastPrice = current_price[tradedCurrency]; this.high24h = high_24h[tradedCurrency]; this.low24h = low_24h[tradedCurrency]; this.change24h = price_change_24h_in_currency[tradedCurrency]; this.change24hPercentage = price_change_percentage_24h_in_currency[tradedCurrency]; this.marketCap = market_cap[tradedCurrency]; return Promise.resolve(); } fetchSymbolError(error) { console.error(error); } } 

, , . fetchSymbol , id , . , тАФ ( @action.bound ), Sentry :


src/utils/initSentry.js
 import * as Sentry from '@sentry/browser'; export function initSentry() { if (SENTRY_URL !== 'false') { Sentry.init({ dsn: SENTRY_URL, }); const originalErrorLogger = console.error; console.error = function consoleErrorCustom(...args) { Sentry.captureException(...args); return originalErrorLogger(...args); }; } } 

, :


src/api/_api.js
 import _ from 'lodash'; import { omitParam, validateRequestParams, makeRequestUrl, makeRequest, validateResponse, } from 'api/utils'; export function request(route, params) { return Promise.resolve() .then(validateRequestParams(route, params)) .then(makeRequestUrl(route, params)) .then(makeRequest) .then(validateResponse(route, params)); } export const apiRoutes = { symbolInfo: { url: params => `https://api.coingecko.com/api/v3/coins/${params.id}`, params: { id: omitParam, localization: _.isBoolean, community_data: _.isBoolean, developer_data: _.isBoolean, tickers: _.isBoolean, }, responseObject: { id: _.isString, name: _.isString, symbol: _.isString, genesis_date: v => _.isString(v) || _.isNil(v), last_updated: _.isString, country_origin: _.isString, coingecko_rank: _.isNumber, coingecko_score: _.isNumber, community_score: _.isNumber, developer_score: _.isNumber, liquidity_score: _.isNumber, market_cap_rank: _.isNumber, block_time_in_minutes: _.isNumber, public_interest_score: _.isNumber, image: _.isPlainObject, links: _.isPlainObject, description: _.isPlainObject, market_data: _.isPlainObject, localization(value, requestParams) { if (requestParams.localization === false) { return true; } return _.isPlainObject(value); }, community_data(value, requestParams) { if (requestParams.community_data === false) { return true; } return _.isPlainObject(value); }, developer_data(value, requestParams) { if (requestParams.developer_data === false) { return true; } return _.isPlainObject(value); }, public_interest_stats: _.isPlainObject, tickers(value, requestParams) { if (requestParams.tickers === false) { return true; } return _.isArray(value); }, categories: _.isArray, status_updates: _.isArray, }, }, }; 

request :


  1. apiRoutes ;
  2. , route.params, , omitParam ;
  3. URL route.url тАФ , , тАФ get- URL;
  4. fetch, JSON;
  5. , route.responseObject route.responseArray ( ). , тАФ , ;
  6. / / / , ( fetchSymbolError ) .

. , Sentry, response:



тАФ ( ), .



, , . , :


  • ;
  • pathname search;
  • / ;
  • location ;
  • beforeEnter, isLoading, ;
  • : , , , beforeEnter, ;
  • / ;
  • / ;
  • .

┬л┬╗, -, тАФ . :


src/routes.js
 export const routes = { marketDetailed: { name: 'marketDetailed', path: '/market/:market/:pair', masks: { pair: /^[a-zA-Z]{3,5}-[a-zA-Z]{3}$/, market: /^[a-zA-Z]{3,4}$/, }, beforeEnter(route, store) { const { params: { pair, market }, } = route; const [symbol, tradedCurrency] = pair.split('-'); const prevMarket = store.marketsList.currentMarket; function optimisticallyUpdate() { store.marketsList.currentMarket = market; } return Promise.resolve() .then(optimisticallyUpdate) .then(store.marketsList.fetchSymbolsList) .then(store.rates.fetchRates) .then(() => store.marketsList.fetchMarketList(market, prevMarket)) .then(() => store.currentTP.fetchSymbol({ symbol, tradedCurrency, }) ) .catch(error => { console.error(error); }); }, }, error404: { name: 'error404', path: '/error404', }, }; 

src/routeComponents.js
 import { MarketDetailed } from 'pages/MarketDetailed'; import { Error404 } from 'pages/Error404'; export const routeComponents = { marketDetailed: MarketDetailed, error404: Error404, }; 

, , тАФ <Link route={routes.marketDetailed}> , . Webpack , .


, location .


src/stores/RouterStore.js
 import _ from 'lodash'; import { makeObservable } from 'utils'; import { routes } from 'routes'; @makeObservable export class RouterStore { /** * @param rootStore {RootStore} */ constructor(rootStore) { this.rootStore = rootStore; this.currentRoute = this._fillRouteSchemaFromUrl(); window.addEventListener('popstate', () => { this.currentRoute = this._fillRouteSchemaFromUrl(); }); } currentRoute = null; _fillRouteSchemaFromUrl() { const pathnameArray = window.location.pathname.split('/'); const routeName = this._getRouteNameMatchingUrl(pathnameArray); if (!routeName) { const currentRoute = routes.error404; window.history.pushState(null, null, currentRoute.path); return currentRoute; } const route = routes[routeName]; const routePathnameArray = route.path.split('/'); const params = {}; routePathnameArray.forEach((pathParam, i) => { const urlParam = pathnameArray[i]; if (pathParam.indexOf(':') === 0) { const paramName = pathParam.replace(':', ''); params[paramName] = urlParam; } }); return Object.assign({}, route, { params, isLoading: true }); } _getRouteNameMatchingUrl(pathnameArray) { return _.findKey(routes, route => { const routePathnameArray = route.path.split('/'); if (routePathnameArray.length !== pathnameArray.length) { return false; } for (let i = 0; i < routePathnameArray.length; i++) { const pathParam = routePathnameArray[i]; const urlParam = pathnameArray[i]; if (pathParam.indexOf(':') !== 0) { if (pathParam !== urlParam) { return false; } } else { const paramName = pathParam.replace(':', ''); const paramMask = _.get(route.masks, paramName); if (paramMask && !paramMask.test(urlParam)) { return false; } } } return true; }); } replaceDynamicParams(route, params) { return Object.entries(params).reduce((pathname, [paramName, value]) => { return pathname.replace(`:${paramName}`, value); }, route.path); } goTo(route, params) { if (route.name === this.currentRoute.name) { if (_.isEqual(this.currentRoute.params, params)) { return false; } this.currentRoute.isLoading = true; this.currentRoute.params = params; const newPathname = this.replaceDynamicParams(this.currentRoute, params); window.history.pushState(null, null, newPathname); return false; } const newPathname = this.replaceDynamicParams(route, params); window.history.pushState(null, null, newPathname); this.currentRoute = this._fillRouteSchemaFromUrl(); } } 

тАФ routes.js . тАФ 404. , ┬л ┬╗, , , тАФ , 'test-test'.


currentRoute , params ( URL) isLoading: true . React- Router:


src/components/Router.js
 import React from 'react'; import _ from 'lodash'; import { useStore } from 'hooks'; import { observer } from 'utils'; import { routeComponents } from 'routeComponents'; function getRouteComponent(route, isLoading) { const Component = routeComponents[route.name]; if (!Component) { console.error( `getRouteComponent: component for ${ route.name } is not defined in routeComponents` ); return null; } return <Component isLoading={isLoading} />; } function useBeforeEnter() { const store = useStore(); const { currentRoute } = store.router; React.useEffect(() => { if (currentRoute.isLoading) { const beforeEnter = _.get(currentRoute, 'beforeEnter'); if (_.isFunction(beforeEnter)) { Promise.resolve() .then(() => beforeEnter(currentRoute, store)) .then(() => { currentRoute.isLoading = false; }) .catch(error => console.error(error)); } else { currentRoute.isLoading = false; } } }); return currentRoute.isLoading; } function Router() { const { router: { currentRoute }, } = useStore(); const isLoading = useBeforeEnter(); return getRouteComponent(currentRoute, isLoading); } export const RouterConnected = observer(Router); 

, , currentRoute == null . тАФ isLoading === true , false , route.beforeEnter ( ). console.error , , .


, тАФ , . React- 2 :


  1. componentWillMount / componentDidMount / useEffect , , . тАФ , ┬л┬╗. тАФ тАФ ;
  2. ( ) , . тАФ тАФ , . тАФ / тАФ real-time , / .

, тАФ ( , , ..), .


beforeEnter , : ┬л ┬╗, ( , , ), тАФ ( тАФ 500 ; ; , ; ..). ┬л┬╗ , MVP .


:


src/components/Link.js
 import React from 'react'; import _ from 'lodash'; import { useStore } from 'hooks'; import { observer } from 'utils'; function checkRouteParamsWithMasks(route, params) { if (route.masks) { Object.entries(route.masks).forEach(([paramName, paramMask]) => { const value = _.get(params, paramName); if (paramMask && !paramMask.test(value)) { console.error( `checkRouteParamsWithMasks: wrong param for ${paramName} in Link to ${ route.name }: ${value}` ); } }); } } function Link(props) { const store = useStore(); const { currentRoute } = store.router; const { route, params, children, onClick, ...otherProps } = props; checkRouteParamsWithMasks(route, params); const filledPath = store.router.replaceDynamicParams(route, params); return ( <a href={filledPath} onClick={e => { e.preventDefault(); if (currentRoute.isLoading) { return false; } store.router.goTo(route, params); if (onClick) { onClick(); } }} {...otherProps} > {children} </a> ); } export const LinkConnected = observer(Link); 

route , params ( ) ( ) href . , beforeEnter , . ┬л, ┬╗, , тАФ .


рдореИрдЯреНрд░рд┐рдХреНрд╕


- ( , , , , ) . . .


тАФ , тАФ , . :


src/api/utils/metrics.js
 import _ from 'lodash'; let metricsArray = []; let sendMetricsCallback = null; export function startMetrics(route, apiRoutes) { return function promiseCallback(data) { clearTimeout(sendMetricsCallback); const apiRouteName = _.findKey(apiRoutes, route); metricsArray.push({ id: apiRouteName, time: new Date().getTime(), }); return data; }; } export function stopMetrics(route, apiRoutes) { return function promiseCallback(data) { const apiRouteName = _.findKey(apiRoutes, route); const metricsData = _.find(metricsArray, ['id', apiRouteName]); metricsData.time = new Date().getTime() - metricsData.time; clearTimeout(sendMetricsCallback); sendMetricsCallback = setTimeout(() => { console.log('Metrics sent:', metricsArray); metricsArray = []; }, 2000); return data; }; } 

middleware request :


 export function request(route, params) { return Promise.resolve() .then(startMetrics(route, apiRoutes)) .then(validateRequestParams(route, params)) .then(makeRequestUrl(route, params)) .then(makeRequest) .then(validateResponse(route, params)) .then(stopMetrics(route, apiRoutes)) .catch(error => { stopMetrics(route, apiRoutes)(); throw error; }); } 

, , 2 , ( ) . тАФ , тАФ , ( ) , .


- тАФ .



end-to-end , Cypress. : ; , ; Continious Integration.


javascript Chai / Sinon , . , , тАФ ./tests, package.json тАФ "dependencies": { "cypress": "3.2.0" }


. Webpack :


tests/cypress/plugins/index.js
 const webpack = require('../../../node_modules/@cypress/webpack-preprocessor'); const webpackConfig = require('../../../webpack-custom/webpack.config'); module.exports = on => { const options = webpack.defaultOptions; options.webpackOptions.module = webpackConfig.module; options.webpackOptions.resolve = webpackConfig.resolve; on('file:preprocessor', webpack(options)); }; 

. module ( ) resolve ( ). ESLint ( describe , cy ) eslint-plugin-cypress . , :


tests/cypress/integration/mixed.js
 describe('Market Listing good scenarios', () => { it('Lots of mixed tests', () => { cy.visit('/market/usd/bch-usd'); cy.location('pathname').should('equal', '/market/usd/bch-usd'); //    ,       cy.wait('@symbolsList') .its('response.body') .should(data => { expect(data).to.be.an('array'); }); //    cy.wait('@rates'); cy.wait('@marketsList'); cy.wait('@symbolInfo'); cy.wait('@chartData'); //       cy.get('#marketTab-eth').click(); cy.location('pathname').should('equal', '/market/eth/bch-usd'); cy.wait('@rates'); cy.wait('@marketsList'); //    cy.contains(''); cy.get('#langSwitcher-en').click(); cy.contains('Markets list'); //    cy.get('body').should('have.class', 'light'); cy.get('#themeSwitcher-dark').click(); cy.get('body').should('have.class', 'dark'); }); }); 

Cypress fetch, , :


tests/cypress/support/index.js
 import { apiRoutes } from 'api'; let polyfill = null; before(() => { const polyfillUrl = 'https://unpkg.com/unfetch/dist/unfetch.umd.js'; cy.request(polyfillUrl).then(response => { polyfill = response.body; }); }); Cypress.on('window:before:load', window => { delete window.fetch; window.eval(polyfill); window.fetch = window.unfetch; }); before(() => { cy.server(); cy.route(`${apiRoutes.symbolsList.url}**`).as('symbolsList'); cy.route(`${apiRoutes.rates.url}**`).as('rates'); cy.route(`${apiRoutes.marketsList.url}**`).as('marketsList'); cy.route(`${apiRoutes.symbolInfo.url({ id: 'bitcoin-cash' })}**`).as( 'symbolInfo' ); cy.route(`${apiRoutes.chartData.url}**`).as('chartData'); }); 

, .


, ?


, - . , , - / - / .


, , , - , - , (real-time , serviceWorker, CI, , , -, , ..).


( Gzip) :



React Developer Tools :



React Hooks + MobX , Redux. , . , , , . . !




Update 13.07.2019


, , :


1. yarn.lock , yarn install --force , "upd": "yarn install && yarn add file:./eslint-custom && yarn add file:./webpack-custom" . ESLint Webpack.


2. webpack-custom/config/configOptimization.js, ,


 lodash: { test: module => module.context.indexOf('node_modules\\lodash') !== -1, name: 'lodash', chunks: 'all', enforce: true, } 


 lodash: { test: /node_modules[\\/]lodash/, name: 'lodash', chunks: 'all', enforce: true, } 

3. useLocalization(__filename, messages) , тАФ


 const messages = { hello: { value: '  {count} {count: ,,}', name: "src/components/TestLocalization/TestLocalization.hello", } }; 

,


 const messagesDefault = { hello: '  {count} {count: ,,}', }; export const messages = Object.keys(messagesDefault).reduce((acc, key) => { acc[key] = { value: messagesDefault[key], name: __dirname.toLowerCase().replace(/\\\\/g, '/') + '.' + key, }; return acc; }, {}); 

IDE , Webpack:


webpack-custom/utils/messagesLoader.js
 module.exports = function messagesLoader(source) { if (source.indexOf('export const messages = messagesDefault;') !== -1) { return source.replace( 'export const messages = messagesDefault;', ` export const messages = Object.keys(messagesDefault).reduce((acc, key) => { acc[key] = { value: messagesDefault[key], name: __dirname.toLowerCase().replace(/\\\\/g, '/') + '.' + key, }; return acc; }, {}); ` ); } return source; }; 

, messages.js :


 const messagesDefault = { someText: '', }; export const messages = messagesDefault; 

, app.js , messages.js , *.json :


src/utils/checkLocalization.js
 import _ from 'lodash'; import ru from 'localization/ru.json'; const showNoTextMessage = true; export function checkLocalization() { const context = require.context('../', true, /messages\.js/); const messagesFiles = context.keys(); const notLocalizedObject = {}; messagesFiles.forEach(path => { const fileExports = context(path); const { messages } = fileExports; _.values(messages).forEach(({ name, value }) => { if (ru[name] == null) { notLocalizedObject[name] = value; } }); }); if (showNoTextMessage && _.size(notLocalizedObject) > 0) { console.log( 'No localization for lang ru:', JSON.stringify(notLocalizedObject, null, 2) ); } } 

,


 No localization for lang ru: { "src/components/TestLocalization/TestLocalization.hello": "  {count} {count: ,,}" } 

*.json тАФ , , .


3. Lodash someResponseParam: _.isString , . :


src/utils/validateObjects.js
 import _ from 'lodash'; import { createError } from './createError'; import { errorsNames } from 'const'; export const validators = { isArray(v) { return _.isArray(v); }, isString(v) { return _.isString(v); }, isNumber(v) { return _.isNumber(v); }, isBoolean(v) { return _.isBoolean(v); }, isPlainObject(v) { return _.isPlainObject(v); }, isArrayNotRequired(v) { return _.isArray(v) || _.isNil(v); }, isStringNotRequired(v) { return _.isString(v) || _.isNil(v); }, isNumberNotRequired(v) { return _.isNumber(v) || _.isNil(v); }, isBooleanNotRequired(v) { return _.isBoolean(v) || _.isNil(v); }, isPlainObjectNotRequired(v) { return _.isPlainObject(v) || _.isNil(v); }, omitParam() { return true; }, }; validators.isArray.notRequired = validators.isArrayNotRequired; validators.isString.notRequired = validators.isStringNotRequired; validators.isNumber.notRequired = validators.isNumberNotRequired; validators.isBoolean.notRequired = validators.isBooleanNotRequired; validators.isPlainObject.notRequired = validators.isPlainObjectNotRequired; export function validateObjects( { validatorsObject, targetObject, prefix }, otherArg ) { if (!_.isPlainObject(validatorsObject)) { throw new Error(`validateObjects: validatorsObject is not an object`); } if (!_.isPlainObject(targetObject)) { throw new Error(`validateObjects: targetObject is not an object`); } Object.entries(validatorsObject).forEach(([paramName, validator]) => { const paramValue = targetObject[paramName]; if (!validator(paramValue, otherArg)) { const validatorName = _.findKey(validators, v => v === validator); throw createError( errorsNames.VALIDATION, `${prefix || ''}${paramName}${ _.isString(validatorName) ? ` [${validatorName}]` : '' }` ); } }); } 

тАФ , someResponseParam [isString] , , . someResponseParam: validators.isString.notRequired , . , , someResponseArray: arrayShape({ someParam: isString }) , .


5. , , . тАФ ( body isEntering , isLeaving ) beforeLeave ( Prompt react-router ), false- ,


 somePage: { path: '/some-page', beforeLeave(store) { return store.modals.raiseConfirm('    ?'); }, } 

, - . , :


тАФ /some/long/auth/path beforeEnter /auth
тАФ beforeEnter /auth , тАФ /profile
тАФ beforeEnter /profile , , , /profile/edit


, тАФ window.history.pushState location.replace . , . ┬л ┬╗ ┬л componentDidMount ┬╗, , .


6. (, ) :


src/utils/withState.js
 export function withState(target, fnName, fnDescriptor) { const original = fnDescriptor.value; fnDescriptor.value = function fnWithState(...args) { if (this.executions[fnName]) { return Promise.resolve(); } return Promise.resolve() .then(() => { this.executions[fnName] = true; }) .then(() => original.apply(this, args)) .then(data => { this.executions[fnName] = false; return data; }) .catch(error => { this.executions[fnName] = false; throw error; }); }; return fnDescriptor; } 

src/stores/CurrentTPStore.js
 import _ from 'lodash'; import { makeObservable, withState } from 'utils'; import { apiRoutes, request } from 'api'; @makeObservable export class CurrentTPStore { /** * @param rootStore {RootStore} */ constructor(rootStore) { this.rootStore = rootStore; this.executions = {}; } @withState fetchSymbol() { return request(apiRoutes.symbolInfo) .then(this.fetchSymbolSuccess) .catch(this.fetchSymbolError); } fetchSymbolSuccess(data) { return Promise.resolve(); } fetchSymbolError(error) { console.error(error); } } 

src/components/TestComponent.js
 import React from 'react'; import { observer } from 'utils'; import { useStore } from 'hooks'; function TestComponent() { const store = useStore(); const { currentTP: { executions } } = store; return <div>{executions.fetchSymbol ? '...' : ''}</div>; } export const TestComponentConnected = observer(TestComponent); 

, .


, , . тАФ , , , , - .

Source: https://habr.com/ru/post/hi450360/


All Articles