上一篇Vite入门从手写一个乞丐版的Vite开始(上)我们已经成功的将页面渲染出来了,这一篇我们来简单的实现一下热更新的功能。

所谓热更新就是修改了文件,不用刷新页面,页面的某个部分就自动更新了,听着似乎挺简单的,但是要实现一个很完善的热更新还是很复杂的,要考虑的情况很多,所以本文只会实现一个最基础的热更新效果。

创建WebSocket连接

浏览器显然是不知道文件有没有修改的,所以需要后端进行推送,我们先来建立一个WebSocket连接。

// app.js
const server = http.createServer(app);
const WebSocket = require("ws");// 创建WebSocket服务
const createWebSocket = () => {// 创建一个服务实例const wss = new WebSocket.Server({ noServer: true });// 不用额外创建http服务,直接使用我们自己创建的http服务// 接收到http的协议升级请求server.on("upgrade", (req, socket, head) => {// 当子协议为vite-hmr时就处理http的升级请求if (req.headers["sec-websocket-protocol"] === "vite-hmr") {wss.handleUpgrade(req, socket, head, (ws) => {wss.emit("connection", ws, req);});}});// 连接成功wss.on("connection", (socket) => {socket.send(JSON.stringify({ type: "connected" }));});// 发送消息方法const sendMsg = (payload) => {const stringified = JSON.stringify(payload, null, 2);wss.clients.forEach((client) => {if (client.readyState === WebSocket.OPEN) {client.send(stringified);}});};return {wss,sendMsg,};
};
const { wss, sendMsg } = createWebSocket();server.listen(3000);

WebSocket和我们的服务共用一个http请求,当接收到http协议的升级请求后,判断子协议是否是vite-hmr,是的话我们就把创建的WebSocket实例连接上去,这个子协议是自己定义的,通过设置子协议,单个服务器可以实现多个WebSocket 连接,就可以根据不同的协议处理不同类型的事情,服务端的WebSocket创建完成以后,客户端也需要创建,但是客户端是不会有这些代码的,所以需要我们手动注入,创建一个文件client.js

// client.js// vite-hmr代表自定义的协议字符串
const socket = new WebSocket("ws://localhost:3000/", "vite-hmr");socket.addEventListener("message", async ({ data }) => {const payload = JSON.parse(data);
});

接下来我们把这个client.js注入到html文件,修改之前html文件拦截的逻辑:

// app.js
const clientPublicPath = "/client.js";app.use(async function (req, res, next) {// 提供html页面if (req.url === "/index.html") {let html = readFile("index.html");const devInjectionCode = `\n<script type="module">import "${clientPublicPath}"</script>\n`;html = html.replace(/<head>/, `$&${devInjectionCode}`);send(res, html, "html");}
})

通过import的方式引入,所以我们需要拦截一下这个请求:

// app.js
app.use(async function (req, res, next) {if (req.url === clientPublicPath) {// 提供client.jslet js = fs.readFileSync(path.join(__dirname, "./client.js"), "utf-8");send(res, js, "js");}
})

可以看到已经连接成功。

监听文件改变

接下来我们要初始化一下对文件修改的监听,监听文件的改变使用chokidar:

// app.js
const chokidar = require(chokidar);// 创建文件监听服务
const createFileWatcher = () => {const watcher = chokidar.watch(basePath, {ignored: [/node_modules/, /\.git/],awaitWriteFinish: {stabilityThreshold: 100,pollInterval: 10,},});return watcher;
};
const watcher = createFileWatcher();watcher.on("change", (file) => {// file文件修改了
})

构建导入依赖图

为什么要构建依赖图呢,很简单,比如一个模块改变了,仅仅更新它自己肯定还不够,依赖它的模块都需要修改才对,要做到这一点自然要能知道哪些模块依赖它才行。

// app.js
const importerMap = new Map();
const importeeMap = new Map();// map : key -> set
// map : 模块 -> 依赖该模块的模块集合
const ensureMapEntry = (map, key) => {let entry = map.get(key);if (!entry) {entry = new Set();map.set(key, entry);}return entry;
};

需要用到的变量和函数就是上面几个,importerMap用来存放模块依赖它的模块之间的映射;importeeMap用来存放模块该模块所依赖的模块的映射,主要作用是用来删除不再依赖的模块,比如a一开始依赖bc,此时importerMap里面存在b -> ac -> a的映射关系,然后我修改了一下a,删除了对c的依赖,那么就需要从importerMap里面也同时删除c -> a的映射关系,这时就可以通过importeeMap来获取到之前的a -> [b, c]的依赖关系,跟此次的依赖关系a -> [b]进行比对,就可以找出不再依赖的c模块,然后在importerMap里删除c -> a的依赖关系。

接下来我们从index.html页面开始构建依赖图,index.html内容如下:

可以看到它依赖了main.js,修改拦截html的方法:

// app.js
app.use(async function (req, res, next) {// 提供html页面if (req.url === "/index.html") {let html = readFile("index.html");// 查找模块依赖图const scriptRE = /(<script\b[^>]*>)([\s\S]*?)<\/script>/gm;const srcRE = /\bsrc=(?:"([^"]+)"|'([^']+)'|([^'"\s]+)\b)/;// 找出script标签html = html.replace(scriptRE, (matched, openTag) => {const srcAttr = openTag.match(srcRE);if (srcAttr) {// 创建script到html的依赖关系const importee = removeQuery(srcAttr[1] || srcAttr[2]);ensureMapEntry(importerMap, importee).add(removeQuery(req.url));}return matched;});// 注入client.js// ...}
})

接下来我们需要分别修改js的拦截方法,注册依赖关系;修改Vue单文件的拦截方法,注册js部分的依赖关系,因为上一篇文章里我们已经把转换裸导入的逻辑都提取成一个公共函数parseBareImport了,所以我们只要修改这个函数就可以了:

// 处理裸导入
// 增加了importer入参,req.url
const parseBareImport = async (js, importer) => {await init;let parseResult = parseEsModule(js);let s = new MagicString(js);importer = removeQuery(importer);// ++parseResult[0].forEach((item) => {let url = "";if (item.n[0] !== "." && item.n[0] !== "/") {url = `/@module/${item.n}?import`;} else {url = `${item.n}?import`;}s.overwrite(item.s, item.e, url);// 注册importer模块所以依赖的模块到它的映射关系ensureMapEntry(importerMap, removeQuery(url)).add(importer);// ++});return s.toString();
};

再来增加一下前面提到的去除不再依赖的关系的逻辑:

// 处理裸导入
const parseBareImport = async (js, importer) => {// ...importer = removeQuery(importer);// 上一次的依赖集合const prevImportees = importeeMap.get(importer);// ++// 这一次的依赖集合const currentImportees = new Set();// ++importeeMap.set(importer, currentImportees);// ++parseResult[0].forEach((item) => {// ...let importee = removeQuery(url);// ++// url -> 依赖currentImportees.add(importee);// ++// 依赖 -> urlensureMapEntry(importerMap, importee).add(importer);});// 删除不再依赖的关系++if (prevImportees) {prevImportees.forEach((importee) => {if (!currentImportees.has(importee)) {// importer不再依赖importee,所以要从importee的依赖集合中删除importerconst importers = importerMap.get(importee);if (importers) {importers.delete(importer);}}});}return s.toString();
};

Vue单文件的热更新

先来实现一下Vue单文件的热更新,先监听一下Vue单文件的改变事件:

// app.js
// 监听文件改变
watcher.on("change", (file) => {if (file.endsWith(".vue")) {handleVueReload(file);}
});

如果修改的文件是以.vue结尾,那么就进行处理,怎么处理呢,Vue单文件会解析成jstemplatestyle三部分,我们把解析数据缓存起来,当文件修改了以后会再次进行解析,然后分别和上一次的解析结果进行比较,判断单文件的哪部分发生变化了,最后给浏览器发送不同的事件,由前端页面来进行不同的处理,缓存我们使用lru-cache:

// app.js
const LRUCache = require("lru-cache");// 缓存Vue单文件的解析结果
const vueCache = new LRUCache({max: 65535,
});

然后修改一下Vue单文件的拦截方法,增加缓存:

// app.js
app.use(async function (req, res, next) {if (/\.vue\??[^.]*$/.test(req.url)) {// ...// vue单文件let descriptor = null;// 如果存在缓存则直接使用缓存let cached = vueCache.get(removeQuery(req.url));if (cached) {descriptor = cached;} else {// 否则进行解析,并且将解析结果进行缓存descriptor = parseVue(vue).descriptor;vueCache.set(removeQuery(req.url), descriptor);}// ...}
})

然后就来到handleVueReload方法了:

// 处理Vue单文件的热更新
const handleVueReload = (file) => {file = filePathToUrl(file);
};// 处理文件路径到url
const filePathToUrl = (file) => {return file.replace(/\\/g, "/").replace(/^\.\.\/test/g, "");
};

我们先转换了一下文件路径,因为监听到的是本地路径,和请求的url是不一样的:

const handleVueReload = (file) => {file = filePathToUrl(file);// 获取上一次的解析结果const prevDescriptor = vueCache.get(file);// 从缓存中删除上一次的解析结果vueCache.del(file);if (!prevDescriptor) {return;}// 解析let vue = readFile(file);descriptor = parseVue(vue).descriptor;vueCache.set(file, descriptor);
};

接着获取了一下缓存数据,然后进行了这一次的解析,并更新缓存,接下来就要判断哪一部分发生了改变。

热更新template

我们先来看一下比较简单的模板热更新:

const handleVueReload = (file) => {// ...// 检查哪部分发生了改变const sendRerender = () => {sendMsg({type: "vue-rerender",path: file,});};// template改变了发送rerender事件if (!isEqualBlock(descriptor.template, prevDescriptor.template)) {return sendRerender();}
}// 判断Vue单文件解析后的两个部分是否相同
function isEqualBlock(a, b) {if (!a && !b) return true;if (!a || !b) return false;if (a.src && b.src && a.src === b.src) return true;if (a.content !== b.content) return false;const keysA = Object.keys(a.attrs);const keysB = Object.keys(b.attrs);if (keysA.length !== keysB.length) {return false;}return keysA.every((key) => a.attrs[key] === b.attrs[key]);
}

逻辑很简单,当template部分发生改变后向浏览器发送一个rerender事件,带上修改模块的url

现在我们来修改一下HelloWorld.vuetemplate看看:

可以看到已经成功收到了消息。

接下来需要修改一下client.js文件,增加收到vue-rerender消息后的处理逻辑。

文件更新了,浏览器肯定需要请求一下更新的文件,Vite使用的是import()方法,但是这个方法js本身是没有的,另外笔者没有找到是哪里注入的,所以加载模块的逻辑只能自己来简单实现一下:

// client.js
// 回调id
let callbackId = 0;
// 记录回调
const callbackMap = new Map();
// 模块导入后调用的全局方法
window.onModuleCallback = (id, module) => {document.body.removeChild(document.getElementById("moduleLoad"));// 执行回调let callback = callbackMap.get(id);if (callback) {callback(module);}
};// 加载模块
const loadModule = ({ url, callback }) => {// 保存回调let id = callbackId++;callbackMap.set(id, callback);// 创建一个模块类型的scriptlet script = document.createElement("script");script.type = "module";script.id = "moduleLoad";script.innerHTML = `import * as module from '${url}'window.onModuleCallback(${id}, module)`;document.body.appendChild(script);
};

因为要加载的都是ES模块,直接请求是不行的,所以创建一个typemodulescript标签,来让浏览器加载,这样请求都不用自己发,只要把想办法获取到模块的导出就行了,这个也很简单,创建一个全局函数即可,这个很像jsonp的原理。

接下来就可以处理vue-rerender消息了:

// app.js
socket.addEventListener("message", async ({ data }) => {const payload = JSON.parse(data);handleMessage(payload);
});const handleMessage = (payload) => {switch (payload.type) {case "vue-rerender":loadModule({url: payload.path + "?type=template&t=" + Date.now(),callback: (module) => {window.__VUE_HMR_RUNTIME__.rerender(payload.path, module.render);},});break;}
};

就这么简单,我们来修改一下HelloWorld.vue文件的模板来看看:

可以看到没有刷新页面,但是更新了,接下来详细解释一下原理。

因为我们修改的是模板部分,所以请求的urlpayload.path + "?type=template,这个源于上一篇文章里我们请求Vue单文件的模板部分是这么设计的,为什么要加个时间戳呢,因为不加的话浏览器认为这个模块已经加载过了,是不会重新请求的。

模板部分的请求结果如下:

导出了一个render函数,这个其实就是HelloWorld.vue组件的渲染函数,所以我们通过module.render来获取这个函数。

__VUE_HMR_RUNTIME__.rerender这个函数是哪里来的呢,其实来自于VueVue非生产环境的源码会提供一个__VUE_HMR_RUNTIME__对象,顾名思义就是用于热更新的,有三个方法:

rerender就是其中一个:

function rerender(id, newRender) {const record = map.get(id);if (!record)return;Array.from(record).forEach(instance => {if (newRender) {instance.render = newRender;// 1}instance.renderCache = [];isHmrUpdating = true;instance.update();// 2isHmrUpdating = false;});
}

核心代码就是上面的1、2两行,直接用新的渲染函数覆盖组件旧的渲染函数,然后触发组件更新就达到了热更新的效果。

另外要解释一下其中涉及到的id,需要热更新的组件会被添加到map里,那怎么判断一个组件是不是需要热更新呢,也很简单,给它添加一个属性即可:

mountComponent方法里会判断组件是否存在__hmrId属性,存在则认为是需要进行热更新的,那么就添加到map里,注册方法如下:

这个__hmrId属性需要我们手动添加,所以需要修改一下之前拦截Vue单文件的方法:

// app.js
app.use(async function (req, res, next) {if (/\.vue\??[^.]*$/.test(req.url)) {// vue单文件// ...// 添加热更新标志code += `\n__script.__hmrId = ${JSON.stringify(removeQuery(req.url))}`;// ++// 导出code += `\nexport default __script`;// ...}
})

热更新js

趁热打铁,接下来看一下Vue单文件中的js部分发生了修改怎么进行热更新。

基本套路是一样的,检查两次的js部分是否发生了修改了,修改了则向浏览器发送热更新消息:

// app.js
const handleVueReload = (file) => {const sendReload = () => {sendMsg({type: "vue-reload",path: file,});};// js部分发生了改变发送reload事件if (!isEqualBlock(descriptor.script, prevDescriptor.script)) {return sendReload();}
}

js部分发生改变了就发送一个vue-reload消息,接下来修改client.js增加对这个消息的处理逻辑:

// client.js
const handleMessage = (payload) => {switch (payload.type) {case "vue-reload":loadModule({url: payload.path + "?t=" + Date.now(),callback: (module) => {window.__VUE_HMR_RUNTIME__.reload(payload.path, module.default);},});break;}
}

和模板热更新很类似,只不过是调用reload方法,这个方法会稍微复杂一点:

function reload(id, newComp) {const record = map.get(id);if (!record)return;Array.from(record).forEach(instance => {const comp = instance.type;if (!hmrDirtyComponents.has(comp)) {// 更新原组件extend(comp, newComp);for (const key in comp) {if (!(key in newComp)) {delete comp[key];}}// 标记为脏组件,在虚拟DOM树patch的时候会直接替换hmrDirtyComponents.add(comp);// 重新加载后取消标记组件queuePostFlushCb(() => {hmrDirtyComponents.delete(comp);});}if (instance.parent) {// 强制父实例重新渲染queueJob(instance.parent.update);}else if (instance.appContext.reload) {// 通过createApp()装载的根实例具有reload方法instance.appContext.reload();}else if (typeof window !== 'undefined') {window.location.reload();}});
}

通过注释应该能大概看出来它的原理,通过强制父实例重新渲染、调用根实例的reload方法、通过标记为脏组件等等方式来重新渲染组件达到更新的效果。

style热更新

样式更新的情况比较多,除了修改样式本身,还有作用域修改了、使用到了CSS变量等情况,简单起见,我们只考虑修改了样式本身。

根据上一篇的介绍,Vue单文件中的样式也是通过js类型发送到浏览器,然后动态创建style标签插入到页面,所以我们需要能删除之前添加的标签,这就需要给添加的style标签增加一个id了,修改一下上一篇文章里我们编写的insertStyle方法:

// app.js
// css to js
const cssToJs = (css, id) => {return `const insertStyle = (css) => {// 删除之前的标签++if ('${id}') {let oldEl = document.getElementById('${id}')if (oldEl) document.head.removeChild(oldEl)}let el = document.createElement('style')el.setAttribute('type', 'text/css')el.id = '${id}' // ++el.innerHTML = cssdocument.head.appendChild(el)}insertStyle(\`${css}\`)export default insertStyle`;
};

style标签增加一个id,然后添加之前先删除之前的标签,接下来需要分别修改一下css的拦截逻辑增加removeQuery(req.url)作为id;以及Vue单文件的style部分的拦截请求,增加removeQuery(req.url) + '-' + index作为id,要加上index是因为一个Vue单文件里可能有多个style标签。

接下来继续修改handleVueReload方法:

// app.js
const handleVueReload = (file) => {// ...// style部分发生了改变const prevStyles = prevDescriptor.styles || []const nextStyles = descriptor.styles || []nextStyles.forEach((_, i) => {if (!prevStyles[i] || !isEqualBlock(prevStyles[i], nextStyles[i])) {sendMsg({type: 'style-update',path: `${file}?import&type=style&index=${i}`,})}})
}

遍历新的样式数据,根据之前的进行对比,如果某个样式块之前没有或者不一样那就发送style-update事件,注意url需要带上importtype=style参数,这是上一篇里我们规定的。

client.js也要配套修改一下:

// client.js
const handleMessage = (payload) => {switch (payload.type) {case "style-update":loadModule({url: payload.path + "&t=" + Date.now(),});break; }
}

很简单,加上时间戳重新加载一下样式文件即可。

不过还有个小问题,比如原来有两个style块,我们删掉了一个,目前页面上还是存在的,比如一开始存在两个style块:

删掉第二个style块,也就是设置背景颜色的那个:

可以看到还是存在,我们是通过索引来添加的,所以更新后有多少个样式块,就会从头覆盖之前已经存在的多少个样式块,最后多出来的是不会被删除的,所以需要手动删除不再需要的标签:

// app.js
const handleVueReload = (file) => {// ...// 删除已经被删掉的样式块prevStyles.slice(nextStyles.length).forEach((_, i) => {sendMsg({type: 'style-remove',path: file,id: `${file}-${i + nextStyles.length}`})})
}

发送一个style-remove事件,通知页面删除不再需要的标签:

// client.js
const handleMessage = (payload) => {switch (payload.type) {case "style-remove":document.head.removeChild(document.getElementById(payload.id));break;}
}

可以看到被成功删掉了。

普通js文件的热更新

最后我们来看一下非Vue单文件,普通js文件更新后要怎么处理。

增加一个处理js热更新的函数:

// app.js
// 监听文件改变
watcher.on("change", (file) => {if (file.endsWith(".vue")) {handleVueReload(file);} else if (file.endsWith(".js")) {// ++handleJsReload(file);// ++}
});

普通js热更新就需要用到前面的依赖图数据了,如果监听到某个js文件修改了,先判断它是否在依赖图中,不是的话就不用管,是的话就递归获取所有依赖它的模块,因为所有模块的最上层依赖肯定是index.html,如果只是简单的获取所有依赖模块再更新,那么每次都相当于要刷新整个页面了,所以我们规定如果检查到某个依赖是Vue单文件,那么就代表支持热更新,否则就相当于走到死胡同,需要刷新整个页面。

// 处理js文件的热更新
const handleJsReload = (file) => {file = filePathToUrl(file);// 因为构建依赖图的时候有些是以相对路径引用的,而监听获取到的都是绝对路径,所以稍微兼容一下let importers = getImporters(file);// 遍历直接依赖if (importers && importers.size > 0) {// 需要进行热更新的模块const hmrBoundaries = new Set();// 递归依赖图获取要更新的模块const hasDeadEnd = walkImportChain(importers, hmrBoundaries);const boundaries = [...hmrBoundaries];// 无法热更新,刷新整个页面if (hasDeadEnd) {sendMsg({type: "full-reload",});} else {// 可以热更新sendMsg({type: "multi",// 可能有多个模块,所以发送一个multi类型的消息updates: boundaries.map((boundary) => {return {type: "vue-reload",path: boundary,};}),});}}
};// 获取模块的直接依赖模块
const getImporters = (file) => {let importers = importerMap.get(file);if (!importers || importers.size <= 0) {importers = importerMap.get("." + file);}return importers;
};

递归获取修改的js文件的依赖模块,判断是否支持热更新,支持则发送热更新事件,否则发送刷新整个页面事件,因为可能同时要更新多个模块,所以通过type=multi来标识。

看一下递归的方法walkImportChain

// 递归遍历依赖图
const walkImportChain = (importers, hmrBoundaries, currentChain = []) => {for (const importer of importers) {if (importer.endsWith(".vue")) {// 依赖是Vue单文件那么支持热更新,添加到热更新模块集合里hmrBoundaries.add(importer);} else {// 获取依赖模块的再上层用来模块let parentImpoters = getImporters(importer);if (!parentImpoters || parentImpoters.size <= 0) {// 如果没有上层依赖了,那么代表走到死胡同了return true;} else if (!currentChain.includes(importer)) {// 通过currentChain来存储已经遍历过的模块// 递归再上层的依赖if (walkImportChain(parentImpoters,hmrBoundaries,currentChain.concat(importer))) {return true;}}}}return false;
};

逻辑很简单,就是递归遇到Vue单文件就停止,否则继续遍历,直到顶端,代表走到死胡同。

最后再来修改一下client.js

// client.js
socket.addEventListener("message", async ({ data }) => {const payload = JSON.parse(data);// 同时需要更新多个模块if (payload.type === "multi") {// ++payload.updates.forEach(handleMessage);// ++} else {handleMessage(payload);}
});

如果消息类型是multi,那么就遍历updates列表依次调用处理方法:

// client.js
const handleMessage = (payload) => {switch (payload.type) {case "full-reload":location.reload();break;}
}

vue-rerender事件之前已经有了,所以只需要增加一个刷新整个页面的方法即可。

测试一下,App.vue里面引入一个test.js文件:

// App.vue
<script>
import test from "./test.js";export default {data() {return {text: "",};},mounted() {this.text = test();},
};
</script><template><div><p>{{ text }}</p></div>
</template>

test.js又引入了test2.js

// test.js
import test2 from "./test2.js";export default function () {let a = test2();let b = "我是测试1";return a + " --- " + b;
}// test2.js
export default function () {return '我是测试2'
}

接下来修改test2.js测试效果:

可以看到重新发送了请求,但是页面并没有更新,这是为什么呢,其实还是缓存问题:

App.vue导入的两个文件之前已经请求过了,所以浏览器会直接使用之前请求的结果,并不会重新发送请求,这要怎么解决呢,很简单,可以看到请求的App.vueurl是带了时间戳的,所以我们可以检查请求模块的url是否存在时间戳,存在则把它依赖的所有模块路径也都带上时间戳,这样就会触发重新请求了,修改一下模块路径转换方法parseBareImport

// app.js
// 处理裸导入
const parseBareImport = async (js, importer) => {// ...// 检查模块url是否存在时间戳let hast = checkQueryExist(importer, "t");// ++// ...parseResult[0].forEach((item) => {let url = "";if (item.n[0] !== "." && item.n[0] !== "/") {url = `/@module/${item.n}?import${hast ? "&t=" + Date.now() : ""}`;// ++} else {url = `${item.n}?import${hast ? "&t=" + Date.now() : ""}`;// ++}// ...})// ...
}

再来测试一下:

可以看到成功更新了。最后我们再来测试运行刷新整个页面的情况,修改一下main.js文件即可:

总结

本文参考Vite-1.0.0-rc.5版本写了一个非常简单的Vite,简化了非常多的细节,旨在对Vite及热更新有一个基础的认识,其中肯定有不合理或错误之处,欢迎指出~

示例代码在:https://github.com/wanglin2/vite-demo。

Vite入门从手写一个乞丐版的Vite开始(下)相关推荐

  1. 肝一波 ~ 手写一个简易版的Mybatis,带你深入领略它的魅力!

    零.准备工作 <dependencies><dependency><groupId>mysql</groupId><artifactId>m ...

  2. 如何手写一个迷你版的RPC

    点击上方"Java后端技术栈"关注 持续推送技术干货 前言 在实际后台服务开发中,比如订单服务(开发者A负责)需要调用商品服务(开发者B负责),那么开发者B会和A约定调用API,以 ...

  3. 手写一个迷你版Spring MVC框架

    前期准备 我这里要写的是一个迷你版的Spring MVC,我将在一个干净的web工程开始开发,不引入Spring,完全通过JDK来实现. 我们先来看一眼工程: 工程代码结构 第一:在annotatio ...

  4. 手写一个迷你版的 Tomcat 猫

    点击上方"芋道源码",选择"设为星标" 管她前浪,还是后浪? 能浪的浪,才是好浪! 每天 8:55 更新文章,每天掉亿点点头发... 源码精品专栏 原创 | J ...

  5. 手写一个迷你版的 Tomcat 喵【转】

    原文地址:https://blog.csdn.net/FYGu18/article/details/82921115 前言 Tomcat,这只3脚猫,大学的时候就认识了,直到现在工作中,也常会和它打交 ...

  6. 极光im java_java手写一个迷你版的Tomcat代码分享

    前言 Tomcat,这只3脚猫,大学的时候就认识了,直到现在工作中,也常会和它打交道.这是一只神奇的猫,今天让我来抽象你,实现你! Write MyTomcat Tomcat是非常流行的Web Ser ...

  7. 手写一个迷你版的 Tomcat 喵

    前言 Write MyTomcat 2.1 MyRequest 2.2 MyResponse 2.3 MyServlet 2.4 ServletMapping 和 ServletMappingConf ...

  8. 手写一个简版的vue

    Vue源码中实现依赖收集(观察者模式),实现了三个类: Dep:扮演观察目标的角色,每一个数据都会有Dep类实例,它内部有个subs队列,subs就是subscribers的意思,保存着依赖本数据的观 ...

  9. 手写一个山寨版的springmvc框架

    文章目录 一,环境准备 二,项目结构搭建 三,简易版的前端控制器 DnDispatcherServlet 四,测试springmvc的性能 五,结束语 首先贴出来一张从网上copy下来的 spring ...

  10. 手写一个简易版本的RPC

    前言 在1024程序员节前夕,学习技术是对节日最好的庆祝. 手写一个简易版的RPC,可以把模糊抽象的概念具像化,属于落地层面了. 1. RPC基本原理 RPC原理 2. 四个版本的迭代 注:api表示 ...

最新文章

  1. 使用C#的HttpWebRequest模拟登陆网站
  2. 全球及中国多非利特行业规模预测与前景运营模式分析报告2022-2027年版
  3. 【C++】运算符重载 Operator Overload
  4. Linux基础优化方法(四)———远程连接缓慢优化
  5. 《Unity着色器和屏幕特效开发秘笈(原书第2版)》一2.9 打包和混合纹理
  6. idea 调试技巧1
  7. 【Project3】技术总结
  8. Ubuntu 16.04 + Nginx + Django 项目部署
  9. 自动初始化 git Bash脚本
  10. 49 - 算法 - 二叉树 - leetcode108.-将有序数组转换为二叉搜索树-中序遍历 - vector
  11. V-rep学习笔记:ROSInterface
  12. Selenium实现网页自动签到的Python脚本-win11
  13. App性能优化:内存优化
  14. 灵敏度分享码显示服务器不可用,和平精英ss12最稳灵敏度设置方法介绍-2021灵敏度分享码...
  15. 开关电源共模电感和X电容的选取?
  16. 推荐8款免费企业邮箱,让你的工作便捷起来
  17. chrome 打印布局_Chrome打印网页中的宽度控制
  18. R语言入门——工作空间管理函数
  19. 方差、标准差和均方根误差的区别总结
  20. CouchDB的简单使用

热门文章

  1. ansys怎么删除线段_如何彻底删除ansys
  2. Oblog的一些整理
  3. elvui显示服务器,魔兽世界怀旧服ElvUI插件
  4. 企业微信hook接口,pc企业微信协议
  5. express 验证码功能
  6. golang学习之五:error、painc、recover
  7. YARN ACCEPTED: waiting for AM container to be allocated, launched and register with RM.
  8. 软件需求工程2018期末题
  9. 需求分析——需求具备哪些特征
  10. 【BZOJ3162】独钓寒江雪 树同构+DP