This is an article about ThinkJS framework, because there is very little introduction to the framework on the Internet, so here I will explain my basic understanding of the framework and provide a project to practice the basic functions.

Since this is a Node.js based framework, let’s start with a little node.js knowledge.

Node. Js briefly

Node.js: Simply put, Node.js is JavaScript running on the server side.

Node.js installation and configuration (how to install Node.js on Windows and Linux)

Node.js official document

Official documentation:

  • Node.js is a JavaScript runtime environment based on Chrome V8 engine.
  • Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Event-driven and asynchronous IO
  • Node.js package manager NPM is the world’s largest open source library ecosystem.

Learn about the package manager NPM (NPM is already installed when node.js is installed)

When we develop on Node.js, we use a lot of JavaScript code written by other people. If we need to use a package written by someone else, every time we search the official document by name, download the code, unpack it, and use it again, it’s tedious. So a centralized management tool came into being: everyone packaged their own modules and put them on the NPM website. If they want to use them, they can use them directly through the NPM installation, regardless of where the code exists and should be downloaded from.

More importantly, if we want to use module A, and module A depends on module B, and module B depends on other modules, then NPM can download and manage all dependent packages based on their dependencies. Otherwise, rely on our own manual management, it is troublesome and easy to make mistakes.

The first Node program

Learn about the components of node.js applications:

  1. Introducing the Required module: We can use the require directive to load the Node.js module.
  2. Create server: The server can listen for requests from clients, similar to Apache, Nginx, etc.
  3. Receiving and responding to requests: A server is easy to create, a client can send HTTP requests using a browser or a terminal, and the server returns data when it receives the request.

To create node.js applications:

Step 1: Introduce the required module

Load the HTTP module with the require directive and assign the instantiated HTTP to the variable HTTP as follows:

var http = require('http');
Copy the code

Step 2: Create a server

Next we create the server using the http.createserver () method and bind port 8888 using the Listen method. The function receives and responds to data using the request and response parameters. Examples are as follows:

var http = require('http'); // Request node.js's built-in HTTP module, Http.createserver (function (request, response) {// Call the module provided by the HTTP module // send the HTTP header // HTTP status value: 200: WriteHead (200, {' content-type ': 'text/plain'}); Response.end ('Hello World\n'); }).listen(8888); // The terminal displays the following information: console.log('Server running at http://127.0.0.1:8888/');Copy the code

Node.js backend framework

Express and Koa (typical framework)

Express: the lightweight and flexible Node. js framework allows you to quickly build applications and is widely used. Express Official Documentation

Koa: Built by the original Express team, Koa aims to be a smaller, more expressive, and more robust building block for Web application and API development. By making use of async functions, KOA helps you discard callback functions and greatly enhances error handling. Koa official document

Express and Koa are the two most basic backend frameworks for Node.js. Because building an app still requires a lot of scaffolding, many other frameworks have sprung up on top of them to reduce the need to write such code. (e.g. ThinkJS, egg.js, etc.)

ThinkJS

Introduction: ThinkJS is a node.js framework for future development, integrating a large number of project best practices to make enterprise development easy and efficient. As of 3.0, the framework is implemented based on KOA2.x, which is compatible with all functions of Koa.

Features:

  • Based on KOA2.x, compatiblemiddleware
  • Kernel compact, supportExtend,AdapterAnd other plug-in methods
  • Excellent performance, high unit test coverage
  • Built-in automatic compilation, automatic update mechanism, convenient and rapid development
  • Use something more elegantasync/awaitHandling asynchronous issues, no longer supported*/yieldway

Quick start

ThinkJS provides scaffolding to quickly create a project. In order to use more ES6 features, the framework requires node.js to be at least 6.x, and LTS is recommended.

Install the ThinkJS command

npm install -g think-cli
Copy the code

After the installation is complete, the system will have thinkjs command (you can run thinkjs-v to check the think-cli version number)

Create a project

Thinkjs new demo // Create a project named demo NPM install // Install depends on NPM start // run the projectCopy the code

The project structure

The default project structure is as follows:

| - development. Js / / development environment entry file | - nginx. Conf / / nginx configuration file | - package. The json | - pm2. Json / / pm2 configuration file | -- - Production. Js / / production environment entry file | - README. Md | - SRC | | - the bootstrap / / start automatically perform directory | | | - master. The js / / master process automatically execute | | | - worker. Js / / worker processes automatically perform | | - config / / configuration file directory | | | - adapter. The js / / adapter configuration file | | | - config. Js / / the default configuration file | | | - config. Production. Js / / production environment of the default configuration file, And config. Js merger | | | -- - the extend. Js / / the extend configuration file | | | - middleware. The js / / middleware configuration file | | | -- - the router, js / / custom routing configuration file | | - controller / / controller directory | | | - base, js | | | - index. The js | | - logic / / logic directory | | | - index. The js | | - model / / models directory | | | - index. The js | - view / / template directory | | - index_index. HTMLCopy the code

Basis function

Config (Configuration)

In a real project, you will definitely need a variety of configurations, including configurations required by the framework and customized configurations for the project. ThinkJS manages all configurations in a unified way. Files are placed in the SRC /config/ directory and divided into different configuration files according to different functions.

  • config.jsSome common configurations
  • adapter.jsAdapter Configuration (database configuration)
  • router.jsUser-defined route configuration
  • middleware.jsMiddleware configuration
  • validator.jsData verification configuration
  • extend.jsThe extend configuration

Configuration format

// SRC /config.js module.exports = {port: 1234, redis: {host: '192.168.1.2', port: 2456, password: ''}}Copy the code

The configuration value can be either a simple string or a complex object, depending on your requirements.

Using a configuration

The framework provides different ways to quickly get configuration in different environments:

  • In CTX (context), you can passctx.config(key)To get the configuration
  • In controller, you can passcontroller.config(key)To get the configuration
  • In other cases, you can passthink.config(key)To get the configuration

In fact, ctx.config and controller.config are a more convenient way to get configuration based on the think-.config wrapper.

Adapter

Adapter is used to address multiple implementations of a type of functionality that provide the same set of interfaces, similar to the factory pattern in the design pattern. For example: support a variety of databases, support a variety of template engines. In this way, you can easily switch between different types. Adapter is usually used in conjunction with Extend.

The framework provides many adapters by default, such as View, Model, Cache, Session, and Websocket. Projects can also be extended as needed, and third-party adapters can also be introduced.

Adapter configuration

The Adapter configuration file is SRC /config/adapter.js in the following format:

const nunjucks = require('think-view-nunjucks'); const ejs = require('think-view-ejs'); const path = require('path'); Exports. view = {type: 'nunjucks', // the default template engine is nunjucks common: {// common configuration viewPath: Path.join (think.root_path, 'view'), sep: '_', extName: '.html'}, nunjucks: {// Handle for nunjucks: }, ejs: {handle: ejs, viewPath: {handle: ejs, viewPath: path.join(think.ROOT_PATH, 'view/ejs/'), } } exports.cache = { ... }Copy the code

The Adapter configuration supports the operating environment. You can set different configurations according to the operating environment, for example: In the development environment and production environment, the database is generally different. In this case, the adapter.development.js and Adapter.production. js can be used to store the different configurations.

Adapter Configuration parsing

The Adapter configuration stores detailed configurations of all types. You need to parse them and select a corresponding configuration. For example, in the configuration file above, the detailed configuration of nunjucks and EJS template engines is configured, but only one template engine will be used in a specific scenario. Of course, configuration resolution does not need to be invoked by the user in the project; it is usually handled in the plug-in’s corresponding methods.

Adapter to use

Adapters are different implementations of one type of functionality. They are usually used together with corresponding extensions and cannot be used independently. For example, the View Adapter (think-view-nunjucks, think-view-EJS) is used together with the think-view extension.

Database :(model Adapter with think-mongo extension for use)

model adapter

/** * model adapter config * @type {Object} */ exports. Model = {type: 'mongo', // Logger: MSG => think.logger.info(MSG) // Print the message logger}, mongo: {host: '127.0.0.1', port: 27017, user: '', password: '', database: 'manannan', // database name options: ''}};Copy the code

extend

const view = require('think-view'); const model = require('think-model'); const cache = require('think-cache'); const session = require('think-session'); const mongo = require('think-mongo'); module.exports = [ view, // make application support view model(think.app), //// pass think.app to model extension Mongo (think.app), cache, session];Copy the code

Extend

Although the framework has a lot of functionality built into it, the functionality provided is far from enough in actual project development. 3.0 introduced an extension mechanism to make it easier to extend the framework. The supported extension types are think, Application, Context, Request, Response, Controller, Logic, and Service.

Many built-in functions of the framework are also extended to achieve, such as: Session, Cache.

Context

Context is an object in Koa that processes user requests throughout the request life cycle. Commonly used in Middleware, Controller, logic, or CTX.

The framework inherits the object and extends many useful properties and methods through the Extend mechanism.

Such as:

ctx.state

Recommended namespace for passing information between middleware and sending information to templates. Avoid directly adding attributes to CTX, as this may overwrite existing attributes and cause strange problems.

ctx.state.user = await User.find(id);
Copy the code

The controller can then retrieve the corresponding value via this.ctx.state.user.

module.exports = class extends think.Controller { indexAction() { const user = this.ctx.state.user; }}Copy the code

ctx.header

Get all header information, equivalent to ctx.request.header.

const headers = ctx.headers;
Copy the code

ctx.headers

Get all header information, equivalent to ctx.header.

ctx.url

Gets the requested address.

Middleware

Middleware, called Middleware, is a very important concept in Koa and can be used to easily handle user requests.

The middleware format is a higher-order function, and external functions receive an options parameter, so that the middleware can provide some configuration information to enable/disable some functions. This function takes CTX, which is short for context, an object in the current request lifecycle that stores some information about the current request, and next, which returns a Promise, This makes it easy to handle the post-logic. (Execution is an onion model)

Configuration format

To facilitate the administration and use of middleware, the framework uses a configuration file called SRC /config/middleware.js to manage middleware.

const path = require('path') const isDev = think.env === 'development' module.exports = [ { handle: LogRequest: isDev, sendResponseTime: isDev,},}, {handle: 'resource', enable: isDev, // Whether to enable the current middleware options: {root: path.join(think.ROOT_PATH, 'WWW '), publicPath: /^\/(static|favicon\.ico)/, }, } ]Copy the code

The configuration items are the list of middleware to be used in the project. Each item supports handle, Enable, Options, and match.

handle

The processing function of the middleware can be used by the system built-in, external import, or the middleware in the project.

enable

Whether to enable the current middleware.

options

Configuration item passed to the middleware in the form of an object that the middleware gets the configuration.

match

The middleware is executed only after matching specific rules. It supports two ways, one is path matching, the other is custom function matching.

Middleware built into the framework

The framework has several middleware built in that can be referenced directly as strings.

  • metaDisplay some meta information. Such as sending ThinkJS version number, interface processing time, etc
  • resourceTo process static resources, you are advised to shut down the production environment and use the WebServer directly
  • traceProcessing error, the development environment will be detailed error information display processing, you can also customize the display error page
  • payloadHandles form submission and file upload, similar tokoa-bodyparserEtc.middleware
  • routerRoute resolution, including user-defined route resolution
  • logicLogic call, data verification
  • controllerThe controller and action

Custom middleware in the project

Sometimes middleware is added to a project for a specific purpose, so it can be placed in the SRC/Middleware directory and referenced directly as a string.

// middleware/log.js const defaultOptions = { consoleExecTime: } module.exports = (options = {}) => {// Consolidates passed configurations options = object. assign({}, defaultOptions,) options); return (ctx, next) => { if(! options.consoleExecTime) { return next(); Const startTime = date.now ();} const startTime = date.now (); let err = null; Return next(). Catch (e => {err = e; }).then(() => {const endTime = date.now (); console.log(`request exec time: ${endTime - startTime}ms`); if(err) return Promise.reject(err); // If there is an error in the subsequent execution logic, the error is returned})}}Copy the code

Usage: in/SRC/config/middleware. Js

Module. exports = [{handle: 'log', // middleware handler options: {consoleExecTime: true,},}]Copy the code

Introduce external middleware

Bringing in external middleware is very simple, just require it in.

const cors = require('koa2-cors');
module.exports = [
  ...,
  {
    handle: cors,
    option: {
      origin: '*'
    }
  },
  ...
]
Copy the code

Controller (Controller)

In the MVC model, the controller is the logical processing part of the user request. For example, put all user-related actions in user.js, and each Action is an Action.

Create a controller

The Controller in the project needs to inherit the Think.Controller class so that it can use some of the built-in methods. Of course, you can create some generic base class in your project, and then the actual controller inherits from that base class.

A base class named base.js is automatically created when the project is created, which other Controllers inherit.

The Action to perform

Action execution is accomplished through the middleware think-controller. The method name of xxxAction is found in the controller and called by ctx. Action value, and the related magic methods are called in the specific order:

  • Instantiate the Controller class, passing in the CTX object

  • Called if the method __before exists, and stopped if the return value is false

  • Execute if the method xxxAction exists, and stop if the return value is false

  • If the method xxxAction does not exist but the __call method does, then __call is called, and if the return value is false, execution stops

  • The __before operation is performed if the method __after exists

  • Executes if method __after exists

The front operation __before

Sometimes in a project, you need to do things in a unified place, such as checking whether you are logged in or not. If you are not logged in, you cannot continue. In this case, this can be done with a built-in __before.

__before is called before the specific Action is called so that some processing can be done within it.

If class inheritance requires a call to the parent __before method, this can be done with super.__before.

After operation __after

The __after operation corresponds to __before and is executed only after the execution of the specific Action. If the specific Action execution returns false, __after is no longer executed.

Logic

When handling user requests in the Action, it is often necessary to obtain the data submitted by the user first, and then verify it. If the verification is ok, the subsequent operations can be carried out. When the parameter verification is completed, sometimes it is necessary to judge the permission, etc., and then the real logical processing can be carried out after all these judgments are correct. Putting all this code into one Action can make the Action code very complex and verbose.

To solve this problem, ThinkJS adds a layer of Logic in front of the controller. The actions in the Logic correspond to the actions in the controller one by one. The system automatically calls the actions in the Logic before calling the actions in the controller.

Router = Router

When a user accesses an address, there needs to be a corresponding logic to process it. Traditionally, a request for a file, such as /user/about.php, would have an entity file called /user/about.php in the project directory. This will solve the problem, but will result in many files, and many file logic functions may be relatively simple.

In the current MVC development model, such problems are usually solved by routing. The solution is to map all user requests to an entry file (such as index.php), then the framework resolves the address of the current request, resolves the corresponding function to be performed according to the configuration or convention, and finally invokes and responds to the user’s request.

Since Node.js starts the HTTP (S) service itself, it already aggregates user requests into an entry file, making it easier to handle route mappings.

In ThinkJS, when a user accesses a URL, it is eventually responded to by a specific action in the Controller. Therefore, it is necessary to resolve the controller and action corresponding to the URL, which is achieved through the Think-Router module.

The routing configuration

Think-router is a middleware that was added to the configuration file SRC /config/middleware.js by default when the project was created.

Path preprocessing

When the user accesses the service, the initial PathName can be obtained through the ctx.url property, but the pathname needs to be preprocessed to facilitate subsequent resolution of controller and action through the Pathname. For example, remove the suffix of.html in the URL and finally get the actual pathname that needs to be parsed later. The default route resolution rule is /controller/ Action.

For Controllers in ThinkJS, you can either write the router or customize the router.

pathname Subset controller controller action note
/ There is no index index The default values are Controllller and Action
/user There is no user index Action is the default value
/user/login There is no user login
/console/user/login There are console/user login There is a subset of controllers console/user
/console/user/login/aaa/b There are console/user login The remaining AAA/B is not resolved

User-defined routing rules

Although the default route resolution method meets the requirements, sometimes urls look less elegant. We prefer shorter urls to facilitate memorization and propagation. The framework provides custom routes to handle this requirement.

The configuration file of the user-defined routing rule is SRC /config/router.js, and the routing rule is a two-dimensional array.

Asynchronous processing

Node.js uses an event-driven, non-blocking I/O model with many asynchronous interfaces, such as file operations and network requests. Although synchronization interfaces for file operations are provided, these interfaces are blocking and should not be used except in special cases.

For asynchronous interfaces, the official API is callback, but this approach can easily lead to the problem of callback hell when the business logic is complex. In order to solve this problem, event, Promise, Generator function, Async Function and other solutions have appeared successively. ThinkJS uses Async Function solution to solve the asynchronous problem.

Async functions

Async functions define functions with Async /await syntax, as in:

async function fn() {
  const value = await getFromApi();
  doSomethimgWithValue();
}
Copy the code
  • There areawaitThere must beasync, but there areasyncIt doesn’t have to beawait
  • Async functionsIt could be a normal function, or it could beArrow functionsThe way of
  • awaitI need to connect it to the backPromiseIf notPromise, does not wait for processing
  • The return value must bePromise

The expressions followed by the return value and await are promises, that is, Async functions are based on promises. If the expression after await returns a value that is not a Promise, then you need to wrap it as a Promise in some way.

Model/database

Relational database (MYSQL)

In the development of the project, it is often necessary to operate the database (such as: add, delete, change, check and other functions), manual spelling OF SQL statements is very troublesome, but also pay attention to SQL injection and other security issues. This framework provides a model function, easy to operate the database.

Extending model functionality

By default, the framework does not provide the function of the model. The corresponding extension needs to be loaded to support it. The corresponding module is think-Model. Modify the extension configuration file SRC /config/extend.js and add the following configuration:

const model = require('think-model'); Module.exports = [model(think.app) // Make the framework support model functionality];Copy the code

Configuring the Database

As the model supports multiple databases, the configuration file format is Adapter and the file path is SRC /config/adapter.js

Mysql

The Adapter of Mysql is think-model-mysql, and the underlying implementation is based on the Mysql library. The connection pool is used to connect to the database. The default connection number is 1.

const mysql = require('think-model-mysql'); Exports. model = {handle: 'root', // Adapter handle user: 'root', // user name password: ", // password database: ", // database host: '127.0.0.1', // host port: 3306, // port connectionLimit: Prefix: ", // table prefix, if there are more than one items in a database, the table between the items can be distinguished by prefix}Copy the code

Create a model file

The model file is stored in the SRC /model/ directory, inheriting the model base class think.model, and the file format is:

// src/model/user.js module.exports = class extends think.Model { getList() { return this.field('name').select(); }}Copy the code

Instantiation model

When the project is started, all Model files under the project will be scanned. After scanning, all Model classes will be stored in the think. App. models object, which will be searched during instantiation.

CRUD operations

The Think.Model base class provides a wealth of methods for CRUD operations, as described below.

Thinkjs.org/zh-cn/doc/3…

MongoDB

Sometimes a relational database is not sufficient for a project and MongoDB is required to store the data. The framework provides a think-Mongo extension to support MongoDB, which is implemented based on MongoDB.

Configuring the MongoDB Database

The database configuration of MongoDB reuses the configuration of the relational database model, which is configured as adapter and placed under model. Js' exports.model = {type: 'mongo', // common: {// common configuration: logConnect: Logger: MSG => think.logger.info(MSG) // Print the logger}, mongo: {host: '127.0.0.1', port: Options: {replicaSet: 'mgSET-3074013 ', authSource: 'admin'}}}Copy the code

Extending MongoDB features

Modify the extension configuration file SRC /config/extend.js and add the following configuration:

const mongo = require('think-mongo'); Module.exports = [mongo(think.app) // Make the framework support model functionality]Copy the code

Think.mongo, think.mongo, ctx. Mongo, and Controller.mongo methods will be injected after the extension is added. Think.mongo is the base class file of Mongo model, and the other methods are instantiation methods of Mongo model. Ctx. mongo and Controller.mongo are wrappers for the think. Mongo method.

Create a model file

The model file is stored in the SRC /model/ directory, inheriting the model base class think.mongo, and the file format is:

// src/model/user.js module.exports = class extends think.Mongo { getList() { return this.field('name').select(); }}Copy the code

Instantiation model

When the project starts, all model files under the project (SRC /model/) will be scanned. After scanning, all model classes will be stored in the think.app.models object.

API

Thinkjs.org/zh-cn/doc/3…

Think object

Frame built-in think global object, easy to use anytime and anywhere in the project.

API

Thinkjs.org/zh-cn/doc/3…

Enabling Customization

When starting a project with NPM start or Node production.js, adding additional logical code to these entry files is not recommended, although it is possible. The system provides additional entry points for starting customizations.

bootstrap

Files in the SRC/Boostrap/directory will be loaded when the system starts.

  • When the Master process is loadedsrc/bootstrap/master.js
  • Load when the Worker process is downsrc/bootstrap/worker.js

So you can put some of the logic that needs to be executed at startup in the corresponding file.

Service/Service

The Service file is stored in the SRC/Service/directory in the following format:

//  src/service/user.js
module.exports = class extends think.Service {
  find(id){
     return {username:'123',id:id} 
  }
}
Copy the code

Services inherit from the Think.Service base class, which does not provide any methods and can be extended by Extend.

Instantiate the Service class

You can instantiate the service class using the think. Service method. There are also corresponding service methods on the controller and CTX, such as ctx.service and controller.service, which are shortcuts to think.

//controller

think.service('user').find(111)
Copy the code

When the project starts, it will scan all the services files under the project and put them in the think.app.services object. During instantiation, it will find the corresponding class file from the object.

The above is the basic understanding of the framework, if it is a new start of the framework, then understand the basic configuration under SRC, including how to add the adapter of the database and extend the model, and then in the model layer database operation, Interface (API) function can be realized by front and back interaction of controller layer, and further learning is needed.

Project source: github.com/mfnn/thinkj…

Note: This project uses the mongoDB database. Basic functions of the project:

1. Obtain the foreground request header (token) to implement user authentication

//   controller/base.js
const jwt = require('jsonwebtoken');
const Token = require('.. /logic/token');

module.exports = class extends think.Controller {
    async __before() {
        if (this.ctx.config('allowUrls').indexOf(this.ctx.url) === -1) {
            if(! this.ctx.request.header.authorization) { this.fail(401,'No authentication');
                return false;
            } else {
                let payload = null;
                const authorization = this.ctx.request.header.authorization;
                const secret = this.ctx.config('secret'); try { payload = jwt.verify(authorization, secret); // The authentication function is in logic/token await token. verify(authorization); this.ctx.state.user_id = payload._id; } catch (error) { this.fail(error.code, error.message);return false; }}}}};Copy the code

2. Set the token, save the token to redis, and set the expiration time

//controller/user.js // The user logs in to asyncloginUserAction() {
        const user = await this.mongo('user').loginUser(this.post('account'), this.post('password'));
        if (think.isEmpty(user)) {
            this.fail(403, 'Login failed, wrong username or password');
        } else {
            let payload = {_id: user._id, account: user.account, password: user.password};
            let token = jwt.sign(payload, think.config('secret'), {expiresIn: 60 * 60 * 24 * 30});
            redis.set(token, payload._id.toString());
            redis.expire(token, token_expire);
            return this.success({token}, 'User logged in successfully'); }}Copy the code

3. Realize wAMP real-time message push

//controller/wamp.js
const autobahn = require('autobahn');
const wampConfig = require('.. /config/config').wamp;
const options = wampConfig.options;

module.exports = {
    roomInfo: (args) => {
        const connection = new autobahn.Connection(options);
        console.log("Connection information",connection);
        connection.onopen = function (session) {
            session.publish(wampConfig.definedTopic, [args]);
            console.log("The theme of the WAMP launch is :" + wampConfig.definedTopic);
            console.log(args);
        };
        console.log("end======"); connection.open(); }};Copy the code
/** * @param {any} user * @returns */ async addRoomWamp(roomInfo) {let sum = 0;
        const rooms = await this.model('room').add(roomInfo);
        if(! (think.isEmpty(rooms))){ const data = {sum:"lalal"}; wamp.roomInfo(data); }}Copy the code

4. Verify identity and permission

// Get all house information asyncgetAllRoomsAction() {
        const userInfo = await this.mongo('user').findUserDetailInfo(this.ctx.state.user_id);
        console.log("userInfo", userInfo);
        if(! (think.isEmpty(userInfo)) && userInfo.role ==='admin') {
            this.success(await this.mongo('room').getAllRooms());
        } else {
            this.fail("No access"); }}Copy the code

(The implementation method is to judge the input user role, you can use ACL, and the project will be updated later)