├── code ├── babel │ └── index.js ├── ws │ ├── example │ │ ├── client.html │ │ └── server.js │ └── index.js ├── style-loader │ ├── example │ │ ├── index.js │ │ ├── style.css │ │ └── build.js │ ├── index.js │ └── package.json ├── bundle │ ├── example │ │ ├── hello.js │ │ ├── math.js │ │ ├── increment.js │ │ └── index.js │ ├── example.js │ ├── package.json │ ├── output.js │ ├── Readme.md │ └── index.js ├── highlight │ ├── example │ │ ├── main.js │ │ ├── index.html │ │ └── prism.html │ ├── index.js │ └── package.json ├── json-loader │ ├── example │ │ ├── index.js │ │ ├── users.json3 │ │ └── build.js │ ├── Readme.md │ ├── package.json │ └── index.js ├── yaml-loader │ ├── example │ │ ├── users.yaml │ │ └── build.js │ ├── Readme.md │ ├── package.json │ └── index.js ├── html-webpack-plugin │ ├── example │ │ ├── add.js │ │ ├── index.js │ │ └── build.js │ ├── package.json │ ├── Readme.md │ └── index.js ├── serve │ ├── example │ │ ├── about.html │ │ ├── index.html │ │ └── users │ │ │ └── index.html │ ├── package.json │ ├── Readme.md │ └── index.ts ├── ssr │ ├── client.js │ ├── src │ │ └── App.js │ ├── webpack.config.server.js │ ├── webpack.config.js │ ├── package.json │ ├── server.js │ └── build │ │ └── main.js.LICENSE.txt ├── koa │ ├── package.json │ ├── example.js │ ├── debug.md │ ├── index.js │ └── Readme.md ├── native-http-server │ ├── example.js │ ├── Readme.md │ └── index.js ├── vdom │ ├── package.json │ ├── Readme.md │ ├── example │ │ ├── index.html │ │ └── main.js │ └── index.js ├── express │ ├── package.json │ ├── example.js │ ├── index.js │ └── Readme.md ├── http-router │ ├── package.json │ ├── example.js │ ├── index.js │ └── Readme.md ├── pipeline │ ├── example.js │ └── index.js ├── apollo-server │ ├── package.json │ ├── example.mjs │ ├── index.mjs │ └── package-lock.json ├── react │ ├── index.js │ └── Readme.md └── p │ └── limit.js ├── .github └── FUNDING.yml ├── .gitignore ├── package.json └── Readme.md /code/babel/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/ws/example/client.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /code/ws/example/server.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: shfshanyue -------------------------------------------------------------------------------- /code/style-loader/example/index.js: -------------------------------------------------------------------------------- 1 | import './style.css' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode 3 | package-lock.gz 4 | dist -------------------------------------------------------------------------------- /code/bundle/example/hello.js: -------------------------------------------------------------------------------- 1 | module.exports = 'hello, world' -------------------------------------------------------------------------------- /code/highlight/example/main.js: -------------------------------------------------------------------------------- 1 | import Prisma from '../index.js' -------------------------------------------------------------------------------- /code/json-loader/example/index.js: -------------------------------------------------------------------------------- 1 | import users from './users.json3' -------------------------------------------------------------------------------- /code/json-loader/example/users.json3: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": 3 4 | } 5 | ] -------------------------------------------------------------------------------- /code/style-loader/example/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #c8c8c8; 3 | } -------------------------------------------------------------------------------- /code/bundle/example/math.js: -------------------------------------------------------------------------------- 1 | exports.add = (...args) => args.reduce((x, y) => x + y, 0) -------------------------------------------------------------------------------- /code/yaml-loader/example/users.yaml: -------------------------------------------------------------------------------- 1 | users: 2 | - id: 10086 3 | name: shanyue 4 | -------------------------------------------------------------------------------- /code/html-webpack-plugin/example/add.js: -------------------------------------------------------------------------------- 1 | const add = (x, y) => x + y 2 | export default add 3 | -------------------------------------------------------------------------------- /code/bundle/example.js: -------------------------------------------------------------------------------- 1 | const bundle = require('.') 2 | 3 | console.log(bundle('./example/index.js')) -------------------------------------------------------------------------------- /code/bundle/example/increment.js: -------------------------------------------------------------------------------- 1 | const add = require('./math.js').add; 2 | exports.increment = x => add(x, 1) 3 | -------------------------------------------------------------------------------- /code/html-webpack-plugin/example/index.js: -------------------------------------------------------------------------------- 1 | import('./add').then(({ default: add }) => { 2 | console.log(add(3, 4)) 3 | }) -------------------------------------------------------------------------------- /code/highlight/index.js: -------------------------------------------------------------------------------- 1 | export function highlight (text, grammer, language) { 2 | 3 | } 4 | 5 | function tokenize (text, grammer) { 6 | 7 | } -------------------------------------------------------------------------------- /code/serve/example/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | hello, about. 8 | 9 | -------------------------------------------------------------------------------- /code/serve/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | hello, shanyue. 8 | 9 | -------------------------------------------------------------------------------- /code/bundle/example/index.js: -------------------------------------------------------------------------------- 1 | const inc = require('./increment.js').increment; 2 | const hello = require('./hello.js') 3 | const a = 1; 4 | 5 | console.log(inc(a)) 6 | console.log(hello) -------------------------------------------------------------------------------- /code/ssr/client.js: -------------------------------------------------------------------------------- 1 | import { hydrateRoot } from 'react-dom/client' 2 | 3 | import App from './src/App' 4 | 5 | hydrateRoot(document.getElementById('root'), ) 6 | -------------------------------------------------------------------------------- /code/serve/example/users/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | hello, users index page. 8 | 9 | -------------------------------------------------------------------------------- /code/ws/index.js: -------------------------------------------------------------------------------- 1 | const EventEmitter = require('events') 2 | 3 | class WebsocketServer extends EventEmitter { 4 | constructor ({ 5 | server 6 | }) { 7 | this.server = server 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /code/koa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-koa", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "example": "node example.js" 8 | }, 9 | "author": "", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /code/native-http-server/example.js: -------------------------------------------------------------------------------- 1 | const { createServer } = require('.') 2 | 3 | const server = createServer((req, res) => { 4 | res.end('hello, world') 5 | }) 6 | 7 | server.listen(8000, () => { 8 | console.log('Listing 8000') 9 | }) 10 | -------------------------------------------------------------------------------- /code/vdom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vdom", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "example": "serve ." 8 | }, 9 | "author": "shfshanyue", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /code/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-express", 3 | "version": "1.0.0", 4 | "description": "如何实现一个最小版本的 express", 5 | "main": "index.js", 6 | "scripts": { 7 | "example": "node example.js" 8 | }, 9 | "author": "shfshanyue", 10 | "license": "ISC" 11 | } 12 | -------------------------------------------------------------------------------- /code/style-loader/index.js: -------------------------------------------------------------------------------- 1 | module.exports = function(source) { 2 | return ` 3 | function injectCss(css) { 4 | const style = document.createElement('style') 5 | style.appendChild(document.createTextNode(css)) 6 | document.head.appendChild(style) 7 | } 8 | 9 | injectCss(\`${source}\`) 10 | ` 11 | } -------------------------------------------------------------------------------- /code/json-loader/Readme.md: -------------------------------------------------------------------------------- 1 | # json loader 2 | 3 | ## 山月的代码实现 4 | 5 | 代码置于 [shfshanyue/mini-code:code/json-loader](https://github.com/shfshanyue/mini-code/blob/master/code/json-loader/index.js) 6 | 7 | 可直接读源码,基本每一行都有注释。 8 | 9 | 使用 `npm run example` 或者 `node example/build.js` 可运行示例代码 10 | 11 | ``` bash 12 | $ npm run example 13 | ``` -------------------------------------------------------------------------------- /code/yaml-loader/Readme.md: -------------------------------------------------------------------------------- 1 | # yaml loader 2 | 3 | ## 山月的代码实现 4 | 5 | 代码置于 [shfshanyue/mini-code:code/yaml-loader](https://github.com/shfshanyue/mini-code/blob/master/code/json-loader/index.js) 6 | 7 | 可直接读源码,基本每一行都有注释。 8 | 9 | 使用 `npm run example` 或者 `node example/build.js` 可运行示例代码 10 | 11 | ``` bash 12 | $ npm run example 13 | ``` -------------------------------------------------------------------------------- /code/json-loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-json-loader", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "example": "cd example && node build.js" 11 | }, 12 | "author": "shfshanyue", 13 | "license": "ISC" 14 | } 15 | -------------------------------------------------------------------------------- /code/http-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-http-router", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "example": "node example" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "path-to-regexp": "^6.2.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /code/style-loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-style-loader", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "example": "cd example && node build.js" 11 | }, 12 | "author": "shfshanyue", 13 | "license": "ISC" 14 | } 15 | -------------------------------------------------------------------------------- /code/highlight/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-highlight", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "shfshanyue", 13 | "license": "ISC" 14 | } 15 | -------------------------------------------------------------------------------- /code/html-webpack-plugin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-html-webpack-plugin", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "example": "cd example && node build.js" 11 | }, 12 | "author": "shfshanyue", 13 | "license": "ISC" 14 | } 15 | -------------------------------------------------------------------------------- /code/pipeline/example.js: -------------------------------------------------------------------------------- 1 | const pipeline = require('.'); 2 | const fs = require('fs'); 3 | const zlib = require('zlib'); 4 | 5 | pipeline( 6 | fs.createReadStream('./package-lock.json'), 7 | zlib.createGzip(), 8 | fs.createWriteStream('package-lock.gz', { highWaterMark: 16 }), 9 | ).then(o => { 10 | console.log('SUCCESS') 11 | }).catch(e => { 12 | console.log('ERROR') 13 | }) 14 | -------------------------------------------------------------------------------- /code/ssr/src/App.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | export default function App({ title }) { 4 | const [value, setValue] = useState('hello') 5 | 6 | return ( 7 |
8 |

{title}

9 |
10 | setValue(e.target.value)} /> 11 | {value} 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /code/yaml-loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-yaml-loader", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "example": "cd example && node build.js" 11 | }, 12 | "author": "shfshanyue", 13 | "license": "ISC", 14 | "dependencies": { 15 | "js-yaml": "^4.1.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /code/vdom/Readme.md: -------------------------------------------------------------------------------- 1 | # WIP 2 | 3 | 1. `element.textContent = 'hello'` 4 | 1. `document.createElement(tagName)` 5 | 1. `element.removeChild(child)` 6 | 1. [parentNode.insertBefore(newNode, referenceNode)](https://developer.mozilla.org/zh-CN/docs/Web/API/Node/insertBefore) 7 | 8 | ## 关于 vDOM 的开源实现 9 | 10 | + [snabbdom](https://github.com/snabbdom/snabbdom) 11 | + [virtual-dom](https://github.com/Matt-Esch/virtual-dom) 12 | -------------------------------------------------------------------------------- /code/apollo-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-server", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.mjs", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "shfshanyue", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@graphql-tools/schema": "^8.3.2", 13 | "graphql": "^16.3.0", 14 | "raw-body": "^2.5.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /code/yaml-loader/index.js: -------------------------------------------------------------------------------- 1 | // loader 文档见: https://webpack.js.org/api/loaders/ 2 | // 3 | // 使用 loader 写一个 yaml-loader,最能理解 webpack 中 loader 以及 parser(Rule.type) 的作用 4 | const yaml = require('js-yaml'); 5 | 6 | module.exports = function (source) { 7 | // 如果 type:json,则最终返回一个 json 数据即可 8 | return JSON.stringify(yaml.load(source)) 9 | 10 | // 如果 type 不填写,即为默认的 type:javascript/auto,则最终返回 cjs/es 数据即可 11 | // return `module.exports ${JSON.stringify(yaml.load(source))}` 12 | } -------------------------------------------------------------------------------- /code/serve/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serve", 3 | "version": "1.0.0", 4 | "description": "> 仓库:[mini-express](https://github.com/shfshanyue/mini-code/tree/master/code/express/)", 5 | "main": "index.ts", 6 | "directories": { 7 | "example": "example" 8 | }, 9 | "scripts": { 10 | "example": "npx ts-node index.ts example -p 8000" 11 | }, 12 | "author": "shfshanyue", 13 | "license": "ISC", 14 | "dependencies": { 15 | "arg": "^5.0.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /code/http-router/example.js: -------------------------------------------------------------------------------- 1 | const http = require('http') 2 | const router = require('.') 3 | 4 | // 参数带有,将解析 userId 5 | router.get('/api/users/:userId', (req, res) => { 6 | res.end(JSON.stringify({ 7 | userId: req.params.userId 8 | })) 9 | }) 10 | 11 | // 前缀路由,匹配所有 /v2 开头的路由 12 | router.use('/v2', (req, res) => { 13 | res.end('hello, v2') 14 | }) 15 | 16 | const server = http.createServer((req, res) => { 17 | router.lookup(req, res) 18 | }) 19 | 20 | server.listen(3000) 21 | -------------------------------------------------------------------------------- /code/ssr/webpack.config.server.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | mode: 'none', 5 | target: 'node', 6 | entry: [ 7 | path.resolve(__dirname, './server.js') 8 | ], 9 | output: { 10 | path: path.resolve(__dirname, './build'), 11 | filename: 'server.js', 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | use: 'babel-loader', 18 | exclude: /node_modules/, 19 | }, 20 | ], 21 | }, 22 | } -------------------------------------------------------------------------------- /code/vdom/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | vDOM Render 8 | 9 | 10 | 11 |

示例一:可正常渲染元素

12 |
13 | 14 |

示例二:带有 key 的列表

15 |
16 | 17 | -------------------------------------------------------------------------------- /code/yaml-loader/example/build.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | const compiler = webpack({ 4 | entry: './users.yaml', 5 | mode: 'none', 6 | output: { 7 | filename: '[name].[contenthash:8].js', 8 | clean: true 9 | }, 10 | module: { 11 | rules: [{ 12 | // webpack 将会自动 require('..') 作为 loader 13 | use: '..', 14 | test: /\.yaml$/, 15 | type: 'json' 16 | }] 17 | } 18 | }) 19 | 20 | compiler.run(() => { 21 | console.log('DONE') 22 | }) -------------------------------------------------------------------------------- /code/html-webpack-plugin/example/build.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const HtmlWebpackPlugin = require('..'); 3 | 4 | const compiler = webpack({ 5 | entry: './index.js', 6 | output: { 7 | filename: '[name].[contenthash:8].js', 8 | clean: true 9 | }, 10 | optimization: { 11 | runtimeChunk: true 12 | }, 13 | plugins: [ 14 | new HtmlWebpackPlugin({ 15 | title: '山月的主页' 16 | }) 17 | ], 18 | }) 19 | 20 | compiler.run(() => { 21 | console.log('DONE') 22 | }) -------------------------------------------------------------------------------- /code/bundle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-bundle", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "main": "index.js", 7 | "scripts": { 8 | "example": "node example.js > output.js" 9 | }, 10 | "author": "shfshanyue", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@babel/generator": "^7.15.0", 14 | "@babel/parser": "^7.15.3", 15 | "@babel/traverse": "^7.15.0", 16 | "@babel/types": "^7.15.0", 17 | "lodash": "^4.17.21" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /code/koa/example.js: -------------------------------------------------------------------------------- 1 | const Application = require('.') 2 | 3 | const app = new Application() 4 | 5 | app.use(async (ctx, next) => { 6 | console.log('Middleware 1 Start') 7 | await next() 8 | console.log('Middleware 1 End') 9 | }) 10 | 11 | app.use(async (ctx, next) => { 12 | console.log('Middleware 2 Start') 13 | await next() 14 | console.log('Middleware 2 End') 15 | 16 | ctx.body = 'hello, world' 17 | }) 18 | 19 | 20 | app.listen(7000, () => { 21 | console.log('Listing 7000...') 22 | }) 23 | -------------------------------------------------------------------------------- /code/json-loader/example/build.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | 3 | const compiler = webpack({ 4 | entry: './index.js', 5 | mode: 'none', 6 | output: { 7 | filename: '[name].[contenthash:8].js', 8 | clean: true 9 | }, 10 | module: { 11 | rules: [{ 12 | // webpack 将会自动 require('..') 做为 loader 13 | use: '..', 14 | // 为了避免与内置的 json-loader 冲突,因此此处命名为 json3 15 | test: /\.json3$/, 16 | }] 17 | } 18 | }) 19 | 20 | compiler.run(() => { 21 | console.log('DONE') 22 | }) -------------------------------------------------------------------------------- /code/json-loader/index.js: -------------------------------------------------------------------------------- 1 | // loader 文档见: https://webpack.js.org/api/loaders/ 2 | // 官方 json-loader 可见 https://github.com/webpack-contrib/json-loader,尽管目前已内置 3 | // 4 | // 使用 loader 写一个 json-loader,最能理解 webpack 中 loader 的作用 5 | 6 | module.exports = function (source) { 7 | // 为了避免 JSON 有语法错误,所以先 parse/stringify 一遍 8 | return `module.exports = ${JSON.stringify(JSON.parse(source))}` 9 | 10 | // 写成 ESM 格式也可以,但是 webpack 内部还需要将 esm 转化为 cjs,为了降低复杂度,直接使用 cjs 11 | // return `export default ${JSON.stringify(JSON.parse(source))}` 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mini-code", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "workspaces": [ 10 | "code/*" 11 | ], 12 | "author": "", 13 | "license": "ISC", 14 | "devDependencies": { 15 | "nodemon": "^2.0.12", 16 | "serve": "^12.0.1" 17 | }, 18 | "dependencies": { 19 | "@graphql-tools/schema": "^8.3.2", 20 | "raw-body": "^2.5.0", 21 | "webpack": "^5.58.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /code/highlight/example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 9 | 10 |
11 |     
12 | const user = {
13 |   id: 'shfshanyue',
14 |   name: '山月',
15 |   repo: 'shfshanyue/minicode'
16 | }
17 |     
18 |   
19 | 20 | 21 | -------------------------------------------------------------------------------- /code/apollo-server/example.mjs: -------------------------------------------------------------------------------- 1 | import GraphQLServer from './index.mjs' 2 | 3 | const typeDefs = ` 4 | type Todo { 5 | title: String 6 | author: String 7 | } 8 | 9 | type Query { 10 | todos: [Todo] 11 | } 12 | ` 13 | 14 | const books = [ 15 | { 16 | title: '三国演义', 17 | author: '施耐庵', 18 | }, 19 | { 20 | title: '西游记', 21 | author: '罗贯中', 22 | }, 23 | ] 24 | 25 | const resolvers = { 26 | Query: { 27 | books: () => { 28 | return books 29 | } 30 | } 31 | } 32 | 33 | const app = new GraphQLServer({ typeDefs, resolvers }) 34 | 35 | app.listen(4000) 36 | -------------------------------------------------------------------------------- /code/style-loader/example/build.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const HtmlWebpackPlugin = require('mini-html-webpack-plugin') 3 | 4 | const compiler = webpack({ 5 | entry: './index.js', 6 | mode: 'none', 7 | output: { 8 | filename: 'main.js', 9 | clean: true 10 | }, 11 | module: { 12 | rules: [{ 13 | // webpack 将会自动 require('..') 最为 loader 14 | use: '..', 15 | // 为了避免与内置的 json-loader 冲突,因此此处命名为 json3 16 | test: /\.css$/, 17 | }] 18 | }, 19 | plugins:[ 20 | new HtmlWebpackPlugin(), 21 | ] 22 | }) 23 | 24 | compiler.run(() => { 25 | console.log('DONE') 26 | }) 27 | -------------------------------------------------------------------------------- /code/ssr/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const isProduction = process.env.NODE_ENV === 'production'; 4 | module.exports = { 5 | devtool: isProduction ? 'source-map' : 'cheap-module-source-map', 6 | mode: isProduction ? 'production' : 'development', 7 | target: 'web', 8 | entry: [ 9 | path.resolve(__dirname, './client.js') 10 | ], 11 | output: { 12 | path: path.resolve(__dirname, './build'), 13 | filename: 'main.js', 14 | }, 15 | module: { 16 | rules: [ 17 | { 18 | test: /\.js$/, 19 | use: 'babel-loader', 20 | exclude: /node_modules/, 21 | }, 22 | ], 23 | }, 24 | } -------------------------------------------------------------------------------- /code/highlight/example/prism.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Prism Demo 8 | 9 | 10 | 11 |
12 |     
13 | const user = {
14 |   id: 'shfshanyue',
15 |   name: '山月',
16 |   repo: 'shfshanyue/minicode'
17 | }
18 |     
19 |   
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /code/html-webpack-plugin/Readme.md: -------------------------------------------------------------------------------- 1 | # 如何实现一个最小版 html-webpack-plugin 2 | 3 | > 仓库:[mini-html-webpack-plugin](https://github.com/shfshanyue/mini-code/tree/master/code/html-webpack-plugin) 4 | 5 | 大家好,我是山月。 6 | 7 | `html-webpack-plugin` 是使用 webpack 的前端业务代码中必备的插件,用以将 Javascript/CSS 等资源注入 html 中。 8 | 9 | 这里山月实现一个最简化的 `html-webpack-plugin`。既可以了解 webpack 是如何写一个 plugin,又能够熟悉 webpack 打包的内部流程。 10 | 11 | ## 山月的代码实现 12 | 13 | 代码置于 [shfshanyue/mini-code:code/html-webpack-plugin](https://github.com/shfshanyue/mini-code/blob/master/code/html-webpack-plugin/index.js) 14 | 15 | 可直接读源码,基本每一行都有注释。 16 | 17 | 使用 `npm run example` 或者 `node example` 可运行示例代码 18 | 19 | ``` bash 20 | $ npm run example 21 | ``` 22 | 23 | ## 源码实现 24 | -------------------------------------------------------------------------------- /code/pipeline/index.js: -------------------------------------------------------------------------------- 1 | function _write (stream, data) { 2 | return new Promise((resolve, reject) => { 3 | if (stream.write(data)) { 4 | resolve() 5 | } 6 | stream.on('drain', resolve) 7 | }) 8 | } 9 | 10 | async function simeplePipeline (...streams) { 11 | let ret = streams[0] 12 | for (let i = 1; i < streams.length; i++) { 13 | const writeStream = streams[i] 14 | for await (const chunk of ret) { 15 | await _write(writeStream, chunk) 16 | } 17 | ret = writeStream 18 | } 19 | return ret 20 | } 21 | 22 | function eos () {} 23 | 24 | function pump () {} 25 | 26 | async function pipeline (...streams) { 27 | 28 | } 29 | 30 | module.exports = { 31 | pipeline, 32 | simeplePipeline 33 | } -------------------------------------------------------------------------------- /code/native-http-server/Readme.md: -------------------------------------------------------------------------------- 1 | ## 功能 2 | 3 | 1. 支持粗糙的请求报文解析与相应报文构建并发送 4 | 1. `res.end`: 支持正常发送数据 5 | 1. `res.write`: 支持 `chunksfer encoding` 6 | 1. `keepAlive` 7 | 8 | ## 演示与示例 9 | 10 | ### hello, world 11 | 12 | ``` js 13 | const { createServer } = require('.') 14 | 15 | const server = createServer((req, res) => { 16 | res.end('hello, world') 17 | }) 18 | 19 | server.listen(8000, () => { 20 | console.log('Listing 8000') 21 | }) 22 | ``` 23 | 24 | ### transfer encoding 25 | 26 | ``` js 27 | const { createServer } = require('.') 28 | 29 | const server = createServer((req, res) => { 30 | res.write('hello, world') 31 | res.end() 32 | }) 33 | 34 | server.listen(8000, () => { 35 | console.log('Listing 8000') 36 | }) 37 | ``` 38 | -------------------------------------------------------------------------------- /code/serve/Readme.md: -------------------------------------------------------------------------------- 1 | # 手动实现一个静态资源服务器 2 | 3 | > 仓库:[mini-serve](https://github.com/shfshanyue/mini-code/tree/master/code/serve/) 4 | 5 | 可参考源码 [serve-handler](https://github.com/vercel/serve-handler) 6 | 7 | 可实现以下功能: 8 | 9 | + 命令行的方式,可传入端口号及目标目录 10 | + 处理 404 11 | + 流式返回资源,处理 Content-Length 12 | + 处理目录,如果请求路径是目录,则自动请求其目录中的 index.html 13 | 14 | 以下功能选做: 15 | 16 | + rewrites 17 | + redirects 18 | + cleanUrls 19 | + trailingSlash 20 | + etag 21 | + symlink 22 | 23 | ## 山月的代码实现 24 | 25 | 代码置于 [shfshanyue/mini-code:code/serve](https://github.com/shfshanyue/mini-code/blob/master/code/serve/index.ts) 26 | 27 | 可直接读源码,基本每一行都有注释。 28 | 29 | 使用 `npm run example` 可运行示例代码 30 | 31 | ``` bash 32 | # npx ts-node index.ts example -p 8000 33 | $ npm run example 34 | ``` 35 | 36 | 访问: 37 | 38 | + 能够正确访问目录: 39 | + 能够正确访问文件: -------------------------------------------------------------------------------- /code/ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ssr", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "server.js", 6 | "scripts": { 7 | "dev": "export NODE_ENV=development && webpack && webpack -c webpack.config.server.js && node build/server.js", 8 | "build": "webpack && webpack -c webpack.config.server.js" 9 | }, 10 | "author": "shfshanyue", 11 | "license": "ISC", 12 | "dependencies": { 13 | "@babel/preset-react": "^7.17.12", 14 | "babel-loader": "^8.2.5", 15 | "babel-preset-react-app": "^10.0.1", 16 | "express": "^4.18.1", 17 | "react": "^18.1.0", 18 | "react-dom": "^18.1.0", 19 | "react-error-boundary": "^3.1.4", 20 | "serve-handler": "^6.1.3", 21 | "webpack": "^5.72.1" 22 | }, 23 | "devDependencies": { 24 | "webpack-cli": "^4.9.2" 25 | }, 26 | "babel": { 27 | "presets": [ 28 | [ 29 | "react-app", 30 | { 31 | "runtime": "automatic" 32 | } 33 | ] 34 | ] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /code/express/example.js: -------------------------------------------------------------------------------- 1 | const express = require('.') 2 | 3 | const app = express() 4 | const sleep = s => new Promise((resolve) => setTimeout(resolve, s)) 5 | 6 | app.get('/wait', (req, res) => { 7 | sleep(3000).then(() => res.end('hello, world')) 8 | }) 9 | 10 | app.use('/api', 11 | (req, res, next) => { 12 | // 应用中间件 A 13 | console.log('Application Level Middleware: A') 14 | next() 15 | }, 16 | (req, res, next) => { 17 | // 应用中间件 B 18 | console.log('Application Level Middleware: B') 19 | next() 20 | } 21 | ) 22 | 23 | app.get('/api', 24 | (req, res, next) => { 25 | // 路由中间件 A 26 | console.log('Route Level Middleware: C') 27 | next() 28 | }, 29 | (req, res, next) => { 30 | // 路由中间件 A 31 | console.log('Route Level Middleware: D') 32 | res.end('hello, world') 33 | } 34 | ) 35 | 36 | app.get('/api/users/:id', (req, res) => { 37 | res.end(JSON.stringify({ userId: req.params.id })) 38 | }) 39 | 40 | app.listen(3000, () => console.log('Listening 3000')) 41 | -------------------------------------------------------------------------------- /code/ssr/server.js: -------------------------------------------------------------------------------- 1 | import http from 'node:http' 2 | 3 | import { renderToString, renderToPipeableStream } from 'react-dom/server' 4 | import handler from 'serve-handler' 5 | 6 | import App from './src/App' 7 | 8 | const server = http.createServer((req, res) => { 9 | if (req.url === '/') { 10 | res.end( 11 | ` 12 | 13 | 14 | 15 | 16 | 17 | Document 18 | 19 | 20 |
21 | ${renderToString()} 22 |
23 | 24 | 25 | 26 | ` 27 | ) 28 | } else { 29 | handler(req, res, { 30 | public: './build' 31 | }) 32 | } 33 | }) 34 | 35 | server.listen(3000) 36 | -------------------------------------------------------------------------------- /code/apollo-server/index.mjs: -------------------------------------------------------------------------------- 1 | import http from 'node:http' 2 | import getRawBody from 'raw-body' 3 | import { graphql } from 'graphql' 4 | import { makeExecutableSchema } from '@graphql-tools/schema' 5 | 6 | class GraphQLServer { 7 | constructor ({ typeDefs, resolvers }) { 8 | this.schema = makeExecutableSchema({ 9 | typeDefs, 10 | resolvers 11 | }) 12 | } 13 | 14 | callback () { 15 | return async (req, res) => { 16 | const buffer = await getRawBody(req) 17 | const { 18 | query, 19 | operationName, 20 | variables 21 | } = JSON.parse(buffer.toString()) 22 | const result = await graphql({ 23 | schema: this.schema, 24 | source: query, 25 | variableValues: variables, 26 | operationName 27 | }) 28 | res.end(JSON.stringify(result)) 29 | } 30 | } 31 | 32 | listen (...args) { 33 | const server = http.createServer(this.callback()) 34 | server.listen(...args) 35 | } 36 | } 37 | 38 | export default GraphQLServer 39 | -------------------------------------------------------------------------------- /code/ssr/build/main.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * @license React 3 | * react-dom.production.min.js 4 | * 5 | * Copyright (c) Facebook, Inc. and its affiliates. 6 | * 7 | * This source code is licensed under the MIT license found in the 8 | * LICENSE file in the root directory of this source tree. 9 | */ 10 | 11 | /** 12 | * @license React 13 | * react-jsx-runtime.production.min.js 14 | * 15 | * Copyright (c) Facebook, Inc. and its affiliates. 16 | * 17 | * This source code is licensed under the MIT license found in the 18 | * LICENSE file in the root directory of this source tree. 19 | */ 20 | 21 | /** 22 | * @license React 23 | * react.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** 32 | * @license React 33 | * scheduler.production.min.js 34 | * 35 | * Copyright (c) Facebook, Inc. and its affiliates. 36 | * 37 | * This source code is licensed under the MIT license found in the 38 | * LICENSE file in the root directory of this source tree. 39 | */ 40 | -------------------------------------------------------------------------------- /code/http-router/index.js: -------------------------------------------------------------------------------- 1 | // [path-to-regexp](https://github.com/pillarjs/path-to-regexp),用以将 `/user/:name` 类参数路由转化为正则表达式 2 | const { match } = require('path-to-regexp') 3 | 4 | const router = { 5 | routes: [], 6 | // 注册路由,此时路由为前缀路由,将匹配该字符串的所有前缀与 http method 7 | use (path, handleRequest, options) { 8 | // 用以匹配请求路径函数,如果匹配成功则返回匹配成功的参数,否则返回 false 9 | // user/:id -> users/18 (id=18) 10 | // end:false -> 匹配前缀 11 | const matchRoute = match(path, { decode: decodeURIComponent, end: false, ...options }) 12 | 13 | // 注册路由,整理数据结构添加入路由数组 14 | this.routes.push({ match: matchRoute, handleRequest, method: options?.method || 'GET' }) 15 | }, 16 | // 注册路由,请求方法为 GET 17 | get (path, handleRequest) { 18 | return this.use(path, handleRequest, { end: true }) 19 | }, 20 | // 注册路由,请求方法为 POST 21 | post (path, handleRequest) { 22 | return this.use(path, handleRequest, { end: true, method: 'POST' }) 23 | }, 24 | // 入口函数 25 | lookup (req, res) { 26 | // 遍历路由,找到匹配路由,并解析路由参数 27 | const route = this.routes.find(route => (req.params = route.match(req.url)?.params) && req.method === route.method) 28 | if (route) { 29 | // 找到路由时,处理该路由的处理逻辑 30 | route.handleRequest(req, res) 31 | } else { 32 | // 如果找不到,返回 404 33 | res.statusCode = 404 34 | res.end('NOT FOUND SHANYUE') 35 | } 36 | } 37 | } 38 | 39 | module.exports = router 40 | -------------------------------------------------------------------------------- /code/react/index.js: -------------------------------------------------------------------------------- 1 | function dispatchAction (fiber, queue, action) { 2 | const eventTime = a 3 | } 4 | 5 | function scheduleUpdateOnFiber (fiber, lane, eventTime) { 6 | 7 | } 8 | 9 | function beginWork () { 10 | 11 | } 12 | 13 | function workLoopConcurrent() { 14 | while (workInProgress !== null && !shouldYield()) { 15 | performUnitOfWork(workInProgress); 16 | } 17 | } 18 | 19 | 20 | function performSyncWorkOnRoot() { 21 | const lanes = getNextLanes(root, NoLanes) 22 | } 23 | 24 | function begineWork(current, workInProgress) { 25 | if (current !== null) { 26 | // 当 current 不为 null 时,说明该节点已经挂载,最大可能地复用旧节点数据 27 | return bailoutOnAlreadyFinishedWork( 28 | current, 29 | workInProgress, 30 | renderLanes, 31 | ); 32 | } else { 33 | didReceiveUpdate = false; 34 | } 35 | 36 | switch (workInProgress.tag) { 37 | } 38 | } 39 | 40 | function reconcileChildren( 41 | current: Fiber | null, 42 | workInProgress: Fiber, 43 | nextChildren: any, 44 | renderLanes: Lanes 45 | ) { 46 | if (current === null) { 47 | // 对于mount的组件 48 | workInProgress.child = mountChildFibers( 49 | workInProgress, 50 | null, 51 | nextChildren, 52 | renderLanes, 53 | ); 54 | } else { 55 | // 对于update的组件 56 | workInProgress.child = reconcileChildFibers( 57 | workInProgress, 58 | current.child, 59 | nextChildren, 60 | renderLanes, 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /code/koa/debug.md: -------------------------------------------------------------------------------- 1 | # 如何阅读及调试 koa 源码 2 | 3 | 由于 `koa` 无需通过打包进行发布,当我们 `import/require koa` 时,直接引用的是源码,而非打包后代码,因此**阅读 koa 源码无需克隆其仓库**。 4 | 5 | 在任意一个地方,写一个关于 `koa` 的示例,断点调试即可。 6 | 7 | ``` js 8 | const Koa = require('koa') 9 | const app = new Koa() 10 | 11 | app.use(ctx => { 12 | ctx.body = 'Hello Koa' 13 | }) 14 | 15 | app.listen(3000) 16 | ``` 17 | 18 | ## 带着疑问阅读源码 19 | 20 | 可根据[我的 koa 代码示例](https://github.com/shfshanyue/node-examples/blob/master/node-server/koa/index.js)及以下疑问,来调试及阅读 koa 源码。 21 | 22 | 1. 了解 koa 的洋葱模型核心,即 koa-compose 23 | 2. koa 是如何捕获异常的 (基于 event-emitter) 24 | 3. koa 是如何处理状态码的 (ctx.body) 25 | 4. koa 是如何发送 JSON 数据的 26 | 5. koa 是如何发送 Stream 数据的 27 | 6. koa 是如何处理 querystring 的 28 | 7. koa context 是如何代理 request/response 的 29 | 30 | ## 断点调试位置 31 | 32 | ![koa-body](https://cdn.jsdelivr.net/gh/shfshanyue/assets@master/src/koa-body.fvayb93ecuw.png) 33 | 34 | 1. [`lib/application.js#144 => callback()`](https://github.com/koajs/koa/blob/2.13.4/lib/application.js#L144): 可从此进入 `koa-compose` 中,并在其中打断点,观察洋葱模型的核心。 35 | 1. [`lib/application.js#231 => respond()`](https://github.com/koajs/koa/blob/2.13.4/lib/application.js#L231): 最后的中间件兜底函数 36 | 1. [`lib/response.js#135 => set body()`](https://github.com/koajs/koa/blob/2.13.4/lib/response.js#L135): 重中之重的函数,可以在该函数打点。当 `ctx.body = content` 时,根据 `getter/setter` 函数自动设置响应头 `Content-Type`、`Content-Length` 以及 `Status Code` 等 37 | 1. [`lib/request.js#455` => get ip()`](https://github.com/koajs/koa/blob/2.13.4/lib/request.js#L455): 了解如何获取 IP 地址 38 | -------------------------------------------------------------------------------- /code/bundle/output.js: -------------------------------------------------------------------------------- 1 | 2 | // 统一扔到块级作用域中,避免污染全局变量 3 | // 为了方便,这里使用 {},而不用 IIFE 4 | // 5 | // 以下代码为打包的三个重要步骤: 6 | // 1. 构建 modules 7 | // 2. 构建 webpackRequire,加载模块,模拟 CommonJS 中的 require 8 | // 3. 运行入口函数 9 | { 10 | // 1. 构建 modules 11 | const modules = [ 12 | 13 | (function (exports, require, module) { 14 | const inc = require(1).increment; 15 | 16 | const hello = require(3); 17 | 18 | const a = 1; 19 | console.log(inc(a)); 20 | console.log(hello); 21 | }), 22 | (function (exports, require, module) { 23 | const add = require(2).add; 24 | 25 | exports.increment = x => add(x, 1); 26 | }), 27 | (function (exports, require, module) { 28 | exports.add = (...args) => args.reduce((x, y) => x + y, 0); 29 | }), 30 | (function (exports, require, module) { 31 | module.exports = 'hello, world'; 32 | }) 33 | ] 34 | 35 | // 模块缓存,所有模块都仅仅会加载并执行一次 36 | const cacheModules = {} 37 | 38 | // 2. 加载模块,模拟代码中的 require 函数 39 | // 打包后,实际上根据模块的 ID 加载,并对 module.exports 进行缓存 40 | function webpackRequire(moduleId) { 41 | const cachedModule = cacheModules[moduleId] 42 | if (cachedModule) { 43 | return cachedModule.exports 44 | } 45 | const targetModule = { exports: {} } 46 | modules[moduleId](targetModule.exports, webpackRequire, targetModule) 47 | cacheModules[moduleId] = targetModule 48 | return targetModule.exports 49 | } 50 | 51 | // 3. 运行入口函数 52 | webpackRequire(0) 53 | } 54 | 55 | -------------------------------------------------------------------------------- /code/vdom/example/main.js: -------------------------------------------------------------------------------- 1 | import { h, patch } from '../index.js' 2 | 3 | // 示例一: 4 | // 可正常渲染元素 5 | { 6 | const list = h('div', { 7 | style: { 8 | border: '1px solid #888', 9 | padding: '4px' 10 | }, 11 | }, [ 12 | h('div', null, [ 13 | h('div', null, [ 14 | 'Author: ', 15 | h('a', { href: "https://github.com/shfshanyue" }, "shanyue") 16 | ]) 17 | ]), 18 | ]) 19 | 20 | const container = document.getElementById('app1') 21 | patch(container, list) 22 | } 23 | 24 | 25 | // 示例二: 26 | // 带有 key 的列表 27 | { 28 | const list = h('div', { 29 | style: { 30 | border: '1px solid #888', 31 | padding: '4px' 32 | }, 33 | }, [ 34 | h('div', { key: 1 }, 'Demo 1'), 35 | h('div', { key: 2 }, 'Demo 2'), 36 | h('div', { key: 3 }, 'Demo 3'), 37 | h('div', { key: 4 }, 'Demo 4') 38 | ]) 39 | 40 | const container = document.getElementById('app2') 41 | patch(container, list) 42 | } 43 | 44 | // 示例三: 45 | // 计数器 46 | { 47 | let vnode 48 | const data = { 49 | count: 0 50 | } 51 | 52 | function Counter (data) { 53 | const handleClick = () => { 54 | data.count++; 55 | render() 56 | } 57 | return h('div', null, [ 58 | h('div', null, data.count), 59 | h('button', { onClick: handleClick }, '+1') 60 | ]) 61 | } 62 | 63 | function render () { 64 | patch(vnode, Counter(data)) 65 | } 66 | const container = document.getElementById('app3') 67 | patch(container, list) 68 | } 69 | -------------------------------------------------------------------------------- /code/p/limit.js: -------------------------------------------------------------------------------- 1 | // 实现一个 Limit 2 | class Limit { 3 | constructor ({ concurrency }) { 4 | this.concurrency = concurrency 5 | this.queue = [] 6 | this.count = 0 7 | } 8 | 9 | enqueue (fn) { 10 | return new Promise((resolve, reject) => { 11 | this.queue.push({ fn, resolve, reject }) 12 | }) 13 | } 14 | 15 | dequeue () { 16 | if (this.count < this.limit && this.queue.length) { 17 | const { fn, resolve, reject } = this.queue.shift() 18 | this.run(fn).then(resolve).catch(reject) 19 | } 20 | } 21 | 22 | async run (fn) { 23 | this.count++ 24 | const value = await fn() 25 | this.count-- 26 | this.dequeue() 27 | return value 28 | } 29 | 30 | exec (fn) { 31 | if (this.count < this.limit) { 32 | return this.run(fn) 33 | } else { 34 | return this.enqueue(fn) 35 | } 36 | } 37 | } 38 | 39 | const limit = new Limit({ concurrency: 1 }) 40 | Promise.all([ 41 | limit.exec(() => Promise.resolve(1)), 42 | limit.exec(() => Promise.resolve(1)), 43 | limit.exec(() => Promise.resolve(1)) 44 | ]) 45 | 46 | // 根据 Limit 来实现一个 Request 47 | class Request { 48 | constructor ({ concurrency }) { 49 | this.limit = new Limit({ concurrency }) 50 | } 51 | 52 | get (url) { 53 | return this.limit.exec(() => fetch(url)) 54 | } 55 | } 56 | 57 | // 如果把二者合在一起,全部代码为 58 | class Request { 59 | constructor ({ concurrency }) { 60 | this.concurrency = concurrency 61 | this.count = 0 62 | this.queue = [] 63 | } 64 | 65 | push (url) { 66 | return new Promise((resolve, reject) => this.queue.push([resolve, reject, url])) 67 | } 68 | 69 | next () { 70 | if (this.count < this.concurrency && this.queue.length) { 71 | const [resolve, reject, url] = this.queue.shift() 72 | this.run(url).then(resolve).catch(reject) 73 | } 74 | } 75 | 76 | async run (url) { 77 | this.count++ 78 | const value = await fetch(url) 79 | this.count-- 80 | this.next() 81 | return value 82 | } 83 | 84 | get (url) { 85 | if (this.count < this.concurrency) { 86 | return this.run(url) 87 | } else { 88 | return this.push(url) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /code/koa/index.js: -------------------------------------------------------------------------------- 1 | // [http 模块](https://nodejs.org/api/http.html),构建 Node 框架的核心 API 2 | const http = require('http') 3 | 4 | // koa 团队通过额外实现一个库: [koa-compose](https://github.com/koajs/compose),来完成洋葱模型的核心,尽管 koa-compose 的核心代码只有十几行 5 | // 以下是洋葱模型的核心实现,可参考 [简述 koa 的中间件原理,手写 koa-compose 代码](https://github.com/shfshanyue/Daily-Question/issues/643) 6 | function compose (middlewares) { 7 | return ctx => { 8 | const dispatch = (i) => { 9 | const middleware = middlewares[i] 10 | if (i === middlewares.length) { 11 | return 12 | } 13 | // 14 | // app.use((ctx, next) => {}) 15 | // 取出当前中间件,并执行 16 | // 当在中间件中调用 next() 时,此时将控制权交给下一个中间件,也是洋葱模型的核心 17 | // 如果中间件未调用 next(),则接下来的中间件将不会执行 18 | return middleware(ctx, () => dispatch(i+1)) 19 | } 20 | // 从第一个中间件开始执行 21 | return dispatch(0) 22 | } 23 | } 24 | 25 | 26 | // 在 koa 代码中,使用 Context 对 req/res 进行了封装 27 | // 并把 req/res 中多个属性代理到 Context 中,方便访问 28 | class Context { 29 | constructor (req, res) { 30 | this.req = req 31 | this.res = res 32 | } 33 | } 34 | 35 | class Application { 36 | constructor () { 37 | this.middlewares = [] 38 | } 39 | 40 | listen (...args) { 41 | // 在 listen 中处理请求并监听端口号 42 | const server = http.createServer(this.callback()) 43 | server.listen(...args) 44 | } 45 | 46 | // 在 koa 中,app.callback() 将返回 Node HTTP API标准的 handleRequest 函数,方便测试 47 | callback () { 48 | return async (req, res) => { 49 | const ctx = new Context(req, res) 50 | 51 | // 使用 compose 合成所有中间件,在中间件中会做一些 52 | // 1. 路由解析 53 | // 2. Body解析 54 | // 3. 异常处理 55 | // 4. 统一认证 56 | // 5. 等等... 57 | const fn = compose(this.middlewares) 58 | 59 | try { 60 | await fn(ctx) 61 | } catch (e) { 62 | // 最基本的异常处理函数,在实际生产环境中,将由一个专业的异常处理中间件来替代,同时也会做 63 | // 1. 确认异常级别 64 | // 2. 异常上报 65 | // 3. 构造与异常对应的状态码,如 429、422 等 66 | console.error(e) 67 | ctx.res.statusCode = 500 68 | ctx.res.end('Internel Server Error') 69 | } 70 | ctx.res.end(ctx.body) 71 | } 72 | } 73 | 74 | // 注册中间件,并收集在中间件数组中 75 | use (middleware) { 76 | this.middlewares.push(middleware) 77 | } 78 | } 79 | 80 | module.exports = Application 81 | 82 | -------------------------------------------------------------------------------- /code/serve/index.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import http, { IncomingMessage, ServerResponse } from 'node:http' 3 | import fs from 'node:fs' 4 | import { URL } from 'node:url' 5 | 6 | import arg from 'arg' 7 | 8 | type Rewrite = { 9 | source: string; 10 | destination: string; 11 | } 12 | 13 | type Redirect = Rewrite; 14 | 15 | interface Config { 16 | entry?: string; 17 | rewrites?: Rewrite[]; 18 | redirects?: Redirect[]; 19 | etag?: boolean; 20 | cleanUrls?: boolean; 21 | trailingSlash?: boolean; 22 | symlink?: boolean; 23 | } 24 | 25 | async function processDirectory(absolutePath: string): Promise<[fs.Stats | null, string]> { 26 | const newAbsolutePath = path.join(absolutePath, 'index.html') 27 | 28 | try { 29 | const newStat = await fs.promises.lstat(newAbsolutePath) 30 | return [newStat, newAbsolutePath] 31 | } catch (e) { 32 | return [null, newAbsolutePath] 33 | } 34 | } 35 | 36 | // 响应 404,此处可做一个优化,比如读取文件系统中的 404.html 文件 37 | function responseNotFound(res: ServerResponse) { 38 | res.statusCode = 404 39 | res.end('NNNNNNot Found') 40 | } 41 | 42 | // mime: 43 | export default async function handler(req: IncomingMessage, res: ServerResponse, config: Config) { 44 | const pathname = new URL('http://localhost:3000' + req.url ?? '').pathname 45 | 46 | let absolutePath = path.resolve(config.entry ?? '', path.join('.', pathname)) 47 | let statusCode = 200 48 | let stat = null 49 | 50 | try { 51 | stat = await fs.promises.lstat(absolutePath) 52 | } catch (e) { } 53 | 54 | if (stat?.isDirectory()) { 55 | // 如果是目录,则去寻找目录中的 index.html 56 | [stat, absolutePath] = await processDirectory(absolutePath) 57 | } 58 | 59 | if (stat === null) { 60 | return responseNotFound(res) 61 | } 62 | 63 | let headers = { 64 | // 取其文件系统中的体积作为其大小 65 | // 问: 文件的大小与其编码格式有关,那么文件系统的体积应该是如何确定的? 66 | 'Content-Length': stat.size 67 | } 68 | 69 | res.writeHead(statusCode, headers) 70 | 71 | fs.createReadStream(absolutePath).pipe(res) 72 | } 73 | 74 | const args = arg({ 75 | '--port': Number, 76 | '-p': '--port' 77 | }) 78 | 79 | function startEndpoint (port: number, entry: string) { 80 | const server = http.createServer((req, res) => { 81 | handler(req, res, { entry }) 82 | }) 83 | 84 | server.on('error', err => { 85 | // 表示端口号已被占用 86 | if ((err as any).code === 'EADDRINUSE') { 87 | startEndpoint(port + 1, entry) 88 | return 89 | } 90 | 91 | process.exit(1); 92 | }) 93 | 94 | server.listen(port, () => { 95 | console.log(`Open http://localhost:${port}`) 96 | }) 97 | } 98 | 99 | startEndpoint(args['--port'] ?? 3000, args._[0]) -------------------------------------------------------------------------------- /code/html-webpack-plugin/index.js: -------------------------------------------------------------------------------- 1 | const { Compilation, Compiler } = require('webpack') 2 | 3 | // 本 plugin 将实现两个最基本的功能 4 | // 5 | // 1. 处理 Chunks Javascript 注入的问题 6 | // 2. 处理 publicPath 的问题 7 | 8 | function getPublicPath (compilation) { 9 | const compilationHash = compilation.hash 10 | 11 | // outputOptions.publicPath 有可能由一个函数设置,这里通过 webpack API 获取到字符串形式的 publicPath 12 | let publicPath = compilation.getAssetPath(compilation.outputOptions.publicPath, { hash: compilationHash }) 13 | 14 | // 如果 output.publicPath 没有设置,则它的选项为 auto 15 | if (publicPath === 'auto') { 16 | publicPath = '/' 17 | } 18 | 19 | if (publicPath.length && publicPath.substr(-1, 1) !== '/') { 20 | publicPath += '/' 21 | } 22 | 23 | return publicPath 24 | } 25 | 26 | function html ({ title, scripts }) { 27 | return ` 28 | 29 | 30 | 31 | 32 | ${title} 33 | ${scripts.map(src => ``).join('\n ')} 34 | 35 | 36 | Hello, World 37 | 38 | 39 | ` 40 | } 41 | 42 | class HtmlWebpackPlugin { 43 | constructor(options) { 44 | this.options = options || {} 45 | } 46 | 47 | apply(compiler) { 48 | const webpack = compiler.webpack 49 | 50 | compiler.hooks.thisCompilation.tap('HtmlWebpackPlugin', (compilation) => { 51 | // compilation 是 webpack 中最重要的对象,文档见 [compilation-object](https://webpack.js.org/api/compilation-object/#compilation-object-methods) 52 | 53 | compilation.hooks.processAssets.tapAsync({ 54 | name: 'HtmlWebpackPlugin', 55 | 56 | // processAssets 处理资源的时机,此阶段为资源已优化后,更多阶段见文档 57 | // https://webpack.js.org/api/compilation-hooks/#list-of-asset-processing-stages 58 | stage: webpack.Compilation.PROCESS_ASSETS_STAGE_OPTIMIZE_INLINE 59 | }, (compilationAssets, callback) => { 60 | // compilationAssets 将得到所有生成的资源,如各个 chunk.js、各个 image、css 61 | 62 | // 获取 webpac.output.publicPath 选项,(PS: publicPath 选项有可能是通过函数设置) 63 | const publicPath = getPublicPath(compilation) 64 | 65 | // 本示例仅仅考虑单个 entryPoint 的情况 66 | // compilation.entrypoints 可获取入口文件信息 67 | const entryNames = Array.from(compilation.entrypoints.keys()) 68 | 69 | // entryPoint.getFiles() 将获取到该入口的所有资源,并能够保证加载顺序!!!如 runtime-chunk -> main-chunk 70 | const assets = entryNames.map(entryName => compilation.entrypoints.get(entryName).getFiles()).flat() 71 | const scripts = assets.map(src => publicPath + src) 72 | const content = html({ title: this.options.title || 'Demo', scripts }) 73 | 74 | // emitAsset 用以生成资源文件,也是最重要的一步 75 | compilation.emitAsset('index.html', new webpack.sources.RawSource(content)) 76 | callback() 77 | }) 78 | }) 79 | } 80 | } 81 | 82 | module.exports = HtmlWebpackPlugin -------------------------------------------------------------------------------- /code/bundle/Readme.md: -------------------------------------------------------------------------------- 1 | # WIP: 如何实现一个打包器 2 | 3 | > 已实现功能: 4 | > 1. 打包 Commonjs 5 | > 1. 打包 Module (TODO) 6 | 7 | + `webpack` 打包后的文件长什么样子 8 | 9 | ## 代码 10 | 11 | ``` bash 12 | $ node example.js > output.js 13 | 14 | $ node output.js 15 | 2 16 | ``` 17 | 18 | ## 本次待打包文件 19 | 20 | ### 01. example.js 21 | 22 | ```javascript 23 | const inc = require('./increment').increment; 24 | const a = 1; 25 | inc(a); // 2 26 | ``` 27 | 28 | ### 02. increment.js 29 | 30 | ```javascript 31 | const add = require('./math').add; 32 | exports.increment = function(val) { 33 | return add(val, 1); 34 | }; 35 | ``` 36 | 37 | ### 03. math.js 38 | 39 | ```javascript 40 | exports.add = function() { 41 | var sum = 0, i = 0, args = arguments, l = args.length; 42 | while (i < l) { 43 | sum += args[i++]; 44 | } 45 | return sum; 46 | }; 47 | ``` 48 | 49 | ### 04. hello.js 50 | 51 | ``` js 52 | module.exports = 'hello, world' 53 | ``` 54 | 55 | ## 打包后骨架代码与解析 56 | 57 | ![webpack5 打包后文件](https://cdn.jsdelivr.net/gh/shfshanyue/assets@master/src/image.ayrao1zd6ko.png) 58 | 59 | 为了显示方便并便于理解,这里直接展示为最简化代码,把用于隔离作用域的所有 IIFE 都给去掉。 60 | 61 | ``` js 62 | const modules = [] 63 | 64 | const moduleCache = {} 65 | function webpackRequire (moduleId) {} 66 | 67 | webpackRequire(0) 68 | ``` 69 | 70 | ## `modules` 71 | 72 | 在 CommonJS 中,模块外有一层包装函数,可见 [The module wrapper](https://nodejs.org/api/modules.html#modules_the_module_wrapper) 73 | 74 | ``` js 75 | (function(exports, require, module, __filename, __dirname) { 76 | // Module code actually lives in here 77 | }); 78 | ``` 79 | 80 | 在前端中没有文件系统,自然也不需要 `__filename` 与 `__dirname`。以下是 `webpack` 中打包后的包裹函数,与 CommonJS 参数一致但位置不同。 81 | 82 | ``` js 83 | const module1 = function (module, __unused_webpack_exports, __webpack_require__) { 84 | const add = __webpack__require__(2).add; 85 | exports.increment = function(val) { 86 | return add(val, 1); 87 | }; 88 | } 89 | ``` 90 | 91 | 而这里的 `modules` 就是 `webpack` 即将打包的所有模块 92 | 93 | ``` js 94 | const modules = [ 95 | entry, //=> index.js 96 | module1, //=> increment.js 97 | module2 //=> math.js 98 | ] 99 | ``` 100 | 101 | ### 确定依赖关系树 102 | 103 | + `index.js` -> 1 104 | + `increment.js` -> 2 105 | + `math.js` -> 3 106 | + `hello.js` -> 4 107 | 108 | 对于以下依赖树,由于 JS 执行查找模块为深度优先搜索遍历,对所有模块构造一个以深度优先的树。 109 | 110 | + entry -> 1 111 | + A -> 2 112 | + B -> 3 113 | + C -> 4 114 | + D -> 5 115 | + E -> 6 116 | + F -> 7 117 | 118 | ### 依赖关系树的构建 119 | 120 | **如何遍历所有的 require,确认模块依赖树?** 121 | 122 | ## `webpackRequire` 123 | 124 | ``` js 125 | function __webpack_require__(moduleId) { 126 | // Check if module is in cache 127 | var cachedModule = __webpack_module_cache__[moduleId]; 128 | if (cachedModule !== undefined) { 129 | return cachedModule.exports; 130 | } 131 | // Create a new module (and put it into the cache) 132 | var module = __webpack_module_cache__[moduleId] = { 133 | // no module.id needed 134 | // no module.loaded needed 135 | exports: {} 136 | }; 137 | 138 | // Execute the module function 139 | __webpack_modules__[moduleId](module, module.exports, __webpack_require__); 140 | 141 | // Return the exports of the module 142 | return module.exports; 143 | } 144 | ``` 145 | 146 | ``` js 147 | function webpackRequire (moduleId) { 148 | const cacheModule = moduleCache[moduleId] 149 | if (cacheModule) { 150 | return cacheModule.exports 151 | } 152 | const targetModule = { exports: {} } 153 | modules[moduleId](targetModule.exports, webpackRequire, targetModule) 154 | cacheModule[moduleId] 155 | return targetModule.exports 156 | } 157 | ``` 158 | 159 | -------------------------------------------------------------------------------- /code/express/index.js: -------------------------------------------------------------------------------- 1 | // [http 模块](https://nodejs.org/api/http.html),构建 Node 框架的核心 API 2 | const http = require('http') 3 | 4 | // [path-to-regexp](https://github.com/pillarjs/path-to-regexp),用以将 `/user/:name` 类参数路由转化为正则表达式 5 | const { pathToRegexp, regexpToFunction, match } = require('path-to-regexp') 6 | 7 | class Application { 8 | constructor () { 9 | this._router = new Router() 10 | } 11 | 12 | // 在 listen 中处理请求并监听端口号,与 koa 一致,或者说基本所有服务端框架都是这么做的 13 | listen (...args) { 14 | // 创建服务,this.handle 为入口函数,在源码中,express app 本身即为入口函数 15 | const server = http.createServer(this.handle.bind(this)) 16 | server.listen(...args) 17 | } 18 | 19 | handle (req, res) { 20 | const router = this._router 21 | router.handle(req, res) 22 | } 23 | 24 | // 注册应用级中间件,收集所有的应用级中间至 this._router.stack 中,后将实现洋葱模型 25 | use (path, ...fns) { 26 | this._router.use(path, ...fns) 27 | } 28 | 29 | // 处理 http 的各种 verb,如 get、post、 30 | // 注册匿名应用级中间件 31 | get (path, ...fns) { 32 | const route = this._router.route(path) 33 | // 对于该应用级中间件所涉及到的所有路由级中间件,在 Route.prototype.get 中进行处理 34 | route.get(...fns) 35 | } 36 | } 37 | 38 | class Router { 39 | constructor () { 40 | // 收集所有应用级中间件 41 | this.stack = [] 42 | } 43 | 44 | // 应用级中间件洋葱模型的实现 45 | // 46 | // 当一次请求来临时,遍历所有 Layer (即对中间件的抽象),当符合要求时,所有 Layer 将通过 next 连接按序执行 47 | handle (req, res) { 48 | const stack = this.stack 49 | let index = 0 50 | 51 | // 调用下一个应用级中间件 52 | const next = () => { 53 | let layer 54 | let match 55 | 56 | while (!match && index < this.stack.length) { 57 | layer = stack[index++] 58 | // 查看请求路径是否匹配该中间件,如果匹配,则返回匹配的 parmas 59 | match = layer.match(req.url) 60 | } 61 | // 遍历中间件,如果无一路径匹配,则状态码为 404 62 | if (!match) { 63 | res.status = 404 64 | res.end('NOT FOUND SHANYUE') 65 | return 66 | } 67 | req.params = match.params 68 | // 处理中间件的函数,如果中间件中调用了 next(),则往下走下一个中间件 69 | layer.handle(req, res, next) 70 | } 71 | next() 72 | } 73 | 74 | // 75 | // app.use('/users/', fn1, fn2, fn3) 76 | // 此处路径在 express 中可省略,则默认为所有路径,为了更好地理解源码,此处不作省略 77 | use (path, ...fns) { 78 | for (const fn of fns) { 79 | // 对于应用级中间件,宽松匹配,前缀匹配,即 /api 将匹配以 /api 开头的所有路径 80 | const layer = new Layer(path, fn, { end: false, strict: false }) 81 | this.stack.push(layer) 82 | } 83 | } 84 | 85 | // 注册应用级路由中间件,是一个匿名中间件,维护一系列关于该路径相关的路由级别中间件, 86 | route (path) { 87 | const route = new Route(path) 88 | // 该匿名中间件的 handleRequest 函数为将应用级中间挂载下的所有路由中间件串联处理 89 | // 对于路由级中间件,完全匹配,即 /api 将仅仅匹配 /api 90 | const layer = new Layer(path, route.dispatch.bind(route), { end: true }) 91 | layer.route = route 92 | this.stack.push(layer) 93 | return route 94 | } 95 | } 96 | 97 | class Route { 98 | constructor (path) { 99 | this.stack = [] 100 | this.path = path 101 | this.methods = {} 102 | } 103 | 104 | // 为应用级中间件注册多个路由级中间件 105 | get (...fns) { 106 | this.methods.get = true 107 | for (const fn of fns) { 108 | // 路由级中间件的路径可忽略,因路径是否匹配已在该路由级中间件的应用中间件层已做匹配 109 | const layer = new Layer('/', fn) 110 | this.stack.push(layer) 111 | } 112 | } 113 | 114 | // 应用级路由中间件的 handleRequest 函数为将应用级中间挂载下的所有路由中间件串联处理 115 | // 路由级中间件的洋葱模型实现 116 | dispatch (req, res, done) { 117 | let index = 0 118 | const stack = this.stack 119 | const next = () => { 120 | const layer = stack[index++] 121 | 122 | // 如果最后一个 123 | if (!layer) { done() } 124 | layer.handle(req, res, next) 125 | } 126 | next() 127 | } 128 | } 129 | 130 | 131 | // 对中间件的一层抽象 132 | class Layer { 133 | // 134 | // 当注册路由 app.use('/users/:id', () => {}) 时,其中以下两个想想为 path 和 handle 135 | // path: /users/:id 136 | // handle: () => {} 137 | constructor (path, handle, options) { 138 | this.path = path 139 | this.handle = handle 140 | this.options = options 141 | this.keys = [] 142 | // 根据 path,生成正则表达式 143 | this.re = pathToRegexp(path, this.keys, options) 144 | } 145 | 146 | // 查看请求路径是否匹配该中间件,如果匹配,则返回匹配的 parmas 147 | match (url) { 148 | const matchRoute = regexpToFunction(this.re, this.keys, { decode: decodeURIComponent }) 149 | return matchRoute(url) 150 | } 151 | } 152 | 153 | module.exports = () => new Application() 154 | -------------------------------------------------------------------------------- /code/bundle/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | // 负责 code -> ast 5 | const { parse } = require('@babel/parser') 6 | // 负责 ast -> ast 7 | const traverse = require('@babel/traverse').default 8 | // 负责 ast -> code 9 | const generate = require('@babel/generator').default 10 | 11 | let moduleId = 0 12 | // 该函数用以解析该文件模块的所有依赖树。对所有目标代码根据 AST 构建为组件树的结构并添加 ID,ID 如果 webpack 一样为深度优先自增。数据结构为: 13 | // 14 | // const rootModule = { 15 | // id: 0, 16 | // filename: '/Documents/app/node_modules/hello/index.js', 17 | // deps: [ moduleA, moduleB ], 18 | // code: 'const a = 3; module.exports = 3', 19 | // } 20 | // 21 | // 如果组件 A 依赖于组件B和组件C 22 | // 23 | // { 24 | // id: 0, 25 | // filename: A, 26 | // deps: [ 27 | // { id: 1, filename: B, deps: [] }, 28 | // { id: 2, filename: C, deps: [] }, 29 | // ] 30 | // } 31 | function buildModule (filename) { 32 | // 如果入口位置为相对路径,则根据此时的 __dirname 生成绝对文件路径 33 | filename = path.resolve(__dirname, filename) 34 | 35 | // 同步读取文件,并使用 utf8 读做字符串 36 | const code = fs.readFileSync(filename, 'utf8') 37 | 38 | // 使用 babel 解析源码为 AST 39 | const ast = parse(code, { 40 | sourceType: 'module' 41 | }) 42 | 43 | const deps = [] 44 | const currentModuleId = moduleId 45 | 46 | traverse(ast, { 47 | enter({ node }) { 48 | // 根据 AST 定位到所有的 require 函数,寻找出所有的依赖 49 | if (node.type === 'CallExpression' && node.callee.name === 'require') { 50 | const argument = node.arguments[0] 51 | 52 | // 找到依赖的模块名称 53 | // require('lodash') -> lodash (argument.value) 54 | if (argument.type === 'StringLiteral') { 55 | 56 | // 深度优先搜索,当寻找到一个依赖时,则 moduleId 自增一 57 | // 并深度递归进入该模块,解析该模块的模块依赖树 58 | moduleId++; 59 | const nextFilename = path.join(path.dirname(filename), argument.value) 60 | 61 | // 如果 lodash 的 moduleId 为 3 的话 62 | // require('lodash') -> require(3) 63 | argument.value = moduleId 64 | deps.push(buildModule(nextFilename)) 65 | } 66 | } 67 | } 68 | }) 69 | return { 70 | filename, 71 | deps, 72 | code: generate(ast).code, 73 | id: currentModuleId 74 | } 75 | } 76 | 77 | // 把模块依赖由树结构更改为数组结构,方便更快的索引 78 | // 79 | // { 80 | // id: 0, 81 | // filename: A, 82 | // deps: [ 83 | // { id: 1, filename: B, deps: [] }, 84 | // { id: 2, filename: C, deps: [] }, 85 | // ] 86 | // } 87 | // ====> 该函数把数据结构由以上转为以下 88 | // [ 89 | // { id: 0, filename: A } 90 | // { id: 1, filename: B } 91 | // { id: 2, filename: C } 92 | // ] 93 | function moduleTreeToQueue (moduleTree) { 94 | const { deps, ...module } = moduleTree 95 | 96 | const moduleQueue = deps.reduce((acc, m) => { 97 | return acc.concat(moduleTreeToQueue(m)) 98 | }, [module]) 99 | 100 | return moduleQueue 101 | } 102 | 103 | // 构建一个浏览器端中虚假的 Commonjs Wrapper 104 | // 注入 exports、require、module 等全局变量,注意这里的顺序与 CommonJS 保持一致,但与 webpack 不一致,但影响不大 105 | // 在 webpack 中,这里的 code 需要使用 webpack loader 进行处理 106 | function createModuleWrapper (code) { 107 | return ` 108 | (function(exports, require, module) { 109 | ${code} 110 | })` 111 | } 112 | 113 | // 根据入口文件进行打包,也是 mini-webpack 的入口函数 114 | function createBundleTemplate (entry) { 115 | // 如同 webpack 中的 __webpack_modules__,以数组的形式存储项目所有依赖的模块 116 | const moduleTree = buildModule(entry) 117 | const modules = moduleTreeToQueue(moduleTree) 118 | 119 | // 生成打包的模板,也就是打包的真正过程 120 | return ` 121 | // 统一扔到块级作用域中,避免污染全局变量 122 | // 为了方便,这里使用 {},而不用 IIFE 123 | // 124 | // 以下代码为打包的三个重要步骤: 125 | // 1. 构建 modules 126 | // 2. 构建 webpackRequire,加载模块,模拟 CommonJS 中的 require 127 | // 3. 运行入口函数 128 | { 129 | // 1. 构建 modules 130 | const modules = [ 131 | ${modules.map(m => createModuleWrapper(m.code))} 132 | ] 133 | 134 | // 模块缓存,所有模块都仅仅会加载并执行一次 135 | const cacheModules = {} 136 | 137 | // 2. 加载模块,模拟代码中的 require 函数 138 | // 打包后,实际上根据模块的 ID 加载,并对 module.exports 进行缓存 139 | function webpackRequire (moduleId) { 140 | const cachedModule = cacheModules[moduleId] 141 | if (cachedModule) { 142 | return cachedModule.exports 143 | } 144 | const targetModule = { exports: {} } 145 | modules[moduleId](targetModule.exports, webpackRequire, targetModule) 146 | cacheModules[moduleId] = targetModule 147 | return targetModule.exports 148 | } 149 | 150 | // 3. 运行入口函数 151 | webpackRequire(0) 152 | } 153 | ` 154 | } 155 | 156 | module.exports = createBundleTemplate 157 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # 流行框架与库的源码分析与最简实现 2 | 3 | 大家好,我是山月,这是我新开的一个坑:[手写源码最小实现](https://github.com/shfshanyue/mini-code),**每一行代码都有注释**。 4 | 5 | 当我们在深入学习一个框架或者库时,为了了解它的思想及设计思路,也为了更好地使用和避免无意的 Bug,源码研究是最好的方法。 6 | 7 | 对于 `koa` 与 `vdom` 这种极为简单,而应用却很广泛的框架/库,莫不如是。为了验证是否已足够了解它,可以实现一个仅具备核心功能的迷你的库。正所谓,麻雀虽小,五脏俱全。 8 | 9 | 对于源码,我将尽可能注释每一行代码,并以文章讲述原理与实现过程。目前已实现列表为: 10 | 11 | + [mini-koa](https://github.com/shfshanyue/mini-code/tree/master/code/koa) 12 | + [mini-http-router](https://github.com/shfshanyue/mini-code/tree/master/code/http-router) 13 | + [mini-express](https://github.com/shfshanyue/mini-code/tree/master/code/express) 14 | + [mini-webpack (有代码,有注释)](https://github.com/shfshanyue/mini-code/tree/master/code/bundle) 15 | + [mini-html-webpack-plugin (有代码,有注释)](https://github.com/shfshanyue/mini-code/tree/master/code/html-webpack-plugin) 16 | + [mini-json-loader (有代码,有注释)](https://github.com/shfshanyue/mini-code/tree/master/code/json-loader) 17 | + [mini-vdom (有代码,有注释)](https://github.com/shfshanyue/mini-code/tree/master/code/vdom) 18 | + [mini-native-http-server (有代码)](https://github.com/shfshanyue/mini-code/tree/master/code/native-http-server) 19 | + [mini-serve (有代码)](https://github.com/shfshanyue/mini-code/tree/master/code/serve) 20 | 21 | 由于目前浏览器对 ESM 已支持良好,对于客户端相关源码使用 ESM 书写,比如 vDOM、React 等。而对于服务端使用 CommonJS 书写,如 koa、express 等。 22 | 23 | ## 运行与目录结构 24 | 25 | 本项目采用 `monorepo` 进行维护,如果你对它不了解,可直接忽略。 26 | 27 | 所有的迷你版本实现置于 `code` 文件夹,源文件置于 `index.js` 中,示例文件置于两个位置: 28 | 29 | + `example.js` 30 | + `example/` 31 | 32 | 关于查看及运行示例,请使用命令 `npm run example` 33 | 34 | ``` bash 35 | $ git clone git@github.com:shfshanyue/mini-code.git 36 | 37 | # 在 npm v7 中,会对所有 workspace 中的依赖进行安装 38 | $ npm i 39 | 40 | # 在 monorepo 中运行某个源码示例 41 | # 或者进入代码目录,再运行示例: cd code/express && npm run example 42 | $ npm run example -w mini-express 43 | ``` 44 | 45 | 如果你对 `monorepo` 不了解: 46 | 47 | ``` bash 48 | $ git clone git@github.com:shfshanyue/mini-code.git 49 | 50 | $ npm i 51 | $ cd example/express 52 | $ npm i 53 | $ npm run example 54 | ``` 55 | 56 | ## 源码之前 57 | 58 | 在调试了千万遍源码之后,才能对源码稍微理解,摆在这里的问题是:如何调试源码? 59 | 60 | > TODO: 以前三篇文章,随后附上 61 | 62 | 1. 浏览器中如何调试源码? 63 | 1. Node 中如何调试源码? 64 | 65 | ## 与我交流 66 | 67 | 添加我的微信 `shanyue-bot`: 68 | 69 | + 拉你进仓库对应的源码交流群,和 5000+ 小伙伴们一起交流源码 70 | + 山月的原创文章与分享 71 | 72 | 73 | 74 | ## 推荐阅读源码清单 75 | 76 | 以下源码按次序,从易到难进行阅读,否则碰到大块头(比如 React),读不下去,容易怀疑自我,从简单的开始读起,有助于培养自信心 77 | 78 | ### 偏客户端 79 | 80 | + [ms](https://github.com/vercel/ms): 时间转换器,Vercel 出品,周下载量 8000 万,几乎是 npm 下载量最高的 package 之一,从它看起,让你知道看源码/发包其实也很简单。 81 | + [github markdown style](https://github.com/sindresorhus/github-markdown-css): 以为很简单,实际上很难。锻炼 CSS 的最好方法,就是给 markdown 写一个主题。 82 | + [lru-cache](https://github.com/isaacs/node-lru-cache): LRU Cache,前端及服务端框架中的常用依赖。 83 | + [tsdx](https://github.com/formium/tsdx): 零配置的 npm 库开发利器,与 CRA 相似,不过它主要面向库开发者而非业务开发者,了解它是如何提供零配置功能,看懂默认配置做了那些优化,并了解它的所有工具链 (prettier、eslint、size、bundleanalyzer、rollup、typescript、storybook)。 84 | + [create-react-app](https://github.com/facebook/create-react-app): React 最广泛的脚手架,读懂三点。一,如何生成脚手架;二,如何实现 eject;三,了解 cra 的所有重要依赖,读懂默认 webpack 配置。 85 | + webpack (runtime code): 读懂两点。一、打包 cjs/esm 后的运行时代码;二、打包有 chunk 后的运行时代码。 86 | + [axios](https://github.com/axios/axios): 请求库,了解它是如何封装源码且如何实现拦截器的。 87 | + [immer](https://github.com/immerjs/immer): 不可变数据,可提升做深拷贝时的性能,可应用在 React 中。 88 | + [use-debounce](https://github.com/xnimorz/use-debounce): React 中的一个 debounce hook。减少 React 的渲染次数,可提升性能。 89 | + [react-virtualized](https://github.com/bvaughn/react-virtualized): React 中的虚拟列表优化,可提升性能。 90 | + [react-query](https://github.com/tannerlinsley/react-query): 用以请求优化的 react hooks,可提升性能。 91 | + [react-router](https://github.com/remix-run/react-router): React 最受欢迎的路由库 92 | + [redux/react-redux](https://github.com/reduxjs/redux): React 最受欢迎的状态库 93 | + [vite](https://github.com/vitejs/vite): 秒级启动及热更行,可大幅提升开发体验。 94 | + [vue3](https://github.com/vuejs/vue-next): 最难的放到最后边,读懂 vue3 的性能优化体验在哪些方面。 95 | + [react](https://github.com/facebook/react): 最难的放到最后边,读懂 Fiber 及其它所提供的性能优化。 96 | 97 | ### 偏服务端 98 | 99 | + [koa](https://github.com/koajs/koa) 100 | + [body-parser](https://github.com/stream-utils/raw-body): express 甚至是大部分服务端框架所依赖的用以解析 body 的库 101 | + [next](https://github.com/vercel/next.js) 102 | + [ws](https://github.com/websockets/ws): 了解 websocket 是如何构造 Frame 并发送数据的 (在此之前可阅读 node/http 源码) 103 | + [serve-handler](https://github.com/vercel/serve-handler): 静态资源服务器 104 | + [apollo-server](https://github.com/apollographql/apollo-server): GraphQL 框架,值得一看 105 | + [node](https://github.com/nodejs/node): 最难的放到最后边 106 | 107 | ### 其它 108 | 109 | + ws 110 | + native http server 111 | + native http client 112 | + lru cache 113 | + trie router 114 | + vdom 115 | + react 116 | + redux 117 | + react-query 118 | + use-debuounce 119 | + axios 120 | + vue 121 | + vite 122 | + dataloader 123 | + mustable 124 | + parser (re/js/css/md) 125 | + stream pipeline (nodejs) 126 | + code highlighter 127 | + babel 128 | + html-webpack-plugin 129 | + react-dnd 130 | + react-dropzone 131 | 132 | -------------------------------------------------------------------------------- /code/react/Readme.md: -------------------------------------------------------------------------------- 1 | ## React.createElement 时做了什么? 2 | 3 | + React.createElement 4 | + ReactElement 5 | 6 | ## 数据结构 7 | 8 | ### workTag 9 | 10 | > 源码位置: [react-reconciler/src/ReactWorkTag.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactWorkTags.js) 11 | 12 | 表示一个 `ReactElement` 的类型,以数字表示,以下列出最常见的几种类型。 13 | 14 | ``` js 15 | // 函数式组件 16 | export const FunctionComponent = 0; 17 | // 类组件 18 | export const ClassComponent = 1; 19 | 20 | // 根元素 21 | export const HostRoot = 3; 22 | 23 | // HTML元素 24 | export const HostComponent = 5; 25 | ``` 26 | 27 | ### updateQueue 28 | 29 | ### FiberNode 30 | 31 | > 源码位置: [react-reconciler/src/ReactInternalTypes.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactInternalTypes.js#L62) 32 | 33 | ``` js 34 | export type Fiber = {| 35 | // 表示一个 `ReactElement` 的类型,以数字表示,以下列出最常见的几种类型。 36 | tag: WorkTag, 37 | 38 | // 如 div、button 等,也可以是 React 组件的名称,如 App()、HomePage() 等 39 | type: any, 40 | 41 | // The local state associated with this fiber. 42 | stateNode: any, 43 | 44 | // 指向父节点 45 | return: Fiber | null, 46 | // 指向首个子节点 47 | child: Fiber | null, 48 | // 指向相邻兄弟节点 49 | sibling: Fiber | null, 50 | 51 | // 维护虚拟节点的 props 52 | // eg.
shanyue
将返回: 53 | // { key: 10086, class: app, children: [shanyue] } 54 | memoizedProps: any, 55 | 56 | // 维护虚拟节点(组件)的 state 57 | memoizedState: any, 58 | 59 | // 即将需要更新的状态队列 60 | updateQueue: mixed, 61 | 62 | // The state used to create the output 63 | memoizedState: any, 64 | 65 | // Dependencies (contexts, events) for this fiber, if it has any 66 | dependencies: Dependencies | null, 67 | 68 | // 副作用 69 | flags: Flags, 70 | subtreeFlags: Flags, 71 | deletions: Array | null, 72 | 73 | // 关于存在副作用的 Fiber 链表,可以快速遍历完所有带有副作用的 FiberNode 74 | nextEffect: Fiber | null, 75 | 76 | // The first and last fiber with side-effect within this subtree. This allows 77 | // us to reuse a slice of the linked list when we reuse the work done within 78 | // this fiber. 79 | firstEffect: Fiber | null, 80 | lastEffect: Fiber | null, 81 | 82 | lanes: Lanes, 83 | childLanes: Lanes, 84 | 85 | // 每一个 FiberNode 都有一个成对的 alternate 86 | alternate: Fiber | null, 87 | 88 | |}; 89 | ``` 90 | 91 | ## Lane 92 | 93 | > 源码位置: [react-reconciler/src/ReactFiberLane.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactInternalTypes.js#L62) 94 | 95 | ``` js 96 | // 0 97 | export const NoLane: Lane = /* */ 0b0000000000000000000000000000000; 98 | 99 | // 1 100 | export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001; 101 | ``` 102 | 103 | ## Flag 104 | 105 | > 源码位置: [react-reconciler/src/ReactFiberFlags.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactInternalTypes.js#L62) 106 | 107 | ``` js 108 | // 0 109 | export const NoFlags = /* */ 0b00000000000000000000000; 110 | 111 | export const PerformedWork = /* */ 0b00000000000000000000001; 112 | export const Placement = /* */ 0b00000000000000000000010; 113 | 114 | // 4 115 | export const Update = /* */ 0b00000000000000000000100; 116 | ``` 117 | 118 | ## EffectList 119 | 120 | ## render 时做了什么? 121 | 122 | ## setState 时做了什么? 123 | 124 | ``` jsx 125 | const [count, setCount] = useState(0) 126 | 127 | const handleClick = () => { 128 | setCount(count + 1) 129 | } 130 | ``` 131 | 132 | + setCount(1) 133 | + dispatchAction(fiber, queue, action) -> FiberNode 134 | + requestEventTime() 135 | + requestUpdateLane(fiber) -> Lane 136 | + lastRenderedReducer(currentState, action) 137 | + scheduleUpdateOnFiber(fiber, lane, eventTime) 138 | + markUpdateLaneFromFiberToRoot(fiber, lane, eventTime) -> FiberRootNode 139 | + markRootUpdated(root, updateLane, eventTime) 140 | + ensureRootIsScheduled(root, eventTime) 141 | + schedulePendingInteractions(root, lane) 142 | + scheduleInteractions(root, lane, interactions) 143 | 144 | ``` js 145 | function requestUpdateLane(fiber) { 146 | // Special cases 147 | var mode = fiber.mode; 148 | 149 | if ((mode & BlockingMode) === NoMode) { 150 | return SyncLane; 151 | } 152 | } 153 | ``` 154 | 155 | ## lastEffect 156 | 157 | ## performUnitOfWork 158 | 159 | ``` js 160 | function performUnitOfWork(unitOfWork) { 161 | // The current, flushed, state of this fiber is the alternate. Ideally 162 | // nothing should rely on this, but relying on it here means that we don't 163 | // need an additional field on the work in progress. 164 | var current = unitOfWork.alternate; 165 | setCurrentFiber(unitOfWork); 166 | var next; 167 | 168 | if ( (unitOfWork.mode & ProfileMode) !== NoMode) { 169 | startProfilerTimer(unitOfWork); 170 | next = beginWork$1(current, unitOfWork, subtreeRenderLanes); 171 | stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); 172 | } else { 173 | next = beginWork$1(current, unitOfWork, subtreeRenderLanes); 174 | } 175 | 176 | resetCurrentFiber(); 177 | unitOfWork.memoizedProps = unitOfWork.pendingProps; 178 | 179 | if (next === null) { 180 | // If this doesn't spawn new work, complete the current work. 181 | completeUnitOfWork(unitOfWork); 182 | } else { 183 | workInProgress = next; 184 | } 185 | 186 | ReactCurrentOwner$2.current = null; 187 | } 188 | ``` 189 | 190 | ## beginWork 191 | 192 | + valueStack 193 | + fiberStack 194 | 195 | + contextFiberStackCursor 196 | 197 | + beginWork(current, workInProgress, renderLanes) 198 | + checkScheduledUpdateOrContext(current, renderLanes) -> boolean 199 | + bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes) -> FiberNode 200 | + updateHostComponent -> FiberNode 201 | + pushHostContext(fiber) 202 | + push(cursor, value, fiber) 203 | + markRef(current, workInProgress) 204 | + reconcileChildren(current, workInProgress, nextChildren, renderLanes); 205 | 206 | ## debug 如何调试按源文件而非打包后的 react.development.js 207 | 208 | + mountWorkInProgressHook 209 | + updateWorkInProgressHook 210 | -------------------------------------------------------------------------------- /code/http-router/Readme.md: -------------------------------------------------------------------------------- 1 | # 如何实现一个最小版 HTTP Server Router 2 | 3 | > 仓库:[mini-http-router](https://github.com/shfshanyue/mini-code/tree/master/code/http-router/) 4 | 5 | 大家好,我是山月。 6 | 7 | 实现一个最简的服务端路由,仅需二十行代码。 8 | 9 | 欲知如何,请往下看。 10 | 11 | ## 山月的代码实现 12 | 13 | 代码置于 [shfshanyue/mini-code:code/http-router](https://github.com/shfshanyue/mini-code/blob/master/code/http-router/index.js) 14 | 15 | 可直接读源码,基本每一行都有注释。 16 | 17 | 使用 `npm run example` 或者 `node example` 可运行示例代码 18 | 19 | ``` bash 20 | # 或者直接 node example.js 21 | $ npm run example 22 | ``` 23 | 24 | 1. 当访问 `/api/users/10086` 时,将正常响应数据 `{ userId: 10086 }` 25 | 1. 当访问 `/v2/randomxxxxx` 时,将相应 `hello, v2` 26 | 1. 当访问 `/api/404` 时,将返回 404 状态码 27 | 28 | ## 目标与示例 29 | 30 | 1. 可定义路由函数 31 | 2. 可定义路由参数 32 | 33 | 可在 [shfshanyue/mini-code:code/http-router/example.js](https://github.com/shfshanyue/mini-code/blob/master/code/http-router/example.js) 查看完整示例。 34 | 35 | ``` js 36 | const http = require('http') 37 | const router = require('.') 38 | 39 | router.get('/api/users/:userId', (req, res) => { 40 | res.end(JSON.stringify({ 41 | userId: req.params.userId 42 | })) 43 | }) 44 | 45 | const server = http.createServer((req, res) => { 46 | router.lookup(req, res) 47 | }) 48 | 49 | server.listen(3000) 50 | ``` 51 | 52 | ## 如何匹配路由? 53 | 54 | 服务端业务根据路由分发不同的业务逻辑处理函数,如下代码,不同的路由不同的处理逻辑: 55 | 56 | ``` js 57 | const routes = [ 58 | { 59 | path: '/api', 60 | method: 'GET', 61 | handleRequest: (req, res) => {}, 62 | }, 63 | { 64 | path: '/api/user', 65 | method: 'GET', 66 | handleRequest: (req, res) => {} 67 | }, 68 | { 69 | path: '/api/book', 70 | method: 'GET', 71 | handleRequest: (req, res) => {} 72 | } 73 | ] 74 | ``` 75 | 76 | 如何匹配路由,分发到正常的请求处理函数? 77 | 78 | 以上路径较为简单,可直接匹配字符串 79 | 80 | ``` js 81 | function lookup (req, res) { 82 | return routes.find(route => route.path === req.url && route.method === req.method) 83 | } 84 | ``` 85 | 86 | ## 携带参数的路由与前缀路由 87 | 88 | 如果路由表中需要匹配参数呢? 89 | 90 | ``` js 91 | const routes = [ 92 | { 93 | path: '/api', 94 | method: 'GET', 95 | handleRequest: (req, res) => {} 96 | }, 97 | { 98 | path: '/api/user', 99 | method: 'GET', 100 | handleRequest: (req, res) => {} 101 | }, 102 | { 103 | // 此处路由携带有参数 userId 104 | path: '/api/users/:userId', 105 | method: 'GET', 106 | handleRequest: (req, res) => {} 107 | } 108 | ] 109 | ``` 110 | 111 | + `/api/users/:userId`: 匹配参数,得到匹配中的 `userId` 112 | 113 | 正则?这个我熟啊。 114 | 115 | 把每次注册路由的路径改成正则,进行正则匹配,伪代码如下: 116 | 117 | ``` js 118 | function lookup (req, res) { 119 | return routes.find(route => route.re.test(req.url) && route.method === req.method) 120 | } 121 | ``` 122 | 123 | 问题来了,如何把路径转化为正则表达式? 124 | 125 | 此时祭出神器 [path-to-regexp](https://npm.devtool.tech/path-to-regexp),将路径转化为正则表达式。无论 `Express`、`Koa` 等服务端框架,还是 `React`、`Vue` 等客户端框架的路由部分,都是它的忠实用户。 126 | 127 | ``` js 128 | const { pathToRegexp } = require('path-to-regexp') 129 | 130 | pathToRegexp('/') 131 | //=> /^\/[\/#\?]?$/i 132 | 133 | // 可用以匹配前缀路由 134 | p.pathToRegexp('/', [], { end: false }) 135 | //=> /^\/(?:[\/#\?](?=[]|$))?/i 136 | 137 | // 对于参数,通过捕获组来捕获参数 138 | pathToRegexp('/api/users/:id') 139 | //=> /^\/api\/users(?:\/([^\/#\?]+?))[\/#\?]?$/i 140 | ``` 141 | 142 | 路由参数与路由前缀问题迎刃而解。 143 | 144 | ## 路由的数据结构 145 | 146 | 使用正则去匹配每次请求的路径,为路由添加一个字段 `re`,根据 `pathToRegexp` 生成正则表达式,此时的数据结构如下所示: 147 | 148 | ``` js 149 | const routes = [ 150 | { 151 | path: '/api', 152 | method: 'GET', 153 | re: pathToRegexp('/api'), 154 | handleRequest: (req, res) => {} 155 | }, 156 | ] 157 | 158 | function lookup (req, res) { 159 | return routes.find(route => route.re.test(req.url) && route.method === req.method) 160 | } 161 | ``` 162 | 163 | 当处理 `/api/users/:userId` 等参数路由时,为路由增加一个方法,用以匹配参数,每次处理请求时,将参数解析并携带到 `req.params` 中。 164 | 165 | 恰好,`path-to-regexp` 可以使用 `match` 直接解析参数,原理是**使用带有捕获组的正则去匹配请求路径**。 166 | 167 | ``` js 168 | const { match } = require('path-to-regexp') 169 | 170 | // 将解析: /api/users/10086 -> { userId: 10086 } 171 | const matchRoute = match('/api/users/:userId', { decode: decodeURIComponent }) 172 | 173 | //=> { params: { userId: 10086 } } 174 | matchRoute('/api/users/10086') 175 | ``` 176 | 177 | 我们将请求是否能匹配某个路由进行抽象为 `match`,最终路由的数据结构如下所示: 178 | 179 | > 在生产环境中,每个路由都会在注册时生成正则表达式,当请求来临时,将根据该正则表达式进行匹配并针对参数路由生成 params 180 | 181 | ``` js 182 | const routes = [ 183 | { 184 | path: '/api', 185 | method: 'GET', 186 | handleRequest: (req, res) => {} 187 | match: (path) => {} 188 | }, 189 | { 190 | path: '/api/user', 191 | method: 'GET', 192 | handleRequest: (req, res) => {} 193 | match: (path) => {} 194 | }, 195 | { 196 | path: '/api/users/:id', 197 | method: 'GET', 198 | handleRequest: (req, res) => {} 199 | match: (path) => {} 200 | } 201 | ] 202 | 203 | function lookup (req, res) { 204 | return routes.find(route => (req.params = route.match(req.url)?.params) && req.method === route.method) 205 | } 206 | ``` 207 | 208 | ## 源码 209 | 210 | ``` js 211 | const { match } = require('path-to-regexp') 212 | 213 | const router = { 214 | routes: [], 215 | // 注册路由,此时路由为前缀路由,将匹配该字符串的所有前缀与 http method 216 | use (path, handleRequest, options) { 217 | // 用以匹配请求路径函数,如果匹配成功则返回匹配成功的参数,否则返回 false 218 | // user/:id -> users/18 (id=18) 219 | const matchRoute = match(path, { decode: decodeURIComponent, end: false, ...options }) 220 | 221 | // 注册路由,整理数据结构添加入路由数组 222 | this.routes.push({ match: matchRoute, handleRequest, method: options.method || 'GET' }) 223 | }, 224 | // 注册路由,请求方法为 GET 225 | get (path, handleRequest) { 226 | return this.use(path, handleRequest, { end: true }) 227 | }, 228 | // 注册路由,请求方法为 POST 229 | post (path, handleRequest) { 230 | return this.use(path, handleRequest, { end: true, method: 'POST' }) 231 | }, 232 | // 入口函数 233 | lookup (req, res) { 234 | // 遍历路由,找到匹配路由,并解析路由参数 235 | const route = this.routes.find(route => (req.params = route.match(req.url)?.params) && req.method === route.method) 236 | if (route) { 237 | // 找到路由时,处理该路由的处理逻辑 238 | route.handleRequest(req, res) 239 | } else { 240 | // 如果找不到,返回 404 241 | res.statusCode = 404 242 | res.end('NOT FOUND SHANYUE') 243 | } 244 | } 245 | } 246 | 247 | module.exports = router 248 | ``` 249 | 250 | ## 结语 251 | 252 | 完。 253 | 254 | 等一下,记得晚上要好好吃饭。 255 | -------------------------------------------------------------------------------- /code/vdom/index.js: -------------------------------------------------------------------------------- 1 | const isPrimitive = x => typeof x === 'string' || typeof x === 'number' 2 | 3 | const sameVnode = (oldVnode, newVnode) => oldVnode.key === newVnode.key && oldVnode.tag === newVnode.tag 4 | 5 | function vnode (tag, props, children, text) { 6 | return { 7 | tag, 8 | props, 9 | children, 10 | text 11 | } 12 | } 13 | 14 | function h (tag, props, children) { 15 | let text = null 16 | if (isPrimitive(children)) { 17 | text = children 18 | children = null 19 | } else if (Array.isArray(children)) { 20 | children = children.map(x => isPrimitive(x) ? vnode(null, null, null, x) : x) 21 | } 22 | return vnode(tag, props, children, text) 23 | } 24 | 25 | function createElementByVNode (vnode) { 26 | const { tag, props, children, text } = vnode 27 | 28 | const element = document.createElement(tag) 29 | 30 | if (Array.isArray(children)) { 31 | for (const childVNode of children) { 32 | element.appendChild( 33 | createElementByVNode(childVNode) 34 | ) 35 | } 36 | } else if (text) { 37 | element.textContent = text 38 | } 39 | updateProps(element, {}, vnode) 40 | vnode.element = element 41 | return element 42 | } 43 | 44 | // 45 | // 删除掉 element.children 中的第 [startIndex, endIndex) 个子元素 46 | // 47 | function removeVnodes (element, children, startIndex, endIndex) { 48 | for (const vNode of children.slice(startIndex, endIndex)) { 49 | element.removeChild(vNode) 50 | } 51 | } 52 | 53 | // 54 | // 新增 element.children 中的第 [startIndex, endIndex) 个子元素 55 | // 56 | function addVnodes (element, children, startIndex, endIndex) { 57 | for (const vNode of children.slice(startIndex, endIndex)) { 58 | element.appendChild(createElementByVNode(vNode)) 59 | } 60 | } 61 | 62 | // 63 | // 更新 className、style、attributes 及更多的属性 64 | // 65 | // 在 snabbdom 中,对于特殊的属性更新使用了 `module` 这个概念,做了更精细的增删改查,如 66 | // 67 | // 1. attribute 68 | // 2. class 69 | // 3. style 70 | // 4. dataset 71 | // 5. eventlistenter 72 | // 73 | // const patch = init([ 74 | // classModule, 75 | // propsModule, 76 | // styleModule, 77 | // eventListenersModule, 78 | // ]); 79 | // 80 | // 对于更新而言,ele.props = newVnode.props 可暴力解决,但有时可能效率过低。分一下三种情况进行讨论 81 | // 82 | // 1. 增: 旧节点无,新节点有。 ele.props = newVnode.props 83 | // 2. 删: 新节点无,旧节点有。 ele.props = null 84 | // 3. 改: 旧节点有,新节点有。 ele.props = newVnode.props (暴力解决) 85 | // + 对于需精细控制的 DOM 操作而言,应该仅仅更新补集 (仅更新存在于新节点而在旧节点中不存在的属性) 86 | // + 需要更新的 props 伪代码表示: Add (newVnode and not oldVnode) + Remove (oldVnode and not newVnode) 87 | function updateProps (element, oldVnode, newVnode) { 88 | // 89 | // 更新 DOM 中的 class 90 | // 91 | function updateClass () { 92 | if (oldVnode.props?.class !== newVnode.props?.class) { 93 | 94 | // 对于 class 暴力解决进行更新,如果精细控制可通过 ClassList API 95 | if (newVnode.props?.class) { 96 | element.className = newVnode.props.class 97 | } else { 98 | element.className = '' 99 | } 100 | } 101 | } 102 | 103 | // 示例一: 104 | // { color: 'red', fontSize: '18px' } => { backgroundColor: 'red', fontSize: '18px' } 105 | // 106 | // 示例二: 107 | // { color: 'red', fontSize: '20px' } => { backgroundColor: 'red', fontSize: '18px' } 108 | function updateStyle () { 109 | const newStyle = newVnode.props?.style || {} 110 | 111 | element.style = Object.entries(newStyle).reduce((acc, [key, value]) => { 112 | return `${acc}${key.replace(/[A-Z]/g, x => '-' + x.toLowerCase())}: ${value};` 113 | }, '') 114 | } 115 | 116 | function updateAttributes () { 117 | const newProps = newVnode.props || {} 118 | 119 | Object.entries(newProps).map(([key, value]) => { 120 | if (key !== 'class' && key !== 'style') { 121 | element.setAttribute(key, value) 122 | } 123 | }) 124 | } 125 | 126 | updateClass() 127 | updateStyle() 128 | updateAttributes() 129 | } 130 | 131 | function updateChildren (element, oldChildren, newChildren) { 132 | 133 | if (oldChildren) { 134 | // 如果仅仅在旧的虚拟节点存在 children,则需要在 DOM 中删除旧节点的所有子节点 135 | removeVnodes(element, oldChildren, 0, oldChildren.length) 136 | return 137 | } else if (newChildren) { 138 | // 如果仅仅在新的虚拟节点存在 children,则需要新的虚拟节点构建 DOM 并插入到 element 下 139 | addVnodes(element, newChildren, 0, newChildren.length) 140 | return 141 | } 142 | 143 | let oldVnodeIndex = 0, newVnodeIndex = 0 144 | let oldVnodeEndIndex = oldChildren.length, newVnodeEndIndx = newChildren.length 145 | while (oldVnodeIndex < oldVnodeEndIndex && newVnodeIndex < newVnodeEndIndx) { 146 | const oldVnode = oldChildren[oldVnodeIndex] 147 | const newVnode = newChildren[newVnodeEndIndx] 148 | if (oldVnode.props.key) { 149 | // 以下是旧新节点对比: 150 | // oldKey: 1 2 3 4 5 151 | // newKey: 4 3 5 1 2 152 | // 生成 newChild 关于 key 与 index 的对应关系 153 | // { 4: 0, 3: 1, 5: 2, 1: 3, 2: 4 } 154 | const newChildrendKeyMapId = newChildren.reduce((acc, x, idx) => { 155 | acc[x.key] = idx 156 | return acc 157 | }, {}) 158 | // 找到与当前旧节点 key 对应的新节点的 id 159 | const id = newChildrendKeyMapId[oldVnode.props.key] 160 | if (id) { 161 | // 如果有相同 key 的新旧节点 162 | patch(oldVnode, newChildren[id]); 163 | [newChildren[id], newChildren[newVnodeIndex]] = [newChildren[newVnodeIndex], newChildren[id]] 164 | oldVnodeIndex++ 165 | newVnodeIndex++ 166 | } else { 167 | // 如果在新节点中找不到与旧节点对应的 key,则删掉该旧节点 168 | // oldKey: 2 1 3 4 169 | // newKey: 3 1 170 | // 操作: Delete 2 171 | removeVnodes(element, oldChildren, oldVnodeIndex, oldVnodeIndex + 1) 172 | oldVnodeIndex++ 173 | } 174 | } else { 175 | patch(oldVnode, newVnode) 176 | } 177 | } 178 | addVnodes(element, newChildren, newVnodeIndex, newVnodeEndIndx) 179 | removeVnodes(element, oldChildren, oldVnodeIndex, oldVnodeEndIndx) 180 | } 181 | 182 | function updateText (oldVnode, newVnode) { 183 | const element = oldVnode.element 184 | element.textContent = newVnode.text 185 | } 186 | 187 | // 188 | // 当两个 vNode 标签及 key 相同时,执行 patchVnode 进行更新 189 | // 190 | // 1. 更新 Props 191 | // 2. 更新 Children (重点) 192 | // 3. 更新 Text 193 | // 194 | function patchVnode (oldVnode, newVnode) { 195 | const element = newVnode.element = oldVnode.element 196 | updateProps(element, oldVnode, newVnode) 197 | updateChildren(oldVnode.element, oldVnode.children, newVnode.children) 198 | updateText(oldVnode, newVnode) 199 | } 200 | 201 | function patch (oldVnode, newVnode) { 202 | if (sameVnode(oldVnode, newVnode)) { 203 | patchVnode (oldVnode, newVnode) 204 | } else if (oldVnode instanceof HTMLElement) { 205 | const element = createElementByVNode(newVnode) 206 | oldVnode.appendChild(element) 207 | } else { 208 | createElementByVNode(newVnode) 209 | } 210 | return newVnode 211 | } 212 | 213 | export { patch, h } 214 | -------------------------------------------------------------------------------- /code/express/Readme.md: -------------------------------------------------------------------------------- 1 | # 如何实现一个最小版 express 2 | 3 | > 仓库:[mini-express](https://github.com/shfshanyue/mini-code/tree/master/code/express/) 4 | 5 | 大家好,我是山月。 6 | 7 | `express` 是 Node 中下载量最多的服务端框架,虽然大多归源于 `webpack` 的依赖。今天手写一个迷你版的 `express`,对其内部实现一探究竟。 8 | 9 | ## 山月的代码实现 10 | 11 | 代码置于 [shfshanyue/mini-code:code/express](https://github.com/shfshanyue/mini-code/blob/master/code/express/index.js) 12 | 13 | 可直接读源码,基本每一行都有注释。 14 | 15 | 使用 `npm run example` 或者 `node example` 可运行示例代码 16 | 17 | ``` bash 18 | $ npm run example 19 | ``` 20 | 21 | ## 关于 express 的个人看法 22 | 23 | 1. 重路由的中间件设计。在 `express` 中所有中间件都会通过 `path-to-regexp` 去匹配路由正则,造成一定的性能下降 (较为有限)。 24 | 1. `querystring` 默认中间件。在 express 中,每次请求都内置中间件解析 qs,造成一定的性能下降 (在 koa 中为按需解析)。 25 | 1. 无 Context 的设计。express 把数据存储在 `req` 中,当然也可自定义 `req.context` 用以存储数据。 26 | 1. `res.send` 直接扔回数据,无 `ctx.body` 灵活。 27 | 1. 源码较难理解,且语法过旧,无 koa 代码清晰。 28 | 1. `express` 默认集成了许多中间件,如 static。 29 | 30 | ## express 的中间件设计 31 | 32 | 在 `express` 中可把中间件分为应用级中间件与路由级中间。 33 | 34 | ``` js 35 | // 应用级中间件 A、B 36 | app.use('/api', 37 | (req, res, next) => { 38 | // 应用中间件 A 39 | console.log('Application Level Middleware: A') 40 | }, 41 | (req, res, next) => { 42 | // 应用中间件 B 43 | console.log('Application Level Middleware: B') 44 | } 45 | ) 46 | 47 | // 使用 app.get 注册了一个应用级中间件(路由),且该中间件由路由级中间件 C、D 组成 48 | app.get('/api', 49 | (req, res, next) => { 50 | // 路由中间件 C 51 | console.log('Route Level Middleware: C') 52 | }, 53 | (req, res, next) => { 54 | // 路由中间件 D 55 | console.log('Route Level Middleware: D') 56 | } 57 | ) 58 | ``` 59 | 60 | 在 `express` 中,使用数据结构 `Layer` 维护中间件,而使用 `stack` 维护中间件列表。 61 | 62 | 所有的中间件都挂载在 `Router.prototype.stack` 或者 `Route.prototype.stack` 下,数据结构如下。 63 | 64 | + app.router.stack: 所有的应用级中间件(即 `app.use` 注册的中间件)。 65 | + app.router.stack[0].route.stack: 某一应用级中间件的所有路由级中间件 (即 `app.get` 所注册的中间件)。 66 | 67 | 以下是上述代码关于 `express` 中间件的伪代码数据结构: 68 | 69 | ``` js 70 | const app = { 71 | stack: [ 72 | Layer({ 73 | path: '/api', 74 | handleRequest: 'A 的中间件处理函数' 75 | }), 76 | Layer({ 77 | path: '/api', 78 | handleRequest: 'B 的中间件处理函数' 79 | }), 80 | Layer({ 81 | path: '/api', 82 | handleRequest: 'dispatch: 用以执行该中间件下的所有路由级中间件', 83 | // 对于 app.get 注册的中间件 (应用级路由中间件),将会带有 route 属性,用以存储该中间件的所有路由级别中间件 84 | route: Route({ 85 | path: '/api', 86 | stack: [ 87 | Layer({ 88 | path: '/', 89 | handleRequest: 'C 的中间件处理函数' 90 | }), 91 | Layer({ 92 | path: '/', 93 | handleRequest: 'D 的中间件处理函数' 94 | }) 95 | ] 96 | }) 97 | }) 98 | ] 99 | } 100 | ``` 101 | 102 | 根据以上伪代码,梳理一下在 `express` 中匹配中间件的流程: 103 | 104 | 1. 注册应用级中间件,配置 handleRquest 与 path,并根据 `path` 生成 `regexp`,如 `/api/users/:id` 生成 `/^\/api\/users(?:\/([^\/#\?]+?))[\/#\?]?$/i` 105 | 2. 请求来临时,遍历中间件数组,根据中间件的 `regexp` 匹配请求路径,得到第一个中间件 106 | 3. 第一个中间件中,若有 next 则回到第二步,找到下一个中间件 107 | 4. 遍历结束 108 | 109 | ## Application 的实现 110 | 111 | 在 `Application` 层只需要实现一个功能 112 | 113 | + 抽象并封装 HTTP handleRequest 114 | 115 | ``` js 116 | class Application { 117 | constructor () { 118 | this._router = new Router() 119 | } 120 | 121 | // 在 listen 中处理请求并监听端口号,与 koa 一致,或者说基本所有服务端框架都是这么做的 122 | listen (...args) { 123 | // 创建服务,this.handle 为入口函数,在源码中,express app 本身即为入口函数 124 | const server = http.createServer(this.handle.bind(this)) 125 | server.listen(...args) 126 | } 127 | 128 | handle (req, res) { 129 | const router = this._router 130 | router.handle(req, res) 131 | } 132 | 133 | // 注册应用级中间件,收集所有的应用级中间至 this._router.stack 中,后将实现洋葱模型 134 | use (path, ...fns) { 135 | } 136 | 137 | // 处理 http 的各种 verb,如 get、post、 138 | // 注册匿名应用级中间件 139 | get (path, ...fns) { 140 | } 141 | } 142 | ``` 143 | 144 | ## 中间件的抽象: Layer 145 | 146 | 1. 中间件的抽象 147 | 1. 中间件的匹配 148 | 149 | 中间件要完成几个功能: 150 | 151 | 1. 如何确定匹配 152 | 1. 如何处理请求 153 | 154 | 基于此设计以下数据结构 155 | 156 | ``` js 157 | Layer({ 158 | path, 159 | re, 160 | handle, 161 | options 162 | }) 163 | ``` 164 | 165 | 其中,正则用以匹配请求路径,根据 `path` 生成。那如何获取到路径中定义的参数呢?用捕获组。 166 | 167 | 此时祭出神器 `path-to-regexp`,路径转化为正则。无论 `Express`、`Koa` 等服务端框架,还是 `React`、`Vue` 等客户端框架的路由部分,它对备受青睐。 168 | 169 | ``` js 170 | const { pathToRegexp } = require('path-to-regexp') 171 | 172 | pathToRegexp('/') 173 | //=> /^\/[\/#\?]?$/i 174 | 175 | // 可用以匹配前缀路由 176 | p.pathToRegexp('/', [], { end: false }) 177 | //=> /^\/(?:[\/#\?](?=[]|$))?/i 178 | 179 | // 对于参数,通过捕获组来捕获参数 180 | pathToRegexp('/api/users/:id') 181 | //=> /^\/api\/users(?:\/([^\/#\?]+?))[\/#\?]?$/i 182 | ``` 183 | 184 | 有了正则,关于匹配中间件的逻辑水到渠成,代码如下 185 | 186 | ``` js 187 | 188 | // 对中间件的一层抽象 189 | class Layer { 190 | // 191 | // 当注册路由 app.use('/users/:id', () => {}) 时,其中以下两个想想为 path 和 handle 192 | // path: /users/:id 193 | // handle: () => {} 194 | constructor (path, handle, options) { 195 | this.path = path 196 | this.handle = handle 197 | this.options = options 198 | this.keys = [] 199 | // 根据 path,生政正则表达式 200 | this.re = pathToRegexp(path, this.keys, options) 201 | } 202 | 203 | // 查看请求路径是否匹配该中间件,如果匹配,则返回匹配的 parmas 204 | match (url) { 205 | const matchRoute = regexpToFunction(this.re, this.keys, { decode: decodeURIComponent }) 206 | return matchRoute(url) 207 | } 208 | } 209 | ``` 210 | 211 | ## 中间件的收集 212 | 213 | `app.use` 及 `app.get` 用以收集中间件,较为简单,代码如下: 214 | 215 | ``` js 216 | class Application { 217 | // 注册应用级中间件,收集所有的应用级中间至 this._router.stack 中,后将实现洋葱模型 218 | use (path, ...fns) { 219 | this._router.use(path, ...fns) 220 | } 221 | 222 | // 处理 http 的各种 verb,如 get、post、 223 | // 注册匿名应用级中间件 224 | get (path, ...fns) { 225 | const route = this._router.route(path) 226 | // 对于该应用级中间件所涉及到的所有路由级中间件,在 Route.prototype.get 中进行处理 227 | route.get(...fns) 228 | } 229 | } 230 | 231 | class Router { 232 | constructor () { 233 | // 收集所有应用级中间件 234 | this.stack = [] 235 | } 236 | 237 | // 应用级中间件洋葱模型的实现 238 | handle (req, res) { 239 | } 240 | 241 | // 242 | // app.use('/users/', fn1, fn2, fn3) 243 | // 此处路径在 express 中可省略,则默认为所有路径,为了更好地理解源码,此处不作省略 244 | use (path, ...fns) { 245 | for (const fn of fns) { 246 | const layer = new Layer(path, fn) 247 | this.stack.push(layer) 248 | } 249 | } 250 | 251 | // 注册应用级路由中间件,是一个匿名中间件,维护一系列关于该路径相关的路由级别中间件, 252 | route (path) { 253 | const route = new Route(path) 254 | // 该匿名中间件的 handleRequest 函数为将应用级中间挂载下的所有路由中间件串联处理 255 | // 对于路由级中间件,完全匹配,即 /api 将仅仅匹配 /api 256 | const layer = new Layer(path, route.dispatch.bind(route), { end: true }) 257 | layer.route = route 258 | this.stack.push(layer) 259 | return route 260 | } 261 | } 262 | ``` 263 | 264 | 其中,关于路由级中间件则由 `Route.prototype.stack` 专门负责收集,多个路由级中间件由 `dispatch` 函数组成一个应用中间件,这中间是一个洋葱模型,接下来讲到。 265 | 266 | ## 中间件与洋葱模型 267 | 268 | 洋葱模型实现起来也较为简单,使用 `next` 连接起所有匹配的中间件,按需执行。 269 | 270 | ``` js 271 | function handle (req, res) { 272 | const stack = this.stack 273 | let index = 0 274 | 275 | // 调用下一个应用级中间件 276 | const next = () => { 277 | let layer 278 | let match 279 | 280 | while (!match && index < this.stack.length) { 281 | layer = stack[index++] 282 | // 查看请求路径是否匹配该中间件,如果匹配,则返回匹配的 parmas 283 | match = layer.match(req.url) 284 | } 285 | // 遍历中间件,如果无一路径匹配,则状态码为 404 286 | if (!match) { 287 | res.status = 404 288 | res.end('NOT FOUND SHANYUE') 289 | return 290 | } 291 | req.params = match.params 292 | // 处理中间件的函数,如果中间件中调用了 next(),则往下走下一个中间件 293 | layer.handle(req, res, next) 294 | } 295 | next() 296 | } 297 | ``` 298 | 299 | 相较而言,路由级中间件洋葱模型的实现简单很多 300 | 301 | ``` js 302 | function dispatch (req, res, done) { 303 | let index = 0 304 | const stack = this.stack 305 | const next = () => { 306 | const layer = stack[index++] 307 | 308 | // 如果最后一个 309 | if (!layer) { done() } 310 | layer.handle(req, res, next) 311 | } 312 | next() 313 | } 314 | ``` 315 | ## 结语 316 | 317 | 完。 318 | 319 | 等一下,记得吃早饭。 320 | -------------------------------------------------------------------------------- /code/native-http-server/index.js: -------------------------------------------------------------------------------- 1 | const net = require('net') 2 | const Stream = require('stream'); 3 | 4 | // _ 代表私有变量,是 Node 源码中的命名风格,尽管在目前最新的 ES 中可以使用 # 作为私有变量,但是太丑,不予采用 5 | 6 | const CRLF = '\r\n' 7 | const HIGH_WATER_MARK = 65535 8 | const STATUS_CODES = { 9 | 100: 'Continue', // RFC 7231 6.2.1 10 | 101: 'Switching Protocols', // RFC 7231 6.2.2 11 | 102: 'Processing', // RFC 2518 10.1 (obsoleted by RFC 4918) 12 | 103: 'Early Hints', // RFC 8297 2 13 | 200: 'OK', // RFC 7231 6.3.1 14 | 201: 'Created', // RFC 7231 6.3.2 15 | 202: 'Accepted', // RFC 7231 6.3.3 16 | 203: 'Non-Authoritative Information', // RFC 7231 6.3.4 17 | 204: 'No Content', // RFC 7231 6.3.5 18 | 205: 'Reset Content', // RFC 7231 6.3.6 19 | 206: 'Partial Content', // RFC 7233 4.1 20 | 207: 'Multi-Status', // RFC 4918 11.1 21 | 208: 'Already Reported', // RFC 5842 7.1 22 | 226: 'IM Used', // RFC 3229 10.4.1 23 | 300: 'Multiple Choices', // RFC 7231 6.4.1 24 | 301: 'Moved Permanently', // RFC 7231 6.4.2 25 | 302: 'Found', // RFC 7231 6.4.3 26 | 303: 'See Other', // RFC 7231 6.4.4 27 | 304: 'Not Modified', // RFC 7232 4.1 28 | 305: 'Use Proxy', // RFC 7231 6.4.5 29 | 307: 'Temporary Redirect', // RFC 7231 6.4.7 30 | 308: 'Permanent Redirect', // RFC 7238 3 31 | 400: 'Bad Request', // RFC 7231 6.5.1 32 | 401: 'Unauthorized', // RFC 7235 3.1 33 | 402: 'Payment Required', // RFC 7231 6.5.2 34 | 403: 'Forbidden', // RFC 7231 6.5.3 35 | 404: 'Not Found', // RFC 7231 6.5.4 36 | 405: 'Method Not Allowed', // RFC 7231 6.5.5 37 | 406: 'Not Acceptable', // RFC 7231 6.5.6 38 | 407: 'Proxy Authentication Required', // RFC 7235 3.2 39 | 408: 'Request Timeout', // RFC 7231 6.5.7 40 | 409: 'Conflict', // RFC 7231 6.5.8 41 | 410: 'Gone', // RFC 7231 6.5.9 42 | 411: 'Length Required', // RFC 7231 6.5.10 43 | 412: 'Precondition Failed', // RFC 7232 4.2 44 | 413: 'Payload Too Large', // RFC 7231 6.5.11 45 | 414: 'URI Too Long', // RFC 7231 6.5.12 46 | 415: 'Unsupported Media Type', // RFC 7231 6.5.13 47 | 416: 'Range Not Satisfiable', // RFC 7233 4.4 48 | 417: 'Expectation Failed', // RFC 7231 6.5.14 49 | 418: 'I\'m a Teapot', // RFC 7168 2.3.3 50 | 421: 'Misdirected Request', // RFC 7540 9.1.2 51 | 422: 'Unprocessable Entity', // RFC 4918 11.2 52 | 423: 'Locked', // RFC 4918 11.3 53 | 424: 'Failed Dependency', // RFC 4918 11.4 54 | 425: 'Too Early', // RFC 8470 5.2 55 | 426: 'Upgrade Required', // RFC 2817 and RFC 7231 6.5.15 56 | 428: 'Precondition Required', // RFC 6585 3 57 | 429: 'Too Many Requests', // RFC 6585 4 58 | 431: 'Request Header Fields Too Large', // RFC 6585 5 59 | 451: 'Unavailable For Legal Reasons', // RFC 7725 3 60 | 500: 'Internal Server Error', // RFC 7231 6.6.1 61 | 501: 'Not Implemented', // RFC 7231 6.6.2 62 | 502: 'Bad Gateway', // RFC 7231 6.6.3 63 | 503: 'Service Unavailable', // RFC 7231 6.6.4 64 | 504: 'Gateway Timeout', // RFC 7231 6.6.5 65 | 505: 'HTTP Version Not Supported', // RFC 7231 6.6.6 66 | 506: 'Variant Also Negotiates', // RFC 2295 8.1 67 | 507: 'Insufficient Storage', // RFC 4918 11.5 68 | 508: 'Loop Detected', // RFC 5842 7.2 69 | 509: 'Bandwidth Limit Exceeded', 70 | 510: 'Not Extended', // RFC 2774 7 71 | 511: 'Network Authentication Required' // RFC 6585 6 72 | }; 73 | 74 | class HTTPParser { 75 | constructor (socket) { 76 | this.socket = socket 77 | this.headerMessage = '' 78 | } 79 | 80 | // 为了服务器的性能,此部分的解析交由 C++ 的 llhttp 来完成 81 | parser () { 82 | this.socket.on('data', (chunk) => { 83 | this.headerMessage += chunk 84 | 85 | // 当接收到 headers 报文时,判断是否以 \r\n\r\n 结尾,表明 header 已经全部接收 86 | if (this.headerMessage.endsWith('\r\n\r\n')) { 87 | this.parserOnData(this.headerMessage) 88 | 89 | // 接受结束本次报文后,再把 header 置空 90 | this.headerMessage = '' 91 | } 92 | }) 93 | } 94 | 95 | // 在该方法中用以解析 method、url、version 以及 headers,此处方法较为粗糙,示例: 96 | // 97 | // 'GET /index.html HTTP/1.1', 98 | // 'Hostname: shanyue.tech', 99 | // 'Content-Length: 9', 100 | // 101 | parserOnData (headerMessage) { 102 | const rows = headerMessage.split('\r\n') 103 | const [method, url, version] = rows[0].split(' ') 104 | 105 | // 解析 Request Header,注意 Hostname: localhost:8080 有可能有两个冒号,也有可能没有冒号 106 | const headers = rows.slice(1, -2) 107 | .filter(row => row.includes(':')) 108 | .map(x => { 109 | const [field, ...value] = x.split(/:/) 110 | return [field.trim(), value.join('').trim()] 111 | }) 112 | this.parserOnHeadersComplete(version, headers, method, url) 113 | } 114 | 115 | parserOnHeadersComplete(version, headers, method, url) { 116 | const req = new IncomingMessage(this.socket) 117 | req.version = version 118 | req.headers = Object.fromEntries(headers) 119 | req.method = method 120 | req.url = url 121 | 122 | return this.onIncoming(req) 123 | } 124 | } 125 | 126 | class IncomingMessage extends Stream.Readable { 127 | constructor(socket) { 128 | super({ 129 | autoDestroy: false, 130 | highWaterMark: socket.readable.highWaterMark 131 | }) 132 | this.socket = socket 133 | this.trailers = null 134 | this.complete = false 135 | this.headerMessage = '' 136 | this.headers = [] 137 | this.rawHeaders = '' 138 | this.url = null 139 | this.method = null 140 | this._readableState.readingMore = true 141 | 142 | } 143 | 144 | _addHeaderLines(headers) { 145 | this.headers = headers 146 | } 147 | } 148 | 149 | // 在 node.js 源码中,ServerResponse 与 ClientRequest 均继承了 OutgoingMessage 150 | class OutgoingMessage extends Stream { 151 | constructor() { 152 | super() 153 | 154 | // 关于 header 的报文信息,当报文发送时赋值构建,如果有值代表 header 即将发送 155 | this._header = null 156 | this._headerSent = false 157 | this._contentLength = 0 158 | this._hasBody = true 159 | this._onPendingData = () => { } 160 | 161 | // 存储 header 的键值对 162 | this.headers = Object.create(null) 163 | this.socket = null 164 | this.outputSize = 0 165 | this.finished = false 166 | this.outputData = [] 167 | this._last = false 168 | } 169 | 170 | _writeRaw(data, encoding, callback) { 171 | if (this.socket.writable) { 172 | return this.socket.write(data, encoding, callback) 173 | } 174 | this.outputData.push({ data, encoding, callback }); 175 | this.outputSize += data.length; 176 | this._onPendingData(data.length); 177 | return this.outputSize < HIGH_WATER_MARK 178 | } 179 | 180 | _send(data, encoding, callback) { 181 | if (!this._headerSent) { 182 | data = this._header + data 183 | this._headerSent = true 184 | } 185 | return this._writeRaw(data, encoding, callback) 186 | } 187 | 188 | write(chunk, encoding, callback) { 189 | if (!this._header) { 190 | this._implicitHeader(200) 191 | } 192 | this._send(chunk, encoding, callback) 193 | } 194 | 195 | setHeader(key, value) { 196 | this.headers[key.toLowerCase()] = value 197 | } 198 | 199 | end(chunk, encoding, callback) { 200 | if (!this._header) { 201 | this._contentLength = Buffer.byteLength(chunk) 202 | this._implicitHeader(200) 203 | } 204 | 205 | // // 拿暖壶瓶子接水,接水满了统一处理 206 | // this.socket.cork() 207 | 208 | this._send(chunk, encoding, callback) 209 | this._send('', 'latin1', () => { 210 | this.emit('finish') 211 | }) 212 | 213 | // 把暖壶瓶子的水全部倒出来进行处理 214 | // this.socket._writableState.corked = 1 215 | // this.socket.uncork() 216 | 217 | this.finished = true 218 | } 219 | } 220 | 221 | class ServerResponse extends OutgoingMessage { 222 | constructor(req) { 223 | super() 224 | this._removedTE = false 225 | this.statusCode = 200 226 | this.socket = req.socket 227 | 228 | // 该响应报文是否为 chunkedEncoding,即响应头: transfer-encoding: chunked 229 | this.chunkedEncoding = false 230 | 231 | // 当处理完响应报文时触发,onFinish 在 end 中处理完最后一个报文时触发 232 | this.on('finish', () => { 233 | this.socket.end() 234 | }) 235 | } 236 | 237 | _implicitHeader(statusCode) { 238 | this.writeHead(statusCode) 239 | } 240 | 241 | // 生成 HTTP 响应头的报文 242 | writeHead(statusCode) { 243 | this.statusCode = statusCode 244 | 245 | let header = `HTTP/1.1 ${statusCode} ${STATUS_CODES[statusCode]}${CRLF}` 246 | 247 | header += 'Date: ' + new Date().toUTCString() + CRLF 248 | 249 | // 相应地,为该 socket 设置 5s 的超时 250 | // header += `Connection: keep-alive${CRLF}` 251 | // header += `Keep-Alive: timeout=5}${CRLF}` 252 | 253 | if (this._contentLength) { 254 | header += 'Content-Length: ' + this._contentLength + CRLF 255 | } else if (this._removedTE) { 256 | header += 'Transfer-Encoding: chunked' + CRLF 257 | this.chunkedEncoding = true; 258 | } 259 | 260 | this._header = header + CRLF 261 | } 262 | } 263 | 264 | class HTTPServer extends net.Server { 265 | constructor(requestListener) { 266 | super({ 267 | allowHalfOpen: true 268 | }) 269 | 270 | this.on('request', requestListener) 271 | this.on('connection', socket => this.connectionListener(socket)); 272 | 273 | this.socket = null 274 | this.timeout = 0 275 | this.keepAliveTimeout = 5000 276 | this.maxHeadersCount = null 277 | this.headersTimeout = 60 * 1000 278 | this.requestTimeout = 0 279 | } 280 | 281 | // 当解析完本次请求的报文时触发此事件,生成 req/res,进入监听请求的回调函数中,即 requestListener 282 | // 该函数,在 http_parser 解析完 header 时,回调触发 283 | onIncoming(req) { 284 | const res = new ServerResponse(req) 285 | 286 | // 触发事件 `request`,当接收到 request 时,进入 requestListener 回调,即 HTTP Server 的入口函数 287 | this.emit('request', req, res) 288 | } 289 | 290 | // 当每次客户端与服务端建立连接时,触发该监听器 291 | connectionListener(socket) { 292 | this.socket = socket 293 | 294 | const parser = new HTTPParser(socket) 295 | parser.onIncoming = this.onIncoming.bind(this) 296 | parser.parser() 297 | } 298 | 299 | } 300 | 301 | function createServer(requestListener) { 302 | return new HTTPServer(requestListener) 303 | } 304 | 305 | module.exports = { createServer } 306 | -------------------------------------------------------------------------------- /code/koa/Readme.md: -------------------------------------------------------------------------------- 1 | # 如何实现一个最小版 Koa 2 | 3 | > 仓库:[mini-koa](https://github.com/shfshanyue/mini-code/tree/master/code/koa) 4 | 5 | 大家好,我是山月。 6 | 7 | Koa 的源码通俗易懂,仅仅有四个文件,Koa 的下载量奇高,是最受欢迎的服务端框架之一。Koa 也是我最推荐阅读源码源码的库或框架。 8 | 9 | 这里山月使用四十行代码实现一个最简化的 Koa。 10 | 11 | ## 如何阅读及调试源码 12 | 13 | 详见 [koa 源码调试](./koa.md) 14 | 15 | ## 山月的代码实现 16 | 17 | 代码置于 [shfshanyue/mini-code:code/koa](https://github.com/shfshanyue/mini-code/blob/master/code/koa/index.js) 18 | 19 | 可直接读源码,基本每一行都有注释。 20 | 21 | 使用 `npm run example` 或者 `node example` 可运行示例代码 22 | 23 | ``` bash 24 | $ npm run example 25 | ``` 26 | 27 | ## 演示与示例 28 | 29 | 以下是一个 koa 核心功能洋葱模型最简化也是最经典的示例: 30 | 31 | ``` js 32 | const Koa = require('koa') 33 | const app = new Koa() 34 | 35 | app.use(async (ctx, next) => { 36 | console.log('Middleware 1 Start') 37 | await next() 38 | console.log('Middleware 1 End') 39 | }) 40 | 41 | app.use(async (ctx, next) => { 42 | console.log('Middleware 2 Start') 43 | await next() 44 | console.log('Middleware 2 End') 45 | 46 | ctx.body = 'hello, world' 47 | }) 48 | 49 | app.listen(3000) 50 | 51 | // 访问任意路由时,将在终端打印以下内容: 52 | // Middleware 1 Start 53 | // Middleware 2 Start 54 | // Middleware 2 End 55 | // Middleware 1 End 56 | ``` 57 | 58 | 在这个最简化的示例中,可以看到有三个清晰的模块,分别如下: 59 | 60 | + Application: 基本服务器框架 61 | + Context: 服务器框架各种基本数据结构的封装,用以 http 请求解析及响应 (由于 Context 包容万物,所以也被称为垃圾桶...) 62 | + Middleware: 中间件,也是洋葱模型的核心机制 63 | 64 | 我们开始逐步实现这三个模块。 65 | 66 | ## 抛开框架,来写一个原生 server 67 | 68 | 我们先基于 node 最基本的 [HTTP API](https://nodejs.org/api/http.html) 来启动一个 http 服务,并通过它来实现最简版的 koa。 69 | 70 | 71 | ``` js 72 | const http = require('http') 73 | 74 | const server = http.createServer((req, res) => { res.end('hello, world') }) 75 | 76 | server.listen(3000) 77 | ``` 78 | 79 | 其中最重要的函数 `http.createServer` 用以创建一个 http 服务,它将会回调获取到两个最重要的参数: Request/Response 关于请求及响应的一切。 80 | 81 | ``` ts 82 | // 创建一个 http 服务,在回调函数即 requestListener 中获取 req/res 83 | function createServer(requestListener?: RequestListener): Server; 84 | 85 | // req、res 分别是一个可读流与可写流 86 | type RequestListener = (req: IncomingMessage, res: ServerResponse) => void; 87 | ``` 88 | 89 | 假设已完成最简版的 `koa` 示例如下,我把最简版的这个 koa 命名为 `mini-koa` 90 | 91 | ``` js 92 | const Koa = require('mini-koa') 93 | const app = new Koa() 94 | 95 | app.use(async (ctx, next) => { 96 | console.log('Middleware 1 Start') 97 | await next() 98 | console.log('Middleware 1 End') 99 | }) 100 | 101 | app.use(async (ctx, next) => { 102 | console.log('Middleware 2 Start') 103 | await next() 104 | console.log('Middleware 2 End') 105 | 106 | ctx.body = 'hello, world' 107 | }) 108 | 109 | app.listen(3000) 110 | ``` 111 | 112 | 从上述代码,可以看出有待实现两个核心 API: 113 | 114 | 1. `new Koa`: 构建 Appliaction 115 | 1. `app.use/ctx`: 构建中间件注册函数与 Context 116 | 117 | 以下将逐步实现: 118 | 119 | ## 构建 Application 120 | 121 | 首先通过 `http.createServer` 构造完成 `Appliacation` 的大体框架: 122 | 123 | 1. `app.listen`: 封装 `http.createServer`,处理请求及响应,并且监听端口 124 | 1. `app.use`: 中间件注册函数,目前阶段仅处理请求并完成响应 125 | 126 | 只有简单的十几行代码,示例如下: 127 | 128 | ``` js 129 | const http = require('http') 130 | 131 | class Application { 132 | constructor () { 133 | this.middleware = null 134 | } 135 | 136 | listen (...args) { 137 | const server = http.createServer(this.middleware) 138 | server.listen(...args) 139 | } 140 | 141 | // 这里依旧调用的是原生 http.createServer 的回调函数 142 | use (middleware) { 143 | this.middleware = middleware 144 | } 145 | } 146 | ``` 147 | 148 | 调用 `Application` 启动服务的代码如下: 149 | 150 | ``` js 151 | const app = new Appliacation() 152 | 153 | app.use((req, res) => { 154 | res.end('hello, world') 155 | }) 156 | 157 | app.listen(3000) 158 | ``` 159 | 160 | 由于 `app.use` 的回调函数依然是原生的 `http.crateServer` 回调函数,而在 `koa` 中回调参数是一个 `Context` 对象。 161 | 162 | 下一步要做的将是使用 `req/res` 构建 `Context` 对象。 163 | 164 | ## 构建 Context 165 | 166 | 在 koa 中,`app.use` 的回调参数为一个 `ctx` 对象,而非原生的 `req/res`。 167 | 168 | 这一步将构建一个 `Context` 对象,并使用 `ctx.body` 响应数据。 169 | 170 | 核心 API 如下: 171 | 172 | + `app.use(ctx => ctx.body = 'hello, world')`: 通过在 `http.createServer` 回调函数中进一步封装 `Context` 实现 173 | + `Context(req, res)`: 以 `request/response` 数据结构为主体构造 Context 对象 174 | 175 | 核心代码如下,注意注释部分: 176 | 177 | ``` js 178 | const http = require('http') 179 | 180 | class Application { 181 | constructor () {} 182 | use () {} 183 | 184 | listen (...args) { 185 | const server = http.createServer((req, res) => { 186 | // 构造 Context 对象 187 | const ctx = new Context(req, res) 188 | 189 | // 此时处理为与 koa 兼容 Context 的 app.use 函数 190 | this.middleware(ctx) 191 | 192 | // ctx.body 为响应内容 193 | ctx.res.end(ctx.body) 194 | }) 195 | server.listen(...args) 196 | } 197 | } 198 | 199 | // 构造一个 Context 的类 200 | class Context { 201 | constructor (req, res) { 202 | this.req = req 203 | this.res = res 204 | } 205 | } 206 | ``` 207 | 208 | 此时 `koa` 被改造如下,`app.use` 可以正常工作: 209 | 210 | ``` js 211 | const app = new Application() 212 | 213 | app.use(ctx => { 214 | ctx.body = 'hello, world' 215 | }) 216 | 217 | app.listen(7000) 218 | ``` 219 | 220 | 实现以上的代码都很简单,现在就剩下一个最重要也是最核心的功能:**洋葱模型** 221 | 222 | ## 洋葱模型及中间件改造 223 | 224 | 上述工作只有简单的一个中间件,然而在现实中中间件会有很多个,如错误处理,权限校验,路由,日志,限流等等。 225 | 226 | 因此我们要改造下 `app.middlewares` 使之成为一个数组: 227 | 228 | + `app.middlewares`: 收集中间件回调函数数组,并并使用 `compose` 串联起来 229 | 230 | 对所有中间件函数通过 `compose` 函数来达到抽象效果,它将对 `Context` 对象作为参数,来接收请求及处理响应: 231 | 232 | ``` js 233 | // this.middlewares 代表所有中间件 234 | // 通过 compose 抽象 235 | const fn = compose(this.middlewares) 236 | await fn(ctx) 237 | 238 | // 当然,也可以写成这种形式,只要带上 ctx 参数 239 | await compose(this.middlewares, ctx) 240 | ``` 241 | 242 | 先不论 `compose` 的实现,此时先完成中间件函数的收集工作: 243 | 244 | ``` js 245 | const http = require('http') 246 | 247 | class Application { 248 | constructor () { 249 | this.middlewares = [] 250 | } 251 | 252 | listen (...args) { 253 | const server = http.createServer(async (req, res) => { 254 | const ctx = new Context(req, res) 255 | 256 | // 对中间件回调函数串联,形成洋葱模型 257 | // 1. 路由解析 258 | // 2. Body解析 259 | // 3. 异常处理 260 | // 4. 统一认证 261 | // 5. 等等... 262 | const fn = compose(this.middlewares) 263 | await fn(ctx) 264 | 265 | ctx.res.end(ctx.body) 266 | }) 267 | server.listen(...args) 268 | } 269 | 270 | use (middleware) { 271 | // 中间件回调函数变为了数组 272 | this.middlewares.push(middleware) 273 | } 274 | } 275 | ``` 276 | 277 | 接下来,重点完成 `compose` 函数,实现洋葱模型的核心。 278 | 279 | ## 洋葱模型核心: compose 函数封装 280 | 281 | koa 的洋葱模型指每一个中间件都像是洋葱的每一层,当一根针从洋葱中心穿过时,每层都会一进一出穿过两次,且最先穿入的一层最后穿出。 282 | 283 | 此时该祭出洋葱模型的神图了: 284 | 285 | ![洋葱模型](https://cdn.jsdelivr.net/gh/shfshanyue/assets@master/src/image.4mum5xlibau0.png) 286 | 287 | + `middleware`: 第一个中间件将会执行 288 | + `next`: 每个中间件将会通过 next 来执行下一个中间件 289 | 290 | 我们如何实现所有的中间件的洋葱模型呢? 291 | 292 | 我们看一看每一个 middleware 的 API 如下 293 | 294 | ``` js 295 | middleware(ctx, next) 296 | ``` 297 | 298 | 而每个中间件中的 `next` 是指执行下一个中间件,我们来把 `next` 函数提取出来,而 `next` 函数中又有 `next`,这应该怎么处理呢? 299 | 300 | ``` js 301 | const next = () => nextMiddleware(ctx, next) 302 | middleware(ctx, next(0)) 303 | ``` 304 | 305 | 是了,使用一个递归完成中间件的改造,并把中间件给连接起来,如下所示: 306 | 307 | ``` js 308 | // dispatch(i) 代表执行第 i 个中间件 309 | const dispatch = (i) => { 310 | return middlewares[i](ctx, () => dispatch(i+1)) 311 | } 312 | dispatch(0) 313 | ``` 314 | 315 | **`dispatch(i)` 代表执行第 i 个中间件,而 `next()` 函数将会执行下一个中间件 `dispatch(i+1)`,于是我们使用递归轻松地完成了洋葱模型。** 316 | 317 | 此时,再把递归的终止条件补充上: 当最后一个中间件函数执行 `next()` 时,直接返回 318 | 319 | ``` js 320 | const dispatch = (i) => { 321 | const middleware = middlewares[i] 322 | if (i === middlewares.length) { 323 | return 324 | } 325 | return middleware(ctx, () => dispatch(i+1)) 326 | } 327 | return dispatch(0) 328 | ``` 329 | 330 | 最终的 `compose` 函数代码如下: 331 | 332 | ``` js 333 | function compose (middlewares) { 334 | return ctx => { 335 | const dispatch = (i) => { 336 | const middleware = middlewares[i] 337 | if (i === middlewares.length) { 338 | return 339 | } 340 | return middleware(ctx, () => dispatch(i+1)) 341 | } 342 | return dispatch(0) 343 | } 344 | } 345 | ``` 346 | 347 | 至此,koa 的核心功能洋葱模型已经完成,写个示例来体验一下吧: 348 | 349 | ``` js 350 | const app = new Application() 351 | 352 | app.use(async (ctx, next) => { 353 | ctx.body = 'hello, one' 354 | await next() 355 | }) 356 | 357 | app.use(async (ctx, next) => { 358 | ctx.body = 'hello, two' 359 | await next() 360 | }) 361 | 362 | app.listen(7000) 363 | ``` 364 | 365 | 此时还有一个小小的但不影响全局的不足:异常处理,下一步将会完成异常捕获的代码 366 | 367 | ## 异常处理 368 | 369 | 如果在你的后端服务中因为某一处报错,而把整个服务给挂掉了怎么办? 370 | 371 | 我们只需要对中间件执行函数进行一次异常处理即可: 372 | 373 | ``` js 374 | try { 375 | const fn = compose(this.middlewares) 376 | await fn(ctx) 377 | } catch (e) { 378 | console.error(e) 379 | ctx.res.statusCode = 500 380 | ctx.res.write('Internel Server Error') 381 | } 382 | ``` 383 | 384 | 然而在日常项目中使用时,我们**必须**在框架层的异常捕捉之前就需要捕捉到它,来做一些异常结构化及异常上报的任务,此时会使用一个异常处理的中间件: 385 | 386 | ``` js 387 | // 错误处理中间件 388 | app.use(async (ctx, next) => { 389 | try { 390 | await next(); 391 | } 392 | catch (err) { 393 | // 1. 异常结构化 394 | // 2. 异常分类 395 | // 3. 异常级别 396 | // 4. 异常上报 397 | } 398 | }) 399 | ``` 400 | 401 | ## 代码 402 | 403 | 以下是关于实现最小化 `koa` 的所有代码,并添加了注释。 404 | 405 | 也可以在此查看源码 [index.js](https://github.com/shfshanyue/mini-code/blob/master/code/koa/index.js)。 406 | 407 | ``` js 408 | // [http 模块](https://nodejs.org/api/http.html),构建 Node 框架的核心 API 409 | const http = require('http') 410 | 411 | // koa 团队通过额外实现一个库: [koa-compose](https://github.com/koajs/compose),来完成洋葱模型的核心,尽管 koa-compose 的核心代码只有十几行 412 | // 以下是洋葱模型的核心实现,可参考 [简述 koa 的中间件原理,手写 koa-compose 代码](https://github.com/shfshanyue/Daily-Question/issues/643) 413 | function compose (middlewares) { 414 | return ctx => { 415 | const dispatch = (i) => { 416 | const middleware = middlewares[i] 417 | if (i === middlewares.length) { 418 | return 419 | } 420 | // 421 | // app.use((ctx, next) => {}) 422 | // 取出当前中间件,并执行 423 | // 当在中间件中调用 next() 时,此时将控制权交给下一个中间件,也是洋葱模型的核心 424 | // 如果中间件未调用 next(),则接下来的中间件将不会执行 425 | return middleware(ctx, () => dispatch(i+1)) 426 | } 427 | // 从第一个中间件开始执行 428 | return dispatch(0) 429 | } 430 | } 431 | 432 | 433 | // 在 koa 代码中,使用 Context 对 req/res 进行了封装 434 | // 并把 req/res 中多个属性代理到 Context 中,方便访问 435 | class Context { 436 | constructor (req, res) { 437 | this.req = req 438 | this.res = res 439 | } 440 | } 441 | 442 | class Application { 443 | constructor () { 444 | this.middlewares = [] 445 | } 446 | 447 | listen (...args) { 448 | // 在 listen 中处理请求并监听端口号 449 | const server = http.createServer(this.callback()) 450 | server.listen(...args) 451 | } 452 | 453 | // 在 koa 中,app.callback() 将返回 Node HTTP API标准的 handleRequest 函数,方便测试 454 | callback () { 455 | return async (req, res) => { 456 | const ctx = new Context(req, res) 457 | 458 | // 使用 compose 合成所有中间件,在中间件中会做一些 459 | // 1. 路由解析 460 | // 2. Body解析 461 | // 3. 异常处理 462 | // 4. 统一认证 463 | // 5. 等等... 464 | const fn = compose(this.middlewares) 465 | 466 | try { 467 | await fn(ctx) 468 | } catch (e) { 469 | // 最基本的异常处理函数,在实际生产环境中,将由一个专业的异常处理中间件来替代,同时也会做 470 | // 1. 确认异常级别 471 | // 2. 异常上报 472 | // 3. 构造与异常对应的状态码,如 429、422 等 473 | console.error(e) 474 | ctx.res.statusCode = 500 475 | ctx.res.end('Internel Server Error') 476 | } 477 | ctx.res.end(ctx.body) 478 | } 479 | } 480 | 481 | // 注册中间件,并收集在中间件数组中 482 | use (middleware) { 483 | this.middlewares.push(middleware) 484 | } 485 | } 486 | 487 | module.exports = Application 488 | 489 | ``` 490 | 491 | ## 小结 492 | 493 | `koa` 的核心代码特别简单,如果你是一个 Node 工程师,非常建议在业务之余研究一下 koa 的源码,并且自己也实现一个最简版的 koa。 494 | -------------------------------------------------------------------------------- /code/apollo-server/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-server", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "graphql-server", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "@graphql-tools/schema": "^8.3.2", 13 | "graphql": "^16.3.0", 14 | "raw-body": "^2.5.0" 15 | } 16 | }, 17 | "node_modules/@graphql-tools/merge": { 18 | "version": "8.2.3", 19 | "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.3.tgz", 20 | "integrity": "sha512-XCSmL6/Xg8259OTWNp69B57CPWiVL69kB7pposFrufG/zaAlI9BS68dgzrxmmSqZV5ZHU4r/6Tbf6fwnEJGiSw==", 21 | "dependencies": { 22 | "@graphql-tools/utils": "^8.6.2", 23 | "tslib": "~2.3.0" 24 | }, 25 | "peerDependencies": { 26 | "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" 27 | } 28 | }, 29 | "node_modules/@graphql-tools/schema": { 30 | "version": "8.3.2", 31 | "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.2.tgz", 32 | "integrity": "sha512-77feSmIuHdoxMXRbRyxE8rEziKesd/AcqKV6fmxe7Zt+PgIQITxNDew2XJJg7qFTMNM43W77Ia6njUSBxNOkwg==", 33 | "dependencies": { 34 | "@graphql-tools/merge": "^8.2.3", 35 | "@graphql-tools/utils": "^8.6.2", 36 | "tslib": "~2.3.0", 37 | "value-or-promise": "1.0.11" 38 | }, 39 | "peerDependencies": { 40 | "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" 41 | } 42 | }, 43 | "node_modules/@graphql-tools/utils": { 44 | "version": "8.6.2", 45 | "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.2.tgz", 46 | "integrity": "sha512-x1DG0cJgpJtImUlNE780B/dfp8pxvVxOD6UeykFH5rHes26S4kGokbgU8F1IgrJ1vAPm/OVBHtd2kicTsPfwdA==", 47 | "dependencies": { 48 | "tslib": "~2.3.0" 49 | }, 50 | "peerDependencies": { 51 | "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" 52 | } 53 | }, 54 | "node_modules/bytes": { 55 | "version": "3.1.2", 56 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 57 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 58 | "engines": { 59 | "node": ">= 0.8" 60 | } 61 | }, 62 | "node_modules/depd": { 63 | "version": "2.0.0", 64 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 65 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 66 | "engines": { 67 | "node": ">= 0.8" 68 | } 69 | }, 70 | "node_modules/graphql": { 71 | "version": "16.3.0", 72 | "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.3.0.tgz", 73 | "integrity": "sha512-xm+ANmA16BzCT5pLjuXySbQVFwH3oJctUVdy81w1sV0vBU0KgDdBGtxQOUd5zqOBk/JayAFeG8Dlmeq74rjm/A==", 74 | "engines": { 75 | "node": "^12.22.0 || ^14.16.0 || >=16.0.0" 76 | } 77 | }, 78 | "node_modules/http-errors": { 79 | "version": "2.0.0", 80 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 81 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 82 | "dependencies": { 83 | "depd": "2.0.0", 84 | "inherits": "2.0.4", 85 | "setprototypeof": "1.2.0", 86 | "statuses": "2.0.1", 87 | "toidentifier": "1.0.1" 88 | }, 89 | "engines": { 90 | "node": ">= 0.8" 91 | } 92 | }, 93 | "node_modules/iconv-lite": { 94 | "version": "0.4.24", 95 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 96 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 97 | "dependencies": { 98 | "safer-buffer": ">= 2.1.2 < 3" 99 | }, 100 | "engines": { 101 | "node": ">=0.10.0" 102 | } 103 | }, 104 | "node_modules/inherits": { 105 | "version": "2.0.4", 106 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 107 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 108 | }, 109 | "node_modules/raw-body": { 110 | "version": "2.5.0", 111 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.0.tgz", 112 | "integrity": "sha512-XpyZ6O7PVu3ItMQl0LslfsRoKxMOxi3SzDkrOtxMES5AqLFpYjQCryxI4LGygUN2jL+RgFsPkMPPlG7cg/47+A==", 113 | "dependencies": { 114 | "bytes": "3.1.2", 115 | "http-errors": "2.0.0", 116 | "iconv-lite": "0.4.24", 117 | "unpipe": "1.0.0" 118 | }, 119 | "engines": { 120 | "node": ">= 0.8" 121 | } 122 | }, 123 | "node_modules/safer-buffer": { 124 | "version": "2.1.2", 125 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 126 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 127 | }, 128 | "node_modules/setprototypeof": { 129 | "version": "1.2.0", 130 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 131 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 132 | }, 133 | "node_modules/statuses": { 134 | "version": "2.0.1", 135 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 136 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 137 | "engines": { 138 | "node": ">= 0.8" 139 | } 140 | }, 141 | "node_modules/toidentifier": { 142 | "version": "1.0.1", 143 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 144 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 145 | "engines": { 146 | "node": ">=0.6" 147 | } 148 | }, 149 | "node_modules/tslib": { 150 | "version": "2.3.1", 151 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", 152 | "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" 153 | }, 154 | "node_modules/unpipe": { 155 | "version": "1.0.0", 156 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 157 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", 158 | "engines": { 159 | "node": ">= 0.8" 160 | } 161 | }, 162 | "node_modules/value-or-promise": { 163 | "version": "1.0.11", 164 | "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz", 165 | "integrity": "sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==", 166 | "engines": { 167 | "node": ">=12" 168 | } 169 | } 170 | }, 171 | "dependencies": { 172 | "@graphql-tools/merge": { 173 | "version": "8.2.3", 174 | "resolved": "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.3.tgz", 175 | "integrity": "sha512-XCSmL6/Xg8259OTWNp69B57CPWiVL69kB7pposFrufG/zaAlI9BS68dgzrxmmSqZV5ZHU4r/6Tbf6fwnEJGiSw==", 176 | "requires": { 177 | "@graphql-tools/utils": "^8.6.2", 178 | "tslib": "~2.3.0" 179 | } 180 | }, 181 | "@graphql-tools/schema": { 182 | "version": "8.3.2", 183 | "resolved": "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.2.tgz", 184 | "integrity": "sha512-77feSmIuHdoxMXRbRyxE8rEziKesd/AcqKV6fmxe7Zt+PgIQITxNDew2XJJg7qFTMNM43W77Ia6njUSBxNOkwg==", 185 | "requires": { 186 | "@graphql-tools/merge": "^8.2.3", 187 | "@graphql-tools/utils": "^8.6.2", 188 | "tslib": "~2.3.0", 189 | "value-or-promise": "1.0.11" 190 | } 191 | }, 192 | "@graphql-tools/utils": { 193 | "version": "8.6.2", 194 | "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.2.tgz", 195 | "integrity": "sha512-x1DG0cJgpJtImUlNE780B/dfp8pxvVxOD6UeykFH5rHes26S4kGokbgU8F1IgrJ1vAPm/OVBHtd2kicTsPfwdA==", 196 | "requires": { 197 | "tslib": "~2.3.0" 198 | } 199 | }, 200 | "bytes": { 201 | "version": "3.1.2", 202 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 203 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" 204 | }, 205 | "depd": { 206 | "version": "2.0.0", 207 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 208 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 209 | }, 210 | "graphql": { 211 | "version": "16.3.0", 212 | "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.3.0.tgz", 213 | "integrity": "sha512-xm+ANmA16BzCT5pLjuXySbQVFwH3oJctUVdy81w1sV0vBU0KgDdBGtxQOUd5zqOBk/JayAFeG8Dlmeq74rjm/A==" 214 | }, 215 | "http-errors": { 216 | "version": "2.0.0", 217 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 218 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 219 | "requires": { 220 | "depd": "2.0.0", 221 | "inherits": "2.0.4", 222 | "setprototypeof": "1.2.0", 223 | "statuses": "2.0.1", 224 | "toidentifier": "1.0.1" 225 | } 226 | }, 227 | "iconv-lite": { 228 | "version": "0.4.24", 229 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 230 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 231 | "requires": { 232 | "safer-buffer": ">= 2.1.2 < 3" 233 | } 234 | }, 235 | "inherits": { 236 | "version": "2.0.4", 237 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 238 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 239 | }, 240 | "raw-body": { 241 | "version": "2.5.0", 242 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.0.tgz", 243 | "integrity": "sha512-XpyZ6O7PVu3ItMQl0LslfsRoKxMOxi3SzDkrOtxMES5AqLFpYjQCryxI4LGygUN2jL+RgFsPkMPPlG7cg/47+A==", 244 | "requires": { 245 | "bytes": "3.1.2", 246 | "http-errors": "2.0.0", 247 | "iconv-lite": "0.4.24", 248 | "unpipe": "1.0.0" 249 | } 250 | }, 251 | "safer-buffer": { 252 | "version": "2.1.2", 253 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 254 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 255 | }, 256 | "setprototypeof": { 257 | "version": "1.2.0", 258 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 259 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 260 | }, 261 | "statuses": { 262 | "version": "2.0.1", 263 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 264 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 265 | }, 266 | "toidentifier": { 267 | "version": "1.0.1", 268 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 269 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 270 | }, 271 | "tslib": { 272 | "version": "2.3.1", 273 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", 274 | "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" 275 | }, 276 | "unpipe": { 277 | "version": "1.0.0", 278 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 279 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 280 | }, 281 | "value-or-promise": { 282 | "version": "1.0.11", 283 | "resolved": "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz", 284 | "integrity": "sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg==" 285 | } 286 | } 287 | } 288 | --------------------------------------------------------------------------------