preface
Hello, I am
If sichuan. This is a
Learn the whole source architecture series
Article 7. Overall structure this word seems to be a little big, let’s just consider the overall structure of the source code, is mainly to learn the overall structure of the code, do not delve into the implementation of other specific functions that are not the main line. This article looks at the actual repository code.
Learn the overall structure of the source code series of articles as follows:
1. Learn the overall architecture of jQuery source code and build its own JS class library 2. Learn underscore source code architecture and build your own functional programming library 3. Learn the overall architecture of LoDash source code and build your own functional programming class library 4. Learn the overall architecture of Sentry source code and build its own front-end exception monitoring SDK 5. Learn the overall architecture of VUEX source code and build your own state management library 6. 7. Learn the overall architecture of KOA source code, and analyze the koA Onion model principle and CO principle 8. Learn the overall architecture of Redux source code and deeply understand the principles of Redux and its middleware
Interested readers can click to read. Other source code plans are: Express, VUe-Router, react-Redux and other source code, I do not know when can write (cry), welcome to continue to pay attention to me (Ruchuan).
Source code articles, general reading is not high. Those who have the ability to understand, read themselves. Don’t want to see, dare not see will not go to see the source code. So my article, as far as possible to write so that want to see the source code and do not know how to see the reader can understand.
If your resume accidentally says you are familiar with KOA, chances are the interviewer will ask:
1. How is koA Onion model implemented? 2. What if the next() method in the middleware fails? 3. What is the principle of CO? Problems, etc.
For example, koA is composed onion model and co library. For example, koA is composed onion model and CO library.
The VERSION of KOA studied in this article is V2.11.0. The master branch of the clone’s official repository. Olle Jonsson eda27608, build: Drop unused Travis sudo: False directive (# 1416).
In this paper, the warehouse here if the koa – analysis of sichuan making https://github.com/lxchuan12/koa-analysis warehouse. Ask for a star.
The best way to read this article
Star my warehouse, and then put it git clone https://github.com/lxchuan12/koa-analysis.git clone. It doesn’t matter if you’ve ever used NodeJS. A little promise, generator, async, await and other knowledge can be understood. If not, you can read ruan Yifeng’s introduction to ES6 standards. Follow the pace of this article and sample code debugging, hands-on debugging (using vscode or chrome) is more impressive. Article long section of code do not have to look at, you can debug and then look at. See this kind of source code article a hundred times, may be better than their own debugging several times. Also welcome to add my wechat exchange lxchuan12.
# Clone my warehouse
git clone https://github.com/lxchuan12/koa-analysis.git
# Chrome debug:
Install http-server globally
npm i -g http-server
hs koa/examples/
Port p 3001 can be specified
# hs -p 3001 koa/examples/
# Open it in a browser
Then open localhost:8080 in your browser and happily debug the code
Copy the code
Here is a brief introduction to the examples folder.
middleware
Folders are forvscode
Debug the whole process.simpleKoa
The folder iskoa
Simplified version for debuggingkoa-compose
How the Onion model connects middleware.koa-convert
Folders are used for debuggingkoa-convert
andco
The source code.co-generator
Folders are mock implementationsco
Sample code for.
Vscode debugging koa source method
Before, I answered a question in Zhihu how to do if the front end does not understand the source code of the front end framework within a year? Recommended some materials, reading volume is good, you can have a look. There are four main points:
2. Search and refer to relevant articles with high praise; 3. Record what you don’t understand and refer to relevant documents. conclusion
Looking at source code, debugging is very important, so I wrote a detailed koA source debugging method to help some readers who may not know how to debug.
# I have cloned it into my KOA-Analysis repository
git clone https://github.com/koajs/koa.git
Copy the code
// package.json
{
"name": "koa".
"version": "2.11.0".
"description": "Koa web app framework".
"main": "lib/application.js".
}
Copy the code
After cloning the source code, look at package.json and find main. The entry file is lib/application.js.
After looking at the project structure and realizing that there is no examples folder (common projects have folders that tell users how to use the project), take a closer look at readme.md. If you have trouble reading readme. md in English, you’ll find a Chinese document v2.x under the Community heading. There is also an examples warehouse.
# I have cloned it into my warehouse
git clone https://github.com/koajs/examples.git
Copy the code
Then happy to clone the examples to their own computer. You can install the dependencies, study the examples here one by one, and accidentally grasp the basic usage of KOA. Of course, I’m not going to write this in detail here, but I’ll write some examples by hand to debug.
Continue to read the documentation to find instructions for writing middleware.
Use middleware in documentationkoa-compose
To debug by example
To learn koa-compose, take a look at two diagrams.
In KOA, the request response is placed in the middleware’s first context object.
To quote again from the Koa Chinese document:
If you’re a front-end developer, you can put next(); While arbitrary code was considered the “capture” phase, this simple GIF illustrates how async functions enable us to properly leverage stack flows for request and response flows:
- Create a date that tracks response times
- Wait for the next middleware to take control
- Create another date-tracking duration
- Wait for the next middleware to take control
- Set the response body to “Hello World”
- Calculated duration
- Output log line
- Calculated response time
- Set up the
X-Response-Time
Header fields- The response is handed over to Koa
After you see this GIF, you can think about how to do it. Based on the representation, you can guess that next is a function and that it may return a promise called with await.
Seeing this GIF, I tearfully deleted the examples/koa-compose debugging method I wrote earlier. Silently write the code on the GIF, thinking it will be easier for readers to understand. I write this code here koa/examples/middleware/app. Js debug.
Under the project path configuration. New vscode/launch. The json file, the program configured to write their own koa/examples/middleware/app. Js file.
.vscode/launch.json code, click here to expand/contract, can be copied
{
"version": "0.2.0".
"configurations": [
{
"type": "node".
"request": "launch".
"name": "Start program".
"skipFiles": [
"<node_internals>/**"
].
"program": "${workspaceFolder}/koa/examples/middleware/app.js"
}
]
}
Copy the code
Press F5 key to start debugging, debugging first go to the main process, where necessary to make breakpoints, do not care about details at the beginning.
Breakpoint debugging tips: The assignment statement can be skipped in one step, just look at the return value, later details. Function execution requires a breakpoint to follow, or you can use comments and context to backtrack what the function did.
The above is a bunch of verbose debugging methods. Is mainly thinking about giving people to fish than giving people to fish, so changed into other source code will be debugged.
//inspect to configure **configure… ** configuration 127.0.0.1: port number (shown in Vscode debugging console). Node.js debuggingGuide debuggingGuide DebuggingGuide DebuggingGuide DebuggingGuide DebuggingGuide DebuggingGuide DebuggingGuide DebuggingGuide DebuggingGuide DebuggingGuide DebuggingGuide DebuggingGuide However, I feel that the nodeJS project experience in Chrome is not very good (probably my way is wrong), so I put most of the specific code in HTML file script and debug in Chrome.
First take a look atnew Koa()
The results ofapp
What is the
Look at the source code I used to look at its instance object structure, generally all attributes and methods are placed on the instance object, and will look up the top attributes and methods through the prototype chain lookup form.
With koa/examples/middleware/app. Js file debugging, see first perform the new koa (), what’s the app, a first impression.
/ / file koa/examples/middleware/app. Js
const Koa = require('.. /.. /lib/application');
// const Koa = require('koa');
// Make a break point here
const app = new Koa();
// x-response-time
// Make a break point here
app.use(async (ctx, next) => {
});
Copy the code
In the debug console, CTRL + the backquote key (usually the key above Tab) evoks, type app, and press Enter to print app. There’s going to be a picture that looks like this.
VScode also has a Debug Visualizer plugin for code debugging.
After the plugin is installed, press CTRL + Shift + P and type Open a new Debug Visualizer View to use it. Enter app and it will look like this.
But the current experience, relatively still compare chicken ribs, can only show a level, and can only show the object, I believe it will be better later. Check out its documentation for more gameplay.
I put the KOA instance object more complete with Xmind draw out, probably look good, have a preliminary impression.
Next, we can look at the app instance, context, Request, request’s official documentation.
App instance, Context, Request, and Request official API documents
- index API | context API | request API | response API
Look at the documentation when you can actually use it.
The main koA process is combed and simplified
Through F5 start debugging (jump directly to the next breakpoint), F10 single step skipping, F11 single step debugging, with important local breakpoints, debugging the whole code, in fact, it is easier to sort out the following main process of the code.
class Emitter{
// Node built-in module
constructor() {
}
}
class Koa extends Emitter{
constructor(options){
super(a);
options = options || {};
this.middleware = [];
this.context = {
method: 'GET'.
url: '/url'.
body: undefined.
set: function(key, val){
console.log('context.set', key, val);
},
};
}
use(fn){
this.middleware.push(fn);
return this;
}
listen(){
const fnMiddleware = compose(this.middleware);
const ctx = this.context;
const handleResponse = (a)= > respond(ctx);
const onerror = function(){
console.log('onerror');
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
}
function respond(ctx){
console.log('handleResponse');
console.log('response.end', ctx.body);
}
Copy the code
The focus is on the compose function in the listen function, which we’ll look at in more detail.
Koa-compose source code (Onion model implementation)
App.use () adds several functions, but executes them in a string. Like the GIF above.
Compose function, which passes in an array and returns a function. Check whether the input parameter is an array and check whether each item in the array is a function.
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array! ')
for (const fn of middleware) {
if (typeoffn ! = ='function') throw new TypeError('Middleware must be composed of functions! ')
}
// Passing the object context returns Promise
return function (context, next) {
// last called middleware #
let index = - 1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if(! fn)return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
Copy the code
Write the simplified code and koa-compose code in one file. koa/examples/simpleKoa/koa-compose.js
hs koa/examples/
Then you can open localhost:8080/simpleKoa and have fun debugging the code
Copy the code
But it seemed a little trouble, I also put the code in the codepen https://codepen.io/lxchuan12/pen/wvarPEb,. You can directly online debugging!!! Feel very sweet ^_^, their own debugging several times to facilitate digestion and understanding.
You’ll notice that compose is something like this (with some judgments removed).
// This will make sense.
// simpleKoaCompose
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
return Promise.resolve(
fn1(context, function next(){
return Promise.resolve(
fn2(context, function next(){
return Promise.resolve(
fn3(context, function next(){
return Promise.resolve();
})
)
})
)
})
);
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);
Copy the code
That is, koa-compose returns a Promise that takes the first function (middleware added by app.use) from the Promise and passes in the context and first next function to execute. The first next function also returns a Promise, which takes a second function (app.use added middleware) and passes in the context and a second next function to execute. The second next function also returns a Promise, which takes a third function (app.use added middleware) and passes in the context and a third next function to execute. The third… And so on. The last middleware call to the next function returns promise.resolve. If not, the next function is not executed. This links all the middleware together. This is what we call the Onion model.
I have to say it was amazing, “Play or god can play”.
This way of storing functions is seen in many source codes. For example, loDash’s lazy evaluation of the source code, Vuex also stores functions such as actions until they are called last.
Once you understand the koa-compose onion model implementation code, the rest of the code is not a problem.
Error handling
Error handling of Chinese documents
If you look closely at the documentation, there are three ways to catch errors.
ctx.onerror
Error capture in middlewareapp.on('error', (err) => {})
The outermost instance event listening form can also be seen in an exampleKoajs/examples/errors/app. Js fileapp.onerror = (err) => {}
rewriteonerror
Custom forms can also be viewedTest case onError
/ / application. Js file
class Application extends Emitter {
// The code has simplified combinations
listen(){
const fnMiddleware = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error'.this.onerror);
const onerror = err= > ctx.onerror(err);
fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
onerror(err) {
// code omitted
// ...
}
}
Copy the code
ctx.onerror
In the lib/context.js file, there is a function onerror and this.app.emit(‘error’, err, this) line.
module.exports = {
onerror(){
// delegate
// app is in the new Koa() instance
this.app.emit('error', err, this);
}
}
Copy the code
app.use(async (ctx, next) => {
try {
await next();
} catch (err) {
err.status = err.statusCode || err.status || 500;
throw err;
}
});
Copy the code
Try catch error or by fnMiddleware(CTX).then(handleResponse).catch(onError); Onerror and ctx. onError call this.app.emit(‘error’, err, this) so app.on(‘error’, Err => {}) can catch errors in the middleware chain. Since KOA inherits from the Events module, there are ’emit’ and on methods.)
A simple comparison of KOA2 and KOA1
The Difference between KOA2 and KOA1 is described in the Chinese documentation
Koa1 is mainly generator functions. Generator functions are converted automatically in KOA2.
// Koa will convert
app.use(function* (next) {
const start = Date.now();
yield next;
const ms = Date.now() - start;
console.log(`The ${this.method} The ${this.url} - ${ms}ms`);
});
Copy the code
Koa – convert the source code
In vscode/launch.json, find the program field and change it to “program”: “${workspaceFolder}/koa/examples/koa-convert/app.js”.
Start debugging with F5 (skip to the next breakpoint), skip F10, and step debugging with F11. Breakpoint debugging is important.
App. use has a layer of validation to determine if it is a generator or not, and if it is, use the koa-convert exposed convert method to reassign and store it in middleware for later use.
class Koa extends Emitter{
use(fn) {
if (typeoffn ! = ='function') throw new TypeError('middleware must be a function! ');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || The '-');
this.middleware.push(fn);
return this;
}
}
Copy the code
Koa-convert source quite a lot, the core code is actually like this.
function convert(){
return function (ctx, next) {
return co.call(ctx, mw.call(ctx, createGenerator(next)))
}
function * createGenerator (next) {
return yield next()
}
}
Copy the code
Finally, the conversion is done by CO. So next look at the source of CO.
Co source
Co warehouse written by TJ God
This section of the sample code in this folder koa/examples/co – generator, hs koa/example, was able to open the debug view at https://localhost:8080/co-generator.
Before looking at the co source code, let’s look at some simple code.
// Write a short version of a request
function request(ms= 1000) {
return new Promise((resolve) = > {
setTimeout((a)= > {
resolve({name: 'if the sichuan'});
}, ms);
});
}
Copy the code
// Get the value of generator
function* generatorFunc(){
const res = yield request();
console.log(res, 'generatorFunc-res');
}
generatorFunc(); // Report, I will not output the result you want
Copy the code
Simply put, CO is simply a generator that executes automatically and returns a promise. The generator function does not execute automatically. It calls next() step by step.
So we have async, await functions.
// await function is executed automatically
async function asyncFunc(){
const res = await request();
console.log(res, 'asyncfun-res await function automatically executed');
}
asyncFunc(); // Output the result
Copy the code
That is, all the CO needs to do is make the generator execute automatically like async and await functions.
Simulation Implementation Brief Version CO (first version)
At this point, let’s simulate implementing the first version of CO. Given the nature of the Generator, it is easy to write the following code.
// Get the value of generator
function* generatorFunc(){
const res = yield request();
console.log(res, 'generatorFunc-res');
}
function coSimple(gen){
gen = gen();
console.log(gen, 'gen');
const ret = gen.next();
const promise = ret.value;
promise.then(res= > {
gen.next(res);
});
}
coSimple(generatorFunc);
// Output the desired result
// {name: "generatorFunc "}"generatorFunc-res"
Copy the code
Simulation Implementation Brief Version CO (Second Edition)
But actually, it’s not that simple. Multiple yield and pass parameters are possible. This can be done with these two lines of code.
const args = Array.prototype.slice.call(arguments.1);
gen = gen.apply(ctx, args);
Copy the code
Two yields, I’ll just call promise again. Then, done.
// Multiple yeild
function* generatorFunc(suffix = ' '){
const res = yield request();
console.log(res, 'generatorFunc-res' + suffix);
const res2 = yield request();
console.log(res2, 'generatorFunc-res-2' + suffix);
}
function coSimple(gen){
const ctx = this;
const args = Array.prototype.slice.call(arguments.1);
gen = gen.apply(ctx, args);
console.log(gen, 'gen');
const ret = gen.next();
const promise = ret.value;
promise.then(res= > {
const ret = gen.next(res);
const promise = ret.value;
promise.then(res= > {
gen.next(res);
});
});
}
coSimple(generatorFunc, 'Gee, I really am a suffix.');
Copy the code
Simulation Implementation Brief Version CO (3rd Edition)
The problem is that there must be more than two, infinite yields, in which case you have to encapsulate the repetition. And the return is a promise, which implements the following version of the code.
function* generatorFunc(suffix = ' '){
const res = yield request();
console.log(res, 'generatorFunc-res' + suffix);
const res2 = yield request();
console.log(res2, 'generatorFunc-res-2' + suffix);
const res3 = yield request();
console.log(res3, 'generatorFunc-res-3' + suffix);
const res4 = yield request();
console.log(res4, 'generatorFunc-res-4' + suffix);
}
function coSimple(gen){
const ctx = this;
const args = Array.prototype.slice.call(arguments.1);
gen = gen.apply(ctx, args);
console.log(gen, 'gen');
return new Promise((resolve, reject) = > {
onFulfilled();
function onFulfilled(res){
const ret = gen.next(res);
next(ret);
}
function next(ret) {
const promise = ret.value;
promise && promise.then(onFulfilled);
}
});
}
coSimple(generatorFunc, 'Gee, I really am a suffix.');
Copy the code
However, the third version of the simulation implementation of the simple version of CO, has not considered the error and some parameters valid cases.
And finally, let’s look atco
The source code
At this point take a look at the co source code, error and error cases, error call reject, is not easy to understand some of the.
function co(gen) {
var ctx = this;
var args = slice.call(arguments.1)
// we wrap everything in a promise to avoid promise chaining,
// which leads to memory leak errors.
// see https://github.com/tj/co/issues/180
return new Promise(function(resolve, reject) {
// Pass the argument to the gen function and execute
if (typeof gen === 'function') gen = gen.apply(ctx, args);
// If the function does not return directly
if(! gen ||typeofgen.next ! = ='function') return resolve(gen);
onFulfilled();
/ * *
* @param {Mixed} res
* @return {Promise}
* @api private
* /
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
}
/ * *
* @param {Error} err
* @return {Promise}
* @api private
* /
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
/ * *
* Get the next value in the generator,
* return a promise.
*
* @param {Object} ret
* @return {Promise}
* @api private
* /
// call yourself repeatedly
function next(ret) {
// Check if the current is Generator and return if so
if (ret.done) return resolve(ret.value);
// Make sure the return value is a Promise object.
var value = toPromise.call(ctx, ret.value);
// Use the then method to add the callback function to the return value, and then call the next function again via the ondepressing function.
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
// Change the state of the Promise object to Rejected if the parameters do not meet the requirements (the parameters are not Thunk functions and Promise objects), thus terminating the execution.
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
Copy the code
Simple comparison between KOA and Express
Compare the Chinese document KOA and Express
It’s pretty thorough in the documentation. In short, KOA2 syntax is more advanced and easier to customize in depth (egg.js, think.js, and the underlying framework are all KOA).
conclusion
This article tells you how to debug the source code by giving people a fish is better than giving people a fish, and read the koa-compose onion model implementation, KOA-convert and CO source code.
Koa-compose is the middleware (function) that adds app.use to the middleware array, concatenated by using promises, and next() returns a Promise.
Koa-convert determines whether the function passed in by app.use is a generator. If so, koa-convert is used, and finally co is used to convert.
Co source code implementation principle: in fact, by constantly calling generator function next() function, to achieve the effect of automatic execution of generator function (similar to async, await function automatic).
Summary of KOA framework: there are four core concepts: Onion model (connecting middleware in series), HTTP request context, HTTP request object and HTTP response object.
In this paper, the warehouse here if the koa – analysis of sichuan making https://github.com/lxchuan12/koa-analysis warehouse. Ask for a star.
git clone https://github.com/lxchuan12/koa-analysis.git
Copy the code
I strongly suggest that the best way to read this article is to clone the code and debug the code to learn more deeply.
If you find anything wrong or can be improved, or if you don’t understand it clearly, you are welcome to comment and add lxchuan12 on wechat. In addition, I think it is well written and helpful to you. I can like it, comment on it and share it with you. It is also a kind of support for the author.
Answer the opening question
For reference only
1. How is koA Onion model implemented?
For the answer, please refer to the simple version of koa-compose.
// This will make sense.
// simpleKoaCompose
const [fn1, fn2, fn3] = this.middleware;
const fnMiddleware = function(context){
return Promise.resolve(
fn1(context, function next(){
return Promise.resolve(
fn2(context, function next(){
return Promise.resolve(
fn3(context, function next(){
return Promise.resolve();
})
)
})
)
})
);
};
fnMiddleware(ctx).then(handleResponse).catch(onerror);
Copy the code
A: App.use () stores middleware functions in the middleware array, which will eventually call the koa-compose export, which returns a promise. The first argument to the intermediate function, CTX, is an object containing the response and request, which will be passed to the next middleware. Next is a function that returns a promise.
2. What if the next() method in the middleware fails?
Refer to the error handling above for answers.
ctx.onerror = function {
this.app.emit('error', err, this);
};
listen(){
const fnMiddleware = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error'.this.onerror);
const onerror = err= > ctx.onerror(err);
fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
onerror(err) {
// code omitted
// ...
}
Copy the code
A: Middleware chain errors are caught by ctx. onError, which calls this.app.emit(‘error’, err, this). You can use the app. On (‘ error ‘(err) = > {}), or app. Onerror = (err) = > {} to capture.
3. What is the principle of CO? A: The principle of CO is to automatically execute generator functions by continuously calling next method of generator functions, similar to async and await functions.
After answering the question, the interviewer may think that the young man knows koA quite well. Of course, you may continue to ask questions until you can’t answer…
What else can be done?
Learned the overall process, koa-compose, KOA-convert and CO source code.
You can also take a closer look at the HTTP request context, HTTP request object, and HTTP response object implementations.
It can also debug various middleware in the KOA organization according to the debugging methods described in my article, such as KOA-BodyParser, KOA-Router, KOA-JWT, KOA-Session, KOA-CORS, etc.
Can also put examples warehouse clone down, I have cloned this warehouse, one by one debugging learning source code.
There are many web frameworks, such as express.js, koa.js, egg.js, Nest.js, next.js, fastify.js, hapi.js, restify.js, loopback.io, Sails.
You can also learn the advantages and disadvantages of these frameworks, design ideas and so on.
In addition, I can continue to learn HTTP protocol, TCP/IP protocol and network related knowledge, which is not koA, but requires in-depth study.
There is no end to learning
Recommended reading
Koa website | | koa warehouse koa group | | co warehouse zhihu @ Yao Dashuai koa2 Chinese document: may be currently on the market more sincerity koa2 source interpretation on zhihu @ zero small white: take you 10 minutes watching the koa source WeChat open community @ Dennis の : May be the most complete koA source code analysis guide IVWEB official account: KOA2 framework principle analysis and implementation of simple vue. Js author Berwin: simple KOA2 principle Ruan Yifeng teacher: CO function library meaning and usage
Another series
Can you emulate the call and apply methods of JS? Can you emulate the new operator of JS
about
Author: Often in the name of ruochuan mixed traces in rivers and lakes. The front road lovers | | PPT know little, only good study. Ruochuan’s blog, using Vuepress reconstruction, reading experience may be better dig column, welcome to follow ~ SegmentFault front view column, welcome to follow ~ Zhihu front view column, welcome to follow ~ Language finch front view column, new language Finch column, welcome to follow ~ github blog, Related source code and resources are put here, ask a star^_^~
Welcome to add wechat communication wechat official account
May be more interesting wechat public number, long press scan code attention. You are welcome to join ruoChuan12 in wechat for long-term communication and learning