We went on to

Egg-core: Source Code Analysis

Load functions loadService, loadController, loadRouter

LoadService function

How do I use a Service in an Egg framework

The loadService function is one of the most complex load functions in the Egg framework

// How to use a service in an egg framework App /service/user1.js // Export a class that extends from require('egg').service. BaseContextClass // eggCore generates an instance of this class. App.context is used as a parameter to load the new instance. this.ctx.service.user1.find(1) const Service = require('egg').Service; Class UserService extends the Service {async find (uid) {/ / at this point we can through this. CTX, enclosing app, enclosing the config, enclosing the Service access to useful information, Query ('select * from user where uid =? ') const user = await this.ctx.db.query('select * from user where uid =? ', uid); return user; } } module.exports = UserService; // Method 2: App /service/user2.js // this method emulates a BaseContextClass, which can also achieve the purpose of method 1. Class UserService {constructor(CTX) {this.ctx = CTX; this.app = ctx.app; this.config = ctx.app.config; this.service = ctx.service; } async find(uid) { const user = await this.ctx.db.query('select * from user where uid = ? ', uid); return user; } } module.exports = UserService; // Method 3: Js // app/service/user3.js // service can also export function, when the load will call this function, pass the appInfo parameter, finally get the result of the function return // in the controller: This. CTX. Service. User3. GetAppName (1), CTX module.exports = (appInfo) => {return {async getAppName(uid){return appinfo.name; }}}; // CTX // CTX // CTX // CTX // CTX // CTX // CTX / this.ctx.service.user4.getAppName(1) module.exports = { async getAppName(uid){ return appInfo.name; }};Copy the code

CTX = this.ctx; this.ctx = this.ctx; this.ctx = this.ctx; this.ctx = this.ctx; this.ctx = this.ctx;

  1. Why do we get the service object from this.ctx on each request: This. CTX inherits from app.context, so if we bind the service to app.context, then the current request context CTX will get the service object. EggLoader does the same
  2. For the above four scenarios, how to deal with the exported instances?
  • If a class is exported, EggLoader initializes the instance as a CTX object and exports it, so we can use this.ctx directly in the class to get the context of the current request
  • If the export is a function, EggLoader runs the function with app as an argument and exports the result
  • If it is a common object, export it directly

FileLoader class implementation analysis

The base class for implementing loadService is FileLoader, which is also the base of the loadMiddleware loadController implementation. This class provides a load function that parses the directory structure and file contents. Return a target object that can be retrieved from the service based on the filename, subfilename, and function name. The target structure looks something like this:

{
    "file1": {
        "file11": {
            "function1": a => a
        }
    },
    "file2": {
        "function2": a => a
    }
}
Copy the code

Let’s look at the implementation of the FileLoader class:

Constructor (options) {/* constructor(options) {/* options All directories where files need to be loaded 2. Target: Initializer: an initialization function that initializes the exported content of the file. This function is used in the loadController implementation. 4. Inject: If the export Object of a file is a function, pass the value to the function and execute the export, usually this.app */ this.options = object. assign({}, defaults, options); Const items = this.parse(); const items = this.parse(); const target = this.options.target; // item1 = { properties: [ 'a', 'b', 'c'], exports1 },item2 = { properties: [ 'a', 'b', 'd'], exports2 } // => target = {a: {b: {c: exports1, d: Exports2}} // Generate a large object target recursively based on the file path name, For (const item of items) {item.properties.reduce((target, property, index) => { let obj; const properties = item.properties.slice(0, index + 1).join('.'); if (index === item.properties.length - 1) { obj = item.exports; if (obj && ! Is.primitive (obj)) {// It was important to make sure that the target was not an export, it was probably just a path obj[FULLPATH] = item.fullpath; obj[EXPORTS] = true; } } else { obj = target[property] || {}; } target[property] = obj; return obj; }, target); } return target; } // Finally generate [{properties: ['a', 'b', 'c'], exports, fullpath}] Parse () {// Let directories = this.options.directory; if (! Array.isArray(directories)) { directories = [ directories ]; } // Walk through all file paths const items = []; For (const directory of directories) {// There may be subdirectories under each directory, Sync (files, {CWD: directory}) const filepaths = globby.sync(files, {CWD: directory}); for (const filepath of filepaths) { const fullpath = path.join(directory, filepath); if (! fs.statSync(fullpath).isFile()) continue; Foo /bar.js => ['foo', 'bar']. Const properties = getProperties(filepath, this.options); // app/service/foo/bar.js => service.foo.bar const pathName = directory.split(/[/\\]/).slice(-1) + '.' + properties.join('.'); Const exports = getExports(fullpath, this.options, pathName); // Exports = getExports(fullpath, this.options, pathName); // Exports = getExports(fullpath, this.options, pathName); / / if the export is a class, can set up some attribute, this property below if for special processing parts of the class is used in (is) a class (exports)) {exports. The prototype. The pathName = the pathName; exports.prototype.fullPath = fullpath; } items.push({ fullpath, properties, exports }); } } return items; }} function getExports(fullPath, {initializer, call, inject}, pathName) { let exports = utils.loadFile(fullpath); If (initializer) {exports = initializer(exports, {path: fullpath, pathName}); } // exports are class, generatorFunction, AsyncFunction directly return the if (is. Class (exports) | | is the generatorFunction (exports) | | is the asyncFunction (exports)) {return exports; } // If it exports a normal function and sets call=true (the default is true) inject will be passed in and called, as mentioned several times above, If (call && is. Function (exports)) {exports = exports(inject); if (exports ! = null) { return exports; } // return exports; }Copy the code

Implementation analysis of ContextLoader class

The loadService function ultimately mounts the service object to app.context, so it provides ContextLoader, which inherits the FileLoader class. Mount the target parsed by FileLoader to app.context. Here’s how it works:

Class ContextLoader extends FileLoader {constructor(options) {const target = options.target = {}; super(options); // inject is app const app = this.options. Inject; // Property is the property to mount, such as "service" const property = options.property; Object.defineproperty (app.context, property, {get() {// Cache. If (! this[CLASSLOADER]) { this[CLASSLOADER] = new Map(); } const classLoader = this[CLASSLOADER]; / / for export as an example, here is the case for this. Above CTX. Service. File1. Fun1 implementation, example here is this. CTX. Service, Let instance = classLoader.get(property); if (! Instance) {// This is passed as an argument to an instance of require('egg').service. This will change depending on the caller. If it's an instance call of app.context, then app.context, if it's an instance call of a subclass of app.context, then it's an instance of a subclass of app.context. Instance = getInstance(target, this); instance = getInstance(target, this); classLoader.set(property, instance); } return instance; }}); Function getInstance(values, CTX) {// FileLoader (CTX, CTX, CTX, CTX, CTX); Const Class = values[exports]; const Class = values[exports]; values : null; let instance; If (Class) {if (is.class(Class)) {// If (is.class(Class)) {instance = new Class(CTX); } else {// exports () {// exports () {// exports () {// exports () {// exports () {// exports () {// exports () {// exports () {// exports () {// exports () {// exports () {// exports (); The function of processing logic loadMiddleware loadService, loadController utility, and the processing logic loadService use instance of the class = class; }} else if (is. Primitive (values)) {// Primitive = primitive; } else {// If the current target part is a path, then a new ClassLoader instance will be created, and this ClassLoader will recursively call getInstance. Instance = new ClassLoader({CTX, properties: values}); } return instance; }Copy the code

The realization of the loadService

With the ContextLoader class, it is very easy to implement the loadService function as follows:

// Call loadToContext loadService(opt) {opt = object. assign({call: true, caseStyle: 'lower', fieldClass: 'serviceClasses', directory: This.getloadunits ().map(unit => path.join(unit.path, 'app/service')), // All loadunits under the directory of service}, opt); const servicePaths = opt.directory; this.loadToContext(servicePaths, 'service', opt); } // loadToContext creates ContextLoader instance directly, Assign ({}, {directory, property, inject: loadToContext(directory, property, opt) {opt = object. assign({}, {directory, property, inject: this.app, }, opt); new ContextLoader(opt).load(); }Copy the code

LoadMiddleware function

Middleware is a very important link in Koa framework. Middleware is introduced through app.use and onion circle model is used, so the loading sequence of middleware is very important. – If the middleware is configured in config above, the system will automatically use the middleware with app.use function – All middleware can be obtained from app.middleware name, which is convenient for dynamic use in business

LoadMiddleware (opt) {const app = this.app; Override: true, caseStyle: override: true, caseStyle: override: true, caseStyle: override: true 'lower', directory: this.getLoadUnits().map(unit => join(unit.path, 'app/middleware')) // Middleware in all load units}, opt); const middlewarePaths = opt.directory; LoadToApp (middlewarePaths, 'middlewares', opt); middlewares (middlewarePaths, 'middlewares', opt); // Rebind each middleware in app.middlewares to app.middleware. The properties of each middleware are not configurable. Non-enumerable for (const name in app.middlewares) {object.defineProperty (app.middleware, name, { get() { return app.middlewares[name]; }, enumerable: false, configurable: false, }); } // Only appMiddleware and coreMiddleware can be used directly in app.use if configured in config. Other middleware is just mounted to the app. Developers can use dynamic const middlewareNames = this. Config. CoreMiddleware. Concat (this. Config. AppMiddleware); const middlewaresMap = new Map(); App. Middlewares for (const name of middlewareNames) {if (! app.middlewares[name]) { throw new TypeError(`Middleware ${name} not found`); } if (middlewaresMap.has(name)) { throw new TypeError(`Middleware ${name} redefined`); } middlewaresMap.set(name, true); const options = this.config[name] || {}; let mw = app.middlewares[name]; // Middleware file definitions must export a normal function and accept two arguments: // options: middleware configuration items that the framework passes app.config[${middlewareName}], app: The current Application instance // executes the exports function to generate the final middleware mw = mw(options, app); mw._name = name; // wrapMiddleware, eventually converting to async function(CTX, next) mw = wrapMiddleware(mw, options); if (mw) { app.use(mw); this.options.logger.info('[egg:loader] Use middleware: %s', name); } else { this.options.logger.info('[egg:loader] Disable middleware: %s', name); }}} // Load and export all files of the specified attributes through FileLoader instance, LoadToApp (directory, property, opt) {const target = this.app[property] = {}; opt = Object.assign({}, { directory, target, inject: this.app, }, opt); new FileLoader(opt).load(); }Copy the code

LoadController function

The functions generated in controller will eventually be used as middleware in router.js, so we need to convert the contents of controller to middleware async Function (CTX, next), Initializer is used to convert the contents of controller into middleware for different situations. The implementation logic of loadController is as follows:

LoadController (opt) {opt = object. assign({caseStyle: 'lower', directory: Path.join (this.options.baseDir, 'app/controller'), // This configuration, as mentioned above, is used to preprocess the exported object's initializer function: (obj, opt) => {// If it is a normal function, call it directly to generate a new object if (is.function(obj) &&! is.generatorFunction(obj) && ! is.class(obj) && ! is.asyncFunction(obj)) { obj = obj(this.app); } if (is.class(obj)) { obj.prototype.pathName = opt.pathName; obj.prototype.fullPath = opt.path; // If it is a class, the class function is converted to async function(CTX, next) middleware and CTX is used to initialize the class. So in controller we can also use this.ctx. XXX return wrapClass(obj); } if (is.object(obj)) {// If it is an object, Async Function (CTX, next) middleware form return wrapObject(obj, opt.path); } if (is.generatorFunction(obj) || is.asyncFunction(obj)) { return wrapObject({ 'module.exports': obj }, opt.path)['module.exports']; } return obj; }, }, opt); // The loadController function also uses the loadToApp function to mount its exported object to the app. Const controllerBase = opt.directory; this.loadToApp(controllerBase, 'controller', opt); },Copy the code

LoadRouter function

The loadRouter function is extremely simple. It requires loading files in the app/router directory. The router property on the EggCore class does all the work

Router is an instance of the Router class, which is implemented based on koA-Router

LoadRouter () {this.loadfile (this.resolvemodule (path.join(this.options.basedir), loadRouter() {this.loadFile(this.resolvemodule (path.join(this.options.basedir), 'app/router'))); Get router() {if (this[router]) {return this[router]; Const Router = this[Router] = new Router({sensitive: true}, this); // Load router middleware references before startup this.beforeStart(() => {this.use(router.middleware())); }); return router; } // Delegate all method functions on the router to EggCore, so we can use app.get('/async',... AsyncMiddlewares, 'subController. SubHome. Async1') way to configure routing utils. The methods. The concat ([' all ', 'resources',' register ', 'redirect' ]).forEach(method => { EggCore.prototype[method] = function(... args) { this.router[method](... args); return this; }; })Copy the code

The Router class extends KoaRouter’s method-related functions, resolves controller notation, and provides resources methods that are compatible with restAPI requests

The Router API is a Router API, and the Router API is a Router API. The Router API is a Router API.

Class Router extends KoaRouter {constructor(opts, app) {super(opts); this.app = app; // Extend this.patchrouterMethod (); } patchRouterMethod() {// To support generator function types, Concat (['all']). ForEach (method => {this[method] =... Args) = > {/ / spliteAndResolveRouterParams mainly to split the router. The routing rules in js, it split into general middleware and middleware part of the controller to generate, See below source const splited = spliteAndResolveRouterParams ({args, app: this app}); args = splited.prefix.concat(splited.middlewares); return super[method](... args); }; }); } / / return the prefix and middleware part of each routing rules in the router function spliteAndResolveRouterParams ({args, app} {the let the prefix. let middlewares; if (args.length >= 3 && (is.string(args[1]) || is.regExp(args[1]))) { // app.get(name, url, [...middleware], Controller) prefix = args. Slice (0, 2); middlewares = args.slice(2); } else {// app.get(URL, [...middleware], controller) prefix = args. Slice (0, 1); middlewares = args.slice(1); } // The controller section is definitely the last const controller = middlewares.pop(); // Get ('/async',...); // get('/async',... AsyncMiddlewares, 'subController. SubHome. Async1') / / writing 2: app. Get ('/async,... AsyncMiddlewares, subController. SubHome. Async1) / / in the end from the app. Get real controller middleware on the controller, Middlewares. Push (resolveController(controller, app)); return { prefix, middlewares }; }Copy the code

conclusion

The above is my summary of the implementation of most of the source code of egg-core. The debug code and timing runtime recording code in the source code have been deleted. The code about app life cycle management has little to do with the loading logic of loadUnits. So I didn’t talk about it. EggCore consists of an EggLoader (plugin, Config, extend, Service, Middleware, Controller, router, etc.) that must be loaded sequentially. Dependencies exist, for example:

  • The application middleware configuration is used when the middleware is loaded
  • The controller configuration is used when loading the Router
  • Config, extend, Service, Middleware, and Controller all depend on plugin to load, and the plugin directory is accessed through plugin configuration
  • Service, middleware, Controller, router must also be loaded depending on extend (extending app) because app is executed as an argument if exports is a function

EggCore is a basic framework. The most important thing about EggCore is that it needs to follow certain constraints and conventions to ensure a consistent code style. It also provides plug-in and framework mechanisms to reuse the same business logic

reference

  • Egg-core source code analysis
  • Agg – the core source code
  • An egg source
  • Egg Official Document