學習 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.pswp.cn/news/274405.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/274405.shtml
英文地址,請注明出處:http://en.pswp.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.我經常喜歡在…

spring配置文件-------通配符

<!-- 這里一定要注意是使用spring的mappingLocations屬性進行通配的 --> <property name"mappingLocations"> <list> <value>classpath:/com/model/domain/*.hbm.xml</value> </list> </proper…

若川知乎問答: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…

Ubuntu下修改file descriptor

要修改Ubuntu下的file descriptor的話&#xff0c;請參照一下步驟。&#xff08;1&#xff09;修改limits.conf  $sudo vi /etc/security/limits.conf  增加一行  *  -  nofile  10000&#xff08;2&#xff09;修改 common-session  $ sudo vi/etc/pam.d/common…

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 — …

使用JXL組件操作Excel和導出文件

使用JXL組件操作Excel和導出文件 原文鏈接&#xff1a;http://tianweili.github.io/blog/2015/01/29/use-jxl-produce-excel/ 前言&#xff1a;這段時間參與的項目要求做幾張Excel報表&#xff0c;由于項目框架使用了jxl組件&#xff0c;所以把jxl組件的詳細用法歸納總結一下。…

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;進入和退出組件以及應提醒用戶的事件之類的…

Weather

public class WeatherModel { #region 定義成員變量 private string _temperature ""; private string _weather ""; private string _wind ""; private string _city ""; private …

線框模型_進行計劃之前:線框和模型

線框模型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…

撰寫論文時word使用技巧(轉)

------------------------------------- 1. Word2007 的表格自定義格式額度功能是很實用的&#xff0c;比如論文中需要經常插入表格的話&#xff0c; 可以在“表格設計”那里“修改表格樣式”一次性把默認的表格樣式設置為三線表&#xff0c;這樣&#xff0c; 你以后每次插入的…

工作經驗教訓_在設計工作五年后獲得的經驗教訓

工作經驗教訓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…

Wayland 源碼解析之代碼結構

來源&#xff1a;http://blog.csdn.net/basilc/article/details/8074895 獲取、編譯 Wayland 及其依賴庫可參考 Wayland 官方網站的 Build 指南&#xff1a;http://wayland.freedesktop.org/building.html。 Wayland 實現的代碼組成可以分成以下四部分&#xff1a; 1. Wayland…

中文排版規則_非設計師的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;幫我測試網站在不同的屏幕響應。下面的這些響應的網頁設計工具…