Skip to content

Commit

Permalink
refactor(di): implement dependency injection
Browse files Browse the repository at this point in the history
  • Loading branch information
qwqcode committed Oct 9, 2024
1 parent baa6386 commit bd783dd
Show file tree
Hide file tree
Showing 35 changed files with 530 additions and 287 deletions.
29 changes: 15 additions & 14 deletions ui/artalk/src/artalk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ import './style/main.scss'
import type { EventHandler } from './lib/event-manager'
import Context from './context'
import { handelCustomConf, convertApiOptions } from './config'
import Services from './service'
import * as Stat from './plugins/stat'
import { Api } from './api'
import type { TInjectedServices } from './service'
import { GlobalPlugins, PluginOptions, load } from './load'
import { GlobalPlugins, PluginOptions, mount } from './load'
import { DataManager } from './data'
import type { ArtalkConfigPartial, EventPayloadMap, ArtalkPlugin, ContextApi } from '@/types'

/**
Expand All @@ -18,23 +17,25 @@ import type { ArtalkConfigPartial, EventPayloadMap, ArtalkPlugin, ContextApi } f
export default class Artalk {
public ctx!: ContextApi

constructor(conf: ArtalkConfigPartial) {
constructor(_conf: ArtalkConfigPartial) {
// Init Config
const handledConf = handelCustomConf(conf, true)
const conf = handelCustomConf(_conf, true)

// Init Context
this.ctx = new Context(handledConf)
// Init Root Element
const $root = conf.el as HTMLElement
$root.classList.add('artalk')
$root.innerHTML = ''
conf.darkMode && $root.classList.add('atk-dark-mode')

// Init Services
Object.entries(Services).forEach(([name, initService]) => {
const obj = initService(this.ctx)
obj && this.ctx.inject(name as keyof TInjectedServices, obj) // auto inject deps to ctx
})
// Init Context
const ctx = this.ctx = new Context(conf, $root)
ctx.on('mounted', () => (ctx.mounted = true))

// Init plugins and mount
if (import.meta.env.DEV && import.meta.env.VITEST) {
global.devLoadArtalk = () => load(this.ctx)
global.devLoadArtalk = () => mount(ctx)
} else {
load(this.ctx)
mount(ctx)
}
}

Expand Down
11 changes: 6 additions & 5 deletions ui/artalk/src/components/checker/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import AdminChecker from './admin'
import type { Api } from '@/api'
import Dialog from '@/components/dialog'
import $t from '@/i18n'
import type { ContextApi } from '@/types'
import type User from '@/lib/user'
import * as Utils from '@/lib/utils'
import type User from '@/lib/user'
import type { LayerManager } from '@/layer'

export interface CheckerCaptchaPayload extends CheckerPayload {
img_data?: string
Expand All @@ -19,8 +19,9 @@ export interface CheckerPayload {
}

export interface CheckerLauncherOptions {
getCtx: () => ContextApi
getApi: () => Api
getLayers: () => LayerManager
getUser: () => User
getCaptchaIframeURL: () => string
onReload: () => void
}
Expand Down Expand Up @@ -61,7 +62,7 @@ export default class CheckerLauncher {

public fire(checker: Checker, payload: CheckerPayload, postFire?: (c: CheckerCtx) => void) {
// 显示层
const layer = this.opts.getCtx().get('layerManager').create(`checker-${new Date().getTime()}`)
const layer = this.opts.getLayers().create(`checker-${new Date().getTime()}`)
layer.show()

const close = () => {
Expand All @@ -77,7 +78,7 @@ export default class CheckerLauncher {
},
get: (key) => checkerStore[key],
getOpts: () => this.opts,
getUser: () => this.opts.getCtx().get('user'),
getUser: () => this.opts.getUser(),
getApi: () => this.opts.getApi(),
hideInteractInput: () => {
hideInteractInput = true
Expand Down
121 changes: 57 additions & 64 deletions ui/artalk/src/context.ts
Original file line number Diff line number Diff line change
@@ -1,160 +1,153 @@
/* eslint-disable @typescript-eslint/no-unsafe-declaration-merging */
import type { TInjectedServices } from './service'
import { Api, ApiHandlers } from './api'

import * as marked from './lib/marked'
import { mergeDeep } from './lib/merge-deep'
import { CheckerCaptchaPayload, CheckerPayload } from './components/checker'
import type { CheckerCaptchaPayload, CheckerPayload } from './components/checker'

import { DataManager } from './data'
import * as I18n from './i18n'

import EventManager from './lib/event-manager'
import { convertApiOptions, createNewApiHandlers, handelCustomConf } from './config'
import { handelCustomConf } from './config'
import { watchConf } from './lib/watch-conf'

import type {
ArtalkConfig,
ArtalkConfigPartial,
CommentData,
ListFetchParams,
ContextApi,
EventPayloadMap,
SidebarShowPayload,
import { createInjectionContainer, InjectionContainerMethods, Constructor } from './lib/injection'

import {
type ArtalkConfig,
type ArtalkConfigPartial,
type CommentData,
type ListFetchParams,
type ContextApi,
type EventPayloadMap,
type SidebarShowPayload,
type DataManagerApi,
} from '@/types'

// Auto dependency injection
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface Context extends TInjectedServices {}

/**
* Artalk Context
*/
class Context implements ContextApi {
/* 运行参数 */
conf: ArtalkConfig
data: DataManager
$root: HTMLElement

/* Event Manager */
private events = new EventManager<EventPayloadMap>()
private mounted = false
conf!: ArtalkConfig
$root!: HTMLElement

constructor(conf: ArtalkConfig) {
constructor(conf: ArtalkConfig, $root: HTMLElement) {
this.conf = conf
this.$root = $root
}

this.$root = conf.el as HTMLElement
this.$root.classList.add('artalk')
this.$root.innerHTML = ''
conf.darkMode && this.$root.classList.add('atk-dark-mode')
/* Event Manager */
private deps: InjectionContainerMethods = createInjectionContainer()

this.data = new DataManager(this.events)
mounted = false

this.on('mounted', () => {
this.mounted = true
})
getEvents() {
return this.deps.inject('events')
}

inject(depName: string, obj: any) {
this[depName] = obj
provide<
K extends keyof InjectDeps,
T extends InjectDeps[K] = any,
D extends readonly (keyof InjectDeps)[] = [],
>(key: K, impl: Constructor<T, D>, deps?: D): void {
this.deps.provide(key, impl, deps)
}

get(depName: string) {
return this[depName]
inject<T = undefined, K extends keyof InjectDeps = any>(
key: K,
): T extends undefined ? InjectDeps[K] : T {
return this.deps.inject(key)
}

get = this.inject

getApi() {
return new Api(convertApiOptions(this.conf, this))
return this.inject('api')
}

private apiHandlers = <ApiHandlers | null>null
getApiHandlers() {
if (!this.apiHandlers) this.apiHandlers = createNewApiHandlers(this)
return this.apiHandlers
return this.inject('apiHandlers')
}

getData() {
return this.data
return this.inject('data')
}

replyComment(commentData: CommentData, $comment: HTMLElement): void {
this.editor.setReply(commentData, $comment)
this.inject('editor').setReply(commentData, $comment)
}

editComment(commentData: CommentData, $comment: HTMLElement): void {
this.editor.setEditComment(commentData, $comment)
this.inject('editor').setEditComment(commentData, $comment)
}

fetch(params: Partial<ListFetchParams>): void {
this.data.fetchComments(params)
this.getData().fetchComments(params)
}

reload(): void {
this.data.fetchComments({ offset: 0 })
this.getData().fetchComments({ offset: 0 })
}

/* List */
listGotoFirst(): void {
this.events.trigger('list-goto-first')
this.getEvents().trigger('list-goto-first')
}

getCommentNodes() {
return this.list.getCommentNodes()
return this.inject('list').getCommentNodes()
}

getComments() {
return this.data.getComments()
return this.getData().getComments()
}

getCommentList = this.getCommentNodes
getCommentDataList = this.getComments

/* Editor */
editorShowLoading(): void {
this.editor.showLoading()
this.inject('editor').showLoading()
}

editorHideLoading(): void {
this.editor.hideLoading()
this.inject('editor').hideLoading()
}

editorShowNotify(msg, type): void {
this.editor.showNotify(msg, type)
this.inject('editor').showNotify(msg, type)
}

editorResetState(): void {
this.editor.resetState()
this.inject('editor').resetState()
}

/* Sidebar */
showSidebar(payload?: SidebarShowPayload): void {
this.sidebarLayer.show(payload)
this.inject('sidebar').show(payload)
}

hideSidebar(): void {
this.sidebarLayer.hide()
this.inject('sidebar').hide()
}

/* Checker */
checkAdmin(payload: CheckerPayload): Promise<void> {
return this.checkerLauncher.checkAdmin(payload)
return this.inject('checkers').checkAdmin(payload)
}

checkCaptcha(payload: CheckerCaptchaPayload): Promise<void> {
return this.checkerLauncher.checkCaptcha(payload)
return this.inject('checkers').checkCaptcha(payload)
}

/* Events */
on(name: any, handler: any) {
this.events.on(name, handler)
this.getEvents().on(name, handler)
}

off(name: any, handler: any) {
this.events.off(name, handler)
this.getEvents().off(name, handler)
}

trigger(name: any, payload?: any) {
this.events.trigger(name, payload)
this.getEvents().trigger(name, payload)
}

/* i18n */
Expand All @@ -168,7 +161,7 @@ class Context implements ContextApi {

updateConf(nConf: ArtalkConfigPartial): void {
this.conf = mergeDeep(this.conf, handelCustomConf(nConf, false))
this.mounted && this.events.trigger('updated', this.conf)
this.mounted && this.getEvents().trigger('updated', this.conf)
}

getConf(): ArtalkConfig {
Expand Down
16 changes: 6 additions & 10 deletions ui/artalk/src/layer/layer-manager.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import { getScrollbarHelper } from './scrollbar-helper'
import { LayerWrap } from './wrap'
import type { ContextApi } from '@/types'

export class LayerManager {
private wrap: LayerWrap

constructor(ctx: ContextApi) {
this.wrap = new LayerWrap()
document.body.appendChild(this.wrap.getWrap())

ctx.on('unmounted', () => {
this.wrap.getWrap().remove()
})
private wrap = new LayerWrap()

constructor() {
// 记录页面原始 CSS 属性
getScrollbarHelper().init()
}
Expand All @@ -24,4 +16,8 @@ export class LayerManager {
create(name: string, el?: HTMLElement) {
return this.wrap.createItem(name, el)
}

destroy() {
this.wrap.getWrap().remove()
}
}
Loading

0 comments on commit bd783dd

Please sign in to comment.