Otus.ru用にSSRとSPAを使用してモノリポジトリを設定する方法

こんにちはhabr!私の名前はFedorで、KTSのフロントエンド開発者です。





2017年の初め、Dmitry Voloshin社の長年の友人がKTSに連絡し、オンライン教育Otusのプラットフォームを作成するよう依頼しました。現在、Otusはかなり成功した有名なプロジェクトであり、すでに数万人の学生を募集しています。そして、それはまだ始まったばかりで、Java開発者コースは1つだけで構成されていましたが、計画はすでにナポレオンでした。





, . , 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