')
351 |
352 | var layer = new Layer(path, {
353 | sensitive: this.caseSensitive, //敏感区分大小写 //默认为false
354 | strict: false, //严格
355 | end: false //结束
356 | }, fn);
357 |
358 | layer.route = undefined;
359 | this.stack.push(layer);
360 | }
361 |
362 | return this;
363 | }
364 |
365 | ```
366 |
367 | `router.use`的主要作用就是将从`app.use`中传递过来的函数,通过`Layer`实例化的处理,添加一些**处理错误**、**处理请求**的方法,以便后续调用处理。同时将传递过来的`path`,通过`path-to-regexp`模块把路径转为正则表达式(`this.regexp`),调用`this.regexp.exec(path)`,将参数提取出来。
368 |
369 | `Layer`代码较多,这里不贴代码了,可以参考[express/lib/router/layer.js](https://github.com/expressjs/express/blob/master/lib/router/layer.js)。
370 |
371 | ## 处理中间件。
372 |
373 | 处理中间件就是将放入`this,stack`的`new Layout([options],fn)`,拿出来依次执行。
374 |
375 | ```javascript
376 | proto.handle = function handle(req, res, out) {
377 | var self = this;
378 |
379 | debug('dispatching %s %s', req.method, req.url);
380 |
381 | var idx = 0;
382 | //获取协议与URL地址
383 | var protohost = getProtohost(req.url) || ''
384 | var removed = '';
385 | //是否添加斜杠
386 | var slashAdded = false;
387 | var paramcalled = {};
388 |
389 | //存储选项请求的选项
390 | //仅在选项请求时使用
391 | var options = [];
392 |
393 | // 中间件和路由
394 | var stack = self.stack;
395 | // 管理inter-router变量
396 | //req.params 请求参数
397 | var parentParams = req.params;
398 | var parentUrl = req.baseUrl || '';
399 | var done = restore(out, req, 'baseUrl', 'next', 'params');
400 |
401 | // 设置下一层
402 | req.next = next;
403 |
404 | // 对于选项请求,如果没有其他响应,则使用默认响应
405 | if (req.method === 'OPTIONS') {
406 | done = wrap(done, function(old, err) {
407 | if (err || options.length === 0) return old(err);
408 | sendOptionsResponse(res, options, old);
409 | });
410 | }
411 |
412 | // 设置基本的req值
413 | req.baseUrl = parentUrl;
414 | req.originalUrl = req.originalUrl || req.url;
415 |
416 | next();
417 |
418 | function next(err) {
419 | var layerError = err === 'route'
420 | ? null
421 | : err;
422 |
423 | //是否添加斜线 默认false
424 | if (slashAdded) {
425 | req.url = req.url.substr(1);
426 | slashAdded = false;
427 | }
428 |
429 | // 恢复改变req.url
430 | if (removed.length !== 0) {
431 | req.baseUrl = parentUrl;
432 | req.url = protohost + removed + req.url.substr(protohost.length);
433 | removed = '';
434 | }
435 |
436 | // 出口路由器信号
437 | if (layerError === 'router') {
438 | setImmediate(done, null)
439 | return
440 | }
441 |
442 | // 不再匹配图层
443 | if (idx >= stack.length) {
444 | setImmediate(done, layerError);
445 | return;
446 | }
447 |
448 | // 获取路径pathname
449 | var path = getPathname(req);
450 |
451 | if (path == null) {
452 | return done(layerError);
453 | }
454 |
455 | // 找到下一个匹配层
456 | var layer;
457 | var match;
458 | var route;
459 |
460 | while (match !== true && idx < stack.length) {
461 | layer = stack[idx++];
462 | //try layer.match(path) catch err
463 | //搜索 path matchLayer有两种状态一种是boolean,一种是string。
464 | match = matchLayer(layer, path);
465 | route = layer.route;
466 |
467 | if (typeof match !== 'boolean') {
468 | layerError = layerError || match;
469 | }
470 |
471 | if (match !== true) {
472 | continue;
473 | }
474 |
475 | if (!route) {
476 | //正常处理非路由处理程序
477 | continue;
478 | }
479 |
480 | if (layerError) {
481 | // routes do not match with a pending error
482 | match = false;
483 | continue;
484 | }
485 |
486 | var method = req.method;
487 | var has_method = route._handles_method(method);
488 |
489 | // build up automatic options response
490 | if (!has_method && method === 'OPTIONS') {
491 | appendMethods(options, route._options());
492 | }
493 |
494 | // don't even bother matching route
495 | if (!has_method && method !== 'HEAD') {
496 | match = false;
497 | continue;
498 | }
499 | }
500 |
501 | // no match
502 | if (match !== true) {
503 | return done(layerError);
504 | }
505 |
506 | //重新赋值router。
507 | if (route) {
508 | req.route = route;
509 | }
510 |
511 | // 合并参数
512 | req.params = self.mergeParams
513 | ? mergeParams(layer.params, parentParams)
514 | : layer.params;
515 |
516 | var layerPath = layer.path;
517 |
518 | // 处理参数
519 | self.process_params(layer, paramcalled, req, res, function (err) {
520 | if (err) {
521 | return next(layerError || err);
522 | }
523 |
524 | if (route) {
525 | return layer.handle_request(req, res, next);
526 | }
527 |
528 | // 处理req.url和layerPath,同时对layer中的请求error和handle_error加tryCatch处理。
529 | trim_prefix(layer, layerError, layerPath, path);
530 | });
531 | }
532 |
533 | ```
534 |
535 | 执行`proto.handle`中间件也就的`while`循环中的一些核心代码,每次调用`app.use`中的回调函数中的`next()`都会让`idx`加一,将`stack[idx++];`赋值给`layer`,调用一开始说到的`layer.handle_request`,然后调用`trim_prefix(layer, layerError, layerPath, path)`,添加一些报错处理。
536 |
537 | `trim_prefix`函数如下:
538 |
539 | ```javascript
540 |
541 | function trim_prefix(layer, layerError, layerPath, path) {
542 | if (layerPath.length !== 0) {
543 | // Validate path breaks on a path separator
544 | var c = path[layerPath.length]
545 | if (c && c !== '/' && c !== '.') return next(layerError)
546 |
547 | // //删除url中与路由匹配的部分
548 | // middleware (.use stuff) needs to have the path stripped
549 | debug('trim prefix (%s) from url %s', layerPath, req.url);
550 | removed = layerPath;
551 | req.url = protohost + req.url.substr(protohost.length + removed.length);
552 |
553 | // Ensure leading slash
554 | if (!protohost && req.url[0] !== '/') {
555 | req.url = '/' + req.url;
556 | slashAdded = true;
557 | }
558 |
559 | // 设置 base URL (no trailing slash)
560 | req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
561 | ? removed.substring(0, removed.length - 1)
562 | : removed);
563 | }
564 |
565 | debug('%s %s : %s', layer.name, layerPath, req.originalUrl);
566 |
567 | if (layerError) {
568 | layer.handle_error(layerError, req, res, next);
569 | } else {
570 | layer.handle_request(req, res, next);
571 | }
572 | }
573 | };
574 |
575 | ```
576 |
577 | ## 总结
578 |
579 | 以上就是通过`app.use`调用之后,一步步执行中间件函数`router.handle`。
580 |
581 | `next`核心代码很简单,但是需要考虑的场景却是很多,通过这次源码阅读,更能进一步的理解express的核心功能。
582 |
583 | 虽说平常做项目用到`express`框架很少,或者可以说基本不用,一般都是用`Koa`或者`Egg`,可以说基本上些规模的场景的项目用的都是`Egg`。
584 |
585 | 但是不可否认得是,`express`框架还是一款非常经典的框架。
586 |
587 |
588 | **以上代码纯属个人理解,如有不合适的地方,望在评论区留言。**
589 |
590 |
591 |
592 |
593 |
594 |
595 |
--------------------------------------------------------------------------------
/从零开始React服务器渲染(SSR)同构😏(基于Koa).md:
--------------------------------------------------------------------------------
1 | ## 前言
2 |
3 | 自前端框架(`React`,`Vue`,`Angelar`)出现以来,每个框架携带不同理念,分为三大阵营,以前使用`JQuery`的时代已经成为过去,以前每个页面就是一个`HTML`,引入相对应的`JS`、`CSS`,同时在`HTML`中书写`DOM`。正因为是这样,每次用户访问进来,由于`HTML`中有`DOM`的存在,给用户的感觉响应其实并不是很慢。
4 |
5 | 但是自从使用了框架之后,无论是多少个页面,就是单独一个单页面,即`SPA`。`HTML`中所有的`DOM`元素,必须在客户端下载完`js`之后,通过调用执行`React.render()`才能够进行渲染,所以就有了很多网站上,一进来很长时间的`loading`动画。
6 |
7 | 为了解决这一并不是很友好的问题,社区上提出了很多方案,例如`预渲染`、`SSR`、`同构`。
8 |
9 | 当然这篇文章主要讲述的是从零开始搭建一个**React服务器渲染同构**。
10 |
11 |
12 | 
13 |
14 | ## 选择方案
15 |
16 | ### 方案一 使用社区精选框架Next.js
17 |
18 | `Next.js` 是一个轻量级的 `React` 服务端渲染应用框架。有兴趣的可以去[`Next.js`](http://nextjs.frontendx.cn/)官网学习下。
19 |
20 | ### 方案二 同构
21 |
22 | 关于同构有两种方案:
23 |
24 | ### 通过babel转义node端代码和React代码后执行
25 |
26 | ```
27 | let app = express();
28 | app.get('/todo', (req, res) => {
29 | let html = renderToString(
30 |
31 |
32 |
33 | )
34 | res.send( indexPage(html) )
35 | }
36 | })
37 |
38 | ```
39 | 在这里有两个问题需要处理:
40 | * `Node`不支持前端的`import`语法,需要引入`babel`支持。
41 | * `Node`不能解析标签语法。
42 |
43 | 所以执行`Node`时,需要使用`babel`来进行转义,如果出现错误了,也无从查起,个人并不推荐这样做。
44 |
45 | 所以这里采用第二种方案
46 | ### webpack进行编译处理
47 |
48 | 使用`webpack`打包两份代码,一份用于`Node`进行服务器渲染,一份用于浏览器进行渲染。
49 |
50 | 下面具体详细说明下。
51 |
52 | ## 搭建Node服务器
53 |
54 | 由于使用习惯,经常使用`Egg`框架,而`Koa`是`Egg`的底层框架,因此,这里我们采用`Koa`框架进行服务搭建。
55 |
56 | 搭建最基本的一个`Node`服务。
57 | ```js
58 | const Koa = require('koa');
59 | const app = new Koa();
60 |
61 | app.listen(3000, () => {
62 | console.log("服务器已启动,请访问http://127.0.0.1:3000")
63 | });
64 | ```
65 |
66 | ## 配置webpack
67 | 众所周知,`React`代码需要经过打包编译才能执行的,而服务端和客户端运行的代码只有一部分相同,甚至有些代码根本不需要将代码打包,这时就需要将客户端代码和服务端运行的代码分开,也就有了两份`webpack`配置
68 |
69 | `webpack` 将同一份代码,通过不同的`webpack`配置,分别为`serverConfig`和`clientConfig`,打包为两份代码。
70 |
71 | ### serverConfig和clientConfig配置
72 |
73 | 通过[webpack](https://www.webpackjs.com/configuration/target/)文档我们可以知道,webpack不仅可以编译web端代码还可以编译其他内容。
74 |
75 |
76 | 
77 |
78 | 这里我们将`target`设为`node`。
79 |
80 | 配置入口文件和出口位置:
81 | ```js
82 | const serverConfig = {
83 | target: 'node',
84 | entry: {
85 | page1: './web/render/serverRouter.js',
86 | },
87 | resolve,
88 | output: {
89 | filename: '[name].js',
90 | path: path.resolve(__dirname, './app/build'),
91 | libraryTarget: 'commonjs'
92 | }
93 | }
94 |
95 | ```
96 | **注意⚠**
97 |
98 | 服务端配置需要配置`libraryTarget`,设置`commonjs`或者`umd`,用于服务端进行`require`引用,不然`require`值为`{}`。
99 |
100 | 在这里客户端和服务端配置没有什么区别,无需配置`target`(默认`web`环境),其他入门文件和输出文件不一致。
101 | ```js
102 | const clientConfig = {
103 | entry: {
104 | page1: './web/render/clientRouter.js'
105 | },
106 | output: {
107 | filename: '[name].js',
108 | path: path.resolve(__dirname, './public')
109 | }
110 | }
111 | ```
112 | ### 配置babel
113 | 由于打包的是`React`代码,因此还需要配置`babel`。
114 | 新建`.babelrc`文件。
115 |
116 | ```js
117 | {
118 | "presets": ["@babel/preset-react",
119 | ["@babel/preset-env",{
120 | "targets": {
121 | "browsers": [
122 | "ie >= 9",
123 | "ff >= 30",
124 | "chrome >= 34",
125 | "safari >= 7",
126 | "opera >= 23",
127 | "bb >= 10"
128 | ]
129 | }
130 | }]
131 | ],
132 | "plugins": [
133 | [
134 | "import",
135 | { "libraryName": "antd", "style": true }
136 | ]
137 | ]
138 | }
139 | ```
140 |
141 | 这份配置由服务端和客户端共用,用来处理`React`和转义为`ES5`和浏览器兼容问题。
142 | ### 处理服务端引用问题
143 |
144 | 服务端使用`CommonJS`规范,而且服务端代码也并不需要构建,因此,对于node_modules中的依赖并不需要打包,所以借助`webpack`第三方模块`webpack-node-externals`来进行处理,经过这样的处理,两份构建过的文件大小已经相差甚远了。
145 |
146 | ### 处理css
147 |
148 | 服务端和客户端的区别,可能就在于一个默认处理,一个需要将`CSS`单独提取出为一个文件,和处理`CSS`前缀。
149 |
150 | 服务端配置
151 | ```js
152 | {
153 | test: /\.(css|less)$/,
154 | use: [
155 | {
156 | loader: 'css-loader',
157 | options: {
158 | importLoaders: 1
159 | }
160 | },
161 | {
162 | loader: 'less-loader',
163 | }
164 | ]
165 | }
166 | ```
167 |
168 | 客户端配置
169 |
170 | ```js
171 | {
172 | test: /\.(css|less)$/,
173 | use: [
174 | {
175 | loader: MiniCssExtractPlugin.loader,
176 | },
177 | {
178 | loader: 'css-loader'
179 | },
180 | {
181 | loader: 'postcss-loader',
182 | options: {
183 | plugins: [
184 | require('precss'),
185 | require('autoprefixer')
186 | ],
187 | }
188 | },
189 | {
190 | loader: 'less-loader',
191 | options: {
192 | javascriptEnabled: true,
193 | // modifyVars: theme //antd默认主题样式
194 | }
195 | }
196 | ],
197 | }
198 | ```
199 |
200 | ## SSR 中客户端渲染与服务器端渲染路由代码的差异
201 |
202 | 实现 `React` 的 `SSR` 架构,我们需要让相同的代码在客户端和服务端各自执行一遍,但是这里各自执行一遍,并不包括路由端的代码,造成这种原因主要是因为客户端是通过地址栏来渲染不同的组件的,而服务端是通过请求路径来进行组件渲染的。
203 | 因此,在客户端我们采用`BrowserRouter`来配置路由,在服务端采用`StaticRouter`来配置路由。
204 |
205 | ### 客户端配置
206 |
207 | ```js
208 | import React from 'react';
209 | import ReactDOM from 'react-dom';
210 | import { BrowserRouter } from "react-router-dom";
211 | import Router from '../router';
212 |
213 | function ClientRender() {
214 | return (
215 |
216 |
217 |
218 | )
219 | }
220 |
221 | ```
222 |
223 | ### 服务端配置
224 |
225 | ```javascript
226 | import React from 'react';
227 | import { StaticRouter } from 'react-router'
228 | import Router from '../router.js';
229 |
230 | function ServerRender(req, initStore) {
231 |
232 | return (props, context) => {
233 | return (
234 |
235 |
236 |
237 | )
238 | }
239 | }
240 |
241 | export default ServerRender;
242 |
243 | ```
244 |
245 | ## 再次配置Node进行服务器渲染
246 | 上面配置的服务器,只是简单启动个服务,没有深入进行配置。
247 | ### 引入ReactDOMServer
248 |
249 |
250 | ```js
251 |
252 | const Koa = require('koa');
253 | const app = new Koa();
254 | const path = require('path');
255 | const React = require('react');
256 | const ReactDOMServer = require('react-dom/server');
257 | const koaStatic = require('koa-static');
258 | const router = new KoaRouter();
259 |
260 | const routerManagement = require('./app/router');
261 | const manifest = require('./public/manifest.json');
262 | /**
263 | * 处理链接
264 | * @param {*要进行服务器渲染的文件名默认是build文件夹下的文件} fileName
265 | */
266 | function handleLink(fileName, req, defineParams) {
267 | let obj = {};
268 | fileName = fileName.indexOf('.') !== -1 ? fileName.split('.')[0] : fileName;
269 |
270 | try {
271 | obj.script = ``;
272 | } catch (error) {
273 | console.error(new Error(error));
274 | }
275 | try {
276 | obj.link = ``;
277 |
278 | } catch (error) {
279 | console.error(new Error(error));
280 | }
281 | //服务器渲染
282 | const dom = require(path.join(process.cwd(),`app/build/${fileName}.js`)).default;
283 | let element = React.createElement(dom(req, defineParams));
284 | obj.html = ReactDOMServer.renderToString(element);
285 |
286 | return obj;
287 | }
288 |
289 | /**
290 | * 设置静态资源
291 | */
292 | app.use(koaStatic(path.resolve(__dirname, './public'), {
293 | maxage: 0, //浏览器缓存max-age(以毫秒为单位)
294 | hidden: false, //允许传输隐藏文件
295 | index: 'index.html', // 默认文件名,默认为'index.html'
296 | defer: false, //如果为true,则使用后return next(),允许任何下游中间件首先响应。
297 | gzip: true, //当客户端支持gzip时,如果存在扩展名为.gz的请求文件,请尝试自动提供文件的gzip压缩版本。默认为true。
298 | }));
299 |
300 | /**
301 | * 处理响应
302 | *
303 | * **/
304 | app.use((ctx) => {
305 | let obj = handleLink('page1', ctx.req, {});
306 | ctx.body = `
307 |
308 |
309 |
310 |
311 |
312 |
313 | koa-React服务器渲染
314 | ${obj.link}
315 |
316 |
317 |
318 |
319 | ${obj.html}
320 |
321 |
322 | ${obj.script}
323 |
324 | `
325 | })
326 |
327 | app.listen(3000, () => {
328 | console.log("服务器已启动,请访问http://127.0.0.1:3000")
329 | });
330 | ```
331 | 这里涉及一个`manifest`文件,这个文件是`webpack`插件`webpack-manifest-plugin`生成的,里面包含编译后的地址和文件。大概结构是这样:
332 | ```js
333 | {
334 | "page1.css": "page1.css",
335 | "page1.js": "page1.js"
336 | }
337 | ```
338 | 我们把他引入到`clientConfig`中,添加如下配置:
339 | ```js
340 | ...
341 | plugins: [
342 | // 提取样式,生成单独文件
343 | new MiniCssExtractPlugin({
344 | filename: `[name].css`,
345 | chunkFilename: `[name].chunk.css`
346 | }),
347 | new ManifestPlugin()
348 | ]
349 | ```
350 | 在上述服务端代码中,我们对于`ServerRender.js`进行了柯里化处理,这样做的目的在于,我们在`ServerRender`中,使用了服务端可以识别的`StaticRouter`,并配置了`location`参数,而`location`需要参数`URL`。
351 | 因此,我们需要在`renderToString`中传递`req`,以让服务端能够正确解析React组件。
352 |
353 | ```js
354 | let element = React.createElement(dom(req, defineParams));
355 | obj.html = ReactDOMServer.renderToString(element);
356 | ```
357 |
358 | 通过`handleLink`的解析,我们可以得到一个`obj`,包含三个参数,`link`(`css`链接),`script`(`JS`链接)和`html`(生成`Dom`元素)。
359 |
360 | 通过`ctx.body`渲染`html`。
361 |
362 | ### renderToString()
363 |
364 | 将 `React` 元素渲染到其初始 `HTML` 中。 该函数应该只在服务器上使用。 `React` 将返回一个 `HTML` 字符串。 您可以使用此方法在服务器上生成 `HTML` ,并在初始请求时发送标记,以加快网页加载速度,并允许搜索引擎抓取你的网页以实现 `SEO` 目的。
365 |
366 | 如果在已经具有此服务器渲染标记的节点上调用 `ReactDOM.hydrate()` ,`React` 将保留它,并且只附加事件处理程序,从而使您拥有非常高性能的第一次加载体验。
367 |
368 | ### renderToStaticMarkup()
369 |
370 | 类似于 `renderToString` ,除了这不会创建 `React` 在内部使用的额外`DOM`属性,如 `data-reactroot`。 如果你想使用`React` 作为一个简单的静态页面生成器,这很有用,因为剥离额外的属性可以节省一些字节。
371 |
372 | 但是如果这种方法是在浏览访问之后,会全部替换掉服务端渲染的内容,因此会造成页面闪烁,所以并不推荐使用该方法。
373 |
374 | ### renderToNodeStream()
375 |
376 | 将 `React` 元素渲染到其最初的 `HTML` 中。返回一个 可读的 流(`stream`) ,即输出 `HTML` 字符串。这个 流(`stream`) 输出的 `HTML` 完全等同于 `ReactDOMServer.renderToString` 将返回的内容。
377 |
378 | 我们也可以使用上述`renderToNodeSteam`将其改造下:
379 |
380 | ```
381 | let element = React.createElement(dom(req, defineParams));
382 |
383 | ctx.res.write('
384 |
385 |
386 |
387 |
388 |
389 | koa-React服务器渲染
390 | ');
391 |
392 | // 把组件渲染成流,并且给Response
393 | const stream = ReactDOMServer.renderToNodeStream(element);
394 | stream.pipe(ctx.res, { end: 'false' });
395 |
396 | // 当React渲染结束后,发送剩余的HTML部分给浏览器
397 | stream.on('end', () => {
398 | ctx.res.end('
');
399 | });
400 | ```
401 |
402 | ### renderToStaticNodeStream()
403 |
404 | 类似于 `renderToNodeStream` ,除了这不会创建 `React` 在内部使用的额外`DOM`属性,如 `data-reactroot` 。 如果你想使用 `React` 作为一个简单的静态页面生成器,这很有用,因为剥离额外的属性可以节省一些字节。
405 |
406 | 这个 流(`stream`) 输出的 `HTML` 完全等同于 `ReactDOMServer.renderToStaticMarkup` 将返回的内容。
407 |
408 |
409 | ## 添加状态管理redux
410 |
411 | 以上开发一个静态网站,或者一个相对于比较简单的项目已经`OK`了,但是对于复杂的项目,这些还远远不够,这里,我们再给它加上全局状态管理`Redux`。
412 |
413 | 服务器渲染中其顺序是同步的,因此,要想在渲染时出现首屏数据渲染,必须得提前准备好数据。
414 |
415 | * 提前获取数据
416 | * 初始化store
417 | * 根据路由显示组件
418 | * 结合数据和组件生成 HTML,一次性返回
419 |
420 | 对于客户端来说添加`redux`和常规的`redux`并无太大差别,只是对于`store`添加了一个初始的`window.__INIT_STORE__`。
421 |
422 | ```js
423 | let initStore = window.__INIT_STORE__;
424 | let store = configStore(initStore);
425 |
426 | function ClientRender() {
427 | return (
428 |
429 |
430 |
431 |
432 |
433 |
434 | )
435 | }
436 | ```
437 |
438 | 而对于服务端来说在初始数据获取完成之后,可以采用`Promise.all()`来进行并发请求,当请求结束时,将数据填充到`script`标签内,命名为`window.__INIT_STORE__`。
439 |
440 | ```html
441 | ``
442 | ```
443 |
444 | 然后将服务端的`store`重新配置下。
445 |
446 | ```js
447 | function ServerRender(req, initStore) {
448 | let store = CreateStore(JSON.parse(initStore.store));
449 |
450 | return (props, context) => {
451 | return (
452 |
453 |
454 |
455 |
456 |
457 | )
458 | }
459 | }
460 | ```
461 |
462 | 
463 |
464 | ## 整理Koa
465 |
466 | 考虑后面开发的便利性,添加如下功能:
467 | * Router功能
468 | * HTML模板
469 | ### 添加Koa-Router
470 |
471 | ```js
472 | /**
473 | * 注册路由
474 | */
475 | const router = new KoaRouter();
476 | const routerManagement = require('./app/router');
477 | ...
478 | routerManagement(router);
479 | app.use(router.routes()).use(router.allowedMethods());
480 |
481 | ```
482 |
483 | 为了保证开发时,接口规整,这里将所有的路由都提到一个新的文件中进行书写。并保证如以下格式:
484 |
485 | ```js
486 | /**
487 | *
488 | * @param {router 实例化对象} router
489 | */
490 |
491 | const home = require('./controller/home');
492 |
493 | module.exports = (router) => {
494 | router.get('/',home.renderHtml);
495 | router.get('/page2',home.renderHtml);
496 | router.get('/favicon.ico',home.favicon);
497 | router.get('/test',home.test);
498 | }
499 |
500 | ```
501 |
502 | ### 处理模板
503 |
504 | 将`html`放入代码中,给人感觉并不是很友好,因此,这里同样引入了服务模板`koa-nunjucks-2`。
505 |
506 | 同时在其上在套一层中间件,以便传递参数和处理各种静态资源链接。
507 |
508 | ```js
509 | ...
510 | const koaNunjucks = require('koa-nunjucks-2');
511 | ...
512 | /**
513 | * 服务器渲染,渲染HTML,渲染模板
514 | * @param {*} ctx
515 | */
516 | function renderServer(ctx) {
517 | return (fileName, defineParams) => {
518 | let obj = handleLink(fileName, ctx.req, defineParams);
519 | // 处理自定义参数
520 | defineParams = String(defineParams) === "[object Object]" ? defineParams : {};
521 | obj = Object.assign(obj, defineParams);
522 | ctx.render('index', obj);
523 | }
524 | }
525 |
526 | ...
527 |
528 | /**
529 | * 模板渲染
530 | */
531 | app.use(koaNunjucks({
532 | ext: 'html',
533 | path: path.join(process.cwd(), 'app/view'),
534 | nunjucksConfig: {
535 | trimBlocks: true
536 | }
537 | }));
538 |
539 | /**
540 | * 渲染Html
541 | */
542 | app.use(async (ctx, next) => {
543 | ctx.renderServer = renderServer(ctx);
544 | await next();
545 | });
546 | ```
547 |
548 | 在用户访问该服务器时,通过调用`renderServer`函数,处理链接,执行到最后,调用`ctx.render`完成渲染。
549 |
550 | ```js
551 |
552 | /**
553 | * 渲染react页面
554 | */
555 |
556 | exports.renderHtml = async (ctx) => {
557 | let initState = ctx.query.state ? JSON.parse(ctx.query.state) : null;
558 | ctx.renderServer("page1", {store: JSON.stringify(initState ? initState : { counter: 1 }) });
559 | }
560 | exports.favicon = (ctx) => {
561 | ctx.body = null;
562 | }
563 |
564 | exports.test = (ctx) => {
565 | ctx.body = {
566 | data: `测试数据`
567 | }
568 | }
569 |
570 | ```
571 | 关于`koa-nunjucks-2`中,在渲染`HTML`时,会将有`< >`进行安全处理,因此,我们还需对我们传入的数据进行过滤处理。
572 |
573 | ```html
574 |
575 |
576 |
577 |
578 |
579 |
580 | koa-React服务器渲染
581 | {{ link | safe }}
582 |
583 |
584 |
585 |
586 | {{ html | safe }}
587 |
588 |
589 |
592 | {{ script | safe }}
593 |
594 | ```
595 | ## 文档结构
596 |
597 | ```js
598 | ├── README.md
599 | ├── app //node端业务代码
600 | │ ├── build
601 | │ │ ├── page1.js
602 | │ │ └── page2.js
603 | │ ├── controller
604 | │ │ └── home.js
605 | │ ├── router.js
606 | │ └── view
607 | │ └── index.html
608 | ├── index.js
609 | ├── package.json
610 | ├── public //前端静态资源
611 | │ ├── manifest.json
612 | │ ├── page1.css
613 | │ ├── page1.js
614 | │ ├── page2.css
615 | │ └── page2.js
616 | ├── web //前端源码
617 | │ ├── action //redux -action
618 | │ │ └── count.js
619 | │ ├── components //组件
620 | │ │ └── layout
621 | │ │ └── index.jsx
622 | │ ├── pages //主页面
623 | │ │ ├── page
624 | │ │ │ ├── index.jsx
625 | │ │ │ └── index.less
626 | │ │ └── page2
627 | │ │ ├── index.jsx
628 | │ │ └── index.less
629 | │ ├── reducer //redux -reducer
630 | │ │ ├── counter.js
631 | │ │ └── index.js
632 | │ ├── render //webpack入口文件
633 | │ │ ├── clientRouter.js
634 | │ │ └── serverRouter.js
635 | │ ├── router.js //前端路由
636 | │ └── store //store
637 | │ └── index.js
638 | └── webpack.config.js
639 | ```
640 | ## 最后
641 |
642 | 目前这个架构目前只能手动启动`Koa`服务和启动`webpack`。
643 |
644 | 如果需要将Koa和webpack跑在一块,这里就涉及另外一个话题了,在这里可以查看我一开始写的文章。
645 |
646 | 《[骚年,Koa和Webpack了解一下?](https://juejin.im/post/5c01f46c51882516d725ee51)》
647 |
648 | 如果需要了解一个完整的服务器需要哪些功能,可以了解我早期的文章。
649 |
650 | 《[如何创建一个可靠稳定的Web服务器](https://juejin.im/post/5c0cf55c51882530544f22e2)》
651 |
652 | 最后`GITHUB`地址如下:
653 |
654 | [基于koa的react服务器渲染](https://github.com/baiyuze/koa-react-ssr-render)
655 |
656 | 参考资料:
657 |
658 | 《[React中文文档](http://react.html.cn/docs/react-dom-server.html)》
659 | 《[Webpack中文文档](https://www.webpackjs.com/configuration/output/#output-librarytarget)》
660 | 《[React 中同构(SSR)原理脉络梳理](https://juejin.im/post/5bc7ea48e51d450e46289eab)》
661 | 《[Redux](https://www.redux.org.cn/docs/basics/Reducers.html)》
662 |
--------------------------------------------------------------------------------
/原来正则表达式我记得这么少.md:
--------------------------------------------------------------------------------
1 | ## 前言
2 |
3 | 记得上一次系统的学习正则表达式,还是刚学前端的事,现在过去那么久了,现在有必要将正则给补一补,也许这一次会有不同的感悟。
4 |
5 | ## 正则的速查表
6 |
7 | | 字符 | 详细 |
8 | | :-----: | :--------------------------------------- |
9 | | \ | 将下一个字符标记为一个特殊字符、或一个原义字符、或一个向后引用、或一个八进制转义符。例如,“`n`"匹配字符"`n`"。"`\n`"匹配一个换行符。串行"`\\`"匹配"`\`"而"`\(`"则匹配"`(`"。 |
10 | | ^ | 匹配输入字符串的开始位置。如果设置了`RegExp`对象的`Multiline`属性,`^`也匹配“`\n`”或“`\r`”之后的位置。 |
11 | | $ | 匹配输入字符串的结束位置。如果设置了`RegExp`对象的`Multiline`属性,`$`也匹配“`\n`”或“`\r`”之前的位置。 |
12 | |* | 匹配前面的子表达式零次或多次。例如,`zo`*能匹配“`z`"以及"`zoo`"。*等价于`{0,}`。 |
13 | | + | 匹配前面的子表达式一次或多次。例如,“`zo+`"能匹配"`zo`"以及"`zoo`",但不能匹配"`z`"。`+`等价于{`1,`}。 |
14 | | ? | 匹配前面的子表达式零次或一次。例如,“`do(es)?`”可以匹配"`does`"或"`does`"中的"`do`"。`?`等价于`{0,1}`。 |
15 | | {n} | `n`是一个非负整数。匹配确定的`n`次。例如,“`o{2}`”不能匹配"`Bob`"中的"`o`",但是能匹配"`food`"中的两个`o`。 |
16 | | {n,} | `n`是一个非负整数。至少匹配n次。例如,"`o{2,}`"不能匹配"`Bob`"中的"`o`",但能匹配"`foooood`"中的所有`o`。"`o{1,}`"等价于"`o+`"。"`o{0,}`"则等价于"`o*`"。 |
17 | | {n,m} | `m`和`n`均为非负整数,其中`n<=m`。最少匹配n次且最多匹配`m`次。例如,"`o{1,3}`"将匹配"`fooooood`"中的前三个`o`。"`o{0,1}`"等价于"`o?`"。请注意在逗号和两个数之间不能有空格。 |
18 | | ? | 当该字符紧跟在任何一个其他限制符`(*,+,?,{n},{n,},{n,m})`后面时,匹配模式是非贪婪的。非贪婪模式尽可能少的匹配所搜索的字符串,而默认的贪婪模式则尽可能多的匹配所搜索的字符串。例如,对于字符串"`oooo`","`o+?`"将匹配单个"`o`",而"`o+`"将匹配所有"`o`"。 |
19 | | . | 匹配除“`\n`”之外的任何单个字符。要匹配包括“`\n`”在内的任何字符,请使用像“`(.\|\n)`”的模式。 |
20 | | (pattern) | 匹配`pattern`并获取这一匹配。所获取的匹配可以从产生的`Matches`集合得到,在`VBScript`中使用`SubMatches`集合,在`JScript`中则使用`$0…$9`属性。要匹配圆括号字符,请使用“\(”或“\)”。 |
21 | | (?:pattern) | 匹配`pattern`但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用或字符“`(|)`”来组合一个模式的各个部分是很有用。例如“`industr(?:y|ies)`”就是一个比“`industry|industries`”更简略的表达式。|
22 | | (?=pattern) | 正向肯定预查,在任何匹配`pattern`的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,“`Windows(?=95|98|NT|2000)`”能匹配“`Windows2000`”中的“`Windows`”,但不能匹配“`Windows3.1`”中的“`Windows`”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。 |
23 | | (?!pattern) |正向否定预查,在任何不匹配`pattern`的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如“`Windows(?!95|98|NT|2000)`”能匹配“`Windows3.1`”中的“`Windows`”,但不能匹配“`Windows2000`”中的“`Windows`”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始|
24 | |(?<=pattern)| 反向肯定预查,与正向肯定预查类拟,只是方向相反。例如,“`(?<=95|98|NT|2000)Windows`”能匹配“`2000Windows`”中的“`Windows`”,但不能匹配“`3.1Windows`”中的“`Windows`”。 |
25 | | (? {
116 | return val.slice(1).toUpperCase();
117 | });
118 | //"getElementById"
119 | ```
120 |
121 | ### 匹配二进制数字
122 | * 方法一
123 | ```js
124 | var str = "10101111";
125 | var reg = /^[01]+$/g;
126 | console.log(reg.test(str));
127 |
128 |
129 | ```
130 | * 方法二
131 | ```js
132 | var str = "81";
133 | var reg = /^(?!0)\d+$/;
134 | console.log(reg.test(str));
135 | ```
136 | * 方法三
137 |
138 | ```js
139 | var str = "0101212312";
140 | var reg = /^[^0]\d+$/g;
141 | console.log(reg.test(str));
142 | //false
143 | ```
144 | ### 分割数字每三个以一个逗号划分
145 | ```js
146 | var str = "12345678901";
147 | function numSplit(str){
148 | var re = /(\d)(?=(\d{3})+$)/g;
149 | //(\d{3})+$ 的意思是连续匹配 3 个数字,且最后一次匹配以 3 个数字结尾。
150 | //要找到所有的单个字符,这些字符的后面跟随的字符的个数必须是3的倍数,并在符合条件的单个字符后面添加,
151 | return str.replace(re,'$1,');
152 | }
153 | console.log(numSplit(str));//12,345,678,901
154 | ```
155 |
156 | ### 如何获取一个字符串中的数字字符,并按数组形式输出
157 | ```js
158 | var str="dgfhfgh254bhku289fgdhdy675";
159 | var re=/(\d+)/g;
160 | console.log(str.match(re));
161 | ```
162 |
163 | ### 求一串字符串中出现次数最多的字符和其出现的次数
164 |
165 | ```js
166 | var str = "qjvj58h7vv9n57v55v5jj";
167 | var n = -1;
168 | while ((new RegExp("(.)(.*?\\1){"+(++n)+"}")).test(str));
169 | console.log("出现次数最多的字符是 "+RegExp.$1+",出现次数 "+n);
170 | ```
171 |
172 | **解释一下**
173 | * `.`代表任意一个字符。
174 | * `(.)`选择任意一个字符进行复制。
175 | * `.*` 代表任意一个字符后面有0个或者多个字符。
176 | * `(.)(.*\\1)`是否存在一个或多个字符与它相同。
177 | * `(.)(.*?\\1)`表示是非贪婪模式。
178 | * `\1`匹配和正则表达式中的括号(计算左括号)中出现相同内容的内容,数字代表匹配第几个括号。
179 | * `\\1`代表第一个圆括号里面的内容是否相同。
180 |
181 | ### 压缩字符串(例如:abcbc压缩后依然是abcbc,而xxxyyyyyyz压缩后就是3x6yz)
182 |
183 | ```js
184 | var str = "xxxyyyyyyz";
185 | str = str.replace(/(.)\1+/ig,function(s,a){return s.length+a;});
186 | console.log(str);
187 | ```
188 | * `i`不区分大小写
189 |
190 | ### 判断是否含有连续字符
191 | ```js
192 | var str = 'a2s3s2d2d3dsfas';
193 | var reg = /(\w)\1/ig
194 | console.log(reg.test(str));
195 | ```
196 |
197 | ### 匹配一个字符串中的正浮点数
198 |
199 | ```js
200 | var reg = /(0.\d+)|(\d+.\d+)/;
201 | console.log(reg.test('0')); // false
202 | console.log(reg.test('0.5')); // true
203 | console.log(reg.test('d0.5')); // true
204 | console.log(reg.test('d0.5s')); // true
205 | console.log(reg.test('d0.a5s')); // false
206 |
207 | ```
208 | ## 最后
209 | **如果有地方不合理的,麻烦提出来一下**
210 | ### 参考文章:
211 |
212 | 《[js正则表达式常见面试题](https://www.cnblogs.com/dxzg/p/8279919.html)》
213 | 《[【大家一起来思考】近段时间整理的...](https://bbs.csdn.net/topics/392083813?page=1)》
214 | 《[js正则表达式常见面试题](https://www.cnblogs.com/dxzg/p/8279919.html)》
215 | 《[MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Regular_Expressions)》
216 |
--------------------------------------------------------------------------------
/如何创建一个可靠稳定的Web服务器.md:
--------------------------------------------------------------------------------
1 | > 延续上篇文章[骚年,Koa和Webpack了解一下?](https://juejin.im/post/5c01f46c51882516d725ee51)
2 |
3 | **本篇文章主要讲述的是如何通过Node创建一个稳定的web服务器,如果你看到这里想起了pm2等工具,那么你可以先抛弃pm2,进来看看,如果有哪些不合适的地方,恳请您指出。**
4 |
5 | ## 创建一个稳定的web服务器需要解决什么问题。
6 |
7 | > * 如何利用多核CPU资源。
8 | > * 多个工作进程的存活状态管理。
9 | > * 工作进程的平滑重启。
10 | > * 进程错误处理。
11 | > * 工作进程限量重启。
12 |
13 | ## 如何利用多核CPU资源
14 |
15 | #### 利用多核CPU资源有多种解决办法。
16 |
17 |
18 | * 通过在单机上部署多个Node服务,然后监听不同端口,通过一台Nginx负载均衡。
19 |
20 | > 这种做法一般用于多台机器,在服务器集群时,采用这种做法,这里我们不采用。
21 |
22 |
23 | * 通过单机启动一个master进程,然后fork多个子进程,master进程发送句柄给子进程后,关闭监听端口,让子进程来处理请求。
24 | > 这种做法也是Node单机集群普遍的做法。
25 |
26 |
27 |
28 | 所幸的是,Node在v0.8版本新增的cluster模块,让我们不必使用[child_process](http://nodejs.cn/api/child_process.html)一步一步的去处理Node集群这么多细节。
29 |
30 | **所以本篇文章讲述的是基于cluster模块解决上述的问题。**
31 |
32 |
33 | **首先创建一个Web服务器**,Node端采用的是**Koa**框架。没有使用过的可以先去看下 ===> [传送门](https://koa.bootcss.com/)
34 |
35 | 下面的代码是创建一个基本的web服务需要的配置,看过上篇文章的可以先直接过滤这块代码,直接看后面。
36 |
37 | ```javascript
38 | const Koa = require('koa');
39 | const app = new Koa();
40 | const koaNunjucks = require('koa-nunjucks-2');
41 | const koaStatic = require('koa-static');
42 | const KoaRouter = require('koa-router');
43 | const router = new KoaRouter();
44 | const path = require('path');
45 | const colors = require('colors');
46 | const compress = require('koa-compress');
47 | const AngelLogger = require('../angel-logger')
48 | const cluster = require('cluster');
49 | const http = require('http');
50 |
51 | class AngelConfig {
52 | constructor(options) {
53 | this.config = require(options.configUrl);
54 | this.app = app;
55 | this.router = require(options.routerUrl);
56 | this.setDefaultConfig();
57 | this.setServerConfig();
58 |
59 | }
60 |
61 | setDefaultConfig() {
62 | //静态文件根目录
63 | this.config.root = this.config.root ? this.config.root : path.join(process.cwd(), 'app/static');
64 | //默认静态配置
65 | this.config.static = this.config.static ? this.config.static : {};
66 | }
67 |
68 | setServerConfig() {
69 | this.port = this.config.listen.port;
70 |
71 | //cookie签名验证
72 | this.app.keys = this.config.keys ? this.config.keys : this.app.keys;
73 |
74 | }
75 | }
76 |
77 | //启动服务器
78 | class AngelServer extends AngelConfig {
79 | constructor(options) {
80 | super(options);
81 | this.startService();
82 | }
83 |
84 | startService() {
85 | //开启gzip压缩
86 | this.app.use(compress(this.config.compress));
87 |
88 | //模板语法
89 | this.app.use(koaNunjucks({
90 | ext: 'html',
91 | path: path.join(process.cwd(), 'app/views'),
92 | nunjucksConfig: {
93 | trimBlocks: true
94 | }
95 | }));
96 | this.app.use(async (ctx, next) => {
97 | ctx.logger = new AngelLogger().logger;
98 | await next();
99 | })
100 |
101 | //访问日志
102 | this.app.use(async (ctx, next) => {
103 | await next();
104 | // console.log(ctx.logger,'loggerloggerlogger');
105 | const rt = ctx.response.get('X-Response-Time');
106 | ctx.logger.info(`angel ${ctx.method}`.green,` ${ctx.url} - `,`${rt}`.green);
107 | });
108 |
109 | // 响应时间
110 | this.app.use(async (ctx, next) => {
111 | const start = Date.now();
112 | await next();
113 | const ms = Date.now() - start;
114 | ctx.set('X-Response-Time', `${ms}ms`);
115 | });
116 |
117 | this.app.use(router.routes())
118 | .use(router.allowedMethods());
119 |
120 | // 静态资源
121 | this.app.use(koaStatic(this.config.root, this.config.static));
122 |
123 | // 启动服务器
124 | this.server = this.app.listen(this.port, () => {
125 | console.log(`当前服务器已经启动,请访问`,`http://127.0.0.1:${this.port}`.green);
126 | this.router({
127 | router,
128 | config: this.config,
129 | app: this.app
130 | });
131 | });
132 | }
133 | }
134 |
135 | module.exports = AngelServer;
136 |
137 | ```
138 |
139 | **在启动服务器之后,将`this.app.listen`赋值给`this.server`,后面会用到。**
140 |
141 | 一般我们做**单机集群**时,我们`fork`的进程数量是机器的CPU数量。当然更多也不限定,只是一般不推荐。
142 |
143 | ```javascript
144 | const cluster = require('cluster');
145 | const { cpus } = require('os');
146 | const AngelServer = require('../server/index.js');
147 | const path = require('path');
148 | let cpusNum = cpus().length;
149 |
150 | //超时
151 | let timeout = null;
152 |
153 | //重启次数
154 | let limit = 10;
155 | // 时间
156 | let during = 60000;
157 | let restart = [];
158 |
159 | //master进程
160 | if(cluster.isMaster) {
161 | //fork多个工作进程
162 | for(let i = 0; i < cpusNum; i++) {
163 | creatServer();
164 | }
165 |
166 | } else {
167 | //worker进程
168 | let angelServer = new AngelServer({
169 | routerUrl: path.join(process.cwd(), 'app/router.js'),//路由地址
170 | configUrl: path.join(process.cwd(), 'config/config.default.js')
171 | //默认读取config/config.default.js
172 | });
173 | }
174 |
175 | // master.js
176 | //创建服务进程
177 | function creatServer() {
178 | let worker = cluster.fork();
179 | console.log(`工作进程已经重启pid: ${worker.process.pid}`);
180 | }
181 |
182 | ```
183 | 使用进程的方式,其实就是通过`cluster.isMaster`和`cluster.isWorker`来进行判断的。
184 |
185 | 主从进程代码写在一块可能也不太好理解。这种写法也是Node官方的写法,当然也有更加清晰的写法,借助[`cluster.setupMaster`](http://nodejs.cn/api/cluster.html#cluster_cluster_setupmaster_settings)实现,这里不去详细解释。
186 |
187 | 通过Node执行代码,看看究竟发生了什么。
188 |
189 |
190 | 
191 |
192 | 首先判断`cluster.isMaster`是否存在,然后循环调用`createServer()`,**fork**4个工作进程。打印工作进程**pid**。
193 |
194 | `cluster`启动时,它会在内部启动**TCP**服务,在`cluster.fork()`子进程时,将这个**TCP**服务端`socket`的文件描述符发送给工作进程。如果工作进程中存在`listen()`监听网络端口的调用,它将拿到该文件的文件描述符,通过**SO_REUSEADDR**端口重用,从而实现多个子进程共享端口。
195 |
196 |
197 | ## 进程管理、平滑重启、和错误处理。
198 |
199 | 一般来说,master进程比较稳定,工作进程并不是太稳定。
200 |
201 | 因为工作进程处理的是业务逻辑,因此,我们需要给工作进程添加**自动重启**的功能,也就是如果子进程因为业务中不可控的原因报错了,而且阻塞了,此时,我们应该停止该进程接收任何请求,然后**优雅**的关闭该工作进程。
202 |
203 | ```javascript
204 | //超时
205 | let timeout = null;
206 |
207 | //重启次数
208 | let limit = 10;
209 | // 时间
210 | let during = 60000;
211 | let restart = [];
212 |
213 | if(cluster.isMaster) {
214 | //fork多个工作进程
215 | for(let i = 0; i < cpusNum; i++) {
216 | creatServer();
217 | }
218 |
219 | } else {
220 | //worker
221 | let angelServer = new AngelServer({
222 | routerUrl: path.join(process.cwd(), 'app/router.js'),//路由地址
223 | configUrl: path.join(process.cwd(), 'config/config.default.js') //默认读取config/config.default.js
224 | });
225 |
226 | //服务器优雅退出
227 | angelServer.app.on('error', err => {
228 | //发送一个自杀信号
229 | process.send({ act: 'suicide' });
230 | cluster.worker.disconnect();
231 | angelServer.server.close(() => {
232 | //所有已有连接断开后,退出进程
233 | process.exit(1);
234 | });
235 | //5秒后退出进程
236 | timeout = setTimeout(() => {
237 | process.exit(1);
238 | },5000);
239 | });
240 | }
241 |
242 | // master.js
243 | //创建服务进程
244 | function creatServer() {
245 |
246 | let worker = cluster.fork();
247 | console.log(`工作进程已经重启pid: ${worker.process.pid}`);
248 | //监听message事件,监听自杀信号,如果有子进程发送自杀信号,则立即重启进程。
249 | //平滑重启 重启在前,自杀在后。
250 | worker.on('message', (msg) => {
251 | //msg为自杀信号,则重启进程
252 | if(msg.act == 'suicide') {
253 | creatServer();
254 | }
255 | });
256 |
257 | //清理定时器。
258 | worker.on('disconnect', () => {
259 | clearTimeout(timeout);
260 | });
261 |
262 | }
263 |
264 | ```
265 |
266 | 我们在实例化`AngelServer`后,得到`angelServer`,通过拿到`angelServer.app`拿到`Koa`的实例,从而监听Koa的`error`事件。
267 |
268 | 当监听到错误发生时,发送一个自杀信号`process.send({ act: 'suicide' })`。
269 | 调用`cluster.worker.disconnect()`方法,调用此方法会关闭所有的server,并等待这些server的 'close'事件执行,然后关闭IPC管道。
270 |
271 | 调用`angelServer.server.close()`方法,当所有连接都关闭后,通往该工作进程的IPC管道将会关闭,允许工作进程优雅地死掉。
272 |
273 | 如果5s的时间还没有退出进程,此时,5s后将强制关闭该进程。
274 |
275 | Koa的`app.listen`是`http.createServer(app.callback()).listen();`的语法糖,因此可以调用close方法。
276 |
277 | **worker**监听`message`,如果是该信号,此时先重启新的进程。
278 | 同时监听`disconnect`事件,清理定时器。
279 |
280 | 正常来说,我们应该监听`process`的`uncaughtException`事件,*如果 Javascript 未捕获的异常,沿着代码调用路径反向传递回事件循环,会触发 'uncaughtException' 事件。*
281 |
282 | 但是`Koa`已经在[middleware](https://github.com/chenshenhai/koajs-design-note/blob/master/note/chapter01/06.md)外边加了`tryCatch`。因此在uncaughtException捕获不到。
283 |
284 | 在这里,还得特别感谢下[大深海](https://github.com/chenshenhai)老哥,深夜里,在群里给我指点迷津。
285 |
286 | ## 限量重启
287 |
288 | 通过自杀信号告知主进程可以使新连接总是有进程服务,但是依然还是有极端的情况。
289 | 工作进程不能无限制的被频繁重启。
290 |
291 | 因此在单位时间规定只能重启多少次,超过限制就触发giveup事件。
292 |
293 | ```javascript
294 | //检查启动次数是否太过频繁,超过一定次数,重新启动。
295 | function isRestartNum() {
296 |
297 | //记录重启的时间
298 | let time = Date.now();
299 | let length = restart.push(time);
300 | if(length > limit) {
301 | //取出最后10个
302 | restart = restart.slice(limit * -1);
303 | }
304 | //1分钟重启的次数是否太过频繁
305 | return restart.length >= limit && restart[restart.length - 1] - restart[0] < during;
306 | }
307 |
308 | ```
309 |
310 | 同时将createServer修改成
311 |
312 | ```javascript
313 | // master.js
314 | //创建服务进程
315 | function creatServer() {
316 | //检查启动是否太过频繁
317 | if(isRestartNum()) {
318 | process.emit('giveup', length, during);
319 | return;
320 | }
321 | let worker = cluster.fork();
322 | console.log(`工作进程已经重启pid: ${worker.process.pid}`);
323 | //监听message事件,监听自杀信号,如果有子进程发送自杀信号,则立即重启进程。
324 | //平滑重启 重启在前,自杀在后。
325 | worker.on('message', (msg) => {
326 | //msg为自杀信号,则重启进程
327 | if(msg.act == 'suicide') {
328 | creatServer();
329 | }
330 | });
331 | //清理定时器。
332 | worker.on('disconnect', () => {
333 | clearTimeout(timeout);
334 | });
335 |
336 | }
337 |
338 | ```
339 |
340 | ## 更改负载均衡策略
341 |
342 | 默认的是操作系统抢占式,就是在一堆工作进程中,闲着的进程对到来的请求进行争抢,谁抢到谁服务。
343 |
344 | 对于是否繁忙是由CPU和I/O决定的,但是影响抢占的是CPU。
345 |
346 | 对于不同的业务,会有的I/O繁忙,但CPU空闲的情况,这时会造成负载不均衡的情况。
347 | 因此我们使用node的另一种策略,名为轮叫制度。
348 |
349 | ```javascript
350 | cluster.schedulingPolicy = cluster.SCHED_RR;
351 | ```
352 |
353 | ## 最后
354 |
355 | 当然创建一个稳定的web服务还需要注意很多地方,比如优化处理进程之间的通信,数据共享等等。
356 |
357 | 本片文章只是给大家一个参考,如果有哪些地方写的不合适的地方,恳请您指出。
358 |
359 | 完整代码请见[github](https://github.com/baiyuze/version-control-system/blob/master/lib/angel-cluster/master.js)。
360 |
361 | 参考资料:**深入浅出nodejs**
362 |
363 |
364 |
365 |
366 |
367 |
--------------------------------------------------------------------------------
/打破思维桎梏:探索业务和技术的交汇,开拓个人职业道路:
--------------------------------------------------------------------------------
1 | ---
2 | theme: fancy
3 | ---
4 |
5 | ## 打破思维桎梏
6 |
7 | ### 一、写在之前
8 | > 本篇文章是自工作到现在的一个思考总结,主要目的是为了阐述个人的观点以及如何保持竞争力,也会从多方面,多维度去展开。本文叙述的重点将从业务、技术(架构)和管理三方面来阐述,当然也有可能说的并不准确,仅代表个人观点。
9 | >
10 | > 欢迎大家进行讨论和指出文章中不足之处,大家共同学习进步。
11 |
12 | ### 二、调整思维模式
13 |
14 | 
15 |
16 | 自2022年年初至今,整个行业形势严峻,各企业业务收缩,裁员优化,市场人才饱和,求职困难,竞争激烈。在此背景下,公司对员工能力要求更高,“前端已死”论调加剧了前端等技术人员的焦虑,今年的求职旺季并不热闹,许多人选择稳守岗位,幸存者庆幸自己未成为被裁员者。
17 |
18 | **站在公司角度,哪些能力的员工面临的裁员风险最小?**
19 |
20 | 我认为主要分为**技术**、**业务**和**管理**三方面。
21 |
22 | 如果团队从事有价值或具有广阔市场前景的业务,拥有上述三个条件中的任何一个就足够。
23 |
24 | 从个人角度看,技术、业务和管理三者相辅相成,至少在初期阶段,技术和业务是密切相关的。
25 |
26 | 有人可能认为,作为技术人员,只需编写优秀的代码,不必关心业务。
27 |
28 | 尽管如此,在开发过程中仅满足需求而不关注业务可能并非最佳策略。
29 |
30 | 在许多情况下,后端开发人员比前端开发人员更深入地理解业务。
31 |
32 | 他们需要充分了解业务才能进行数据库和架构设计,满足现有和未来功能扩展需求。而前端开发人员似乎只需完成产品需求文档,无需了解整个业务逻辑。
33 |
34 | 然而,这样做真的好吗?
35 |
36 | 事实上,后端开发人员更有可能担任部门或项目主管。
37 |
38 | 这可能与前端技术的兴起时间较晚有关,但也可能是因为后端开发人员对业务理解更深刻。
39 |
40 | 对公司来说,始终关心的不是开发团队的技术实力,而是如何满足市场需求,技术的深度都是附加的。
41 |
42 | 技术部门是公司的固定成本,投入多少取决于产品带来的利润以及保证产品稳定性和用户体验的需求。
43 |
44 | 我们也很容易沉迷于技术,探究深层原理,提高编程能力,这会提升我们在团队中的地位,让我们成为技术大牛,承担更复杂的功能开发。
45 |
46 | 但换个角度思考,作为技术大牛,承担更复杂任务的同时,我们是否也成为了一种资源?既然如此,为何不成为分配资源的角色呢?
47 |
48 | ### 三、突破思维边界
49 |
50 | 
51 |
52 | 突破思维边界,个人理解为**多维度思考**,这种思考方式有助于开阔视野、提高创新能力和自我提升等方面。
53 |
54 | **具备多维度思考能力后,你所扮演的角色将不仅限于开发者,而是站在更高的维度来思考问题。**
55 |
56 | 举个简单例子:
57 |
58 | 当产品经理提出新需求时,通常会有需求评审会议。在会议过程中,如果你作为普通开发者,你可能会考虑功能如何实现、模块间是否合理、是否需要抽象或其他人的配合。
59 |
60 | 甚至可能觉得功能难实现,而抵触需求,要求产品简化功能。
61 |
62 | 这时,作为开发者的我们变得被动,节奏受产品经理控制。
63 |
64 | 遇到善解人意的产品经理会替开发者着想;若产品经理强势,我们会更加被动。
65 |
66 | 如果换个角度呢?
67 |
68 | 在充分了解业务需求、市场需求及站在用户角度思考后,我们便具备主动性。
69 |
70 | 在会议上,我们可以抓住重点,大胆提出想法,归纳总结,排除不合理需求,从而占据主动地位。这样一来,在会议上不仅具备发言权,还能潜移默化地提升个人威望和竞争力。
71 |
72 | #### 个人品牌与影响力
73 |
74 |
75 | 
76 |
77 | 想象一下,在一个公司工作的开发人员,他们的项目是开发一个出行平台,为用户提供一站式的出行解决方案。作为这个团队的一员,你可能会专注于自己的任务,例如前端设计、后端开发或者数据处理等。
78 |
79 | 然而,如果你能够运用多维度思考,站在不同角色的立场,理解他们的需求和痛点,你的影响力和竞争力将大大提升。
80 |
81 | 实际上,我们所学到的跨领域知识和沟通技巧也能潜移默化的提升竞争力。
82 |
83 | 当具备了多维度思考的能力,将更容易适应不同的行业和岗位,拓宽职业道路。
84 |
85 | 这种能力会在面对市场变化和行业竞争时,具备更强的抗风险能力。
86 |
87 | **通过多维度思考,能够更好地发现和把握个人发展的机会,这将有助于在职业生涯中找到自己的核心竞争力,从而在激烈的竞争中脱颖而出。**
88 |
89 | 在这个过程中,将会逐渐建立起自己的个人品牌和专业声誉,这将对我们的职业发展产生长远的积极影响。
90 |
91 | 总之,多维度思考不仅可以帮助你为公司创造价值,还可以提升个人的职业竞争力和价值,通过不断地学习和实践,能够在职场上取得更大的成功,实现个人和团队的持续发展。
92 |
93 | 所以,从个人的角度来看,多维度思考是一种投资自己的方式,它将使你在未来获得更好的回报。
94 |
95 | ### 四、理解三者之间的关系
96 |
97 | **技术、业务、管理,这三种能力,我认为他们是相辅相成的,技术是为了更好的服务于业务,管理是为了更好的发展业务,一切的前提就是以业务为基础的。**
98 |
99 | 可能会有人反驳这一观点,没错,这个观点是站在整个公司产品层面去考虑的,而我们作为普通开发者,如何去平衡三者之间的关系呢?
100 |
101 |
102 | 
103 |
104 | #### 1、业务能力
105 |
106 | **业务能力是业务架构的关键组成,表示企业执行业务活动的能力,是对完成某一业务目标的一些列业务活动的抽象与封装。**
107 |
108 | 在此背景下,业务能力不仅仅是对公司业务的熟悉程度,而是一种基于日常工作中抽象、封装和总结的业务架构能力。
109 |
110 | 这种能力可以帮助我们迅速熟悉各种业务需求,深入了解公司产品、需求及发展方向,并为之提供针对性的业务理论指导。
111 |
112 | 作为技术人员,我们需要将自己的技术能力与现有业务模型相结合,构建一套独特的方法论。
113 |
114 | 在IT行业中,业务能力可以从以下几个方面来总结:产品背景、业务流程、市场需求、问题分析与解决方案以及创新。
115 |
116 | 在实际工作中,要构建自己的业务模型体系,我们需要对以下问题心中有数:
117 |
118 | **1、为什么要这么做?**
119 |
120 | **2、能带来什么价值?**
121 |
122 | **3、我该如何做?**
123 |
124 | 我们都见过在会议讨论中,总有一个人能够直击问题要害并给出解决方案,或者对整个会议进行总结。虽然表面上我们可能不以为然,但内心可能都希望那个人是自己。
125 |
126 | 拥有一定的业务能力不仅有助于在当前行业的提升,还可推广到其他行业。
127 |
128 | 这是一种专业化的业务架构能力。
129 |
130 | 因此,我认为提升个人业务能力是提升个人影响力的关键环节,通过提高个人影响力,进一步提升个人竞争力。
131 |
132 | #### 2、技术能力
133 |
134 | * 在国内,我们可以爱好编程,为爱发电,去为技术贡献自己的一份力。
135 |
136 | * 但是我们仍需记得,在工作中,业务永远比技术重要,技术只是手段,不是目的,技术是服务于业务的。
137 |
138 | * 技术的难度对于公司来言没有任何意义,公司关注的永远只是解决需求,带来盈利。
139 |
140 | * 技术要带来盈利,无论是间接的,还是直接的。
141 |
142 | **选择比努力更重要。**
143 |
144 | 曾了解过一些公司的晋升体系,对于晋升者来说,前期的一些努力是必要的,有的人选择了优质的业务,有的人通过私下去整合轮子,发布到社区,提升个人影响力。
145 |
146 | 对于选择了业务并充分运用个人技术与架构能力的人来说,他们在产品中所带来的价值越高,主导性越强,产品盈利越好,晋升成功率通常也越高。
147 |
148 | 毕竟,资本往往是短视的,即便是在国内的科研机构也是如此。[知乎例子](https://zhuanlan.zhihu.com/p/48289828)
149 |
150 | 因此,我们需要在追求技术进步的同时,关注业务需求和市场变化,以确保我们在职业生涯中做出明智的选择和投入。
151 |
152 | -------------
153 |
154 | 
155 | 前面说了这么多,其实明确说下来就一点,一定要有一个清晰的认知。
156 |
157 | 该技术能力,我其实表达的是技术架构能力,对于一个新公司,或者接触到新业务时,需要重新构建自己的业务模型,分析当下和未来业务的发展方向,充分利用个人的技术能力,提前为业务布局。
158 |
159 | 在这里,我并不会多说如何去针对业务进行技术架构分析。而是将具体业务细化,然后分析业务,落实到技术分析中,从而布局整个技术架构。
160 |
161 | 通常考虑进行架构设计时,往往都是以哪个技术深,技术新,有可玩性而进行设计;如果站在业务侧,需要既满足业务所需的基础上进行扩展,保证产品的稳定性,才是关键所在。
162 |
163 | 合理的利用个人架构能力,能让我们在接触新业务时迅速构建自己的业务模型,分析当前和未来业务的发展方向,并充分利用个人技术能力为业务提前布局。
164 |
165 | * **全面了解业务需求**
166 | > 与业务团队密切沟通,确保充分理解业务需求、目标和痛点。深入了解用户需求和市场趋势,以便在技术架构设计时能够紧密结合业务需求。
167 |
168 | * **设计灵活、可扩展的架构**
169 | > 在技术架构设计时,要考虑到业务的可持续发展和变化。设计一个灵活、可扩展的架构,以便在业务发展和变化时能够快速调整和适应。
170 |
171 | * **考虑性能与稳定性**
172 | > 在技术架构设计中,不仅要关注技术的深度和新颖性,还要关注产品的性能和稳定性。确保架构能够在满足业务需求的同时,提供良好的用户体验和系统稳定性。
173 |
174 | * **具备敏捷开发的思维**
175 | > 在技术架构设计过程中,要具备敏捷开发的思维,能够快速响应业务变化和市场需求,迅速迭代和优化产品,以便及时满足业务需求。
176 |
177 | 综上所述,技术架构设计应该以业务需求为核心,结合业务模型、技术深度和可玩性,以及考虑性能、稳定性和敏捷开发的思维,才能为公司创造最大的价值。
178 |
179 | 以上都是站在如何提升竞争力的基础上去思考的。
180 |
181 | #### 3、管理能力
182 |
183 | **在这里,我所强调的管理能力是为了让我们在日常工作中对职业规划有更全面的思考。**
184 |
185 | 我们不应该仅限于**业务**、**技术**或**管理**方面的单一发展,而应该从各个方面来综合考虑。
186 |
187 | 有些人可能认为他们未来应该朝技术专家方向发展,而不需要考虑管理问题。
188 |
189 | 虽然个人能力的突出也是一种职业规划方向,但我们此次讨论的重点是如何拓展职业道路以提高个人竞争力。
190 |
191 | 我们可以通过归纳和总结工作中遇到的问题,提炼出一套适合自己的方法论,并在工作中充分运用。很多时候,只有在实际执行过程中,我们才能发现问题并解决问题。
192 |
193 | 在这里,我并没有详细讨论管理需要具备哪些能力,这个部分仅仅是为了强调管理能力的重要性。最终,提升管理能力也是为了促进业务的更好发展。
194 |
195 | 从个人角度来看,具备管理能力可以从整个团队层面来考虑问题,拓宽视野角度,有着充分的业务理解,协同团队成员,以目标导向,提升个人执行力,进而实施。
196 |
197 | ### 五、语言组织能力
198 |
199 |
200 | 
201 |
202 | 我认为当个人能力突出时,如果有着良好的沟通能力,也是极大加分的。
203 |
204 | #### 归纳总结
205 |
206 | * 确定核心要点
207 | * 提炼关键信息
208 | * 提供支持性论据
209 | * 结构清晰
210 |
211 | 此外,在一场会议中,能够从众多纷繁的信息中,根据自己的理解归纳和提取关键信息,并总结出结论或论据,这不仅便于我们个人迅速地理解与记忆,还有利于日后查阅。
212 |
213 | 清晰的结构不仅便于我们自己再次查阅,也有助于他人更快地理解相关内容。此外,这样的会议总结还能促进跨部门或团队间的沟通与协作,使得信息传递更加高效,提高整个组织的工作效率。
214 |
215 | 当然,最重要是,在不断地总结中,加强个人归纳总结的能力,为个人表达能力打下基础。
216 |
217 | #### 表达能力
218 |
219 | 良好的表达能力,我认为在整篇文章中应当是最重要的一点。即使能力再强,如果无法清晰地表达出来,或者表达的意思难以理解,明明自己已经理解了,但说出来的话却失去了原意。
220 |
221 | 在日常工作中,我相信很多人都会遇到这样的人:在描述一个问题时,虽然说了很多,但大家仍然不明白他想表达什么。或者,本可以用一句话简洁地描述的事情,却说了一大堆无关紧要的话。
222 |
223 | 因此,在日常沟通协作中,准确而简洁地描述重点变得尤为重要。
224 |
225 | 举个例子:
226 |
227 | 在企业招聘中,招聘要求通常会包括**沟通能力良好**或**较强的沟通能力**等。
228 |
229 | 以技术面试为例,面试官往往会提出一些与源代码相关的问题,面试者需要回答这些问题以证明自己的能力。
230 |
231 | 源代码问题的答案通常都是简单的,例如`Vue 2`和`Vue 3`的核心原理,或者`React`的**异步调度引擎**等。
232 |
233 | 这些问题的难度在于源代码的封装、抽象以及设计思想和解决问题的思路。
234 |
235 | 因此,我们可以借鉴《**金字塔原理**》这本书,通过结构化的思考和组织方式来回答问题。首先提出问题的**结论**和**关键点**,然后通过**分层逻辑**和**支持性细节**来展开解释。
236 |
237 | 只有我们自己的思维结构足够清晰,听众才能更好地理解。
238 |
239 | 这种能力不仅可以运用在面试中,还可以广泛应用到生活的各个方面。
240 |
241 | ## 最后
242 |
243 | 本篇文章,自从去年下半年以来,我一直在构思如何编写,却始终没有找到合适的切入点。
244 |
245 | 距离我上次在掘金发布文章已有两年,当时的我也没想到,这次我不再分享技术内容,而是分享关于思考和感悟的内容。
246 |
247 | 我认为这样的分享非常有必要,因为它是对我多年工作经验的一次认知总结。
248 |
249 | 将自己的思考用文字表达出来,不仅能加深理解,还能锻炼写作能力,总是有收获的😆。
250 |
251 | 希望大家阅读完本文后能有所启发。
252 |
253 | 欢迎大家一起讨论。
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
--------------------------------------------------------------------------------
/浏览器地址栏里输入URL后的全过程.md:
--------------------------------------------------------------------------------
1 | ## 什么是URL
2 | `URL`是**统一资源定位符**(Uniform Resource Locator),是资源标识最常见的形式。`URL`描述了一台特定服务器上某资源的特定位置。它们可以明确说明如何从一个精确、固定的位置获取资源。
3 |
4 | `URL`说明了协议、服务器和本地资源。
5 |
6 | 而浏览器都是基于`HTTP`协议,而`HTTP`是个应用层的协议。`HTTP`无需操心网络通信的具体细节都交给了`TCP/IP`。
7 | **`TCP`**:
8 | * 无差错的数据传输。
9 | * 按序传输(数据总是按照发送的顺序到达)。
10 | * 未分段的数据流(可以在任意时刻将数据发送出去)。
11 |
12 | HTTP协议位于TCP的上层。HTTP使用TCP来传输其报文数据。
13 |
14 | 
15 |
16 | ## 解析URL
17 | 当用户输入一个完整的`URL`之后,浏览器就开始解析`URL`的构成,以便于查找资源地址,大多数URL的语法通用格式如下:
18 | ```http
19 | ://:@:/;?#
20 | ```
21 | 基本上没有哪个`URL`包含了所有这些组件。`URL`最重要的3个部分是方案`scheme`,主机`host`和路径`path`。
22 | 如果`URL`中不包含`port`,浏览器会默认使用`80`端口进行访问。
23 |
24 | ## DNS域名解析
25 |
26 | ### 什么是DNS?
27 | `DNS`( Domain Name System)是“域名系统”的英文缩写,DNS是应用层协议,事实上他是为其他应用层协议工作的,包括不限于HTTP和SMTP以及FTP,用于将用户提供的主机名解析为ip地址。
28 |
29 |
30 | DNS 查询的过程如下图所示。
31 | 
32 |
33 | * 在浏览器中输入`www.qq.com` 域名,操作系统会先检查自己本地的`hosts`文件是否有这个网址映射关系,如果有,就先调用这个`IP`地址映射,完成域名解析。
34 |
35 | * 如果`hosts`里没有这个域名的映射,则查找本地`DNS`解析器缓存,是否有这个网址映射关系,如果有,直接返回,完成域名解析。
36 |
37 | * 如果`hosts`与本地`DNS`解析器缓存都没有相应的网址映射关系,首先会找`TCP/ip`参数中设置的首选`DNS`服务器,在此我们叫它本地`DNS`服务器,此服务器收到查询时,如果要查询的域名,包含在本地配置区域资源中,则返回解析结果给客户机,完成域名解析,此解析具有权威性。
38 |
39 | * 如果要查询的域名,不由本地`DNS`服务器区域解析,但该服务器已缓存了此网址映射关系,则调用这个`IP`地址映射,完成域名解析,此解析不具有权威性。
40 |
41 | * 如果本地`DNS`服务器本地区域文件与缓存解析都失效,则根据本地`DNS`服务器的设置(是否设置转发器)进行查询,如果未用**转发模式**,本地`DNS`就把请求发至[13台根`DNS`](https://baike.baidu.com/item/%E6%A0%B9%E5%9F%9F%E5%90%8D%E6%9C%8D%E5%8A%A1%E5%99%A8/5907519?fr=aladdin),根`DNS`服务器收到请求后会判断这个域名(`.com`)是谁来授权管理,并会返回一个负责该顶级域名服务器的一个`IP`。本地`DNS`服务器收到`IP`信息后,将会联系负责`.com`域的这台服务器。这台负责`.com`域的服务器收到请求后,如果自己无法解析,它就会找一个管理`.com`域的下一级`DNS`服务器地址(`http://qq.com`)给本地`DNS`服务器。当本地`DNS`服务器收到这个地址后,就会找`http://qq.com`域服务器,重复上面的动作,进行查询,直至找到`www.qq.com`主机。
42 |
43 | * 如果用的是转发模式,此`DNS`服务器就会把请求转发至上一级`DNS`服务器,由上一级服务器进行解析,上一级服务器如果不能解析,或找根`DNS`或把转请求转至上上级,以此循环。不管是本地`DNS`服务器用是是转发,还是根提示,最后都是把结果返回给本地`DNS`服务器,由此`DNS`服务器再返回给客户机。
44 |
45 | **从客户端到本地`DNS`服务器是属于递归查询,而DNS服务器之间就是的交互查询就是迭代查询。**
46 |
47 | ## 建立TCP连接
48 | ### `TCP`根据不同的当前状态(常规或加星)所发送的内容:
49 | | 常规状态| 说 明 | 发 送 | 加 星 状 态 | 发 送 |
50 | | :----: | :----: | :----: |:----: | :----: | :----: |
51 | | CLOSED | 关闭 | RST, ACK | | | |
52 | | LISTEN | 监听连接请求(被动打开) | | | | |
53 | | SYN_SENT | 已发出SYN (主动打开) | SYN |SYN_SENT* |SYN, FIN |
54 | | SYN_RCVD | 已经发出和收到SYN;等待ACK | SYN, ACK |SYN_RCVD* | SYN, FIN, ACK |
55 | | ESTABLISHED | 连接已经建立(数据传输) | ACK |ESTABLISHED* |SYN, ACK |
56 | | CLOSE_WAIT | 收到FIN,等待应用程序关闭 | ACK |CLOSE_WAIT* |SYN, FIN |
57 | | FIN_WAIT_1 | 已经关闭,发出FIN;等待ACK和FIN | FIN, ACK |FIN_WAIT_1 |SYN, FIN, ACK |
58 | | CLOSING | 两端同时关闭;等待ACK | FIN, ACK |CLOSING* |SYN, FIN, ACK |
59 | | LAST_ACK | 收到FIN已经关闭;等待ACK | FIN, ACK |LAST_ACK* |SYN, FIN, ACK |
60 | | FIN_WAIT_2 | 已经关闭;等待FIN | ACK | | |
61 | | TIME_WAIT | 主动关闭后长达2 M S L的等待状态 | ACK | | |
62 |
63 | `TCP`中定义了7个**扩展状态**,这些扩展状态都称为加星状态。它们分别是:`SYN_SENT*`、
64 | `SYN_RCVD*`、`ESTABLISHED *`、`CLOSE_WAIT *`、`LAST_ACK *`、`FIN_WAIT_1 *`和`CLOSING *`。
65 |
66 | ### `TCP`输入的处理顺序
67 |
68 | `TCP`协议收到报文段时,对其中所携带的各种控制信息 ( `SYN`、`FIN`、`ACK`、`URG`和`RST`
69 | 标志,还可能有数据和选项 )的处理顺序不是随意的,也不是各种实现可以自行决定的。
70 |
71 |
72 | 
73 |
74 | 从 `CLOSED`状态到`SYN_SENT`状态的变迁就标明发送了一个`SYN`报文段。在图中则没有采用这种标记方法,而是在每个状态框中标出处于该状态时要发送的报文段类型。例如,当处于`SYN_RECV`状态时,要发出一个带有 `SYN`
75 | 的报文段,其中还包括对所收到`SYN`的确认( `ACK` )。而当处于`CLOSE_WAIT`状态时,要发出
76 | 对所收到`FIN`的确认( `ACK` )。
77 |
78 | 我们之所以要这样做是因为,在`TCP`协议中我们经常需要处理可能造成多次状态变迁的
79 | 报文段。于是在处理一个报文段时,重要的是处理完报文段后连接所处的最终状态,因为它决定了应答的内容。而如果不使用`TCP`协议,每收到一个报文段通常至多只引起一次状态
80 | 变迁,只有在收到`SYN/ACK`报文段时才是例外。
81 |
82 | ## 三次握手
83 |
84 |
85 | 
86 |
87 | 客户端和服务器之间建立连接需要经过**三次确认**的阶段,我们称之为`TCP`的三次握手。
88 |
89 | ### 第一次
90 | > 客户端发送一个`syn`报文,设置发送序号为`X`,客户端进入`SYN_SENT`状态,等待服务器回应。
91 |
92 | ### 第二次
93 | > 服务端收到`syn`报文,但是服务端必须确定客户端的`syn(ack= X + 1)`, 因此服务端也要发送一个`syn=Y`给客户端进行确认,表示服务端已经收到客户端的请求。
94 | 服务端需要发送`ack+syn`给客户端,此时服务器进入`SYN_RECV`状态。
95 |
96 | ### 第三次
97 | 客户端收到服务器的`syn+ack`包,向服务器发送确认包`ack(ack=Y+1)`,此包发送完毕,客户端和服务器进入`ESTABLISHED`(`TCP`连接成功)状态,完成三次握手。
98 |
99 | ### 举个例子
100 | 比如你走在路上,发现前面有你的朋友向你走过来,你向你朋友挥手(**第一次握手**)。
101 |
102 | 你朋友看见了你向他打招呼,但是你朋友因为距离你太远,并不确定是否是跟他打招呼的,因此,你朋友用手指了下自己,并向你示意(**第二次握手**)。
103 |
104 | 你看见了你朋友的表情和动作,你需要给你朋友一个肯定,此时,你点头示意(**第三次握手**)。
105 |
106 | 此时你和你朋友互相聊天(`TCP已连接`)。
107 |
108 | ## 四次挥手
109 |
110 | 
111 |
112 | 由于`TCP`连接是全双工的,因此每个方向都必须单独进行关闭。
113 |
114 | 这原则是当一方完成它的数据发送任务后就能发送一个`FIN`来终止这个方向的连接。收到一个 `FIN`只意味着这一方向上没有数据流动,一个`TCP`连接在收到一个`FIN`后仍能发送数据。
115 |
116 | 首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。
117 |
118 | * `TCP`客户端发送一个`FIN`,用来关闭客户到服务器的数据传送。
119 | * 服务器收到这个`FIN`,它发回一个`ACK`,确认序号为收到的序号加1。和`SYN`一样,一个`FIN`将占用一个序号。
120 | * 服务器关闭客户端的连接,发送一个`FIN`给客户端。
121 | * 客户端发回`ACK`报文确认,并将确认序号设置为收到序号加1。
122 |
123 | ## 挥手例子
124 |
125 | 你和你朋友聊天,聊着聊着,突然想起来女朋友钥匙丢了,你是回家开门的,现在耽误了半小时了,吓得冷汗都出来了,一想起榴莲。。。
126 |
127 | 这个时候,你赶紧跟你朋友说了情况,你说你马上得回去了,下次再聊(**第一次挥手**)。
128 |
129 | 你朋友听了,觉得也是得赶紧回去,就跟你说你赶紧回去吧。(**第二次挥手**)。
130 |
131 | 然后,你朋友走了,并向你挥手道别(**第三次挥手**)。
132 |
133 | 你看见你朋友跟你道别,你同样也跟你朋友道别(**第四次挥手**)。
134 |
135 | 回去之后,你就需要玩玩你的榴莲了。
136 |
137 | ## 页面渲染
138 |
139 | 浏览器渲染页面。(下一篇更新)
140 |
141 | ## 最后
142 |
143 | 参考文章:
144 |
145 | 1、`HTTP`权威指南。
146 |
147 | 2、[DNS原理及其解析过程【精彩剖析】](http://blog.51cto.com/369369/812889)。
148 |
149 | 3、`TCP-IP`详解卷
150 |
151 |
--------------------------------------------------------------------------------
/浏览器渲染原理(处理重排和重绘).md:
--------------------------------------------------------------------------------
1 |
2 | 
3 | > 继续上篇《[浏览器地址栏里输入URL后的全过程](https://juejin.im/post/5c354b656fb9a049e553ce68)》
4 |
5 | ## 前言
6 |
7 | 为什么要了解浏览器的渲染原理?了解浏览器的渲染原理有什么好处?我们做前端开发为什么非要了解浏览器的原理?直接把网页做出来,什么需求,直接一把梭,撸完收工不好吗。
8 |
9 | 但是经常会有人会问,什么是**重排**和**重绘**?
10 |
11 | **重排**也叫**回流**(`Reflow`),**重绘**(`Repaint`),会影响到浏览器的性能,给用户的感觉就是网页访问慢,或者网页会卡顿,不流畅,从而使网页访问量下降。
12 |
13 | 所以,想要尽可能的避免**重排**和**重绘**,就需要了解浏览器的**渲染原理**。
14 |
15 | ## 浏览器工作流程
16 |
17 | 
18 |
19 | 上图我们可以看出,浏览器会解析三个模块:
20 | * `HTML`,`SVG`,`XHTML`,解析生成`DOM`树。
21 | * `CSS`解析生成`CSS`规则树。
22 | * `JavaScript`用来操作`DOM API`和`CSSOM API`,生成`DOM Tree`和`CSSOM API`。
23 |
24 | 解析完成后,浏览器会通过已经解析好的`DOM Tree` 和 `CSS`规则树来构造 `Rendering` `Tree`。
25 |
26 | * `Rendering Tree` 渲染树并不等同于`DOM`树,因为一些像`Header`或`display:none`的东西就没必要放在渲染树中了。
27 |
28 | * `CSS` 的 `Rule Tree`主要是为了完成匹配并把`CSS Rule`附加上`Rendering`。
29 | * `Tree`上的每个`Element`。也就是`DOM`结点,即`Frame`。然后,计算每个`Frame`(也就是每个`Element`)的位置,这又叫`layout`和`reflow`过程。
30 | * 最后通过调用操作系统`Native GUI`的`API`绘制。
31 |
32 | ## 不同内核的浏览器渲染
33 |
34 |
35 | 
36 | 上图是`webkit`内核的渲染流程,和总体渲染流程差不多,要构建`HTML`的`DOM Tree`,和`CSS`规则树,然后合并生成`Render Tree`,最后渲染。
37 |
38 |
39 | 
40 | 这个是`Mozilla`的`Gecko`渲染引擎。
41 | 总体看来渲染流程差不多,只不过在生成渲染树或者`Frame`树时,两者叫法不一致,`webkit`称之为`Layout`,`Gecko`叫做`Reflow`。
42 |
43 | ## 渲染顺序
44 |
45 |
46 | 
47 | * 当浏览器拿到一个网页后,首先浏览器会先解析`HTML`,如果遇到了外链的`css`,会一下载`css`,一边解析`HTML`。
48 | * 当`css`下载完成后,会继续解析`css`,生成`css Rules tree`,不会影响到`HTML`的解析。
49 | * 当遇到`