Soma.js – Your Way Out of Chaotic JavaScript

http://flippinawesome.org/2013/07/15/soma-js-your-way-out-of-chaotic-javascript/

 

What is a scalable application?

A scalable application has the ability to grow in a capable manner. Applications often start small but also grow very quickly. Sometimes applications need to be entirely rewritten as they grow and become unmanageable.

Coding with scalability in mind, prevents problems down the line. This article focuses on how to write JavaScript functions that are independent, making it possible for components to be added, removed, enhanced, refactored, and interchanged with a minimal impact on other components. But also, we’ll discuss how to write code that is testable, maintainable, debuggable and intuitive.

We’ll use the soma.js framework to demonstrate how vanilla JavaScript can be written in a manner that allows maintainability and scalability.

Programming is an art form that fights back. – Chad Z. Hower

What is soma.js?

soma.js is a JavaScript framework that provides tools to create a loosely-coupled architecture broken down into smaller pieces. Success in building large-scale applications relies on smaller single-purposed parts of a larger system.

soma.js is not tied to a specific architecture pattern. The framework can be used as a model-view-controller or MV* framework, where actors called “models” represent the state of the application, and actors called “views” display information. But soma.js can also be used to manage independent modules, create standalone widgets or any other architecture.

Basically, soma.js allows developers to be kind to their future selves, by writing code which solves today’s problems in a way that is easily changed or easily adapted, in a week’s or even a year’s time.

The Coupling problem

Let’s imagine a common scenario whereby component “A” needs a component “B” in order to function, meaning it has a direct dependency to it. Code that contains components that are dependent on nearly everything else is referred to as “highly-coupled code“. The result is an application that is hard to maintain, with components that are hard to change without impacting other portions of the code.

Coupling can make a great difference in a developer’s daily work. It can mean the difference between ease of developing and a struggle;between solving a problem in five minutes versus five hours. It is a very important matter, accepted and understood by developers in many languages, though not always in the JavaScript world.

Coupling or dependency is the degree to which each program module relies on each one of the other modules

To enable a system to grow and be maintainable, its parts must have a very limited knowledge of their surroundings. The components must not be tied to each other. A structure that follows the Law of Demeter makes its entities reusable and interchangeable, allowing the system to scale up.

The solution is very simple in theory, but very hard in practice: writing components that are self-contained and encapsulated and don’t know about other components, reducing the dependency they have to each other.

A design pattern is a general, reusable solution to a commonly occurring problem.

Reducing dependencies brings another problem: how to communicate between components without them to know about each other. This is where design patterns can help. soma.js is a set of tools and design patterns solutions to build a long term architecture that is decoupled and easily testable. The tools provided by the framework are dependency injection, observer pattern, mediator pattern, facade pattern, command pattern, OOP utilities and a DOM manipulation template engine as an optional plugin.

Dependency injection

Dependency injection allows the removal of hard-coded dependencies and makes it possible to change them.

Dependency injection is an antidote for highly-coupled code. The cost required to wire objects together falls next to zero.

A common dependency injection system has a separate object (an injector, often called container), that instantiates new objects and populates them with what they need: the dependencies. Dependency injection is also a useful pattern to solve nested dependencies, as they can chain and be nested through several layers of the application.

The benefits of dependency injection are:

  • More re-usable components;
  • Improved code maintenance;
  • More testable components (mocking object);
  • More readable code (reduction of boilerplate code).

Dependency injection involves a way of thinking called the Hollywood principle “don’t call us, we’ll call you”. This is exactly how it works in soma.js. Entities ask for their dependencies using properties and/or constructor parameters.

Using dependency injection implies creating rules, so the injector knows “what” and “how” to populate the instances. These are called “mapping rules”. A rule is simply “mapping” a name (string) to an “object” (string, number, boolean, function, object, and so on). These mapping names are used to inject the corresponding objects in either a JavaScript constructor or a variable.

For more information, feel free to refer to the full documentation on soma.js dependency injection.

What is a mediator?

A mediator is a behavioral design pattern that is used to reduce dependencies. It promotes loose coupling by keeping objects from explicitly referring to each other. It is an object that represents another object (or a set of objects). The communication between objects are encapsulated within the mediators rather than the objects they represents, thus reducing coupling.

A mediator in soma.js is frequently used to represent a view object or a DOM element, but can be used to represent anything. A mediator reacts to events and uses the reference to the represented object to update its state.

An object represented by a mediator provides a public API, a set of public methods. In the following example, the view provides an “update” method.

function View = function() {

};
View.prototype.update = function(data) {
    // update the view with some data
};

The mediator receives a “target” as an argument that is the reference to the view. The mediator and can listen to events to update the view, removing the communication from the view and reducing the coupling.

function Mediator = function(target) {
	// target is the object represented: an instance of View
	// target.update(data) can be used when an event occurs
};

A Sample Application Using Soma.js

The following application is composed of three buttons, which create three different clocks on the screen: digital, analog and polar. Different design patterns are used to ensure that our elements, and at least the views and model, are reusable outside of this project. Over the following sections, we will walk through how this application is built.

clock-visual

See the clock application demo in action.

Note: the application does not work on browsers that don’t support querySelector. This was done intentionally to keep the code as simple as possible.

Plan and decouple elements

Planning and decomposing a task in different parts is a very important step in writing code. In the following exercise, there are two differents types of functions:

  • functions that have no dependencies (ideally all models and views so that they are reusable);
  • functions that remove dependencies from others.

The application contains these different entities:

  • An application instance that is the starting point and bootstraps our application preparing what is needed. It receives a DOM Element as a parameter, so the application has no hard DOM references. Its role is defining dependencies and creating elements.
    /js/app/clock.js (ClockDemo)
  • A model that holds the application state (the time). Its role is delivering the time information needed so that the views can display the current time.
    /js/app/models/timer.js (TimerModel)
  • Three clock views that represent a DOM Element. All views implement the same interface so they receive the time information in the same way. Their role is showing a clock on the screen, each one in a different way.
    /js/app/views/clocks/analog/analog.js (AnalogView)
    /js/app/views/clocks/digital/digital.js (DigitalView)
    /js/app/views/clocks/polar/polar.js (PolarView)
  • A mediator that represents a DOM element. Its role is destroying and creating clocks. It also links the timer model to the view so it can receive the time. The mediator encapsulates the communication and removes the dependencies from both the models and the view.
    /js/app/mediators/clock.js (ClockMediator)
  • A selector view that represents the three buttons used to create different clocks. Its role is dispatching a user event to inform the interested elements that they need to remove the current clock and create a new one.
    /js/app/views/selector.js (SelectorView)

Clock application source code on github

Below you can see what the file structure should look like as well as an illustration representing the architecture of the application.

clock-app-structure

architecture

Thinking with interfaces

Interfaces don’t exist in the JavaScript language as they do in Java or other languages. However their concept can be easily applied, if not enforced.

An interface is simply a description of a list of public methods and properties. This description acts a contract applied to instances so the application knows they can respond to a signature. A function that “implements” an interface needs to implement all the methods of this interface.

Interfaces solve many problems associated with code reuse in object-oriented programming. It is a programming discipline that is based on the separation of the public interface (API) from the real implementation.

Some strict JavaScript supersets such as Typescript have interfaces. As an example, the interface of a “car” function could look like this:

interface ICar {
	engine: IEngine;
	basePrice: number;
	state: string;
	make: string;
	model: string;
	year: number;
}

class Car implements Icar {
	// must implement the ICar signature in this class
}

In JavaScript, interfaces are not a built-in feature of the language, but several functions can be written to implement the same signature.

A developer should think about what are the interfaces for the reusable elements within the application. For example, in the clock application, clock views must be interchangeable and provide the exact same methods so they can be swapped out without modifying other elements.

The interfaces of the timer model and the clock views could look like this:

interface ITimerModel {
	add(callback: function);
	remove(callback: function);
	update();
}

interface IClockView {
	update(time: Object);
	dispose();
}

The below diagram shows the different interfaces implemented in the clock application:

clock-uml

The Application Instance

The first step to build a soma.js application is to create an application instance. This is the only moment a framework function has to be extended, all the other entities can be re-usable vanilla JavaScript functions, and can be framework agnostic.

The application instance executes two functions so the application can be setup with the architecture needed: the init and start functions.

(function(clock, soma) {

	var ClockDemo = soma.Application.extend({
		init: function() {

		},
		start: function() {

		}
	});

	var clockDemo = new ClockDemo();

})(window.clock = window.clock || {}, soma);

For more information, read the full soma.js documentation on the application instance.

A Self-contained Application

It is a good practice to use a DOM Element as the root within the application. This is useful to self-contain the application. Any DOM selections and manipulations should start from this reference.

Using CSS selectors with “id” rather than “class” is generally a bad idea. They cause the application to have hard dependencies on specific DOM Elements.

var ClockDemo = soma.Application.extend({
	constructor: function(element) {
		// store the root DOM Element
		this.element = element;
		// call the super constructor
		soma.Application.call(this);
	},
	init: function() {

	},
	start: function() {

	}
});

var clockDemo = new ClockDemo(document.querySelector('.clock-app'));

An easy way to test that the application is self-contained is to create many of them on the screen. They all should work independently.

Injection mapping rules

Now that the application has a starting point, the injection mapping rules can be created. It is a good practice to have them in the same place, but it is not a requirement, should the application needs to create other rules elsewhere.

A mapping rule is nothing more than assigning a function or a value to a string. The strings are used as “named variables” in other places so the injector knows what to inject.

this.injector.mapClass('timer', clock.TimerModel, true);

This mapping rule lets the injector know that when a “timer” variable is encountered, an instance of the function “clock.TimerModel” should be injected. The third parameter tells the injector to always inject the same instance and not create a new one.

this.injector.mapClass('face', clock.FaceView);
this.injector.mapClass('needleSeconds', clock.NeedleSeconds);
this.injector.mapClass('needleMinutes', clock.NeedleMinutes);
this.injector.mapClass('needleHours', clock.NeedleHours);

These four mapping rules are required for the analog clock as the elements of the analog clock have been broken into several views.

this.injector.mapValue('views', {
    'digital': clock.DigitalView,
    'analog': clock.AnalogView,
    'polar': clock.PolarView
});

An object that contains all the different clocks is also created and mapped in the injector. The clock mediator in charge of creating the clocks uses this object to instantiate the correct views.

Instantiate the Clock Mediator

The clock mediator represents the DOM Element in which the clocks are created. The first parameter is the mediator function to instantiate, and the second parameter is the DOM Element it represents. A “target” variable is injected representing the DOM Element.

Create the mediator using the framework core element “mediators”:

this.mediators.create(clock.ClockMediator, this.element.querySelector('.clock'));

Below is the code for the clock mediator:

(function(clock) {
	var ClockMediator = function(target) {

	};
	clock.ClockMediator = ClockMediator;
})(window.clock = window.clock || {});

Instantiate the Selector View

The selector view represents the DOM Element that contains the three buttons to create the clocks.

(function(clock) {
	var SelectorView = function() {

	};
	clock.SelectorView = SelectorView;
})(window.clock = window.clock || {});

Create the First Clock

Finally, a “create” event is dispatched from the application to create the first clock without a user interaction.

this.dispatcher.dispatch('create', 'analog');

Read the full documentation for more information about events.

Complete Application Instance Code

Source code on Github.

(function(clock, soma) {

	var ClockDemo = soma.Application.extend({
		constructor: function(element) {
			// store root DOM Element
			this.element = element;
			// call super constructor
			soma.Application.call(this);
		},
		init: function() {
			// mapping rules
			this.injector.mapClass('timer', clock.TimerModel, true);
			this.injector.mapClass('face', clock.FaceView);
			this.injector.mapClass('needleSeconds', clock.NeedleSeconds);
			this.injector.mapClass('needleMinutes', clock.NeedleMinutes);
			this.injector.mapClass('needleHours', clock.NeedleHours);
			this.injector.mapValue('views', {
				'digital': clock.DigitalView,
				'analog': clock.AnalogView,
				'polar': clock.PolarView
			});
			// create clock mediator
			this.mediators.create(clock.ClockMediator, this.element.querySelector('.clock'));
			// create clock selector template
			this.createTemplate(clock.SelectorView, this.element.querySelector('.clock-selector'));
		},
		start: function() {
			// dispatch event to create an analog clock
			this.dispatcher.dispatch('create', 'analog');
		}
	});

	// instantiate clock application with a root DOM Element
	var clockDemo = new ClockDemo(document.querySelector('.clock-app'));

})(window.clock = window.clock || {}, soma);

The Timer Model

The timer model function’s role is to provide the current time to other elements, without knowing anything about them. Its interface provides two methods: “add” and “remove”. They both take a parameter that is a function to send the current time to.

(function(clock) {
	var TimerModel = function() {

	};
	TimerModel.prototype.add = function(callback) {
		// register functions
	};
	TimerModel.prototype.remove = function(callback) {
		// remove registered functions
	};
	clock.TimerModel = TimerModel;
})(window.clock = window.clock || {});

The timer model has no dependencies. It doesn’t instantiate other functions and is interchangeable with other models as long as they implement the same interface (the “add” and “remove” functions). For example, another time model that takes the time from a server could be created and easily swapped out. The other elements in the application, such as the views and the clock mediator, wouldn’t have to be modified.

The following lines would be the only change required, even if the model is used in several places in the application:

var ModelFunction = isOnline ? ServerTimeModel : TimeModel;
this.injector.mapClass('timer', ModelFunction, true);

Note: dispatching an event to notify components of a time change could have been an option rather than using callbacks.

Source code on Github.

The Selector View

The selector view’s only role is handling a user event. When the user clicks on one of the buttons, the view catches this click event. A custom event is dispatched through the framework, the clock mediator listens for it and creates a new clock.

jQuery or vanilla JavaScript could have been used, but, for the puposes of example, the soma-template template engine (available as a standalone library or soma.js plugin) is used to listen to the user click event.

<div class="clock-selector">
	<button data-click="select('digital')">Digital clock</button>
	<button data-click="select('analog')">Analog clock</button>
	<button data-click="select('polar')">Polar clock</button>
</div>

(function(clock) {
	var SelectorView = function(scope, dispatcher) {
		scope.select = function(event, id) {
			dispatcher.dispatch('create', id);
		};
	};
	clock.SelectorView = SelectorView;
})(window.clock = window.clock || {});

Using events (and commands) is very powerful. They can make changes or adding new features easier to implement as they can listen for these events without interfering or creating hard dependencies. For example, consider a new feature such as a header that shows the current clock name. It would only be a matter of listening to the same event to update the screen. This makes the application very scalable.

Source code on Github.

Check the documentation for more information on soma-template.

The Clock Mediator

The clock mediator’s role is to link the timer model to the current clock view. It is very important as it is what makes the timer model and the clock views completely reusable and free of framework code. Without it, the application would be highly-coupled as the timer and the views would have a direct relation to each other.

The mediator is allowed to know more about the application, using injected references.

The parameters injected are:

  • target: represents a DOM Element;
  • dispatcher: the framework core element to listen to the “create” event;
  • mediators: the framework core element to create mediators;
  • timer: the timer model that hold the current time;
  • views: a list of available clock views to instantiate (key object).

To inject these values into the mediator, the mediator just needs to “ask” for them using their names. The injector knows that there are mapping rules for these specific “named” parameters, and sends the right references to the mediator.

(function(clock) {
	var ClockMediator = function(target, dispatcher, mediators, timer, views) {

	};
	clock.ClockMediator = ClockMediator;
})(window.clock = window.clock || {});

The mediator listens to a “create” event to know when it should destroy the current clock and create a new one.

dispatcher.addEventListener('create', function(event) {
	// destroy current clock and create a new one
	var clockId = event.params;
});

The current clock reference (the view) is stored in a variable, so it be can removed and destroyed before creating a new one.

var currentClock;

It is a good practice to make a “dispose” method available on views, models, or any functions that are meant to be destroyed. The dispose method cleans up the internal code of these elements.

// destroy previous clock
if (currentClock) {
	timer.remove(currentClock.update);
	currentClock.dispose();
}

To create the new clock, the mediator uses the parameter that has been sent with the “create” event, which can be “analog”, “digital” and “polar”. This “id” is used as the key for the list of views so the mediator knows which one to create.

The core framework element “mediators” is used to instantiate the new clock. The first parameter is the clock function to instantiate, and the second parameter is the DOM Element where the clock is created.

// create clock
var id = event.params;
var clockView = views[id];
currentClock = mediators.create(clockView, target);

Finally, the timer model is linked to the view. It is important that all the clock views have the same interface, an “update” method receives the time from the model. The view is also updated with the current time immediately, so the screen gets updated before the next tick (one second tick).

// register clock with timer model
timer.add(currentClock.update);
// update timer immediately
currentClock.update(timer.time);

Below is the complete code of the clock mediator.

(function(clock) {

	var ClockMediator = function(target, dispatcher, mediators, timer, views) {

		var currentClock;

		dispatcher.addEventListener('create', function(event) {

			// destroy previous clock
			if (currentClock) {
				timer.remove(currentClock.update);
				currentClock.dispose();
			}

			// create clock
			var id = event.params;
			var clockView = views[id];
			currentClock = mediators.create(clockView, target);

			// register clock with timer model
			timer.add(currentClock.update);

			// update timer immediately
			currentClock.update(timer.time);

		});

	};

	clock.ClockMediator = ClockMediator;

})(window.clock = window.clock || {});

Source code on Github.

Read the full soma.js documentation for more information on mediators.

The Clock Views

Three types of clock views are available in the application:

  • Analog view (clock.AnalogView);
  • DigitalView (clock.DigitalView);
  • PolarView (clock.PolarView);

As with the timer model, the views are highly reusable because:

  • They are not aware of the other application elements;
  • They are free of framework code;
  • They provide a simple API to update their current state.

The views are also interchangeable because they all provide the same interface:

  • a constructor that receives a DOM Element;
  • an “update” method that receives the current time.

It would be very easy to take them from this application and reuse them in another project. The application is also scalable as it would be trivial to create new clocks without changing the code of the different elements in the application.

As an example, here is the structure of the digital clock view.

(function(clock) {
	var DigitalView = function(target) {

	};
	DigitalView.prototype.update = function(time) {

	};
	DigitalView.prototype.dispose = function() {

	};
	clock.DigitalView = DigitalView;
})(window.clock = window.clock || {});

Source code on Github:

Dependency Injection Map

di-links-exp

Unit testing

The goal of unit testing is to isolate each part of the application and show that the individual parts are correct. A unit test provides a strict, written contract that the piece of code must satisfy. In other words, unit tests are a list of methods that are executed to retrieve a list of successes and failures.

Unit testing components is a very important step. The process affords several benefits, so many that it is hard to explain and list them all. Unit testing has a direct benefit on the quality of the code but goes beyond that. It also has invaluable benefits for a developers’ skills.

Here are just some of the benefits of unit testing:

  • Improve quality code;
  • Insure that a piece of code work;
  • Insure that a bug has been solved and will not reappear;
  • Insure that new features are not breaking existing ones;
  • Improve feature integration;
  • Find problems early;
  • Testing time improves as it runs fast;
  • Expose edge cases and make developers think harder on a problem;
  • Force developers to decouple their code so that it is testable;
  • Acts as a knowledge base that grows over time;
  • Work as a type of up-to-date documentation always in sync with the code;
  • Reduce manual testing time;
  • Instant visual feedback that is very rewarding, gives confidence and a sense of achievement;
  • Helps to understand deeply the design of the code;
  • Helps to find mistakes in public API;
  • Helps code reuse;
  • Tests can be automated;
  • Tests can run on web hooks (continuous integration).

A question that is often asked is: “How many unit tests should I write?” Unit tests can be compared to food. Eating too much will get you into trouble, but not eating enough will probably affect your health as well. A right balance must be found between production code and test code. It comes naturally with experience, and it is very common to constantly jump from production code to unit test code.

Clock application tests

Mocha and Jasmine are a couple of the many available JavaScript unit testing frameworks. The elements of the clock application are highly testable because they are not highly-coupled with others. This is one the benefits of dependency injection. Most dependencies can be sent in the instances, in our case using the constructor. Mocking objects can easily be created to simulate functionality and test each element separately.

Tests the clock application in the browser.

See the unit tests source code.

The tests can also run from the command line. The dependencies needs to be installed using NPM:

$ npm install
$ npm install -g mocha

Run the tests:

$ npm test

Or with mocha directly:

$ mocha -u bdd --reporter spec --timeout 5000 tests/specs/*.js

clock-app-mocha-cli-small

Continuous integration

Unit tests can be automated; this is called continuous integration. The topic is very large but as an example, the clock application has been integrated with a service called Travis. The service can run unit tests every time a new piece code is pushed to a Github repository. The developers receive an email if a failure appears, which ensures the sanity of the code on the repository.

See the clock application tests on Travis.

Conclusion

To create a scalable application or to improve an existing one, one must go through two different processes: “analysing” and “solving”. More or less answering to the two following questions:

  • What makes an application not scalable?
  • How to make an application more scalable?

What makes an application not scalable?

There are different ways to find out the level of scalability of an application. The first step before being able to see the whole picture is to go through all the elements of the application and ask:

  • Should this element be reusable?
  • Is this element testable?
  • Does this element have dependencies?
  • Is this element single purposed?

If a reusable element is hardly testable, or handles too many concerns, or has too many dependencies, there is a big chance that this element should be improved to make the application more scalable (bad code smells).

How to make an application more scalable?

Here is a list of tasks that can be performed to improve reusable elements. Each improvements will make the whole application more scalable.

  • Identify elements that are not single-purposed to break them down.
  • Look out for “bad code smells” to refactor.
  • Avoid code duplication (DRY).
  • Avoid large functions.
  • Avoid anonymous functions.
  • Make a usable public API so it is testable.
  • Avoid instantiating objects, use references using the constructor or setters.
  • Remove dependencies as much as possible.
  • Use the observer pattern (events) to remove dependencies and send information.
  • Create mediators for the element to remove dependencies and receive information.
  • Implement clear interfaces as much as you can.
  • Hide or make private concerns that are not relevant to the other elements.
  • Prefer composition over inheritance when possible.

The element is successfully improved when the following is possible:

  • The element can easily be interchanged with another without breaking the application.
  • The element is easily reusable outside of the project.
  • The element can be successfully unit tested.

In general, identify where bad code smells so the code can be refactored and improved. This term “code smell” refers to any symptom in the source code of a program that possibly indicates a deeper problem.

Analyze the clock application

In the clock application, two elements are dependent on the framework:

  • The application instance;
  • and the clock mediator.

The other elements are completely independent and highly reusable:

  • The timer model;
  • and the clock views.

The clock application has been planned so that the clock views and the timer model have no dependencies and are reusable outside of the project. To achieve this result, the application has been broken down, and all elements are single purposed:

  • The timer model handles the time.
  • The clock mediator destroys and creates clocks.
  • The selector view handles a user event.
  • The clock views create visuals on the screen.

The mediator pattern has been used to remove the dependencies in both the timer model and the clock views. Without the mediator, they would have had a direct dependency to each other.

The observer pattern (“create” event) has been used to decouple the selector view from everything else. It also makes the application more scalable as other elements can listen to the same framework events.

Dependency injection has been used to send references to all the elements, decoupling them and making them highly testable.

The clock views receive a reference to a DOM Element in the constructor, which makes them usable with any other DOM Element. They also provide a public API to update their content, so that they are usable by other elements externally without knowing about them.

The timer model provides an interface to add and remove callbacks, so it can send the time to a list of registered actors without knowing about them. Dispatching a time event from the model to the mediator could have been another elegant solution.

This architecture makes it easier to refactor the code, change the structure, add or remove elements, test each element separately, and update single element without changing the whole application.

Credits

Thank you to all the developers and non-developers who helped me on bits and pieces one day or another. Especially to the clever bunch of people at Stinkdigital. Big thanks to Henry Schmieder, author of the first version of soma.js, for his help on both the framework and this article. And thank you Brian Rinaldi for editing and trusting me with cutting-edge subjects such as this one!

It is probably not usual to thank “tools”, but they make everything so much easier that I have to mention them.

Pagekite is an amazing tunneling solution than I use constantly. Thank you Mocha to make my life easier with asynchronous tests. JSdom is not talked about enough, amazing Node.js library that makes everything possible from the command line. Grunt.js to make any automation possible. And last but not least, the people at JetBrains for making Webstorm such a good JavaScript editor.

Links

Article links:

soma.js links:

Unit testing links:

Other links:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值