Tìm kiếm trang web

javascript - Thư viện tiêm phụ thuộc - đổi tên các giá trị được tiêm


Tôi muốn tiêm lodash theo tên, giống như thế này:

let val = function(lodash){
   // lodash will be injected, simply by using require('lodash');
};

nhưng giả sử tôi muốn đổi tên phần nhập khẩu, tôi muốn làm điều gì đó như thế này:

let val = function({lodash:_}){

};

hoặc

let val = function(lodash as _){

};

có cách nào làm điều này với ES6/ES7/ES8 hoặc TypeScript không?

Lưu ý rằng khung DI này làm nhiều việc hơn là chỉ require('x')... nó sẽ cố gắng tiêm các giá trị khác trước, nếu không có gì khác tồn tại, thì nó sẽ cố gắng require giá trị đó.

Ngoài ra cũng lưu ý rằng các yêu cầu ở đây là khi bạn gọi val.toString() thì "lodash" sẽ được xem như tên đối số. Nhưng _ thay vì lodash sẽ được nhìn thấy khi chạy thời gian thực bên trong thân hàm. Điều này là bởi vì để chèn lodash, chúng ta gọi fn.toString() để lấy tên các đối số.

Câu trả lời #1

Cập nhật

Đây là liên kết đến gói npm di-proxy (được lấy cảm hứng từ câu trả lời này) với 100% độ bao phủ mã, và hỗ trợ ghi nhớ (memoization) để tăng hiệu suất, tương thích với Node.js >=6.0.0.

Câu trả lời cũ

Đây là một giải pháp tuyệt vời mà tôi đã nghĩ ra khi thử nghiệm với phá vỡ đối tượng và Proxy:

/* MIT License */
/* Copyright 2017 Patrick Roberts */
// dependency injection utility
function inject(callbackfn) {
  const handler = {
    get(target, name) {
      /* this is just a demo, swap these two lines for actual injection */
      // return require(name);
      return { name };
    }
  };
  const proxy = new Proxy({}, handler);

  return (...args) => callbackfn.call(this, proxy, ...args);
}

// usage

// wrap function declaration with inject()
const val = inject(function ({ lodash: _, 'socket.io': sio, jquery: $, express, fs }, other, args) {
  // already have access to lodash, no need to even require() here
  console.log(_);
  console.log(sio);
  console.log($);
  console.log(express);
  console.log(fs);
  console.log(other, args);
});

// execute wrapped function with automatic injection
val('other', 'args');
.as-console-wrapper {
  max-height: 100% !important;
}

Cách nó hoạt động

Truyền tham số vào một hàm thông qua việc phá vỡ đối tượng (object destructuring) sẽ gọi các phương thức getter cho từng thuộc tính trên đối tượng nguyên literal để xác định các giá trị khi hàm được thực thi.

Nếu đối tượng đang được giải cấu trúc được khởi tạo dưới dạng một Proxy, bạn có thể chặn mỗi lần gọi getter với một tham chiếu tới tên thuộc tính đang cố gắng được giải quyết, và trả về một giá trị mà bạn chọn để giải quyết nó. Trong trường hợp này, việc giải quyết nên là require(name), điều này chèn module chỉ bằng cách chỉ định nó như một tên thuộc tính trong tham số đối tượng hàm.

Dưới đây là một liên kết đến bản demo nơi bạn thực sự có thể thấy nó hoạt động trong Node.js.

Thử nó trực tuyến!

Đây là mã trong bản demo đó chỉ để tham khảo, vì nó minh họa việc phân rã đối tượng ở mức độ lớn hơn:

/* MIT License */
/* Copyright 2017 Patrick Roberts */
// dependency injection utility
function inject(callbackfn) {
  const handler = {
    get(target, name) {
      return require(name);
    }
  };
  const proxy = new Proxy({}, handler);

  return (...args) => callbackfn.call(this, proxy, ...args);
}

// usage

// wrap function declaration with inject()
const val = inject(function ({
  fs: { readFile: fsRead, writeFile: fsWrite },
  child_process: { fork: cpF, spawn: cpS, exec: cpE },
  events: { EventEmitter }
}, other, args) {
  // already have access to modules, no need to require() here
  console.log('fs:', { fsRead, fsWrite });
  console.log('child_process:', { fork: cpF, spawn: cpS, exec: cpE });
  console.log('EventEmitter:', EventEmitter);
  console.log(other, args);
});

// execute wrapped function with automatic injection
val('other', 'args');

Như đã nêu ở trên, tôi đã phát hành một gói npm đầy đủ triển khai khái niệm này. Tôi khuyên bạn nên xem qua nếu bạn thích cú pháp này và muốn một thứ gì đó hiệu suất hơn và đã được kiểm tra hơn so với ví dụ rất cơ bản này.

Câu trả lời #2

Không có cú pháp nào trong JavaScript hỗ trợ ánh xạ như vậy. Ngay cả khi một bộ phân tích cú pháp chữ ký hàm tùy chỉnh được viết để cung cấp hành vi mong muốn cho các tham số bị phân rã như function({lodash:_}) ..., nó sẽ thất bại đối với các hàm đã được biên dịch lại, đây là một lỗi lớn. Cách đơn giản nhất để xử lý việc này là

function foo(lodash){
  const _ = lodash;
  ...
}

Và rõ ràng nó sẽ không hoạt động với các tên biến không hợp lệ như lodash.pick.

Một thực tiễn phổ biến đối với các công thức DI để làm việc này là cung cấp chú thích. Tất cả các chú thích đã mô tả có thể được kết hợp với nhau. Chúng đặc biệt được thực hiện trong Angular DI. Bộ tiêm Angular có sẵn để sử dụng độc lập (bao gồm Node) dưới dạng thư viện injection-js.

Thuộc tính chú thích

Bằng cách này, chữ ký hàm và danh sách các phụ thuộc không cần phải khớp nhau. Công thức này có thể được thấy trong AngularJS.

Thuộc tính này chứa một danh sách các token DI. Chúng có thể là tên của các phụ thuộc sẽ được tải với require hoặc một thứ gì đó khác.

// may be more convenient when it's a string
const ANNOTATION = Symbol();

...

foo[ANNOTATION] = ['lodash'];
function foo(_) {
  ...
}

bar[ANNOTATION] = ['lodash'];
function bar() {
  // doesn't need a param in signature
  const _ = arguments[0];
  ...
}

Và DI được thực hiện như

const fnArgs = require('fn-args');
const annotation = foo[ANNOTATION] || fnArgs(foo);
foo(...annotation.map(depName => require(depName));

Phong cách chú thích này có xu hướng sử dụng định nghĩa hàm, vì hoisting cho phép đặt chú thích phía trên chữ ký hàm để tiện lợi.

Chú thích mảng

Chữ ký hàm và danh sách các phụ thuộc không cần phải khớp nhau. Công thức này cũng có thể được thấy trong AngularJS.

Khi hàm được biểu diễn dưới dạng mảng, điều này có nghĩa là đó là hàm có chú thích, và các tham số của nó nên được coi là các chú thích, và tham số cuối cùng là chính hàm.

const foo = [
  'lodash',
  function foo(_) {
  ...
  }
];

...

const fn = foo[foo.length - 1];
const annotation = foo.slice(0, foo.length - 1);
foo(...annotation.map(depName => require(depName));

chú thích kiểu TypeScript

Công thức này có thể được thấy trong Angular (phiên bản 2 trở lên) và dựa vào các kiểu của TypeScript. Các kiểu có thể được trích xuất từ chữ ký constructor và sử dụng cho DI. Những điều làm điều này trở nên khả thi là đề xuất Reflect metadata và tính năng emitDecoratorMetadata riêng của TypeScript.

Các loại constructor được phát ra được lưu trữ dưới dạng siêu dữ liệu cho các lớp tương ứng và có thể được truy xuất bằng API Reflect để giải quyết các phụ thuộc. Đây là DI dựa trên lớp, vì các decorator chỉ được hỗ trợ trên các lớp, nó hoạt động tốt nhất với các container DI:

import 'core-js/es7/reflect';

abstract class Dep {}

function di(target) { /* can be noop to emit metadata */ }

@di
class Foo {
  constructor(dep: Dep) {
    ...
  }
}

...

const diContainer = { Dep: require('lodash') };
const annotations = Reflect.getMetadata('design:paramtypes', Foo);
new (Foo.bind(Foo, ...annotations.map(dep => diContainer [dep]))();

Điều này sẽ tạo ra mã JS có thể hoạt động nhưng sẽ tạo ra vấn đề về kiểu, vì đối tượng Lodash không phải là một thể hiện của lớp token Dep. Phương pháp này chủ yếu hiệu quả đối với các phụ thuộc lớp được tiêm vào các lớp.

Đối với DI không phải lớp, cần có giải pháp thay thế cho các chú thích khác.

Bài viết liên quan