学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

前言

这是学习源码整体架构系列第七篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。本篇文章学习的是实际仓库的代码。

学习源码整体架构系列文章如下:

1.学习 jQuery 源码整体架构,打造属于自己的 js 类库
2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库
3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库
4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK
5.学习 vuex 源码整体架构,打造属于自己的状态管理库
6.学习 axios 源码整体架构,打造属于自己的请求库

感兴趣的读者可以点击阅读。
其他源码计划中的有:expressvue-rotuerredux、  react-redux 等源码,不知何时能写完(哭泣),欢迎持续关注我(若川)。

源码类文章,一般阅读量不高。已经有能力看懂的,自己就看了。不想看,不敢看的就不会去看源码。
所以我的文章,尽量写得让想看源码又不知道怎么看的读者能看懂。

如果你简历上一不小心写了熟悉koa,面试官大概率会问:

1、koa洋葱模型怎么实现的。
2、如果中间件中的next()方法报错了怎么办。
3、co的原理是怎样的。
等等问题

导读
文章通过例子调试koa,梳理koa的主流程,来理解koa-compose洋葱模型原理和co库的原理,相信看完一定会有所收获。

本文目录

本文学习的koa版本是v2.11.0。克隆的官方仓库的master分支。截至目前(2020年3月11日),最新一次commit2020-01-04 07:41 Olle Jonsson eda27608build: Drop unused Travis sudo: false directive (#1416)

本文仓库在这里若川的 koa-analysis github 仓库 https://github.com/lxchuan12/koa-analysis。求个star呀。

本文阅读最佳方式

star一下我的仓库,再把它git clone https://github.com/lxchuan12/koa-analysis.git克隆下来。不用管你是否用过nodejs。会一点点promise、generator、async、await等知识即可看懂。如果一点点也不会,可以边看阮一峰老师的《ES6标准入门》相关章节。跟着文章节奏调试和示例代码调试,动手调试(用vscode或者chrome)印象更加深刻。文章长段代码不用细看,可以调试时再细看。看这类源码文章百遍,可能不如自己多调试几遍。也欢迎加我微信交流lxchuan12

# 克隆我的这个仓库
git clone https://github.com/lxchuan12/koa-analysis.git
# chrome 调试:
# 全局安装 http-server
npm i -g http-server
hs koa/examples/
# 可以指定端口 -p 3001
# hs -p 3001 koa/examples/
# 浏览器中打开
# 然后在浏览器中打开localhost:8080,开心的把代码调试起来

这里把这个examples文件夹做个简单介绍。

  • middleware文件夹是用来vscode调试整体流程的。

  • simpleKoa 文件夹是koa简化版,为了调试koa-compose洋葱模型如何串联起来各个中间件的。

  • koa-convert文件夹是用来调试koa-convertco源码的。

  • co-generator文件夹是模拟实现co的示例代码。

vscode 调试 koa 源码方法

之前,我在知乎回答了一个问题一年内的前端看不懂前端框架源码怎么办?推荐了一些资料,阅读量还不错,大家有兴趣可以看看。主要有四点:

1.借助调试
2.搜索查阅相关高赞文章
3.把不懂的地方记录下来,查阅相关文档
4.总结

看源码,调试很重要,所以我详细写下 koa 源码调试方法,帮助一些可能不知道如何调试的读者。

# 我已经克隆到我的koa-analysis仓库了
git clone https://github.com/koajs/koa.git
// package.json
{"name": "koa","version": "2.11.0","description": "Koa web app framework","main": "lib/application.js",
}

克隆源码后,看package.json找到main,就知道入口文件是lib/application.js了。

大概看完项目结构后发现没有examples文件夹(一般项目都会有这个文件夹,告知用户如何使用该项目),这时仔细看README.md。如果看英文README.md有些吃力,会发现在Community标题下有一个中文文档 v2.x。同时也有一个examples仓库。

# 我已经克隆下来到我的仓库了
git clone https://github.com/koajs/examples.git

这时再开心的把examples克隆到自己电脑。可以安装好依赖,逐个研究学习下这里的例子,然后可能就一不小心掌握了koa的基本用法。当然,我这里不详细写这一块了,我是自己手写一些例子来调试。

继续看文档会发现使用指南讲述编写中间件

使用文档中的中间件koa-compose例子来调试

学习 koa-compose 前,先看两张图。

洋葱模型示意图
洋葱模型中间件示意图

koa中,请求响应都放在中间件的第一个参数context对象中了。

再引用Koa中文文档中的一段:

如果您是前端开发人员,您可以将 next(); 之前的任意代码视为“捕获”阶段,这个简易的 gif 说明了 async 函数如何使我们能够恰当地利用堆栈流来实现请求和响应流:

中间件gif图
  1. 创建一个跟踪响应时间的日期

  2. 等待下一个中间件的控制

  3. 创建另一个日期跟踪持续时间

  4. 等待下一个中间件的控制

  5. 将响应主体设置为“Hello World”

  6. 计算持续时间

  7. 输出日志行

  8. 计算响应时间

  9. 设置 X-Response-Time 头字段

  10. 交给 Koa 处理响应

读者们看完这个gif图,也可以思考下如何实现的。根据表现,可以猜测是next是一个函数,而且返回的可能是一个promise,被await调用。

看到这个gif图,我把之前写的examples/koa-compose的调试方法含泪删除了。默默写上gif图上的这些代码,想着这个读者们更容易读懂。我把这段代码写在这里 koa/examples/middleware/app.js便于调试。

在项目路径下配置新建.vscode/launch.json文件,program配置为自己写的koa/examples/middleware/app.js文件。

.vscode/launch.json 代码,点击这里展开/收缩,可以复制

F5键开始调试,调试时先走主流程,必要的地方打上断点,不用一开始就关心细枝末节。

断点调试要领:
赋值语句可以一步跳过,看返回值即可,后续详细再看。
函数执行需要断点跟着看,也可以结合注释和上下文倒推这个函数做了什么。

上述比较啰嗦的写了一堆调试方法。主要是想着授人予鱼不如授人予渔,这样换成其他源码也会调试了。

简单说下chrome调试nodejschrome浏览器打开chrome://inspect,点击配置**configure...**配置127.0.0.1:端口号(端口号在Vscode 调试控制台显示了)。
更多可以查看English Debugging Guide
中文调试指南
喜欢看视频的读者也可以看慕课网这个视频node.js调试入门,讲得还是比较详细的。
不过我感觉在chrome调试nodejs项目体验不是很好(可能是我方式不对),所以我大部分具体的代码时都放在html文件script形式,在chrome调试了。

先看看 new Koa() 结果app是什么

看源码我习惯性看它的实例对象结构,一般所有属性和方法都放在实例对象上了,而且会通过原型链查找形式查找最顶端的属性和方法。

koa/examples/middleware/app.js文件调试时,先看下执行new Koa()之后,app是什么,有个初步印象。

// 文件 koa/examples/middleware/app.js
const Koa = require('../../lib/application');// const Koa = require('koa');
// 这里打个断点
const app = new Koa();
// x-response-time// 这里打个断点
app.use(async (ctx, next) => {});

在调试控制台ctrl + 反引号键(一般在Tab上方的按键)唤起,输入app,按enter键打印app。会有一张这样的图。

koa 实例对象调试图

VScode也有一个代码调试神器插件Debug Visualizer

安装好后插件后,按ctrl + shift + p,输入Open a new Debug Visualizer View,来使用,输入app,显示是这样的。

koa 实例对象可视化简版

不过目前体验来看,相对还比较鸡肋,只能显示一级,而且只能显示对象,相信以后会更好。更多玩法可以查看它的文档。

我把koa实例对象比较完整的用xmind画出来了,大概看看就好,有个初步印象。

koa 实例对象

接着,我们可以看下app 实例、context、request、request的官方文档。

app 实例、context、request、request 官方API文档

  • index API | context API | request API | response API

可以真正使用的时候再去仔细看文档。

koa 主流程梳理简化

通过F5启动调试(直接跳到下一个断点处)F10单步跳过F11单步调试等,配合重要的地方断点,调试完整体代码,其实比较容易整理出如下主流程的代码。

class Emitter{// node 内置模块constructor(){}
}
class Koa extends Emitter{constructor(options){super();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 = () => 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);
}

重点就在listen函数里的compose这个函数,接下来我们就详细来欣赏下这个函数。

koa-compose 源码(洋葱模型实现)

通过app.use() 添加了若干函数,但是要把它们串起来执行呀。像上文的gif图一样。

compose函数,传入一个数组,返回一个函数。对入参是不是数组和校验数组每一项是不是函数。

function compose (middleware) {if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')for (const fn of middleware) {if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')}//  传入对象 context 返回Promisereturn function (context, next) {// last called middleware #let index = -1return dispatch(0)function dispatch (i) {if (i <= index) return Promise.reject(new Error('next() called multiple times'))index = ilet fn = middleware[i]if (i === middleware.length) fn = nextif (!fn) return Promise.resolve()try {return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));} catch (err) {return Promise.reject(err)}}}
}

把简化的代码和koa-compose代码写在了一个文件中。koa/examples/simpleKoa/koa-compose.js

hs koa/examples/
# 然后可以打开localhost:8080/simpleKoa,开心的把代码调试起来

不过这样好像还是有点麻烦,我还把这些代码放在codepen https://codepen.io/lxchuan12/pen/wvarPEb中,直接可以在线调试啦。是不是觉得很贴心^_^,自己多调试几遍便于消化理解。

你会发现compose就是类似这样的结构(移除一些判断)。

// 这样就可能更好理解了。
// 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);

也就是说koa-compose返回的是一个PromisePromise中取出第一个函数(app.use添加的中间件),传入context和第一个next函数来执行。
第一个next函数里也是返回的是一个PromisePromise中取出第二个函数(app.use添加的中间件),传入context和第二个next函数来执行。
第二个next函数里也是返回的是一个PromisePromise中取出第三个函数(app.use添加的中间件),传入context和第三个next函数来执行。
第三个...
以此类推。最后一个中间件中有调用next函数,则返回Promise.resolve。如果没有,则不执行next函数。这样就把所有中间件串联起来了。这也就是我们常说的洋葱模型。

不得不说非常惊艳,“玩还是大神会玩”

这种把函数存储下来的方式,在很多源码中都有看到。比如lodash源码的惰性求值,vuex也是把action等函数存储下,最后才去调用。

搞懂了koa-compose 洋葱模型实现的代码,其他代码就不在话下了。

错误处理

中文文档 错误处理

仔细看文档,文档中写了三种捕获错误的方式。

  • ctx.onerror 中间件中的错误捕获

  • app.on('error', (err) => {}) 最外层实例事件监听形式 也可以看看例子koajs/examples/errors/app.js 文件

  • app.onerror = (err) => {} 重写onerror自定义形式 也可以看测试用例 onerror

// application.js 文件
class Application extends Emitter {// 代码有简化组合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) {// 代码省略// ...}
}

ctx.onerror

lib/context.js文件中,有一个函数onerror,而且有这么一行代码this.app.emit('error', err, this)

module.exports = {onerror(){// delegate// app 是在new Koa() 实例this.app.emit('error', err, this);}
}
app.use(async (ctx, next) => {try {await next();} catch (err) {err.status = err.statusCode || err.status || 500;throw err;}
});

try catch 错误或被fnMiddleware(ctx).then(handleResponse).catch(onerror);,这里的onerrorctx.onerror
ctx.onerror函数中又调用了this.app.emit('error', err, this),所以在最外围app.on('error',err => {})可以捕获中间件链中的错误。因为koa继承自events模块,所以有'emit'和on等方法)

koa2 和 koa1 的简单对比

中文文档中描述了 koa2 和 koa1 的区别

koa1中主要是generator函数。koa2中会自动转换generator函数。

// Koa 将转换
app.use(function *(next) {const start = Date.now();yield next;const ms = Date.now() - start;console.log(`${this.method} ${this.url} - ${ms}ms`);
});

koa-convert 源码

vscode/launch.json文件,找到这个program字段,修改为"program": "${workspaceFolder}/koa/examples/koa-convert/app.js"

通过F5启动调试(直接跳到下一个断点处)F10单步跳过F11单步调试调试走一遍流程。重要地方断点调试。

app.use时有一层判断,是否是generator函数,如果是则用koa-convert暴露的方法convert来转换重新赋值,再存入middleware,后续再使用。

class Koa extends Emitter{use(fn) {if (typeof fn !== '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 || '-');this.middleware.push(fn);return this;}
}

koa-convert源码挺多,核心代码其实是这样的。

function convert(){return function (ctx, next) {return co.call(ctx, mw.call(ctx, createGenerator(next)))}function * createGenerator (next) {return yield next()}
}

最后还是通过co来转换的。所以接下来看co的源码。

co 源码

tj大神写的co 仓库

本小节的示例代码都在这个文件夹koa/examples/co-generator中,hs koa/example,可以自行打开https://localhost:8080/co-generator调试查看。

co源码前,先看几段简单代码。

// 写一个请求简版请求
function request(ms= 1000) {return new Promise((resolve) => {setTimeout(() => {resolve({name: '若川'});}, ms);});
}
// 获取generator的值
function* generatorFunc(){const res = yield request();console.log(res, 'generatorFunc-res');
}
generatorFunc(); // 报告,我不会输出你想要的结果的

简单来说co,就是把generator自动执行,再返回一个promisegenerator函数这玩意它不自动执行呀,还要一步步调用next(),也就是叫它走一步才走一步

所以有了async、await函数。

// await 函数 自动执行
async function asyncFunc(){const res = await request();console.log(res, 'asyncFunc-res await 函数 自动执行');
}
asyncFunc(); // 输出结果

也就是说co需要做的事情,是让generatorasync、await函数一样自动执行。

模拟实现简版 co(第一版)

这时,我们来模拟实现第一版的co。根据generator的特性,其实容易写出如下代码。

// 获取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);
// 输出了想要的结果
// {name: "若川"}"generatorFunc-res"

模拟实现简版 co(第二版)

但是实际上,不会上面那么简单的。有可能是多个yield和传参数的情况。传参可以通过这如下两行代码来解决。

const args = Array.prototype.slice.call(arguments, 1);
gen = gen.apply(ctx, args);

两个yield,我大不了重新调用一下promise.then,搞定。

// 多个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, ' 哎呀,我真的是后缀');

模拟实现简版 co(第三版)

问题是肯定不止两次,无限次的yield的呢,这时肯定要把重复的封装起来。而且返回是promise,这就实现了如下版本的代码。

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, ' 哎呀,我真的是后缀');

但第三版的模拟实现简版co中,还没有考虑报错和一些参数合法的情况。

最终来看下co源码

这时来看看co的源码,报错和错误的情况,错误时调用reject,是不是就好理解了一些呢。

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/180return new Promise(function(resolve, reject) {// 把参数传递给gen函数并执行if (typeof gen === 'function') gen = gen.apply(ctx, args);// 如果不是函数 直接返回if (!gen || typeof gen.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*/// 反复执行调用自己function next(ret) {// 检查当前是否为 Generator 函数的最后一步,如果是就返回if (ret.done) return resolve(ret.value);// 确保返回值是promise对象。var value = toPromise.call(ctx, ret.value);// 使用 then 方法,为返回值加上回调函数,然后通过 onFulfilled 函数再次调用 next 函数。if (value && isPromise(value)) return value.then(onFulfilled, onRejected);// 在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为 rejected,从而终止执行。return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '+ 'but the following object was passed: "' + String(ret.value) + '"'));}});
}

koa 和 express 简单对比

中文文档 koa 和 express 对比

文档里写的挺全面的。简单来说koa2语法更先进,更容易深度定制(egg.jsthink.js、底层框架都是koa)。

总结

文章通过授人予鱼不如授人予鱼的方式,告知如何调试源码,看完了koa-compose洋葱模型实现,koa-convertco等源码。

koa-compose是将app.use添加到middleware数组中的中间件(函数),通过使用Promise串联起来,next()返回的是一个promise

koa-convert 判断app.use传入的函数是否是generator函数,如果是则用koa-convert来转换,最终还是调用的co来转换。

co源码实现原理:其实就是通过不断的调用generator函数的next()函数,来达到自动执行generator函数的效果(类似async、await函数的自动自行)。

koa框架总结:主要就是四个核心概念,洋葱模型(把中间件串联起来),http请求上下文(context)、http请求对象、http响应对象。

本文仓库在这里若川的 koa-analysis github 仓库 https://github.com/lxchuan12/koa-analysis。求个star呀。

git clone https://github.com/lxchuan12/koa-analysis.git

再强烈建议下按照本文阅读最佳方式,克隆代码下来,动手调试代码学习更加深刻

如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出,也欢迎加我微信交流lxchuan12。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持,万分感谢。

解答下开头的提问

仅供参考

1、koa洋葱模型怎么实现的。

可以参考上文整理的简版koa-compose作答。

// 这样就可能更好理解了。
// 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);

答:app.use() 把中间件函数存储在middleware数组中,最终会调用koa-compose导出的函数compose返回一个promise,中间函数的第一个参数ctx是包含响应和请求的一个对象,会不断传递给下一个中间件。next是一个函数,返回的是一个promise

2、如果中间件中的next()方法报错了怎么办。

可参考上文整理的错误处理作答。

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) {// 代码省略// ...}

答:中间件链错误会由ctx.onerror捕获,该函数中会调用this.app.emit('error', err, this)(因为koa继承自events模块,所以有'emit'和on等方法),可以使用app.on('error', (err) => {}),或者app.onerror = (err) => {}进行捕获。

3、co的原理是怎样的。
答:co的原理是通过不断调用generator函数的next方法来达到自动执行generator函数的,类似async、await函数自动执行。

答完,面试官可能觉得小伙子还是蛮懂koa的啊。当然也可能继续追问,直到答不出...

还能做些什么 ?

学完了整体流程,koa-composekoa-convertco的源码。

还能仔细看看看http请求上下文(context)、http请求对象、http响应对象的具体实现。

还能根据我文章说的调试方式调试koa 组织中的各种中间件,比如koa-bodyparser, koa-routerkoa-jwtkoa-sessionkoa-cors等等。

还能把examples仓库克隆下来,我的这个仓库已经克隆了,挨个调试学习下源码。

web框架有很多,比如Express.jsKoa.jsEgg.jsNest.jsNext.jsFastify.jsHapi.jsRestify.jsLoopback.ioSails.jsMidway.js等等。

还能把这些框架的优势劣势、设计思想等学习下。

还能继续学习HTTP协议、TCP/IP协议网络相关,虽然不属于koa的知识,但需深入学习掌握。

学无止境~~~

历史非技术精选文章

如何制定有价值的目标
若川的2019年度总结,波澜不惊
高考七年后、工作三年后的感悟
工作一年后,我有些感悟(写于2017年)
知乎问答:你写过什么自认为惊艳的诗?

欢迎加微信交流 微信公众号

作者:常以若川为名混迹于江湖。前端路上 | PPT 爱好者 | 所知甚少,唯善学。博客:https://lxchuan12.cn,阅读体验可能更好些。

若川视野

主要发布前端 | PPT | 生活 | 效率相关的文章,长按扫码关注。欢迎加我微信lxchuan12(注明来源,基本来者不拒),拉您进【前端视野交流群】,长期交流学习~

小提醒:若川视野公众号原创文章合集在菜单栏中间【原创精选】按钮,欢迎点击阅读。

另外回复 pdf 可以获取前端优质书籍pdf。

由于公众号限制外链,点击阅读原文,或许阅读体验更佳,觉得文章不错,可以点个在看呀^_^另外欢迎留言交流~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/274405.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

公网对讲机修改对讲机程序_更少的对讲机,对讲机-更多专心,专心

公网对讲机修改对讲机程序重点 (Top highlight)I often like to put a stick into the bike wheel of the UX industry as it’s strolling along feeling proud of itself. I believe — strongly — that as designers we should primarily be doers not talkers.我经常喜欢在…

若川知乎问答:2年前端经验,做的项目没什么技术含量,怎么办?

知乎问答&#xff1a;做了两年前端开发&#xff0c;平时就是拿 Vue 写写页面和组件&#xff0c;简历的项目经历应该怎么写得好看&#xff1f;以下是我的回答&#xff0c;阅读量5000&#xff0c;所以发布到公众号申明原创。题主说的2年经验做的东西没什么技术含量&#xff0c;应…

ui设计基础_我不知道的UI设计的9个重要基础

ui设计基础重点 (Top highlight)After listening to Craig Federighi’s talk on how to be a better software engineer I was sold on the idea that it is super important for a software engineer to learn the basic principles of software design.听了克雷格费德里希(C…

C# 多线程控制 通讯 和切换

一.多线程的概念   Windows是一个多任务的系统&#xff0c;如果你使用的是windows 2000及其以上版本&#xff0c;你可以通过任务管理器查看当前系统运行的程序和进程。什么是进程呢&#xff1f;当一个程序开始运行时&#xff0c;它就是一个进程&#xff0c;进程所指包括运行中…

vue路由匹配实现包容性_包容性设计:面向老年用户的数字平等

vue路由匹配实现包容性In Covid world, a lot of older users are getting online for the first time or using technology more than they previously had. For some, help may be needed.在Covid世界中&#xff0c;许多年长用户首次上网或使用的技术比以前更多。 对于某些人…

IPhone开发 用子类搞定不同的设备(iphone和ipad)

用子类搞定不同的设备 因为要判断我们的程序正运行在哪个设备上&#xff0c;所以&#xff0c;我们的代码有些混乱了&#xff0c;IF来ELSE去的&#xff0c;记住&#xff0c;将来你花在维护代码上的时间要比花在写代码上的时间多&#xff0c;如果你的项目比较大&#xff0c;且IF语…

见证开户_见证中的发现

见证开户Each time we pick up a new video game, we’re faced with the same dilemma: “How do I play this game?” Most games now feature tutorials, which can range from the innocuous — gently introducing each mechanic at a time through natural gameplay — …

facebook有哪些信息_关于Facebook表情表情符号的所有信息

facebook有哪些信息Ever since worldwide lockdown and restriction on travel have been imposed, platforms like #Facebook, #Instagram, #Zoom, #GoogleDuo, & #Whatsapp have become more important than ever to connect with your loved ones (apart from the sourc…

M2总结报告

团队成员 李嘉良 http://home.cnblogs.com/u/daisuke/ 王熹 http://home.cnblogs.com/u/vvnx/ 王冬 http://home.cnblogs.com/u/darewin/ 王泓洋 http://home.cnblogs.com/u/fiverice/ 刘明 http://home.cnblogs.com/u/liumingbuaa/ 由之望 http://www.cnbl…

react动画库_React 2020动画库

react动画库Animations are important in instances like page transitions, scroll events, entering and exiting components, and events that the user should be alerted to.动画在诸如页面过渡&#xff0c;滚动事件&#xff0c;进入和退出组件以及应提醒用户的事件之类的…

线框模型_进行计划之前:线框和模型

线框模型Before we start developing something, we need a plan about what we’re doing and what is the expected result from the project. Same as developing a website, we need to create a mockup before we start developing (coding) because it will cost so much…

工作经验教训_在设计工作五年后获得的经验教训

工作经验教训This June it has been five years since I graduated from college. Since then I’ve been working as a UX designer for a lot of different companies, including a start-up, an application developer, and two consultancy firms.我从大学毕业已经五年了&a…

中文排版规则_非设计师的5条排版规则

中文排版规则01仅以一种字体开始 (01 Start with only one font) The first tip for non-designers dealing with typography is simple and will make your life much easier: Stop combining different fonts you like individually and try using only one font in your fut…

基本响应性的Web设计测试工具

在重新设计页面的过程中。要使页面完全响应的设计&#xff08;这意味着它会重新调整大小根据浏览器的尺寸和方向&#xff09;。如iPhone和iPad的移动电话和平板电脑我碰到了一些非常方便的响应设计工具&#xff0c;帮我测试网站在不同的屏幕响应。下面的这些响应的网页设计工具…

ux设计_声音建议:设计UX声音的快速指南

ux设计Mating calls, warning grunts, and supportive coos are some of the sounds heard throughout the animal kingdom. All species use finely-tuned noises to communicate to one another and inform others of an action or behavior. We humans aren’t all that dif…

css3高级和低级样式属性先后顺序

写css hack 时&#xff0c;由于hack主要针对的是个别浏览器&#xff0c;hack的书写顺序应当是从一般到特殊的写法。 如&#xff1a; .box { width:200px; height:200px; position:fixed; left:0; top:0; _position:absolute; } 如果颠倒顺序&#xff0c;从特殊到一般&#xff0…

sans serif_Sans和Serif相遇可爱

sans serifI first noticed it in this tweet. Exciting upcoming product and snazzy motion work aside, “What a fascinating logotype!”, I exclaimed!我在此推文中首先注意到了它。 我惊呼即将推出的激动人心的产品和令人眼花&#xff0c;乱的动作&#xff0c;“多么迷人…

[ckeditor系列]ckeditor 自己写的一个简单的image上传js 运用iframe的ajax上传

ckeditor最近修改一个上传的&#xff0c;原来的Image的上传插件功能很多&#xff0c;但是自己用&#xff0c;没有必要&#xff0c;就进行了修改&#xff0c;后来就改成了目前的样子&#xff0c;根据_samples/api_dialog.html 进行了修改&#xff0c;把页面里面的调用都进行了修…

sql 避免除0错误_设计简历时避免这3个常见的UX错误

sql 避免除0错误重点 (Top highlight)Having a great looking resume on hand is very important when you’re looking for a job. It is your ticket to land the interview that will get you one step closer to that one job you’ve been dreaming of.在找工作时&#xf…

一个网站自动化测试程序的设计与实现

CSDN博客不再经常更新&#xff0c;更多优质文章请来 粉丝联盟网 FansUnion.cn! (FansUnion) 代码 下载地址&#xff1a;http://download.csdn.net/detail/fansunion/5018357(免积分) 代码亮点&#xff1a;可读性很好&#xff0c;注释详尽 背景 工作中&#xff0c;在维护一…