Недавно открыл новый проект Node, используя TypeScript для разработки, используя множество декораторов в управлении базой данных и маршрутизацией, и обнаружил, что это действительно хорошая вещь.
Декоратор — это функция, которая все еще находится в черновике.В настоящее время нет среды, которая напрямую поддерживает этот синтаксис, но его можно преобразовать в старый синтаксис через babel и тому подобное для достижения эффекта, поэтому в TypeScript вы можете используйте его с уверенностью@Decorator
.
что такое декоратор
Декоратор — это украшение класса, функции, свойства и т. д., к которому можно добавить дополнительное поведение.
Популярное понимание можно рассматривать как обертывание слоя логики обработки во внешний слой исходного кода.
Лично считаю декораторы решением, а не узким@Decorator
, последний является просто синтаксическим сахаром.
Примеры декораторов можно увидеть повсюду.Простой пример барботер на кране-декоратор.После его установки он будет подмешивать воздух в поток воды и подмешивать в воду много пузырьков.
Тем не менее, установлен ли барботер или нет, это не влияет на сам кран.Даже если барботер будет удален, он все равно будет работать.Функция крана-управление клапаном.
Поэтому для декораторов это можно просто понимать как ненавязчивую модификацию поведения.
Зачем использовать декораторы
Могут быть случаи, когда мы будем судить о типе входящих параметров, сортировать и фильтровать возвращаемое значение, добавлять к функциям троттлинг, анти-шейк или другие функциональные коды, основанные на наследовании нескольких классов, различных и сама по себе логика функции не имеет ничего делать с повторяющимся кодом.
роль в функции
Представьте, что у нас есть служебный класс, предоставляющий функцию для получения данных:
class Model1 {
getData() {
// 此处省略获取数据的逻辑
return [{
id: 1,
name: 'Niko'
}, {
id: 2,
name: 'Bellic'
}]
}
}
console.log(new Model1().getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]
console.log(Model1.prototype.getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]
Теперь мы хотим добавить функцию, которая записывает, сколько времени занимает выполнение функции.
Поскольку эта функция используется многими людьми для добавления трудоемкой статистической логики, нежелательной в вызывающей программе, поэтому мы хотимModel1
Проводящий:
class Model1 {
getData() {
+ let start = new Date().valueOf()
+ try {
// 此处省略获取数据的逻辑
return [{
id: 1,
name: 'Niko'
}, {
id: 2,
name: 'Bellic'
}]
+ } finally {
+ let end = new Date().valueOf()
+ console.log(`start: ${start} end: ${end} consume: ${end - start}`)
+ }
}
}
// start: XXX end: XXX consume: XXX
console.log(new Model1().getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]
// start: XXX end: XXX consume: XXX
console.log(Model1.prototype.getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]
Таким образом, после вызова метода мы можем увидеть трудоемкий вывод в консоли.
Однако прямое изменение исходного кода функции таким образом имеет следующие проблемы:
- Трудоемкий связанный код не имеет ничего общего с логикой самой функции, что влияет на понимание самой исходной функции и вызывает деструктивные изменения в структуре функции.
- Если на более позднем этапе есть еще похожие функции, которым необходимо добавить статистически трудоемкий код, очевидно, что добавлять такой код к каждой функции будет неэффективно, а затраты на обслуживание слишком высоки.
Поэтому, чтобы сделать логику статистических затрат времени более гибкой, мы создадим новую служебную функцию, которая будет обертывать функцию, которая должна задавать статистические затраты времени.
поставивClass
с целевой функциейname
Передано в функцию для реализации общей трудоемкой статистики:
function wrap(Model, key) {
// 获取Class对应的原型
let target = Model.prototype
// 获取函数对应的描述符
let descriptor = Object.getOwnPropertyDescriptor(target, key)
// 生成新的函数,添加耗时统计逻辑
let log = function (...arg) {
let start = new Date().valueOf()
try {
return descriptor.value.apply(this, arg) // 调用之前的函数
} finally {
let end = new Date().valueOf()
console.log(`start: ${start} end: ${end} consume: ${end - start}`)
}
}
// 将修改后的函数重新定义到原型链上
Object.defineProperty(target, key, {
...descriptor,
value: log // 覆盖描述符重的value
})
}
wrap(Model1, 'getData')
wrap(Model2, 'getData')
// start: XXX end: XXX consume: XXX
console.log(new Model1().getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]
// start: XXX end: XXX consume: XXX
console.log(Model2.prototype.getData()) // [ { id: 1, name: 'Niko'}, { id: 2, name: 'Bellic' } ]
Далее мы хотим управлять одним изModel
Функция не может быть переопределена другими, поэтому добавьте новую логику:
function wrap(Model, key) {
// 获取Class对应的原型
let target = Model.prototype
// 获取函数对应的描述符
let descriptor = Object.getOwnPropertyDescriptor(target, key)
Object.defineProperty(target, key, {
...descriptor,
writable: false // 设置属性不可被修改
})
}
wrap(Model1, 'getData')
Model1.prototype.getData = 1 // 无效
Видно, что дваwrap
В функции много повторений, и логика изменения поведения программы на самом деле зависит отObject.defineProperty
Три параметра прошли.
Итак, мы сосредоточились наwrap
После внесения изменений преобразуйте его в универсальный класс:
function wrap(decorator) {
return function (Model, key) {
let target = Model.prototype
let dscriptor = Object.getOwnPropertyDescriptor(target, key)
decorator(target, key, descriptor)
}
}
let log = function (target, key, descriptor) {
// 将修改后的函数重新定义到原型链上
Object.defineProperty(target, key, {
...descriptor,
value: function (...arg) {
let start = new Date().valueOf()
try {
return descriptor.value.apply(this, arg) // 调用之前的函数
} finally {
let end = new Date().valueOf()
console.log(`start: ${start} end: ${end} consume: ${end - start}`)
}
}
})
}
let seal = function (target, key, descriptor) {
Object.defineProperty(target, key, {
...descriptor,
writable: false
})
}
// 参数的转换处理
log = wrap(log)
seal = warp(seal)
// 添加耗时统计
log(Model1, 'getData')
log(Model2, 'getData')
// 设置属性不可被修改
seal(Model1, 'getData')
После этого шага мы можем сказатьlog
а такжеseal
Нам как декоратору удобно добавлять поведение к некоторым функциям.
Эти разделенные функции можно использовать там, где может возникнуть потребность в будущем, без необходимости переделывать ту же логику.
Роль в классе
Как упоминалось выше, на данном этапе в JS существует несколько вариантов наследования.Class
Это головная боль, нет прямого синтаксиса для наследования нескольких классов.
class A { say () { return 1 } }
class B { hi () { return 2 } }
class C extends A, B {} // Error
class C extends A extends B {} // Error
// 这样才是可以的
class C {}
for (let key of Object.getOwnPropertyNames(A.prototype)) {
if (key === 'constructor') continue
Object.defineProperty(C.prototype, key, Object.getOwnPropertyDescriptor(A.prototype, key))
}
for (let key of Object.getOwnPropertyNames(B.prototype)) {
if (key === 'constructor') continue
Object.defineProperty(C.prototype, key, Object.getOwnPropertyDescriptor(B.prototype, key))
}
let c = new C()
console.log(c.say(), c.hi()) // 1, 2
Итак, вReact
есть одинmixin
концепция, используемая для объединения несколькихClass
скопировать функционал на новыйClass
начальство.
Общая идея описана выше, но этоmixin
даReact
Операцию, встроенную в , мы можем превратить в более точную реализацию декоратора.
без изменения оригиналаClass
случай, другойClass
Свойства копируются поверх:
function mixin(constructor) {
return function (...args) {
for (let arg of args) {
for (let key of Object.getOwnPropertyNames(arg.prototype)) {
if (key === 'constructor') continue // 跳过构造函数
Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key))
}
}
}
}
mixin(C)(A, B)
let c = new C()
console.log(c.say(), c.hi()) // 1, 2
Выше приведен декоратор в функции,Class
Способ реализации (по крайней мере на данный момент), но в черновике есть особый сладкий грамматический сахар, т.е.@Decorator
.
Это может сэкономить вам много утомительных шагов по использованию декораторов.
Как использовать @Декоратор
Декоратор в черновике, или, можно сказать, декоратор, реализованный TS, дополнительно инкапсулирует два вышеупомянутых и разделяет их на более тонкие приложения-декораторы.В настоящее время поддерживаются следующие варианты использования:
- Class
- функция
- получить доступ к набору
- Свойства экземпляра, статические функции и свойства
- параметр функции
Синтаксис @Decorator относительно прост, то есть через@
За символом следует ссылка на функцию-декоратор:
@tag
class A {
@method
hi () {}
}
function tag(constructor) {
console.log(constructor === A) // true
}
function method(target) {
console.log(target.constructor === A, target === A.prototype) // true, true
}
функцияtag
а такжеmethod
Будет вclass A
Выполняется при определении.
@Decorator используется в классе
Декоратор будет вызываться до того, как класс будет определен, если функция возвращает значение, оно будет рассмотрено перед новым конструктором для замены конструктора.
Функция принимает один параметр:
- конструктор перед конструктором
Мы можем внести некоторые изменения в исходный конструктор:
добавить некоторые свойства
Если вы хотите добавить некоторые свойства, есть два варианта:
- создать новый
class
унаследовано от оригиналаclass
и добавьте атрибут - для текущего
class
модифицировать
Последний имеет более узкую область применения и ближе к тому, как обрабатываются миксины.
@name
class Person {
sayHi() {
console.log(`My name is: ${this.name}`)
}
}
// 创建一个继承自Person的匿名类
// 直接返回并替换原有的构造函数
function name(constructor) {
return class extends constructor {
name = 'Niko'
}
}
new Person().sayHi()
Изменить дескриптор исходного атрибута
@seal
class Person {
sayHi() {}
}
function seal(constructor) {
let descriptor = Object.getOwnPropertyDescriptor(constructor.prototype, 'sayHi')
Object.defineProperty(constructor.prototype, 'sayHi', {
...descriptor,
writable: false
})
}
Person.prototype.sayHi = 1 // 无效
Используйте замыкания для улучшения функциональности декораторов
Известен как фабрика декораторов в документации TS.
потому что@
За символом следует ссылка на функцию, поэтому для реализации миксинов мы можем легко использовать замыкания для реализации:
class A { say() { return 1 } }
class B { hi() { return 2 } }
@mixin(A, B)
class C { }
function mixin(...args) {
// 调用函数返回装饰器实际应用的函数
return function(constructor) {
for (let arg of args) {
for (let key of Object.getOwnPropertyNames(arg.prototype)) {
if (key === 'constructor') continue // 跳过构造函数
Object.defineProperty(constructor.prototype, key, Object.getOwnPropertyDescriptor(arg.prototype, key))
}
}
}
}
let c = new C()
console.log(c.say(), c.hi()) // 1, 2
Применение нескольких декораторов
Декоратор можно применять более одного за раз (иначе он теряет свой первоначальный смысл).
Использование заключается в следующем:
@decorator1
@decorator2
class { }
Порядок выполнения такойdecorator2
-> decorator1
,Покинутьclass
Определяет самые последние для выполнения в первую очередь.
Думайте об этом в виде вложенных функций:
decorator1(decorator2(class {}))
Использование @Decorator в членах класса
@Decorator для членов класса должен быть наиболее широко используемым, функции, свойства,get
,set
Средства доступа, все из которых можно считать членами класса.
в документации ТС делится наMethod Decorator
,Accessor Decorator
а такжеProperty Decorator
, по сути то же самое.
Относительно этого типа декоратора получаются следующие три параметра:
- Если декоратор смонтирован на статическом члене, он вернет конструктор, если он смонтирован на члене-экземпляре, он вернет прототип класса.
- Имя члена, смонтированного декоратором
- Дескриптор члена, т.е.
Object.getOwnPropertyDescriptor
Возвращаемое значение
Property Decorator
Третий параметр возвращаться не будет, но вы можете получить его вручную самостоятельно
Предпосылкой являются статические члены, а не члены экземпляра, потому что декораторы запускаются при создании класса, а члены экземпляра выполняются при создании экземпляра класса, поэтому нет способа получить соответствующий дескриптор.
Разница между статическим членом и членом-экземпляром в возвращаемом значении
Это может быть немного яснее, разница между статическими членами и членами-экземплярами:
class Model {
// 实例成员
method1 () {}
method2 = () => {}
// 静态成员
static method3 () {}
static method4 = () => {}
}
method1
а такжеmethod2
является членом экземпляра,method1
Существовать вprototype
выше, иmethod2
Только после создания экземпляра объекта.
как статический членmethod3
а такжеmethod4
, разница между ними заключается в настройке перечислимых дескрипторов, поэтому вы можете просто подумать, что приведенный выше код преобразован в версию ES5 следующим образом:
function Model () {
// 成员仅在实例化时赋值
this.method2 = function () {}
}
// 成员被定义在原型链上
Object.defineProperty(Model.prototype, 'method1', {
value: function () {},
writable: true,
enumerable: false, // 设置不可被枚举
configurable: true
})
// 成员被定义在构造函数上,且是默认的可被枚举
Model.method4 = function () {}
// 成员被定义在构造函数上
Object.defineProperty(Model, 'method3', {
value: function () {},
writable: true,
enumerable: false, // 设置不可被枚举
configurable: true
})
Как видно, толькоmethod2
Он назначается только при создании экземпляра, несуществующее свойство не будет иметь значения.descriptor
, вот почему TS нацеленProperty Decorator
Причина непередачи третьего параметра, почему статический элемент не передаетсяdescriptor
, в настоящее время не найдено разумного объяснения, но если вы хотите использовать его явно, вы можете получить его вручную.
Как и в приведенном выше примере, после того, как мы добавили декораторы ко всем четырем членам,method1
а такжеmethod2
Первый параметрModel.prototype
,а такжеmethod3
а такжеmethod4
Первый параметрModel
.
class Model {
// 实例成员
@instance
method1 () {}
@instance
method2 = () => {}
// 静态成员
@static
static method3 () {}
@static
static method4 = () => {}
}
function instance(target) {
console.log(target.constructor === Model)
}
function static(target) {
console.log(target === Model)
}
Разница между функциями, аксессорами и декораторами свойств
функция
Во-первых, это функция, возвращаемое значение декоратора функции будет использоваться как свойство по умолчанию.value
Дескриптор существует, если возвращаемое значениеundefined
Игнорируется, перед использованиемdescriptor
Ссылки как дескрипторы функций.
Итак, для нашей первоначальной статистической трудоемкой логики мы можем сделать это:
class Model {
@log1
getData1() {}
@log2
getData2() {}
}
// 方案一,返回新的value描述符
function log1(tag, name, descriptor) {
return {
...descriptor,
value(...args) {
let start = new Date().valueOf()
try {
return descriptor.value.apply(this, args)
} finally {
let end = new Date().valueOf()
console.log(`start: ${start} end: ${end} consume: ${end - start}`)
}
}
}
}
// 方案二、修改现有描述符
function log2(tag, name, descriptor) {
let func = descriptor.value // 先获取之前的函数
// 修改对应的value
descriptor.value = function (...args) {
let start = new Date().valueOf()
try {
return func.apply(this, args)
} finally {
let end = new Date().valueOf()
console.log(`start: ${start} end: ${end} consume: ${end - start}`)
}
}
}
аксессуар
Аксессуар должен добавитьget
,set
Префиксная функция используется для управления операцией присваивания и значения атрибутов.Она ничем не отличается от используемой функции, и нет никакой разницы в обработке возвращаемого значения.
Но нам нужно установить соответствующийget
илиset
Дескрипторы просто:
class Modal {
_name = 'Niko'
@prefix
get name() { return this._name }
}
function prefix(target, name, descriptor) {
return {
...descriptor,
get () {
return `wrap_${this._name}`
}
}
}
console.log(new Modal().name) // wrap_Niko
Атрибуты
Для декораторов недвижимости нет возвратаdescriptor
, и возвращаемое значение функции декоратора также будет проигнорировано.Если мы хотим изменить статическое свойство, нам нужно получить его самостоятельноdescriptor
:
class Modal {
@prefix
static name1 = 'Niko'
}
function prefix(target, name) {
let descriptor = Object.getOwnPropertyDescriptor(target, name)
Object.defineProperty(target, name, {
...descriptor,
value: `wrap_${descriptor.value}`
})
return target
}
console.log(Modal.name1) // wrap_Niko
Для свойств экземпляра нет схемы прямого изменения, но мы можем объединить некоторые другие декораторы, чтобы спасти страну.
Например, у нас есть класс, который будет передавать имя и возраст в качестве параметров инициализации, а затем нам нужно установить соответствующую проверку формата для этих двух параметров:
const validateConf = {} // 存储校验信息
@validator
class Person {
@validate('string')
name
@validate('number')
age
constructor(name, age) {
this.name = name
this.age = age
}
}
function validator(constructor) {
return class extends constructor {
constructor(...args) {
super(...args)
// 遍历所有的校验信息进行验证
for (let [key, type] of Object.entries(validateConf)) {
if (typeof this[key] !== type) throw new Error(`${key} must be ${type}`)
}
}
}
}
function validate(type) {
return function (target, name, descriptor) {
// 向全局对象中传入要校验的属性名及类型
validateConf[name] = type
}
}
new Person('Niko', '18') // throw new error: [age must be number]
Во-первых, добавьте декоратор в класс@validator
, а затем добавьте два параметра, которые необходимо проверить.@validate
Decorator, два декоратора используются для передачи информации глобальному объекту, чтобы записать, какие свойства необходимо проверить.
затем вvalidator
Наследовать исходный объект класса и просмотреть всю проверочную информацию, только что установленную для проверки после создания экземпляра.Если обнаружится, что существует ошибка типа, исключение будет выдано напрямую.
Операция проверки этого типаClass
Это почти незаметно.
декоратор параметров функции
Наконец, есть декоратор для параметров функции.Этот декоратор тоже как свойство экземпляра.Его нельзя использовать отдельно.Ведь функция вызывается во время выполнения,и какой бы декоратор это ни был,он объявление класса при вызове (что можно считать временем псевдокомпиляции).
Декоратор параметров функции принимает три параметра:
- Аналогично вышеописанной операции прототип класса или конструктор класса
- Имя функции, в которой находится параметр
- Позиция параметра в формальных параметрах функции (первый параметр в сигнатуре функции)
Простой пример, мы можем объединить декоратор функций, чтобы завершить преобразование типов параметров функции:
const parseConf = {}
class Modal {
@parseFunc
addOne(@parse('number') num) {
return num + 1
}
}
// 在函数调用前执行格式化操作
function parseFunc (target, name, descriptor) {
return {
...descriptor,
value (...arg) {
// 获取格式化配置
for (let [index, type] of parseConf) {
switch (type) {
case 'number': arg[index] = Number(arg[index]) break
case 'string': arg[index] = String(arg[index]) break
case 'boolean': arg[index] = String(arg[index]) === 'true' break
}
}
return descriptor.value.apply(this, arg)
}
}
}
// 向全局对象中添加对应的格式化信息
function parse(type) {
return function (target, name, index) {
parseConf[index] = type
}
}
console.log(new Modal().addOne('10')) // 11
Реализация интересной оболочки Koa с помощью декораторов
Например, при написании интерфейса узла можно использоватьkoa
илиexpress
, вообще параметров запроса может быть много, есть изheaders
Да, есть изbody
, даже изquery
,cookie
из.
Так что вполне вероятно, чтоrouter
Первые несколько строк операции выглядят так:
router.get('/', async (ctx, next) => {
let id = ctx.query.id
let uid = ctx.cookies.get('uid')
let device = ctx.header['device']
})
и если у нас есть большое количество интерфейсов, может быть большое количествоrouter.get
,router.post
.
и если вы хотите классифицировать модули, может быть большое количествоnew Router
операция.
Эти коды не связаны с самой бизнес-логикой, поэтому мы должны максимально упростить пропорции этих кодов, и использование декораторов может помочь нам в достижении этой цели.
Подготовка декоратора.
// 首先,我们要创建几个用来存储信息的全局List
export const routerList = []
export const controllerList = []
export const parseList = []
export const paramList = []
// 虽说我们要有一个能够创建Router实例的装饰器
// 但是并不会直接去创建,而是在装饰器执行的时候进行一次注册
export function Router(basename = '') {
return (constrcutor) => {
routerList.push({
constrcutor,
basename
})
}
}
// 然后我们在创建对应的Get Post请求监听的装饰器
// 同样的,我们并不打算去修改他的任何属性,只是为了获取函数的引用
export function Method(type) {
return (path) => (target, name, descriptor) => {
controllerList.push({
target,
type,
path,
method: name,
controller: descriptor.value
})
}
}
// 接下来我们还需要用来格式化参数的装饰器
export function Parse(type) {
return (target, name, index) => {
parseList.push({
target,
type,
method: name,
index
})
}
}
// 以及最后我们要处理的各种参数的获取
export function Param(position) {
return (key) => (target, name, index) => {
paramList.push({
target,
key,
position,
method: name,
index
})
}
}
export const Body = Param('body')
export const Header = Param('header')
export const Cookie = Param('cookie')
export const Query = Param('query')
export const Get = Method('get')
export const Post = Method('post')
Обработка услуг Koa
Вышеупомянутое предназначено для создания всех декораторов, которые необходимо использовать, но это сохраняет только различную информацию, которая нам нужна, и как использовать эти декораторы — это следующее, что нужно сделать:
const routers = []
// 遍历所有添加了装饰器的Class,并创建对应的Router对象
routerList.forEach(item => {
let { basename, constrcutor } = item
let router = new Router({
prefix: basename
})
controllerList
.filter(i => i.target === constrcutor.prototype)
.forEach(controller => {
router[controller.type](controller.path, async (ctx, next) => {
let args = []
// 获取当前函数对应的参数获取
paramList
.filter( param => param.target === constrcutor.prototype && param.method === controller.method )
.map(param => {
let { index, key } = param
switch (param.position) {
case 'body': args[index] = ctx.request.body[key] break
case 'header': args[index] = ctx.headers[key] break
case 'cookie': args[index] = ctx.cookies.get(key) break
case 'query': args[index] = ctx.query[key] break
}
})
// 获取当前函数对应的参数格式化
parseList
.filter( parse => parse.target === constrcutor.prototype && parse.method === controller.method )
.map(parse => {
let { index } = parse
switch (parse.type) {
case 'number': args[index] = Number(args[index]) break
case 'string': args[index] = String(args[index]) break
case 'boolean': args[index] = String(args[index]) === 'true' break
}
})
// 调用实际的函数,处理业务逻辑
let results = controller.controller(...args)
ctx.body = results
})
})
routers.push(router.routes())
})
const app = new Koa()
app.use(bodyParse())
app.use(compose(routers))
app.listen(12306, () => console.log('server run as http://127.0.0.1:12306'))
Вышеприведенный код собрал пакет Koa и включает в себя обработку различных декораторов Далее следует практическое применение этих декораторов:
import { Router, Get, Query, Parse } from "../decorators"
@Router('')
export default class {
@Get('/')
index (@Parse('number') @Query('id') id: number) {
return {
code: 200,
id,
type: typeof id
}
}
@Post('/detail')
detail (
@Parse('number') @Query('id') id: number,
@Parse('number') @Body('age') age: number
) {
return {
code: 200,
age: age + 1
}
}
}
Легко добитьсяrouter
Создание, путь, обработка методов, включая получение различных параметров, преобразование типов.
Все виды кода, не связанного с бизнес-логикой, выполняются декоратором, а сама функция отвечает только за обработку своей собственной логики.
Вот полный код:GitHub. После установки зависимостейnpm start
Вы можете увидеть эффект.
Преимущество такой разработки заключается в том, чтобы сделать код более читабельным и больше сосредоточиться на том, что вы должны делать в функции.
И если имя самого декоратора достаточно хорошее, его можно в какой-то степени рассматривать как комментарий к документу (в Java есть нечто подобное, называемое аннотацией).
Суммировать
Рациональное использование декораторов может значительно повысить эффективность разработки, а инкапсуляция и уточнение некоторых нелогически связанных кодов может помочь нам быстро выполнять повторяющиеся задачи и экономить время.
Но как бы ни был вкусен сахар, не ешь лишнего, зубы сломать легко, а то же злоупотребление декораторами запутает логику самого кода, если ты решишь, что кусок кода не будет использоваться в другом месте или основная логика функции. Это код, поэтому нет необходимости использовать его как декоратор для существования.
использованная литература
One more thing
В нашу компанию сейчас набирается очень много людей, есть ХК по направлениям front-end и Node.
Название организации:Blued, координаты Чаоян Шуанцзин, столица империи
Основной стек технологий — React, также будут возможности поиграть в ReactNative и Electron.
Node direction версия 8.x + коа новый проект будет основан на TS
Заинтересованные друзья могут связаться со мной для получения подробной информации:
email: jiashunming@blued.com
wechat: github_jiasm