Hallo habr! Mein Name ist Fedor und ich bin Front-End-Entwickler bei KTS .
Anfang 2017 kontaktierte ein langjähriger Freund der Firma Dmitry Voloshin KTS mit der Bitte, eine Plattform für Online-Bildung Otus zu schaffen. Jetzt ist Otus ein ziemlich erfolgreiches und bekanntes Projekt, das bereits Zehntausende von Studenten rekrutiert hat. Und dann fing es gerade erst an und bestand nur aus einem Java-Entwicklerkurs, aber die Pläne waren bereits napoleonisch.
, . , MVP . Django . , , , , .
, Otus : , .
: . , , .
- , . .
Python + Django, vanilla js + jquery, . : Go, React, . , .
. , React Otus. , , .
, !
2 . , , SSR . () , SPA. .
:
: shared - UI-, ; internal - ; external - . , , - .
. , , .
. :
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. :
CSS- (css-modules). Next.js , [name].module.css. internal- /\.module\.s?css/
react-css-modules styleName="style" Next.js "", Postcss 8, postcss-nested, postcss-scss.
styled-components. internal-, SSR styled-components babel head .
styled-components. css-in-js , .
styled-components internal .
external :
babel.config.js
babel- className rehydration.
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
, , , - .