Cómo configuramos un monorrepositorio con SSR y SPA para Otus.ru

¡Hola habr! Mi nombre es Fedor y soy desarrollador front-end en KTS .





A principios de 2017, un viejo amigo de la empresa Dmitry Voloshin se puso en contacto con KTS con una solicitud para crear una plataforma para la educación en línea Otus. Ahora Otus es un proyecto bastante exitoso y conocido, que ya ha reclutado a decenas de miles de estudiantes. Y luego estaba comenzando y consistía en un solo curso para desarrolladores de Java, pero los planes ya eran napoleónicos.





, . , MVP . Django . , , , , .





, Otus : , .





: . , , .

- , . .





Python + Django, vanilla js + jquery, . : Go, React, . , .





. , React Otus. , , .





, !





2 . , , SSR . () , SPA. .





:





  1. : shared - UI-, ; internal - ; external - . , , - .





  2. . , , .





. :









  1. React, SSR





  2. typescript





  3. eslint

















  4. ,





javascript- : lerna, yarn workspaces. .



Lerna npm yarn , . Yarn workspaces , lerna, .



, yarn workspaces, , , lerna, .





lerna.json:





{
  "packages": [
    "apps/*"
  ],
  "version": "1.0.0",
  "npmClient": "yarn",
  "useWorkspaces": true
}
      
      



package.json:





{
  "name": "otus",
  "version": "1.0.0",
  "workspaces": [
    "apps/*"
  ],
  "private": true,
  "devDependencies": {
    "lerna": "^3.22.1"
  }
}
      
      



package.json :





{
  "name": "@otus/external",
  "version": "1.0.0",
  "private": true
}
      
      



internal-

,
,

Internal- SPA.



webpack + babel. , , .





webpack.config.js babel-loader javascript dev-.





internal/webpack.config.js





module.exports = (opts, args) => {
  return {
    entry: './src/index.jsx',
    output: {
      path: buildPath,
      filename: `js/[name]-[hash].js`,
      publicPath: '/',
    },

    module: {
      rules: [
        {
          test: /\\.jsx?$/,
          exclude: /node_modules/,
          loader: ‘babel-loader’
        },
      ],
    },
    devServer: {
      port: 9002,
      host: 'localhost',
      ...
    },
  };
   ...
  };
};
      
      



babel @babel/preset-env targets @babel/preset-react jsx.





internal/babel.config.js:





module.exports = api => {
  api.cache(() => process.env.NODE_ENV);
  
  return {
    presets: [
      [
        require('@babel/preset-env'),
        {
          targets: {
            browsers: ['> 0.25%, not dead']
          }
        }
      ],
      require('@babel/preset-react'),
    ],
  };
};
      
      



dev- package.json





internal/package.json:





{
  "scripts": {
    "dev": "webpack serve --mode development",
   },
   ...
}
      
      



External-.

external- , . . React- (Gatsby, Next.js), Node.js.





Gatsby

c GraphQL " ". Static Site Generation (SSG). . , Gatsby , , , .





Next.js





SSR, SSG . " " typescript, css-modules, api-. , , Gatsby.





SSR Node.js

, : , . , .





, , , Next.js.





:





package.json external-:





{
  "scripts": {
    "dev": "next dev -p 9001"
  }
  ...
}
      
      



dev- lerna:





{
  "scripts": {
    "dev": "lerna run --parallel dev"
  },
  ...
 }
      
      



typescript

internal- typescript babel. babel- ts. Next.js typescript " " babel.





tsconfig.base.json. tsconfig.json, typescript typescript-. tsconfig.json, .





internal





.ts/.tsx babel-loader:





{
  test: /\\.(ts|js)x?$/,
  exclude: /node_modules/,
  loader: 'babel-loader'
}
      
      



babel.config.js:





module.exports = api => {
  ...  
  return {
    presets: [
      ...
      require('@babel/preset-typescript'),
    ],
  };
};
      
      







  tsconfig.base.json:
{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types"],
     ...
  }
}

  internal:
{
  "extends": "../../tsconfig.base.json",
  "include": ["./src/**/*"],
  "exclude": ["node_modules"]
}

      
      



tsconfig.json external, Next.js next-env.d.ts , tsconfig.json.





external:





{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {...},
  "include": [
    "next-env.d.ts",
    ...
  ],
  "exclude": [
    "node_modules"
  ]
}

      
      



eslint

eslint typescript: , . 





WebStorm , WebStorm eslint , eslint . , eslint- package.json, package.json .





eslint
module.exports = {
  root: true,
  parser: '@typescript-eslint/parser',
  env: {
    browser: true,
    es6: true
  },
  extends: [
    'eslint:recommended',
    'prettier',
    'prettier/react',
    'plugin:import/errors',
    'plugin:import/warnings',
    'plugin:import/typescript',
    'plugin:react/recommended',
    'plugin:@typescript-eslint/recommended'
  ],
  parserOptions: {
    ecmaFeatures: {
      jsx: true
    },
    ecmaVersion: 2018,
    sourceType: 'module',
    project: './apps/**/tsconfig.json'
  },
  plugins: [...],
  rules: {...},
  settings: {
 'import/parsers': {
   '@typescript-eslint/parser': ['.ts', '.tsx']
 },
 'import/resolver': {
   "typescript": {
     "project": "tsconfig.json"
   },
 },
}
};

      
      



.eslintrc.js :





const path = require('path');

module.exports = {
  extends: path.resolve('../../.eslintrc.js'),
  ...
};
      
      



- . : 





import Button from 'shared/components/Button';







UIKit-. .





Aliases internal

Aliases internal webpack. . eslint eslint-import-resolver-typescript, tsconfig.json paths, eslint- .





webpack.config.js:





module.exports = (opts, args) => {
  return {
    ...
    resolve: {
      extensions: ['.ts', '.tsx', '.js', '.jsx'],
      alias: {
        shared: path.join(appsPath, 'shared/src'),
      }
    },
		...
  };
};
      
      



tsconfig.base.json:





{
  "compilerOptions": {
    "baseUrl": "apps",
    "paths": {
      "shared/*": ["shared/src/*"],
      "internal/*": ["internal/src/*"],
      "external/*": ["external/src/*"]
    },
    ...
  }
}
      
      



internal .





import * as React from 'react';
import { render } from 'react-dom';
import Button from 'shared/components/Button'; <-- 

render(
  <div>
    <Button />
  </div>,
  document.getElementById('root')
);
      
      



Aliases external





external-:





aliases external paths tsconfig.json.

alias external- . alias ( shared), next-transpile-modules, , Next.js .

next.config.js.





tsconfig.json:





{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "baseUrl": "..",
    "paths": {
      "shared/*": ["shared/src/*"],
      "components/*": ["external/components/*"]
    },
    ...
}
      
      



next.config.js:





const withPlugins = require("next-compose-plugins");
const withTM = require('next-transpile-modules')(['shared']);

const plugins = [
  [withTM],
];

module.exports = withPlugins(plugins);
      
      



scss. :





  1. CSS- (css-modules). Next.js , [name].module.css. internal- /\.module\.s?css/





  2. react-css-modules styleName="style" Next.js "", Postcss 8, postcss-nested, postcss-scss.





  3. styled-components. internal-, SSR styled-components babel head .





styled-components. css-in-js , .





styled-components internal .





external :









  1. babel.config.js





  2. babel- className rehydration.





  3. next.config.js babel.config.js next-plugin-custom-babel-config





Document- style- head .





babel-config.js external :





module.exports =function(api) {
  api.cache(() => process.env.NODE_ENV);

const presets = ['next/babel'];

const plugins = [
    [
      'babel-plugin-styled-components',
      {
        'ssr':true,
        'displayName':true,
      }
    ]
  ];

return{
    presets,
    plugins
  };
};

      
      



next.config.js:





const withPlugins= require('next-compose-plugins');
const withTM = require('next-transpile-modules')(['shared']);
const withCustomBabelConfig= require('next-plugin-custom-babel-config');
const path = require('path');

const plugins = [
  [
     withCustomBabelConfig,
    { babelConfigFile: path.resolve('./babel.config.js') },
  ],
   [withTM],
];

module.exports = withPlugins(plugins);

      
      



_document.tsx:





import Document,
{
  Head,
  Main,
  NextScript,
	DocumentContext,
	DocumentProps,
	Html,
} from 'next/document';
import * as React from 'react';

import { ServerStyleSheet } from 'styled-components';

class MyDocument extends Document<DocumentProps & { styleTags:Array<React.ReactElement> }
> {
static async getInitialProps(ctx: DocumentContext) {
	const initialProps = await Document.getInitialProps(ctx);
	const sheet = new ServerStyleSheet();
	
	const page = ctx.renderPage((App) => (props) =>
	      sheet.collectStyles(<App {...props} />)
	    );
	
	const styleTags = sheet.getStyleElement();
	
	return { ...initialProps, ...page, styleTags };
  }

  render() {
   return(
      <Html>
        <Head>{this.props.styleTags}</Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
      
      







  • lerna + yarn workspaces ;





  • , , Next.js;





  • typescript;





  • eslint;





  • styled-components 





, , , - .





.








All Articles