Awilix
Awilix là một Dependency Injection (DI) container vô cùng mạnh mẽ, hiệu suất cao và đã được thử nghiệm trên chiến trường cho JavaScript/Node, được viết trong TypeScript.
Awilix cho phép bạn viết phần mềm có khả năng kết hợp, có thể kiểm thử bằng cách sử dụng dependency injection mà không cần các chú thích đặc biệt , điều này từ đó tách rời mã ứng dụng cốt lõi của bạn khỏi các chi tiết phức tạp của cơ chế DI.
💡 Hãy xem qua intro to Dependency Injection with Awilix này
Cài đặt
Cài đặt bằng npm
npm install awilix
Hoặc yarn
yarn add awilix
Bạn cũng có thể sử dụng phiên bản UMD từ unpkg
<script src="https://unpkg.com/awilix/lib/awilix.umd.js"/>
<script>
const container = Awilix.createContainer()
</script>
Sử dụng
Awilix có một API khá đơn giản (nhưng có nhiều cách khả dĩ để gọi nó). Tối thiểu, bạn cần làm 3 điều sau:
- Tạo một container
- Đăng ký một số module vào trong
- Giải quyết và sử dụng!
index.js
const awilix = require('awilix')
// Create the container and set the injectionMode to PROXY (which is also the default).
const container = awilix.createContainer({
injectionMode: awilix.InjectionMode.PROXY
})
// This is our app code... We can use
// factory functions, constructor functions
// and classes freely.
class UserController {
// We are using constructor injection.
constructor(opts) {
// Save a reference to our dependency.
this.userService = opts.userService
}
// imagine ctx is our HTTP request context...
getUser(ctx) {
return this.userService.getUser(ctx.params.id)
}
}
container.register({
// Here we are telling Awilix how to resolve a
// userController: by instantiating a class.
userController: awilix.asClass(UserController)
})
// Let's try with a factory function.
const makeUserService = ({ db }) => {
// Notice how we can use destructuring
// to access dependencies
return {
getUser: id => {
return db.query(`select * from users where id=${id}`)
}
}
}
container.register({
// the `userService` is resolved by
// invoking the function.
userService: awilix.asFunction(makeUserService)
})
// Alright, now we need a database.
// Let's make that a constructor function.
// Notice how the dependency is referenced by name
// directly instead of destructuring an object.
// This is because we register it in "CLASSIC"
// injection mode below.
function Database(connectionString, timeout) {
// We can inject plain values as well!
this.conn = connectToYourDatabaseSomehow(connectionString, timeout)
}
Database.prototype.query = function(sql) {
// blah....
return this.conn.rawSql(sql)
}
// We use register coupled with asClass to tell Awilix to
// use `new Database(...)` instead of just `Database(...)`.
// We also want to use `CLASSIC` injection mode for this
// registration. Read more about injection modes below.
container.register({
db: awilix.asClass(Database).classic()
})
// Lastly we register the connection string and timeout values
// as we need them in the Database constructor.
container.register({
// We can register things as-is - this is not just
// limited to strings and numbers, it can be anything,
// really - they will be passed through directly.
connectionString: awilix.asValue(process.env.CONN_STR),
timeout: awilix.asValue(1000)
})
// We have now wired everything up!
// Let's use it! (use your imagination with the router thing..)
router.get('/api/users/:id', container.resolve('userController').getUser)
// Alternatively, using the `cradle` proxy..
router.get('/api/users/:id', container.cradle.userController.getUser)
// Using `container.cradle.userController` is actually the same as calling
// `container.resolve('userController')` - the cradle is our proxy!
Ví dụ này khá dài, nhưng nếu bạn trích xuất các thành phần vào các tệp thích hợp, nó sẽ trở nên dễ quản lý hơn.
Check out a working Koa example!
Quản lý tuổi thọ
Awilix hỗ trợ quản lý tuổi thọ của các thể hiện. Điều này có nghĩa rằng bạn có thể kiểm soát xem các đối tượng có được giải quyết và sử dụng một lần, được lưu trong một phạm vi cụ thể, hoặc được lưu trong suốt thời gian hoạt động của quá trình.
Có 3 loại tuổi thọ có sẵn.
Lifetime.TRANSIENT
: Đây là mặc định. Việc đăng ký sẽ được giải quyết mỗi khi cần. Điều này có nghĩa nếu bạn giải quyết một lớp nhiều hơn một lần, bạn sẽ nhận lại một thể hiện mới mỗi lần.Lifetime.SCOPED
: Đăng ký được giới hạn trong container – điều này có nghĩa rằng giá trị được giải quyết sẽ được sử dụng lại khi giải quyết từ cùng phạm vi (hoặc phạm vi con).Lifetime.SINGLETON
: Đăng ký luôn được sử dụng lại bất kể điều gì – điều này có nghĩa rằng giá trị được giải quyết được lưu trong container gốc.
Chúng được tiết lộ trên đối tượng awilix.Lifetime
.
const Lifetime = awilix.Lifetime
Để đăng ký một module với tuổi thọ cụ thể:
const { asClass, asFunction, asValue } = awilix
class MailService {}
container.register({
mailService: asClass(MailService, { lifetime: Lifetime.SINGLETON })
})
// or using the chaining configuration API..
container.register({
mailService: asClass(MailService).setLifetime(Lifetime.SINGLETON)
})
// or..
container.register({
mailService: asClass(MailService).singleton()
})
// or.......
container.register('mailService', asClass(MailService, { lifetime: SINGLETON }))
Tuổi thọ scoped
Trong ứng dụng web, quản lý trạng thái mà không phụ thuộc quá nhiều vào khung viện web có thể trở nên khó khăn. Phải truyền rất nhiều thông tin vào mọi hàm chỉ để đưa ra các lựa chọn đúng dựa trên người dùng đã xác thực.
Tuổi thọ scoped trong Awilix làm cho điều này trở nên đơn giản – và thú vị!
const { createContainer, asClass, asValue } = awilix
const container = createContainer()
class MessageService {
constructor({ currentUser }) {
this.user = currentUser
}
getMessages() {
const id = this.user.id
// wee!
}
}
container.register({
messageService: asClass(MessageService).scoped()
})
// imagine middleware in some web framework..
app.use((req, res, next) => {
// create a scoped container
req.scope = container.createScope()
// register some request-specific data..
req.scope.register({
currentUser: asValue(req.user)
})
next()
})
app.get('/messages', (req, res) => {
// for each request we get a new message service!
const messageService = req.scope.resolve('messageService')
messageService.getMessages().then(messages => {
res.send(200, messages)
})
})
// The message service can now be tested
// without depending on any request data!
QUAN TRỌNG! Nếu một singleton được giải quyết và nó phụ thuộc vào một đăng ký scoped hoặc transient, những đối tượng này sẽ được giữ lại trong singleton trong suốt tuổi thọ của nó!
const makePrintTime = ({ time }) => () => {
console.log('Time:', time)
}
const getTime = () => new Date().toString()
container.register({
printTime: asFunction(makePrintTime).singleton(),
time: asFunction(getTime).transient()
})
// Resolving `time` 2 times will
// invoke `getTime` 2 times.
container.resolve('time')
container.resolve('time')
// These will print the same timestamp at all times,
// because `printTime` is singleton and
// `getTime` was invoked when making the singleton.
container.resolve('printTime')()
container.resolve('printTime')()
Đọc tài liệu tại container.createScope() để biết thêm ví dụ.
Chế độ tiêm (Injection modes)
Chế độ tiêm xác định cách một hàm/constructor nhận các phụ thuộc của nó. Trước phiên bản 2.3.0, chỉ có một chế độ được hỗ trợ – PROXY
– vẫn là chế độ mặc định.
Awilix v2.3.0 giới thiệu một chế độ tiêm thay thế: CLASSIC
. Các chế độ tiêm có sẵn trên awilix.InjectionMode
InjectionMode.PROXY
(mặc định): Tiêm một proxy vào các hàm/constructor có vẻ giống như một đối tượng thông thường.
class UserService {
constructor(opts) {
this.emailService = opts.emailService
this.logger = opts.logger
}
}
hoặc sử dụng destructuring:
class UserService {
constructor({ emailService, logger }) {
this.emailService = emailService
this.logger = logger
}
}
InjectionMode.CLASSIC
: Phân tích các tham số của hàm/constructor và so khớp chúng với các đăng ký trong container. Chế độ CLASSIC
có một chi phí khởi tạo cao hơn một chút vì nó phải phân tích cú pháp của hàm/lớp để xác định các phụ thuộc tại thời điểm đăng ký, tuy nhiên việc giải quyết chúng sẽ nhanh hơn nhiều so với việc sử dụng PROXY
. Không sử dụng _CLASSIC_
nếu bạn tối giản mã code của mình! Chúng tôi khuyến nghị sử dụng CLASSIC
trên Node và PROXY
trong môi trường cần tối giản mã.
class UserService {
constructor(emailService, logger) {
this.emailService = emailService
this.logger = logger
}
}
Ngoài ra, nếu lớp có một lớp cơ sở nhưng không khai báo một constructor riêng, Awilix đơn giản là gọi constructor cơ sở với bất kỳ phụ thuộc nào mà nó cần.
class Car {
constructor(engine) {
this.engine = engine
}
}
class Porsche extends Car {
vroom() {
console.log(this.engine) // whatever "engine" is
}
}
Chế độ tiêm có thể được thiết lập theo từng container và từng resolver. Chế độ cụ thể nhất sẽ chiến thắng.
Lưu ý: Cá nhân tôi không hiểu tại sao bạn muốn có các chế độ tiêm khác nhau trong dự án, nhưng nếu có nhu cầu, Awilix hỗ trợ.
Toàn bộ container :
const { createContainer, InjectionMode } = require('awilix')
const container = createContainer({ injectionMode: InjectionMode.CLASSIC })
Cho từng resolver :
const container = createContainer()
container.register({
logger: asClass(Logger).classic(),
// or..
emailService: asFunction(makeEmailService).proxy()
// or..
notificationService: asClass(NotificationService).setInjectionMode(InjectionMode.CLASSIC)
})
// or..
container.register({
logger: asClass(Logger, { injectionMode: InjectionMode.CLASSIC })
})
Cho việc tải các module tự động :
const container = createContainer()
container.loadModules(['services/**/*.js', 'repositories/**/*.js'], {
resolverOptions: {
injectionMode: InjectionMode.CLASSIC
}
})
Chọn cái phù hợp với phong cách của bạn.
PROXY
kỹ thuật cho phép bạn trì hoãn việc kéo các phụ thuộc (để hỗ trợ phụ thuộc vòng tròn), nhưng không được khuyến nghị.CLASSIC
cảm giác giống hơn với DI mà bạn đã sử dụng ở các ngôn ngữ khác.PROXY
mô tả rõ hơn và tạo ra các bài kiểm tra có thể đọc được hơn; khi kiểm tra đơn vị các lớp/hàm của bạn mà không sử dụng Awilix, bạn không cần phải lo lắng về thứ tự tham số như bạn làm vớiCLASSIC
.- Về hiệu suất,
CLASSIC
một chút nhanh hơn vì nó chỉ đọc phụ thuộc từ constructor/hàm một lần (khi gọiasClass
/asFunction
), trong khi truy cập phụ thuộc trên Proxy có thể gây ra một chút chi phí thêm cho mỗi lần giải quyết. **CLASSIC**
sẽ không hoạt động khi mã code của bạn bị tối giản hóa! Nó đọc chữ ký của hàm để xác định phụ thuộc cần tiêm. Các công cụ tối giản thường sẽ làm thay đổi tên này.
Dưới đây là một ví dụ giới thiệu về các điểm có thể kiểm thử được đề cập.
// CLASSIC
function database(connectionString, timeout, logger) {
// ...
}
// Shorter, but less readable, order-sensitive
const db = database('localhost:1337;user=123...', 4000, new LoggerMock())
// PROXY
function database({ connectionString, timeout, logger }) {
// ...
}
// Longer, more readable, order does not matter
const db = database({
logger: new LoggerMock(),
timeout: 4000,
connectionString: 'localhost:1337;user=123...'
})
Tải các module tự động
Khi bạn đã tạo container của mình, việc đăng ký hàng trăm lớp có thể trở nên nhàm chán. Bạn có thể tự động hóa điều này bằng cách sử dụng loadModules
.
Quan trọng : tải các module tự động xem xét đến export mặc định của tệp, có thể là:
module.exports = ...
module.exports.default = ...
export default ...
Để tải một export không phải mặc định, đặt thuộc tính [RESOLVER]
trên nó:
const { RESOLVER } = require('awilix')
export class ServiceClass {}
ServiceClass[RESOLVER] = {}
Hoặc thậm chí ngắn gọn hơn sử dụng TypeScript:
// TypeScript
import { RESOLVER } from 'awilix'
export class ServiceClass {
static [RESOLVER] = {}
}
Lưu ý rằng có thể đăng ký nhiều dịch vụ trên mỗi tệp, tức là có thể có một tệp với export mặc định và export theo tên và tất cả chúng đều được tải. Các export theo tên thực sự cần yêu cầu token RESOLVER
được nhận dạng.
Hãy tưởng tượng cấu trúc ứng dụng này:
app
services
UserService.js
– xuất mộtclass UserService {}
theo chuẩn ES6emailService.js
– xuất một hàm factoryfunction makeEmailService() {}
repositories
UserRepository.js
– xuất mộtclass UserRepository {}
theo chuẩn ES6index.js
– tập lệnh chính của chúng ta
Trong tập lệnh chính của chúng ta, chúng ta sẽ thực hiện như sau:
const awilix = require('awilix')
const container = awilix.createContainer()
// Load our modules!
container.loadModules([
// Globs!
[
// To have different resolverOptions for specific modules.
'models/**/*.js',
{
register: awilix.asValue,
lifetime: Lifetime.SINGLETON
}
],
'services/**/*.js',
'repositories/**/*.js'
], {
// We want to register `UserService` as `userService` -
// by default loaded modules are registered with the
// name of the file (minus the extension)
formatName: 'camelCase',
// Apply resolver options to all modules.
resolverOptions: {
// We can give these auto-loaded modules
// the deal of a lifetime! (see what I did there?)
// By default it's `TRANSIENT`.
lifetime: Lifetime.SINGLETON,
// We can tell Awilix what to register everything as,
// instead of guessing. If omitted, will inspect the
// module to determine what to register as.
register: awilix.asClass
}
)
// We are now ready! We now have a userService, userRepository and emailService!
container.resolve('userService').getUser(1)
Quan trọng : Tải các module tự động dựa trên thư viện glob
và do đó không hoạt động với các công cụ đóng gói như Webpack, Rollup và Browserify.
Tiêm vào từng module cục bộ
Một số module có thể cần một số giá trị cấu hình bổ sung ngoài các phụ thuộc.
Ví dụ, userRepository
của chúng ta muốn một module db
được đăng ký trong container, nhưng nó cũng muốn một giá trị timeout
. timeout
là một tên rất chung và chúng ta không muốn đăng ký nó như một giá trị có thể được truy cập bởi tất cả các module trong container (có thể các module khác có thời gian chờ khác nhau?)
export default function userRepository({ db, timeout }) {
return {
find() {
return Promise.race([
db.query('select * from users'),
Promise.delay(timeout).then(() =>
Promise.reject(new Error('Timed out'))
)
])
}
}
}
Awilix 2.5 đã thêm tính năng tiêm vào từng module cục bộ. Đoạn mã sau chứa tất cả các cách có thể để thiết lập điều này.
import { createContainer, Lifetime, asFunction } from 'awilix'
import createUserRepository from './repositories/userRepository'
const container = createContainer()
// Using the fluid variant:
.register({
userRepository: asFunction(createUserRepository)
// Provide an injection function that returns an object with locals.
// The function is called once per resolve of the registration
// it is attached to.
.inject(() => ({ timeout: 2000 }))
})
// Shorthand variants
.register({
userRepository: asFunction(createUserRepository, {
injector: () => ({ timeout: 2000 })
})
})
// Stringly-typed shorthand
.register(
'userRepository',
asFunction(createUserRepository, {
injector: () => ({ timeout: 2000 })
})
)
// with `loadModules`
.loadModules([['repositories/*.js', { injector: () => ({ timeout: 2000 }) }]])
Bây giờ, timeout
chỉ có sẵn cho các module nó được cấu hình cho.
QUAN TRỌNG : cách hoạt động của điều này là bằng cách bọc cradle
trong một proxy khác cung cấp các giá trị trả về từ hàm inject
. Điều này có nghĩa nếu bạn chuyển đối tượng cradle đã tiêm vào, bất cứ thứ gì có quyền truy cập vào nó đều có thể truy cập vào các tiêm vào cục bộ.
Nạp tùy chọn resolver vào dòng
Awilix 2.8 đã thêm hỗ trợ cho tùy chọn resolver nạp vào dòng. Đây là tốt nhất được giải thích qua một ví dụ.
services/awesome-service.js :
import { RESOLVER, Lifetime, InjectionMode } from 'awilix'
export default class AwesomeService {
constructor(awesomeRepository) {
this.awesomeRepository = awesomeRepository
}
}
// `RESOLVER` is a Symbol.
AwesomeService[RESOLVER] = {
lifetime: Lifetime.SCOPED,
injectionMode: InjectionMode.CLASSIC
}
index.js :
import { createContainer, asClass } from 'awilix'
import AwesomeService from './services/awesome-service.js'
const container = createContainer().register({
awesomeService: asClass(AwesomeService)
})
console.log(container.registrations.awesomeService.lifetime) // 'SCOPED'
console.log(container.registrations.awesomeService.injectionMode) // ‘CLASSIC’
Ngoài ra, nếu chúng ta thêm một trường name
và sử dụng loadModules
, name
sẽ được sử dụng để đăng ký (bỏ qua formatName
nếu được cung cấp).
// `RESOLVER` is a Symbol.
AwesomeService[RESOLVER] = {
+ name: 'superService',
lifetime: Lifetime.SCOPED,
injectionMode: InjectionMode.CLASSIC
}
const container = createContainer().loadModules(['services/*.js'])
console.log(container.registrations.superService.lifetime) // 'SCOPED'
console.log(container.registrations.superService.injectionMode) // ‘CLASSIC’
Quan trọng : trường name
chỉ được sử dụng bởi loadModules
.
Loại bỏ
Kể từ Awilix v3.0, bạn có thể gọi container.dispose()
để xóa bộ nhớ cache resolver và gọi bất kỳ disposers đã đăng ký nào. Điều này rất hữu ích để loại bỏ các tài nguyên như connection pool một cách đúng đắn, đặc biệt khi sử dụng chế độ theo dõi trong các bài kiểm tra tích hợp của bạn.
Ví dụ, thư viện kết nối cơ sở dữ liệu thường có một số loại hàm destroy
hoặc end
để đóng kết nối. Bạn có thể yêu cầu Awilix gọi chúng cho bạn khi gọi container.dispose()
.
Quan trọng: container được loại bỏ sẽ không loại bỏ các scope của nó. Nó chỉ loại bỏ các giá trị trong bộ nhớ cache của nó.
import { createContainer, asClass } from 'awilix'
import pg from 'pg'
class TodoStore {
constructor({ pool }) {
this.pool = pool
}
async getTodos() {
const result = await this.pool.query('SELECT * FROM todos')
return result.rows
}
}
function configureContainer() {
return container.register({
todoStore: asClass(TodoStore),
pool: asFunction(() => new pg.Pool())
// Disposables must be either `scoped` or `singleton`.
.singleton()
// This is called when the pool is going to be disposed.
// If it returns a Promise, it will be awaited by `dispose`.
.disposer(pool => pool.end())
})
}
const container = configureContainer()
const todoStore = container.resolve('todoStore')
// Later...
container.dispose().then(() => {
console.log('Container has been disposed!')
})
Một trường hợp sử dụng hoàn hảo cho điều này là khi sử dụng Awilix với một máy chủ HTTP.
import express from 'express'
import http from 'http'
function createServer() {
const app = express()
const container = configureContainer()
app.get('/todos', async (req, res) => {
const store = container.resolve('todoStore')
const todos = await store.getTodos()
res.status(200).json(todos)
})
const server = http.createServer(app)
// Dispose container when the server closes.
server.on('close', () => container.dispose())
return server
}
test('server does server things', async () => {
const server = createServer()
server.listen(3000)
/// .. run your tests..
// Disposes everything, and your process no
// longer hangs on to zombie connections!
server.close()
})
API
Đối tượng awilix
Khi nhập awilix
, bạn sẽ có API cấp cao như sau:
createContainer
listModules
AwilixResolutionError
asValue
asFunction
asClass
aliasTo
Lifetime
– được tài liệu ở trên.InjectionMode
– được tài liệu ở trên.
Những điều này được tài liệu ở dưới đây.
Tùy chọn Resolver
Mỗi khi bạn thấy một nơi mà bạn có thể truyền vào tùy chọn resolver , bạn có thể truyền vào một đối tượng với các thuộc tính sau:
lifetime
: Một chuỗiawilix.Lifetime.*
, nhưawilix.Lifetime.SCOPED
injectionMode
: Một chuỗiawilix.InjectionMode.*
, nhưawilix.InjectionMode.CLASSIC
injector
: Một hàm injector – xem Per-module local injectionsregister
: Chỉ được sử dụng trongloadModules
, xác định cách đăng ký module đã tải một cách rõ ràng
Ví dụ về cách sử dụng:
container.register({
stuff: asClass(MyClass, { injectionMode: InjectionMode.CLASSIC })
})
container.loadModules([['some/path/to/*.js', { register: asClass }]], {
resolverOptions: {
lifetime: Lifetime.SCOPED
}
})
createContainer()
Tạo một container Awilix mới. Phần container đã được tài liệu ở phía dưới.
Tham số:
options
: Đối tượng tùy chọn. Tùy chọn.options.require
: Hàm được sử dụng khi yêu cầu các module. Mặc định làrequire
. Hữu ích khi sử dụng một cái gì đó giống như require-stack. Tùy chọn.options.injectionMode
: Xác định phương pháp để giải quyết các phụ thuộc. Các chế độ hợp lệ là:PROXY
: Sử dụng cơ chế giải quyết phụ thuộc mặc định củaawilix
(tức là tiêm cradle vào hàm hoặc lớp). Đây là chế độ tiêm mặc định.CLASSIC
: Sử dụng cơ chế giải quyết phụ thuộc có tên. Phụ thuộc phải được đặt tên chính xác như chúng đã được đăng ký. Ví dụ, một phụ thuộc đăng ký dưới tênrepository
không thể được tham chiếu trong constructor lớp dưới dạngrepo
.
asFunction()
Sử dụng với container.register({ userService: asFunction(makeUserService) })
. Cho biết Awilix gọi hàm mà không có bất kỳ ngữ cảnh nào.
Resolver trả về có API chuỗi (fluid) sau:
asFunction(fn).setLifetime(lifetime: string)
: đặt tuổi thọ của đăng ký thành giá trị đã cho.asFunction(fn).transient()
: tương tự nhưasFunction(fn).setLifetime(Lifetime.TRANSIENT)
.asFunction(fn).scoped()
: tương tự nhưasFunction(fn).setLifetime(Lifetime.SCOPED)
.asFunction(fn).singleton()
: tương tự nhưasFunction(fn).setLifetime(Lifetime.SINGLETON)
.asFunction(fn).inject(injector: Function)
: Cho phép bạn cung cấp các phụ thuộc cục bộ chỉ có sẵn cho module này.injector
nhận container như đối số đầu tiên và duy nhất và nên trả về một đối tượng.
asClass()
Sử dụng với container.register({ userService: asClass(UserService) })
. Cho biết Awilix khởi tạo hàm đã cho như một lớp sử dụng new
.
Resolver trả về có cùng API chuỗi như asFunction.
asValue()
Sử dụng với container.register({ dbHost: asValue('localhost') })
. Cho biết Awilix cung cấp giá trị đã cho nguyên vẹn.
aliasTo()
Giải quyết phụ thuộc được chỉ định.
container.register({
val: asValue(123),
aliasVal: aliasTo('val')
})
container.resolve('aliasVal') === container.resolve('val')
listModules()
Trả về một mảng các cặp {name, path}
, trong đó name
là tên module và path
là đường dẫn đầy đủ thực sự đến module.
Điều này được sử dụng bên trong, nhưng cũng hữu ích cho những điều khác, ví dụ như tải động động một thư mục api
.
Tham số:
globPatterns
: một chuỗi mẫu glob hoặc một mảng của chúng.opts.cwd
: Thư mục làm việc hiện tại được truyền choglob
. Mặc định làprocess.cwd()
.- trả về : một mảng các đối tượng với:
name
: Tên module – ví dụdb
path
: Đường dẫn đến module liên quan đếnoptions.cwd
– ví dụlib/db.js
Ví dụ:
const listModules = require('awilix').listModules
const result = listModules(['services/*.js'])
console.log(result)
// << [{ name: 'someService', path: 'path/to/services/someService.js' }]
Quan trọng : listModules
dựa vào glob
và do đó không được hỗ trợ với các công cụ đóng gói như Webpack, Rollup và Browserify.
AwilixResolutionError
Đây là một lỗi đặc biệt được ném khi Awilix không thể giải quyết tất cả các phụ thuộc (do phụ thuộc thiếu hoặc phụ thuộc vòng tròn). Bạn có thể bắt lỗi này và sử dụng err instanceof AwilixResolutionError
nếu bạn muốn. Nó sẽ cho bạn biết những phụ thuộc nào mà nó không thể tìm thấy hoặc những phụ thuộc nào gây ra vòng tròn.
Đối tượng AwilixContainer
Container được trả về từ createContainer
có một số phương thức và thuộc tính.
container.cradle
Đây đó! Đây là nơi ma thuật xảy ra! cradle
là một proxy, và tất cả các getter sẽ kích hoạt một container.resolve
. Thực tế, cradle
được truyền vào constructor/hàm factory, đó là cách mọi thứ được kết nối với nhau.
container.registrations
Một getter chỉ đọc trả về các đăng ký nội bộ. Khi được gọi trên một scope , nó sẽ hiển thị các đăng ký cho scope cha của nó, và scope cha của scope cha, và cứ thế.
Không thực sự hữu ích cho việc sử dụng công khai.
container.cache
Một Map<string, CacheEntry>
được sử dụng bên trong để lưu trữ các giá trị được giải quyết. Không dành cho việc sử dụng công khai, nhưng nếu bạn thấy nó hữu ích, hãy sử dụng nhưng cẩn thận.
Mỗi scope có bộ nhớ cache riêng của nó, và kiểm tra bộ nhớ cache của các scope cha của nó.
let counter = 1
container.register({
count: asFunction(() => counter++).singleton()
})
container.cradle.count === 1
container.cradle.count === 1
container.cache.delete('count')
container.cradle.count === 2
container.options
Các tùy chọn được truyền vào createContainer
được lưu trữ ở đây.
const container = createContainer({
injectionMode: InjectionMode.CLASSIC
})
console.log(container.options.injectionMode) // ‘CLASSIC’
container.resolve()
Giải quyết đăng ký với tên đã cho. Được sử dụng bởi cradle.
Chữ ký
-
resolve<T>(name: string, [resolveOpts: ResolveOptions]): T
container.register({
leet: asFunction(() => 1337)
})container.resolve(‘leet’) === 1337
container.cradle.leet === 1337
resolveOpts
tùy chọn có các trường sau:
allowUnregistered
: nếutrue
, trả vềundefined
khi phụ thuộc không tồn tại, thay vì ném lỗi.
container.register()
Chữ ký
register(name: string, resolver: Resolver): AwilixContainer
register(nameAndResolverPair: NameAndResolverPair): AwilixContainer
Awilix cần biết cách giải quyết các module, vì vậy hãy trích ra các hàm resolver:
const awilix = require('awilix')
const { asValue, asFunction, asClass } = awilix
asValue
: Giải quyết giá trị đã cho nguyên vẹn.asFunction
: Giải quyết bằng cách gọi hàm với cradle của container như đối số đầu tiên và duy nhất.asClass
: Tương tựasFunction
nhưng sử dụngnew
.
Bây giờ chúng ta cần sử dụng chúng. Có nhiều cú pháp cho hàm register
, hãy chọn cú pháp mà bạn thích nhất – hoặc sử dụng tất cả chúng, tôi thật sự không quan tâm! 😎
Cả hai kiểu hỗ trợ chuỗi kết nối!**register**
trả về container!
// name-resolver
container.register('connectionString', asValue('localhost:1433;user=...'))
container.register('mailService', asFunction(makeMailService))
container.register('context', asClass(SessionContext))
// object
container.register({
connectionString: asValue('localhost:1433;user=...'),
mailService: asFunction(makeMailService, { lifetime: Lifetime.SINGLETON }),
context: asClass(SessionContext, { lifetime: Lifetime.SCOPED })
})
// `asClass` and `asFunction` also supports a fluid syntax.
// This...
container.register(
'mailService',
asFunction(makeMailService).setLifetime(Lifetime.SINGLETON)
)
// .. is the same as this:
container.register('context', asClass(SessionContext).singleton())
// .. and here are the other `Lifetime` variants as fluid functions.
container.register('context', asClass(SessionContext).transient())
container.register('context', asClass(SessionContext).scoped())
Cú pháp đối tượng, cú pháp key-value và chuỗi kết nối đều hợp lệ cho tất cả các cuộc gọi**register**
!
container.hasRegistration()
container.hasRegistration(name: string | symbol): boolean
Xác định xem container có đăng ký với tên đã cho hay không. Cũng kiểm tra các container cha.
container.loadModules()
Với một mảng các biểu thức glob, đăng ký các module và trả về container.
💡 Khi sử dụng
opts.esModules
, mộtPromise
được trả về do sử dụngimport()
bất đồng bộ.
Awilix sẽ sử dụng require
trên các module đã tải và đăng ký hàm hoặc lớp được xuất theo mặc định dưới tên của tệp.
Điều này sử dụng một phương pháp triển khai để xác định xem đó có phải là hàm khởi tạo hay không (**function Database() {...}**
); nếu tên hàm bắt đầu bằng một chữ hoa, nó sẽ được**new**
!
Tham số:
globPatterns
: Mảng các biểu thức glob phù hợp với các tệp JS để tải.opts.cwd
:cwd
được truyền choglob
. Mặc định làprocess.cwd()
.opts.formatName
: Có thể là'camelCase'
, hoặc một hàm nhận tên hiện tại như tham số đầu tiên và trả về tên mới. Giá trị mặc định là truyền tên qua như vậy. Tham số thứ hai là một mô tả đầy đủ về module.opts.resolverOptions
: Mộtobject
được truyền vào các resolver. Được sử dụng để cấu hình tuổi thọ, chế độ tiêm và nhiều hơn nữa cho các module đã tải.opts.esModules
: Tải các module bằng cách sử dụng native ES modules của Node. Điều này làm cho**container.loadModules**
trở thành bất đồng bộ và do đó sẽ trả về một**Promise**
! Điều này chỉ được hỗ trợ trên Node 14.0+ và chỉ nên được sử dụng nếu bạn đang sử dụng Native Node ES modules
Ví dụ:
// index.js
container.loadModules(['services/*.js', 'repositories/*.js', 'db/db.js'])
container.cradle.userService.getUser(123)
// to configure lifetime for all modules loaded..
container.loadModules([
'services/*.js',
'repositories/*.js',
'db/db.js'
], {
resolverOptions: {
lifetime: Lifetime.SINGLETON
}
})
container.cradle.userService.getUser(123)
// to configure lifetime for specific globs..
container.loadModules([
['services/*.js', Lifetime.SCOPED], // all services will have scoped lifetime
'repositories/*.js',
'db/db.js'
], {
resolverOptions: {
lifetime: Lifetime.SINGLETON // db and repositories will be singleton
}
)
container.cradle.userService.getUser(123)
// to use camelCase for modules where filenames are not camelCase
container.loadModules(['repositories/account-repository.js', 'db/db.js'], {
formatName: 'camelCase'
})
container.cradle.accountRepository.getUser(123)
// to customize how modules are named in the container (and for injection)
container.loadModules(['repository/account.js', 'service/email.js'], {
// This formats the module name so `repository/account.js` becomes `accountRepository`
formatName: (name, descriptor) => {
const splat = descriptor.path.split('/')
const namespace = splat[splat.length - 2] // `repository` or `service`
const upperNamespace =
namespace.charAt(0).toUpperCase() + namespace.substring(1)
return name + upperNamespace
}
})
container.cradle.accountRepository.getUser(123)
container.cradle.emailService.sendEmail('test@test.com', 'waddup')
Cú pháp ['glob', Lifetime.SCOPED]
là một cách viết tắt để truyền các tùy chọn của bộ giải quyết như sau: ['glob', { lifetime: Lifetime.SCOPED }]
Quan trọng : loadModules
phụ thuộc vào fast-glob
và do đó không được hỗ trợ trong các công cụ đóng gói module như Webpack, Rollup, esbuild và Browserify.
container.createScope()
Tạo một phạm vi mới. Tất cả các đăng ký với Lifetime.SCOPED
sẽ được lưu vào bộ nhớ cache bên trong một phạm vi. Một phạm vi về cơ bản là một container “con”.
-
trả về
AwilixContainer
// Increments the counter every time it is resolved.
let counter = 1
container.register({
counterValue: asFunction(() => counter++).scoped()
})
const scope1 = container.createScope()
const scope2 = container.createScope()const scope1Child = scope1.createScope()
scope1.cradle.counterValue === 1
scope1.cradle.counterValue === 1
scope2.cradle.counterValue === 2
scope2.cradle.counterValue === 2scope1Child.cradle.counterValue === 3
Một Phạm vi duy trì cache riêng của nó cho các đăng ký Lifetime.SCOPED
, có nghĩa là nó không sử dụng cache của phạm vi cha cho các đăng ký phạm vi.
let counter = 1
container.register({
counterValue: asFunction(() => counter++).scoped()
})
const scope1 = container.createScope()
const scope2 = container.createScope()
// The root container is also a scope.
container.cradle.counterValue === 1
container.cradle.counterValue === 1
// This scope resolves and caches it's own.
scope1.cradle.counterValue === 2
scope1.cradle.counterValue === 2
// This scope resolves and caches it's own.
scope2.cradle.counterValue === 3
scope2.cradle.counterValue === 3
Một phạm vi cũng có thể đăng ký thêm các thứ khác – chúng chỉ có sẵn trong phạm vi đó và các phạm vi con của nó.
// Register a transient function
// that returns the value of the scope-provided dependency.
// For this example we could also use scoped lifetime.
container.register({
scopedValue: asFunction(cradle => 'Hello ' + cradle.someValue)
})
// Create a scope and register a value.
const scope = container.createScope()
scope.register({
someValue: asValue('scope')
})
scope.cradle.scopedValue === 'Hello scope'
container.cradle.someValue
// throws AwilixResolutionException
// because the root container does not know
// of the resolver.
Những thứ đăng ký trong phạm vi được ưu tiên hơn so với phạm vi cha của nó.
// It does not matter when the scope is created,
// it will still have anything that is registered
// in it's parent.
const scope = container.createScope()
container.register({
value: asValue('root'),
usedValue: asFunction(cradle => cradle.value)
})
scope.register({
value: asValue('scope')
})
container.cradle.usedValue === 'root'
scope.cradle.usedValue === ‘scope’
container.build()
Xây dựng một phiên bản của một lớp (hoặc một hàm) bằng cách tiêm các phụ thuộc, nhưng không đăng ký nó trong container.
Điều này về cơ bản là một lối tắt cho asClass(MyClass).resolve(container)
.
Đối số:
targetOrResolver
: Một lớp, hàm hoặc bộ giải quyết (ví dụ:asClass(..)
,asFunction(..)
)opts
: Các tùy chọn của bộ giải quyết.
Trả về một phiên bản của bất cứ thứ gì được truyền vào, hoặc kết quả của việc gọi bộ giải quyết.
Quan trọng : nếu bạn thường xuyên thực hiện điều này cho cùng một lớp/hàm, hãy xem xét sử dụng phương pháp rõ ràng và lưu bộ giải quyết, đặc biệt là nếu bạn đang sử dụng việc giải quyết cổ điển vì nó quét lớp constructor/hàm khi gọi asClass(Class)
/ asFunction(func)
.
// The following are equivelant..
class MyClass {
constructor({ ping }) {
this.ping = ping
}
pong() {
return this.ping
}
}
const createMyFunc = ({ ping }) => ({
pong: () => ping
})
container.register({
ping: asValue('pong')
})
// Shorthand
// This uses `utils.isClass()` to determine whether to
// use `asClass` or `asFunction`. This is fine for
// one-time resolutions.
const myClass = container.build(MyClass)
const myFunc = container.build(createMyFunc)
// Explicit
// Save the resolver if you are planning on invoking often.
// **Especially** if you're using classic resolution.
const myClassResolver = asClass(MyClass)
const myFuncResolver = asFunction(MyFunc)
const myClass = container.build(myClassResolver)
const myFunc = container.build(myFuncResolver)
container.dispose()
Trả về một Promise
mà giải quyết khi tất cả các bộ tiêu diệt của các giải quyết được lưu cache đã giải quyết. Chỉ các giá trị lưu cache sẽ được tiêu diệt, có nghĩa là chúng phải có**Lifetime**
là**SCOPED**
hoặc**SINGLETON**
, nếu không chúng sẽ không được lưu cache bởi container và do đó không thể bị tiêu diệt bởi nó.
Điều này cũng xóa bộ nhớ cache của container.
const pg = require('pg')
container.register({
pool: asFunction(() => new pg.Pool())
.disposer(pool => pool.end())
// IMPORTANT! Must be either singleton or scoped!
.singleton()
})
const pool = container.resolve('pool')
pool.query('...')
// Later..
container.dispose().then(() => {
console.log('All dependencies disposed, you can exit now. :)')
})
Universal Module (Hỗ trợ trình duyệt)
Kể từ phiên bản v3 , Awilix được cung cấp kèm theo sự hỗ trợ chính thức cho môi trường trình duyệt!
Gói bao gồm 4 phiên bản.
- CommonJS, định dạng cũ quen thuộc của Node –
lib/awilix.js
- ES Modules, để sử dụng với các công cụ đóng gói module trong Node –
lib/awilix.module.mjs
- ES Modules, để sử dụng với các công cụ đóng gói module trong trình duyệt –
lib/awilix.browser.js
- UMD, để nhúng vào thẻ script –
lib/awilix.umd.js
package.json
bao gồm các trường thích hợp cho các công cụ đóng gói như Webpack, Rollup và Browserify để chọn phiên bản đúng, vì vậy bạn không cần phải cấu hình gì thêm. 😎
Quan trọng : phiên bản cho trình duyệt không hỗ trợ loadModules
hoặc listModules
, vì chúng phụ thuộc vào các gói cụ thể cho Node.
Cũng quan trọng : do sử dụng Proxy
+ các phương thức Reflect
khác nhau, Awilix chỉ được cho là hoạt động trên:
- Chrome >= 49
- Firefox >= 18
- Edge >= 12
- Opera >= 36
- Safari >= 10
- Internet Explorer không được hỗ trợ
Đóng góp
Vui lòng xem contributing.md
Có gì trong một cái tên?
Awilix là nữ thần mặt trăng trong văn hóa Maya, và cũng là nhân vật yêu thích của tôi trong trò chơi SMITE.
Chi tiết Tải về:
Tác giả: jeffijoe
Mã nguồn: https://github.com/jeffijoe/awilix
Giấy phép: MIT license