こんにちは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. .
:
: 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
, , , - .