├── .vscode └── settings.json ├── GIT └── index.md ├── HTML&CSS ├── CSS布局.md ├── css框架_方案.md ├── index.md └── 响应式布局.md ├── HTTP ├── HTTP常用状态码.md ├── Http 缓存策略.md ├── Restful API.md └── index.md ├── JavaScript ├── debounce.md ├── index.md ├── throttle.md ├── 代码基本功测试:JS 的数据类型.md ├── 函数.md ├── 并发任务控制.md └── 异步处理.md ├── LICENSE ├── MarkDown └── index.md ├── SERVER └── linux服务器安装nginx及使用.md ├── Tool ├── index.md ├── 包管理.md ├── 打包工具.md └── 模块化_模块标准.md ├── TypeScript ├── Typescript基础.md ├── index.md ├── 实用类型 Utility type.md └── 泛型 Generics.md ├── images └── bg.png └── readme.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "liveServer.settings.port": 5001 3 | } -------------------------------------------------------------------------------- /GIT/index.md: -------------------------------------------------------------------------------- 1 | #### 后续更新 -------------------------------------------------------------------------------- /HTML&CSS/css框架_方案.md: -------------------------------------------------------------------------------- 1 | css 作用域是全局的,项目越来越大,人越来越多,命名慢慢成为了问题,于是CSS 社区也诞生了相应的模块化解决方案:BEM、Atomic CSS、OOCSS、SMACSS、ITCSS,以及 CSS Modules 和 CSS-in-JS 等。 2 | 3 | 根据这些 CSS 模块化方案的特点,简单的分为三大类: 4 | 5 | 1. **CSS 命名方法论**:通过人工的方式来约定命名规则。 6 | 2. **CSS Modules**:一个 CSS 文件就是一个独立的模块。 7 | 3. **CSS-in-JS**:在 JS 中写 CSS。 8 | 9 | ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25602859/1647589265351-677ebef0-297d-4f36-8a59-2341669ee724.png#clientId=u73e146df-ac9c-4&from=paste&height=507&id=u4ac5186f&name=image.png&originHeight=507&originWidth=732&originalType=binary&ratio=1&rotation=0&showTitle=false&size=57687&status=done&style=none&taskId=uab1caeaa-0afe-42bf-8652-cd724d86aad&title=&width=732) 10 | 11 | ## 一、BEM 12 | BEM即为块级元素修饰字符(Block Element Modifier),以 .block__element--modifier 形式命名,即 .模块名__元素名--修饰器名 三个部分,用双下划线 __ 来明确区分模块名和元素名,用双横线 -- 来明确区分元素名和修饰器名。 13 | ```javascript 14 | 15 |
16 |
17 | 23 |
24 |
25 |
26 |
27 | ``` 28 | 在 BEM 中不建议使用子代选择器,因为每一个类名已经都是全局唯一的了,除非是 block 相互嵌套的场景。 29 | ```javascript 30 | .card {} 31 | .card__head {} 32 | .card__menu {} 33 | .card__menu-item {} 34 | .card__menu-item--active {} 35 | .card__menu-item--disable {} 36 | .card__body {} 37 | .card__foot {} 38 | ``` 39 | 使用 Sass/Less/Stylus 的父元素选择器 & 可以更高效的编写 BEM: 40 | ```javascript 41 | .card { 42 | &__head {} 43 | &__menu { 44 | &-item { 45 | &--active {} 46 | &--disable {} 47 | } 48 | } 49 | &__body {} 50 | &__foot {} 51 | } 52 | ``` 53 | 54 | ## 二、Atomic CSS 55 | 原子化 CSS 结构。 56 | 57 | 优点是可以写基础 视觉功能小的,单用途的 CSS,相当于把每一个单一的作用定义一个Class,确保整个样式表没有一条重复的样式,这样复用性是最高的,代码也最少,但是每个元素就需要一堆的 Class。 58 | 59 | 这种思路可谓另辟蹊径,独树一帜。当然优缺点都很明显:CSS 代码最小化了,而 HTML 膨胀了;虽然不用考虑命名,但是要记一堆新规则。例如css类名为 60 | mt-1: 对应margin-top: 1px; 61 | w-200:对应width: 200px; 62 | Bgc(#0280ae.5) H(90px) IbBox W(50%) foo_W(100%): 63 | width: 50%; 64 | height: 90px; 65 | background-color: rgba(2,128,174,.5); 66 | 更多样式规则可以参考:[https://acss.io/](https://acss.io/) 67 | 68 | ## 三、CSS module 69 | [CSS Modules](https://link.segmentfault.com/?enc=HQgveA280ddEkV4oH6NGzQ%3D%3D.vE9Um3BECv0Goxxd%2Frt3Rmx0TNvfPZvwlP%2BI4jP5%2FlIN8631084D8bjhajKxTwd3) 是一种模块化工具。 70 | CSS Modules 特性: 71 | 72 | - **作用域**:模块中的名称默认都属于本地作用域,定义在 :local 中的名称也属于本地作用域,定义在 :global 中的名称属于全局作用域,全局名称不会被编译成哈希字符串。 73 | - **命名**:对于本地类名称,CSS Modules 建议使用 camelCase 方式来命名,这样会使 JS 文件更干净,即 styles.className。 74 | 但是你仍然可以固执己见地使用 styles['class-name'],允许但不提倡。🤪 75 | - **组合**:使用 composes 属性来继承另一个选择器的样式,这与 Sass 的 @extend 规则类似。 76 | - **变量**:使用 @value 来定义变量,不过需要安装 PostCSS 和 [postcss-modules-values](https://link.segmentfault.com/?enc=W2EZndvQ1m6YsQfGEGU8iw%3D%3D.qtYphXVq58ZEVrIE3%2BInwN51xtQRSKriJwtzUsc%2FK1whOli97my%2BnJ6jju5%2FRU5yN%2BoVCeIe58PticT4HeN6dg%3D%3D) 插件。 77 | 78 | 示例: 79 | ### 3.1 webpack中配置css-loader 80 | ```javascript 81 | module.exports = { 82 | entry: __dirname + '/index.js', 83 | output: { 84 | publicPath: '/', 85 | filename: './bundle.js' 86 | }, 87 | module: { 88 | loaders: [ 89 | { 90 | test: /\.jsx?$/, 91 | exclude: /node_modules/, 92 | loader: 'babel', 93 | query: { 94 | presets: ['es2015', 'stage-0', 'react'] 95 | } 96 | }, 97 | { 98 | test: /\.css$/, 99 | loader: "style-loader!css-loader?modules" 100 | }, 101 | ] 102 | } 103 | }; 104 | ``` 105 | 上面代码中,关键的一行是style-loader!css-loader?modules,它在css-loader后面加了一个查询参数modules,表示打开 CSS Modules 功能。 106 | css-loader默认的哈希算法是[hash:base64],这会将.title编译成._3zyde4l1yATCOkgn-DBWEL这样的字符串。webpack配置上面可以定制哈希类名: 107 | ```javascript 108 | // const getCSSModuleLocalIdent = require('react-dev-utils/getCSSModuleLocalIdent'); 109 | 110 | module: { 111 | loaders: [ 112 | // ... 113 | { 114 | test: /\.css$/, 115 | loader: "style-loader!css-loader?modules&localIdentName=[path][name]---[local]---[hash:base64:5]" 116 | }, 117 | ] 118 | } 119 | ``` 120 | 你会发现.title被编译成了demo03-components-App---title---GpMto 121 | [path][name]---[local]---[hash:base64:5] 122 | 123 | 如果是老项目中途引入CSS Module,也可以这么配置: 124 | ```javascript 125 | const sassRegex = /\.(scss|sass)$/; 126 | const sassModuleRegex = /\.module\.(scss|sass)$/; 127 | a.scss 128 | a.module.scss 129 | 130 | 131 | module.exports = { 132 | entry: __dirname + '/index.js', 133 | output: { 134 | publicPath: '/', 135 | filename: './bundle.js' 136 | }, 137 | module: { 138 | loaders: [ 139 | { 140 | test: /\.jsx?$/, 141 | exclude: /node_modules/, 142 | loader: 'babel', 143 | query: { 144 | presets: ['es2015', 'stage-0', 'react'] 145 | } 146 | }, 147 | { 148 | test: sassRegex, 149 | exclude: sassModuleRegex, 150 | loader: "style-loader!css-loader!sass-loader" 151 | }, 152 | { 153 | test: sassModuleRegex, 154 | loader: "style-loader!css-loader?modules!sass-loader" 155 | }, 156 | ] 157 | } 158 | }; 159 | ``` 160 | 161 | ### 3.2 业务代码 162 | ```javascript 163 | import React from 'react'; 164 | import style from './App.css'; 165 | 166 | export default () => { 167 | return ( 168 | <> 169 |

四个demo

170 |

171 | 第一个:local局部作用域 172 |

173 |

174 | 第二个:global全局作用域 175 |

176 | 177 |
178 | 第三个:composes组合class 179 |
180 |
181 | 第四个:输入变量 182 |
183 | 184 | ); 185 | }; 186 | ``` 187 | 188 | #### 细说第一个:local局部作用域 189 | ```javascript 190 | // ===== html ===== 191 |

192 | 第一个:local局部作用域 193 |

194 | 195 | // ===== css ===== 196 | .title1 { 197 | color: red; 198 | } 199 | 200 | ._3zyde4l1yATCOkgn-DBWEL { 201 | color: red; 202 | } 203 | 204 | :local(.title2) { 205 | color: green; 206 | } 207 | ``` 208 | 209 | #### 细说第二个:global全局作用域 210 | ```javascript 211 | // ===== html ===== 212 |

213 | 第二个:global全局作用域 214 |

215 | 216 | // ===== css ===== 217 | :global(.title2) { 218 | color: green; 219 | } 220 | ``` 221 | 222 | #### 细说第三个:composes组合class 223 | ```javascript 224 | // ===== html ===== 225 |
226 | 第三个:composes组合class 227 |
228 | 229 | // ===== css ===== 230 | .className { 231 | background-color: blue; 232 | } 233 | 234 | .box1 { 235 | composes: className; 236 | composes: className from './another.css'; 237 | color: red; 238 | } 239 | 240 | // 提问,1、支持多个composes组合吗,2、组合后样式的优先级是什么样的 241 | ``` 242 | 243 | #### 细说第四个:输入变量 244 | CSS Modules 支持使用变量,不过需要安装 PostCSS 和 postcss-modules-values。 245 | 1、npm install --save postcss-loader postcss-modules-values 246 | 2、把 postcss-loader 加入 webpack.config.js。 247 | 3、业务代码如下: 248 | ```javascript 249 | // ===== html ===== 250 |
251 | 第四个:输入变量 252 |
253 | 254 | // ===== css ===== 255 | @value blue: #0c77f8; 256 | @value red: #ff0000; 257 | @value green: #aaf200; 258 | 259 | .box2 { 260 | color: red; 261 | background-color: blue; 262 | } 263 | ``` 264 | 使用 CSS Modules 时,推荐配合 CSS 预处理器(Sass/Less/Stylus)一起使用。 265 | CSS 预处理器提供了许多有用的功能,如嵌套、变量、mixins、functions 等,同时也让定义本地名称或全局名称变得容易。 266 | [ 267 | 268 | ](https://blog.csdn.net/wulala_hei/article/details/84633258) 269 | ## 四、CSS in JS 270 | css-in-js 是一种技术,而不是一个具体的库的实现。 271 | 在js文件中写css就是css-in-js技术,而不是独立为一些 css、scss或less这类的文件,这样你就可以在 css 中使用一些属于 js 的如模块声明、变量定义、函数调用和条件判断等语言特性来提供灵活的可扩展的样式定义。 272 | css-in-js 在react社区的热度是最高的,因为 react 本身不会管用户怎么去为组件定义样式问题,而vue有属于框架自己的一套定义样式的方案。 273 | 好处: 274 | - 支持一些js的特性 275 | - 继承 276 | - 变量 277 | - 函数 278 | - 支持框架的特性 279 | - 传值特性 280 | 缺点: 281 | 运行成本:需要通过JavaScript加载,解析和执行样式。 282 | PostCSS没有解析这些库,因为PostCSS不是设计用于运行时的。 283 | 语法高亮等工具还不支持。 CSS-in-JS正在以非常快的速度发展,文本编辑器扩展,linters,代码格式化等等需要追赶新功能以保持同等水平。 284 | 285 | CSS-in-JS 库目前已有几十种实现,你可以在 [CSS in JS Playground](https://link.segmentfault.com/?enc=J%2FUyHo5xWZyXOcRjb6GUKw%3D%3D.nsDHW1o788cbVmzvCJ6mN%2BdXTOuw0J9hQpd%2BfhWsX%2B5Xqm1c7%2BPUA3elWXz2Bj9q) 上快速尝试不同的实现。下面列举一些流行的 CSS-in-JS 库: 286 | 287 | - styled-components:[https://github.com/styled-com...](https://link.segmentfault.com/?enc=JQ%2FdZ6qZF4xMkFW9BCk3jA%3D%3D.bi9USEgrzw9TXCHBx9GKDIx6ykz%2B9zIeVhhBe7x1y53E8FUFqmLrUMKYzSkyuMDgsw24JcXGI7WwJDlm2H8qpA%3D%3D) 33k(**推荐**) 288 | - emotion:[https://github.com/emotion-js...](https://link.segmentfault.com/?enc=BtV1WRdonylYpVvDBJviBQ%3D%3D.OE93hrkt46EleGZou%2BCTtsvn%2FxD%2B%2F5cHIWa919NMoXMkxW6EYZdd89cn6ZIF7yp6) 13k 289 | - Radium:[https://github.com/Formidable...](https://link.segmentfault.com/?enc=noe51xTTSiZ0fAWgc6QWFg%3D%3D.FUDUJjS1rL6tytvIyLzGnMnAKqfWY0poZOsLmVLGDorojohMEZ4rRgYIkJtkD4Ep) 7k(已不再维护) 290 | - Styled System:[https://github.com/styled-sys...](https://link.segmentfault.com/?enc=qz9aNakTZYjKC6MYCB9ztQ%3D%3D.vo3OMhUDEihsqJ8KxdWKkTNYypNe3gMWyXavtj%2BDsff63FBr9tZbDKOAHcmqyhxC) 7k 291 | - styled-jsx:[https://github.com/vercel/sty...](https://link.segmentfault.com/?enc=rtq7F%2FFDwRVVa26xDlAzXw%3D%3D.rpaWMONvwXO0xQqh3z%2FlNIQVhdiKwL0u7VJJYDMhTU4nhx0tUVW2%2FjgY4dKMX1A8) 6k 292 | - JSS:[https://github.com/cssinjs/jss](https://link.segmentfault.com/?enc=WDQ2H%2B6FVcqxE6g5trx1Yw%3D%3D.zZKEizlL8zGtHgtIOiBNEnWLzf%2Fa4li8bnrBfk1js6M%3D) 6k 293 | 294 | ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25602859/1647753078103-8341aebe-01f5-4516-bab9-5d30311252c4.png#clientId=u5de5f62e-0fd0-4&from=paste&height=487&id=ud6335025&name=image.png&originHeight=487&originWidth=732&originalType=binary&ratio=1&rotation=0&showTitle=false&size=122195&status=done&style=none&taskId=ue10cc413-8932-4b39-a2bb-d720c3d74c7&title=&width=732) 295 | 296 | ### 🍑 styled-components 库示例: 297 | #### 🏃‍♀️ 第一步 298 | 299 | - 使用styled-components前需要安装: 300 | ```javascript 301 | npm i -S styled-components 302 | ``` 303 | 304 | - 由于css后期会在模板字符串中编写,默认情况下 vsconde 是没有css样式代码片段的(写样式的时候没有代码提示的),为了提高css代码在模板字符串中编写的效率,建议安装一个vscode的扩展: 305 | - vscode-styled-components 306 | - ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25602859/1647751186604-aa1a0966-e2fb-4121-aa5f-e03f99a9f1c1.png#clientId=u5de5f62e-0fd0-4&from=paste&height=559&id=u30b3ddef&name=image.png&originHeight=559&originWidth=643&originalType=binary&ratio=1&rotation=0&showTitle=false&size=505338&status=done&style=none&taskId=uf0c12442-78bf-4325-a99a-cc0d9fd3699&title=&width=643) 307 | - ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25602859/1647760646103-eb5e28e6-89ea-4281-bb51-bf6d73af5775.png#clientId=u6b998dd4-bb14-4&from=paste&height=442&id=ud4adc30e&name=image.png&originHeight=442&originWidth=637&originalType=binary&ratio=1&rotation=0&showTitle=false&size=213384&status=done&style=none&taskId=u50b6651d-8e6b-4ead-af95-b1166fac72d&title=&width=637) 308 | 309 | #### 🏃‍♀️ 第二步 310 | ##### 2.1 内部写法 311 | ```javascript 312 | // ======== Basic.jsx文件 ========== 313 | import React, { Component } from 'react'; 314 | // 导入样式组件 315 | import styled from 'styled-components'; 316 | 317 | class Basic extends Component { 318 | render() { 319 | return ( 320 |
321 | 内部文件写法 322 |
323 | ); 324 | } 325 | } 326 | const Ha = styled.div` 327 | font-size: 50px; 328 | color: red; 329 | background: pink; 330 | width: 100%; 331 | height: 100vh; 332 | ` 333 | 334 | export default Basic; 335 | ``` 336 | ##### 2.2 外部写法(导入样式组件) 337 | ```javascript 338 | // ======== Card.jsx文件 ========== 339 | // 导入样式组件 340 | import { CardText } from './style'; 341 | 342 | function Card() { 343 | return ( 344 |
345 | Card卡片 346 | 外部Card卡片写法 347 |
348 | ); 349 | } 350 | 351 | export default Card; 352 | ``` 353 | ```javascript 354 | // ======== style.js文件 ========== 355 | // ======== 外部写法(导入样式组件) ======== 356 | import styled from 'styled-components'; 357 | 358 | // const 标签名(首字母大写)= styled.HTML标签名`css样式` 359 | // 导出 360 | export const CardText = styled.div` 361 | font-size: 50px; 362 | font-family: 华文行楷; 363 | color: orange; 364 | background-color: blue; 365 | width: 40vw; 366 | ` 367 | ``` 368 | 展示效果如下:![image.png](https://cdn.nlark.com/yuque/0/2022/png/25602859/1647761247586-779ee350-7b41-468d-b201-4828847fe379.png#clientId=u6b998dd4-bb14-4&from=paste&height=787&id=u4719964f&name=image.png&originHeight=787&originWidth=1439&originalType=binary&ratio=1&rotation=0&showTitle=false&size=143166&status=done&style=none&taskId=uc75704f0-143c-4e5c-a92c-e2dfda0a35b&title=&width=1439) 369 | 370 | ##### 2.3 样式继承 371 | 2.3.1. 在styled-components中也可以使用样式的继承 372 | 2.3.2. 其继承思想与react的组件继承相似: 373 | - 继承父的样式: 374 | - 父有,子没有,继承后子也有 375 | - 重载父的样式 376 | - 父有,子有,继承后子覆盖父的 377 | ```javascript 378 | // ======== style1.js文件 ========== 379 | // ======== 样式继承 ======== 380 | import styled from 'styled-components'; 381 | 382 | // const 标签名(首字母大写)= styled.HTML标签名`css样式` 383 | // 导出 384 | const Fu = styled.div` 385 | font-size: 50px; 386 | font-family:'Courier New',Courier,monospace,kai; 387 | color: green; 388 | width: 30vw; 389 | ` 390 | // 子继承父 391 | // 继承:color 和 font-family子没有,会用父的 392 | // 重载:font-size两者都有,以子为准 393 | const Zi = styled(Fu)` 394 | font-size: 80px; 395 | background: yellowgreen; 396 | width: 30vw; 397 | ` 398 | 399 | export { Fu, Zi }; 400 | 401 | ``` 402 | ```javascript 403 | // ======== JiCheng.js文件 ========== 404 | import React, { Component } from 'react'; 405 | // 导入样式组件 406 | import {Fu, Zi} from './style1'; 407 | 408 | class JiCheng extends Component { 409 | render() { 410 | return ( 411 |
412 | 继承-原先的样式 413 | 继承-现在的样式 414 |
415 | ); 416 | } 417 | } 418 | 419 | export default JiCheng; 420 | 421 | ``` 422 | 423 | ##### 2.4 属性传递 424 | 425 | - 属性传递:样式值的动态传参(组件传值) 426 | - 基于css-in-js的特性,在styled-components中也允许我们使用props(父传子),这样一来,我们可以对部分需要的样式进行传参,方便动态控制样式的改变 427 | ```javascript 428 | // ======== style2.js文件 ========== 429 | // ======== 属性传递 ======== 430 | import styled from 'styled-components'; 431 | 432 | // 动态属性传递样式的值 433 | // 没传值就是默认值green,传了值就是值的属性 434 | const ChuanDiStyle = styled.div` 435 | background: ${(props) => props.bgColor || 'red'}; 436 | font-size: ${(props) => props.Size || '60px'}; 437 | width: 20vw; 438 | ` 439 | export { ChuanDiStyle }; 440 | ``` 441 | ```javascript 442 | // ======== ChuanDi.js文件 ========== 443 | import React, { Component } from 'react'; 444 | // 导入样式组件 445 | import { ChuanDiStyle } from './style2'; 446 | 447 | class ChuanDi extends Component { 448 | render() { 449 | return ( 450 |
451 | 原先的样式 452 | 传值的样式 453 |
454 | ); 455 | } 456 | } 457 | 458 | export default ChuanDi; 459 | ``` 460 | 展示效果如下: 461 | ![image.png](https://cdn.nlark.com/yuque/0/2022/png/25602859/1647762607463-80dbf282-1e36-43e2-9200-c86f810a527a.png#clientId=u6b998dd4-bb14-4&from=paste&height=786&id=u0f1d1634&name=image.png&originHeight=786&originWidth=1439&originalType=binary&ratio=1&rotation=0&showTitle=false&size=162086&status=done&style=none&taskId=u56989cd2-a88c-4088-8572-60cf7d05649&title=&width=1439) 462 | 463 | ### 扩展 -》定制表格 464 | 参考:[https://segmentfault.com/a/1190000017549783](https://segmentfault.com/a/1190000017549783) 465 | ### 扩展-》主题色 466 | ThemeProvider 官方文档:[https://styled-components.com/docs/advanced#theming](https://styled-components.com/docs/advanced#theming) 467 | ```javascript 468 | const Wrapper = styled.div` 469 | /* 应用于Wrapper组件本身和Wrapper组件里的所有html标签 */ 470 | color: black; 471 | 472 | /* 应用于Wrapper组件里的h3标签 */ 473 | h3 { 474 | color: red 475 | } 476 | 477 | /* 应用于Wrapper组件里的className为blue的html标签 */ 478 | .blue { 479 | color: blue 480 | } 481 | ` 482 | 483 | render( 484 | 485 |

黑色 p 标签

486 |

红色 h3 标签

487 |

蓝色 p 标签

488 |
489 | ) 490 | ``` 491 | ```javascript 492 | // react 中使用 styled-components 493 | import styled, { ThemeProvider } from 'styled-components'; 494 | 495 | const Box = styled.div` 496 | color: ${props => props.theme.color}; 497 | `; 498 | 499 | 500 | I'm mediumseagreen! 501 | 502 | ``` 503 | 504 | CSS Modules 与 styled-components 是两种截然不同的 CSS 模块化方案,它们最本质的区别是:前者是在外部管理 CSS,后者是在组件中管理 CSS。两者没有孰好孰坏。 505 | 506 | ### styled-components更多使用技巧 507 | (更具体的内容请参考 [官方文档](https://link.segmentfault.com/?enc=5ttmy4UvoGcBV%2FppAEfwJA%3D%3D.Zdr713jjMzuhCdF1w8nmF8YIZ45bMK7d6aWe0n7%2FINo%3D)): 508 | 509 | - 可以通过插值的方式给样式组件传递参数(props),这在需要动态生成样式规则时特别有用。 510 | - 可以通过构造函数 styled() 来继承另一个组件的样式。 511 | - 使用 createGlobalStyle 来创建全局 CSS 规则。 512 | - styled-components 会为自动添加浏览器兼容性前缀。 513 | - styled-components 基于 [stylis](https://link.segmentfault.com/?enc=kRSPtxoaSqyAgmzEHCROJQ%3D%3D.7Xc4diIe7T9JC%2F%2B%2BoE3NwHaGl3OI4VEcl7DIhh7DGRZcxJXQ%2BJANlhp4rWFtfPbx)(一个轻量级的 CSS 预处理器),你可以在样式组件中直接使用嵌套语法,就像在 Sass/Less/Stylus 中的那样。 514 | - 强烈推荐使用 styled-components 的 Babel 插件 [babel-plugin-styled-components](https://link.segmentfault.com/?enc=3kEEvE1RWGJa1fyx9vTZxw%3D%3D.AAlY8doG1mj5nZnksyWUPVbscQjXnl7vJQsNblpUhp5R3%2BfVD%2F5EVVGsilVpxHOyjC0IvGcxFFnk%2Fpnx7TPGipVTMiUrTh8pv8N5ERtEZrA%3D)(当然这不是必须的)。它提供了更好的调试体验的支持,比如更清晰的类名、SSR 支持、压缩代码等等。 515 | - 你也可以在 Vue 中使用 styled-components,[vue-styled-components](https://link.segmentfault.com/?enc=xEb3JzERsX%2BSUq20dsgFTQ%3D%3D.5UeZvYAWC8N28Lw7aR3F1yFRyHJRFDXBmEr0MHrWHtIIE3tk8hUh%2Fvk8LOogJTT8i%2B0f8dwDXTQg%2Bc2kuS7GIA%3D%3D),不过好像没人会这么做\~ 516 | - 默认情况下,模版字符串中的 CSS 代码在 VSCode 中是没有智能提示和语法高亮效果的,需要安装 [扩展](https://link.segmentfault.com/?enc=QiXf%2B344Z4lTUziLw6KLiQ%3D%3D.FvccRb9yrggzVRPeInehrYk4DTgLuV%2BFSBdA%2FAvh9Wxl%2FPxOzV1dVOXE43pvWpx9UsKi2pdAdryXBd5cTKLLvfY2LCs3agzI0DyMh6jCxJ1qfrM%2BV14k7yHZMK%2FIYWCn)。 517 | 518 | ## 源码 519 | ### css-modules-demos:[https://gitee.com/vvweb/css-modules-demos](https://gitee.com/vvweb/css-modules-demos) 520 | ### css-in-js-demos:[https://gitee.com/vvweb/css-in-js-demo](https://gitee.com/vvweb/css-in-js-demo) 521 | -------------------------------------------------------------------------------- /HTML&CSS/index.md: -------------------------------------------------------------------------------- 1 | # HTML & CSS 2 | 3 | |目录 | 内容描述 4 | |- | -:| 5 | CSS 布局 | [使用技巧](./css布局.md) 6 | 响应式布局 | [使用技巧](./响应式布局.md) 7 | css框架_方案 | [使用技巧](./css框架_方案.md) -------------------------------------------------------------------------------- /HTML&CSS/响应式布局.md: -------------------------------------------------------------------------------- 1 | **响应式**是一个网站能够兼容多个终端——而不是为每个终端做一个特定的版本。这个概念是为解决移动互联网浏览而诞生的。 2 | 3 | 随着智能手机、3G、4G、HTML5 的普及,使用手机访问网站的人越来越多,为了让用户在手机上看到更合适的布局,且兼顾开发的效率,响应式的概念就被提出了。 4 | 5 | 为了实现平板、手机和电脑不同的预览效果,并不是只有响应式布局一种技术,还有另一种技术 —— **自适应**。 6 | 7 | **自适应**就是为不同客户端分别提供一套独立的前端代码,和响应式使用一套代码适配多种客户端不同。 8 | 9 | **响应式**适合应用在一些简单的官网、展示类页面,展示的**内容大致相同**。而**自适应**适合应用在不同客户端 10 | 类型有**较大差异**的网站,这种网站只使用一套前端代码会使得代码难以维护。 11 | 12 | ### 响应式布局的规则 13 | #### 1. 分段响应规则 14 | 响应式的响应,面向的核心对象是浏览器窗口的宽度,而不是设备类型。所以当我们打开响应式网站的时候,通过改变浏览器的宽度,就可以看见不同的展示效果。 15 | 16 | 浏览器宽度每达到一个数值(Breakpoint)的时候,页面的排版和样式就会发生明显的变化,而这就是响应式设计最重要的功能 —— 分段展示。 17 | ![uisdc-wy-20200818-6.webp](https://cdn.nlark.com/yuque/0/2022/webp/26213291/1648287872215-d7fc67a1-2c0a-4946-a36a-77fa02efdad5.webp#clientId=ua42d2802-6e61-4&from=ui&id=u00955dd6&name=uisdc-wy-20200818-6.webp&originHeight=360&originWidth=640&originalType=binary&ratio=1&rotation=0&showTitle=false&size=761016&status=done&style=none&taskId=ud11f0e76-363e-45ff-9888-7da69b23996&title=) 18 | 响应式规则就是为页面分配不同的宽度区间,每个区间有各自展示的样式,用来应对不同的场景和设备类型。 19 | 20 | **2. 组件宽度适应** 21 | 分段式响应,是响应式布局的第一层逻辑。而在触发关键值(Breakpoint) 之间的区间,我们拖动窗口的宽度,会发现组件的宽度也随之改动,这就是 —— 组件宽度适应。 22 | ![uisdc-wy-20200818-14.gif](https://cdn.nlark.com/yuque/0/2022/gif/26213291/1648297698420-0fdfbacd-f2b9-4b43-9a5f-80de91ff47f8.gif#averageHue=%23dbdada&clientId=ua42d2802-6e61-4&from=ui&id=ufc5d3d01&name=uisdc-wy-20200818-14.gif&originHeight=400&originWidth=640&originalType=binary&ratio=1&rotation=0&showTitle=false&size=708076&status=done&style=none&taskId=u961b21dc-4156-443f-a25f-f9efff3c801&title=) 23 | 总结一下,响应式的规则就是页面组件先遵循当前分段展示的布局效果,并在这个区间内支持小范围宽度的变更和适应。 24 | #### 25 | ## em 26 | > em作为font-size的单位时,其代表父元素的字体大小,em作为其他属性单位时,代表自身字体大小——MDN 27 | 28 | 使用场景:需要根据字体大小改变而更改相应样式的尺寸,比如 button 字体大小改变,相应的 padding 和 margin 的改变,这种情况是使用 em 就会非常方便。 29 | #### 30 | ## rem 31 | > rem作用于非根元素时,相对于**根元素字体大小**;rem作用于根元素字体大小时,相对于其出**初始字体大小**——MDN 32 | 33 | ```javascript 34 | /* 作用于根元素,相对于原始大小(16px),所以html的font-size为32px*/ 35 | html {font-size: 2rem} 36 | 37 | /* 作用于非根元素,相对于根元素字体大小,所以为64px */ 38 | p {font-size: 2rem} 39 | ``` 40 | 使用场景:在移动端中需要适配不同的屏幕大小,把屏幕宽度和根元素字体大小联系在一起就可以使得使用 rem 作为单位的元素随着屏幕大小的改变而变化。 41 | ## media query 42 | 媒体查询(Media Queries)早在在css2时代就存在,经过css3的洗礼后变得更加强大bootstrap的响应式特性就是从此而来的. 43 | 44 | 简单的来讲媒体查询是一种用于修饰css何时起作用的语法. 45 | 46 | > Media Queries 的引入,其作用就是允许添加表达式用以确定媒体的环境情况,以此来应用不同的样式表。换句话说,其允许我们在不改变内容的情况下,改变页面的布局以精确适应不同的设备。 47 | 48 | 49 | 既然媒体查询是用于控制样式的,而样式的使用无外乎如下几种规则: 50 | 51 | - 使用link引入 52 | - 使用style标签 53 | - 使用style属性 54 | - 使用@import引入 55 | 56 | 而显式的使用媒体查询声明样式我们有如下三种方法: 57 | 58 | - 使用link引入时使用media属性 59 | - 使用style标签时添加media属性 60 | - 在样式中使用条件规则组 61 | 62 | 在 link 中的使用方法: 63 | ```html 64 | 65 | ``` 66 | **一旦使用了媒体查询修饰link标签后,就意味着符合媒体查询后这个样式就会被启用,同样的规则适用于style标签。** 67 | 68 | ### 媒体查询的三个部分 69 | > 每条媒体查询语句都由一个可选的**媒体类型**和任意数量的**媒体特性表达式**构成。可以使用多种逻辑操作符合并多条媒体查询语句。媒体查询语句不区分大小写。 70 | 71 | 72 | 看起来很复杂,但是实际上一个媒体查询的**声明**就分为以下三个部分: 73 | 74 | - 媒体类型 - 形容设备 75 | - 媒体特性(媒体特征/媒体功能) - 形容设备的状态 76 | - 逻辑操作符 - 连接多个规则 77 | 78 | ### 媒体类型 79 | _媒体类型_(_Media types_)描述设备的一般类别。除非使用 not 或 only 逻辑操作符,媒体类型是可选的,并且会(隐式地)应用 all 类型。 80 | 81 | - all 82 | - 适用于所有设备。 83 | - print 84 | - 适用于在打印预览模式下在屏幕上查看的分页材料和文档。 (有关特定于这些格式的格式问题的信息,请参阅[分页媒体](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Paged_Media)。) 85 | - screen 86 | - 主要用于屏幕。 87 | - speech 88 | - 主要用于语音合成器。 89 | 90 | > **被废弃的媒体类型:** CSS2.1 和 [Media Queries 3](https://drafts.csswg.org/mediaqueries-3/#background) 定义了一些额外的媒体类型(tty, tv, projection, handheld, braille, embossed, 以及 aural),但是他们在[Media Queries 4](https://dev.w3.org/csswg/mediaqueries/#media-types) 中已经被废弃,并且不应该被使用。aural类型被替换为具有相似效果的speech。 91 | 92 | 93 | ### 媒体特性 94 | _媒体特性_(_Media features_)描述了 user agent、输出设备,或是浏览环境的具体特征。媒体特性表达式是完全可选的,它负责测试这些特性或特征是否存在、值为多少。每条媒体特性表达式都必须用括号括起来。 95 | 96 | | 名称 | 简介 | 备注 | 97 | | --- | --- | --- | 98 | | [any-hover](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/any-hover) | 是否有任何可用的输入机制允许用户(将鼠标等)悬停在元素上? | 在 Media Queries Level 4 中被添加。 | 99 | | [any-pointer](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/any-pointer) | 可用的输入机制中是否有任何指针设备,如果有,它的精度如何? | 在 Media Queries Level 4 中被添加。 | 100 | | [aspect-ratio](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/aspect-ratio) | 视窗(viewport)的宽高比 | | 101 | | [color(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/color) | 输出设备每个像素的比特值,常见的有 8、16、32 位。如果设备不支持输出彩色,则该值为 0 | | 102 | | [color-gamut(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/color-gamut) | 用户代理和输出设备大致程度上支持的色域 | 在 Media Queries Level 4 中被添加。 | 103 | | [color-index(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/color-index) | 输出设备的颜色查询表(color lookup table)中的条目数量,如果设备不使用颜色查询表,则该值为 0 | | 104 | | [device-aspect-ratio(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/device-aspect-ratio) 105 | | 输出设备的宽高比 | 已在 Media Queries Level 4 中被弃用。 | 106 | | [device-height](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/device-height) 107 | | 输出设备渲染表面(如屏幕)的高度 | 已在 Media Queries Level 4 中被弃用。 | 108 | | [device-width(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/device-width) 109 | | 输出设备渲染表面(如屏幕)的宽度 | 已在 Media Queries Level 4 中被弃用。 | 110 | | [display-mode(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/display-mode) | 应用程序的显示模式,如web app的manifest中的[display](https://developer.mozilla.org/zh-CN/docs/Web/Manifest#display) 成员所指定 | 在 [Web App Manifest spec](http://w3c.github.io/manifest/#the-display-mode-media-feature) 111 | 被定义. | 112 | | [forced-colors(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/forced-colors) | 检测是user agent否限制调色板 | 在 Media Queries Level 5 中被添加。 | 113 | | [grid(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/grid) | 输出设备使用网格屏幕还是点阵屏幕? | | 114 | | [height(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/height) | 视窗(viewport)的高度 | | 115 | | [hover](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/hover) | 主要输入模式是否允许用户在元素上悬停 | 在 Media Queries Level 4 中被添加。 | 116 | | [inverted-colors(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/inverted-colors) | user agent或者底层操作系统是否反转了颜色 | 在 Media Queries Level 5 中被添加。 | 117 | | light-level | 环境光亮度 | 在 Media Queries Level 5 中被添加。 | 118 | | [monochrome(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/monochrome) | 输出设备单色帧缓冲区中每个像素的位深度。如果设备并非黑白屏幕,则该值为 0 | | 119 | | [orientation](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/orientation) | 视窗(viewport)的旋转方向 | | 120 | | [overflow-block(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/overflow-block) | 输出设备如何处理沿块轴溢出视窗(viewport)的内容 | 在 Media Queries Level 4 中被添加。 | 121 | | [overflow-inline(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/overflow-inline) | 沿内联轴溢出视窗(viewport)的内容是否可以滚动? | 在 Media Queries Level 4 中被添加。 | 122 | | [pointer(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/pointer) | 主要输入机制是一个指针设备吗?如果是,它的精度如何? | 在 Media Queries Level 4 中被添加。 | 123 | | [prefers-color-scheme](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/prefers-color-scheme) | 探测用户倾向于选择亮色还是暗色的配色方案 | 在 Media Queries Level 5 中被添加。 | 124 | | [prefers-contrast(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-contrast) | 探测用户是否有向系统要求提高或降低相近颜色之间的对比度 | 在 Media Queries Level 5 中被添加。 | 125 | | [prefers-reduced-motion](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/prefers-reduced-motion) | 用户是否希望页面上出现更少的动态效果 | 在 Media Queries Level 5 中被添加。 | 126 | | prefers-reduced-transparency | 用户是否倾向于选择更低的透明度 | 在 Media Queries Level 5 中被添加。 | 127 | | [resolution(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/resolution) | 输出设备的像素密度(分辨率) | | 128 | | scan | 输出设备的扫描过程(适用于电视等) | | 129 | | [scripting(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/scripting) | 探测脚本(例如 JavaScript)是否可用 | 在 Media Queries Level 5 中被添加。 | 130 | | [update(en-US)](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/update-frequency) | 输出设备更新内容的渲染结果的频率 | 在 Media Queries Level 4 中被添加。 | 131 | | [width](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@media/width) | 视窗(viewport)的宽度,包括纵向滚动条的宽度 | | 132 | 133 | ```html 134 | @media print { 135 | body { font-size: 10pt; } 136 | } 137 | 138 | @media screen { 139 | body { font-size: 13px; } 140 | } 141 | 142 | @media screen, print { 143 | body { line-height: 1.2; } 144 | } 145 | 146 | @media only screen 147 | and (min-width: 320px) 148 | and (max-width: 480px) 149 | and (resolution: 150dpi) { 150 | body { line-height: 1.4; } 151 | } 152 | ``` 153 | Media Queries Level 4 引入了一种新的范围语法,在测试接受范围的任何特性时允许更简洁的媒体查询,如下面的示例所示: 154 | ```html 155 | @media (height > 600px) { 156 | body { line-height: 1.4; } 157 | } 158 | 159 | @media (400px <= width <= 700px) { 160 | body { line-height: 1.4; } 161 | } 162 | ``` 163 | ## 视口(VIEWPORT)比例的长度单位 164 | 视口百分比长度定义相对于 [viewport](https://developer.mozilla.org/zh-CN/docs/Glossary/Viewport) 的大小的 值,即文档的可见部分。 165 | 166 | - vh 167 | - 视口的初始[包含块](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Containing_block)的高度的 1%。 168 | - vw 169 | - 视口的初始[包含块](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Containing_block)的宽度的 1%。 170 | - vmin 171 | - 视口高度 vw 和宽度 vh 两者之间的最小值。 172 | - vmax 173 | - 视口高度 vw 和宽度 vh 两者之间的最大值。 174 | ## percentage 175 | [CSS](https://developer.mozilla.org/en-US/CSS) 数据类型 表述一个百分比值。许多 [CSS 属性](https://developer.mozilla.org/en-US/CSS_Reference) 可以取百分比值,经常用以根据父对象来确定大小。百分比值由一个具体数值后跟着%符号构成.。就像其它的 css 单位一样,在%和数值之间是不允许有空格的。 176 | 177 | 许多长度属性都可以使用百分比,如 [width](https://developer.mozilla.org/zh-CN/docs/Web/CSS/width), [height](https://developer.mozilla.org/zh-CN/docs/Web/CSS/height), [margin](https://developer.mozilla.org/zh-CN/docs/Web/CSS/margin), [padding](https://developer.mozilla.org/zh-CN/docs/Web/CSS/padding)。百分比也可以在 [font-size](https://developer.mozilla.org/zh-CN/docs/Web/CSS/font-size) 看到,其中的文字大小是与其父级元素的大小相关。 178 | -------------------------------------------------------------------------------- /HTTP/HTTP常用状态码.md: -------------------------------------------------------------------------------- 1 | # Http 相关分享 2 | 3 | > HTTP 协议一般指 HTTP(超文本传输协议)。超文本传输协议(英语:HyperText Transfer Protocol,缩写:HTTP)是一种用于分布式、协作式和超媒体信息系统的应用层协议,是因特网上应用最为广泛的一种网络传输协议,所有的 WWW 文件都必须遵守这个标准。 4 | 5 | HTTP 是为 Web 浏览器与 Web 服务器之间的通信而设计的,但也可以用于其他目的。 6 | 7 | HTTP 是一个基于 TCP/IP 通信协议来传递数据的(HTML 文件、图片文件、查询结果等)。 8 | 9 | 10 | ## HTTP 常见状态码及其应用 11 | 12 | ### 1.HTTP 状态码分类 13 | 14 | > 状态码的职责是当客户端向服务器端发送请求时,描述返回的请求结果.用户可以知道服务器是正常处理了请求,还是出现了错误. 15 | 16 | | 分类 | 分类描述 | 17 | | :---: | :---: | 18 | | 1** | 信息,服务器收到请求,需要请求者继续执行操作 | 19 | | 2** | 成功,操作被成功接收并处理 | 20 | | 3** | 重定向,需要进一步的操作以完成请求 | 21 | | 4** | 客户端错误,请求包含语法错误或无法完成请求 | 22 | | 5** | 服务器错误,服务器在处理请求的过程中发生了错误 | 23 | 24 | 25 | ### 2.HTTP 常见状态码 26 | 27 | 据查,仅记录在 [RFC2616](https://cloud.tencent.com/developer/section/1190064) 上的 HTTP 状态码就达到 40 种,若再加上 WebDAV(Web-based Distributed Authoring and Versioning,基于万维网的分布式创作和版本控制)([RFC4918](https://cloud.tencent.com/developer/section/1190067),5842)和附加 HTTP 状态码([RFC6585](https://www.rfc-editor.org/rfc/rfc6585))等扩展,数量就达 60 余种.别看种类繁多,实际上经常使用的大概只有 14 种,如下表所示: 28 | 29 | | 状态码 | 状态码英文名称 | 中文描述 | 30 | | :---: | :--- | :--- | 31 | | 200 | OK | 请求成功。一般用于 GET 与 POST 请求 | 32 | | 204 | No Content | 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档 | 33 | | 206 | Partial Content | 部分内容。服务器成功处理了部分 GET 请求 | 34 | | 301 | Moved Permanently | 永久移动。请求的资源已被永久的移动到新 URI,返回信息会包括新的 URI,浏览器会自动定向到新 URI。今后任何新的请求都应使用新的 URI 代替 | 35 | | 302 | Found | 临时移动。与 301 类似。但资源只是临时被移动。客户端应继续使用原有 URI | 36 | | 303 | See Other | 查看其它地址。与 301 类似。使用 GET 和 POST 请求查看 | 37 | | 304 | Not Modified | 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源 | 38 | | 307 | Temporary Redirect | 临时重定向。与 302 类似。使用 GET 请求重定向 | 39 | | 400 | Bad Request | 客户端请求的语法错误,服务器无法理解 | 40 | | 401 | Unauthorized | 请求要求用户的身份认证 | 41 | | 403 | Forbidden | 服务器理解请求客户端的请求,但是拒绝执行此请求 | 42 | | 404 | Not Found | 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面 | 43 | | 405 | Method Not Allowed | 客户端请求中的方法被禁止 | 44 | | 500 | Internal Server Error | 服务器内部错误,无法完成请求 | 45 | | 503 | Service Unavailable | 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的 Retry-After 头信息中 | 46 | 47 | 48 | - 更多状态码 [https://www.runoob.com/http/http-status-codes.html]() 49 | 50 | ## HTTP 常见请求头/响应头及其应用 51 | 52 | HTTP 协议的请求和响应报文中必定包含 HTTP 首部。首部内容为客户端和服务器分别处理请求和响应提供所需要的信息,主要包括报文主体大小,所使用的语言,认证信息等。 53 | 54 | ### 3.首部字段的类型 55 | 56 | HTTP 首部字段根据实际用途被分为以下四种类型: 57 | 58 | - 通用首部字段:请求与响应报文都会使用的首部 59 | - 请求首部字段:请求报文使用,补充请求的附加内容,客户端信息,响应内容相关优先级等信息 60 | - 响应首部字段:响应报文使用,补充响应的附加内容,也会要求客户端附加额外的内容信息 61 | - 实体首部字段:针对请求和响应报文的实体部分使用的首部,补充了资源内容更新时间等与实体有关的信息。 62 | 63 | ### 4.通用首部字段 64 | | 首部字段名 | 说明 | 65 | | :---: | :---: | 66 | | Cache-Control | 控制缓存的行为 | 67 | | Connection | 逐跳首部、连接的管理 | 68 | | Date | 创建报文的日期时间 | 69 | | Pragma | 报文指令 | 70 | | Trailer | 报文末端的首部一览 | 71 | | Transfer-Encoding | 指定报文主体的传输编码方式 | 72 | | Upgrade | 升级为其他协议 | 73 | | Via | 代理服务器的相关信息 | 74 | | Warning | 错误通知 | 75 | 76 | 77 | ### 5.请求首部字段 78 | | 首部字段名 | 说明 | 79 | | :---: | :---: | 80 | | Accept | 用户代理可处理的媒体类型 | 81 | | Accept-Charset | 优先的字符集 | 82 | | Accept-Encoding | 优先的内容编码 | 83 | | Accept-Language | 优先的语言(自然语言) | 84 | | Authorization | Web 认证信息 | 85 | | Expect | 期待服务器的特定行为 | 86 | | From | 用户的电子邮箱地址 | 87 | | Host | 请求资源所在服务器 | 88 | | If-Match | 比较实体标记(ETag) | 89 | | If-Modified-Since | 比较资源的更新时间 | 90 | | If-None-Match | 比较实体标记(与 If-Match 相反) | 91 | | If-Range | 资源未更新时发送实体 Byte 的范围请求 | 92 | | If-Unmodified-Since | 比较资源的更新时间(与 If-Modified-Since 相反) | 93 | | Max-Forwards | 最大传输逐跳数 | 94 | | Proxy-Authorization | 代理服务器要求客户端的认证信息 | 95 | | Range | 实体的字节范围请求 | 96 | | Referer | 对请求中 URI 的原始获取方 | 97 | | TE | 传输编码的优先级 | 98 | | User-Agent | HTTP 客户端程序的信息 | 99 | 100 | 101 | ### 6.请求首部字段 102 | | 首部字段名 | 说明 | 103 | | :---: | :---: | 104 | | Accept-Ranges | 是否接受字节范围请求 | 105 | | Age | 推算资源创建经过时间 | 106 | | ETag | 资源的匹配信息 | 107 | | Location | 令客户端重定向至指定 URI | 108 | | Proxy-Authenticate | 代理服务器对客户端的认证信息 | 109 | | Retry-After | 对再次发起请求的时机要求 | 110 | | Server | HTTP 服务器的安装信息 | 111 | | Vary | 代理服务器缓存的管理信息 | 112 | | WWW-Authenticate | 服务器对客户端的认证信息 | 113 | 114 | 115 | ### 7.实体首部字段 116 | | 首部字段名 | 说明 | 117 | | :---: | :---: | 118 | | Allow | 资源可支持的 HTTP 方法 | 119 | | Content-Encoding | 实体主体适用的编码方式 | 120 | | Content-Language | 实体主体的自然语言 | 121 | | Content-Length | 实体主体的大小(单位:字节) | 122 | | Content-Location | 替代对应资源的 URI | 123 | | Content-MD5 | 实体主体的报文摘要 | 124 | | Content-Range | 实体主体的位置范围 | 125 | | Content-Type | 实体主体的媒体类型 | 126 | | Expires | 实体主体过期的日期时间 | 127 | | Last-Modified | 资源的最后修改日期时间 | 128 | 129 | 130 | ## 演示常用的字段作用 131 | 132 | example:Content-Type 的常见格式 133 | 134 | text/html : HTML 格式 135 | 136 | text/plain :纯文本格式 137 | 138 | text/xml : XML 格式 139 | 140 | image/gif :gif 图片格式 141 | 142 | image/jpeg :jpg 图片格式 143 | 144 | image/png:png 图片格式 145 | 146 | application/xhtml+xml :XHTML 格式 147 | 148 | application/xml : XML 数据格式 149 | 150 | application/atom+xml :Atom XML 聚合格式 151 | 152 | application/json : JSON 数据格式 153 | 154 | application/pdf :pdf 格式 155 | 156 | application/msword : Word 文档格式 157 | 158 | application/octet-stream : 二进制流数据(如常见的文件下载) 159 | 160 | application/x-www-form-urlencoded : 中默认的 encType,form 表单数据被编码为 key/value 格式发送到服务器(表单默认的提交数据的格式) 161 | 162 | multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式 以上就是我们在日常的开发中,经常会用到的若干 content-type 的内容格式。 163 | 164 | ... 165 | -------------------------------------------------------------------------------- /HTTP/Http 缓存策略.md: -------------------------------------------------------------------------------- 1 | ## 背景与定义 2 | ### 1.背景 3 | > 为什么Web应用需要使用http缓存策略 4 | 5 | 一个Web应用在生产环境中,往往经常会被用户访问很多次,现代计算机的发展水平,网页页面加载和渲染的速度是非常快速的,可以做到毫秒级别。然而在页面加载和渲染整个过程中,比较慢的环节就是网络请求。考虑到用户的使用体验,我们期望应用的加载速度越快越好,因此在做页面性能优化的时候,我们就要去针对那个最大的瓶颈入手——网络请求。而且由于网络环境具有不稳定性,会加剧我们的网络请求以及页面加载的不稳定性。因此更加提升了我们优化网络请求的必要性。 6 | 7 | 为了让网络请求更快一些,我们就要尽量减少我们网络请求资源的“体积”和“数量”,基于这个背景,我们需要使用缓存策略。而Http协议也为我们提供了非常强大的缓存机制。 8 | ### 2.定义 9 | > 什么是浏览器缓存 10 | 11 | 浏览器缓存是浏览器在本地磁盘对用户最近请求过的特定资源进行存储,当访问者再次访问同一页面时,浏览器就可以直接从本地磁盘加载可以多次使用的特定资源。用来减少发送不必要的网络请求,加块页面的加载速度,提升用户的使用体验。 12 | ## 强制缓存和协商缓存 13 | ### 1.强制缓存 14 | ![图片1.png](https://cdn.nlark.com/yuque/0/2022/png/21814148/1650191055530-e9f72aac-cc3e-4b2f-9a74-779083e0ac45.png#clientId=ucd45c617-c8fa-4&from=ui&id=u63ef1bd8&name=%E5%9B%BE%E7%89%871.png&originHeight=554&originWidth=768&originalType=binary&ratio=1&rotation=0&showTitle=false&size=108390&status=done&style=none&taskId=u728ee179-71b6-44d9-bc21-167cf2da200&title=) 15 | 初次请求,如果服务端感觉这个资源可以被缓存的话(通常是一些静态资源,如js、css、图片等),它就会在 response headers 中加一个Cache-Control,如果服务端感觉这个资源,没法被缓存,或者说不适合被缓存,它就不会加这个Cache-Control,也就是说,Cache-Control是在 response headers中由服务端返回的。 16 | ![图片5.png](https://cdn.nlark.com/yuque/0/2022/png/21814148/1650195962871-0f3bd219-430e-43cf-b30e-779e765ae8b3.png#clientId=u81ddd212-b674-4&from=ui&id=ueed1f9dd&name=%E5%9B%BE%E7%89%875.png&originHeight=420&originWidth=676&originalType=binary&ratio=1&rotation=0&showTitle=false&size=41666&status=done&style=none&taskId=uacb8c2a1-b568-4358-85a0-264440f7f0b&title=) 17 | max-age :以秒级别的缓存最长的过期时间; 18 | 19 | no-cache:不用强制缓存(本地缓存); 20 | 21 | no-store:不用强制缓存,也不用协商缓存,就直接让服务端简单粗暴地把资源再重新返给浏览器客户端 22 | 23 | private:针对最终用户做一个缓存 24 | 25 | public:允许中间的一些路由或者中间一些代理也可以做缓存 26 | 27 | expires 28 | 29 | 例如Cache-Control: max-age=31536000(单位是秒)这里就代表一年的时间。 30 | 31 | ![图片2.png](https://cdn.nlark.com/yuque/0/2022/png/21814148/1650194866150-e1f75825-d3e3-44f5-8e22-0f9d5fdf20ff.png#clientId=u81ddd212-b674-4&from=ui&id=u77b5fe1f&name=%E5%9B%BE%E7%89%872.png&originHeight=472&originWidth=800&originalType=binary&ratio=1&rotation=0&showTitle=false&size=202226&status=done&style=none&taskId=u2927a808-0a6e-4734-b659-f753f62621c&title=) 32 | 这里举百度某个页面的一个例子,max-age=5184000,单位是秒 。缓存挺长一段时间。 33 | 34 | 35 | ![图片3.png](https://cdn.nlark.com/yuque/0/2022/png/21814148/1650194959769-a0dee231-f9ff-4f2f-a468-6ee071d781ba.png#clientId=u81ddd212-b674-4&from=ui&id=u7bebf8a9&name=%E5%9B%BE%E7%89%873.png&originHeight=550&originWidth=693&originalType=binary&ratio=1&rotation=0&showTitle=false&size=116131&status=done&style=none&taskId=ubcd07ee6-346a-4ec9-b042-6772b11fb85&title=) 36 | 如果客户端再次请求,之前有Cache-Control,客户端会把这个资源给缓存下来。再次请求的时候,它只要判断这个Cache-Control这个时间还有效,没有过期,然后浏览器就会在本地缓存里面去找这个资源,他就不会到服务端,不经过网络,然后直接返回这个资源。这样就会更快一些,因为读取本地缓存是一个非常快的一个过程。 37 | 38 | 39 | ![图片4.png](https://cdn.nlark.com/yuque/0/2022/png/21814148/1650195378483-7d3c542e-00df-49fd-a1ac-6f55cb45b6ee.png#clientId=u81ddd212-b674-4&from=ui&id=u2faa2404&name=%E5%9B%BE%E7%89%874.png&originHeight=316&originWidth=933&originalType=binary&ratio=1&rotation=0&showTitle=false&size=84422&status=done&style=none&taskId=u1ccfd950-7e0a-4bec-8d44-2afd981c7d5&title=) 40 | 41 | 这里是一个例子,它使我们达到了要加快访问速度的一个目的。前提是你之前有过初次请求并且服务端已经设置了Cache-Control,然后到浏览器,浏览器会自动地根据Cache-Control,来把这个资源存下来。 42 | 43 | 44 | 如果有一天,缓存失效了,也就是说浏览器判断之前的Cache-Control的这个max-age的时间和现在对不上了,缓存已经失效了,已经超过最大的一个时间,这个时候浏览器就会再次去请求服务端,再次请求服务端,服务端会重新返回这个资源和 Cache-Control,又去设置了一个新的过期时间 max-age。如果再次请求,就又会执行一遍刚刚的逻辑。 45 | 46 | 这就是http缓存策略中的强制缓存。 47 | 48 | ### 2.协商缓存 49 | 协商缓存是一个服务端缓存策略,也就是,服务端来判断这个资源是不是可以使用缓存。 50 | 51 | 如果和本地缓存的资源一致,服务端返回304,命中缓存;否则返回200和最新的资源。 52 | ![图片7.png](https://cdn.nlark.com/yuque/0/2022/png/21814148/1650196961356-c3c79f8e-0a80-4384-bae9-74661d78ce19.png#clientId=u81ddd212-b674-4&from=ui&id=ubcbc78df&name=%E5%9B%BE%E7%89%877.png&originHeight=546&originWidth=732&originalType=binary&ratio=1&rotation=0&showTitle=false&size=126550&status=done&style=none&taskId=uc0ddc555-ec7b-4d33-9256-f06bc4db13b&title=) 53 | 54 | 这里因为资源标识比较短,体积比较小,我们带上之后他可以很快地到达我们这个服务端。 55 | 56 | 这里资源标识,首先它是在response headers里面的,因为它是服务端返回的,有两种: 57 | 58 | Last-Modified:资源的最后修改时间 59 | 60 | Etag:资源的唯一标识(服务端可以根据资源的文件内容生成它唯一的一个标识,通常是一个字符串,类似于人的指纹) 61 | ![图片8.png](https://cdn.nlark.com/yuque/0/2022/png/21814148/1650197574974-094e1803-7926-4f28-9e5e-8b17820963ca.png#clientId=u81ddd212-b674-4&from=ui&id=u097abad6&name=%E5%9B%BE%E7%89%878.png&originHeight=558&originWidth=834&originalType=binary&ratio=1&rotation=0&showTitle=false&size=131692&status=done&style=none&taskId=u00bbc7c9-d8cd-4aa7-b3a0-7828f260ed4&title=) 62 | if-modified-since:它的值就是Last-Modified,就是这个资源的最后修改时间 63 | 64 | 初次请求到服务端,服务端感觉这个资源可以被缓存,然后就返回资源,然后这个Last-Modified(资源的最后修改时间),然后到浏览器,浏览器接收到资源之后,就可以把资源先缓存起来,因为将来它可能会命中缓存。 65 | 如果是一样那就证明我之前在返回这个last-modified的之后,我服务端的资源还没有改过。如果是在这种情况下,服务端会返回一个304,告诉浏览器说,我们这个资源从上次返回到现在都没有改动过,你可以继续用缓存,这个时候这次返回,体积就非常小,因为它只返回了304的一个状态码,效率非常高。 66 | 67 | 如果有一种情况说,我们上次返回资源之后,我们的这个资源被修改了,这个之前的if-modified-since的时间就不是最新的了,服务端就知道你请求的资源已经被改过了,这个时候我就可以返回一个新的last-modified,而不是304了,所以说我们就返回一个200和新的资源以及last-modified。if-modified-since带的永远是最近一次返回的last-modified的返回的那个时间。然后服务端根据这个if-modified-since带来的这个时间和我这个资源最后修改时间做一个协商做一个对比,然后看看能不能返回一个304,如果说资源被改了,那我们哪怕慢一点我们也不能返回一个错误的信息。我就直接返回200,资源,新的last-modified,力求下次能命中缓存。 68 | ![图片9.png](https://cdn.nlark.com/yuque/0/2022/png/21814148/1650197955028-0c4ce948-1917-451b-a663-0a699abcd657.png#clientId=u81ddd212-b674-4&from=ui&id=uf19c450c&name=%E5%9B%BE%E7%89%879.png&originHeight=558&originWidth=863&originalType=binary&ratio=1&rotation=0&showTitle=false&size=122146&status=done&style=none&taskId=u358851eb-ea9d-4192-aef3-dbf0d9f9b1f&title=) 69 | if-none-match:它的值就是Etag,就是这个资源的唯一标识。 70 | 71 | Etag是一样的,具体过程类比last-modified。只不过etag是拿这个资源算一个类似于指纹一样的字符串(唯一的)来做对比,做协商。最后的结果呢就是如果命中成功,我们就返回一个304,否则就返回200,新的资源,新的etag。 72 | ![图片10.png](https://cdn.nlark.com/yuque/0/2022/png/21814148/1650198042414-98464e2b-49b9-4be5-a147-c03b45bc3014.png#clientId=u81ddd212-b674-4&from=ui&id=uea2c92d9&name=%E5%9B%BE%E7%89%8710.png&originHeight=439&originWidth=1061&originalType=binary&ratio=1&rotation=0&showTitle=false&size=217145&status=done&style=none&taskId=u9463bf09-d7c4-4416-b940-f0396bd33e6&title=) 73 | ![图片11.png](https://cdn.nlark.com/yuque/0/2022/png/21814148/1650198109932-36a26fe2-8525-43ff-b5b6-818799d94914.png#clientId=u81ddd212-b674-4&from=ui&id=ud26d1fba&name=%E5%9B%BE%E7%89%8711.png&originHeight=513&originWidth=738&originalType=binary&ratio=1&rotation=0&showTitle=false&size=162818&status=done&style=none&taskId=u258697ad-1a87-4318-bbb5-7e8a236d758&title=) 74 | 没有返回新的资源,只是返回一个304 , 这个size是非常小的。0.6k和5.6kb相比中间相差还是非常大的。所以说,命中缓存是一个非常高效非常快速的一个方式。 75 | 76 | 共存的话,会优先使用etag,原因是last-modified只能精确到秒级,秒级对于计算机来说他还是比较宽泛的一个时间段。码农角度来看计算机我们所有的程序执行是以毫秒为单位的。甚至是在毫秒以内,所以说秒级还是比较宽泛的概念,如果资源被重复生成,而内容不变,这个etag会更精准一些。 77 | 78 | ![图片12.png](https://cdn.nlark.com/yuque/0/2022/png/21814148/1650198322725-9e7e9963-1120-42d4-89d1-6a18bd535bfb.png#clientId=u81ddd212-b674-4&from=ui&id=u476fccef&name=%E5%9B%BE%E7%89%8712.png&originHeight=554&originWidth=725&originalType=binary&ratio=1&rotation=0&showTitle=false&size=110891&status=done&style=none&taskId=u881384c6-a039-419d-8ba3-f7893b781e9&title=) 79 | 最后我们拿一个统一的图做一个例子。 80 | 81 | 首先发送请求,有缓存,也就是我们命中这个cache-control也就是强制缓存,这是客户端,然后缓存是否过期,也就是说我们cache-control里面有个max-age,也就是最大缓存时间,如果缓存没有过期的话就读取缓存,也就是强制缓存,然后页面呈现,这个就是我们讲的这个cache-control的这个强制缓存。 82 | 如果max-age过期了,那就看一下有没有etag 和last-modified,如果没有etag和last-modified,那我们没法进行我们的协商缓存,那就向http发送请求,然后服务器返回资源,然后页面呈现。 83 | 84 | 如果说有etag和last-modified,我们向服务器发送请求的时候就需要带上if-none-match和这个if-modified-since,这两个值分别是这个etag的值和last-modified这个值,让服务端去判断去协商,服务端判断这个缓存是不是可用,是服务器根据if-none-match和if-modified-since去做对比,如果是缓存可用的话,就返回304,返回304然后客户端可以读取本地缓存,然后就页面呈现,这个就是协商缓存,如果这个地方判断过期了,缓存不可用,那就是返回状态码200,直接返回资源,然后页面呈现。 85 | 86 | 以上就是Http缓存策略。 87 | 88 | ## 浏览器的三种刷新操作 89 | ![图片13.png](https://cdn.nlark.com/yuque/0/2022/png/21814148/1650207322551-3e6db248-73a9-40fd-ba3d-06f18e0fa119.png#clientId=u41c7b69f-f882-4&from=ui&id=u9480bdf6&name=%E5%9B%BE%E7%89%8713.png&originHeight=412&originWidth=859&originalType=binary&ratio=1&rotation=0&showTitle=false&size=87526&status=done&style=none&taskId=uf0ac1163-1325-4a1c-900d-f72db7f96f4&title=) 90 | ![图片14.png](https://cdn.nlark.com/yuque/0/2022/png/21814148/1650207465360-f4a89fd6-90a5-4904-b0b8-5de2fdc9896b.png#clientId=u41c7b69f-f882-4&from=ui&id=ub80543a6&name=%E5%9B%BE%E7%89%8714.png&originHeight=410&originWidth=849&originalType=binary&ratio=1&rotation=0&showTitle=false&size=126026&status=done&style=none&taskId=udfef04b4-1428-428e-8031-eede28b53a0&title=) 91 | 这三种操作对于缓存是有一定影响的: 92 | 93 | 如果手动点击刷新按钮,这个时候强制缓存就会失效,Cache-control就会失效。但是把所有的请求发给服务端,服务端协商缓存还是有效的。 94 | 95 | 强制缓存和协商缓存这两个策略,一是在客户端,一个是在服务端。 96 | 97 | 手动刷新的时候,强制缓存失效,这个时候我们还有一个兜底的方案, 就是我们在服务端可以做一个协商缓存,这样也会让我们的页面加载速度更快一些。 98 | 99 | 如果是强制刷新的话,也就是我们按住 Ctrl + F5 的话,那我们的强制缓存和协商缓存就全部都失效。因为如果我们要强制刷新,服务端肯定得把所有资源都全部返回,不管加载多慢,也要返回给我们最新的结果,所以说这两个全部失效。那就所有的请求全部都返回200,返回最新的资源。 100 | 101 | 102 | -------------------------------------------------------------------------------- /HTTP/Restful API.md: -------------------------------------------------------------------------------- 1 | 总耗时约30分钟,若时间过长需简短可告知哈~ 2 | # 1.Restful API是什么? 3 | ## 1.1来源 4 | REST首次出现在2000年[Roy Thomas Fielding](https://zh.wikipedia.org/w/index.php?title=Roy_Thomas_Fielding&action=edit&redlink=1)博士的《架构风格与基于网络的软件架构设计》论文中提出来的一种[万维网](https://zh.wikipedia.org/wiki/%E4%B8%87%E7%BB%B4%E7%BD%91)[软件架构](https://zh.wikipedia.org/wiki/%E8%BD%AF%E4%BB%B6%E6%9E%B6%E6%9E%84)风格,Roy Fielding是HTTP规范的主要编写者之一。 他在论文中提到:"我这篇文章的写作目的,就是想在**符合架构原理**的前提下,理解和评估以网络为基础的应用软件的架构设计,得到一个功能强、性能好、适宜通信的架构。" 5 | 6 | - REST 本身并没有创造新的技术、组件或服务,它只是一种**软件架构风格**,是一组**架构约束条件和原则**,而不是技术框架。 7 | - 隐藏在RESTful背后的理念就是**使用Web的现有特征和能力**,更好地使用现有Web标准中的一些准则和约束。 8 | 9 | 论文地址:[Architectural Styles and the Design of Network-based Software Architectures](https://link.zhihu.com/?target=http%3A//www.ics.uci.edu/~fielding/pubs/dissertation/top.htm) 10 | REST章节:[Fielding Dissertation: CHAPTER 5: Representational State Transfer (REST)](https://link.zhihu.com/?target=http%3A//www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) 11 | ## 1.2名称解释 12 | ### REST 13 | 全称: **Re**presentational **S**tate **T**ransfer (省略了主语Resource) 14 | 维基百科翻译为:**表现层状态转换** 15 | 通俗来说就是:资源在网络中以某种表现形式进行状态转移。 16 | 分解开来: 17 | 18 | - Resource:资源,即数据 19 | - 它可以是一段文本、一张图片、一首歌曲、一种服务等。 20 | - 每种资源对应一个特定的URI(统一资源定位符)。要获取这个资源,访问它的URI就可以。 21 | - Representational:表现层 22 | - **我们把"资源"具体呈现出来的形式,叫做它的"表现层"(Representation)。** 23 | - 比如: 24 | - 文本可以用txt格式表现,也可以用HTML格式、XML格式、JSON格式表现,甚至可以采用二进制格式; 25 | - 图片可以用JPG格式表现,也可以用PNG格式表现。 26 | - State Transfer:**状态转换** 27 | - 访问一个网站,就代表了客户端和服务器的一个互动过程。会涉及到数据和状态的变化。 28 | ### Restful 29 | 其中ful代表形容词,代表满足REST原则的。 30 | ### RESTful API 31 | RESTful API代表符合REST规范的Web API。 32 | ## **1.3REST架构限制条件** 33 | [Roy Thomas Fielding](https://zh.wikipedia.org/w/index.php?title=Roy_Thomas_Fielding&action=edit&redlink=1)的博士在论文中提出REST架构应满足以下6个原则: 34 | 35 | 1. 客户端-服务端(Client-Server) 36 | - 通过客户端往服务端发送请求 37 | - 客户端和服务端的分离可以更好的实现跨多平台的应用 38 | 2. 无状态(Stateless) 39 | - 服务器不保存客户端的信息(请求,无须保持 session) 40 | - 客户端保存状态信息,每次请求携带所有必须的状态信息(如:请求的数据、头部信息、请求的返回等),服务器端根据这些状态信息来处理请求。 41 | - 无状态对于服务端的弹性扩容很重要 42 | - _(服务器可以将会话状态信息传递给其他服务,比如数据库服务,这样可以保持一段时间的状态信息,从而实现认证功能。)_ 43 | 3. 可缓存(Cacheability) 44 | - 服务端需回复是否可以缓存 45 | - 管理良好的缓存机制可以减少客户端-服务器之间的交互。(_甚至完全避免客户端-服务器交互,这进一步提了高性能和可扩展性,并预防客户端在将来进行请求的时候得到陈旧的或者不恰当的数据。_) 46 | 4. 统一接口(Uniform Interface) 47 | - 是 RESTful 系统设计的基本出发点。通过一定的原则设计接口,可以降低耦合性,简化系统架构(可以让所有模块各自独立的进行改进。) 48 | 5. 分层系统(Layered System) 49 | - 客户端一般不知道是否直接连接到了最终的服务器,或者是路径上的中间服务器。 50 | - 分层允许我们灵活的部署服务端项目,例如中间服务器可以通过负载均衡和共享缓存的机制提高系统的可扩展性,这样可也便于安全策略的部署 51 | 6. 按需代码(Code-On-Demand,可选) 52 | - 服务器可以通过发送可执行代码给客户端的方式,临时性的扩展功能或者定制功能。(例如Java Applet、Flash或JavaScript。) 53 | # 2.为什么需要Restful API? 54 | 随着安卓、IOS、小程序等形式的客户端层出不穷,客户端的种类出现多元化,**而客户端和服务端需要接口进行通信**,但接口的**规范性**就成了一个问题: 55 | ![](https://cdn.nlark.com/yuque/0/2022/png/26213287/1650763588982-bb4dddbb-4041-4927-b123-f96b9a00c8fd.png#clientId=u44902f92-fbdb-4&from=paste&id=u75f96e2a&originHeight=782&originWidth=1646&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=ufef987aa-30b2-4136-8318-ed5e461b9fe&title=) 56 | 所以定义一套**结构清晰、符合标准、易于理解、扩展方便**,让大部分人都能够理解和接受的**接口风格**就显得越来越重要,而RESTful风格的接口刚好有以上特点,就逐渐流行起来。 57 | ![](https://cdn.nlark.com/yuque/0/2022/png/26213287/1650763588975-fc6f3af9-8d89-4040-9e4c-94251e4b53ad.png#clientId=u44902f92-fbdb-4&from=paste&id=u243e009e&originHeight=698&originWidth=1560&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u0c7cb9fa-3436-437a-b413-a0e6ef46c09&title=) 58 | # 3.Restful API怎么用? 59 | ## RESTful API设计原则 60 | RESTful API 的核心是**规范**。不同公司、不同团队、不同项目可能采取不同的 REST 设计原则,以下所列的是大家比较**公认**的一些原则。 61 | ### 1.请求设计 62 | 客户端可以向服务端发送携带数据的请求、服务端会返回适当的响应,构造一个请求可能需要: 63 | 64 | 1. **端点(Endpoint)**:它是 REST 服务器监听的 URL 地址。 65 | 2. **请求方法(Method)**:REST 为不同的请求类型实现了许多“方法”,以下是最常用的:-**GET**:从服务器获取资源。-**POST**:在服务器上创建资源。-**PUT**:更新(全部更新/替换)服务器上的现有资源。需传递所有属性。-**PATCH**:部分更新服务器上的现有资源。传递需更新的(部分)属性即可。-**DELETE**:删除服务器上现有的资源。 66 | 3. **头部信息(Headers)**:用于客户端和服务端通讯的额外信息(REST 是无状态的)。常见的头部信息如下:**请求头(Request)**:-_host_:客户端的 IP 地址(或请求的源地址)-_accept-language_:客户端接受的语言类型-_user-agent_:客户端的详细信息,如操作系统和浏览器类型**响应头(Response)**:-_status_:请求的状态或 HTTP 状态码-_content-type_:服务器返回的资源的类型-_set-cookie_:服务器设置的 cookies 67 | 4. **请求数据(Data)**:(也称为请求体或消息)包含了将要发送给服务器的数据 68 | ### 2.URI 设计 69 | 资源都是使用 URI 标识的,我们应该按照一定的规范来设计 URI,通过规范化可以使我们的 API 接口更加易读、易用。 70 | 以下是 URI 设计时,应该遵循的一些规范: 71 | 72 | - URL 被视为“资源”,资源名使用名词而不是动词(遵循可寻址性原则,具有自描述性,需要在形式上给人以直觉上的关联) 73 | - 传统API设计:(把操作数据这类动词放在url里面) 74 | - 添加图书 :/book/add 75 | - 查找某一本书 :/book/get?id=123 76 | - 查找某个作者的书籍 :/book/getByAuthor?author=1&page=1 77 | - Restful API设计:(把动词都放在method 请求方法里面) 78 | - 添加图书 :POST /books 79 | - 查找某一本书 :GET /books/123 80 | - 查找某个作者的书籍 :GET /author/1/book?page=1 81 | - 名词用复数表示。 82 | - 因为所用的名词往往与数据库的表格名对应。一般来说,数据库中的表都是同种记录的"**集合**"(collection),所以API中的名词也应该使用复数。 83 | - 资源分为 Collection 和 Member 两种。 84 | - Collection:一堆资源的集合。 85 | - 例如我们系统里有很多用户(User), 这些用户的集合就是 Collection。Collection 的 URI 标识应该是 域名/资源名复数, 例如https:// iam.api.marmotedu.com/users。 86 | - Member:单个特定资源。 87 | - 例如系统中特定名字的用户,就是 Collection 里的一个 Member。Member 的 URI 标识应该是 域名/资源名复数/资源名称, 例如https:// iam.api.marmotedu/users/admin。 88 | - URI 结尾不应包含/。 89 | - URI 中不能出现下划线 _,必须用中杠线 -代替(这里有些人推荐用 _,有些人推荐用 -,统一使用一种格式即可,推荐用 - )。 90 | - URI 路径用小写,不要用大写。 91 | - 避免层级过深的 URI。超过 2 层的资源嵌套会很乱,建议将其他资源转化为?参数,比如:请求某个学校里某个班级里的学生 92 | -  # 不推荐 93 |  /schools/qinghua/classes/rooma/students 94 |  # 推荐 95 |  /students?school=qinghua&class=rooma 96 | 97 | 这里有个地方需要注意:在实际的 API 开发中,可能会发现有些操作不能很好地映射为一个 REST 资源,这时候,你可以参考下面的做法: 98 | 99 | - 将一个操作变成**资源的一个属性**,比如想在系统中暂时禁用某个用户,可以这么设计 URI:/users/zhangsan?active=false。 100 | - 将操作当作是一个资源的嵌套资源,比如一个 GitHub 的加星/去星操作: 101 | -  PUT /gists/:id/star # github star action 102 |  DELETE /gists/:id/star # github unstar action 103 | - 如果以上都不能解决问题,有时可以打破这类规范。比如登录操作,登录不属于任何一个资源,URI 可以设计为:/login。 104 | - 在设计 URI 时,如果你遇到一些不确定的地方,推荐参考 [GitHub 标准 RESTful API](https://docs.github.com/cn/rest)。 105 | ### 3.REST 资源操作映射为 HTTP 方法 106 | REST 风格虽然适用于很多传输协议,但在实际开发中, HTTP 协议已经成了实现 RESTful API 事实上的标准(虽然REST本身受Web技术的影响很深, 但是理论上REST架构风格并不是绑定在HTTP上)。 107 | 基本上 RESTful API 所有的行为都应该是由客户端通过HTTP 协议的4个动词,对服务器端资源进行操作,实现“表现层状态转化”。 108 | REST 架构对资源的操作包括获取、创建、修改和删除,这些操作对应 HTTP 协议提供的 GET、POST、PUT 和 DELETE 方法。 109 | 形成的规范如下表所示:(在资源集合下,x代表x;在单个资源下,x代表x。) 110 | ![image.png](https://cdn.nlark.com/yuque/0/2022/png/26213287/1650763589685-b21f975e-c215-4595-ac53-26df670c0f08.png#clientId=u44902f92-fbdb-4&from=paste&id=u9f0dffae&name=image.png&originHeight=698&originWidth=1524&originalType=url&ratio=1&rotation=0&showTitle=false&size=139233&status=done&style=none&taskId=ucfadfd6b-bbb0-4aa6-a73b-ca4969060d5&title=) 111 | 例:(GET /users 代表x) 112 | ![image.png](https://cdn.nlark.com/yuque/0/2022/png/26213287/1650763589534-ca610f38-fcd7-4338-abd0-769f2bbf75b7.png#clientId=u44902f92-fbdb-4&from=paste&id=ueb81fec8&name=image.png&originHeight=582&originWidth=1754&originalType=url&ratio=1&rotation=0&showTitle=false&size=120270&status=done&style=none&taskId=u83403d25-915e-446c-827a-5f67b045fbb&title=) 113 | 对资源的操作应该满足**安全性**和**幂等性**: 114 | 115 | - 安全性:不会改变资源状态,可以理解为只读的。 116 | - 幂等性:执行 1 次和执行 N 次,对资源状态改变的效果是等价的。 117 | 118 | 使用不同 HTTP 方法时,资源操作的安全性和幂等性对照见下表: 119 | ![image.png](https://cdn.nlark.com/yuque/0/2022/png/26213287/1650763589224-e96b4072-0731-4371-a9aa-70bdae7261db.png#clientId=u44902f92-fbdb-4&from=paste&id=u4a541cac&name=image.png&originHeight=448&originWidth=1434&originalType=url&ratio=1&rotation=0&showTitle=false&size=38498&status=done&style=none&taskId=ua1a1dbf2-9b16-494e-aaa8-9608eb66e80&title=) 120 | 121 | - 安全性 122 | - 只有GET是只读的,POST、PUT、DELETE都可能改变资源状态。所以GET是具有安全性,POST、PUT、DELETE不具有安全性。 123 | - 幂等性 124 | - GET对资源查询多次,结果都是一样的,是幂等操作; 125 | - POST不是幂等操作,一次请求添加一份新资源,二次请求则添加了两份新资源,多次请求会产生不同的结果,因此POST不是幂等操作; 126 | - PUT将A修改为B,它第一次请求时值变为了B,再进行多次此操作,最终的结果还是B,与一次执行的结果是一样的,所以PUT是幂等操作; 127 | - DELETE第一次将资源删除后,后面多次进行此删除请求,最终结果是一样的,将资源删除掉了; 128 | 129 | 下面来看一个幂等性的例子: 130 | 一个为员工涨薪的接口,本次请求发送后为 1 号员工涨薪 500 元。 131 |  PUT https://edu.lagou.com/employee/salary 132 |  {"id" : "1,"incr_salary":500} 133 | **【反面典型 】**如果**没有考虑幂等特性**,可能会这么写,伪代码如下: 134 |  //查询1号员工数据 135 |  Employee employee = employeeService.selectById(1); 136 |  //更新工资为原工资+500 137 |  employee.setSalary(employee.getSalary() + incrSalary); 138 |  //执行更新语句 139 |  employeeService.update(employee) 140 | 对于这段代码,单独执行没有任何问题。 141 | 但是在分布式环境下,为了保证消息的高可靠性,往往客户端会采用重试或消息补偿的形式重复发送同一个请求。 142 | 在这种情况下,这段代码就会出现问题:因为每一个重复请求被发送到服务器都会让该员工工资加 500,最终该员工工资会大于实际应收工资。这样就不符合PUT是幂等的特性。 143 | 那如何解决呢?有一种**乐观锁设计**,在员工表额外增加一个 version 的字段,代表当前数据的版本,任何对该数据修改操作都会让 version 字段值 +1,这个 version 的数值要求附加在请求中,如下所示: 144 |  PUT https://edu.lagou.com/employee/salary 145 |  {"id" : "1,"incr_salary":500 , "version":1} 146 | 执行更新操作前,1 号员工原始数据如下: 147 | 148 | | **id** | **salary** | **version** | 149 | | --- | --- | --- | 150 | | 1 | 3000 | 1 | 151 | 152 | 而服务器端代码也作出如下调整,SQL 语句要增加版本号的比对。 153 | ``` 154 | //查询1号员工数据 155 | Employee employee = employeeService.selectById(1); 156 | //更新工资为原工资+500 157 | employee.setSalary(employee.getSalary() + incrSalary); 158 | //update执行成功,版本号+1 159 | employee.setVersion(employee.getVersion() + 1)); 160 | //执行更新语句 161 | employeeService.update(employee) 162 | ``` 163 | 注意:此时 update 方法,除了 id 条件外,还要增加 version 的判断,如下所示。 164 |  update employee set salary = 3500,version=2 where id = 1 and version=1 165 | 当执行成功后,1 号员工原始数据将工资更新为 3500,版本为 2。 166 | 那这种设计如何保证幂等呢?假设客户端再次发送重复的请求: 167 |  PUT https://edu.lagou.com/employee/salary 168 |  {"id" : "1,"incr_salary":500 , "version":1} 169 | 后台按逻辑仍会执行下面的 update 语句: 170 |  update employee set salary = 3500,version=2 where id = 1 and version=1 171 | 此时,因为数据表中 version 字段已经更新为 2,where 筛选条件将无法得到任何数据。自然就不会产生实质的更新操作,我们幂等性的初衷就已经达到了。 172 | 以上介绍的是实现幂等性方案中 **MVCC 多版本并发控制方案**。**除此以外**,保证幂等性还可以采用**幂等表、分布式锁、状态机**等实现方式。 173 | 在使用 HTTP 方法的时候,需要注意: 174 | 175 | - GET 返回的结果,要尽量可用于 PUT、POST 操作中。 176 | - 例如,用 GET 方法获得了一个 user 的信息,调用者修改 user 的信息,然后将此结果再用 PUT 方法更新。这要求 GET、PUT、POST 操作的资源属性是一致的。 177 | 178 | 在设计 API 时,经常会有批量删除的需求,需要在请求中携带多个需要删除的资源名,但是 HTTP 的 DELETE 方法不能携带多个资源名,这时候可以通过下面三种方式来解决: 179 | 180 | 1. 发起多个 DELETE 请求。 181 | 2. 操作路径中带多个 id,id 之间用分隔符分隔, 例如:DELETE /users?ids=1,2,3 。 182 | 3. 直接使用 POST 方式来批量删除,在body 中传入需要删除的资源列表。 183 | 184 | 其中,第二种是最推荐的方式,因为使用了匹配的 DELETE 动词,并且不需要发送多次 DELETE 请求。 185 | 这三种方式都有各自的使用场景,你可以根据需要自行选择。如果选择了某一种方式,那么整个项目都需要统一用这种方式。 186 | ### 4.统一的返回格式 187 | 接口返回最常用的数据格式是JSON。由于JSON能直接被JavaScript读取,所以,以JSON格式编写的REST风格的API具有简单、易读、易用的特点。 188 | 一般来说,一个系统的 RESTful API 会向外界开放多个资源的接口,接口的返回格式需要注意以下几点: 189 | 190 | - 每个接口的返回格式要保持一致。 191 | - 不然客户端代码要适配不同接口的返回格式,会大大增加学习和使用成本。 192 | - 要包含 code、message 两项,分别对应了服务器处理结果与返回的消息内容。 193 | - data 属性是可选项,包含从响应返回的额外数据。 194 | - 如:查询结果、新增或更新后的数据。 195 | - 遵循统一的 code、message 命名标准。 196 | - 例如:code 以 1xxx 开头代表参数异常,2xxx 开头代表数据库处理异常。不同的公司有不同的命名规则,一定要提前定义好并要求开发团队严格按语义使用编码 197 | - RESTful 接口要求返回的是 JSON 或者 XML 数据,绝不可以包含任何与具体展现相关的内容。 198 | 199 | 返回的格式没有强制的标准,可以根据实际的业务需要返回不同的格式。 200 | **【反面典型 】**例如,要查询 10 号员工数据。 201 |  GET http(s)://edu.com/employee/10 202 |  { 203 |      "code" : "0" , 204 |      "message" : "success" , 205 |      "data" : "

员工姓名:张三

..." 206 |  } 207 | 在上面的 JSON 响应中,data 返回了与具体展现相关的内容,会导致不好扩展等问题。 208 | 改为下面的样式是较好的数据结构,最终数据如何展现将由客户端 HTML、APP 进行渲染。 209 |  GET http(s)://edu.com/employee/10 210 |  { 211 |      "code" : "0" ,         //--->服务器处理结果 212 |      "message" : "success" ,//--->返回的消息内容 213 |      "data" : {             //--->返回的数据 214 |          "name":"张三", 215 |          "age" : 36 216 |     } 217 |  } 218 | ### 5.API 版本管理 219 | 随着时间的推移、需求的变更,一个 API 往往满足不了现有的需求,这时候就需要对 API 进行修改。对 API 进行修改时,不能影响**其他调用系统**的正常使用,这就要求 API 变更做到**向下兼容**,也就是**新老版本共存**。 220 | 但在实际场景中,很可能会出现同一个 API 无法向下兼容的情况。这时候最好的解决办法是从一开始就引入 API 版本机制,当不能向下兼容时,就引入一个**新的版本,老的版本则保留原样**。这样既能保证服务的可用性和安全性,同时也能满足新需求。 221 | API 版本有不同的标识方法,在 RESTful API 开发中,通常将版本标识放在如下 3 个位置: 222 | 223 | - URL 中,比如/v1/users。 224 | - 这样做的好处是直观,GitHub、Kubernetes、Etcd 等很多优秀的 API 均采用这种方式。 225 | - HTTP Header 中,比如Accept: vnd.example-com.foo+json; version=1.0。 226 | - Form 参数中,比如/users?version=v1。 227 | 228 | 这里要注意,有些开发人员不建议将版本放在 URL 中,因为他们觉得不同的版本可以理解成 同一种资源的不同表现形式,所以应该采用同一个 URI。 229 | 对于这一点,没有严格的标准,根据项目实际需要选择一种方式即可。 230 | ### 6.API 命名 231 | API 通常的命名方式有三种,分别是驼峰命名法 (serverAddress)、蛇形命名法 (server_address) 和脊柱命名法 (server-address)。 232 | 驼峰命名法和蛇形命名法都需要切换输入法,会增加操作的复杂性,也容易出错,所以一般建议用脊柱命名法。GitHub API 用的就是脊柱命名法,例如 [selected-actions](https://docs.github.com/en/rest/reference/actions#get-allowed-actions-for-an-organization)。 233 | ### 7.统一分页 / 过滤 / 排序 / 搜索功能 234 | REST 资源的查询接口,通常情况下都需要实现分页、过滤、排序、搜索功能,因为这些功能是每个 REST 资源都可能会用到的,所以可以实现为一个公共的 API 组件。 235 | 下面是一些常见的参数: 236 | 237 | - 分页: 238 | - 在列出一个 Collection 下所有的 Member 时,应该提供分页功能,例如/users?offset=0&limit=20 239 | - limit,指定返回记录的数量; 240 | - offset,指定返回记录的开始位置 241 | - 引入分页功能可以减少 API 响应的延时,同时可以避免返回太多条目,导致服务器 / 客户端响应特别慢,(甚至导致服务器 / 客户端 crash 的情况)。 242 | - 过滤: 243 | - 如果用户不需要一个资源的全部状态属性,可以在 URI 参数里指定返回哪些属性,例如/users?fields=email,username,address。 244 | - 排序: 245 | - 用户很多时候会根据创建时间或者其他因素,列出一个 Collection 中前 100 个 Member,这时可以在 URI 参数中指明排序参数,例如/users?sort=age,desc。 246 | - 搜索: 247 | - 当一个资源的 Member 太多时,用户可能想通过搜索,快速找到所需要的 Member,或着想搜下有没有名字为 xxx 的某类资源,这时候就需要提供搜索功能。搜索建议按模糊匹配来搜索。 248 | ### 8.域名 249 | API 的域名设置主要有两种方式: 250 | 251 | - [https://marmotedu.com/api](https://marmotedu.com/api) (域名/api ),这种方式适合 API 将来**不会**有进一步扩展的情况,比如刚开始 marmotedu.com 域名下只有一套 API 系统,未来也只有这一套 API 系统。 252 | - [https://iam.api.marmotedu.com](https://iam.api.marmotedu.com) (api在域名中),如果 marmotedu.com 域名下未来会新增另一个系统 API,这时候最好的方式是每个系统的 API 拥有专有的 API 域名,比如:storage.api.marmotedu.com,network.api.marmotedu.com。腾讯云的域名就是采用这种方式。 253 | ### 9.状态码 254 | 这部分和HTTP部分相似,就不赘述了。 255 | 不同的公司有不同的命名规则。 256 | #### 状态码必须精确 257 | 客户端的每一次请求,服务器都必须给出回应。回应包括 HTTP 状态码和数据两部分。 258 | HTTP 状态码就是一个三位数,分成五个类别。 259 | 260 | - 1xx:相关信息 261 | - 2xx:操作成功 262 | - 3xx:重定向 263 | - 4xx:客户端错误 264 | - 5xx:服务器错误 265 | 266 | 这五大类总共包含[100多种](https://en.wikipedia.org/wiki/List_of_HTTP_status_codes)状态码,覆盖了绝大部分可能遇到的情况。每一种状态码都有标准的(或者约定的)解释,客户端只需查看状态码,就可以判断出发生了什么情况,所以服务器应该返回尽可能精确的状态码。 267 | 下面介绍其他四类状态码的精确含义: 268 | **2xx 状态码** 269 | 200状态码表示操作成功,但是不同的方法可以返回更精确的状态码。 270 | 271 | - GET: 200 OK 272 | - POST: 201 Created 273 | - PUT: 200 OK 274 | - DELETE: 204 No Content 275 | 276 | 上面代码中,POST返回201状态码,表示生成了新的资源;DELETE返回204状态码,表示资源已经不存在。 277 | 此外,202 Accepted状态码表示服务器已经收到请求,但还未进行处理,会在未来再处理,通常用于异步操作。下面是一个例子。 278 |  HTTP/1.1 202 Accepted 279 |   280 |  { 281 |  "task": { 282 |   "href": "/api/company/job-management/jobs/2130040", 283 |   "id": "2130040" 284 |  } 285 |  } 286 | **3xx 状态码** 287 | API 用不到301状态码(永久重定向)和302状态码(暂时重定向,307也是这个含义),因为它们可以由应用级别返回,浏览器会直接跳转,API 级别可以不考虑这两种情况。 288 | API 用到的3xx状态码,主要是303 See Other,表示参考另一个 URL。它与302和307的含义一样,也是"暂时重定向",区别在于302和307用于GET请求,而303用于POST、PUT和DELETE请求。收到303以后,浏览器不会自动跳转,而会让用户自己决定下一步怎么办。下面是一个例子。 289 |  HTTP/1.1 303 See Other 290 |  Location: /api/orders/12345 291 | **4xx 状态码** 292 | 4xx状态码表示客户端错误,主要有下面几种。 293 | 400 Bad Request:服务器不理解客户端的请求,未做任何处理。 294 | 401 Unauthorized:用户未提供身份验证凭据,或者没有通过身份验证。 295 | 403 Forbidden:用户通过了身份验证,但是不具有访问资源所需的权限。 296 | 404 Not Found:所请求的资源不存在,或不可用。 297 | 405 Method Not Allowed:用户已经通过身份验证,但是所用的 HTTP 方法不在他的权限之内。 298 | 410 Gone:所请求的资源已从这个地址转移,不再可用。 299 | 415 Unsupported Media Type:客户端要求的返回格式不支持。比如,API 只能返回 JSON 格式,但是客户端要求返回 XML 格式。 300 | 422 Unprocessable Entity :客户端上传的附件无法处理,导致请求失败。 301 | 429 Too Many Requests:客户端的请求次数超过限额。 302 | **5xx 状态码** 303 | 5xx状态码表示服务端错误。一般来说,API 不会向用户透露服务器的详细信息,所以只要两个状态码就够了。 304 | 500 Internal Server Error:客户端请求有效,服务器处理时发生了意外。 305 | 503 Service Unavailable:服务器无法处理请求,一般用于网站维护状态。 306 | #### 状态码典型用法: 307 | GET 308 | 309 | - 200(OK) - 表示已在响应中发出 310 | - 204(无内容) - 资源有空表示 311 | - 301(Moved Permanently) - 资源的URI已被更新 312 | - 303(See Other) - 其他(如,负载均衡) 313 | - 304(not modified)- 资源未更改(缓存) 314 | - 400 (bad request)- 指代坏请求(如,参数错误) 315 | - 404 (not found)- 资源不存在 316 | - 406 (not acceptable)- 服务端不支持所需表示 317 | - 500 (internal server error)- 通用错误响应 318 | - 503 (Service Unavailable)- 服务端当前无法处理请求 319 | 320 | POST 321 | 322 | - 200(OK)- 如果现有资源已被更改 323 | - 201(created)- 如果新资源被创建 324 | - 202(accepted)- 已接受处理请求但尚未完成(异步处理) 325 | - 301(Moved Permanently)- 资源的URI被更新 326 | - 303(See Other)- 其他(如,负载均衡) 327 | - 400(bad request)- 指代坏请求 328 | - 404 (not found)- 资源不存在 329 | - 406 (not acceptable)- 服务端不支持所需表示 330 | - 409 (conflict)- 通用冲突 331 | - 412 (Precondition Failed)- 前置条件失败(如执行条件更新时的冲突) 332 | - 415 (unsupported media type)- 接受到的表示不受支持 333 | - 500 (internal server error)- 通用错误响应 334 | - 503 (Service Unavailable)- 服务当前无法处理请求 335 | 336 | PUT 337 | 338 | - 200 (OK)- 如果已存在资源被更改 339 | - 201 (created)- 如果新资源被创建 340 | - 301(Moved Permanently)- 资源的URI已更改 341 | - 303 (See Other)- 其他(如,负载均衡) 342 | - 400 (bad request)- 指代坏请求 343 | - 404 (not found)- 资源不存在 344 | - 406 (not acceptable)- 服务端不支持所需表示 345 | - 409 (conflict)- 通用冲突 346 | - 412 (Precondition Failed)- 前置条件失败(如执行条件更新时的冲突) 347 | - 415 (unsupported media type)- 接受到的表示不受支持 348 | - 500 (internal server error)- 通用错误响应 349 | - 503 (Service Unavailable)- 服务当前无法处理请求 350 | 351 | DELETE 352 | 353 | - 200 (OK)- 资源已被删除 354 | - 301 (Moved Permanently)- 资源的URI已更改 355 | - 303 (See Other)- 其他,如负载均衡 356 | - 400 (bad request)- 指代坏请求 357 | - 404 (not found)- 资源不存在 358 | - 409 (conflict)- 通用冲突 359 | - 500 (internal server error)- 通用错误响应 360 | - 503 (Service Unavailable)- 服务端当前无法处理请求 361 | # 4.与其他解决方案对比 362 | 这部分我不太了解,是直接复制网上的资料,大家有了解的可以帮忙补充下 363 | 确定使用哪种 API 风格,需要根据项目需求,结合 API 风格的特点,这对以后的编码实现、通信方式和通信效率都有很大的影响。 364 | ## 4.1除REST外其它解决方案 365 | 366 | - [远程过程调用](https://zh.wikipedia.org/wiki/%E8%BF%9C%E7%A8%8B%E8%BF%87%E7%A8%8B%E8%B0%83%E7%94%A8)(RPC)-来自:[维基百科](https://zh.wikipedia.org/wiki/%E8%A1%A8%E7%8E%B0%E5%B1%82%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2) 367 | - [服务导向架构](https://zh.wikipedia.org/wiki/%E6%9C%8D%E5%8B%99%E5%B0%8E%E5%90%91%E6%9E%B6%E6%A7%8B)(SOA)-来自:[维基百科](https://zh.wikipedia.org/wiki/%E8%A1%A8%E7%8E%B0%E5%B1%82%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2) 368 | - GraphQL(一种用于 API 的查询语言) -来自[freeCodeCamp](https://chinese.freecodecamp.org/news/rest-api-tutorial-rest-client-rest-service-and-api-calls-explained-with-code-examples/) 369 | - JSON-Pure -来自[freeCodeCamp](https://chinese.freecodecamp.org/news/rest-api-tutorial-rest-client-rest-service-and-api-calls-explained-with-code-examples/) 370 | - oData(开放数据协议) -来自[freeCodeCamp](https://chinese.freecodecamp.org/news/rest-api-tutorial-rest-client-rest-service-and-api-calls-explained-with-code-examples/) 371 | ## 4.2 RPC vs REST 372 | 来自:[微服务架构核心 20 讲-杨波](https://time.geekbang.org/course/detail/100003901-2274) 373 | ![image.png](https://cdn.nlark.com/yuque/0/2022/png/26213287/1650763613002-f24349d7-78fc-4349-88f7-eae8018b9a44.png#clientId=u44902f92-fbdb-4&from=paste&height=407&id=u66f6fcb4&name=image.png&originHeight=814&originWidth=1580&originalType=binary&ratio=1&rotation=0&showTitle=false&size=1122293&status=done&style=none&taskId=uaac95606-8d0e-4406-83ea-2368c533c78&title=&width=790) 374 | 和REST相关的内容有: 375 | 376 | - 耦合性:客户端和服务端没有强的消息耦合 377 | - 消息协议:一般消息协议是文本的,如XML、JSON。开发者可读,但大小一般比二进制大 378 | - 通讯协议:一般采用HTTP/HTTP2进行通信 379 | - 性能:由于消息协议是基于文本协议和通信协议的原因,REST性能一般低于RPC,但还需看实际场景 380 | - 接口契约IDL:swagger可以对接口进行契约规范 381 | - 客户端:由于REST本身是松散的机制,一般HTTP客户端都可调用Restful API(不一定要强的生成客户端)。用接口定义契约如Swagger也可以自动生成强类型客户端,支持多语言的 382 | - 开发者友好:由于消息协议是基于文本协议,对开发者调试较方便, 在浏览器中直接就可以调用REST接口 383 | - 对外开放:可以直接对外开放,省掉了中间的转换 384 | ## 4.3 REST总结 385 | ### 优点 386 | 来自:[维基百科](https://zh.wikipedia.org/wiki/%E8%A1%A8%E7%8E%B0%E5%B1%82%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2) 387 | 388 | - 简洁:与复杂的[SOAP](https://zh.wikipedia.org/wiki/SOAP)和[XML-RPC](https://zh.wikipedia.org/wiki/XML-RPC)相比更加简洁 389 | - 低耦合、轻量、自解释 390 | - 无状态 391 | - 提高开发效率 392 | - **前后端分离**:前端拿到数据只负责展示和渲染,不对数据做任何处理。后端处理数据并以JSON格式传输出去,定义这样一套统一的接口,你完全不用考虑用户使用什么系统,无论在web,ios,android三端都可以用相同的API接口 393 | - 不用关心接口实现细节 394 | - 相对更规范,更标准,更通用 395 | - 可更高效利用缓存来提高响应速度 396 | - 可提高服务器的扩展性:通讯本身的无状态性可以让不同的服务器的处理一系列请求中的不同请求 397 | - 浏览器即可作为客户端,简化软件需求 398 | - 相对于其他叠加在[HTTP协议](https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE)之上的机制,REST的软件依赖性更小 399 | - 不需要额外的资源发现机制 400 | - 在软件技术演进中的长期的兼容性更好 401 | ### 缺点 402 | 403 | - 性能不如 RPC 高 404 | - 多端 (多次数据交互) 405 | - 当要获取多个资源时必须请求多个不同的 URL,进而带来多次数据交互。 406 | - 随着应用变得越来越复杂,管理这些 API 也变得更加困难。 407 | # 参考链接: 408 | 409 | 1. [Go 语言项目开发实战-API 风格(上):如何设计RESTful API?-极客时间-孔令飞](https://time.geekbang.org/column/article/386970) 410 | 2. [RESTful API 设计指南-阮一峰](https://www.ruanyifeng.com/blog/2014/05/restful_api.html) 411 | 3. [RESTful API 中文网](http://restful.p2hp.com/tech/rest-api-design-tutorial-with-example) 412 | 4. [维基百科](https://zh.wikipedia.org/wiki/%E8%A1%A8%E7%8E%B0%E5%B1%82%E7%8A%B6%E6%80%81%E8%BD%AC%E6%8D%A2) 413 | 5. [怎样用通俗的语言解释REST,以及RESTful?-zhihu](https://www.zhihu.com/question/28557115/answer/48094438?utm_source=wechat_session&utm_medium=social&utm_oi=38249267462144&utm_content=group2_Answer&utm_campaign=shareopn) 414 | 6. [restful是什么,restful的api协议是什么-zhihu](https://www.zhihu.com/zvideo/1406988851005452288) 同 [RESTful接口设计,不止命名规范那么简单-拉钩教育](https://kaiwu.lagou.com/course/courseInfo.htm?courseId=1130&sid=20-h5Url-0&lgec_type=website&lgec_sign=56C78768E1A134C49750D2797FB14316&buyFrom=1&pageId=1pz4#/detail/pc?id=8452) 415 | 7. [GitHub 标准 RESTful API](https://docs.github.com/cn/rest) 416 | 8. [RESTful 架构详解-runoob](https://www.runoob.com/w3cnote/restful-architecture.html) 417 | 9. [REST API 教程:REST 客户端,REST 服务及 API 调用(含代码示例)-freeCodeCamp](https://chinese.freecodecamp.org/news/rest-api-tutorial-rest-client-rest-service-and-api-calls-explained-with-code-examples/) 418 | 10. [微服务架构核心 20 讲-杨波](https://time.geekbang.org/course/detail/100003901-2274) 419 | 420 | 实践中常见的问题: 421 | 422 | - 客户端不一定都支持这些HTTP方法吧? 423 | 424 | 的确有这种情况,特别是一些比较古老的基于浏览器的客户端,只能支持GET和POST两种方法。 425 | 在实践上,客户端和服务端都可能需要做一些妥协。例如rails框架就支持通过隐藏参数_method=DELETE来传递真实的请求方法, 而像Backbone这样的客户端MVC框架则允许传递_method传输和设置X-HTTP-Method-Override头来规避这个问题。 426 | 427 | - 统一接口是否意味着不能扩展带特殊语义的方法? 428 | 429 | 统一接口并不阻止你扩展方法,只要方法对资源的操作有着具体的、可识别的语义即可,并能够保持整个接口的统一性。 430 | 像WebDAV就对HTTP方法进行了扩展,增加了LOCK、UPLOCK等方法。而github的API则支持使用PATCH方法来进行issue的更新,例如: 431 |  PATCH /repos/:owner/:repo/issues/:number 432 | 不过,需要注意的是,像PATCH这种不是HTTP标准方法的,服务端需要考虑客户端是否能够支持的问题。 433 | 434 | - RESTful API和http一样么?区别是? 435 | 436 | 不一样 437 | REST通过HTTP实现,把用户的需求抽象成对资源的操作,用户必须通过HTTP协议的GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS七种基本操作去和服务器交互。 438 | 439 | -------------------------------------------------------------------------------- /HTTP/index.md: -------------------------------------------------------------------------------- 1 | # HTTP 2 | 3 | |目录 | 内容描述 4 | |- | -:| 5 | Http 缓存策略 | [使用技巧](./Http 缓存策略.md) 6 | Restful API | [使用技巧](./Restful API.md) 7 | HTTP常用状态码 | [使用技巧](./HTTP常用状态码.md) -------------------------------------------------------------------------------- /JavaScript/debounce.md: -------------------------------------------------------------------------------- 1 | ```js 2 | 3 | var btn = document.getElementById("btn"); 4 | // 防抖函数 5 | /** 6 | * @param {Function} func 传入函数 7 | * @param {Number} wait 等待时间 8 | * @param {Boolean} immediate 是否立即执行 9 | * @return {Function} 10 | */ 11 | function debounce (func, wait, immediate) { 12 | // 判断 func 是否为一个函数 13 | if (typeof func !== 'function') throw new TypeError('debounce func must be a function') 14 | // 判断 wait 是否存在 15 | if (typeof wait === 'undefined') wait = 300 16 | // 判断 immediate 是否存在 17 | if (typeof immediate === 'undefined') immediate = false 18 | // 判断 wait 是否为 boolean 19 | if (typeof wait === 'boolean') { 20 | immediate = wait 21 | wait = 300 22 | } 23 | // 防抖函数 -> 开启一个定时器, 让 func 函数在规定时间内执行 24 | // 采用高阶函数中的函数作为返回值, 利用闭包的特性存储 timer 25 | var timer; 26 | return function (...args) { 27 | const that = this 28 | // 每次执行函数之前我们需要清除定时器, 目的:清除每次触发事情多余的次数 29 | timer && clearTimeout(timer) 30 | // 判断 immediate 是否为真 并且 timer 为假 才执行函数 31 | immediate && !timer ? func.apply(that, args) : null 32 | // 开启定时器 33 | // 在 setTimeout this 指向的 window 34 | timer = setTimeout(function(){ 35 | timer = null; 36 | // 如果 immediate 立即执行 定时器结束后不需要执行 37 | !immediate ? func.apply(that, args) : null 38 | }, wait) 39 | } 40 | } 41 | 42 | // btn.onclick = function() { 43 | // console.log("btn clicked") 44 | // } 45 | function click (ev) { 46 | console.log(ev) 47 | console.log(this) 48 | console.log("btn clicked") 49 | } 50 | // btn.onclick = debounce(click, 1000, false); 51 | btn.onclick = debounce(click, 1000, true); -------------------------------------------------------------------------------- /JavaScript/index.md: -------------------------------------------------------------------------------- 1 | # JavaScript 2 | 3 | |目录 | 内容描述 4 | |- | -:| 5 | 防抖 | [使用技巧](./debounce.md) 6 | throttle | [使用技巧](./throttle.md) 7 | 函数 | [使用技巧](./函数.md) 8 | 异步处理 | [使用技巧](./异步处理.md) -------------------------------------------------------------------------------- /JavaScript/throttle.md: -------------------------------------------------------------------------------- 1 | ```js 2 | // 节流函数规定的时间触发,只要有触发就会执行 3 | /** 4 | * @param {Function} func 执行的函数 5 | * @param {Number} wait 等待时间 6 | */ 7 | function myThrottle (func, wait) { 8 | if (typeof func !== "function") throw new Error(`${func} is not a function`) 9 | if (typeof wait === "undefined") wait = 500 10 | // 记录上一次执行结束的时间 11 | let previous = 0 12 | let timer = null; 13 | return function (...args) { 14 | // 存储this 15 | const that = this 16 | // 记录当前触发的时间 17 | const now = new Date() 18 | // 判断当前的触发频率 19 | const interval = wait - (now - previous) 20 | // 低频触发 立即执行 21 | if (interval <= 0) { 22 | func.call(that, ...args) 23 | // 保存当前函数执行的时间 24 | previous = new Date() 25 | clearTimeout(timer) // 清空定时器的操作,不能将timer值重置 26 | // 手动重置 27 | timer = null 28 | } else if (!timer){ 29 | timer = setTimeout(function () { 30 | clearTimeout(timer) // 清空定时器的操作,不能将timer值重置 31 | // 手动重置 32 | timer = null 33 | func.call(that, ...args) 34 | previous = new Date() 35 | }, interval) 36 | } 37 | } 38 | } 39 | // document.onscroll = function () { 40 | // console.log(1) 41 | // } 42 | function scroll () { 43 | console.log(1) 44 | } 45 | document.onscroll = myThrottle(scroll, 1000) -------------------------------------------------------------------------------- /JavaScript/代码基本功测试:JS 的数据类型.md: -------------------------------------------------------------------------------- 1 | 2 | 作为 JavaScript 的入门级知识点,JS 数据类型在整个 JavaScript 的学习过程中其实尤为重要。因为在 JavaScript 编程中,我们经常会遇到边界数据类型条件判断问题,很多代码只有在某种特定的数据类型下,才能可靠地执行。 3 | 4 | 尤其在大厂面试中,经常需要你现场手写代码,因此你很有必要提前考虑好数据类型的边界判断问题,并在你的 JavaScript 逻辑编写前进行前置判断,这样才能让面试官看到你严谨的编程逻辑和深入思考的能力,面试才可以加分。 5 | 6 | 因此,这一讲我将从数据类型的概念、检测方法、转换方法几个方面,帮你梳理和深入学习 JavaScript 的数据类型的知识点。 7 | 8 | 我希望通过本讲的学习,你能够熟练掌握数据类型的判断以及转换等相关知识点,并且在遇到数据类型判断以及数据类型的隐式转换等问题时可以轻松应对。 9 | ## 数据类型概念 10 | 11 | JavaScript 的数据类型有下图所示的 8 种: 12 | ![image-20210616083000538.png](https://cdn.nlark.com/yuque/0/2021/png/2968916/1627267258876-5e99a5ab-9ff2-4937-85e2-97114b45ab69.png#clientId=ue89d34af-3aa6-4&from=paste&height=508&id=u0f04888f&originHeight=1016&originWidth=1944&originalType=binary&ratio=1&size=181260&status=done&style=none&taskId=uc6b21895-daa0-4577-8845-60ebda886ff&width=972) 13 | 14 | 其中,前 7 种类型为基础类型,最后 1 种(Object)为引用类型,也是你需要重点关注的,因为它在日常工作中是使用得最频繁,也是需要关注最多技术细节的数据类型。 15 | 16 | 而引用数据类型(Object)又分为图上这几种常见的类型:Array - 数组对象、RegExp - 正则对象、Date - 日期对象、Math - 数学函数、Function - 函数对象。 17 | 18 | 在这里,我想先请你重点了解下面两点,因为各种 JavaScript 的数据类型最后都会在初始化之后放在不同的内存中,因此上面的数据类型大致可以分成两类来进行存储: 19 | 20 | 1. 基础类型存储在栈内存,被引用或拷贝时,会创建一个完全相等的变量; 21 | 2. 引用类型存储在堆内存,存储的是地址,多个引用指向同一个地址,这里会涉及一个“共享”的概念。 22 | 23 | 关于引用类型下面直接通过两段代码来讲解,让你深入理解一下核心“共享”的概念。 24 | 25 | #### 题目一:初出茅庐 26 | 27 | ```javascript 28 | let a = { 29 | name: 'lee', 30 | age: 18 31 | } 32 | let b = a; 33 | console.log(a.name); //第一个console 34 | b.name = 'son'; 35 | console.log(a.name); //第二个console 36 | console.log(b.name); //第三个console 37 | ``` 38 | 39 | 这道题比较简单,我们可以看到第一个 console 打出来 name 是 'lee',这应该没什么疑问;但是在执行了 b.name='son' 之后,结果你会发现 a 和 b 的属性 name 都是 'son',第二个和第三个打印结果是一样的,这里就体现了引用类型的“共享”的特性,即这两个值都存在同一块内存中共享,一个发生了改变,另外一个也随之跟着变化。 40 | 41 | #### 题目二:渐入佳境 42 | 43 | ```javascript 44 | let a = { 45 | name: 'Julia', 46 | age: 20 47 | } 48 | 49 | function change(o) { 50 | o.age = 24; 51 | o = { 52 | name: 'Kath', 53 | age: 30 54 | } 55 | return o; 56 | } 57 | let b = change(a); // 注意这里没有new,后面new相关会有专门文章讲解 58 | console.log(b.age); // 第一个console 59 | console.log(a.age); // 第二个console 60 | ``` 61 | 62 | 这道题涉及了 function,你通过上述代码可以看到第一个 console 的结果是 30,b 最后打印结果是 {name: "Kath", age: 30};第二个 console 的返回结果是 24,而 a 最后的打印结果是 {name: "Julia", age: 24}。 63 | 64 | 是不是和你预想的有些区别?你要注意的是,**这里的 function 和 return 带来了不一样的东西**。 65 | 66 | 原因在于:函数传参进来的 o,传递的是对象在堆中的内存地址值,通过调用 o.age = 24(第 7 行代码)确实改变了 a 对象的 age 属性;但是第 12 行代码的 return 却又把 o 变成了另一个内存地址,将 {name: "Kath", age: 30} 存入其中,最后返回 b 的值就变成了 {name: "Kath", age: 30}。而如果把第 12 行去掉,那么 b 就会返回 undefined。这里你可以再仔细琢磨一下。 67 | 68 | 讲完数据类型的基本概念,我们继续看下一部分,如何对数据类型进行检测,这也是比较重要的问题。 69 | 70 | ### 数据类型检测 71 | 72 | 数据类型检测也是面试过程中经常会遇到的问题,比如:如何判断是否为数组?让你写一段代码把 JavaScript 的各种数据类型判断出来,等等。类似的题目会很多,而且在平常写代码过程中我们也会经常用到。 73 | 74 | 我也经常在面试一些候选人的时候,有些回答比如“用 typeof 来判断”,然后就没有其他答案了,但这样的回答是不能令面试官满意的,因为他要考察你对 JS 的数据类型理解的深度,所以我们先要做到的是对各种数据类型的判断方法了然于胸,然后再进行归纳总结,给面试官一个满意的答案。 75 | 76 | 数据类型的判断方法其实有很多种,比如 typeof 和 instanceof,下面我来重点介绍三种在工作中经常会遇到的数据类型检测方法。 77 | 78 | #### 第一种判断方法:typeof 79 | 80 | 这是比较常用的一种,那么我们通过一段代码来快速回顾一下这个方法。 81 | 82 | ```javascript 83 | typeof 1 // 'number' 84 | typeof '1' // 'string' 85 | typeof undefined // 'undefined' 86 | typeof true // 'boolean' 87 | typeof Symbol() // 'symbol' 88 | typeof null // 'object' 89 | typeof [] // 'object' 90 | typeof {} // 'object' 91 | typeof console // 'object' 92 | typeof console.log // 'function' 93 | ``` 94 | 95 | 你可以看到,前 6 个都是基础数据类型,而为什么第 6 个 null 的 typeof 是 'object' 呢?这里要和你强调一下,虽然 typeof null 会输出 object,但这只是 JS 存在的一个悠久 Bug,不代表 null 就是引用数据类型,并且 null 本身也不是对象。因此,null 在 typeof 之后返回的是有问题的结果,不能作为判断 null 的方法。如果你需要在 if 语句中判断是否为 null,直接通过 ‘===null’来判断就好。 96 | 97 | 此外还要注意,引用数据类型 Object,用 typeof 来判断的话,除了 function 会判断为 OK 以外,其余都是 'object',是无法判断出来的。 98 | 99 | #### 第二种判断方法:instanceof 100 | 101 | 想必 instanceof 的方法你也听说过,我们 new 一个对象,那么这个新对象就是它原型链继承上面的对象了,通过 instanceof 我们能判断这个对象是否是之前那个构造函数生成的对象,这样就基本可以判断出这个新对象的数据类型。下面通过代码来了解一下。 102 | 103 | ```javascript 104 | let Car = function() {} 105 | let benz = new Car() 106 | benz instanceof Car // true 107 | let car = new String('Mercedes Benz') 108 | car instanceof String // true 109 | let str = 'Covid-19' 110 | str instanceof String // false 111 | ``` 112 | 113 | 上面就是用 instanceof 方法判断数据类型的大致流程,那么如果让你自己实现一个 instanceof 的底层实现,应该怎么写呢?请看下面的代码。 114 | 115 | ```javascript 116 | function myInstanceof(left, right) { 117 | // 这里先用typeof来判断基础数据类型,如果是,直接返回false 118 | if(typeof left !== 'object' || left === null) return false; 119 | // getProtypeOf是Object对象自带的API,能够拿到参数的原型对象 120 | let proto = Object.getPrototypeOf(left); 121 | while(true) { //循环往下寻找,直到找到相同的原型对象 122 | if(proto === null) return false; 123 | if(proto === right.prototype) return true;//找到相同原型对象,返回true 124 | proto = Object.getPrototypeof(proto); 125 | } 126 | } 127 | // 验证一下自己实现的myInstanceof是否OK 128 | console.log(myInstanceof(new Number(123), Number)); // true 129 | console.log(myInstanceof(123, Number)); // false 130 | ``` 131 | 132 | 现在你知道了两种判断数据类型的方法,那么它们之间有什么差异呢?我总结了下面两点: 133 | 134 | 1. instanceof 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型; 135 | 2. 而 typeof 也存在弊端,它虽然可以判断基础数据类型(null 除外),但是引用数据类型中,除了 function 类型以外,其他的也无法判断。 136 | 137 | 总之,不管单独用 typeof 还是 instanceof,都不能满足所有场景的需求,而只能通过二者混写的方式来判断。但是这种方式判断出来的其实也只是大多数情况,并且写起来也比较难受,你也可以试着写一下。 138 | 139 | 其实我个人还是比较推荐下面的第三种方法,相比上述两个而言,能更好地解决数据类型检测问题。 140 | 141 | #### 第三种判断方法:Object.prototype.toString 142 | 143 | toString() 是 Object 的原型方法,调用该方法,可以统一返回格式为 “[object Xxx]” 的字符串,其中 Xxx 就是对象的类型。对于 Object 对象,直接调用 toString() 就能返回 [object Object];而对于其他对象,则需要通过 call 来调用,才能返回正确的类型信息。我们来看一下代码。 144 | 145 | ```javascript 146 | Object.prototype.toString({}) // "[object Object]" 147 | Object.prototype.toString.call({}) // 同上结果,加上call也ok 148 | Object.prototype.toString.call(1) // "[object Number]" 149 | Object.prototype.toString.call('1') // "[object String]" 150 | Object.prototype.toString.call(true) // "[object Boolean]" 151 | Object.prototype.toString.call(function(){}) // "[object Function]" 152 | Object.prototype.toString.call(null) //"[object Null]" 153 | Object.prototype.toString.call(undefined) //"[object Undefined]" 154 | Object.prototype.toString.call(/123/g) //"[object RegExp]" 155 | Object.prototype.toString.call(new Date()) //"[object Date]" 156 | Object.prototype.toString.call([]) //"[object Array]" 157 | Object.prototype.toString.call(document) //"[object HTMLDocument]" 158 | Object.prototype.toString.call(window) //"[object Window]" 159 | ``` 160 | 161 | 从上面这段代码可以看出,Object.prototype.toString.call() 可以很好地判断引用类型,甚至可以把 document 和 window 都区分开来。 162 | 163 | 但是在写判断条件的时候一定要注意,使用这个方法最后返回统一字符串格式为 "[object Xxx]" ,而这里字符串里面的 "Xxx" ,**第一个首字母要大写**(注意:使用 typeof 返回的是小写),这里需要多加留意。 164 | 165 | 那么下面来实现一个全局通用的数据类型判断方法,来加深你的理解,代码如下。 166 | 167 | ```javascript 168 | function getType(obj){ 169 | let type = typeof obj; 170 | if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返回 171 | return type; 172 | } 173 | // 对于typeof返回结果是object的,再进行如下的判断,正则返回结果 174 | return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1'); // 注意正则中间有个空格 175 | } 176 | /* 代码验证,需要注意大小写,哪些是typeof判断,哪些是toString判断?思考下 */ 177 | getType([]) // "Array" typeof []是object,因此toString返回 178 | getType('123') // "string" typeof 直接返回 179 | getType(window) // "Window" toString返回 180 | getType(null) // "Null"首字母大写,typeof null是object,需toString来判断 181 | getType(undefined) // "undefined" typeof 直接返回 182 | getType() // "undefined" typeof 直接返回 183 | getType(function(){}) // "function" typeof能判断,因此首字母小写 184 | getType(/123/g) //"RegExp" toString返回 185 | ``` 186 | 187 | 到这里,数据类型检测的三种方法就介绍完了,最后也给出来了示例代码,希望你可以对比着来学习、使用,并且不断加深记忆,以便遇到问题时不会手忙脚乱。你如果一遍记不住可以多次来回看巩固,直到把上面的代码都能全部理解,并且把几个特殊的问题都强化记忆,这样未来你去做类似题目才不会有问题。 188 | 189 | 下面我们来看本讲的最后一部分:数据类型的转换。 190 | 191 | ### 数据类型转换 192 | 193 | 在日常的业务开发中,经常会遇到 JavaScript 数据类型转换问题,有的时候需要我们主动进行强制转换,而有的时候 JavaScript 会进行隐式转换,隐式转换的时候就需要我们多加留心。 194 | 195 | 那么这部分都会涉及哪些内容呢?我们先看一段代码,了解下大致的情况。 196 | 197 | ```javascript 198 | '123' == 123 // false or true? 199 | '' == null // false or true? 200 | '' == 0 // false or true? 201 | [] == 0 // false or true? 202 | [] == '' // false or true? 203 | [] == ![] // false or true? 204 | null == undefined //  false or true? 205 | Number(null) // 返回什么? 206 | Number('') // 返回什么? 207 | parseInt('') // 返回什么? 208 | {}+10 // 返回什么? 209 | let obj = { 210 | [Symbol.toPrimitive]() { 211 | return 200; 212 | }, 213 | valueOf() { 214 | return 300; 215 | }, 216 | toString() { 217 | return 'Hello'; 218 | } 219 | } 220 | console.log(obj + 200); // 这里打印出来是多少? 221 | ``` 222 | 223 | 上面这 12 个问题相信你并不陌生,基本涵盖了我们平常容易疏漏的一些情况,这就是在做数据类型转换时经常会遇到的强制转换和隐式转换的方式,那么下面我就围绕数据类型的两种转换方式详细讲解一下,希望可以为你提供一些借鉴。 224 | 225 | #### 强制类型转换 226 | 227 | 强制类型转换方式包括 Number()、parseInt()、parseFloat()、toString()、String()、Boolean(),这几种方法都比较类似,通过字面意思可以很容易理解,都是通过自身的方法来进行数据类型的强制转换。下面我列举一些来详细说明。 228 | 229 | 上面代码中,第 8 行的结果是 0,第 9 行的结果同样是 0,第 10 行的结果是 NaN。这些都是很明显的强制类型转换,因为用到了 Number() 和 parseInt()。 230 | 231 | 其实上述几个强制类型转换的原理大致相同,下面我挑两个比较有代表性的方法进行讲解。 232 | 233 | **Number() 方法的强制转换规则** 234 | 235 | - 如果是布尔值,true 和 false 分别被转换为 1 和 0; 236 | - 如果是数字,返回自身; 237 | - 如果是 null,返回 0; 238 | - 如果是 undefined,返回 NaN; 239 | - 如果是字符串,遵循以下规则:如果字符串中只包含数字(或者是 0X / 0x 开头的十六进制数字字符串,允许包含正负号),则将其转换为十进制;如果字符串中包含有效的浮点格式,将其转换为浮点数值;如果是空字符串,将其转换为 0;如果不是以上格式的字符串,均返回 NaN; 240 | - 如果是 Symbol,抛出错误; 241 | - 如果是对象,并且部署了 [Symbol.toPrimitive] ,那么调用此方法,否则调用对象的 valueOf() 方法,然后依据前面的规则转换返回的值;如果转换的结果是 NaN ,则调用对象的 toString() 方法,再次依照前面的顺序转换返回对应的值(Object 转换规则会在下面细讲)。 242 | 243 | 下面通过一段代码来说明上述规则。 244 | 245 | ```javascript 246 | Number(true); // 1 247 | Number(false); // 0 248 | Number('0111'); //111 249 | Number(null); //0 250 | Number(''); //0 251 | Number('1a'); //NaN 252 | Number(-0X11); //-17 253 | Number('0X11') //17 254 | ``` 255 | 256 | 其中,我分别列举了比较常见的 Number 转换的例子,它们都会把对应的非数字类型转换成数字类型,而有一些实在无法转换成数字的,最后只能输出 NaN 的结果。 257 | 258 | **Boolean() 方法的强制转换规则** 259 | 260 | 这个方法的规则是:除了 undefined、 null、 false、 ''、 0(包括 +0,-0)、 NaN 转换出来是 false,其他都是 true。 261 | 262 | 这个规则应该很好理解,没有那么多条条框框,我们还是通过代码来形成认知,如下所示。 263 | 264 | ```javascript 265 | Boolean(0) //false 266 | Boolean(null) //false 267 | Boolean(undefined) //false 268 | Boolean(NaN) //false 269 | Boolean(1) //true 270 | Boolean(13) //true 271 | Boolean('12') //true 272 | ``` 273 | 274 | 其余的 parseInt()、parseFloat()、toString()、String() 这几个方法,你可以按照我的方式去整理一下规则,在这里不占过多篇幅了。 275 | 276 | #### 隐式类型转换 277 | 278 | 凡是通过逻辑运算符 (&&、 ||、 !)、运算符 (+、-、*、/)、关系操作符 (>、 <、 <= 、>=)、相等运算符 (==) 或者 if/while 条件的操作,如果遇到两个数据类型不一样的情况,都会出现隐式类型转换。这里你需要重点关注一下,因为比较隐蔽,特别容易让人忽视。 279 | 280 | 下面着重讲解一下日常用得比较多的“==”和“+”这两个符号的隐式转换规则。 281 | 282 | **'==' 的隐式类型转换规则** 283 | 284 | - 如果类型相同,无须进行类型转换; 285 | - 如果其中一个操作值是 null 或者 undefined,那么另一个操作符必须为 null 或者 undefined,才会返回 true,否则都返回 false; 286 | - 如果其中一个是 Symbol 类型,那么返回 false; 287 | - 两个操作值如果为 string 和 number 类型,那么就会将字符串转换为 number; 288 | - 如果一个操作值是 boolean,那么转换成 number; 289 | - 如果一个操作值为 object 且另一方为 string、number 或者 symbol,就会把 object 转为原始类型再进行判断(调用 object 的 valueOf/toString 方法进行转换)。 290 | 291 | ```javascript 292 | null == undefined // true 规则2 293 | null == 0 // false 规则2 294 | '' == null // false 规则2 295 | '' == 0 // true 规则4 字符串转隐式转换成Number之后再对比 296 | '123' == 123 // true 规则4 字符串转隐式转换成Number之后再对比 297 | 0 == false // true e规则 布尔型隐式转换成Number之后再对比 298 | 1 == true // true e规则 布尔型隐式转换成Number之后再对比 299 | var a = { 300 |   value: 0, 301 |   valueOf: function() { 302 |     this.value++; 303 |     return this.value; 304 |   } 305 | }; 306 | // 注意这里a又可以等于1、2、3 307 | console.log(a == 1 && a == 2 && a ==3); //true f规则 Object隐式转换 308 | // 注:但是执行过3遍之后,再重新执行a==3或之前的数字就是false,因为value已经加上去了,这里需要注意一下 309 | ``` 310 | 311 | 对照着这个规则看完上面的代码和注解之后,你可以再回过头做一下我在讲解“数据类型转换”之前的那 12 道题目,是不是就很容易解决了? 312 | 313 | **'+' 的隐式类型转换规则** 314 | 315 | '+' 号操作符,不仅可以用作数字相加,还可以用作字符串拼接。仅当 '+' 号两边都是数字时,进行的是加法运算;如果两边都是字符串,则直接拼接,无须进行隐式类型转换。 316 | 317 | 除了上述比较常规的情况外,还有一些特殊的规则,如下所示。 318 | 319 | - 如果其中有一个是字符串,另外一个是 undefined、null 或布尔型,则调用 toString() 方法进行字符串拼接;如果是纯对象、数组、正则等,则默认调用对象的转换方法会存在优先级(下一讲会专门介绍),然后再进行拼接。 320 | - 如果其中有一个是数字,另外一个是 undefined、null、布尔型或数字,则会将其转换成数字进行加法运算,对象的情况还是参考上一条规则。 321 | - 如果其中一个是字符串、一个是数字,则按照字符串规则进行拼接。 322 | 323 | 下面还是结合代码来理解上述规则,如下所示。 324 | 325 | ```javascript 326 | 1 + 2 // 3 常规情况 327 | '1' + '2' // '12' 常规情况 328 | // 下面看一下特殊情况 329 | '1' + undefined // "1undefined" 规则1,undefined转换字符串 330 | '1' + null // "1null" 规则1,null转换字符串 331 | '1' + true // "1true" 规则1,true转换字符串 332 | '1' + 1n // '11' 比较特殊字符串和BigInt相加,BigInt转换为字符串 333 | 1 + undefined // NaN 规则2,undefined转换数字相加NaN 334 | 1 + null // 1 规则2,null转换为0 335 | 1 + true // 2 规则2,true转换为1,二者相加为2 336 | 1 + 1n // 错误 不能把BigInt和Number类型直接混合相加 337 | '1' + 3 // '13' 规则3,字符串拼接 338 | ``` 339 | 340 | 整体来看,如果数据中有字符串,JavaScript 类型转换还是更倾向于转换成字符串,因为第三条规则中可以看到,在字符串和数字相加的过程中最后返回的还是字符串,这里需要关注一下。 341 | 342 | 了解了 '+' 的转换规则后,我们最后再看一下 Object 的转换规则。 343 | 344 | **Object 的转换规则** 345 | 346 | 对象转换的规则,会先调用内置的 [ToPrimitive] 函数,其规则逻辑如下: 347 | 348 | - 如果部署了 Symbol.toPrimitive 方法,优先调用再返回; 349 | - 调用 valueOf(),如果转换为基础类型,则返回; 350 | - 调用 toString(),如果转换为基础类型,则返回; 351 | - 如果都没有返回基础类型,会报错。 352 | 353 | 直接理解有些晦涩,还是直接来看代码,你也可以在控制台自己敲一遍来加深印象。 354 | 355 | ```javascript 356 | var obj = { 357 | value: 1, 358 | valueOf() { 359 | return 2; 360 | }, 361 | toString() { 362 | return '3' 363 | }, 364 | [Symbol.toPrimitive]() { 365 | return 4 366 | } 367 | } 368 | console.log(obj + 1); // 输出5 369 | // 因为有Symbol.toPrimitive,就优先执行这个;如果Symbol.toPrimitive这段代码删掉,则执行valueOf打印结果为3;如果valueOf也去掉,则调用toString返回'31'(字符串拼接) 370 | // 再看两个特殊的case: 371 | 10 + {} 372 | // "10[object Object]",注意:{}会默认调用valueOf是{},不是基础类型继续转换,调用toString,返回结果"[object Object]",于是和10进行'+'运算,按照字符串拼接规则来,参考'+'的规则C 373 | [1,2,undefined,4,5] + 10 374 | // "1,2,,4,510",注意[1,2,undefined,4,5]会默认先调用valueOf结果还是这个数组,不是基础数据类型继续转换,也还是调用toString,返回"1,2,,4,5",然后再和10进行运算,还是按照字符串拼接规则,参考'+'的第3条规则 375 | ``` 376 | 377 | 关于 Object 的转化,就讲解到这里,希望你可以深刻体会一下上面讲的原理和内容。 378 | 379 | ### 总结 380 | 381 | 我们从三个方面学习了数据类型相关内容,下面整体回顾一下。 382 | 383 | 1. 数据类型的基本概念:这是必须掌握的知识点,作为深入理解 JavaScript 的基础。 384 | 2. 数据类型的判断方法:typeof 和 instanceof,以及 Object.prototype.toString 的判断数据类型、手写 instanceof 代码片段,这些是日常开发中经常会遇到的,因此你需要好好掌握。 385 | 3. 数据类型的转换方式:两种数据类型的转换方式,日常写代码过程中隐式转换需要多留意,如果理解不到位,很容易引起在编码过程中的 bug,得到一些意想不到的结果。 386 | -------------------------------------------------------------------------------- /JavaScript/函数.md: -------------------------------------------------------------------------------- 1 | # 🍑函数中的this关键字 2 | > 对于javascript函数中的this,这里先给出三个定律: 3 | 4 | ### 定律 5 | ⚫️ 普通函数中的this,定义时未知;运行时,this指向该函数的调用方 6 | 🟢 箭头函数中的this,定义时,已明确this指向,指向该箭头函数所在作用域中的this 7 | 🟠 普通函数实例化,构造函数中的this指向对应实例。 8 | #### 示例💻 9 | ```javascript 10 | var x = 0; 11 | function Foo () { 12 | this.x = 1; 13 | } 14 | Foo.prototype.print = function () { 15 | console.log(this); 16 | console.log(this.x); 17 | (function () { 18 | console.log(this); 19 | console.log(this.x) 20 | })() 21 | } 22 | 23 | let foo = new Foo(); 24 | foo.print.call({x: 2}); 25 | ``` 26 | ```javascript 27 | var x = 0; 28 | function Foo () { 29 | this.x = 1; 30 | } 31 | Foo.prototype.print = () => { 32 | console.log(this); 33 | console.log(this.x); 34 | (function () { 35 | console.log(this); 36 | console.log(this.x) 37 | })() 38 | } 39 | 40 | let foo = new Foo(); 41 | foo.print.call({x: 2}); 42 | ``` 43 | ### 📖 Apply 44 | > 明确原函数中的this指向,同时传参(参数形式用数组表示),并触发原函数调用。 45 | 46 | ```javascript 47 | Function.prototype.myApply = function(thisObj, args){ 48 | Object.prototype.runOuterMethod = this; 49 | thisObj.runOuterMethod(...args) 50 | delete Object.prototype.runOuterMethod; 51 | } 52 | ``` 53 | ### 🎹 Call 54 | > 明确原函数中的this指向,同时传参(参数形式用展开数组表示),并触发原函数调用。 55 | 56 | ```javascript 57 | Function.prototype.myCall = function(...args){ 58 | const thisObj = args[0]; //this 59 | args.splice(0,1); 60 | Object.prototype.runOuterMethod = this; 61 | thisObj.runOuterMethod(...args) 62 | delete Object.prototype.runOuterMethod; 63 | } 64 | ``` 65 | ### 🎵 Bind 66 | > 明确原函数中的this指向,同时传参(参数形式用展开数组表示)/传递部分参数, 67 | > 并返回一个新函数(可传递剩余参数)。 68 | 69 | ```javascript 70 | Function.prototype.myBind = function(...args){ 71 | const thisObj = args[0]; //this 72 | args.splice(0,1); 73 | Object.prototype.runOuterMethod = this; 74 | return function(...params){ 75 | thisObj.runOuterMethod(...(args.concat(params))) 76 | delete Object.prototype.runOuterMethod; 77 | } 78 | 79 | } 80 | ``` 81 | # 🍉高阶函数 82 | > 常用的高阶函数 map、every、some 83 | 84 | 1. 函数作为参数 85 | ```javascript 86 | const forEach=(arr, fn) => { 87 | for(const item of arr) fn(item); 88 | } 89 | ``` 90 | 91 | 2. 函数作为返回值 92 | ```javascript 93 | const once = (fn) => { 94 | let done = false; 95 | return (...args) => { 96 | if(!done){ 97 | done = true 98 | return fn.apply(this, args); 99 | }else{ 100 | return "已经支付过了" 101 | } 102 | } 103 | } 104 | const pay = once((money)=>{ 105 | return ("支付了:"+money) 106 | }) 107 | console.log(pay(100)); 108 | console.log(pay(100)); 109 | console.log(pay(100)); 110 | ``` 111 | ## 意义📢 112 | 帮助我们屏蔽具体业务逻辑实现的细节,将复杂的逻辑编写在函数中,我们只需要关心具体的业务功能,根据功能来调用不同的函数即可。高阶函数就是帮助我们抽象这些细节,如forEach中的for循环细节 113 | # 🧅函数组合 114 | > 如果一个函数A的调用结果是另一个函数B的参数,而B的调用结果又是函数C的参数,C的调用结果又是函数D的参数,D的调用结果又是... ... ... 115 | > 你将看到的是: F(E(D(C(B(A()))))); ⚠️⚠️⚠️ 116 | 117 | 🥬 为了解决上面过深的函数嵌套,我们希望用下面这种编码风格来规避掉上面的无限嵌套: 118 | ```typescript 119 | const 买菜A = 菜 => 买来的菜🥦 120 | const 洗菜B = 买来的菜 => 洗好的菜🥦💦 121 | const 炒菜C = 洗好的菜 => 炒好的菜🍲 122 | // 常规嵌套调用方式: 123 | C(B(A(菜))); 124 | // 期望的调用方式:提高代码可读性 125 | cooking(A,B,C)(菜); 126 | ``` 127 | ## 🤖️函数组合生成器 128 | > pipe: 根据参数传递的顺序进行依次调用 129 | > compose: 根据参数传递的逆序进行依次调用 130 | 131 | ### pipe ⛲️ 132 | ```javascript 133 | const pipe = (...args) => (param) => args.reduce((result, item) => item(result), param); 134 | ``` 135 | ### compose 🪀 136 | ```javascript 137 | const compose = (...args) => (param) => args.reverse().reduce((result, item) => item(result), param); 138 | ``` 139 | # 面向过程编程 140 | > 分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候进行依次调用。 141 | > 可维护性差、可扩展性差 142 | 143 | ```javascript 144 | // 步骤1 145 | const createDom = (name) => document.createElement(name); 146 | // 步骤2 147 | const addText = (dom, text) => dom.innerText = text 148 | // 步骤3 149 | const addDomInBody = (dom) => document.body.appendChild(dom) 150 | 151 | // 主函数中调用 152 | function main(){ 153 | const div = createDom('div') 154 | addText(div, "div元素"); 155 | addDomInBody(div); 156 | } 157 | ``` 158 | # 函数式编程FP 159 | > 对运算过程做出一定的抽象 160 | > 柯里化后的函数(将多元函数转化为一元函数),方便对参数做细粒度的封装。 161 | > 可维护性强、可扩展性强、可复用性强 162 | 163 | 函数运行的结果只依赖于输入的参数,而不依赖于外部状态,因此,我们常常说函数式编程没有副作用。 164 | 没有副作用有个巨大的好处,就是函数内部无状态,即输入确定,输出就是确定的,容易测试和维护。 165 | ```javascript 166 | function makeDom(name,text) { 167 | const dom = document.createElement(name); 168 | dom.innerText = text; 169 | document.body.appendChild(dom); 170 | } 171 | makeDom('button','BUTTON') 172 | ``` 173 | ## 纯函数 174 | > 固定的输入,有固定的输出,同时,函数的调用结果不会产生副作用 175 | 176 | ```javascript 177 | // 非纯函数,带有副作用 178 | let num = 18; 179 | function moreThan18 (age){ 180 | return age>=num; 181 | } 182 | // 纯函数 183 | function moreThan18(age){ 184 | const num = 18; 185 | return age>=num; 186 | } 187 | ``` 188 | ## 偏函数 189 | > 函数 190 | > partial application 191 | > 固定一个函数的一些参数,然后产生另一个更小元的函数 192 | 193 | ```javascript 194 | // 原函数 195 | function makeDom(name,text,style) { 196 | const dom = document.createElement(name); 197 | dom.innerText = text; 198 | Object.assign(dom.style,style) 199 | document.body.appendChild(dom); 200 | } 201 | // 偏函数 202 | function makeDomCury(name) { 203 | const dom = document.createElement(name); 204 | return function(text){ 205 | dom.innerText = text; 206 | return function (style) { 207 | Object.assign(dom.style,style) 208 | document.body.appendChild(dom); 209 | } 210 | } 211 | } 212 | ``` 213 | ## 柯里化 214 | > 技术 215 | > 将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术(多元函数转为一元函数的技术) 216 | > 柯里化可以实现偏函数生成函数 217 | 218 | ```javascript 219 | const curry = (fn) => function fun(...args) { 220 | if (args.length < fn.length) { 221 | return (...others) => fun(...(args.concat(others))) 222 | } 223 | return fn(...args) 224 | } 225 | ``` 226 | ## [point free](http://www.ruanyifeng.com/blog/2017/03/pointfree.html) 227 | > 一种函数式编程风格 228 | > 不关心所要处理的中间值,只合成运算过程,从而获得运算结果。 229 | > pipe函数、compose函数 是该编程风格的一种体现。 230 | 231 | 232 | ## 函数式编程常用的库 233 | #### [lodash](https://lodash.com/) 234 | ```javascript 235 | const _ = require('lodash') 236 | function add(a,b,c) { 237 | return a+b+c 238 | } 239 | const add_curry = _.curry(add); 240 | console.log(add_curry(1)(3)(2)) 241 | ``` 242 | #### [ramda](https://ramdajs.com/) 243 | > Ramda 的数据一律放在最后一个参数,理念是"function first,data last" 244 | > 所有方法都支持柯里化 245 | 246 | ```javascript 247 | const R = require('ramda') 248 | console.log(R.lt(2,1)) 249 | ``` 250 | ## 函数式编程的优缺点 251 | ### 优点 252 | 管理状态:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少这些未知、优化代码、减少出错情况; 253 | 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响 254 | 更优雅的组合:更强的复用性,带来更强大的组合性 255 | ### 缺点 256 | 性能:可能会对一个方法进行过度包装,从而产生上下文切换的性能开销 257 | 资源占用:在 JS 中为了实现对象状态的不可变,往往会创建新的对象,因此,它对垃圾回收所产生的压力远远超过其他编程方式 258 | 递归陷阱:在函数式编程中,为了实现迭代,通常会采用递归操作。 259 | # 面向对象编程OOP 260 | > 对现实中的事物做抽象,通过不同对象之间的依赖关系完成需求开发。 261 | > 封装、继承、多态 是面向对象的核心。 262 | > 可维护性、可扩展性、可复用性 皆优 263 | 264 | 通过对业务中不同的对象职责作出抽象,明确每个对象中的属性及方法,以及对象与对象之间的依赖关系(一般是以接口的形式表现),我们就可以从宏观上得知当前业务场景下各个对象的协作关系了。更加明确当前业务场景的实现方式。在开发前明确了依赖关系之后,多人开发的时候,只需要根据对象的依赖图即可实现分工协作,互不影响。 265 | ```javascript 266 | class DomCreator { 267 | dom = document.createElement('div'); 268 | createDom = (name) => this.dom = document.createElement(name); 269 | addText = (text) => this.dom.innerText = text; 270 | addDomInBody = () => document.body.appendChild(this.dom) 271 | } 272 | 273 | const btn = new DomCreator(); 274 | btn.createDom('button'); 275 | btn.addText('Button'); 276 | btn.addDomInBody(); 277 | ``` 278 | # 示例代码 279 | ```javascript 280 | const createDom = (name) => document.createElement(name); 281 | const addText = (dom, text) => dom.innerText = text 282 | const addStyle = (dom, style) => Object.assign(dom.style, style); 283 | // const addEvent = (dom,eventName,event) => dom.addEventListener(eventName,event); 284 | const addDomInBody = (dom) => document.body.appendChild(dom) 285 | 286 | const div = createDom('div') 287 | addText(div, "div元素"); 288 | addStyle(div,{color: 'red'}) 289 | // addEvent(div,'click',()=>console.log("div")) 290 | addDomInBody(div); 291 | 292 | // ========================== 函数式编程 ====================== 293 | // function makeDom(name,text,style) { 294 | // const dom = document.createElement(name); 295 | // dom.innerText = text; 296 | // Object.assign(dom.style,style) 297 | // document.body.appendChild(dom); 298 | // } 299 | // makeDom('button','Button',{color:'red'}) 300 | 301 | // function makeDomCury(name) { 302 | // const dom = document.createElement(name); 303 | // return function(text){ 304 | // dom.innerText = text; 305 | // return function (style) { 306 | // Object.assign(dom.style,style) 307 | // document.body.appendChild(dom); 308 | // } 309 | // } 310 | // } 311 | // makeDomCury('button')('BUTTON')({color:"blue"}) 312 | 313 | 314 | // ======================== 面向对象 =========================== 315 | 316 | // class DomCreator { 317 | // dom = document.createElement('div'); 318 | // createDom = (name) => this.dom = document.createElement(name); 319 | // addText = (text) => this.dom.innerText = text; 320 | // addDomInBody = () => document.body.appendChild(this.dom) 321 | // } 322 | // 323 | // class DomCreatorWithStyle extends DomCreator{ 324 | // addStyle = (style) => Object.assign(this.dom.style, style); 325 | // } 326 | // class DomCreatorWithEvent extends DomCreatorWithStyle{ 327 | // addEvent = (eventName,event) => this.dom.addEventListener(eventName,event); 328 | // } 329 | // class RedBtn extends DomCreatorWithEvent{ 330 | // dom = document.createElement('button'); 331 | // getBtn = ()=>{ 332 | // this.addText('Button'); 333 | // this.addStyle({color: 'red'}) 334 | // this.addEvent('click',()=>console.log("btn!")) 335 | // } 336 | // } 337 | // 338 | // const btn = new RedBtn(); 339 | // btn.getBtn() 340 | // btn.addDomInBody(); 341 | 342 | 343 | 344 | ``` 345 | 346 | -------------------------------------------------------------------------------- /JavaScript/并发任务控制.md: -------------------------------------------------------------------------------- 1 | ```txt 2 | 并发数量控制,分析题目,实现 SuperTask类 3 | ``` 4 | 5 | ```js 6 | function timeout (time) { 7 | return new Promise((resolve) => { 8 | setTimeout(() => { 9 | resolve() 10 | }, time) 11 | }) 12 | } 13 | // 要实现的函数SuperTask 14 | const superTask = new SuperTask() 15 | 16 | function addTask (time, name) { 17 | superTask.add(() => timeout(time)).then(() => { 18 | console.log(`任务${name}完成`) 19 | }) 20 | } 21 | // 输出结果 22 | addTask(10000, 1) // 10000ms后输出,任务1完成 23 | addTask(5000, 2) // 5000ms后输出,任务2完成 24 | addTask(3000, 3) // 8000ms后输出,任务3完成 25 | addTask(4000, 4) // 12000ms后输出,任务4完成 26 | addTask(5000, 5) // 15000ms后输出,任务5完成 27 | ``` 28 | ```txt 29 | 分析输出结果:最大并发量为2 30 | ``` 31 | ```js 32 | SuperTask实现如下: 33 | class SuperTask { 34 | constructor (parallelCount = 2) { 35 | this.parallelCount = parallelCount // 最大的并发数量 36 | this.runningCount = 0 // 当前正在执行的任务数量 37 | this.tasks = [] // 记录任务列表 38 | } 39 | 40 | add (task) { 41 | return new Promise((resolve, reject) => { 42 | // 这里不能去调用这个任务,因为无法判断任务任务列表中是否超过两个任务,故这里只能等待只有等待函数完成后才能去调用 43 | // 一个Promise的完成和拒绝,当前函数无法决定,需要在其他函数中去完成和拒绝,但是其他函数没有Promise 44 | // 所以我们可以把resolve和reject传到可以完成和拒绝的函数中 45 | this.tasks.push({ task, resolve, reject }) 46 | this._run() 47 | }) 48 | } 49 | 50 | // 依次运行tasks队列中的所有任务 51 | _run () { 52 | // 当前正在运行的任务数量小于最大的并发数量,并且任务列表中有任务 53 | while (this.runningCount < this.parallelCount && this.tasks.length) { 54 | const { task, resolve, reject } = this.tasks.shift() 55 | // 每调用一个任务,当前正在运行的任务+1 56 | this.runningCount++ 57 | // 取出任务,去调用他的resolve和reject 58 | task().then(resolve, reject).finally(() => { 59 | // 每次任务执行完成后,当前正在执行的任务-1 60 | this.runningCount-- 61 | // 当前正在运行的任务-1后,意味着其他任务可以进来继续执行了,递归执行_run() 62 | this._run() 63 | }) 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | -------------------------------------------------------------------------------- /JavaScript/异步处理.md: -------------------------------------------------------------------------------- 1 | #### 一. Callback ([回调函数](https://so.csdn.net/so/search?q=%E5%9B%9E%E8%B0%83%E5%87%BD%E6%95%B0&spm=1001.2101.3001.7020)) 2 | **1.定义:把函数当作变量传到另一个函数里,等待之后的执行。** 3 | **2.优点:解决了同步的问题**(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。) 4 | **3.缺点:** 5 | 6 | - 嵌套函数过多的多话,很难处理错误, 书写不习惯 7 | - 缺乏顺序性: 回调地狱导致的调试困难,和大脑的思维方式不符 8 | - 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身,即(**控制反转**) 9 | ```javascript 10 | ajax('XXX1', () => { 11 | // callback 函数体 12 | ajax('XXX2', () => { 13 | // callback 函数体 14 | ajax('XXX3', () => { 15 | // callback 函数体 16 | }) 17 | }) 18 | }) 19 | ``` 20 | #### 二. Promise 21 | Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。 22 | 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。 23 | Promise对象有以下两个特点。 24 | (1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。 25 | (2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。 26 | 注意,为了行文方便,本章后面的resolved统一只指fulfilled状态,不包含rejected状态。 27 | 有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。 28 | Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 29 | 30 | ```javascript 31 | ajax('XXX1') 32 | .then(res => { 33 | // 操作逻辑 34 | return ajax('XXX2') 35 | }).then(res => { 36 | // 操作逻辑 37 | return ajax('XXX3') 38 | }).then(res => { 39 | // 操作逻辑 40 | }) 41 | 42 | ``` 43 | ```javascript 44 | const PENDING = 'pending'; // 等待 45 | const FULFILLED = 'fulfilled'; // 成功 46 | const REJECTED = 'rejected'; // 失败 47 | 48 | class MyPromise { 49 | constructor (executor) { 50 | try { 51 | executor(this.resolve, this.reject) 52 | } catch (e) { 53 | this.reject(e); 54 | } 55 | } 56 | // promsie 状态 57 | status = PENDING; 58 | // 成功之后的值 59 | value = undefined; 60 | // 失败后的原因 61 | reason = undefined; 62 | // 成功回调 63 | successCallback = []; 64 | // 失败回调 65 | failCallback = []; 66 | 67 | resolve = value => { 68 | // 如果状态不是等待 阻止程序向下执行 69 | if (this.status !== PENDING) return; 70 | // 将状态更改为成功 71 | this.status = FULFILLED; 72 | // 保存成功之后的值 73 | this.value = value; 74 | // 判断成功回调是否存在 如果存在 调用 75 | // this.successCallback && this.successCallback(this.value); 76 | while(this.successCallback.length) this.successCallback.shift()() 77 | } 78 | reject = reason => { 79 | // 如果状态不是等待 阻止程序向下执行 80 | if (this.status !== PENDING) return; 81 | // 将状态更改为失败 82 | this.status = REJECTED; 83 | // 保存失败后的原因 84 | this.reason = reason; 85 | // 判断失败回调是否存在 如果存在 调用 86 | // this.failCallback && this.failCallback(this.reason); 87 | while(this.failCallback.length) this.failCallback.shift()() 88 | } 89 | then (successCallback, failCallback) { 90 | // 参数可选 91 | successCallback = successCallback ? successCallback : value => value; 92 | // 参数可选 93 | failCallback = failCallback ? failCallback: reason => { throw reason }; 94 | let promsie2 = new MyPromise((resolve, reject) => { 95 | // 判断状态 96 | if (this.status === FULFILLED) { 97 | setTimeout(() => { 98 | try { 99 | let x = successCallback(this.value); 100 | // 判断 x 的值是普通值还是promise对象 101 | // 如果是普通值 直接调用resolve 102 | // 如果是promise对象 查看promsie对象返回的结果 103 | // 再根据promise对象返回的结果 决定调用resolve 还是调用reject 104 | resolvePromise(promsie2, x, resolve, reject) 105 | }catch (e) { 106 | reject(e); 107 | } 108 | }, 0) 109 | }else if (this.status === REJECTED) { 110 | setTimeout(() => { 111 | try { 112 | let x = failCallback(this.reason); 113 | // 判断 x 的值是普通值还是promise对象 114 | // 如果是普通值 直接调用resolve 115 | // 如果是promise对象 查看promsie对象返回的结果 116 | // 再根据promise对象返回的结果 决定调用resolve 还是调用reject 117 | resolvePromise(promsie2, x, resolve, reject) 118 | }catch (e) { 119 | reject(e); 120 | } 121 | }, 0) 122 | } else { 123 | // 等待 124 | // 将成功回调和失败回调存储起来 125 | this.successCallback.push(() => { 126 | setTimeout(() => { 127 | try { 128 | let x = successCallback(this.value); 129 | // 判断 x 的值是普通值还是promise对象 130 | // 如果是普通值 直接调用resolve 131 | // 如果是promise对象 查看promsie对象返回的结果 132 | // 再根据promise对象返回的结果 决定调用resolve 还是调用reject 133 | resolvePromise(promsie2, x, resolve, reject) 134 | }catch (e) { 135 | reject(e); 136 | } 137 | }, 0) 138 | }); 139 | this.failCallback.push(() => { 140 | setTimeout(() => { 141 | try { 142 | let x = failCallback(this.reason); 143 | // 判断 x 的值是普通值还是promise对象 144 | // 如果是普通值 直接调用resolve 145 | // 如果是promise对象 查看promsie对象返回的结果 146 | // 再根据promise对象返回的结果 决定调用resolve 还是调用reject 147 | resolvePromise(promsie2, x, resolve, reject) 148 | }catch (e) { 149 | reject(e); 150 | } 151 | }, 0) 152 | }); 153 | } 154 | }); 155 | return promsie2; 156 | } 157 | finally (callback) { 158 | return this.then(value => { 159 | return MyPromise.resolve(callback()).then(() => value); 160 | }, reason => { 161 | return MyPromise.resolve(callback()).then(() => { throw reason }) 162 | }) 163 | } 164 | catch (failCallback) { 165 | return this.then(undefined, failCallback) 166 | } 167 | static all (array) { 168 | let result = []; 169 | let index = 0; 170 | return new MyPromise((resolve, reject) => { 171 | function addData (key, value) { 172 | result[key] = value; 173 | index++; 174 | if (index === array.length) { 175 | resolve(result); 176 | } 177 | } 178 | for (let i = 0; i < array.length; i++) { 179 | let current = array[i]; 180 | if (current instanceof MyPromise) { 181 | // promise 对象 182 | current.then(value => addData(i, value), reason => reject(reason)) 183 | }else { 184 | // 普通值 185 | addData(i, array[i]); 186 | } 187 | } 188 | }) 189 | } 190 | static resolve (value) { 191 | if (value instanceof MyPromise) return value; 192 | return new MyPromise(resolve => resolve(value)); 193 | } 194 | } 195 | 196 | function resolvePromise (promsie2, x, resolve, reject) { 197 | if (promsie2 === x) { 198 | return reject(new TypeError('Chaining cycle detected for promise #')) 199 | } 200 | if (x instanceof MyPromise) { 201 | // promise 对象 202 | // x.then(value => resolve(value), reason => reject(reason)); 203 | x.then(resolve, reject); 204 | } else { 205 | // 普通值 206 | resolve(x); 207 | } 208 | } 209 | 210 | module.exports = MyPromise; 211 | ``` 212 | 213 | #### 三.Generator 214 | Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同. 215 | Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。 216 | 执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。 217 | 形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。 218 | 219 | 220 | ```javascript 221 | function* helloWorldGenerator() { 222 | yield 'hello'; 223 | yield 'world'; 224 | return 'ending'; 225 | } 226 | 227 | var hw = helloWorldGenerator(); 228 | 229 | 230 | hw.next() 231 | // { value: 'hello', done: false } 232 | 233 | hw.next() 234 | // { value: 'world', done: false } 235 | 236 | hw.next() 237 | // { value: 'ending', done: true } 238 | 239 | hw.next() 240 | // { value: undefined, done: true } 241 | 242 | ``` 243 | 由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。 244 | 遍历器对象的next方法的运行逻辑如下。 245 | (1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。 246 | (2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。 247 | (3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。 248 | (4)如果该函数没有return语句,则返回的对象的value属性值为undefined。 249 | 需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。 250 | ```javascript 251 | function* gen() { 252 | yield 123 + 456; 253 | } 254 | 255 | ``` 256 | 上面代码中,yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值。 257 | 258 | yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。 259 | ```javascript 260 | function* f() { 261 | for(var i = 0; true; i++) { 262 | var reset = yield i; 263 | if(reset) { i = -1; } 264 | } 265 | } 266 | 267 | var g = f(); 268 | 269 | g.next() // { value: 0, done: false } 270 | g.next() // { value: 1, done: false } 271 | g.next(true) // { value: 0, done: false } 272 | ``` 273 | 上面代码先定义了一个可以无限运行的 Generator 函数f,如果next方法没有参数,每次运行到yield表达式,变量reset的值总是undefined。当next方法带一个参数true时,变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增。 274 | 这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。 275 | 276 | Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。 277 | ```javascript 278 | var g = function* () { 279 | try { 280 | yield; 281 | } catch (e) { 282 | console.log('内部捕获', e); 283 | } 284 | }; 285 | 286 | var i = g(); 287 | i.next(); 288 | 289 | try { 290 | i.throw('a'); 291 | i.throw('b'); 292 | } catch (e) { 293 | console.log('外部捕获', e); 294 | } 295 | ``` 296 | Generator 函数返回的遍历器对象,还有一个return()方法,可以返回给定的值,并且终结遍历 Generator 函数。 297 | 298 | ```javascript 299 | function* gen() { 300 | yield 1; 301 | yield 2; 302 | yield 3; 303 | } 304 | 305 | var g = gen(); 306 | 307 | g.next() // { value: 1, done: false } 308 | g.return('foo') // { value: "foo", done: true } 309 | g.next() // { value: undefined, done: true } 310 | 311 | ``` 312 | next()、throw()、return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。 313 | next()是将yield表达式替换成一个值。 314 | ```javascript 315 | const g = function* (x, y) { 316 | let result = yield x + y; 317 | return result; 318 | }; 319 | 320 | const gen = g(1, 2); 321 | gen.next(); // Object {value: 3, done: false} 322 | 323 | gen.next(1); // Object {value: 1, done: true} 324 | // 相当于将 let result = yield x + y 325 | // 替换成 let result = 1; 326 | ``` 327 | 上面代码中,第二个next(1)方法就相当于将yield表达式替换成一个值1。如果next方法没有参数,就相当于替换成undefined。 328 | throw()是将yield表达式替换成一个throw语句。 329 | 330 | ```javascript 331 | gen.throw(new Error('出错了')); // Uncaught Error: 出错了 332 | // 相当于将 let result = yield x + y 333 | // 替换成 let result = throw(new Error('出错了')); 334 | ``` 335 | return()是将yield表达式替换成一个return语句。 336 | 337 | ```javascript 338 | gen.return(2); // Object {value: 2, done: true} 339 | // 相当于将 let result = yield x + y 340 | // 替换成 let result = return 2; 341 | ``` 342 | ## 343 | 344 | 如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。 345 | ```javascript 346 | function* foo() { 347 | yield 'a'; 348 | yield 'b'; 349 | } 350 | 351 | function* bar() { 352 | yield 'x'; 353 | // 手动遍历 foo() 354 | for (let i of foo()) { 355 | console.log(i); 356 | } 357 | yield 'y'; 358 | } 359 | 360 | for (let v of bar()){ 361 | console.log(v); 362 | } 363 | ``` 364 | 上面代码中,foo和bar都是 Generator 函数,在bar里面调用foo,就需要手动遍历foo。如果有多个 Generator 函数嵌套,写起来就非常麻烦。 365 | ES6 提供了yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。 366 | 367 | ```javascript 368 | function* bar() { 369 | yield 'x'; 370 | yield* foo(); 371 | yield 'y'; 372 | } 373 | 374 | // 等同于 375 | function* bar() { 376 | yield 'x'; 377 | yield 'a'; 378 | yield 'b'; 379 | yield 'y'; 380 | } 381 | 382 | // 等同于 383 | function* bar() { 384 | yield 'x'; 385 | for (let v of foo()) { 386 | yield v; 387 | } 388 | yield 'y'; 389 | } 390 | 391 | for (let v of bar()){ 392 | console.log(v); 393 | } 394 | ``` 395 | 396 | #### 四. [Async](https://so.csdn.net/so/search?q=Async&spm=1001.2101.3001.7020)/await 397 | ES2017 标准引入了 async 函数,使得异步操作变得更加方便。 398 | async 函数是什么?一句话,它就是 Generator 函数的语法糖。 399 | 400 | ```javascript 401 | const fs = require('fs'); 402 | 403 | const readFile = function (fileName) { 404 | return new Promise(function (resolve, reject) { 405 | fs.readFile(fileName, function(error, data) { 406 | if (error) return reject(error); 407 | resolve(data); 408 | }); 409 | }); 410 | }; 411 | 412 | const gen = function* () { 413 | const f1 = yield readFile('/etc/fstab'); 414 | const f2 = yield readFile('/etc/shells'); 415 | console.log(f1.toString()); 416 | console.log(f2.toString()); 417 | }; 418 | ``` 419 | ```javascript 420 | const asyncReadFile = async function () { 421 | const f1 = await readFile('/etc/fstab'); 422 | const f2 = await readFile('/etc/shells'); 423 | console.log(f1.toString()); 424 | console.log(f2.toString()); 425 | }; 426 | 427 | ``` 428 | 一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。 429 | 430 | async函数对 Generator 函数的改进,体现在以下四点。 431 | 1 内置执行器。 432 | Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。 433 | ```javascript 434 | asyncReadFile(); 435 | 436 | ``` 437 | 438 | 上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。 439 | 2 更好的语义。 440 | async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。 441 | 3 更广的适用性。 442 | co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。 443 | 4 返回值是 Promise。 444 | async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。 445 | 进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。 446 | 447 | async/await语法糖就是使用Generator函数+自动执行器来运作的。 我们可以参考以下例子 448 | ```javascript 449 | // 定义了一个promise,用来模拟异步请求,作用是传入参数++ 450 | function getNum(num){ 451 | return new Promise((resolve, reject) => { 452 | setTimeout(() => { 453 | resolve(num+1) 454 | }, 1000) 455 | }) 456 | } 457 | 458 | //自动执行器,如果一个Generator函数没有执行完,则递归调用 459 | function asyncFun(func){ 460 | var gen = func(); 461 | 462 | function next(data){ 463 | var result = gen.next(data); 464 | if (result.done) return result.value; 465 | result.value.then(function(data){ 466 | next(data); 467 | }); 468 | } 469 | 470 | next(); 471 | } 472 | 473 | // 所需要执行的Generator函数,内部的数据在执行完成一步的promise之后,再调用下一步 474 | var func = function* (){ 475 | var f1 = yield getNum(1); 476 | var f2 = yield getNum(f1); 477 | console.log(f2) ; 478 | }; 479 | asyncFun(func); 480 | 481 | ``` 482 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 n0liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MarkDown/index.md: -------------------------------------------------------------------------------- 1 | #### 后续更新 -------------------------------------------------------------------------------- /SERVER/linux服务器安装nginx及使用.md: -------------------------------------------------------------------------------- 1 | # linux服务器安装nginx及使用 2 | 3 | Nginx在个人的使用之后,感觉非常的方便,所以在这里给出自己安装配置方案。它是一款高性能的 Web和 反向代理 服务器,也是一个 IMAP/POP3/SMTP 代理服务器。负载均衡是个不错的选择。 4 | 5 | 我的linux服务器是阿里云的 CentOS 7.4 64位,下面是安装过程 6 | 7 | ### 😀 **第一步:先安装PCRE pcre-devel 和Zlib,再配置nginx的时候会用到** 8 | 9 | PCRE(Perl Compatible Regular Expressions) 是一个Perl库,包括 perl 兼容的正则表达式库。nginx 的 http 模块使用 pcre 来解析正则表达式,所以需要在 linux 上安装 pcre 库,pcre-devel 是使用 pcre 开发的一个二次开发库。nginx也需要此库。命令: 10 | 11 | ```bash 12 | yum install -y pcre pcre-devel 13 | ``` 14 | 15 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409121542122-2035469450.png) 16 | 17 | zlib 库提供了很多种压缩和解压缩的方式, nginx 使用 zlib 对 http 包的内容进行 gzip ,所以需要在Centos 上安装 zlib 库。 18 | 19 | ```bash 20 | yum install -y zlib zlib-devel 21 | ``` 22 | 23 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409121703911-1003096243.png) 24 | 25 | 安装好这两个之后就可以安装nginx了,但是如果安装的时候有问题的话可能需要安装GCC和OpenSSL以下提供命令 26 | 27 | ```bash 28 | yum install gcc-c++ yum install -y openssl openssl-devel 29 | ``` 30 | 31 | ### 😃 **第二步:安装nginx 1.14.0** 32 | 33 | ```bash 34 | wget -c https://nginx.org/download/nginx-1.14.0.tar.gz 35 | ``` 36 | 37 | 解压并进入nginx目录 38 | 39 | ```bash 40 | tar -zxvf nginx-1.14.0.tar.gz 41 | cd nginx-1.14.0 42 | ``` 43 | 44 | 使用nginx的默认配置 45 | 46 | ```bash 47 | ./configure 48 | ``` 49 | 50 | 编译安装 51 | 52 | ```bash 53 | make 54 | make install 55 | ``` 56 | 57 | 查找安装路径: 58 | 59 | ```bash 60 | whereis nginx 61 | ``` 62 | 63 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409141710895-1972575936.png) 64 | 65 | 进入`sbin`目录,可以看到有一个可执行文件`nginx`,直接./执行就OK了。 66 | 67 | 运行起来之后访问服务器ip,可以看到nginx的欢迎页面 68 | 69 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409141912922-232230531.png) 70 | 71 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409151623454-1676344291.png) 72 | 73 | > **这里提几点需要注意的地方** 74 | 75 | 1.安装好启动好后无法访问到页面 76 | 77 | 查看是否安装好 78 | 79 | ```bash 80 | ps -ef|grep nginx 81 | ``` 82 | 83 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409152151927-909034755.png) 84 | 85 | 如果如上图有nginx的进程说明启动好了这个时候如果无法访问nginx页面可以先查看一下你服务器的安全组策略**是否有启用80端口** 86 | 87 | 下图表示已开启 88 | 89 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409152428264-647363183.png) 90 | 91 | 如果启用之后还是无法访问需要查看nginx的配置文件`nginx.conf` 92 | 93 | **先查找自己的**\*\*`nginx`\*\***安装目录** 94 | 95 | ```bash 96 | whereis nginx 97 | ``` 98 | 99 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409152712344-1875541177.png) 100 | 101 | 目录在`/usr/local/nginx`中,进入`sbin`文件夹下面发现有一个`nginx`的可执行文件 102 | 103 | 在`sbin`中可以执行下面这个语句查询自己使用的`nginx.conf`在哪个位置,同时这个语句也可以验证你的`nginx.conf`文件是否是正确的。正确的格式会提示`test is successful` 104 | 105 | ```bash 106 | ./nginx -t 107 | ``` 108 | 109 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409153042438-1377542008.png) 110 | 111 | 找到这个配置文件目录在`/usr/local/nginx/conf`下 112 | 113 | 我们编辑里面的映射路径 114 | 115 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409153506109-1419287243.png) 116 | 117 | 把这个路径改为你的文件存放路径 118 | 119 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409153602121-86768224.png) 120 | 121 | 这样的话基本没有问题了,有其他问题也可以说出来一起探讨。 122 | 123 | 最后是nginx的一些基本命令,有一些已经在前面提到了,这里也一并列出 124 | 125 | **启动** 126 | 127 | 启动代码格式:nginx安装目录地址 -c nginx配置文件地址 128 | 129 | ```bash 130 | /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf 131 | ``` 132 | 133 | **停止** 134 | 135 | nginx的停止有三种方式 136 | 137 | **从容停止** 138 | 139 | ```bash 140 | ps -ef|grep nginx 141 | ``` 142 | 143 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409154711813-1966650821.png) 144 | 145 | 杀死进程 146 | 147 | ```bash 148 | kill -QUIT 3905 149 | ``` 150 | 151 | **快速停止** 152 | 153 | ```abap 154 | kill -TERM 3905 155 | ``` 156 | 157 | 或者 158 | 159 | ```bash 160 | kill -INT 3905 161 | ``` 162 | 163 | **强制停止** 164 | 165 | ```bash 166 | pkill -9 nginx 167 | ``` 168 | 169 | **重启** 170 | 171 | 方法一:进入`nginx`可执行目录`sbin`下,输入命令`./nginx -s reload` 即可 172 | 173 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409155547898-1115398652.png) 174 | 175 | 方法二:查找当前`nginx`进程号,然后输入命令:`kill -HUP`进程号 实现重启 176 | 177 | ![](https://img2018.cnblogs.com/blog/1470384/201904/1470384-20190409155908331-1050350400.png) 178 | -------------------------------------------------------------------------------- /Tool/index.md: -------------------------------------------------------------------------------- 1 | ### 前端工程化工具 2 | 3 | |目录 | 内容描述 4 | |- | -:| 5 | 打包工具 | [使用技巧](./打包工具.md) 6 | 包管理 | [使用技巧](./包管理.md) 7 | 模块化_模块标准 | [使用技巧](./模块化_模块标准.md) -------------------------------------------------------------------------------- /Tool/包管理.md: -------------------------------------------------------------------------------- 1 | # package.json 2 | 在每个前端项目中,都有package.json文件,它是项目的配置文件,常见的配置有配置项目启动、打包命令,声明依赖包等。package.json文件是一个JSON对象,该对象的每一个成员就是当前项目的一项设置。 3 | ```json 4 | { 5 | "name": "my-app", 6 | "version": "0.1.0", 7 | "private": true, 8 | "dependencies": { 9 | "@testing-library/jest-dom": "^5.14.1", 10 | "@testing-library/react": "^11.2.7", 11 | "@testing-library/user-event": "^12.8.3", 12 | "react": "^17.0.2", 13 | "react-dom": "^17.0.2", 14 | "react-scripts": "4.0.3", 15 | "web-vitals": "^1.1.2" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test", 21 | "eject": "react-scripts eject" 22 | }, 23 | "eslintConfig": { 24 | "extends": [ 25 | "react-app", 26 | "react-app/jest" 27 | ] 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | 43 | ``` 44 | package.json 常见配置项如下: 45 | ![](https://cdn.nlark.com/yuque/0/2022/webp/5372952/1651652962768-2c2a4853-3bb8-404f-a43b-8a14a4129947.webp#clientId=u209e9f5f-b3b6-4&from=paste&id=u469a42e7&originHeight=856&originWidth=1304&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=uce103db0-7d52-4fb9-b252-d25102afd02&title=) 46 | ## 必须属性 47 | ### name 48 | name就是最简单的项目的名称,但是它有几点注意事项: 49 | 50 | - 名称的长度必须小于或等于214个字符,不能以“.”和“_”开头,**不能包含大写字母** 51 | - 如果需要发布,那么名称不能和其他模块重复,可以使用`npm view`查询是否重复,本地使用不需要考虑 52 | ### version 53 | version字段表示该项目包的版本号,项目改动后,需要更新新的版本号,具体规范如下: 54 | 55 | - 版本号的命名遵循语义化版本2.0.0规范,格式为:**主版本号.次版本号.修订号**,通常情况下,修改主版本号是做了大的功能性的改动,修改次版本号是新增了新功能,修改修订号就是修复了一些bug; 56 | - 如果某个版本的改动较大,并且不稳定,可能如法满足预期的兼容性需求,就需要发布先行版本,先行版本通过会加在版本号的后面,通过“-”号连接以点分隔的标识符和版本编译信息:内部版本(alpha)、公测版本(beta)和候选版本(rc,即release candiate) 57 | ## 描述信息 58 | ### description 59 | description就是字面意思,用来描述这个项目包。 60 | ### keywords 61 | keywords也是字面意思,表示这个项目包的关键词。 62 | ![ZTU(RK5O[UEGPUA]3(1~P]9.png](https://cdn.nlark.com/yuque/0/2022/png/5372952/1651665093384-8a4d44c3-f893-49e5-ae45-8e351d6353fa.png#clientId=u209e9f5f-b3b6-4&from=paste&height=175&id=u43abf52a&name=ZTU%28RK5O%5BUEGPUA%5D3%281~P%5D9.png&originHeight=175&originWidth=845&originalType=binary&ratio=1&rotation=0&showTitle=false&size=20203&status=done&style=none&taskId=uf807d5a8-680f-4c6c-b822-186159224bd&title=&width=845) 63 | ### author 64 | author也是字面意思,表示项目包的作者,会有两种形式表示 65 | ```json 66 | // 字符串形式 67 | "author": "GAOJUN (https://juejin.cn/user/xxx)" 68 | 69 | // 对象形式 70 | "author": { 71 | "name" : "GAOJUN", 72 | "email" : "xxxxx@xx.com", 73 | "url" : "https://juejin.cn/user/xxx" 74 | } 75 | ``` 76 | ### contributors 77 | contributors表示该项目包的所有贡献者,也会有两种形式表示 78 | ```json 79 | "contributors": [ 80 | "GAOJUN1 (https://juejin.cn/user/xxx)", 81 | "GAOJUN2 (https://juejin.cn/user/xxx)" 82 | ] 83 | 84 | "contributors": [ 85 | { 86 | "name" : "GAOJUN1", 87 | "email" : "xxxxx@xx.com", 88 | "url" : "https://juejin.cn/user/xxx" 89 | }, 90 | { 91 | "name" : "GAOJUN2", 92 | "email" : "xxxxx@xx.com", 93 | "url" : "https://juejin.cn/user/xxx" 94 | } 95 | ] 96 | 97 | ``` 98 | ### homepage 99 | homepage就是项目的主页地址 100 | ### repository 101 | repository表示代码的存放仓库地址,通常有两种书写形式: 102 | ```json 103 | // 字符串 104 | "repository": "https://github.com/facebook/react.git" 105 | 106 | // 对象 107 | "repository": { 108 | "type": "git", 109 | "url": "https://github.com/facebook/react.git" 110 | } 111 | 112 | ``` 113 | ### bugs 114 | bugs表示项目提交问题的地址,一个提交问题的地址和反馈的邮箱,最常见的bugs就是Github中的issues页面,下上就是react的issues页面地址 115 | ```json 116 | "bugs": { 117 | "url" : "https://github.com/facebook/react/issues", 118 | "email" : "xxxxx@xx.com" 119 | } 120 | ``` 121 | ## 依赖配置 122 | 通常情况下,我们的项目会依赖多个外部的依赖包,根据依赖包的不同用途会分为:dependencies、devDependencies、peerDependencies、bundledDependencies、optionalDependencies。 123 | ### dependencies 124 | dependencies表示项目生产环境所必须的依赖包,也就是打包后依旧需要运行的。常用的操作如下 125 | ```javascript 126 | npm install 127 | yarn add 128 | 129 | npm install --save/-S 130 | ``` 131 | ### devDependencies 132 | devDependencies中声明的是开发阶段需要的依赖包,如Webpack、Eslint、Babel等,用于辅助开发,它们最后不需要运行在生产环境,不需要打包,安装这些依赖时需要设定特定的参数去确定放在这里。 133 | ```javascript 134 | npm install --save-dev/-D 135 | yarn add --dev/-D 136 | ``` 137 | ### peerDependencies 138 | 有些情况下,我们的项目和所依赖的模块,都会同时依赖另一个模块,下次依赖的时候应该只下载一次,而不是每次依赖下,这样核心依赖就算重复也只会下载一次。 139 | 举例说明: 140 | 目前使用的基于`react`的ui组件库`ant-design`会依赖`react`和`react-dom`,那么我们项目中引入`react`,`react-dom`和`ant-design`,那么就会有重复的`react`和`react-dom`,于是在`ant-design`中package.json中配置 141 | ```json 142 | "peerDependencies": { 143 | "react": ">=16.0.0", 144 | "react-dom": ">=16.0.0" 145 | } 146 | ``` 147 | 然后在我们项目的package.json中正常配置大于等于16.0.0版本的`react`,`react-dom`就行,最后只会下载一次依赖。 148 | 从npm 3.0版开始,peerDependencies不再会默认安装了。 149 | ### optionalDependencies 150 | optionalDependencies可以看作是可选项,当它中一些依赖安装失败时项目正常运行或npm继续安装别的依包,注意:optionalDependencies会覆盖dependencies中的同名的依赖包,所以依赖包不要出现在两个地方 151 | ### bundledDependencies 152 | bundledDependencies是一个数组,将指定的依赖包在发布时一起打包,这个字段数组中的值必须是在dependencies, devDependencies两个里面声明过的包才行。 153 | ### engines 154 | engines中说明具体的版本号,一些项目需要特定版本的npm包的版本或者Node版本。 155 | ```json 156 | "engines": { 157 | "node": ">=8.10.3 <12.13.0", 158 | "npm": ">=6.9.0" 159 | } 160 | ``` 161 | ## 脚本配置 162 | ### scripts 163 | scripts 是 package.json中内置的脚本入口,是key-value键值对配置,key为可运行的命令,可以通过 npm run 来执行命令。除了运行基本的scripts命令,还可以结合pre和post完成前置和后续操作。 164 | ```json 165 | "scripts": { 166 | "dev": "node index.js", 167 | "predev": "node beforeIndex.js", 168 | "postdev": "node afterIndex.js" 169 | } 170 | // 执行顺序是predev→dev→postdev 171 | ``` 172 | 然后我们常用的打包build,测试test等操作都定义在这里 173 | ```json 174 | "scripts": { 175 | "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", 176 | "start": "npm run dev", 177 | "unit": "jest --config test/unit/jest.conf.js --coverage", 178 | "test": "npm run unit", 179 | "lint": "eslint --ext .js,.vue src test/unit", 180 | "build": "node build/build.js", 181 | "ci": "npm run lint && npm test" 182 | } 183 | ``` 184 | ### config 185 | config用来配置scripts运行时的配置参数 186 | ```json 187 | "config": { 188 | "port": 3000 189 | } 190 | ``` 191 | 拿到这个配置参数`process.env.npm_package_config_port` 192 | ## 文件&目录 193 | ### main 194 | main用来指定加载的入口文件,在 browser 和 Node 环境中都可以使用。不指定,默认为根目录下的index.js 195 | ### browser 196 | browser用来指定 npm 包只在 browser 环境下的入口文件 197 | ### module 198 | module用来定义 npm 包的 ESM 规范的入口文件,browser 环境和 node 环境均可使用 199 | ```json 200 | "main": "./src/index.js", 201 | "browser": "./src/index.js", 202 | "module": "./src/index.mjs", 203 | ``` 204 | ### bin 205 | bin 用来指定内部命令对应的可执行文件的位置,是key-value键值对配置,key为可执行文件的位置 206 | ```json 207 | "bin": { 208 | "someTool": "./bin/someTool.js" 209 | } 210 | 211 | scripts: { 212 | start: './node_modules/bin/someTool.js build' 213 | } 214 | // 简写 215 | scripts: { 216 | start: 'someTool build' 217 | } 218 | ``` 219 | 这里,someTool 命令对应的可执行文件为 bin 目录下的 someTool.js,就是 ./node_modules/.bin/someTool.js ,npm为scripts字段中的脚本路径自动添加了node_modules/.bin前缀,所以可以简写 220 | ### files 221 | files配置是一个数组,用来描述当把npm包作为依赖包安装时需要说明的文件列表,当npm包发布时,files指定的文件会被推送到npm服务器中,如果指定的是文件夹,那么该文件夹下面所有的文件都会被提交。 222 | ```json 223 | "files": [ 224 | "LICENSE", 225 | "Readme.md", 226 | "index.js", 227 | "lib/" 228 | ] 229 | ``` 230 | ## 发布配置 231 | ### private 232 | private可以防止我们意外地将私有库发布到npm服务器。只需要将该字段设置为true 233 | ```json 234 | "private": true 235 | ``` 236 | ### preferGlobal 237 | preferGlobal表示当用户不把该模块安装为全局模块时,如果设置为true就会显示警告。它并不会真正的防止用户进行局部的安装,只是对用户进行提示,防止产生误解。 238 | ```json 239 | "preferGlobal": true 240 | ``` 241 | ### publishConfig 242 | publishConfig配置会在模块发布时生效,用于设置发布时一些配置项的集合。如果不想模块被默认标记为最新,或者不想发布到公共仓库,可以在这里配置tag或仓库地址。 243 | 通常情况下,publishConfig会配合private来使用,如果只想让模块发布到特定npm仓库,就可以这样来配置: 244 | ```json 245 | "private": true, 246 | "publishConfig": { 247 | "tag": "1.1.0", 248 | "registry": "https://registry.npmjs.org/", // 想要发布的地址 249 | "access": "public" 250 | } 251 | ``` 252 | ### os 253 | os可以让我们设置该npm包可以在什么操作系统使用,不能再什么操作系统使用。 254 | ```json 255 | "os" ["linux"] // 适用的操作系统 256 | "os" ["!win32"] // 禁用的操作系统 257 | ``` 258 | ### cpu 259 | 该配置和OS配置类似,用CPU可以更准确的限制用户的安装环境: 260 | ```json 261 | "cpu" ["x64", "AMD64"] // 适用的cpu 262 | "cpu" ["!arm", "!mips"] // 禁用的cpu 263 | ``` 264 | ### license 265 | license 字段用于指定软件的开源协议,开源协议表述了其他人获得代码后拥有的权利,可以对代码进行何种操作,何种操作又是被禁止的。常见的协议如下: 266 | 267 | - MIT :只要用户在项目副本中包含了版权声明和许可声明,他们就可以拿你的代码做任何想做的事情,你也无需承担任何责任。 268 | - Apache :类似于 MIT ,同时还包含了贡献者向用户提供专利授权相关的条款。 269 | - GPL :修改项目代码的用户再次分发源码或二进制代码时,必须公布他的相关修改。 270 | ```json 271 | "license": "MIT" 272 | ``` 273 | ## 第三方配置 274 | package.json 文件还可以承载命令特有的配置,例如 Babel、ESLint 等。它们每个都有特有的属性,例如 eslintConfig、babel 等。 它们是命令特有的,可以在相应的命令/项目文档中找到如何使用它们。最好都使用单独的配置文件。 275 | # npm/yarn等 276 | yarn 代表另一个资源谈判者。yarn 包管理器是 npm 的一个替代方案,由Facebook于2016年10月发布。yarn最初的目标是处理npm的缺点,比如性能和安全问题。yarn很快被定位为一个安全、快速、可靠的JavaScript依赖管理工具。 277 | ## npm和yarn区别 278 | 279 | 1. npm预先安装在 Node 中的,不需要手动安装,yarn需要通过`npm install -g yarn`全局安装 280 | 2. npm和yarn安装依赖,命令有细微的差别 281 | ```json 282 | npm install | yarn:安装依赖 283 | npm install [package] | yarn add [package]:安装一个包 284 | npm install --save-dev [package] | yarn add --dev [package]:安装包作为开发依赖项 285 | npm uninstall [package] | yarn remove [package]:卸载一个包 286 | npm uninstall --save-dev [package] | yarn remove [package]:卸载开发依赖包 287 | npm update | yarn upgrade:更新的依赖关系 288 | npm update [package] | yarn upgrade [package]:更新包 289 | ``` 290 | 291 | 3. npm是逐个安装依赖,而yarn是并行安装,效率会有所提高,同时也会失败重试,目前最新版本两种差不多 292 | 4. yarn安装模块扁平化,npm不会,目前最新版本npm也有优化 293 | 5. 为避免包版本不匹配,确切安装的版本被固定在包锁定文件中,每次添加模块时,npm 和 yarn 分别创建(或更新)一个 package-lock.json 和 yarn.lock 文件。 294 | 6. yarn使用缓存机制,实现了离线模式 ,目前的npm也有类似的实现 295 | 296 | [https://juejin.cn/post/7014096605569613860](https://juejin.cn/post/7014096605569613860) 297 | [https://juejin.cn/post/7060844948316225572](https://juejin.cn/post/7060844948316225572) 298 | ## cnpm 299 | 300 | - cnpm跟npm用法完全一致,只是在执行命令时将npm改为cnpm 301 | - npm安装插件是从国外服务器下载,受网络影响大,可能出现异常,如果npm的服务器在中国就好了,于是淘宝团队干了这事。来自官网:“这是一个完整 npmjs.org镜像,你可以用此代替官方版本(只读),同步频率目前为 10分钟 一次以保证尽量与官方服务同步。 302 | - 安装: npm install -g cnpm --registry=https://registry.npm.taobao.org 303 | ## pnpm 304 | 305 | - pnpm运行起来非常的快,[超过了npm和yarn](https://github.com/pnpm/node-package-manager-benchmark) 306 | - pnpm采用了一种巧妙的方法,利用硬链接和符号链接来避免复制所有本地缓存源文件,这是yarn的最大的性能弱点之一 307 | - 使用链接并不容易,会带来一堆问题需要考虑。 308 | - pnpm继承了yarn的所有优点,包括离线模式和确定性安装 309 | 310 | # multirepo/monorepo 311 | 当我们开发了一个组件库,其中有各种各样的组件,我们需要发布到npm上供其他人使用,这时候我们希望一个项目去管理所有的包。 312 | 这时候就会有两种项目的组织方式: 313 | 一种是每一个包对应一个项目(multirepo),一种是一个项目去管理多个模块或包(monorepo)。 314 | 下面是monorepo的项目的结构 315 | ![RZ%DZ%EC[0T52WN2_$9NZJX.png](https://cdn.nlark.com/yuque/0/2022/png/5372952/1651994614570-c571d694-1144-4197-8e67-15c1c498c1a3.png#clientId=u6535fdaf-6dd7-4&from=paste&height=449&id=u69089f57&name=RZ%25DZ%25EC%5B0T52WN2_%249NZJX.png&originHeight=449&originWidth=285&originalType=binary&ratio=1&rotation=0&showTitle=false&size=17577&status=done&style=none&taskId=uf9c35592-fbbf-4c1e-9c6d-f4f697ee1de&title=&width=285) 316 | ## 如何控制每一个项目中的依赖(yarn workspace) 317 | ```shell 318 | // 给特定的包安装依赖 319 | yarn workspace 320 | 321 | 例如: 322 | yarn workspace lg-button add lodash@4 323 | yarn workspace lg-button remove lodash@4 324 | 325 | // 给所有的包都添加相同的依赖 326 | yarn add some-package -W 327 | ``` 328 | ## 如何将一个项目中的所有包都发布到npm 329 | ```shell 330 | yarn global add lerna 331 | lerna init 332 | lerna publish 333 | ``` 334 | 注意:修改项目了,需要重新提交后,再`lerna publish` 335 | 336 | [https://juejin.cn/post/6927472790438150152](https://juejin.cn/post/6927472790438150152) 337 | # npm 发包 338 | 操作流程: 339 | 340 | 1. 到[官网](https://links.jianshu.com/go?to=https%3A%2F%2Fwww.npmjs.com%2F)注册一个npm账号,记住用户名,密码和邮箱,注册之后需要在邮箱中验证一下才能使用。 341 | 2. `npm init` 创建一个npm包,构建项目,修改 package.json 342 | 3. `npm login` 登录 npm 账号 343 | 4. `npm publish`将包发布到npm 344 | 345 | **修改包:** 346 | 同一个版本号不能反复发布两次,所以每次修改后发版前都要修改版本号,修改方法是执行`npm version patch`,`npm version major``,npm version minor`,三者的区别是修改的版本号不同,以1.0.0为例,执行三者后的效果 347 | `npm version patch`:1.0.0会变成1.0.1 修复bug 348 | `npm version major`:1.0.0会变成2.0.0 大功能性改动 349 | `npm version minor`:1.0.0会变成1.1.0 加了新功能 350 | 修改完版本号就可以执行npm publish重新发包 351 | 一般来说还有先行版本,测试版本等 352 | `npm version 1.0.0-alpha.1` 353 | `npm publish --tag=beta` 354 | **删除包:** 355 | `npm unpublish 包名 --force` 删除已发布包 356 | 357 | - npm unpublish 命令只能删除 72 小时以内发布的包 358 | - npm unpublish 删除的包,在 24 小时内不允许重复发布 359 | 360 | -------------------------------------------------------------------------------- /Tool/打包工具.md: -------------------------------------------------------------------------------- 1 | 工具还是用来提效的,如果没有明显的收益,也不会成为前端的必备技能之一,那么这个工具能够做什么? 2 | 3 | 1. 我们在开发的应用,为了方便我们调试,在每次启动的时候都会先进行打包这个操作,打包的结果存储在内存中,浏览器访问这个结果就呈现这个内容。热更新就是我们修改文件,这个服务会监听相关联文件的变动,重新进行打包操作,然后通知客户端主动获取最新的资源。 4 | 2. 部署应用的时候,我们不可能把本地开发的代码一股脑梭哈到服务器上,在这个过程中我们需要考虑资源的整合。例如,静态资源的压缩、源代码的混淆、代码的兼容性、加载速度优化。 5 | 6 | 上述的两种例子只是我们随处可以看到的应用场景,如果不使用这些工具当然也能够做到,但是通过工具能够极大的简化这些功能的使用方式,通过简单的配置的方式就可以完成 7 | # webpack 8 | > webpack是一个用于现代javascript应用程序的静态模块打包工具 9 | 10 | 静态模块,是指的是在开发阶段可以被webpack直接引用的资源 11 | ## 基本使用 12 | ```shell 13 | yarn add webpack webpack-cli 14 | yarn webpack --config 15 | ``` 16 | ## 依赖图 17 | 一个文件依赖于另一个文件,webpack 就把此视为文件之间有依赖关系 。从这些入口起点开始,webpack 递归地构建一个依赖图,这个依赖图包含着应用程序所需的每个模块,只有存在依赖关系的文件才会参与webpack的打包 18 | ## 入口 19 | webpack执行的入口,从这里开始,按照依赖图的关系把所有的资源按照规则整合成指定的样子,默认情况就是一个js、html、css文件。 20 | ```javascript 21 | module.exports = { 22 | entry: '/main.js', 23 | }; 24 | 25 | // 多入口打包 26 | module.exports = { 27 | entry: { 28 | main: './app.js', 29 | vendor: './vendor.js' 30 | } 31 | } 32 | ``` 33 | ## 输出 34 | 告诉webpack执行结果在哪里输出以及如何命名这些文件。 35 | ```javascript 36 | module.exports = { 37 | entry: { 38 | main: './app.js', 39 | }, 40 | output: { 41 | path: path.resolve(__dirname, 'dist'), 42 | filename: '[name].[contenthash:6].bundle.js' // 强缓存 优化~~ 43 | } 44 | } 45 | ``` 46 | 这里配置了一个入口文件。对于这个文件的输出文件带上了[contenthash] 这意思是按照文件内容生成对应的hash值,那么当该文件更新的时候,文件的hash值就会变化。这样浏览器加载对应的文件的时候,不管是进行缓存还是及时更新都有益。 47 | ## resolve 48 | ### alias 49 | 可以设置import或require引入文件的别名,对于相同模块下的文件引用,可以使用同样的类型引用名称。 50 | ```javascript 51 | const path = require('path'); 52 | 53 | module.exports = { 54 | //... 55 | resolve: { 56 | alias: { 57 | utils: path.resolve(__dirname, 'src/public/utils'), 58 | }, 59 | }, 60 | }; 61 | ``` 62 | ### enforceExtension 63 | 是否允许省略扩展名称 64 | ```javascript 65 | module.exports = { 66 | //... 67 | resolve: { 68 | enforceExtension: boolean, 69 | }, 70 | }; 71 | 72 | // false 73 | import foo from './index'; // success 74 | import foo from './index.js'; // success 75 | 76 | // true 77 | import foo from './index'; // error 78 | import foo from './index.js'; // success 79 | ``` 80 | ### extensions 81 | 默认情况下引入文件不写后缀名称,可以匹配到哪些文件类型的文件 82 | ```javascript 83 | module.exports = { 84 | //... 85 | resolve: { 86 | extensions: ['.js', '.json', '.wasm'], // 意思是项目中引入文件不写后缀名称的只会匹配 js json wasm类型的文件 87 | }, 88 | }; 89 | ``` 90 | ### mainFiles 91 | 引入文件时,不精准定位到哪个文件,解析已什么文件名称开头的文件 92 | ```javascript 93 | module.exports = { 94 | //... 95 | resolve: { 96 | mainFiles: ['index'], 97 | }, 98 | }; 99 | 100 | import index from './src'; 101 | // src 目录下必须存在一个index文件,不然引入文件就会报错 102 | ``` 103 | ## loader 104 | webpack默认只能处理js和json文件,这是webpack开箱自带的能力。而当需要处理其他资源,例如css、scss、html、字体等文件时,webpack无法直接处理。而loader可以让webpack去处理其他类型的文件。 105 | ### 如何使用loader? 106 | 107 | 1. 配置方式(推荐):在webpack.config.js文件中指定loader 108 | 2. 内联方式:在每个import语句中显示的指定loader 109 | ```javascript 110 | // 配置 111 | module.exports = { 112 | module: { 113 | rules: [ 114 | { 115 | test: /\.css$/, // 处理什么文件 116 | use: ['style-loader'] //使用什么处理 117 | } 118 | ] 119 | } 120 | } 121 | 122 | // 内联方式 123 | import style from 'style-loader!./style.css' 124 | ``` 125 | ### loader特性 126 | 127 | - loader支持链式调用。链中的每个loader会将转换,应用在已处理过的资源上。一组链式的loader将按照相反的顺序执行(从右往左,从下往上)。链中的第一个loader将其结果(也就是应用过转换后的资源)传递给下一个loader,依此类推。最后,链中的最后一个loader,返回webpack所期望的javascript。 128 | - 简单的来说就是当需要处理一个less文件时,需要配置less-loader、css-loader、style-loader,通过这种链式调用的方式可得到期望的结果 129 | - loader可以是同步的,也可以是异步的。 130 | - 取决于loader内部实现的返回callback方式,如果是直接返回或者直接调用this.callback就是同步的。如果是通过this.async()获得的callback就是异步的方式 131 | - loader运行在node.js中,并且能够执行任何操作。 132 | - loader 可以通过 options 对象配置 133 | - 比如当使用postcss-loader配合css-loader、autoprefixer进行css兼容性处理的时候,一般的配置顺序都是['style-loader', 'css-loader', 'postcss-loader'],而在css代码中书写@import语法引入其他css资源时,postcss-loader无法解析@import,导致@import引入的css未做兼容性处理,因为loader处理管道流,默认情况下是不会返回到上一个插件的。这个时候需要告诉css-loader当匹配到这样的语法的时候,要往回走,重新再处理一遍,通过对css-loader配置options.importLoaders 134 | ### 手写loader 135 | 136 | 1. loader导出的是一个函数 137 | 2. loader就是文件的输入和输出 138 | 3. 和管道一样,从管道的一头输入,经过多个loader的处理,从管道的另一头输出。 139 | 4. 自定义loader要遵循xxx-loader的命名规则 140 | 5. 函数中的 this 作为上下文会被 webpack 填充,this中的具体内容[参考官网](https://webpack.docschina.org/api/loaders/#asynchronous-loaders) 141 | ```javascript 142 | const marked = require('marked'); 143 | 144 | module.exports = function(source) { 145 | // 这个source默认就是文件的内容了 146 | // 处理markdown文件 147 | const html = marked(source); 148 | 149 | // 返回html字符串交给下一个loader处理 150 | return html 151 | } 152 | ``` 153 | ### loader使用 154 | #### css-loader 155 | css-loader处理css类命名冲突的方式 156 | ```javascript 157 | module.exports = { 158 | module: { 159 | rules: [ 160 | { 161 | test: /\.css$/i, 162 | loader: "css-loader", 163 | options: { 164 | modules: { 165 | localIdentName: "[path][name]__[local]--[hash:base64:5]", 166 | }, 167 | }, 168 | }, 169 | ], 170 | }, 171 | }; 172 | 173 | ``` 174 | #### postcss- loader 175 | 当使用postcss-loader配合css-loader、autoprefixer进行css兼容性处理的时候,一般的配置顺序都是['style-loader', 'css-loader', 'postcss-loader'],而在css代码中书写@import语法引入其他css资源时,postcss-loader无法解析@import,导致@import引入的css未做兼容性处理,因为loader处理管道流,默认情况下是不会返回到上一个插件的。这个时候需要告诉css-loader当匹配到这样的语法的时候,要往回走,重新再处理一遍 176 | ```javascript 177 | { 178 | test: /\.css$/, 179 | use: [ 180 | 'style-loader', 181 | { 182 | loader: 'css-loader', 183 | options: { 184 | // 这个1是代表,往前找几个插件,默认为0 185 | // 当遇到css需要处理的,就往前找一个插件来进行处理,比如说这里往前一个就是postcss-loader 186 | importLoaders: 1 187 | } 188 | } 189 | 'postcss-loader' 190 | ] 191 | } 192 | ``` 193 | #### file-loader 194 | 195 | 1. 设置这个loader可以处理二进制资源或者返回一个js能够处理的资源 196 | 2. file-loader版本更新,上一个版本直接返回字符串形式直接使用,webpack5适配file-loader返回的是一个对象,需要在require(<图片地址>).default 197 | 3. 如果需要使用require来导入,但是不想加.defaule做后缀,可以给file-loader添加配置esModule: false 198 | 4. background: url('./img.png') css-loader看到这个导入代码,会替换成require语法,而require返回的是esModule对象,所以css-loader设置参数esModule: false 199 | ```javascript 200 | { 201 | test: /\.(png|svg|gif|jpe?g)$/, 202 | use: [ 203 | { 204 | loader: 'file-loader', 205 | options: { 206 | // 配置输出图片路径和名称 207 | name: 'img/[name].[hash:6].[ext]', 208 | // esModule: false, // 不转为 esModule require导入图片时不需要使用default 209 | // outputPath: 'img' // 输出的目录 210 | } 211 | } 212 | ] 213 | } 214 | ``` 215 | #### url-loader 216 | 217 | 1. url-loader将uri文件以base64打到文件当中,这样可以减少文件请求次数 218 | 2. file-loader是将文件拷贝到指定的目录,分开请求 219 | 3. url-loader内部也使用了file-loader 220 | 4. url-loader与file-loader的适用性 221 | 1. 小文件可以使用file-loader 222 | 2. 大文件转换成base64也很大,还是直接拷贝资源比较合理 223 | 3. limit设置转换成base64的阈值,决定是使用url-loader还是file-loader 224 | ```javascript 225 | { 226 | test: /\.(png|svg|gif|jpe?g)$/, 227 | use: [{ 228 | loader: 'url-loader', 229 | options: { 230 | // 配置输出图片路径和名称 231 | name: 'img/[name].[hash:6].[ext]', 232 | limit: 25 * 1024 233 | } 234 | }] 235 | } 236 | ``` 237 | #### webpack5 assets模块 238 | 239 | 1. asset自动选择导出为单独文件或者dataURL形式(默认8kb)。之前有url-loader设置asset size limit限制实现 240 | 2. asset/resource将资源分割为单独的文件,并导出url,就是之前的file- loader的功能 241 | 3. asset/inline 将资源导出为dataURL(url(data:))的形式,就是之前的url- loader的功能 242 | 4. asset/source将资源导出为源码(source code)。之前的raw- loader功能 243 | ```javascript 244 | { 245 | test: /\.(png|svg|gif|jpe?g)$/, 246 | type: 'asset', 247 | generator: { 248 | // 配置输出图片路径和名称 249 | filename: 'img/[name].[hash:6].[ext]', 250 | }, 251 | parser: { 252 | dataUrlCondition: { 253 | maxSize: 10 * 1024 254 | } 255 | } 256 | }, 257 | { 258 | // 拷贝字体资源到font目录下 259 | test: /\.(ttf|woff2?)$/, 260 | type: 'asset/resource', 261 | generator: { 262 | filename: 'font/[name].[hash:3][ext]' 263 | } 264 | }, 265 | ``` 266 | ## 插件 267 | plugin 用来处理loader不能做的事,比loader更加丰富的功能,插件可以在webpack运行的任何生命周期插进去。在webpack执行流程中会广播很多事件,plugin 可以监听这些事件,在合适的时机通过 webpack 提供的 API 改变输出结果。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。 268 | ### 手写plugin 269 | 270 | - 一个 JavaScript 命名函数或 JavaScript 类。 271 | - 在插件函数的 prototype 上定义一个 apply 方法。 272 | - 指定一个绑定到 webpack 自身的[事件钩子](https://webpack.docschina.org/api/compiler-hooks/)。 273 | - 处理 webpack 内部实例的特定数据。 274 | - 功能完成后调用 webpack 提供的回调。 275 | ```javascript 276 | class MyPlugin { 277 | apply(compiler) { 278 | // compiler 这个变量贯穿webpack的整个生命周期 279 | // 代表着webpack运行的所有配置信息 280 | console.log('my plugin 启动'); 281 | console.log('compiler: ', compiler); 282 | 283 | compiler.hooks.emit.tap('MyPlugin', compilation => { 284 | // compilation 可以理解为此次打包的上下文 285 | console.log('compilation: ', compilation); 286 | }) 287 | } 288 | } 289 | ``` 290 | ## devtool 291 | 这项配置在项目中的作用主要体现在可以在浏览器上快速的定位代码位置。这个建议在开发的时候开启,在生产环境关闭,我个人建议开发环境使用cheap-module-eval-source-map 292 | 293 | | devtool | 构建速度 | 重新构建速度 | 生产环境 | 品质 | 294 | | --- | --- | --- | --- | --- | 295 | | none | +++ | +++ | yes | 打包后的代码 | 296 | | eval | +++ | +++ | no | 生成后的代码 | 297 | | cheap-eval-source-map | + | ++ | no | 转换过的代码(仅限行) | 298 | | cheap-module-eval-source-map | o | ++ | no | 原始源代码(仅限行) | 299 | | eval-source-map | -- | + | no | 原始源代码 | 300 | | cheap-source-map | + | o | no | 转换过的代码(仅限行) | 301 | | cheap-module-source-map | o | - | no | 原始源代码(仅限行) | 302 | | inline-cheap-source-map | + | o | no | 转换过的代码(仅限行) | 303 | | inline-cheap-module-source-map | o | - | no | 原始源代码(仅限行) | 304 | | source-map | -- | -- | yes | 原始源代码 | 305 | | inline-source-map | -- | -- | no | 原始源代码 | 306 | | hidden-source-map | -- | -- | yes | 原始源代码 | 307 | | nosources-source-map | -- | -- | yes | 无源代码内容 | 308 | 309 | > +++ 非常快速、++ 快速、+ 比较快、o 中等、 - 比较慢、-- 慢 310 | 311 | ## dev server 312 | ```javascript 313 | module.exports = { 314 | // webpack-dev-server 和 webpack-dev-middle 里 Watch 模式默认开启 315 | watch: true, 316 | 317 | devServer: { 318 | // 仅显示错误级别的输出,从而减少输出信息 319 | stats: 'error-only', 320 | hot: true, // 模块热替换 321 | 322 | // 从环境变量中传入 host 和 port,从而达到可配置 323 | // 324 | // 如果你是用 Docker、Vagrant 或者 Cloud9,那么把 325 | // host 设置为 "0.0.0.0" 326 | // 327 | // 0.0.0.0 对于所有的网络设备都是可用的 328 | // 而默认的 `localhost` 不行 329 | host: process.env.HOST, // 默认为 `localhost` 330 | port: process.env.PORT, // 默认为 8080 331 | open: true, // 在浏览器打开 332 | }, 333 | }; 334 | ``` 335 | ### 历史记录路由 336 | 配置项 historyApiFallback 用于方便地开发使用 HTML5 History API 的单页应用。 337 | 这类单页应用要求服务器在针对任何命中的路由时,都返回一个对应的 HTML 文件。 338 | 例如在访问 http://localhost/user 和 http://localhost/home 时都返回 index.html 文件,浏览器端的 JavaScript 代码会从 URL 里解析出当前页面的状态,显示对应的界面。 339 | ```javascript 340 | historyApiFallback: true; 341 | ``` 342 | 只能用于只有一个 HTML 文件的应用。 343 | 如果是多页应用。 344 | ```javascript 345 | historyApiFallback: { 346 | // 使用正则匹配命中路由 347 | rewrites: [ 348 | // /user 开头的都返回 user.html 349 | { from: /^\/user/, to: '/user.html' }, 350 | { from: /^\/game/, to: '/game.html' }, 351 | // 其他的都返回 index.html 352 | { from: /./, to: '/index.html' }, 353 | ]; 354 | } 355 | ``` 356 | ### 设置响应头 357 | 配置项 headers 用于配置 HTTP 相应中注入一些 HTTP 响应头。 358 | ```javascript 359 | headers: { 360 | 'X-Frame-Options': 'DENY' 361 | }, 362 | ``` 363 | # vite 364 | > 是一种新型前端构建工具,能够显著提升前端开发体验。它主要由两部分组成: 365 | > - 一个开发服务器,它基于 原生 ES 模块 提供了 丰富的内建功能,如速度快到惊人的 模块热更新(HMR)。 366 | > - 一套构建指令,它使用 Rollup 打包你的代码,并且它是预配置的,可输出用于生产环境的高度优化过的静态资源。 367 | 368 | ## 为什么会出现vite 369 | 现在的打包工具,在开发过程中的体验会随着项目体量的增加,体验逐渐下降。最明显的表现就是重新启动一个项目的时候和热更新速度越来越慢。使用工具的初衷就是为了降低开发者的成本和体验,很显然随着项目体量的增加会极大的降低工具在开发的作用。而vite的介绍新型前端构建工具,能够显著提升前端开发体验。 370 | ### webpack出现的问题 371 | ![bundle.png](https://cdn.nlark.com/yuque/0/2022/png/418769/1651828620312-842d41e6-eadc-4332-b2e4-c8cdd1479243.png#clientId=ufd6748b3-86ad-4&from=ui&height=263&id=u5bb73c58&name=bundle.png&originHeight=1068&originWidth=1918&originalType=binary&ratio=1&rotation=0&showTitle=false&size=30471&status=done&style=none&taskId=u82c4077b-128f-4f1d-9456-ab17cd7200c&title=&width=472) 372 | 现在的打包工具,在开发过程中启动的方式是先打包编译完成后,然后服务才会起起来,可以被浏览器访问对应的静态资源。在打包编译这个过程中速度显然会受到项目大小限制。在开发过程中的热更新也会受到影响,即使只有很小的改动,webpack依然需要构建完整的模块依赖图,并根据依赖图来进行转换。 373 | ### vite和webpack的对比 374 | | webpack | vite | 375 | | --- | --- | 376 | | 先打包生成bundle,再启动开发服务器 | 先启动开发服务器,利用新一代浏览器的esm能力,无需打包,直接请求所需模块并实时编译 | 377 | | hmr时需要把改动模块及相关依赖全部编译 | hmr时只需让浏览器重新请求该模块,同时利用浏览器的缓存(源码模块协商缓存,依赖模块强缓存)来优化请求 | 378 | 379 | vite利用了esm和浏览器缓存技术,更新速度与项目复杂度无关。当浏览器发出原生的esm请求,服务收到请求只需要编译当前文件后返回给浏览器,不需要管理依赖。 380 | ![vite-esm.png](https://cdn.nlark.com/yuque/0/2022/png/418769/1651851921253-3476ce5d-37bc-411a-be46-25288493476c.png#clientId=u52d88e54-a57f-4&from=ui&id=u738f7c18&name=vite-esm.png&originHeight=1030&originWidth=1646&originalType=binary&ratio=1&rotation=0&showTitle=false&size=73223&status=done&style=none&taskId=u5f252035-4aac-4728-bb99-c782ed4526d&title=) 381 | ## vite基本使用 382 | 以react脚手架为例 383 | ```shell 384 | # 创建模板 385 | yarn create vite 386 | # 下载依赖 387 | yarn 388 | # 开启服务 389 | yarn dev 390 | ``` 391 | ### 脚手架创建的文件目录 392 | ```markdown 393 | . 394 | ├── index.html 395 | ├── package.json 396 | ├── yarn.lock 397 | ├── src 398 | │ ├── App.css 399 | │ ├── App.tsx 400 | │ ├── favicon.svg 401 | │ ├── index.css 402 | │ ├── logo.svg 403 | │ ├── main.tsx 404 | │ └── vite-env.d.ts 405 | ├── tsconfig.json 406 | └── vite.config.ts 407 | ``` 408 | 根据官方文档 409 | > 在开发期间 Vite 是一个服务器,而 index.html 是该 Vite 项目的入口文件 410 | 411 | ![QQ20220507-010504.png](https://cdn.nlark.com/yuque/0/2022/png/418769/1651856726487-e4251724-b7c5-4fe1-acca-c0a61a49a385.png#clientId=ue8ab5749-71d2-4&from=ui&height=262&id=uc77c2d2d&name=QQ20220507-010504.png&originHeight=328&originWidth=763&originalType=binary&ratio=1&rotation=0&showTitle=false&size=112812&status=done&style=none&taskId=u8a74a8bd-7bc9-4618-b557-3683048c38b&title=&width=610) 412 | 新版本浏览器原生支持 esm 模块规范,因此原生的esm语法也可以直接放到浏览器中执行,只需要在 script 标签中声明 type="module" 即可。同时 src 指向了/src/main.tsx文件,此时相当于请求了http://localhost:3000/src/main.tsx这个资源,Vite 的 Dev Server 此时会接受到这个请求,然后读取对应的文件内容,进行一定的中间处理,最后将处理的结果返回给浏览器。 413 | ![QQ20220507-010758@2x.png](https://cdn.nlark.com/yuque/0/2022/png/418769/1651856888141-69a0914c-8356-4510-8105-6cea3aec3a98.png#clientId=ue8ab5749-71d2-4&from=ui&id=u57b6d745&name=QQ20220507-010758%402x.png&originHeight=366&originWidth=1680&originalType=binary&ratio=1&rotation=0&showTitle=false&size=122181&status=done&style=none&taskId=u906f7e31-4e61-4268-a789-fc48abdd8de&title=) 414 | 更改index.html的路径,配置一下root选项 415 | ```javascript 416 | export default defineConfig({ 417 | root: path.resolve(__dirname, 'src'), 418 | plugins: [react()] 419 | }) 420 | ``` 421 | ### 解决css样式冲突 422 | 导入文件方式index.module.css,.module.css自动开启css modules模式 423 | ```javascript 424 | export default defineConfig({ 425 | css: { 426 | modules: { 427 | // 通过 generateScopedName 属性来对生成的类名进行自定义 428 | // 其中,name 表示当前文件名,local 表示类名 429 | scopeBehaviour: 'local', 430 | generateScopedName: "[name]__[local]___[hash:base64:5]" 431 | }, 432 | // 进行 PostCSS 配置 433 | postcss: { 434 | plugins: [ 435 | autoprefixer({ 436 | // 指定目标浏览器 437 | overrideBrowserslist: ['Chrome > 40', 'ff > 31', 'ie 11'] 438 | }) 439 | ] 440 | } 441 | } 442 | }) 443 | 444 | ``` 445 | ### 打包 446 | ```shell 447 | yarn build 448 | 449 | # 查看打包产物 450 | yarn preview 451 | ``` 452 | ## 依赖预构建 453 | ### 依赖预构建是什么? 454 | 通常模块代码分为两部分,一部分是编写的业务代码,一部分是第三方依赖的代码,也就是node_modules中的代码。而vite不对代码进行打包针对的是业务代码,对于第三方依赖的代码还是进行了打包,但是对这些代码进行打包后,并不会向其它工具那样只是保存在内存中,而在在node_modules目录下会有个.vite目录,访问的依赖代码都是来自于这个目录。 455 | ![deps-pre-build.png](https://cdn.nlark.com/yuque/0/2022/png/418769/1651937427308-09a6bb72-8913-404e-bc46-1d992427a258.png#clientId=u5f248dc8-12df-4&from=ui&height=174&id=u4b392eee&name=deps-pre-build.png&originHeight=410&originWidth=1492&originalType=binary&ratio=1&rotation=0&showTitle=false&size=131676&status=done&style=none&taskId=u75eb4cda-1a48-4ddf-8a3c-88f4a37eb6b&title=&width=634) 456 | ### 为什么需要依赖预构建? 457 | #### commonjs和umd的兼容性 458 | vite是基于浏览器原生的esm规范实现的开发服务,不论是应用代码还是第三方依赖代码,都需要符合这个规范才能够正常运行。但是对于第三方依赖代码的打包规范,很多依赖都没有采用esm的方式,比如React: 459 | ```javascript 460 | // node_modules/react/package.json 461 | { 462 | "main": "index.js" 463 | } 464 | 465 | // node_modules/react/index.js 466 | 'use strict'; 467 | 468 | if (process.env.NODE_ENV === 'production') { 469 | module.exports = require('./cjs/react.production.min.js'); 470 | } else { 471 | module.exports = require('./cjs/react.development.js'); 472 | } 473 | ``` 474 | 这种代码在vite服务中无法直接运行,需要转换成esm格式的产物才可以运行 475 | #### 性能-请求瀑布流问题 476 | lodash-es这个采用的是esm打包方式,但是如果我不采用依赖预构建的方式,会出现什么问题? 477 | ![QQ20220507-234549.png](https://cdn.nlark.com/yuque/0/2022/png/418769/1651938362476-2426f457-9dde-491e-9740-44fe63af5931.png#clientId=u5f248dc8-12df-4&from=ui&id=u1176e664&name=QQ20220507-234549.png&originHeight=176&originWidth=479&originalType=binary&ratio=1&rotation=0&showTitle=false&size=25385&status=done&style=none&taskId=uda245d7c-c7da-4243-8d6d-aaa7a3d1d72&title=) 478 | 在vite配置中,我把依赖预构建lodash-es排除在外 479 | ```javascript 480 | export default defineConfig({ 481 | optimizeDeps: { 482 | exclude: ['lodash-es'] 483 | } 484 | }) 485 | ``` 486 | 确实可以直接进行加载,但是占用网络加载 487 | ![QQ20220507-234853@2x.png](https://cdn.nlark.com/yuque/0/2022/png/418769/1651938563642-0a423d26-9a06-4df6-bfd8-2edf23b17611.png#clientId=u5f248dc8-12df-4&from=ui&id=u18f63ff1&name=QQ20220507-234853%402x.png&originHeight=486&originWidth=1548&originalType=binary&ratio=1&rotation=0&showTitle=false&size=188838&status=done&style=none&taskId=u366ee5a3-c7c5-423a-9037-bf7f10f63de&title=) 488 | 每个import都会触发一次新的文件请求,因此这种依赖层级深、涉及模块数量多的情况下,会产生成百上千的请求,巨大的请求量和chrome对同一域名下http并发请求的限制,导致页面加载十分缓慢,这与vite设计理念不符(新型前端构建工具,能够显著提升前端开发体验)。但是在进行预构建之后,相关依赖会被打包成一个文件,这与请求量会大量减少。 489 | ![QQ20220508-000112@2x.png](https://cdn.nlark.com/yuque/0/2022/png/418769/1651939287635-f8a874bb-7b29-40de-8ccb-9d7fbe69b612.png#clientId=u5f248dc8-12df-4&from=ui&id=u4baa04dc&name=QQ20220508-000112%402x.png&originHeight=574&originWidth=2106&originalType=binary&ratio=1&rotation=0&showTitle=false&size=209046&status=done&style=none&taskId=ueb9ab29f-7a65-47f0-b29a-9ec3dd927e6&title=) 490 | 这两点全部由性能优异的esbuild完成,不是传统的由nodejs编写的工具,而是基于golang开发的,也不会有明显的打包性能问题。[https://esbuild.github.io/](https://esbuild.github.io/) 491 | ### 什么时候会进行预构建 492 | 在项目启动的时候,以这个react脚手架为例,会抓取index.html文件,将html文件作为入口,根据入口文件获得项目中用到的第三方依赖,对这些依赖进行编译。可以手动配置项目入口文件 493 | ```javascript 494 | // vite.config.ts 495 | { 496 | optimizeDeps: { 497 | // 为一个字符串数组 498 | entries: ["./src/main.tsx"]; 499 | } 500 | } 501 | ``` 502 | 对于这些依赖的请求结果,vite的开发服务会设置强制缓存: 503 | ![QQ20220508-001349@2x.png](https://cdn.nlark.com/yuque/0/2022/png/418769/1651940038631-d2227ac3-f6f9-41d8-ad52-54e5616e4ac2.png#clientId=u5f248dc8-12df-4&from=ui&height=348&id=u15c2aa4f&name=QQ20220508-001349%402x.png&originHeight=496&originWidth=796&originalType=binary&ratio=1&rotation=0&showTitle=false&size=93534&status=done&style=none&taskId=u044a8580-3bd9-4c46-90ae-e3649ae10bf&title=&width=558) 504 | 除了http缓存,vite设置了文件本地缓存,所有构建产物都在node_modules/.vite目录下。如果以下几个地方没有改动,vite会一直使用缓存文件: 505 | 506 | - package.json中的dependencies字段 507 | - yarn中的yarn.lock或npm中的package-lock.json等 508 | - vite.config.ts中的optimizeDeps配置内容 509 | ### include 510 | 该字段决定可以强制使用预构建的依赖项 511 | ```javascript 512 | optimizeDeps: { 513 | // 字符串数组,将lodash-es包强制进行预构建 514 | include: ["lodash-es"]; 515 | } 516 | ``` 517 | #### 应用场景 518 | vite会根据应用的入口自动搜集依赖,但是vite并不是能够100%准确的收集到依赖。这个时候需要配合该字段来达到完美的预构建效果 519 | #### 动态import 520 | 由于vite需要被加载的特性,经常会导致某些依赖只能在运行时才会被识别出来 521 | ```javascript 522 | // src/lang.ts 523 | import objectAssign from 'object-assign'; 524 | console.log(objectAssign); 525 | 526 | // main.tsx 527 | const objectAssign = (m) => import(`./${m}.ts`); 528 | objectAssign('lang') 529 | ``` 530 | 在这个例子中,动态 import 的路径只有运行时才能确定,无法在预构建阶段被扫描出来。因此,在访问项目时控制台会出现下面的日志信息 531 | ![QQ20220508-002820.png](https://cdn.nlark.com/yuque/0/2022/png/418769/1651940912164-514900b7-4fbe-4d3b-972d-11386ed04b1f.png#clientId=u5f248dc8-12df-4&from=ui&id=u1696f80e&name=QQ20220508-002820.png&originHeight=283&originWidth=2116&originalType=binary&ratio=1&rotation=0&showTitle=false&size=174039&status=done&style=none&taskId=u1fc8f068-c997-441c-b34d-f9a87f22b49&title=)这段日志的意思是vite运行发现了新的依赖,需要重新构建依赖,并刷新页面。这个过程也叫做二次预构。在项目体量较大的项目中,这样的过程会被执行很多次。二次预构建的成本也比较大。我们不仅需要把预构建的流程重新运行一遍,还得重新刷新页面,并且需要重新请求所有的模块。对于这种情况,可以在include字段中提前预构建遗漏的依赖添加上去,这样可以尽量避免这样的情况发生。 532 | #### 某些包被手动exclude 533 | exclude 是optimizeDeps中的另一个配置项,与include相对,用于将某些依赖从预构建的过程中排除。可能手动排除的包具有esm的格式,但是这个包依赖的某个包不具备esm格式,导致运行时加载失败,这个时候用include指定对应的包进行预构建就可以了 534 | ```javascript 535 | // old 536 | { 537 | optimizeDeps: { 538 | exclude: ["@loadable/component"]; 539 | } 540 | } 541 | 542 | // new 543 | { 544 | optimizeDeps: { 545 | include: [ 546 | // 间接依赖的声明语法,通过`>`分开, 如`a > b`表示 a 中依赖的 b 547 | "@loadable/component > hoist-non-react-statics", 548 | ]; 549 | } 550 | } 551 | ``` 552 | ## 插件 553 | 插件详细查看官方文档 [https://vitejs.cn/guide/using-plugins.html](https://vitejs.cn/guide/using-plugins.html) 554 | ```javascript 555 | import react from '@vitejs/plugin-react' // 提供完整的 React 支持 556 | 557 | { 558 | plugin: [react()] 559 | } 560 | ``` 561 | ### 插件钩子 562 | ```typescript 563 | export default function testHookPlugin () { 564 | return { 565 | name: 'test-hooks-plugin', 566 | 567 | /* 568 | * @desc vite独有钩子 569 | * @state 在解析 vite 配置前调用 570 | */ 571 | config(config) { 572 | console.log('config'); 573 | }, 574 | 575 | /* 576 | * @desc vite独有钩子 577 | * @state 在解析 vite 配置后调用。 578 | */ 579 | configResolved(resolvedCofnig) { 580 | console.log('configResolved'); 581 | }, 582 | 583 | /* 584 | * @desc 通用钩子 585 | * @state 服务器启动的时候调用 586 | */ 587 | options(opts) { 588 | console.log('options'); 589 | return opts; 590 | }, 591 | 592 | /* 593 | * @desc vite独有钩子 594 | * @state 钩子将在内部中间件被安装前调用,所以自定义的中间件将会默认会比内部中间件早运行 595 | */ 596 | configureServer(server) { 597 | console.log('configureServer'); 598 | setTimeout(() => { 599 | // 手动退出进程 600 | process.kill(process.pid, 'SIGTERM'); 601 | }, 3000) 602 | }, 603 | 604 | /* 605 | * @desc 通用钩子 606 | * @state 服务器启动的时候调用 607 | */ 608 | buildStart() { 609 | console.log('buildStart'); 610 | }, 611 | 612 | /* 613 | * @desc 通用钩子 614 | * @state 在服务器关闭时被调用 615 | */ 616 | buildEnd() { 617 | console.log('buildEnd'); 618 | }, 619 | 620 | /* 621 | * @desc 通用钩子 622 | * @state 在服务器关闭时被调用 623 | */ 624 | closeBundle() { 625 | console.log('closeBundle'); 626 | } 627 | } 628 | ``` 629 | 启动日志 630 | ```markdown 631 | config 632 | configResolved 633 | options 634 | configureServer 635 | buildStart 636 | 637 | vite v2.9.8 dev server running at: 638 | 639 | > Local: http://localhost:3000/ 640 | > Network: use `--host` to expose 641 | 642 | ready in 334ms. 643 | 644 | options 645 | buildEnd 646 | closeBundle 647 | ``` 648 | vite插件内部钩子执行顺序: 649 | ![QQ20220508-143150@2x.png](https://cdn.nlark.com/yuque/0/2022/png/418769/1651991520230-6e0f6ba3-a4b6-4aa8-8f27-6e3e95aa8684.png#clientId=uc925aef9-1fbc-4&from=ui&height=329&id=GEWQ7&name=QQ20220508-143150%402x.png&originHeight=986&originWidth=1728&originalType=binary&ratio=1&rotation=0&showTitle=false&size=105191&status=done&style=none&taskId=u6e4d819c-6b5e-4506-b729-385d6f4bced&title=&width=577) 650 | ### vite插件执行顺序 651 | 652 | 1. Alias (路径别名)相关的插件。 653 | 2. 带有 enforce: 'pre' 的用户插件。 654 | 3. Vite 核心插件。 655 | 4. 没有 enforce 值的用户插件,也叫普通插件。 656 | 5. Vite 生产环境构建用的插件。 657 | 6. 带有 enforce: 'post' 的用户插件。 658 | 7. Vite 后置构建插件(如压缩插件)。 659 | ### 如何编写一个vite插件 660 | ```javascript 661 | export default function myPlugin() { 662 | const virtualModuleId = '@my-virtual-module' 663 | // vite中约定对于虚拟模块,解析后的路径需要加上`\0`前缀 664 | const resolvedVirtualModuleId = '\0' + virtualModuleId 665 | 666 | return { 667 | name: 'my-plugin', // 必须的,将会在 warning 和 error 中显示 668 | resolveId(id) { 669 | if (id === virtualModuleId) { 670 | return resolvedVirtualModuleId 671 | } 672 | }, 673 | load(id) { 674 | if (id === resolvedVirtualModuleId) { 675 | return `export const msg = "from virtual module"` 676 | } 677 | } 678 | } 679 | } 680 | 681 | import { msg } from '@my-virtual-module' 682 | 683 | console.log(msg) 684 | ``` 685 | > 如果插件是一个 npm 包,在package.json中的包命名也推荐以vite-plugin开头 686 | 687 | ## vite原理介绍 688 | ![02910cd2c6894bcdb3a9e0fc9e59f4c2.webp](https://cdn.nlark.com/yuque/0/2022/webp/418769/1651995396126-b19acc57-373c-4dea-95d6-870d95ac875f.webp#clientId=u5f801fc3-5ce7-4&from=ui&id=SJcCf&name=02910cd2c6894bcdb3a9e0fc9e59f4c2.webp&originHeight=625&originWidth=1304&originalType=binary&ratio=1&rotation=0&showTitle=false&size=26810&status=done&style=none&taskId=u2ebd3c83-e58e-461b-8dfd-dda05c18a94&title=) 689 | > 参考神三元的vite小册 690 | 691 | 这一张图胜过千言万语。。。。。vite在开发环境主要以预构建和dev server为主。dev server做请求拦截,把相关的资源返回给客户端。生产环境主要采用的是rollup进行构建。 692 | ### ESBuild 693 | #### 依赖预构建 694 | 在开发阶段,vite采用esbuild完成依赖预构建,vite1.x版本采用的是rollup进行,而由于esbuild的性能,vite2.x版本采用的是esbuild。优点就不多说,来说说缺点 [https://vitejs.cn/guide/why.html#why-not-bundle-with-esbuild](https://vitejs.cn/guide/why.html#why-not-bundle-with-esbuild) 695 | 696 | 1. 不支持降级到 ES5 的代码。这意味着在低端浏览器代码会跑不起来。 697 | 2. 不支持 const enum 等语法。这意味着单独使用这些语法在 esbuild 中会直接抛错。 698 | 3. 不提供操作打包产物的接口,像 Rollup 中灵活处理打包产物的能力(如renderChunk钩子)在 esbuild 当中完全没有。 699 | 4. 不支持自定义 code splitting 策略。传统的 webpack 和 rollup 都提供了自定义拆包策略的 API,而 esbuild 并未提供,从而降级了拆包优化的灵活性。 700 | #### 单文件编译 701 | 在依赖预构建阶段, esbuild 作为 bundler 的角色存在。但是在 tsx/jsx单文件编译上面,vite 也使用 esbuild 进行语法转译,也就是将 esbuild 作为 transformer 来用。可以在架构图中Vite Plugin Pipeline部分中看到。 702 | ![image.png](https://cdn.nlark.com/yuque/0/2022/png/418769/1651996875676-2bce2475-376b-45f2-abad-b532e45bf011.png#clientId=u5f801fc3-5ce7-4&from=paste&height=216&id=NLI9w&name=image.png&originHeight=432&originWidth=1636&originalType=binary&ratio=1&rotation=0&showTitle=false&size=166777&status=done&style=none&taskId=u9b2a2c66-93b2-4aaa-8b77-683dce3f778&title=&width=818) 703 | 也就是说,esbuild 转译 ts 或者 jsx 的能力通过 vite 插件提供,这个 vite 插件在开发环境和生产环境都会执行。 704 | 这个转换主要是替换了babel和tsx,它们的速度实在是太慢了,来[对比](https://link.juejin.cn/?target=https%3A%2F%2Fdatastation.multiprocess.io%2Fblog%2F2021-11-13-benchmarking-esbuild-swc-typescript-babel.html)esbuild、babel、tsc 包括 [swc](https://github.com/swc-project/swc) 的编译性能: 705 | ![react-benchmark-small.webp](https://cdn.nlark.com/yuque/0/2022/webp/418769/1651997155400-a34fe0f6-f01c-4c5c-9ade-482e9d1aecc2.webp#clientId=u5f801fc3-5ce7-4&from=ui&id=Xmp0M&name=react-benchmark-small.webp&originHeight=1024&originWidth=2048&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12154&status=done&style=none&taskId=u8860c32b-b809-4a7c-9788-ba4a2ee00c8&title=) 706 | ![react-benchmark-medium.webp](https://cdn.nlark.com/yuque/0/2022/webp/418769/1651997128422-b3910d58-ae28-4efd-97d7-a1bbe303866b.webp#clientId=u5f801fc3-5ce7-4&from=ui&id=tOpJQ&name=react-benchmark-medium.webp&originHeight=1024&originWidth=2048&originalType=binary&ratio=1&rotation=0&showTitle=false&size=11960&status=done&style=none&taskId=u97015d92-b869-4020-bc72-e57b6a40531&title=) 707 | ![react-benchmark-large.webp](https://cdn.nlark.com/yuque/0/2022/webp/418769/1651997137513-4e8bc071-ff85-4024-b4d7-9603e1ea31ee.webp#clientId=u5f801fc3-5ce7-4&from=ui&id=H9Lra&name=react-benchmark-large.webp&originHeight=1024&originWidth=2048&originalType=binary&ratio=1&rotation=0&showTitle=false&size=12636&status=done&style=none&taskId=u8b56019f-662b-48dd-a007-8247bc7c217&title=) 708 | #### 代码压缩 709 | > Vite 从 2.6 版本开始,就官宣默认使用 Esbuild 来进行生产环境的代码压缩,包括 JS 代码和 CSS 代码。 710 | 711 | 传统的方式都是使用 terser 这种 js开发的压缩器来实现,在 webpack 或者 rollup 中作为一个插件来完成代码打包后的压缩混淆的工作。但 terser 其实很慢,主要有 2 个原因。 712 | 713 | 1. 压缩这项工作涉及大量 AST 操作,并且在传统的构建流程中,AST 在各个工具之间无法共享,比如 terser 就无法与 babel 共享同一个 AST,造成了很多重复解析的过程。 714 | 2. JS 本身属于解释性 + JIT(即时编译) 的语言,对于压缩这种 CPU 密集型的工作,其性能远远比不上 golang 这种原生语言。 715 | 716 | 因此,esbuild 这种从头到尾共享 AST 以及原生语言编写的 minifier 在性能上能够甩开传统工具的好几十倍。 717 | 举个例子,我们可以看下面这个实际大型库(antd)的压缩性能[测试项目](https://github.com/privatenumber/minification-benchmarks/blob/master/readme.md): 718 | ![QQ20220508-163346@2x.png](https://cdn.nlark.com/yuque/0/2022/png/418769/1651998836128-bbd065ca-a9d8-40d3-91da-1866ee4d582a.png#clientId=u5f801fc3-5ce7-4&from=ui&id=U10ZZ&name=QQ20220508-163346%402x.png&originHeight=942&originWidth=1906&originalType=binary&ratio=1&rotation=0&showTitle=false&size=214010&status=done&style=none&taskId=u02ad395f-e392-4d7b-9122-1f6ca84b0ea&title=) 719 | 压缩一个大小为6.69MB的库,terser 需要耗费9005ms,而 esbuild 仅仅需要593ms,压缩效率较 terser 提升10x+,并且产物的体积几乎没有劣化,因此 vite 果断将其内置为默认的压缩方案。 720 | ### 构建基石rollup 721 | #### 生产环境bundle 722 | 为什么不采用esbuild?介绍esbuild的时候已经说明过其缺点了。[https://vitejs.cn/guide/why.html#why-not-bundle-with-esbuild](https://vitejs.cn/guide/why.html#why-not-bundle-with-esbuild) 723 | Vite 默认选择在生产环境中利用 Rollup 打包,并基于 Rollup 本身成熟的打包能力进行扩展和优化,主要包含 3 个方面: 724 | 725 | 1. CSS 代码分割。如果某个异步模块中引入了一些 CSS 代码,vite 就会自动将这些 CSS 抽取出来生成单独的文件,提高线上产物的缓存复用率。 726 | 2. 自动预加载。vite 会自动为入口 chunk 的依赖自动生成预加载标签 ,如: 727 | ```html 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | ``` 736 | 这种适当预加载的做法会让浏览器提前下载好资源,优化页面性能。 737 | 738 | 3. 异步 Chunk 加载优化。在异步引入的 Chunk 中,通常会有一些公用的模块,如现有两个异步引入的 Chunk: A 和 B,而且两者有一个公共依赖 C,如下图: 739 | 740 | ![](https://cdn.nlark.com/yuque/0/2022/webp/418769/1651999115521-bac9e182-ab97-47b6-845e-e4cfd5dc3643.webp#clientId=u5f801fc3-5ce7-4&from=paste&id=l2Rav&originHeight=491&originWidth=1304&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u3ab5675c-4d86-40ab-9d13-16c368cff1f&title=) 741 | 一般情况下,Rollup 打包之后,会先请求 A,然后浏览器在加载 A 的过程中才决定请求和加载 C,但 vite 进行优化之后,请求 A 的同时会自动预加载 C,通过优化 Rollup 产物依赖加载方式节省了不必要的网络开销。 742 | #### 插件兼容机制 743 | 无论是开发阶段还是生产环境,vite 都根植于 rollup 的插件机制和生态。在开发阶段,vite 借鉴了 [WMR](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fpreactjs%2Fwmr) 的思路,自己实现了一个 Plugin Container,用来模拟 rollup 调度各个 vite 插件的执行逻辑,而 vite 的插件写法完全兼容 rollup,因此在生产环境中将所有的 vite 插件传入 rollup 也没有问题。但是rollup 插件却不一定能完全兼容 vite。目前有不少 rollup 插件可以直接复用到 vite 中,通过地址查看所有兼容 vite 的 rollup 插件: [vite-rollup-plugins.patak.dev/](https://link.juejin.cn/?target=https%3A%2F%2Fvite-rollup-plugins.patak.dev%2F) 。 744 | ### 请求拦截 745 | 启动一个 koa 服务器拦截由浏览器请求 esm的请求。通过请求的路径找到目录下对应的文件做一定的处理最终以 esm的格式返回给客户端。 746 | > [https://zhuanlan.zhihu.com/p/424842555](https://zhuanlan.zhihu.com/p/424842555) 747 | 748 | ![v2-e3852ed76ec402c75f06c8d6ec892.webp](https://cdn.nlark.com/yuque/0/2022/webp/418769/1651999756354-fa10074a-1229-4b5a-9c13-c224ee4bc0d1.webp#clientId=u5f801fc3-5ce7-4&from=ui&id=PmuTf&name=v2-e3852ed76ec402c75f06c8d6ec892.webp&originHeight=985&originWidth=1440&originalType=binary&ratio=1&rotation=0&showTitle=false&size=36654&status=done&style=none&taskId=ufcef7553-8f56-4e96-a4b5-4a01e3812a6&title=) 749 | -------------------------------------------------------------------------------- /Tool/模块化_模块标准.md: -------------------------------------------------------------------------------- 1 | # 模块化开发 2 | 3 | **模块化开发是当下最重要的前端开发范式,“模块化”只是思想** 4 | 5 | ## 内容概要 6 | 7 | - 模块化演变过程 8 | - 模块化规范 9 | 10 | ## 模块化的演进过程 11 | 12 | 早期前端技术标准根本没有预料到前端能有今天的这样的规模,设计上的遗留问题导致我们去实现前端模块化的时候会遇到很多问题,现在这些问题都被一些标准或者工具解决了,但是它们的一个演进过程是值得去了解的 13 | 14 | ### Stage 1 - 文件划分 15 | 16 | 约定每个文件就是一个独立的模块,使用模块就是将模块引入到页面当中,一个 script 标签就对应一个模块 17 | 18 | 问题: 19 | 20 | - 污染全局作用域 21 | - 命名冲突问题 22 | - 无法管理模块依赖关系 23 | 24 | 早期模块化完全依靠约定 25 | 26 | ### Stage 2 - 命名空间 27 | 28 | 每个模块只暴露一个全局对象,模块的所有内容都挂载到这个对象下面,减小了命名冲突的坑但是模块任然没有私有空间 29 | 30 | ### Stage 3 - IIFE 31 | 32 | 将模块所有的成员都放在一个函数提供的私有作用域当中,对于需要暴露给外部的成员可以挂载到全局对象的方式去实现,私有成员只能通过内部的成员通过闭包的方式去访问,这样确保了私有变量的安全,而且自执行函数的参数可以传入模块的依赖 33 | 34 | 早期在没有工具和规范的情况下对模块化的落地方式 35 | 36 | ## 模块化规范的出现 37 | 38 | ### commonjs 39 | 40 | 上面的方式以原始的模块系统为基础,通过约定的方式去实现模块化的代码组织,对于不同的开发者去实施会有细微的差别,为了统一不同的开发者和不同的项目之间的差异,就需要一个标准去规范模块化的实现方式 41 | 42 | 且对于模块加载的方式都是通过手动写 script 的方式去加载的,模块的加载并不受代码的控制,一旦时间久了,维护就变十分棘手 43 | 44 | 所以我们需要模块化规范+模块加载器 45 | 46 | CommonJS 规范是 node.js 中提出的标准 47 | 48 | - 一个文件就是一个模块 49 | - 每个模块都有单独的作用域 50 | - 通过 module.exports 导出成员 51 | - 通过 require 函数载入模块 52 | 53 | ```javascript 54 | // 比如说有个文件内容是这样的 55 | console.log(__dirname) 56 | 57 | // 执行的是后会变成类似下面这样的形式,这个是 node 加上的 58 | function( 59 | __filename, 60 | __dirname, 61 | timer, // setTimeout之类的函数 62 | process, 63 | require, 64 | module, 65 | exports 66 | ) { 67 | console.log(__dirname) 68 | } 69 | ``` 70 | 71 | 注意点:exports 只是 module.exports 的别名,不能重新赋值,只能以 `exports.xxx = (你要赋的值)`, 如果是 `exports = {}` ,那这个 exports 可能就没用啦 72 | 73 | ### AMD 74 | 75 | 如果在浏览器端也使用这个 CommonJS 规范的话就会出现一些问题,CommonJS 是以同步模式加载模块,因为 node 的执行机制是在启动时去加载模块,执行过程中是不需要去加载的,它只会去使用到模块,这个模式在 node 中是没有问题的,但在浏览器端就不太合适了,因为每个页面的加载都会导致大量的同步请求出现,所以在早期的模块化当中,并没有选择 CommonJS 这个规范,而是专门为浏览器端,结合浏览器的特点,重新设计了一个规范,叫 AMD (Asynchronous Module Definition),同时还有 Require.js 的出现,它实现了 AMD 这个规范,它本身也是强大的模块加载器 76 | 77 | AMD规范采用**异步方式加载模块**,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。这里介绍用require.js实现AMD规范的模块化:用`require.config()`指定引用路径等,用`definde()`定义模块,用`require()`加载模块。 78 | 79 | ```javascript 80 | // 定义一个模块 81 | define( 82 | "module1", // 模块的名字 83 | ["jquery", "./module2"], // 声明模块的依赖项 84 | function ($, module2) { // 形参与依赖项一一对应,这个函数就是为当前模块提供一个私有空间,如果需要向外部去导出一些成员的话 return 就行了 85 | return { 86 | start: function () { 87 | $("body").animate({ margin: "200px" }); 88 | module2(); 89 | }, 90 | }; 91 | }); 92 | // 载入一个模块,当需要加载一个模块的时候,它内部会自动的去创建一个 script 标签去发送对应的脚本文件的请求,并且去执行相应的模块的代码 93 | require(['./modules/module1'], function (module1) { 94 | module1.start() 95 | }) 96 | ``` 97 | 98 | 目前绝大多数第三方库都支持 AMD 规范,但也有别的问题 99 | 100 | - AMD 使用起来相对复杂 101 | - 模块分得过于细致的话,模块 JS 文件请求频繁 102 | 103 | ### CMD 104 | 105 | 除此之外,还有淘宝推出的一个 Sea.js 的库,它实现的是 CMD(Common Module Definition)的标准,有点类似 CommonJS,在使用上也和 Require.js 差不多,为的就是 CMD 写出来的代码尽量和 CommonJS 类似,从而减轻开发者的学习成本,后来这种方式也被 Require.js 所兼容了 106 | 107 | Sea.js 在全局暴露了一个 define 方法,你写的代码调这个 define,define 主要接受一个标识(路径)和一个函数(其实还有依赖),define 会先把函数先 `toString()` 去用正则分析它的 `require` 依赖,然后继续加载,只有函数里的 require 的依赖都加载完了才会去执行你写的模块的内容,整个过程就是递归,先从主入口进,到最后所有依赖都执行了,再往回执行 108 | 109 | [sea.js](https://seajs.github.io/seajs/docs/#intro) 官方文档,sea.js 总共就不到一千行源代码,想看源码可以根据官方的例子 debug 看整个加载流程,虽然现在已经不维护了,不过看了 sea.js 源码就能更加深刻地理解这些规范之间的差异 110 | 111 | 同样都是异步加载模块,AMD在加载模块完成后就会执行该模块,所有模块都加载执行完后会进入require的回调函数,执行主逻辑,这样的效果就是依赖模块的执行顺序和书写顺序不一定一致,看网络速度,哪个先下载下来,哪个先执行,但是主逻辑一定在所有依赖加载完成后才执行 112 | 113 | CMD加载完某个依赖模块后并不执行,只是下载而已,在所有依赖模块加载完成后进入主逻辑,遇到require语句的时候才执行对应的模块,这样模块的执行顺序和书写顺序是完全一致的 114 | 115 | AMD 和 CMD 都只能算是前端模块化演进道路上的一步,它是一种妥协的实现方式,并不能算是最终的解决方案,不过在当时的环境背景下,它还是很有意义的,它毕竟给了前端模块化提了一个标准 116 | 117 | ### umd 118 | 119 | 大概就是兼容多种标准规范的写法 120 | 121 | ```javascript 122 | (function(root, factory) { 123 | if (typeof module === 'object' && typeof module.exports === 'object') { 124 | console.log('是commonjs模块规范,nodejs环境') 125 | var depModule = require('./umd-module-depended') 126 | module.exports = factory(depModule); 127 | } else if (typeof define === 'function' && define.amd) { 128 | console.log('是AMD模块规范,如require.js') 129 | define(['depModule'], factory) 130 | } else if (typeof define === 'function' && define.cmd) { 131 | console.log('是CMD模块规范,如sea.js') 132 | define(function(require, exports, module) { 133 | var depModule = require('depModule') 134 | module.exports = factory(depModule) 135 | }) 136 | } else { 137 | console.log('没有模块环境,直接挂载在全局对象上') 138 | root.umdModule = factory(root.depModule); 139 | } 140 | }(this, function(depModule) { 141 | console.log('我调用了依赖模块', depModule) 142 | if (typeof window !== 'undefined') { 143 | // 判断一下,因为nodejs没有dom的 144 | if (document.querySelector('#content2')) { 145 | document.querySelector('#content2').innerText = '我调用了依赖模块:' + depModule.name 146 | } else { 147 | window.addEventListener('load', function() { 148 | document.querySelector('#content2').innerText = '我调用了依赖模块:' + depModule.name 149 | }) 150 | } 151 | } 152 | return { 153 | name: '我自己是一个umd模块' 154 | } 155 | })) 156 | ``` 157 | 158 | ## 模块化标准规范 159 | 160 | 而现在 JavaScript 的标准也越来越完善了,现如今的前端模块化已经非常成熟了,而且目前大家针对前端模块化的最佳实践方式也都基本统一了 161 | 162 | 在 node.js 环境下遵循 CommonJS 规范去组织模块,在浏览器环境当中我们会采用 ES Module 的规范 163 | 164 | 在 node.js 内置 CommonJS 的模块系统,没有什么环境问题,ES Module 就不一样了,它是在 ECMAScript 2015 中定义的最新的模块系统,所有会有环境兼容问题,随着 webpack 一系列打包工具的流行,这一规范在逐渐开始普及,现在 ES Module 已经是最主流的前端模块化方案了 165 | 166 | 相比于 AMD 这种社区提出的开发规范,ES Module 可以说是在语言层面实现了模块化,现如今大多数浏览器也已经支持 ES Module 的特性了,原生支持,这样以后我们就可以直接用 ES Module 去开发我们的网页应用了,短期内也不会再有针对模块化的一个轮子或者标准出现了,所以在不同的环境如何使用好 ES Module 就成了重点 167 | 168 | ## ES Module 特性 169 | 170 | 在 HTML 中通过给 script 标签添加 `type="module"` 的属性,就可以以 ES Module 的标准执行其中的 JS 代码了 171 | 172 | 1. ESM 自动采用严格模式,忽略 `'use strict'` 173 | 2. 每个 ES Module 都是运行在单独的私有作用域中 174 | 3. ESM 是通过 CORS 的方式请求外部 JS 模块的,所以也不支持文件形式去访问,所以要用 http 的方式去让页面工作起来 175 | 4. ESM 的 script 标签会自动延迟执行脚本,相当于加了 defer 属性,网页对默认的 script 标签采用的是立即执行的机制,页面的渲染会等待这个脚本执行完成才会往下渲染 176 | 1. 普通 script 的 download 是会阻塞别的线程的 177 | 2. async 的 download 不阻塞,但是下载完就是马上执行,和 **DOMContentLoaded** 不一定有关系 178 | 3. defer 的 download 不但不阻塞,而且是在 html parse 之后执行,在 **DOMContentLoaded** 之前 179 | 180 | ## ES Module 导出和导入 181 | 182 | ESM 里 import 导入,export 是导出 183 | 184 | ```javascript 185 | // module.js 186 | var name = 'foo module' 187 | export { 188 | name as default 189 | } 190 | 191 | // app.js 192 | import { default as name } from './module.js' // 这里必须重命名 default,因为 default 是关键字 193 | // 当然写成下面的这样也是可以的 194 | import name from './module.js' 195 | ``` 196 | 197 | 注意事项 198 | 199 | `export {}` 的这个 `{}` 不完全等同于 `js` 中对象的概念,这里的括号是 ESM 语法 200 | 201 | ```javascript 202 | // module.js 203 | var name = 'jack' 204 | var age = 18 205 | // export { name, age } // 这里不是导出的字面量声明的对象,这里是语法,必须使用花括弧 206 | export default { name, age } // 这里导出的才是字面量声明的对象,这个不一定是对象,只要是一个值就行 207 | 208 | // app.js 209 | import { name, age } from './module.js' // 如果上面是 export default,那这里的 import 是要报错的,import 后面的跟的这个花括弧也不是解构,就只是语法 210 | ``` 211 | 212 | es module export 导出的都是引用,node 中导出的是值的浅拷贝 213 | 214 | export 的是引用,即使声明的不是常量,但是 import 的值是只读的 215 | 216 | ```javascript 217 | // module.js 218 | var name = 'jack' 219 | var age = 18 220 | export { name, age } 221 | 222 | // app.js 223 | import { name, age } from './module.js' 224 | name = 'tom' // 报 TypeError,import 的 name 是常量,即使在 module 中声明不是常量 225 | 226 | // 下面这种情况是可以的 227 | // module.js 228 | var a = { name: 'jack' } 229 | export default a 230 | 231 | // app.js 232 | import a from './module.js' 233 | a.name = 'xixi' // 这样是可以的 234 | ``` 235 | 236 | 原生的 `import xxx from './xxx.js'` 这里的 `.js` 是不能省略的,哪怕是 `index.js` 都是不能省略的,如果是在 webpack 下打包是可以的。 237 | 238 | ```javascript 239 | // 1 240 | import { name } from 'module.js' // 这个相对路径的要以 `.` 开头,不然的话会被当成第三方模块 241 | import { name } from '/src/module.js' // 也可以用绝对路径,就是从项目的根目录下去算路径 242 | import { name } from 'http://localhost:8080/src/module.js' // 也可以用完整的 url 去加载模块,这样我们也可以直接去引用 cdn 上的资源 243 | 244 | // 2 245 | import {} from './module.js' // 只执行模块,但是不需要提取模块的成员的话 246 | import './module.js' // 这是上一个的简写 247 | 248 | // 3 249 | import * as mod from './module.js' // 如果导出和使用的对象特别多 250 | 251 | // 4 252 | var modulePath = './module.js' 253 | import { name } from modulePath 254 | if (true) { 255 | import { name } from './module.js' 256 | } 257 | // 上面这个变量和条件判断的导入方法都是不可行的(原生) 258 | // 需要用到根据逻辑去导入的时候就可以用 import 函数,函数返回 promise 259 | // 模块加载结束之后,会自动去执行 then 里的函数 260 | import('./module.js').then((module) => console.log(module)) 261 | 262 | // 5 263 | import { default as title, name } from './module.js' // 当有默认成员和具名成员都要提取的时候,可以这样写 264 | import title/*这个title可以命名成任何非关键字*/, { name } from './module.js' // 简写 265 | ``` 266 | 267 | 导入导出结合 268 | 269 | ```javascript 270 | export { foo, bar } from './module.js' // 导出了刚引入的成员,但是这些成员在这个模块内也是不能使用了 271 | 272 | export { default as foo1 } from './module.js' 273 | ``` 274 | 275 | 这样的我们也经常在文件夹下写一个 `index.js` 去集中导出文件夹下的所有其他模块,但是如果不是所有的组件都要每次打包都要用的话,而且打包也没有去处理副作用的话,这种集中导出的方式可能就不太合适了 276 | -------------------------------------------------------------------------------- /TypeScript/Typescript基础.md: -------------------------------------------------------------------------------- 1 | # 一. Typescript 概念 2 | - TypeScript 是添加了类型系统的 JavaScript(超集),适用于任何规模的项目 3 | - TypeScript 是一门静态类型、弱类型的语言 4 | - TypeScript 是完全兼容 JavaScript 的,它不会修改 JavaScript 运行时的特性 5 | - TypeScript 拥有很多编译选项,类型检查的严格程度由你决定 6 | - TypeScript 可以和 JavaScript 共存,这意味着 JavaScript 项目能够渐进式的迁移到 TypeScript 7 | - TypeScript 增强了编辑器(IDE)的功能,提供了代码补全、接口提示、跳转到定义、代码重构等能力 8 | - TypeScript 拥有活跃的社区,大多数常用的第三方库都提供了类型声明 9 | - TypeScript 与标准同步发展,符合最新的 ECMAScript 标准(stage 3) 10 | # 二. 安装 Typescript 11 | 在开发环境下安装 tsc 命令 12 | ```javascript 13 | npm install typescript --dev/yarn add typescript --dev 14 | //npm或yarn安装 15 | ``` 16 | 编译一个 TypeScript 文件 17 | ```powershell 18 | yarn tsc xxx.ts 19 | ``` 20 | 我们约定使用 TypeScript 编写的文件以 .ts 为后缀,用 TypeScript 编写 React 时,以 .tsx 为后缀。 21 | 22 | # 三. tsconfig 23 | 创建 tsconfig.json 文件 24 | ```powershell 25 | .\node_modules\.bin\tsc --init / yarn tsc --init 26 | ``` 27 | 常用的 tsconfig 选项以及注解 28 | ```json 29 | { 30 | "compilerOptions": { 31 | "target": "ESNext", // 编译的目标是什么版本的 32 | "lib": [ // 编译过程中需要引入的库文件的列表 33 | "es5", 34 | "es2015", 35 | "es2016", 36 | "es2017", 37 | "es2018", 38 | "dom" 39 | ], 40 | "sourceMap": true, // 是否生成 map 文件 41 | "strict": true, //是否开启严格模式 42 | 43 | "allowUnreachableCode": true, // 不报告执行不到的代码错误。 44 | "allowUnusedLabels": false, // 不报告未使用的标签错误 45 | "alwaysStrict": false, // 以严格模式解析并为每个源文件生成 "use strict"语句 46 | "baseUrl": ".", // 工作根目录 47 | "experimentalDecorators": true, // 启用实验性的 ES 装饰器 48 | "jsx": "react", // 在 .tsx 文件里支持 JSX 49 | "module": "commonjs", // 指定生成哪个模块系统代码 50 | "noImplicitAny": false, // 是否默认禁用 any 51 | "removeComments": true, // 是否移除注释 52 | "types": [ //指定引入的类型声明文件,默认是自动引入所有声明文件,一旦指定该选项,则会禁用自动引入,改为只引入指定的类型声明文件,如果指定空数组[]则不引用任何文件 53 | "node", // 引入 node 的类型声明 54 | ], 55 | "paths": { // 指定模块的路径,和 baseUrl 有关联,和 webpack 中 resolve.alias 配置一样 56 | "src": [ //指定后可以在文件之直接 import * from 'src'; 57 | "./src" 58 | ], 59 | }, 60 | "outDir": "dist", // 输出目录 61 | "rootDir": "src", // 文件目录 62 | "declaration": true, // 是否自动创建类型声明文件 63 | "declarationDir": "./lib", // 类型声明文件的输出目录 64 | "allowJs": true, // 允许编译 javascript 文件。 65 | }, 66 | // 指定一个匹配列表(属于自动指定该路径下的所有 ts 相关文件) 67 | "include": [ 68 | "src/**/*" 69 | ], 70 | // 指定一个排除列表(include 的反向操作) 71 | "exclude": [ 72 | "demo.ts" 73 | ], 74 | // 指定哪些文件使用该配置(属于手动一个个指定文件) 75 | "files": [ 76 | "demo.ts" 77 | ] 78 | } 79 | ``` 80 | # 四. Typescript 基础类型 81 | ### 1.原始数据类型(Primitive data types) 82 | 83 | 原始数据类型包括:布尔值、数值、字符串、null、undefined 以及 ES6 中的新类型 Symbol 和 ES2020 中的新类型 BigInt 84 | ```javascript 85 | const a:string='123' 86 | const b:number=123 87 | const c:boolean=true; 88 | //const d:boolean=null; 89 | const e:void=undefined; 90 | const f:null=null; 91 | const g:undefined=undefined; 92 | const h:symbol=Symbol(); 93 | const i:bigint=BigInt(9007199254740992); 94 | ``` 95 | ### 2.Object 类型(Object types) 96 | 97 | 在 Typescript 中的 object 并不单指普通的对象类型,是泛指所有非原始数据类型,也就是对象、数组、函数 98 | ```javascript 99 | const fun1:object={}; 100 | const fun2:object=[]; 101 | const fun3:object=function(){}; 102 | //这里的 object 是全小写 103 | ``` 104 | 如果需要声明一个普通的对象类型,就需要使用类似于对象字面量的语法 105 | ```javascript 106 | const obj:{attr1:number,attr2:string}={age:18,name:'lgl'} 107 | ``` 108 | 但是在 typescript 中限制对象类型一般会使用接口,这里就不做过多的介绍了 109 | ### 3.Array 类型(Array types) 110 | 111 | 在 Typescript 中,数组类型有多种定义方式,比较灵活。常见如下两种 112 | ```javascript 113 | const arr1: number[] = [1, 2, 3] 114 | //类型 + 方括号 115 | const arr2: Array = [1,2,3] 116 | //数组泛型 117 | ``` 118 | ### 4.元组类型(Tuple types) 119 | 120 | 元组(Tuple)是合并不同类型的对象,数组是合并了相同类型的对象 121 | ```javascript 122 | const tuple:[number,string]=[18,'lgl'] 123 | const age=tuple[0]; 124 | const name=tuple[1]; 125 | //用数组下标的方式获取元组中每一个元素的值 126 | const [age1,name1]=tuple 127 | //数组结构 128 | Object.entries({ 129 | age:18, 130 | name:'lgl' 131 | }) 132 | //Object.entries()方法返回一个给定对象自身可枚举属性的键值对数组 133 | //其排列与使用 for...in 循环遍历该对象时返回的顺序一致 134 | //固定长度的元组 135 | ``` 136 | ### 5.枚举类型(Enum Types) 137 | 138 | 枚举(Enum)类型用于取值被限定在一定范围内的场景,比如发布文章状态分为草稿、未发布、已发布。 139 | ```javascript 140 | const article={ 141 | title:'123', 142 | content:'typescript', 143 | status:0 //0 草稿 1 未发布 2已发布 144 | } 145 | ``` 146 | 如果我们直接用0、1、2这种方式表示状态的话,时间久了就有可能出现状态混淆、未知状态等问题。 147 | 148 | 在很多编程语言中都有枚举这种数据结构,但是在JavaScript中没有,我们一般使用对象模拟实现枚举 149 | ```javascript 150 | const PostStatus1={ 151 | Draft:0, 152 | Unpubulished:1, 153 | Published:2 154 | } 155 | ``` 156 | 在 Typescript中提供了enum关键字。使用enum的优势在于一个枚举类型只会存在几个固定的值,不会存在超出范围的可能性。 157 | ```javascript 158 | enum PostStatus { 159 | Draft=0, 160 | Unpubulished=1, 161 | Published=2 162 | //这里是= 不是: 163 | } 164 | ``` 165 | 枚举类型的值可以不指定,默认是从0开始累加。如果指定了第一个值,后面所有的值都会在其基础上累加。 166 | 167 | 枚举类型的值可以是字符串,但是字符串无法像数字累加,就只能指定对应的默认值 168 | ```javascript 169 | enum PostStatus { 170 | Draft='aa', 171 | Unpubulished='bb', 172 | Published='cc' 173 | } 174 | //PostStatus[0]=>Draft 175 | ``` 176 | tips:枚举类型会影响编译后的结果。在typescript中大多数类型在编译转换后会被移除,因为其目的是为了做类型检查,但是枚举类型不会,他会被编译为一个双向键值对对象 177 | ```javascript 178 | var PostStatus1; 179 | (function (PostStatus1) { 180 | PostStatus1[PostStatus1["Draft"] = 0] = "Draft"; 181 | PostStatus1[PostStatus1["Unpubulished"] = 1] = "Unpubulished"; 182 | PostStatus1[PostStatus1["Published"] = 2] = "Published"; 183 | })(PostStatus1 || (PostStatus1 = {})); 184 | //编译后 185 | ``` 186 | 当不使用索引器的方式访问枚举的时候可以使用常量枚举,常量枚举与普通枚举的区别是,它会在编译阶段被删除,并且不能包含计算成员。 187 | ```javascript 188 | const enum PostStatus { 189 | Draft, 190 | Unpubulished, 191 | Published 192 | } 193 | //编译前 194 | var PostStatus2; 195 | (function (PostStatus2) { 196 | PostStatus2["Draft"] = "aa"; 197 | PostStatus2["Unpubulished"] = "bb"; 198 | PostStatus2["Published"] = "cc"; 199 | })(PostStatus2 || (PostStatus2 = {})); 200 | //编译后 201 | ``` 202 | ### 6.函数类型(Function Types) 203 | 204 | 一个函数有输入和输出,要在 TypeScript 中对其进行约束,需要把输入和输出都考虑到,其中函数声明的类型定义比较简单 205 | ```javascript 206 | function sum(x: number, y: number): number { 207 | return x + y; 208 | } 209 | 210 | console.log(sum(1, 2)) 211 | ``` 212 | 输入多余的(或者少于要求的)参数,是不被允许的。如果需要定义可选参数,我们可以用?表示可选的参数。可选参数必须接在必需参数后面 213 | ```javascript 214 | function sum1(x: number, y?: number): number { 215 | return x; 216 | } 217 | ``` 218 | 在ES6中,我们允许给函数的参数添加默认值,TypeScript 会将添加了默认值的参数识别为可选参数,此时就不受可选参数必须接在必需参数后面的限制了。 219 | ```javascript 220 | function sum2(x: number=10, y: number): number { 221 | return x + y; 222 | } 223 | 224 | console.log(sum2(20,10)) 225 | ``` 226 | 如果需要接收任意数量的参数,可以使用 es6的 ...rest 的方式获取函数中的剩余参数(rest 参数) 227 | ```javascript 228 | function sum3(x: number, y: number = 10, ...rest: number[]): number { 229 | let total = 0; 230 | for (var i of rest) { 231 | total += i; 232 | } 233 | return x + y + total; 234 | } 235 | 236 | console.log(sum3(20,10,1,2,3,4,5)) 237 | ``` 238 | 如果要我们现在写一个对函数表达式的定义,可能会写成这样: 239 | ```javascript 240 | const mySum = function (x: number, y: number): number { 241 | return x + y; 242 | }; 243 | ``` 244 | 这是可以通过编译的,不过事实上,上面的代码只对等号右侧的匿名函数进行了类型定义,而等号左边的 mySum,是通过赋值操作进行类型推论而推断出来的。如果需要我们手动给 mySum 添加类型,则应该是这样: 245 | ```javascript 246 | const mySum: (x: number, y: number) => number = function (x: number, y: number): number { 247 | return x + y; 248 | }; 249 | ``` 250 | tips:在 TypeScript 的类型定义中,=> 用来表示函数的定义,左边是输入类型,需要用括号括起来,右边是输出类型。 251 | ### 7.任意类型(Any Types) 252 | 253 | 任意值(Any)用来表示允许赋值为任意类型 254 | ```javascript 255 | function stringify(value:any){ 256 | return JSON.stringify(value) 257 | } 258 | stringify('123') 259 | stringify(123) 260 | stringify(true) 261 | ``` 262 | 可以认为,声明一个变量为任意值之后,对它的任何操作,返回的内容的类型都是任意值。 263 | -------------------------------------------------------------------------------- /TypeScript/index.md: -------------------------------------------------------------------------------- 1 | # TypeScript 2 | 3 | |目录 | 内容描述 4 | |- | -:| 5 | Typescript基础 | [使用技巧](./Typescript基础.md) 6 | 范型 Generics | [使用技巧](./范型 Generics.md) 7 | 实用类型 Utility type | [使用技巧](./实用类型 Utility type.md) -------------------------------------------------------------------------------- /TypeScript/实用类型 Utility type.md: -------------------------------------------------------------------------------- 1 | ## 什么是 Utility Types 2 | TS 内置的 [实用类型](https://www.typescriptlang.org/docs/handbook/utility-types.html),用于类型转换 3 | 把它理解透彻将会对你的 TS 水平有很大提升 4 | 本文将从实现、用法和场景三个方面对每个内置 Utility Type 进行说明 5 | ## 内置 Utility Types 6 | ### Partial 7 | _将传入的 T 类型所有属性置为可选_ 8 | 9 | - 源码 10 | ```typescript 11 | /** 12 | * Make all properties in T optional 13 | */ 14 | type Partial = { 15 | [P in keyof T]?: T[P] 16 | } 17 | ``` 18 | 19 | - 源码解析 20 | 21 | Partial 仅接收一个泛型参数 T, 22 | keyof 理解起来较为简单,索引类型查询操作符,就是将一个 索引类型 的 key 提取为联合类型,如 23 | ```typescript 24 | interface Dogs { 25 | dogName: string 26 | dogAge: number 27 | dogKind: string 28 | } 29 | type DogsKey = keyof Dogs // 等同于 type DogsKey = "dogName" | "dogAge" | "dogKind" 30 | ``` 31 | in 关键字是理解这段源码的关键,TS 的官方文档中,给出了[定义](https://yzl.xyz/lin/2021/05/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BATS%E7%9A%84Utility-Types/typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#key-remapping-in-mapped-types):key remapping in mapped types,也就是[映射类型](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) 32 | 它的语法往往是如下形式: 33 | ```typescript 34 | // OldType 为一个联合类型 35 | type NewType = { [K in OldType]: NewResultType } 36 | ``` 37 | [![](https://cdn.nlark.com/yuque/0/2022/jpeg/26213304/1649573753525-ad30a23d-ce3a-426d-b8c1-03865101cbc2.jpeg#clientId=u65a1ab80-3fb1-4&from=paste&id=u2fe8e640&originHeight=148&originWidth=914&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u5d43cf7a-c89d-4b44-baff-efbc585d7dc&title=)](https://yzl.xyz/lin/2021/05/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BATS%E7%9A%84Utility-Types/0b5a4c408b0d/key-mapping-type-example.jpg) 38 | 它大致包含 5 个部分 39 | 1.红色区域:用于承载它的类型别名 40 | 2.白色区域:类型别名 K (或者其他别名),它会被依次绑定到联合类型的每个属性 41 | 3.蓝色区域:in 关键字 42 | 4.橙色区域:由 number、symbol 或 string 的字面量组成的 联合类型,它包含了要迭代的属性名的集合,也可能直接是 number、symbol 或 string 三种类型,当然这种写法与 { [key: string]: ResultType } 的写法相同 43 | 5.粉色区域:属性的结果类型 44 | TS 4.1 以上可以在橙色区域后使用 as 操作符重新映射映射类型中的键,它的作用目标是白色区域的键;除了这 5 个部分,下文中还会提到属性修饰符 readonly 和 ? 45 | 假如在上述代码中,OldType 为 type OldType = "key1" | "key2",那么 NewType 等同于 46 | ```typescript 47 | type NewType = { 48 | key1: NewResultType 49 | key2: NewResultType 50 | } 51 | ``` 52 | 你可以在 TS 官网中看到类似的例子。 53 | 在索引类型中,这样的写法([属性修饰符](https://www.typescriptlang.org/docs/handbook/2/objects.html#property-modifiers):?)是不行的 54 | ```typescript 55 | type IndexType = { 56 | [key: string]?: string // 错误的写法 57 | } 58 | ``` 59 | 但在映射类型中,? 的写法是可以的 60 | ```typescript 61 | type MappingType = { 62 | [key in OldType]?: NewResultType // 正确的写法 63 | } 64 | ``` 65 | 上面的代码会得到一个这样的类型 66 | ```typescript 67 | type NewType = { 68 | key1?: NewResultType | undefined 69 | key2?: NewResultType | undefined 70 | } 71 | ``` 72 | 对于属性的结果类型,源码中是这样处理的:T[P],也就是[索引访问](https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html) 73 | 索引访问 能通过 索引 访问到其对应的具体类型,举例: 74 | ```typescript 75 | interface Dogs { 76 | dogName: string 77 | dogAge: number 78 | dogKind: string 79 | } 80 | 81 | type DogName = Dogs["dogName"] // 得到 string 类型 82 | ``` 83 | 如果字符串 "dogName" 代表一个[字面量类型](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types),那么下面的这种写法就与 T[P] 是相似的 84 | ```typescript 85 | type DogNameKey = "dogName" 86 | type DogName = Dogs[DogNameKey] 87 | ``` 88 | 对于源码的 [P in keyof T] 部分中的 P,在 in 操作符的作用下会是联合类型中的某一个具体的字面量类型 89 | 而 T 是原始的(被传入的)索引类型,T[P] 也就访问到了 P 索引对应的具体的类型了 90 | 91 | - 使用场景举例 92 | 1. 对象的扩展运算符,比如我们实现基于 useReducer 实现一个简单的 setState 93 | ```typescript 94 | type State = { 95 | loading: boolean 96 | list: Array 97 | page: number 98 | } 99 | const [state, setState] = useReducer( 100 | (state: State, nextState: Partial) => { 101 | return { ...state, ...nextState } 102 | }, 103 | { 104 | loading: false, 105 | list: [], 106 | page: 0, 107 | } 108 | ) 109 | // 使用 110 | setState({ page: 1 }) 111 | ``` 112 | 上面的代码中 nextState 被传入后,会与原 state 做合并操作,nextState 并不需要含有 State 类型的所有键,故使用 Partial 进行类型的定义 113 | 114 | 1. 都是非必传参但使用参数时如果没有传则会初始化参数 115 | ```typescript 116 | interface Params { 117 | param1: string 118 | param2: number 119 | param3: Array 120 | } 121 | function testFunction(params: Partial) { 122 | const requiredParams: Params = { 123 | param1: params.param1 ?? "", 124 | param2: params.param2 ?? 0, 125 | param3: params.param3 ?? [], 126 | } 127 | return requiredParams 128 | } 129 | ``` 130 | ### Required 131 | _让所有属性都变成必选的_ 132 | 133 | - 源码 134 | ```typescript 135 | /** 136 | * Make all properties in T required 137 | */ 138 | type Required = { 139 | [P in keyof T]-?: T[P] 140 | } 141 | ``` 142 | 143 | - 源码解析 144 | 145 | TS 在 2.8 版本改进了对[映射类型修饰符的控制](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#improved-control-over-mapped-type-modifiers),[映射修饰符-文档](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#mapping-modifiers) 146 | 在这个版本以后,可以通过在映射类型的属性修饰符(readonly 或 ?)前面增加 - 或 + 前缀,表示应删除或添加该修饰符,也就是上一章节中的 Partial 也的实现也可以长这样 147 | ```typescript 148 | type Partial = { 149 | [P in keyof T]+?: T[P] 150 | } 151 | ``` 152 | 也就是说 -? 的写法会去除可选属性这一属性修饰符,达到让每个属性都变为必选的目的 153 | 同时依据文档描述,--strictNullChecks 模式下,如果属性是包含了 undefined 的联合类型,那么 Required 也会将 undefined 移除 154 | ```typescript 155 | interface TestNullCheck { 156 | // 如果没有 number 类型,仅有 undefined 类型,则会保留 undefined 157 | testParam?: number | undefined 158 | } 159 | 160 | type Test = Required // 得到 { testParam: number } 161 | ``` 162 | 163 | - 使用场景举例 164 | 165 | 与 Partial 相反的场景 166 | ### Readonly 167 | _将所有属性变为只读_ 168 | 169 | - 源码 170 | ```typescript 171 | /** 172 | * Make all properties in T readonly 173 | */ 174 | type Readonly = { 175 | readonly [P in keyof T]: T[P] 176 | } 177 | ``` 178 | 179 | - 源码解析 180 | 181 | 与 Partial 和 Required 的实现基本相同,不同的是它的属性修饰符为 [readonly](https://www.typescriptlang.org/docs/handbook/2/objects.html#readonly-properties),无修饰符前缀 182 | readonly 修饰符会让被修饰的属性变为只读的(不能重写 re-written),但不能作用于该属性的子属性 183 | 184 | - 使用场景举例 185 | 1. 参考 Object.freeze 的声明 186 | 2. 某些项目中定义的常量,防止在后续维护中,不小心在其他位置做了修改,可以使用 Readonly 187 | ### Pick 188 | _从 T 类型选择一组属性构造新的类型_ 189 | 190 | - 源码 191 | ```typescript 192 | /** 193 | * From T, pick a set of properties whose keys are in the union K 194 | */ 195 | type Pick = { 196 | [P in K]: T[P] 197 | } 198 | ``` 199 | 200 | - 源码解析 201 | 202 | 使用 Pick 时,需传递两个泛型参数,第一个参数为一个[对象类型](https://www.typescriptlang.org/docs/handbook/2/objects.html)(或映射类型),第二个参数为第一个参数的键(属性)组成的联合类型(或单个字面量类型),Pick 构造的新类型中,属性为第二个参数中的联合类型的所有联合类型成员 203 | 示例: 204 | ```typescript 205 | interface Dogs { 206 | dogName: string 207 | dogAge: number 208 | dogKind: string 209 | } 210 | // 联合类型 211 | type NameAndAge = Pick // { dogName: string; dogAge: number } 212 | 213 | // 单个字符串类型 214 | type DogKind = Pick // { dogKind: string; } 215 | ``` 216 | 在 Pick 的实现中,引入了新的语法,[泛型](https://www.typescriptlang.org/docs/handbook/2/generics.html)、[extends](https://www.typescriptlang.org/docs/handbook/2/generics.html#generic-constraints) 217 | extends 在 TS 中,不同的位置使用有不同的含义,在这里是约束(Generic Constraints)的含义,extends 左侧类型一定要满足可分配给右侧类型 218 | keyof T 的写法在前文中已经讲到(另外泛型参数中,靠后的参数的 extends 子句能使用靠前参数的类型别名),T 是一个对象类型,那么 keyof T 是一个由 string 或 number (没有 symbol)组成的联合类型,因此 K 是 T 的所有属性名构成的联合类型的子类型 219 | in 映射类型可参考 Partial 章节,在 Pick 中,K 会被迭代,P 是在每次迭代中都是某个字面量类型,也是 T 的某一个属性名,通过索引访问 T[P] 能得到该属性名对应的属性值的具体类型,最后 Pick 得到一个新的对象类型 220 | 221 | - 使用场景举例 222 | 1. 某个位置需要全部的属性,其他位置仅需要部分属性的情况,如上文的 Dogs 例子 223 | 2. 参考 [lodash](https://lodash.com.cn/docs/chunk).pick 的声明和实现 224 | 3. 二次封装第三方组件,仅向外暴露部分参数的情况 225 | ### Record 226 | 227 | - 源码 228 | 229 | _基于一个联合类型构造一个新类型,其属性键为 K,属性值为 T_ 230 | ```typescript 231 | /** 232 | * Construct a type with a set of properties K of type T 233 | */ 234 | type Record = { 235 | [P in K]: T 236 | } 237 | ``` 238 | 239 | - 源码解析 240 | 241 | Record 源码的含义较为容易理解,即将 K 中的每个属性,都转为 T 类型 242 | 使用起来就是 243 | ```typescript 244 | interface Dogs { 245 | dogName: string 246 | dogAge: number 247 | dogKind: string 248 | } 249 | 250 | type KeyofDogs = keyof Dogs // "dogName" | "dogAge" | "dogKind" 251 | 252 | type StringDogs = Record 253 | // StringDogs 与下面的类型相同 254 | type StringDogs = { 255 | dogName: string 256 | dogAge: string 257 | dogKind: string 258 | } 259 | ``` 260 | 但你可能对于 keyof any 不太理解 261 | ```typescript 262 | type KeyofAny = keyof any 263 | // 等同于 264 | type KeyofAny = string | number | symbol 265 | ``` 266 | 被上述代码中 KeyofAny 约束的类型可以是如下类型 267 | ```typescript 268 | type A = "a" 269 | type B = "a" | "b" 270 | type C = "a" | "b" | string 271 | type D = "a" | 1 272 | type E = "a" | symbol 273 | type F = 1 274 | type G = string 275 | type H = number 276 | type I = symbol 277 | type J = symbol | 1 278 | ``` 279 | 也就是 由 symbol 、number 或 string 排列组合形成的联合类型、或字面量类型、或字面量类型组成的联合类型 280 | 至于 keyof unknown、keyof never,它们得到的结果都是 never 281 | 282 | - 使用场景举例 283 | 1. 通过 Record 构造索引类型 Record 得到 { [key: string]: string } 284 | 2. 在策略模式中使用 285 | ```typescript 286 | type DogsRecord = Record< 287 | "dogKind1" | "dogKind2", 288 | (currentAge: number) => number 289 | > 290 | function getRestAgeByCurrentAgeAndKinds( 291 | kind: "dogKind1" | "dogKind2", 292 | currentAge: number 293 | ) { 294 | // 计算不同类型的狗的可能的剩余年龄 295 | const dogsRestAge: DogsRecord = { 296 | dogKind1: function (currentAge: number) { 297 | return 20 - currentAge 298 | }, 299 | dogKind2: function (currentAge: number) { 300 | return 15 - currentAge 301 | }, 302 | } 303 | return dogsRestAge[kind](currentAge) 304 | } 305 | getRestAgeByCurrentAgeAndKinds("dogKind1", 1) 306 | ``` 307 | ### Exclude 308 | _从 T 的联合类型成员中排除可分配给类型 U 的所有联合成员来构造类型_ 309 | 310 | - 源码 311 | ```typescript 312 | /** 313 | * Exclude from T those types that are assignable to U 314 | */ 315 | type Exclude = T extends U ? never : T 316 | ``` 317 | 318 | - 源码解析 319 | 320 | 使用 Exclude 的例子 321 | ```typescript 322 | interface Dogs { 323 | dogName: string 324 | dogAge: number 325 | dogKind: string 326 | } 327 | 328 | type KeyofDogs = keyof Dogs // "dogName" | "dogAge" | "dogKind" 329 | 330 | type KeysWithoutKind = Exclude // "dogName" | "dogAge" 331 | ``` 332 | 在 Exclude 的源码中,引入了新的语法,[条件类型 Conditional Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html) 333 | 条件类型的 extends 与在 泛型中的 extends 含义不同,后者代表的是约束,而前者是判断(可分配),判断 extends 左侧类型是否可分配给右侧类型(判断方法大概就是右侧要的左侧有就可以,没有就不行,满足结构性兼容即可),如果可以则是冒号左边的类型,否则为右边的类型(与 js 的 true ? 1 : 2 用法类似) 334 | 在上面的例子中,你可能会想,KeyofDogs 并不能分配给 "dogKind" 类型,会得到 T 类型,也就是 KeyofDogs 类型本身,但实际的结果是 "dogName" | "dogAge",从 KeyofDogs 中移除了 "dogKind" 类型 335 | 从已有的条件我们并不能看出 Exclude 的原理是什么,TS 对条件类型有一种特殊情况,也就是[分布条件类型 Distributive Conditional Types](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types),其定义是**当条件类型作用于泛型类型时,并且这个泛型类型是联合类型,那么它就是分布式条件类型** 336 | 泛型类型很好理解,即 type Example = T 中的 T 就是一个泛型类型 337 | 源码中的 T extends U ? never : T, T 是一个泛型类型,同时这也是一个条件类型,满足分布条件类型的定义,会由联合类型 T 中的每个联合类型成员依次与 extends 右侧类型进行比对,上面代码中的 KeyofDogs 是一个联合类型,传入 Exclude 后,变为了一个泛型类型 T,"dogName" | "dogAge" | "dogKind" 会依次与 "dogKind" 进行比对,只有 "dogKind" 可以分配给 "dogKind",但得到的类型为 never,其他两个无法分配给 "dogKind",得到它们本身的字面量类型 "dogName" 和 "dogAge",它们组成的联合类型 "dogName" | "dogAge" 就是最终的结果 338 | **其他场景:** 339 | 如果 Exclude 第一个参数不是联合类型会怎么样? 340 | ```typescript 341 | type ExampleA = Exclude<1, 2> // 会走正常的条件类型,1 不能分配给 2,会得到第一个泛型参数的类型,也就是字面量类型 1 342 | 343 | type ExampleB = Exclude<{ 2: string }, 2> // 原理同上方注释,也是传入的第一个泛型参数的类型 { 2: string } 344 | ``` 345 | 346 | - 使用场景举例 347 | 1. 与映射类型配合使用,参考 Omit 的实现 348 | ### Extract 349 | _从 T 的联合类型成员中提取可分配给类型 U 的所有联合成员来构造类型_ 350 | 351 | - 源码 352 | ```typescript 353 | /** 354 | * Extract from T those types that are assignable to U 355 | */ 356 | type Extract = T extends U ? T : never 357 | ``` 358 | 359 | - 源码解析 360 | 361 | 在 Exclude 章节我们讲到了分布条件类型,Extract 的作用和 Exclude 正好相反,在 Exclude 中,会依次将 T 中的联合类型成员与类型 U 对比,如果其可以分配给类型 U,则得到该类型 362 | ```typescript 363 | interface Dogs { 364 | dogName: string 365 | dogAge: number 366 | dogKind: string 367 | } 368 | 369 | type KeyofDogs = keyof Dogs // "dogName" | "dogAge" | "dogKind" 370 | 371 | type KeysOnlyKind = Extract // "dogKind" 372 | ``` 373 | 374 | - 使用场景举例 375 | 1. 与映射类型配合使用,参考 Omit 的实现 376 | ```typescript 377 | // 提取 T 类型的部分(或全部)键构造一个新类型 378 | type Include = { 379 | [Key in Extract]: T[Key] 380 | } 381 | // 或 382 | type Include = Pick> 383 | ``` 384 | ### Omit 385 | _删除 T 类型中与 K 的所有联合类型成员有交集的键构造一个新类型_ 386 | 387 | - 源码 388 | ```typescript 389 | /** 390 | * Construct a type with the properties of T except for those in type K. 391 | */ 392 | type Omit = Pick> 393 | ``` 394 | Omit 源码借助了 Pick 和 Exclude,Pick 会构造一个基于第一个参数,且属性为第二个参数(联合类型)的联合类型成员的类型 395 | 第一个参数为 T,其第二个参数是 Exclude,Exclude 第一个参数为 keyof T,即 T 的所有键构成的联合类型 396 | K 是外部传入 Omit 的泛型类型,也会作为第二个参数传给 Exclude,由 Exclude 得到一个 keyof T 剔除掉与 K 交集的部分形成的联合类型 397 | 这样 Pick 生成的新类型的键就会仅包含由 Exclude 得到的联合类型中的联合类型成员 398 | 最终 Omit 会**删除 T 类型中与 K 的所有联合类型成员有交集的键构造一个新类型** 399 | ```typescript 400 | interface Dogs { 401 | dogName: string 402 | dogAge: number 403 | dogKind: string 404 | } 405 | 406 | type DogsWithoutKind = Omit // { dogName: string; dogAge: number; } 407 | ``` 408 | 409 | - 使用场景举例 410 | 1. 对 HTML 元素进行组件封装时,用它替换默认的属性类型 411 | ```typescript 412 | import _ from "lodash" 413 | import React from "react" 414 | 415 | type InputSize = "large" | "middle" | "small" 416 | type InputName = "first-name-input" | "last-name-input" | "address-input" 417 | type CoverAttr = "size" | "name" 418 | interface InputProps 419 | extends Omit, CoverAttr> { 420 | size?: InputSize 421 | name?: InputName 422 | } 423 | 424 | const Input: React.FC = (props) => { 425 | const classNames = `${props.className} ${props.size}` 426 | const omitProps = _.omit(props, ["size", "name"]) 427 | 428 | return 429 | } 430 | 431 | Input.defaultProps = { 432 | size: "middle", 433 | } 434 | ``` 435 | 436 | 1. 对第三方 UI 组件二次封装时,替换其参数 437 | 2. 其他(组件,函数,对象等)向使用者提供时,省略一些已处理的参数 438 | ```typescript 439 | interface Dogs { 440 | dogName: string 441 | dogAge: number 442 | dogKind: string 443 | } 444 | /* 445 | * 狗狗清洗登记,登记狗狗名字(假设狗狗名字独一无二)后返回一张凭证 446 | * 凭借凭证和狗狗的种类、年龄(设年龄不变大)到清洗处清洗 447 | */ 448 | const wash = (dog: Dogs) => { 449 | /** 洗狗 */ 450 | } 451 | // 登记的狗 452 | const queue = new Set([]) 453 | 454 | function dogsCleanRegister(dog: Dogs) { 455 | queue.add(dog.dogName) 456 | 457 | return function washTicket(dogNeedCheckInfo: Omit) { 458 | if ( 459 | dogNeedCheckInfo.dogAge === dog.dogAge && 460 | dogNeedCheckInfo.dogKind === dog.dogKind 461 | ) { 462 | wash(dog) 463 | queue.delete(dog.dogName) 464 | } else { 465 | throw new Error("凭证和狗狗不对应") 466 | } 467 | } 468 | } 469 | // 我用自己的狗登记 470 | const myDog = { 471 | dogName: "小明", 472 | dogAge: 5, 473 | dogKind: "柯基", 474 | } 475 | 476 | const goToWash = dogsCleanRegister(myDog) 477 | 478 | // 我拿别人的狗去洗 479 | const myBrothersDog = { 480 | dogName: "大明", 481 | dogAge: 6, 482 | dogKind: "哈士奇", 483 | } 484 | 485 | // 校验失败 486 | goToWash(myBrothersDog) // '凭证和狗狗不对应' 487 | ``` 488 | ### NonNullable 489 | _新类型不可为空_ 490 | 491 | - 源码 492 | ```typescript 493 | /** 494 | * Exclude null and undefined from T 495 | */ 496 | type NonNullable = T extends null | undefined ? never : T 497 | ``` 498 | 499 | - 源码解析 500 | 501 | NonNullable 中也用到了分布条件类型,如果泛型类型 T 为联合类型,则其每个联合类型成员中可被分配给 null | undefined 的类型也就是(never、null 和undefined)会被剔除 502 | 如下所示: 503 | ```typescript 504 | // 狗狗名字的实际情况,流浪狗可能没人起名字即为 null,刚出生的狗可能还没来的及起名字即为 undefined 505 | type DogsName = 'husky' | 'corgi' | null | undefined 506 | 507 | // 到商店洗狗时,不允许没有名字的狗 508 | type NonNullableDogsName = NonNullable // 得到 type NonNullableDogsName = "husky" | "corgi" 509 | ``` 510 | 另外,当传入的参数不为联合类型时,除 null 和 undefined,都会得到传入类型本身 511 | ```typescript 512 | // any 513 | type NonNullableAny = NonNullable // any,any 的条件类型比较特殊,会得到两个分支类型的联合类型 514 | // unknown 515 | type NonNullableUnknown = NonNullable // unknown 516 | // never 517 | type NonNullableNever = NonNullable // never 518 | // null 519 | type NonNullableNull = NonNullable // never 520 | ``` 521 | 522 | - 使用场景举例 523 | 1. 过滤掉 null 类型和 undefined 类型 524 | ```typescript 525 | // strictNullChecks 模式 526 | // 函数类型的定义见 Parameters 章节 527 | // 洗动物的方法,记载了需要提供的信息,虽然有方法,但可能没有工作人员,这时候是未定义 528 | interface WashFunctions { 529 | washDogs?: (params: { dogName: string; dogAge: number }) => void 530 | washCats?: (params: { catName: string; catAge: number }) => void 531 | } 532 | // 假设要从洗动物的方法中自动生成表单信息给顾客填写 533 | // 需求,提取出参数类型 534 | // Parameters 用于提取函数类型的参数列表类型,详见下一章节 535 | // Parameters 仅能传入函数类型 536 | type ParamsByCallbackMap = { 537 | [Key in keyof WashFunctions]-?: 538 | Parameters>[0] 539 | } 540 | // 得到 541 | /** 542 | *type ParamsByCallbackMap = { 543 | * washDogs: { 544 | * dogName: string; 545 | * dogAge: number; 546 | * }; 547 | * washCats: { 548 | * catName: string; 549 | * catAge: number; 550 | * }; 551 | *} 552 | */ 553 | ``` 554 | ### Parameters 555 | _基于函数类型 T 的参数类型构造一个元组类型_ 556 | 557 | - 源码 558 | ```typescript 559 | /** 560 | * Obtain the parameters of a function type in a tuple 561 | */ 562 | type Parameters any> = T extends (...args: infer P) => any ? P : never; 563 | ``` 564 | 565 | - 源码解析 566 | 567 | 了解 Parameters 的原理之前,首先得知道,函数的类型如何进行定义 568 | 569 | 1. 最常见且简单的方式,使用类型别名([函数类型表达式 function-type-expressions](https://www.typescriptlang.org/docs/handbook/2/functions.html#function-type-expressions)) 570 | ```typescript 571 | type Func1 = (...args: string[]) => string 572 | type Func2 = (arg1: string, arg2: number) => string 573 | type Func3 = (arg1: string, arg2: number, ...args: Array) => string 574 | const arrowFunc: Func3 = ( 575 | arg1: string, 576 | arg2: number, 577 | ...args: Array 578 | ) => arg1 + [arg2, ...args].reduce((preTotal, current) => preTotal + current, 0) 579 | ``` 580 | 581 | 1. 使用接口进行定义(下面代码中的 Func3 语法为 [调用签名 call-signatures](https://www.typescriptlang.org/docs/handbook/2/functions.html#call-signatures)) 582 | ```typescript 583 | // Func['func1'] 和 Func['func2'] 为函数类型 584 | interface Func { 585 | func1(arg: number): number 586 | func2(arg: string): number 587 | } 588 | const func: Func = { 589 | func1(arg: number) { 590 | return arg 591 | }, 592 | func2: (arg: string) => Number(arg) 593 | } 594 | // Func3 即为函数类型 595 | interface Func3 { 596 | (arg: number): string 597 | } 598 | const func3: Func3 = (arg: number) => { 599 | return arg.toString() 600 | } 601 | ``` 602 | 603 | 1. 使用接口进行重载(实际上是接口的合并) 604 | ```typescript 605 | interface Func { 606 | (arg: number): string 607 | (arg: string): string 608 | } 609 | const func: Func = (arg: number | string) => { 610 | return arg.toString() 611 | } 612 | ``` 613 | 614 | 1. 使用 declare 进行类型定义([contextual-typing](https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#contextual-typing)) 615 | ```typescript 616 | declare function Func(...args: string[]): string 617 | const func: typeof Func = (...args: string[]) => { 618 | return args.join('') 619 | } 620 | ``` 621 | 622 | 1. 使用函数声明进行重载([函数重载 function-overloads](https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads)) 623 | ```typescript 624 | function func4(...args: string[]): string 625 | function func4(...args: number[]): string 626 | function func4(...args: (string | number)[]) { 627 | return args.join('') 628 | } 629 | ``` 630 | 摘自文档:从具有多个调用签名的类型(例如重载函数的类型)进行推断时,将从最后一个签名进行推断。无法基于参数类型列表执行重载解析 631 | 632 | 1. 其他方式(见下文,或参考[官方文档](https://www.typescriptlang.org/docs/handbook/2/functions.html)) 633 | 634 | Parameters 泛型 T 的约束为 (...args: any) => any(注:Function 类型没有内容和签名,不能分配给它),上述5种定义函数类型的方式,都可以分配到该类型 635 | 因此都可以作为参数传给 Parameters,Parameters 的实现也使用了条件类型,如果泛型 T 可以分配给 (...args: infer P) => any,则为 P 类型 636 | infer 关键字见[在条件类型中推断](https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types),infer 的作用是让 TS 自己推断,并将推断的结果存储到一个类型变量中,infer 只能用于 extends 语句中 637 | 几个用它进行推导的例子 638 | ```typescript 639 | type Example1 = Array extends Array ? T : never // number 640 | type Example2 = { a: string } extends { a: infer T } ? T : never // string 641 | ``` 642 | 看到上面两个例子,可能 Parameters 源码你已经能看懂了 643 | 但是问题来了,TS 的子类型是基于 结构子类型 的,只要结构可以兼容,就是子类型,一般来讲,在条件类型中,extends 左侧类型可分配给右侧类型,那么左侧类型就是右侧类型的子类型 644 | 如果左侧类型是一个对象类型,在下面的例子中,只有 Example3 中,左侧是不可分配给右侧的,因为它缺少了 b 属性 645 | 对象类型中,子类型中必须包含源类型所有的属性和方法(可多但是不能少) 646 | ```typescript 647 | type Example1 = { a: string } extends { a: string } ? true : false // true 648 | type Example2 = { a: string; b: number } extends { a: string } ? true : false // true 649 | type Example3 = { a: string; } extends { a: string; b: number } ? true : false // false 650 | ``` 651 | 如果左侧类型是一个联合类型,在下面的例子中,只有 Example2 中,左侧是不可分配给右侧的,因为 'b' 不能分配给 'a' 652 | 联合类型中,子类型必须仅有源类型中的部分或全部成员(可以少但是不能多) 653 | ```typescript 654 | type Example1 = 'a' extends 'a' ? true : false // true 655 | type Example2 = 'a' | 'b' extends 'a' ? true : false // false 656 | type Example3 = 'a' extends 'a' | 'b' ? true : false // true 657 | ``` 658 | 那么,某个函数类型的子类型是什么样的呢? 659 | 了解这一点之前,你需要知道[协变](https://baike.baidu.com/item/%E5%8D%8F%E5%8F%98)和逆变这样的概念在 TS 中也存在 660 | 对于函数类型来说,函数参数的类型兼容是反向的,称之为 逆变 ,返回值的类型兼容是正向的,称之为 协变 661 | 参考 [你可能不知道的 TypeScript 高级技巧](https://juejin.cn/post/6844904037922373639) 662 | **函数参数:我可以仅使用你传给我的一部分(或全部)属性或方法** 663 | 还是洗狗的例子,带狗来店里进行清洗的客户需要向我们店的员工说明狗的信息,但是不管你说多少信息,店员只使用年龄和品种 664 | ```typescript 665 | // 店员获取狗的信息 666 | function staffGetDogInfo(params: { dogAge: number; dosKind: string }): void { 667 | document.write(params.dogAge + params.dosKind) // 登记信息,写在纸上 668 | } 669 | // 客户说狗的信息,要有个人做“听他说”这件事,也就是参数 receiveAction 方法 670 | function customerSayDogInfo(receiveAction: (params: { dogAge: number; dosKind: string; dogName: string }) => any) { 671 | const dogInfo = { 672 | dogAge: 1, 673 | dosKind: "husky", 674 | dogName: "狗蛋", 675 | } 676 | receiveAction(dogInfo) 677 | } 678 | // ts 类型校验通过,staffGetDogInfo 可以分配给 customerSayDogInfo 的参数类型 receiveAction 679 | customerSayDogInfo(staffGetDogInfo) 680 | ``` 681 | 示例中仅展示单一参数的情况,对于参数个数,这个规则也是类似的,你可以多给我几个参数,但我可以不用 682 | 总结起来就是,两个函数类型做比较,函数参数少(或参数数量相等)的且对应位置参数需要的属性/方法少的(不超过另一个类型对应位置参数需要的属性/方法数量,但注意联合类型和对象类型是反着的,联合类型的成员要比对应位置参数类型的联合类型成员多),在不考虑函数返回值的情况下,是函数子类型 683 | **函数返回值:我要的你必须给我,可以多但是一个都不能少** 684 | 继续使用上面的例子,我们把场景改造下,但还是让参数的类型仍然满足函数子类型的规则,除了店员需要的信息,客户还会说一些不需要的,店员记录完以后,会给一个回执 685 | 客户要从回执中知道的就是,什么时候开始洗,什么时候洗完,谁来给我的狗洗 686 | ```typescript 687 | // 店员获取狗的信息,登记后给回执 688 | function staffGetDogInfo(params: { dogAge: number; dosKind: string }): { 689 | washPersonName: string, 690 | washStartTime: string, 691 | washEndTime: string, 692 | payedMoney: number, 693 | isVip: boolean 694 | } { 695 | document.write(params.dogAge + params.dosKind) // 登记信息,写在纸上 696 | // 返回回执 697 | return { 698 | washPersonName: 'staff Alice', 699 | washStartTime: '2021/5/25 20:00:00', 700 | washEndTime: '2021/5/25 20:30:00', 701 | payedMoney: 100, 702 | isVip: true 703 | } 704 | } 705 | // 客户说狗的信息,要有个人做“听他说”这件事,也就是参数 receiveAction 方法 706 | // 得到回执后,看回执上面的结束时间,过多久再回来 707 | function customerSayDogInfo( 708 | receiveAction: ( 709 | params: { dogAge: number; dosKind: string; dogName: string } 710 | ) => { washEndTime: string } 711 | ) { 712 | const dogInfo = { 713 | dogAge: 1, 714 | dosKind: "husky", 715 | dogName: "狗蛋", 716 | } 717 | const receipt = receiveAction(dogInfo) 718 | setTimeout(() => { 719 | // 回来取狗 720 | }, Number(new Date(receipt.washEndTime)) - Number(new Date())) 721 | } 722 | // ts 类型校验通过,staffGetDogInfo 可以分配给 customerSayDogInfo 的参数类型 receiveAction 723 | customerSayDogInfo(staffGetDogInfo) 724 | ``` 725 | 结合上面两个例子,容易得到:**两个函数类型做比较,函数参数少(或参数数量相等)的且对应位置参数需要的属性/方法少的(不超过另一个类型对应位置参数需要的属性/方法数量,但注意联合类型和对象类型是反着的,联合类型的成员要比对应位置参数类型的联合类型成员多),且函数返回值类型中含有的属性或方法要多于另一个函数类型的时候(注意联合类型是成员更少的),该类型是函数子类型** 726 | extends 左右两侧为函数类型时,会得到哪个分支的类型,就显而易见了 727 | 728 | - 使用场景 729 | 1. 高阶函数,不使用泛型的情况下,某些场景可以用 Parameters 提取出传入的函数的参数类型 730 | ### ConstructorParameters 731 | _从构造函数类型 T 的参数类型构造元组或数组类型(如果 T 不是函数,则为 never)_ 732 | 733 | - 源码 734 | ```typescript 735 | /** 736 | * Obtain the parameters of a constructor function type in a tuple 737 | */ 738 | type ConstructorParameters any> = T extends new (...args: infer P) => any ? P : never; 739 | ``` 740 | 741 | - 源码解析 742 | 743 | ConstructorParameters 的源码与 Parameters 的源码极其相似,只是在函数类型前多了一个 new 744 | 在函数类型前面写一个 new 关键字的语法在 TS 中被称为[构造签名 Construct Signatures](https://www.typescriptlang.org/docs/handbook/2/functions.html#construct-signatures) 745 | 构造签名一般用在 JS 运行环境中自带的构造函数的声明(现有 API),或在 .d.ts 声明文件中使用 746 | 如果你写了一个构造函数,请不要使用构造签名进行类型定义,因为你很难定义出来 747 | 其他情况,你可以将它作为一个约束类型来使用(比如定义函数参数的类型必须为一个构造函数),而不是直接用于函数或类的类型声明 748 | ConstructorParameters 的使用也与 Parameters 相似 749 | ```typescript 750 | class Dog { 751 | private dogAge: number 752 | private isMale: boolean 753 | private dogKind: string 754 | constructor(isMale: boolean, dogKind: string) { 755 | this.dogAge = 0 756 | this.isMale = isMale 757 | this.dogKind = dogKind 758 | } 759 | } 760 | type DogGaveBirthNeedInfo = ConstructorParameters // 得到 [boolean, string] 类型 761 | ``` 762 | ### ReturnType 763 | _基于函数类型 T 的返回值类型构造一个新类型_ 764 | 765 | - 源码 766 | ```typescript 767 | /** 768 | * Obtain the return type of a function type 769 | */ 770 | type ReturnType any> = T extends (...args: any) => infer R ? R : any; 771 | ``` 772 | 773 | - 源码解析 774 | 775 | 与 Parameters 源码不同的是,其 infer 的 R 在函数类型的返回值位置 776 | ```typescript 777 | function washDog() { 778 | return { 779 | dogName: 'linlin', 780 | dogAge: 20, 781 | dogKind: 'husky' 782 | } 783 | } 784 | type WashTicket = ReturnType 785 | /* 786 | * 会的到这样的类型,也就是函数 washDog 返回值的类型 787 | *type WashTicket = { 788 | * dogName: string 789 | * dogAge: number 790 | * dogKind: string 791 | *} 792 | */ 793 | ``` 794 | 795 | - 使用场景举例 796 | 1. 高阶函数,不使用泛型的情况下,某些场景可以用 ReturnType 提取出传入的函数的返回值类型 797 | ### InstanceType 798 | _基于一个构造函数类型 T 的返回值构造一个新类型_ 799 | 800 | - 源码 801 | ```typescript 802 | /** 803 | * Obtain the return type of a constructor function type 804 | */ 805 | type InstanceType any> = T extends new (...args: any) => infer R ? R : any; 806 | ``` 807 | 808 | - 源码解析 809 | 810 | InstanceType 与 ReturnType 的区别是它多了构造签名,与 ConstructorParameters 的区别是它推断的不是参数类型,而是返回值类型 811 | ```typescript 812 | class Dog { 813 | private dogAge: number 814 | private isMale: boolean 815 | private dogKind: string 816 | constructor(isMale: boolean, dogKind: string) { 817 | this.dogAge = 0 818 | this.isMale = isMale 819 | this.dogKind = dogKind 820 | } 821 | } 822 | type DogGaveBirthNeedInfo = InstanceType // 得到 Dog 类型 823 | ``` 824 | 也许你会疑问,为什么还得到 Dog 本身了? 825 | 请看下图 826 | [![](https://cdn.nlark.com/yuque/0/2022/jpeg/26213304/1649573753500-75b40664-144e-47ad-ac2b-aa9d451d8eb7.jpeg#clientId=u65a1ab80-3fb1-4&from=paste&id=u28c1237f&originHeight=245&originWidth=628&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=ucf96434a-45bf-4b4c-8c80-e46cded6738&title=)](https://yzl.xyz/lin/2021/05/%E6%B7%B1%E5%85%A5%E6%B5%85%E5%87%BATS%E7%9A%84Utility-Types/0b5a4c408b0d/class-type-example.jpg) 827 | class 定义的类本身也是一种类型,它的实例的类型可以用它本身来进行描述 828 | 如 Dog['dogAge'] 能得到实例的私有属性 dogAge 的类型 number 829 | ### Uppercase 830 | _将字符串的字面量类型转为大写_ 831 | Uppercase 的实现为编译器内置,[TS 4.1 新增](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-1.html#template-literal-types),可以模板字符串类型配合使用,文档见:[内置字符串操作类型 Intrinsic String Manipulation Types](https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html#intrinsic-string-manipulation-types),参考 [commit](https://github.com/microsoft/TypeScript/commit/fbce4f6c989e4296ab43873ffc78e9c17809cac9),下同 832 | 833 | - 源码 834 | ```typescript 835 | /** 836 | * Convert string literal type to uppercase 837 | */ 838 | type Uppercase = intrinsic; 839 | ``` 840 | 841 | - 用法 842 | ```typescript 843 | type DogName = "LinLin" 844 | type UppercaseDogName = Uppercase // 得到 "LINLIN" 845 | ``` 846 | 如果传入的类型为联合类型,则会得到一个新类型,其每个成员都会转为大写(二十六个字母) 847 | 如果传入的类型为 any 或者是 string,则会得到它们本身 848 | ### Lowercase 849 | _将字符串的字面量类型转换为小写_ 850 | Lowercase 的实现为编译器内置 851 | 852 | - 源码 853 | ```typescript 854 | /** 855 | * Convert string literal type to lowercase 856 | */ 857 | type Lowercase = intrinsic; 858 | ``` 859 | 860 | - 用法 861 | ```typescript 862 | type DogName = "LinLin" 863 | type LowercaseDogName = Lowercase // 得到 "linlin" 864 | ``` 865 | ### Capitalize 866 | _将字符串的字面量类型首字母转换为大写_ 867 | Capitalize 的实现为编译器内置 868 | 869 | - 源码 870 | ```typescript 871 | /** 872 | * Convert first character of string literal type to uppercase 873 | */ 874 | type Capitalize = intrinsic; 875 | ``` 876 | 877 | - 用法 878 | ```typescript 879 | type DogName = "linlin" 880 | type CapitalizeDogName = Capitalize // 得到 "LinLin" 881 | ``` 882 | ### Uncapitalize 883 | _将字符串的字面量类型首字母转换为小写_ 884 | Uncapitalize 的实现为编译器内置 885 | 886 | - 源码 887 | ```typescript 888 | /** 889 | * Convert first character of string literal type to lowercase 890 | */ 891 | type Uncapitalize = intrinsic; 892 | ``` 893 | 894 | - 用法 895 | ```typescript 896 | type DogName = "LinLin" 897 | type UncapitalizeDogName = Uncapitalize // 得到 "linlin" 898 | ``` 899 | 900 | - 使用场景 901 | 1. 上述四个字符串操作类型,可与模板字符串类型配合使用,实现高级的类型定义 902 | ### ThisType 903 | _增强对象字面量类型中 this 的类型_ 904 | 905 | - 源码 906 | ```typescript 907 | /** 908 | * Marker for contextual 'this' type 909 | */ 910 | interface ThisType { } 911 | ``` 912 | 除了在对象字面量类型中使用(需要启用 --noImplicitThis),其余位置使用都仅是一个空接口,具体可参考[文档 ThisType](https://www.typescriptlang.org/docs/handbook/utility-types.html#thistypetype)中的例子 913 | ### ThisParameterType 914 | _提取函数声明的 this 类型_ 915 | 916 | - 源码 917 | ```typescript 918 | /** 919 | * Extracts the type of the 'this' parameter of a function type, or 'unknown' if the function type has no 'this' parameter. 920 | */ 921 | type ThisParameterType = T extends (this: infer U, ...args: any[]) => any ? U : unknown; 922 | ``` 923 | 924 | - 源码解析 925 | 926 | 如果你需要[在函数的实现中使用 this](https://www.typescriptlang.org/docs/handbook/2/functions.html#declaring-this-in-a-function),那么你可以在第一个参数位置显示的声明它(函数参数类型依次顺延,第二位为函数的第一个参数),如下所示 927 | ```typescript 928 | interface Dog { 929 | voice: { 930 | bark(): void 931 | } 932 | } 933 | function dogBark(this: Dog) { 934 | this.voice.bark() 935 | } 936 | // 如果不显示的声明 this,则 this 会根据函数声明所在的环境进行推导 937 | // 声明 this 后,在函数调用时,TS 会校验当前上下文中的 this 是否与所需的 this 相匹配 938 | // dogBark 的调用方式如下 939 | declare const dog: Dog 940 | dogBark.call(dog) 941 | // 或 942 | declare const dog: Dog & { dogBark: typeof dogBark } 943 | dog.dogBark() 944 | ``` 945 | 不要在箭头函数里面显示的定义 this,箭头函数的 this 不可改变 946 | ThisParameterType 和 Parameters 的实现类似,都是基于 infer,ThisParameterType 推导的是 this 的类型,如果没有显示的声明 this,则为 unknown 947 | ### OmitThisParameter 948 | _基于一个函数类型构造一个没有 this 声明的函数类型_ 949 | 950 | - 源码 951 | ```typescript 952 | /** 953 | * Removes the 'this' parameter from a function type. 954 | */ 955 | type OmitThisParameter = unknown extends ThisParameterType ? T : T extends (...args: infer A) => infer R ? (...args: A) => R : T; 956 | ``` 957 | 958 | - 源码解析 959 | 960 | 这就类似于把可选属性的属性修饰符 ? 给去掉,为了去掉这个修饰符,TS 专门提供了一种方式,而 OmitThisParameter 是用 TS 已有的其他方式来对 this 进行剔除 961 | OmitThisParameter 可以接受一个函数类型 T,如果 ThisParameterType 得到 unknown 类型(未显示指定 this 或 不是函数类型),则直接返回类型 T,否则将类型 T 与类型 (...args: infer A) => infer R 做比较并提取参数类型 A 和 返回值类型 R,如果前者(T)是后者的子类型,则得到一个新的函数类型,它的参数类型为 A,返回值类型为 R,否则得到 T 类型本身 962 | 注意,带有 this 类型的函数类型是不带 this 的但参数类型与前者一致的函数类型的子类型 963 | -------------------------------------------------------------------------------- /TypeScript/泛型 Generics.md: -------------------------------------------------------------------------------- 1 | ## 一、泛型是什么 2 | 用来处理不同类型的对象而并非单一类型的对象。它具有下列特性 3 | 4 | 1. 复用性 5 | 2. 支持未来数据类型 6 | 3. 泛型就是对类型进行编程 7 | 8 | 复用性 9 | ```typescript 10 | function id(arg: boolean): boolean { 11 | return arg; 12 | } 13 | function id(arg: number): number { 14 | return arg; 15 | } 16 | function id(arg: string): string { 17 | return arg; 18 | } 19 | ``` 20 | 如果想输入什么,输出就是什么,在不使用泛型的情况下只能只用any 21 | ```typescript 22 | function identity(arg: any): any { 23 | return arg; 24 | } 25 | ``` 26 | 但是如果我们使用any类型,在我们调用这个方法获得返回值后我们就失去了这个输出结果的数据类型。我们如果输入一个数字(number),那么能得到的就只有any类型。失去了类型保护和语法提示 27 | 因此,我们需要使用类型捕捉的方式来进行类型的获取。这样我们在获取返回值时也可以获取到返回值的类型。在这里,我们使用一种叫做_类型变量_(type variable)的特殊变量。这种变量专门处理变量的类型而不是变量的值。 28 | 使用泛型对上面的代码进行重构 29 | T 是一个抽象类型,只有在调用的时候才确定它的值 30 | ```typescript 31 | function id(arg: T): T { 32 | return arg; 33 | } 34 | ``` 35 | 为了便于大家更好地理解上述的内容,我们来举个例子,在这个例子中,我们将一步步揭示泛型的作用。 36 | 首先定义一个类: 37 | ```typescript 38 | class People { 39 | name!: string; 40 | age!: number; 41 | constructor(name: string, age: number) { 42 | this.name = name; 43 | this.age = age; 44 | } 45 | } 46 | 47 | ``` 48 | 再定义一个工厂函数: 49 | ```typescript 50 | function create(Constructor: { new (...args: any): any }) { 51 | return new Constructor(); 52 | } 53 | 54 | ``` 55 | 此时我们在调用工厂函数返回值得时候,其实已经失去了约束和提示,因为我们的参数设定的是any,返回值类型也是any 56 | ```typescript 57 | // function create(Constructor: new (...args: any) => any): any 58 | create(People).wsy; 59 | 60 | ``` 61 | 加入泛型,此时就能提示出类上有的属性 62 | ```typescript 63 | function create(Constructor: { new (...args: any): T }) { 64 | return new Constructor(); 65 | } 66 | create(People); 67 | ``` 68 | 对于刚接触 TypeScript 泛型的读者来说,首次看到 语法会感到陌生。但这没什么可担心的,就像传递参数一样,我们传递了我们想要用于特定函数调用的类型。 69 | 其中 T 代表 **Type**,在定义泛型时通常用作第一个类型变量名称。但实际上 T 可以用任何有效名称代替。除了 T 之外,以下是常见泛型变量代表的意思: 70 | 71 | - K(Key):表示对象中的键类型; 72 | - V(Value):表示对象中的值类型; 73 | - E(Element):表示元素类型。 74 | ```typescript 75 | function identity (value: T, message: U) : T { 76 | console.log(message); 77 | return value; 78 | } 79 | 80 | console.log(identity(68, "Semlinker")); 81 | 82 | ``` 83 | ![](https://cdn.nlark.com/yuque/0/2022/webp/973111/1649532607723-8bd4b570-4afe-43ae-81a8-f21015c83f53.webp#clientId=u5c2d4c07-bff2-4&from=paste&id=JllbW&originHeight=564&originWidth=1178&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=u13342680-2026-4aaa-98bf-2189ba7b719&title=) 84 | 对于上述代码,编译器足够聪明,能够知道我们的参数类型,并将它们赋值给 T 和 U,而不需要开发人员显式指定它们。下面我们来看张动图,直观地感受一下类型传递的过程: 85 | ![](https://cdn.nlark.com/yuque/0/2022/webp/973111/1649524242475-9e5c763b-c8af-4669-abfc-554da52b8132.webp#clientId=u5c2d4c07-bff2-4&from=paste&id=GXssu&originHeight=480&originWidth=780&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=ub0643196-e0db-4504-9055-02f4173c60e&title=) 86 | ## 二、泛型接口 87 | 为了解决上面提到的问题,首先让我们创建一个用于的 identity 函数通用 Identities 接口: 88 | ```typescript 89 | interface Identities { 90 | value: V, 91 | message: M 92 | } 93 | 94 | ``` 95 | 在上述的 Identities 接口中,我们引入了类型变量 V 和 M,来进一步说明有效的字母都可以用于表示类型变量,之后我们就可以将 Identities 接口作为 identity 函数的返回类型: 96 | ```typescript 97 | function identity (value: T, message: U): Identities { 98 | console.log(value + ": " + typeof (value)); 99 | console.log(message + ": " + typeof (message)); 100 | let identities: Identities = { 101 | value, 102 | message 103 | }; 104 | return identities; 105 | } 106 | 107 | console.log(identity(68, "Semlinker")); 108 | 109 | ``` 110 | 以上代码成功运行后,在控制台会输出以下结果: 111 | ```typescript 112 | { value: 68, message: 'Semlinker' } 113 | ``` 114 | ## 三、泛型类 115 | 在类中使用泛型也很简单,我们只需要在类名后面,使用 的语法定义任意多个类型变量,具体示例如下: 116 | ```typescript 117 | interface GenericInterface { 118 | value: U 119 | getIdentity: () => U 120 | } 121 | 122 | class IdentityClass implements GenericInterface { 123 | value: T 124 | 125 | constructor(value: T) { 126 | this.value = value 127 | } 128 | 129 | getIdentity(): T { 130 | return this.value 131 | } 132 | 133 | } 134 | 135 | const myNumberClass = new IdentityClass(68); 136 | console.log(myNumberClass.getIdentity()); // 68 137 | 138 | const myStringClass = new IdentityClass("Semlinker!"); 139 | console.log(myStringClass.getIdentity()); // Semlinker! 140 | 141 | ``` 142 | 我们在什么时候需要使用泛型呢?通常在决定是否使用泛型时,我们有以下两个参考标准: 143 | 144 | - 当你的函数、接口或类将处理多种数据类型时; 145 | - 当函数、接口或类在多个地方使用该数据类型时。 146 | 147 | 很有可能你没有办法保证在项目早期就使用泛型的组件,但是随着项目的发展,组件的功能通常会被扩展。这种增加的可扩展性最终很可能会满足上述两个条件,在这种情况下,引入泛型将比复制组件来满足一系列数据类型更干净。 148 | 我们将在本文的后面探讨更多满足这两个条件的用例。不过在这样做之前,让我们先介绍一下 Typescript 泛型提供的其他功能。 149 | ## 四、泛型约束 150 | 有时我们可能希望限制每个类型变量接受的类型数量,这就是泛型约束的作用。下面我们来举几个例子,介绍一下如何使用泛型约束。 151 | ### 4.1 确保属性存在 152 | ```typescript 153 | 154 | function identity(arg: T): T { 155 | console.log(arg.length); // 可以获取length属性 156 | return arg; 157 | } 158 | ``` 159 | T extends { length: number } 用于告诉编译器,我们支持已经实现 Length 接口的任何类型。之后,当我们使用不含有 length 属性的对象作为参数调用 identity 函数时,TypeScript 会提示相关的错误信息: 160 | ```typescript 161 | identity(1) 162 | // Argument of type 'number' is not assignable to parameter of type '{ length: number; }' 163 | ``` 164 | ### 4.2 约束类型 165 | ```typescript 166 | function sortChinese(arr: Array): T[] { 167 | return arr.sort((a, b) => { 168 | return a.localeCompare(b, 'zh-CN'); 169 | }); 170 | } 171 | // Property 'localeCompare' does not exist on type 'T' 172 | 173 | ``` 174 | 此时会提示类型“T”上不存在属性“localeCompare” 175 | 用extends约束T的类型 176 | ```typescript 177 | function sortChinese(arr: Array): T[] { 178 | return arr.sort((a, b) => { 179 | return a.localeCompare(b, 'zh-CN'); 180 | }); 181 | } 182 | ``` 183 | ### 4.3 检查对象上的键是否存在 184 | 泛型约束的另一个常见的使用场景就是检查对象上的键是否存在。不过在看具体示例之前,我们得来了解一下 keyof 操作符, 185 | ```typescript 186 | interface Person { 187 | name: string; 188 | age: number; 189 | location: string; 190 | } 191 | 192 | type K1 = keyof Person; // "name" | "age" | "location" 193 | type K2 = keyof Person[]; // number | "length" | "push" | "concat" | ... 194 | type K3 = keyof { [x: string]: Person }; // string | number 195 | 196 | ``` 197 | 通过 keyof 操作符,我们就可以获取指定类型的所有键,之后我们就可以结合前面介绍的 extends 约束,即限制输入的属性名包含在 keyof 返回的联合类型中。具体的使用方式如下: 198 | ```typescript 199 | function getProperty(obj: T, key: K): T[K] { 200 | return obj[key]; 201 | } 202 | ``` 203 | 在以上的 getProperty 函数中,我们通过 K extends keyof T 确保参数 key 一定是对象中含有的键,这样就不会发生运行时错误。这是一个类型安全的解决方案,与简单调用 let value = obj[key]; 不同。 204 | ```typescript 205 | const a = { 206 | name: 'Semlinker', 207 | age: 18, 208 | height: 18, 209 | }; 210 | 211 | function getProperty(obj: T, key: K): T[K] { 212 | return obj[key]; 213 | } 214 | console.log(getProperty(a, 'name')); 215 | ``` 216 | 可能会有疑问的是,如果按照下列的写法去写也能够约束key是T的属性 217 | ```typescript 218 | function getProperty(obj: T, key: keyof T) { 219 | return obj[key]; 220 | } 221 | const a1 = getProperty(a, 'name'); 222 | console.log(a1); 223 | ``` 224 | 若使用 keyof T 作为 key 的类型,那 obj[key] 就是 T[keyof T] 类型——这无疑不够精确 225 | ```typescript 226 | const a1: string | number 227 | ``` 228 | 结论就是不够精准,**泛型约束是必须的,是为了让 obj[key] 成为类型正确的表达式**。 229 | ## 五、泛型参数默认类型 230 | 未指定默认值 231 | ```typescript 232 | function sortChinese(arr: Array): T[] { 233 | return arr.sort((a, b) => { 234 | return a.localeCompare(b, 'zh-CN'); 235 | }); 236 | } 237 | 238 | sortChinese(1); 239 | 240 | // 类型“number”的参数不能赋给类型“unknown[]”的参数。 241 | ``` 242 | 指定了默认值 243 | ```typescript 244 | function sortChinese(arr: Array): T[] { 245 | return arr.sort((a, b) => { 246 | return a.localeCompare(b, 'zh-CN'); 247 | }); 248 | } 249 | 250 | sortChinese(['1']); 251 | // function sortChinese(arr: string[]): string[] 252 | ``` 253 | 如果添加了约束 254 | ```typescript 255 | function sortChinese(arr: Array): T[] { 256 | return arr.sort((a, b) => { 257 | return a.localeCompare(b, 'zh-CN'); 258 | }); 259 | } 260 | 261 | sortChinese(['1', 's']); 262 | // function sortChinese<"1" | "s">(arr: ("1" | "s")[]): ("1" | "s")[] 263 | ``` 264 | ## 六、类型工具前置条件 265 | #### keyof 索引查询 266 | 对应任何类型T,keyof T的结果为该类型上所有公有属性key的联合: 267 | ```typescript 268 | interface Eg1 { 269 | name: string, 270 | readonly age: number, 271 | } 272 | // T1的类型实则是name | age 273 | type T1 = keyof Eg1 274 | 275 | class Eg2 { 276 | private name: string; 277 | public readonly age: number; 278 | protected home: string; 279 | } 280 | // T2实则被约束为 age 281 | // 而name和home不是公有属性,所以不能被keyof获取到 282 | type T2 = keyof Eg2 283 | 284 | ``` 285 | #### T[K] 索引访问 286 | ```typescript 287 | interface Eg1 { 288 | name: string, 289 | readonly age: number, 290 | } 291 | // string 292 | type V1 = Eg1['name'] 293 | // string | number 294 | type V2 = Eg1['name' | 'age'] 295 | // any 296 | type V2 = Eg1['name' | 'age2222'] 297 | // string | number 298 | type V3 = Eg1[keyof Eg1] 299 | 300 | ``` 301 | T[keyof T]的方式,可以获取到T所有key的类型组成的联合类型; T[keyof K]的方式,获取到的是T中的key且同时存在于K时的类型组成的联合类型; 注意:如果[]中的key有不存在T中的,则是any;因为ts也不知道该key最终是什么类型,所以是any;且也会报错; 302 | #### extends关键词特性 303 | ##### 用于接口,表示继承 304 | ```typescript 305 | interface T1 { 306 | name: string, 307 | } 308 | 309 | interface T2 { 310 | sex: number, 311 | } 312 | 313 | /** 314 | * @example 315 | * T3 = {name: string, sex: number, age: number} 316 | */ 317 | interface T3 extends T1, T2 { 318 | age: number, 319 | } 320 | 321 | ``` 322 | 注意,接口支持多重继承,语法为逗号隔开。如果是type实现继承,则可以使用交叉类型type A = B & C & D。 323 | ##### 表示条件类型,可用于条件判断 324 | ```typescript 325 | /** 326 | * @example 327 | * type A1 = 1 328 | */ 329 | type A1 = 'x' extends 'x' ? 1 : 2; 330 | 331 | /** 332 | * @example 333 | * type A2 = 2 334 | */ 335 | type A2 = 'x' | 'y' extends 'x' ? 1 : 2; 336 | 337 | /** 338 | * @example 339 | * type A3 = 1 | 2 340 | */ 341 | type P = T extends 'x' ? 1 : 2; 342 | type A3 = P<'x' | 'y'> 343 | 344 | ``` 345 | 为什么A2和A3的值不一样? 346 | 347 | - 如果用于简单的条件判断,则是直接判断前面的类型是否可分配给后面的类型 348 | - 若extends前面的类型是泛型,且泛型传入的是联合类型时,则会依次判断该联合类型的所有子类型是否可分配给extends后面的类型(是一个分发的过程)。 349 | 350 | 总结,就是extends前面的参数为联合类型时则会分解(依次遍历所有的子类型进行条件判断)联合类型进行判断。然后将最终的结果组成新的联合类型。 351 | 阻止extends关键词对于联合类型的分发特性 352 | ```typescript 353 | type P = [T] extends ['x'] ? 1 : 2; 354 | /** 355 | * type A4 = 2; 356 | */ 357 | type A4 = P<'x' | 'y'> 358 | 359 | ``` 360 | #### 类型兼容性 361 | 集合论中,如果一个集合A的所有元素在集合B中都存在,则A是B的子集; 362 | 类型系统中,如果一个类型的属性更具体,则该类型是子类型。(因为属性更少则说明该类型约束的更宽泛,是父类型) 363 | **因此,我们可以得出基本的结论:子类型比父类型更加具体,父类型比子类型更宽泛。** 364 | ##### 可赋值性 365 | ```typescript 366 | interface Animal { 367 | name: string; 368 | } 369 | 370 | interface Dog extends Animal { 371 | break(): void; 372 | } 373 | 374 | let a: Animal; 375 | let b: Dog; 376 | 377 | // 可以赋值,子类型更佳具体,可以赋值给更佳宽泛的父类型 378 | a = b; 379 | // 反过来不行 380 | b = a; 381 | 382 | ``` 383 | 从这个例子里可以看出,animal 是一个「更宽泛」的类型,它的属性比较少,所以更「具体」的子类型是可以赋值给它的,因为你是知道 animal 上只有 age 这个属性的,你只会去使用这个属性,dog 上拥有 animal 所拥有的一切类型,赋值给 animal 是不会出现类型安全问题的。 384 | 反之,如果 dog = animal,那么后续使用者会期望 dog 上拥有 bark 属性,当他调用了 dog.bark() 就会引发运行时的崩溃。 385 | 从可赋值性角度来说,子类型是可以赋值给父类型的,也就是 父类型变量 = 子类型变量 是安全的,因为子类型上涵盖了父类型所拥有的的一切属性。 386 | ##### 联合类型 387 | ```typescript 388 | type A = 1 | 2 | 3; 389 | type B = 2 | 3; 390 | let a: A; 391 | let b: B; 392 | 393 | // 不可赋值 394 | b = a; 395 | // 可以赋值 396 | a = b; 397 | 398 | ``` 399 | 是不是A的类型更多,A就是子类型呢?恰恰相反,A此处类型更多但是其表达的类型更宽泛,所以A是父类型,B是子类型。 400 | 因此b = a不成立(父类型不能赋值给子类型),而a = b成立(子类型可以赋值给父类型)。 401 | ##### 函数类型 402 | 函数类型的兼容性判断,要查看 x 是否能赋值给 y,首先看它们的参数列表。 403 | x 的每个参数必须能在 y 里找到对应类型的参数,注意的是参数的名字相同与否无所谓,只看它们的类型。 404 | 这里,x 的每个参数在 y 中都能找到对应的参数类型,所以允许赋值: 405 | 其实也就是说 y 更加宽泛,y是父类,x是子类? 406 | ```typescript 407 | let x = (a: number) => 0; 408 | let y = (b: number, s: string) => 0; 409 | 410 | y = x; // OK 411 | x = y; // Error 不能将类型“(b: number, s: string) => number”分配给类型“(a: number) => number”。 412 | ``` 413 | ##### 类的类型兼容性 414 | 仅仅只有实例成员和方法会相比较,构造函数和静态成员不会被检查: 415 | ```typescript 416 | class Animal { 417 | feet: number; 418 | constructor(name: string, numFeet: number) {} 419 | } 420 | 421 | class Size { 422 | feet: number; 423 | constructor(meters: number) {} 424 | } 425 | 426 | let a: Animal; 427 | let s: Size; 428 | 429 | a = s; // OK 430 | s = a; // OK 431 | ``` 432 | ##### 泛型的类型兼容性 433 | 泛型本身就是不确定的类型,它的表现根据是否被成员使用而不同. 434 | 由于没有被成员使用泛型,所以这里是没问题的。 435 | ```typescript 436 | interface Person { 437 | 438 | } 439 | 440 | let x : Person 441 | let y : Person 442 | 443 | x = y // ok 444 | y = x // ok 445 | ``` 446 | 这里由于泛型 T 被成员 name 使用了,所以类型不再兼容。 447 | ```typescript 448 | interface Person { 449 | name: T 450 | } 451 | 452 | let x : Person 453 | let y : Person 454 | 455 | x = y // 不能将类型“Person”分配给类型“Person”。 456 | y = x // 不能将类型“Person”分配给类型“Person”。 457 | 458 | ``` 459 | ##### 协变 460 | > 协变与逆变(Covariance and contravariance )是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。 461 | 462 | 简单说就是,具有父子关系的多个类型,在通过某种构造关系构造成的新的类型,如果还具有父子关系则是协变的,而关系逆转了(子变父,父变子)就是逆变的。可能听起来有些抽象,下面我们将用更具体的例子进行演示说明: 463 | ```typescript 464 | interface Animal { 465 | name: string; 466 | } 467 | 468 | interface Dog extends Animal { 469 | break(): void; 470 | } 471 | 472 | let Eg1: Animal; 473 | let Eg2: Dog; 474 | // 兼容,可以赋值 475 | Eg1 = Eg2; 476 | 477 | let Eg3: Array 478 | let Eg4: Array 479 | // 兼容,可以赋值 480 | Eg3 = Eg4 481 | 482 | ``` 483 | 通过Eg3和Eg4来看,在Animal和Dog在变成数组后,Array依旧可以赋值给Array,因此对于type MakeArray = Array来说就是协变的。 484 | ##### 逆变 485 | ```typescript 486 | interface Animal { 487 | name: string; 488 | } 489 | 490 | interface Dog extends Animal { 491 | break(): void; 492 | } 493 | 494 | type AnimalFn = (arg: Animal) => void 495 | type DogFn = (arg: Dog) => void 496 | 497 | let Eg1: AnimalFn; 498 | let Eg2: DogFn; 499 | // 不再可以赋值了, 500 | // AnimalFn = DogFn不可以赋值了, Animal = Dog是可以的 501 | Eg1 = Eg2; 502 | // 反过来可以 503 | Eg2 = Eg1; 504 | 505 | ``` 506 | 理论上,Animal = Dog是类型安全的,那么AnimalFn = DogFn也应该类型安全才对,为什么Ts认为不安全呢?看下面的例子: 507 | ```typescript 508 | let animal: AnimalFn = (arg: Animal) => {} 509 | let dog: DogFn = (arg: Dog) => { 510 | arg.break(); 511 | } 512 | 513 | // 假设类型安全可以赋值 514 | animal = dog; 515 | // 那么animal在调用时约束的参数,缺少dog所需的参数,此时会导致错误 516 | animal({name: 'cat'}); 517 | 518 | ``` 519 | 从这个例子看到,如果dog函数赋值给animal函数,那么animal函数在调用时,约束的是参数必须要为Animal类型(而不是Dog),但是animal实际为dog的调用,此时就会出现错误。 520 | 因此,Animal和Dog在进行type Fn = (arg: T) => void构造器构造后,父子关系逆转了,此时成为“逆变”。 521 | ##### 双向协变 522 | Ts在函数参数的比较中实际上默认采取的策略是双向协变:只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 523 | 这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息(典型的就是上述的逆变)。 但是实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式: 524 | ```typescript 525 | // lib.dom.d.ts中EventListener的接口定义 526 | interface EventListener { 527 | (evt: Event): void; 528 | } 529 | // 简化后的Event 530 | interface Event { 531 | readonly target: EventTarget | null; 532 | preventDefault(): void; 533 | } 534 | // 简化合并后的MouseEvent 535 | interface MouseEvent extends Event { 536 | readonly x: number; 537 | readonly y: number; 538 | } 539 | 540 | // 简化后的Window接口 541 | interface Window { 542 | // 简化后的addEventListener 543 | addEventListener(type: string, listener: EventListener) 544 | } 545 | 546 | // 日常使用 547 | window.addEventListener('click', (e: Event) => {}); 548 | window.addEventListener('mouseover', (e: MouseEvent) => {}); 549 | 550 | ``` 551 | #### infer 552 | 主要是用于extends的条件类型中让Ts自己推到类型 553 | ##### infer推导的名称相同并且都处于逆变的位置,则推导的结果将会是交叉类型。 554 | ```typescript 555 | type Bar = T extends { 556 | a: (x: infer U) => void; 557 | b: (x: infer U) => void; 558 | } ? U : never; 559 | 560 | // type T1 = string 561 | type T1 = Bar<{ a: (x: string) => void; b: (x: string) => void }>; 562 | 563 | // type T2 = never 564 | type T2 = Bar<{ a: (x: string) => void; b: (x: number) => void }>; 565 | 566 | ``` 567 | ##### infer推导的名称相同并且都处于协变的位置,则推导的结果将会是联合类型。 568 | ```typescript 569 | type Foo = T extends { 570 | a: infer U; 571 | b: infer U; 572 | } ? U : never; 573 | 574 | // type T1 = string 575 | type T1 = Foo<{ a: string; b: string }>; 576 | 577 | // type T2 = string | number 578 | type T2 = Foo<{ a: string; b: number }>; 579 | 580 | ``` 581 | -------------------------------------------------------------------------------- /images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/n0liu/learning-notes/b11e07c60306b736a1a2658db1e7b1ac03999f5a/images/bg.png -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # 学习笔记 2 | 3 | |目录 | 内容描述 4 | |- | -:| 5 | Markdown | [使用技巧](./MarkDown) 6 | HTML&CSS | [使用技巧](./HTML&CSS) 7 | JavaScript | [使用技巧](./JavaScript/index.md) 8 | TypeScript | [使用技巧](./TypeScript/index.md) 9 | GIT | [使用技巧](./GIT/index.md) 10 | TOOL | [使用技巧](./Tool/index.md) 11 | HTTP | [使用技巧](./HTTP/index.md) --------------------------------------------------------------------------------