Практика TypeScript в реактивном проекте

внешний интерфейс TypeScript React.js ESLint

я писал некоторое время назадПрактика TypeScript в Node-проектах.
Есть объяснение, почему использоватьTS, И вNodeКакова структура проекта в .
Но это был чисто интерфейсный проект, недавно мне довелось заниматься рефакторингом другого проекта, после последней практики я попробовалTSСладость, принесенная, без колебаний выберете использованиеTS+Reactпровести рефакторинг этого проекта.
Эта реконструкция включает не толькоNodeрефакторинг (ранее это былоExpressпроект), а также рефакторинг фронтенда (ранее делалjQueryуправляемые многостраничные приложения).

Структура проекта

Поскольку в текущем проекте не планируется разделять переднюю и заднюю части (проект внутренней инструментальной платформы), общая структура основана на последнемNodeСтруктура проекта, с некоторыми дополнениями поверх негоFrontEndСтруктура каталогов:

  .
  ├── README.md
  ├── copy-static-assets.ts
  ├── nodemon.json
  ├── package.json
+ ├── client-dist
+ │   ├── bundle.js
+ │   ├── bundle.js.map
+ │   ├── logo.png
+ │   └── vendors.dll.js
  ├── dist
  ├── src
  │   ├── config
  │   ├── controllers
  │   ├── entity
  │   ├── models
  │   ├── middleware
  │   ├── public
  │   ├── app.ts
  │   ├── server.ts
  │   ├── types
+ │   ├── common
  │   └── utils
+ ├── client-src
+ │   ├── components
+ │   │   └── Header.tsx
+ │   ├── conf
+ │   │   └── host.ts
+ │   ├── dist
+ │   ├── utils
+ │   ├── index.ejs
+ │   ├── index.tsx
+ │   ├── webpack
+ │   ├── package.json
+ │   └── tsconfig.json
+ ├── views
+ │   └── index.ejs
  ├── tsconfig.json
  └── tslint.json

который отмечен зеленым (также может быть+отображаемый номер) на этот раз добавлен заново.
вclient-distа такжеviewsна всем протяженииwebpackСгенерированы, фактические исходные файлы находятся вclient-srcВниз.Нет затрат на разделение до и после разделения этой структуры.
Некоторые из следующих папок разделены ниже:

dir/file desc
index.ejs вход в проектhtmlфайл, используяejsкак движок рендеринга
index.tsx вход в проектjsфайл, используется суффиксtsx, по двум причинам:
1. Мы будем использоватьtsпровестиReactразработка программы
2. .tsxфайл по сравнению с кодомiconлучше выглядишь :р
tsconfig.json используется дляtscНекоторые конфигурационные файлы для компиляции и выполнения
components Каталог, в котором хранятся компоненты
config Место, где хранятся различные элементы конфигурации, аналогично интерфейсу запроса.hostили различные состоянияmapОтображение и тому подобное (можно понять, что объекты перечисления все здесь)
utils Место, где хранятся некоторые общедоступные функции, и здесь должны быть размещены различные повторно используемые коды.
dist Место хранения различных статических ресурсов, картинок и других файлов
webpack Существуют различные средыwebpackкоманды сценария иdllпоколение

Попытка повторного использования кода между интерфейсом и сервером

На самом деле отсутствует новая папка, мы находимся вsrcДобавлен новый каталогcommonКаталог, этот каталог предназначен для хранения некоторых общедоступных функций и общедоступныхconfig, отличный отutilsилиconfigДело в том, что код здесь разделяется между фронтендом и бекендом, поэтому функции здесь должны быть полностью свободны от каких-либо зависимостей от окружения и бизнес-логики.

Похожие числа на тысячные, форматирование даты или номер порта, который слушает сервис, эти коды, не содержащие никакой логики и не сильно зависящие от окружения, мы можем поставить их здесь.
Это также небольшая приятность, связанная с отсутствием разделения на переднюю и заднюю часть, и некоторый код может быть разделен до и после.

Для достижения такой конфигурации необходимо внести следующие изменения на основе вышеуказанных проектов:

  1. srcвнизutilsа такжеconfigНекоторый код мигрирует вcommonПод папкой он в основном используется для определения того, можно ли его использовать до и после
  2. для сравнения предыдущегоnodeСтруктурные воздействия сведены к минимуму, нам необходимоcommonДобавить новый в папкуindex.tsиндексный файл, а вutils/index.tsцитировать его ниже, так что дляnodeС точки зрения использования вам не нужно заботиться о том, исходит ли этот файл изutilsещеcommon
// src/common/utils/comma.ts
export default (num: number): string => String(num).replace(/\B(?=(\d{3})+$)/g, ',')

// src/common/utils/index.ts
export { default as comma } from './comma'

// src/utils.index.ts
export * from '../common/utils'

// src/app.ts
import { comma } from './utils' // 并不需要关心是来自common还是来自utils

console.log(comma(1234567)) // 1,234,567
  1. затем настройтеwebpackизaliasсвойства дляwebpackможет правильно найти свой путь
// client-src/webpack/base.js
module.exports = {
  resolve: {
    alias: {
       '@Common': path.resolve(__dirname, '../../src/common'),
    }
  }
}
  1. Нам также необходимо настроитьtsconfig.jsonиспользуется дляvs codeВы можете найти соответствующий каталог, иначе вам будет предложено в редактореcan't find module XXX
// client-src/tsconfig.json
{
  "compilerOptions": {
    "paths": {
      // 用于引入某个`module`
      "@Common/*": [
        "../src/common/*"
      ]
    }
  }
}
  1. Наконец вclient-src/utils/index.tsнаписать что-то вродеserverокончание обработки
// client-src/utils/index.ts
export * from '@Common/utils'

// client-src/index.tsx
import { comma } from './utils'

console.log(comma(1234567)) // 1,234,567

Строительство окружающей среды

При использованииvs codeРазвитие и использованиеESLintЕсли его нужно изменитьTSГрамматически поддерживаемые суффиксы, добавьтеtypescriptreactнекоторую обработку, это автоматически исправит некоторыеESLintправило:

"eslint.validate": [
  "javascript",
  "javascriptreact",
  {
    "language": "typescript",
    "autoFix": true
  },
  {
    "language": "typescriptreact",
    "autoFix": true
  }
]

конфигурация веб-пакета

Потому что он используется в передней частиReact, согласно нынешнему мейнстриму,webpackОпределенно необходимо.
не выбрал зрелогоcra(create-react-app) для построения среды по следующим причинам:

  1. webpackЯ не пробовал с тех пор как обновился до 4, сам хочу поиграть
  2. комбинированныйTSПомимо вещей внутри компании, будут некоторые ситуации с пользовательской конфигурацией, опасаясь, что вторичная разработка будет слишком громоздкой.

Но на самом деле конфигурации не так много.Фреймворк пользовательского интерфейса, выбранный для этой реконструкции, является реализацией Google Material:material-ui
И они использовалиjssчтобы написать стиль, чтобы он не включал предыдущий идиоматическийscssнаборloader.

webpackОн разделен на следующие файлы:

file desc
common.js публичныйwebpackконфигурация, какenvтакие варианты, как
dll.js Используется для упаковки некоторых сторонних библиотек, которые не будут изменены заранее, чтобы ускорить эффективность компиляции во время разработки.
base.js можно понимать какwebpackБазовый файл конфигурации, общийloaderтак же какpluginsэто здесь
pro.js Специальная конфигурация для производственной среды (сжатие кода, загрузка ресурсов)
dev.js Специальная конфигурация для среды разработки (source-map)

dllЭто очень старая процедура, и ее, вероятно, необходимо изменить следующим образом:

  1. создать отдельныйwebpackфайл для генерацииdllдокумент
  2. в обычномwebpackгенерируется ссылкой в ​​файлеdllдокумент
// dll.js
{
  entry: {
    // 需要提前打包的库
    vendors: [
      'react',
      'react-dom',
      'react-router-dom',
      'babel-polyfill',
    ],
  },
  output: {
    filename: 'vendors.dll.js',
    path: path.resolve(__dirname, '../../client-dist'),
    // 输出时不要少了这个option
    library: 'vendors_lib',
  },
  plugins: [
    new webpack.DllPlugin({
      context: __dirname,
      // 向外抛出的`vendors.dll.js`代码的具体映射,引用`dll`文件的时候通过它来做映射关系的
      path: path.join(__dirname, '../dist/vendors-manifest.json'),
      name: 'vendors_lib',
    })
  ]
}

// base.js
{
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require('../dist/vendors-manifest.json'),
    }),
  ]
}

так вwatchфайл, упаковка будет пропущенаverdorsТе пакеты, которые существуют в .
Следует отметить одну вещь: если вам в конечном итоге понадобится загрузить эти статические ресурсы, не забудьте включитьverdors.dll.jsзагрузить вместе

При локальном развитииvendorsфайлы не внедряются автоматически вhtmlшаблон, поэтому мы использовали другой плагин,add-asset-html-webpack-plugin. В то же время вы также можете столкнуться сwebpackНеограниченная переупаковка, для этого требуется настройкаignoreрешать -.-:

// dev.js
const HtmlWebpackPlugin = require('html-webpack-plugin')
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')

{
  plugins: [
    // 将`ejs`模版文件放到目标文件夹,并注入入口`js`文件
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, '../index.ejs'),
      filename: path.resolve(__dirname, '../../views/index.ejs'),
    }),
    // 将`vendors`文件注入到`ejs`模版中
    new AddAssetHtmlPlugin({
      filepath: path.resolve(__dirname, '../../client-dist/vendors.dll.js'),
      includeSourcemap: false,
    }),
    // 忽略`ejs`和`js`的文件变化,避免`webpack`无限重新打包的问题
    new webpack.WatchIgnorePlugin([
      /\.ejs$/,
      /\.js$/,
    ]),
  ]
}

Конфигурация, связанная с TypeScript

TSКонфигурация разделена на две части, однаwebpackконфиг, другойtsconfigКонфигурация.

прежде всегоwebpack,противts,tsxфайл мы используем дваloader:

{
  rules: [
    {
      test: /\.tsx?$/,
      use: ['babel-loader', 'ts-loader'],
      exclude: /node_modules/,
    }
  ],
  resolve: {
    // 一定不要忘记配置ts tsx后缀
    extensions: ['.tsx', '.ts', '.js'],
  }
}

ts-loaderдляTSНекоторые свойстваJSсовместимый синтаксис, затем выполнитеbabelобрабатыватьreact/jsxСоответствующий код, который в конечном итоге генерирует исполняемый файлJSкод.

ПослеtsconfigКонфигурация,ts-loaderВыполнение основано на конфигурации здесь, и общая конфигурация выглядит следующим образом:

{
  "compilerOptions": {
    "module": "esnext",
    "target": "es6",
    "allowSyntheticDefaultImports": true,
    // import的相对起始路径
    "baseUrl": ".",
    "sourceMap": true,
    // 构建输出目录,但因为使用了`webpack`,所以这个配置并没有什么卵用
    "outDir": "../client-dist",
    // 开启`JSX`模式, 
    // `preserve`的配置让`tsc`不会去处理它,而是使用后续的`babel-loader`进行处理
    "jsx": "preserve", 
    "strict": true,
    "moduleResolution": "node",
    // 开启装饰器的使用
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    // `vs code`所需要的,在开发时找到对应的路径,真实的引用是在`webpack`中配置的`alias`
    "paths": {
      "@Common": [
        "../src/common"
      ],
      "@Common/*": [
        "../src/common/*"
      ]
    }
  },
  "exclude": [
    "node_modules"
  ]
}

Конфигурация ESLint

Недавно наша команда была основана наairbnbизESLintПравила немного доработали, создав свои собственныеeslint-config-blued
также существуетreactа такжеtypescriptдве производные версии.

оESLintфайл конфигурации.eslintrc, в этом проекте две копии. один является корневым каталогомblued-typescript, другойclient-srcвнизblued-react + blued-typescript.
Поскольку корневой каталог больше используется дляnodeпроект, поэтому нет необходимости ставитьreactКакие зависимости также установлены.

# .eslintrc
extends: blued-typescript

# client-src/.eslintrc
extends: 
  - blued-react
  - blued-typescript

Небольшая деталь, на которую стоит обратить внимание
потому что нашreactа такжеtypescriptиспользуется в версии реализацииparser.
reactиспользуетbabel-eslint,typescriptиспользуетtypescript-eslint-parser.
ноparserМожет быть только один, изoptionЭто видно из названияextends,plugins,rules,прибытьparserНет множественного числа.
Итак, эти два плагина находятся вextendsПорядок, в котором становится критическим,babelЯ не могу понять это сейчасTSсинтаксис, но похожеbabelРазработчик имеет поддержкуTSизбуду.
Но пока мы должны обеспечитьreactспереди,typescriptпосле, вот такparserбудет использованоtypescript-eslint-parserпокрывать.

Модификация слоя узла

В дополнение к общему коду на обоих концах, упомянутому выше, вам также необходимо добавитьcontrollerиспользуется, чтобы выплюнуть страницу, потому что с помощьюrouting-controllersЭта библиотека, отображающая статическую страницу, очень хорошо инкапсулирована, нужно только изменить две страницы, одну для настройкиrenderКорневой каталог шаблона, другой используется для установки имени выдаваемого шаблона:

// controller/index.ts
import {
  Get,
  Controller,
  Render,
} from 'routing-controllers'

@Controller('/')
export default class {
  @Get('/')
  @Render('index') // 指定一个模版的名字
  async router() {
    // 渲染页面时的一些变量
    // 类似之前的 ctx.state = XXX
    return {
      title: 'First TypeScript React App',
    }
  }
}

// app.ts
import koaViews from 'koa-views'

// 添加模版所在的目录
// 以及使用的渲染引擎、文件后缀
app.use(koaViews(path.join(__dirname, '../views'), {
  options: {
    ext: 'ejs',
  },
  extension: 'ejs',
}))

Если страниц несколько, создайте несколько страниц дляRenderизtsФайл вроде

глубокая яма, обратите внимание

токrouting-controllerдляKoaПоддержка не очень, (у оригинального автора естьKoaне очень понимаю, что приводит кRenderПосле того, как соответствующий интерфейс будет запрошен один раз, все последующие интерфейсы будут напрямую возвращать файл шаблона, потому что шаблон отвечает за отрисовкуURLПри запуске он должен возвращать данные, но текущая обработка заключается в добавлении промежуточного программного обеспечения кKoaВ шаблоне, поэтому любые запросы будут возвращены в виде файла данных), поэтому@Renderне относится кKoaводить машину.
Но я уже представилPR, прогнал тесткейс, и дождался слияния кода, но это план временной модификации, который предполагает порядок регистрации этой библиотеки для внешнего промежуточного ПО, поэтому дляapp.tsДля этого требуются дополнительные модификации.

// app.ts 的修改
import 'reflect-metadata'
import Koa from 'koa'
import koaViews from 'koa-views'
import { useKoaServer } from 'routing-controllers'
import { distPath } from './config'

// 手动创建koa实例,然后添加`render`的中间件,确保`ctx.render`方法会在请求的头部就被添加进去
const koa = new Koa()

koa.use(koaViews(path.join(__dirname, '../views'), {
  options: {
    ext: 'ejs',
  },
  extension: 'ejs',
}))

// 使用`useKoaServer`而不是`createKoaServer`
const app = useKoaServer(koa, {
  controllers: [`${__dirname}/controllers/**/*{.js,.ts}`],
})

// 后续的逻辑就都一样了
export default app

Конечно, это логика после выхода новой версии, и ее можно обойти на основе существующей структуры, но нельзя использовать@RenderДекоратор, оставь этоkoa-viewsнепосредственно использовать внутреннююconsolidate:

// controller/index.ts
// 这个修改不需要改动`app.ts`,可以直接使用`createKoaServer`
import {
  Get,
  Controller,
} from 'routing-controllers'
import cons from 'consolidate'
import path from 'path'

@Controller()
export default class {
  @Get('/')
  async router() {
    // 直接在接口返回时获取模版渲染后的数据
    return cons.ejs(path.resolve(__dirname, '../../views/index.ejs'), {
      title: 'Example For TypeScript React App',
    })
  }
}

Текущий пример кода использует приведенную выше схему.

резюме

На данный момент построена полная архитектура проекта TS front-end и back-end (оставшаяся задача — залить код в скелет).
я обновил предыдущийtypescript-exmapleДобавлены некоторые внешние интерфейсы, используемые в этом рефакторинге.TS+Reactнапример, также включает@Renderнекоторые совместимы.

TypeScriptотличная идея, решил N многоjavaScriptДосадная проблема.
Использование статического языка для разработки может не только повысить эффективность разработки, но и снизить вероятность ошибок.
в сочетании с мощнымvs code, Наслаждайся этим.

при использованииTSЕсли у вас есть какие-либо вопросы в процессе или есть идеи получше, добро пожаловать на общение и обсуждение.

One more things

Команда Blued front-end/Node набирает сотрудников. . Неполная средняя школа и средняя школа имеют HC
Координаты Чаоян Шуанцзин, имперской столицы, если интересно, свяжитесь со мной:
чат: github_jiasm
Почта:jiashunming@blued.com

Добро пожаловать, чтобы оставить свое резюме